mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-03-17 04:13:17 +08:00
♻️ 卡池&活动组件重构
This commit is contained in:
@@ -1,372 +1,44 @@
|
||||
<template>
|
||||
<THomeCard :append="hasNew">
|
||||
<THomeCard :append="false">
|
||||
<template #title>限时祈愿</template>
|
||||
<template #title-append>
|
||||
<v-switch class="pool-switch" @change="showNew = !showNew" />
|
||||
<span>{{ showNew ? "查看当前祈愿" : "查看后续祈愿" }}</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<!-- todo 组件化 -->
|
||||
<div class="pool-grid">
|
||||
<div v-for="pool in poolSelect" :key="pool.postId" class="pool-card">
|
||||
<div class="pool-cover" @click="createPost(pool.postId, pool.title)">
|
||||
<TMiImg :src="pool.cover" alt="cover" :ori="true" />
|
||||
</div>
|
||||
<div class="pool-bottom">
|
||||
<div class="pool-character">
|
||||
<div class="pool-icons">
|
||||
<div
|
||||
v-for="character in pool.characters"
|
||||
:key="character.url"
|
||||
class="pool-icon"
|
||||
@click="toOuter(character, pool.title)"
|
||||
>
|
||||
<TItembox
|
||||
:title="character.info.name"
|
||||
v-if="character.info"
|
||||
:model-value="getCBox(character.info)"
|
||||
/>
|
||||
<img v-else :src="character.icon" alt="character" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pool-time">
|
||||
<div class="left">
|
||||
<v-icon>mdi-calendar-clock</v-icon>
|
||||
<span>{{ pool.time.str }}</span>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="
|
||||
pool.stat !== 'now' ? 100 : (pool.timeRest * 100) / pool.time.totalStamp
|
||||
"
|
||||
:rounded="true"
|
||||
/>
|
||||
<div v-if="pool.stat !== 'now'" class="time">
|
||||
{{ pool.stat === "future" ? "未开始" : "已结束" }}
|
||||
</div>
|
||||
<div v-else class="time">
|
||||
<span>剩余时间:</span>
|
||||
<span>{{ stamp2LastTime(pool.timeRest) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PhPoolCard v-for="(pool, idx) in pools" :key="idx" :pool="pool" />
|
||||
</div>
|
||||
</template>
|
||||
</THomeCard>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import TItembox, { type TItemBoxData } from "@comp/app/t-itemBox.vue";
|
||||
import TMiImg from "@comp/app/t-mi-img.vue";
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import PhPoolCard from "@comp/pageHome/ph-pool-card.vue";
|
||||
import { onMounted, shallowRef } from "vue";
|
||||
|
||||
import THomeCard from "./ph-comp-card.vue";
|
||||
|
||||
import { AppCharacterData } from "@/data/index.js";
|
||||
import { useHomeStore } from "@/store/modules/home.js";
|
||||
import { createPost, createTGWindow } from "@/utils/TGWindow.js";
|
||||
import { stamp2LastTime } from "@/utils/toolFunc.js";
|
||||
import postReq from "@/web/request/postReq.js";
|
||||
import TGLogger from "@/utils/TGLogger.js";
|
||||
import takumiReq from "@/web/request/takumiReq.js";
|
||||
|
||||
type TPoolEmits = (e: "success") => void;
|
||||
type PoolStat = "future" | "now" | "past"; // 未开始 | 进行中 | 已结束
|
||||
type AvatarRender = {
|
||||
icon: string;
|
||||
url: string;
|
||||
info?: TGApp.App.Character.WikiBriefInfo;
|
||||
};
|
||||
type PoolCard = {
|
||||
id: number;
|
||||
title: string;
|
||||
cover: string;
|
||||
postId: number;
|
||||
characters: Array<AvatarRender>;
|
||||
time: {
|
||||
str: string;
|
||||
startStamp: number;
|
||||
endStamp: number;
|
||||
totalStamp: number;
|
||||
};
|
||||
timeRest: number;
|
||||
stat: PoolStat;
|
||||
};
|
||||
|
||||
const emits = defineEmits<TPoolEmits>();
|
||||
const { poolCover } = storeToRefs(useHomeStore());
|
||||
const router = useRouter();
|
||||
// eslint-disable-next-line no-undef
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const showNew = ref<boolean>(false);
|
||||
const poolCards = ref<Array<PoolCard>>([]);
|
||||
const hasNew = computed<boolean>(
|
||||
() => poolCards.value.find((pool) => pool.stat === "future") !== undefined,
|
||||
);
|
||||
const poolSelect = computed<Array<PoolCard>>(() => {
|
||||
if (!hasNew.value) return poolCards.value;
|
||||
if (showNew.value) return poolCards.value.filter((pool) => pool.stat === "future");
|
||||
return poolCards.value.filter((pool) => pool.stat !== "future");
|
||||
});
|
||||
const pools = shallowRef<Array<TGApp.BBS.Obc.GachaItem>>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const gachaData = await takumiReq.obc.gacha();
|
||||
if (checkCover(gachaData)) {
|
||||
poolCards.value = await getGachaCard(gachaData, poolCover.value);
|
||||
} else {
|
||||
poolCards.value = await getGachaCard(gachaData);
|
||||
const coverData: Record<string, string> = {};
|
||||
for (const pool of poolCards.value) coverData[pool.id] = pool.cover;
|
||||
poolCover.value = coverData;
|
||||
const resp = await takumiReq.obc.gacha();
|
||||
if (Array.isArray(resp)) pools.value = resp;
|
||||
else {
|
||||
showSnackbar.error(`获取限时祈愿失败:[${resp.retcode}-${resp.message}`);
|
||||
await TGLogger.Error(`获取限时祈愿失败:[${resp.retcode}-${resp.message}`);
|
||||
}
|
||||
if (timer !== null) clearInterval(timer);
|
||||
timer = setInterval(poolTimeout, 1000);
|
||||
emits("success");
|
||||
});
|
||||
|
||||
async function getGachaItemCard(
|
||||
data: TGApp.BBS.Obc.GachaItem,
|
||||
poolCover?: string,
|
||||
): Promise<PoolCard | null> {
|
||||
let cover = "/source/UI/empty.webp";
|
||||
const postId: number | undefined = Number(data.activity_url.split("/").pop()) || undefined;
|
||||
if (postId === undefined || isNaN(postId)) return null;
|
||||
if (poolCover !== undefined) {
|
||||
cover = poolCover;
|
||||
} else {
|
||||
const postResp = await postReq.post(postId);
|
||||
if ("retcode" in postResp) {
|
||||
showSnackbar.error(`[${postResp.retcode}] ${postResp.message}`);
|
||||
return null;
|
||||
}
|
||||
cover = postResp.cover?.url ?? postResp.post.images[0];
|
||||
}
|
||||
const timeStr = `${data.start_time} ~ ${data.end_time}`;
|
||||
const characters: Array<AvatarRender> = [];
|
||||
for (const character of data.pool) {
|
||||
const item: AvatarRender = { icon: character.icon, url: character.url };
|
||||
const contentId = character.url.match(/(?<=content\/)\d+/)?.[0];
|
||||
if (contentId) {
|
||||
const itemF = AppCharacterData.find((item) => item.contentId.toString() === contentId);
|
||||
if (itemF) item.info = itemF;
|
||||
}
|
||||
characters.push(item);
|
||||
}
|
||||
const endTs = new Date(data.end_time).getTime();
|
||||
const totalTs = endTs - new Date(data.start_time).getTime();
|
||||
const restTs = endTs - Date.now();
|
||||
return {
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
cover,
|
||||
postId,
|
||||
characters,
|
||||
time: {
|
||||
str: timeStr,
|
||||
startStamp: new Date(data.start_time).getTime(),
|
||||
endStamp: endTs,
|
||||
totalStamp: totalTs,
|
||||
},
|
||||
timeRest: restTs,
|
||||
stat: restTs > totalTs ? "future" : restTs > 0 ? "now" : "past",
|
||||
};
|
||||
}
|
||||
|
||||
async function getGachaCard(
|
||||
gachaData: Array<TGApp.BBS.Obc.GachaItem>,
|
||||
poolCover?: Record<number, string>,
|
||||
): Promise<Array<PoolCard>> {
|
||||
const gachaCard: Array<PoolCard> = [];
|
||||
for (const data of gachaData) {
|
||||
const item = await getGachaItemCard(data, poolCover?.[Number(data.id)]);
|
||||
if (item !== null) gachaCard.push(item);
|
||||
}
|
||||
return gachaCard;
|
||||
}
|
||||
|
||||
function poolTimeout(): void {
|
||||
for (const pool of poolCards.value) {
|
||||
if (pool.stat === "past") {
|
||||
if (pool.timeRest !== -1) pool.timeRest = -1;
|
||||
continue;
|
||||
}
|
||||
const timeRest = pool.time.endStamp - Date.now();
|
||||
if (timeRest >= pool.time.totalStamp) {
|
||||
pool.stat = "future";
|
||||
pool.timeRest = timeRest;
|
||||
continue;
|
||||
}
|
||||
if (timeRest <= 0) {
|
||||
pool.stat = "past";
|
||||
pool.timeRest = -1;
|
||||
continue;
|
||||
}
|
||||
pool.stat = "now";
|
||||
pool.timeRest = timeRest;
|
||||
}
|
||||
}
|
||||
|
||||
function checkCover(data: Array<TGApp.BBS.Obc.GachaItem>): boolean {
|
||||
if (poolCover.value === undefined || Object.keys(poolCover.value).length === 0) return false;
|
||||
let checkList = data.length;
|
||||
Object.entries(poolCover.value).forEach(([key, value]: [string, unknown]) => {
|
||||
const pool = data.find((item) => item.id.toString() === key);
|
||||
if (pool && value !== "/source/UI/empty.webp") checkList--;
|
||||
});
|
||||
return checkList === 0;
|
||||
}
|
||||
|
||||
async function toOuter(character: AvatarRender, title: string): Promise<void> {
|
||||
if (character.info !== undefined) {
|
||||
await router.push({ name: "角色图鉴", params: { id: character.info.id } });
|
||||
return;
|
||||
}
|
||||
const url = character.url;
|
||||
if (url === "") {
|
||||
showSnackbar.warn("链接为空!");
|
||||
return;
|
||||
}
|
||||
await createTGWindow(url, "Sub_window", `Pool_${title}`, 1200, 800, true, true);
|
||||
}
|
||||
|
||||
function getCBox(info: TGApp.App.Character.WikiBriefInfo): TItemBoxData {
|
||||
return {
|
||||
bg: `/icon/bg/${info.star}-Star.webp`,
|
||||
icon: `/WIKI/character/${info.id}.webp`,
|
||||
size: "60px",
|
||||
height: "60px",
|
||||
display: "inner",
|
||||
clickable: true,
|
||||
lt: `/icon/element/${info.element}元素.webp`,
|
||||
ltSize: "15px",
|
||||
innerHeight: 20,
|
||||
innerIcon: `/icon/weapon/${info.weapon}.webp`,
|
||||
innerText: info.name,
|
||||
};
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer !== null) clearInterval(timer);
|
||||
timer = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.pool-switch {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-grid {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.pool-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
aspect-ratio: 69 / 32;
|
||||
box-shadow: 2px 2px 5px var(--common-shadow-2);
|
||||
}
|
||||
|
||||
.pool-cover {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.pool-cover img {
|
||||
width: 100%;
|
||||
border-radius: 5px;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.pool-cover :hover {
|
||||
cursor: pointer;
|
||||
transform: scale(1.1);
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.pool-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.pool-character {
|
||||
display: flex;
|
||||
overflow: hidden auto;
|
||||
width: auto;
|
||||
max-width: 280px;
|
||||
height: 60px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.pool-character::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
background: var(--common-shadow-t-4);
|
||||
}
|
||||
|
||||
.pool-icons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pool-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
transition: all ease-in-out 0.3s;
|
||||
}
|
||||
|
||||
.pool-icon:hover {
|
||||
transform: scale(0.95);
|
||||
transition: all ease-in-out 0.3s;
|
||||
}
|
||||
|
||||
.pool-icon img {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pool-time {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
margin-right: 10px;
|
||||
color: var(--tgc-white-1);
|
||||
font-size: 12px;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(2, 0.5fr);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,94 +3,34 @@
|
||||
<template #title>近期活动</template>
|
||||
<template #default>
|
||||
<div class="position-grid">
|
||||
<PhPositionCard v-for="(card, index) in positionCards" :key="index" :position="card" />
|
||||
<PhPositionCard v-for="(card, index) in positions" :key="index" :pos="card" />
|
||||
</div>
|
||||
</template>
|
||||
</THomeCard>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import PhPositionCard from "@comp/pageHome/ph-position-card.vue";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { onMounted, shallowRef } from "vue";
|
||||
|
||||
import THomeCard from "./ph-comp-card.vue";
|
||||
|
||||
import TGLogger from "@/utils/TGLogger.js";
|
||||
import takumiReq from "@/web/request/takumiReq.js";
|
||||
|
||||
type TPositionEmits = (e: "success") => void;
|
||||
type PositionStat = "past" | "now" | "future" | "unknown"; // 已结束 | 进行中 | 未开始 | 未知
|
||||
export type PositionCard = {
|
||||
title: string;
|
||||
postId: number;
|
||||
link: string;
|
||||
icon: string;
|
||||
abstract: string;
|
||||
time: { startStamp: number; endStamp: number; totalStamp: number };
|
||||
timeRest: number;
|
||||
stat: PositionStat;
|
||||
};
|
||||
const emits = defineEmits<TPositionEmits>();
|
||||
// eslint-disable-next-line no-undef
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
const positionCards = ref<Array<PositionCard>>([]);
|
||||
const positions = shallowRef<Array<TGApp.BBS.Obc.PositionItem>>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const resp = await takumiReq.obc.position();
|
||||
console.log("positionCards", resp);
|
||||
positionCards.value = getPositionCard(resp);
|
||||
if (timer !== null) clearInterval(timer);
|
||||
timer = setInterval(getPositionTimer, 1000);
|
||||
if (Array.isArray(resp)) positions.value = resp;
|
||||
else {
|
||||
showSnackbar.error(`获取近期活动失败:[${resp.retcode}-${resp.message}`);
|
||||
await TGLogger.Error(`获取近期活动失败:[${resp.retcode}-${resp.message}`);
|
||||
}
|
||||
emits("success");
|
||||
});
|
||||
|
||||
function getPositionCard(list: Array<TGApp.BBS.Obc.PositionItem>): Array<PositionCard> {
|
||||
const res: Array<PositionCard> = [];
|
||||
for (const position of list) {
|
||||
let link = position.url;
|
||||
if (position.url === "" && position.content_id !== 0) {
|
||||
link = `https://bbs.mihoyo.com/ys/obc/content/${position.content_id}/detail?bbs_presentation_style=no_header`;
|
||||
}
|
||||
const startTs = new Date(position.create_time).getTime();
|
||||
const endTs = Number(position.end_time);
|
||||
const totalTs = endTs - startTs;
|
||||
const restTs = endTs === 0 || totalTs < 0 ? 0 : endTs - Date.now();
|
||||
const card: PositionCard = {
|
||||
title: position.title,
|
||||
postId: position.url !== "" ? Number(position.url.split("/").pop()) : position.content_id,
|
||||
link: link,
|
||||
icon: position.icon,
|
||||
abstract: position.abstract,
|
||||
time: { startStamp: startTs, endStamp: endTs, totalStamp: totalTs },
|
||||
timeRest: restTs,
|
||||
stat: restTs === 0 ? "unknown" : restTs > totalTs ? "future" : restTs > 0 ? "now" : "past",
|
||||
};
|
||||
res.push(card);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getPositionTimer(): void {
|
||||
for (const position of positionCards.value) {
|
||||
if (position.stat === "unknown") continue;
|
||||
if (position.stat === "past") {
|
||||
position.timeRest = 0;
|
||||
continue;
|
||||
}
|
||||
position.timeRest = position.time.endStamp - Date.now();
|
||||
if (position.timeRest <= 0) {
|
||||
position.stat = "past";
|
||||
position.timeRest = 0;
|
||||
} else if (position.timeRest > position.time.totalStamp) {
|
||||
position.stat = "future";
|
||||
} else {
|
||||
position.stat = "now";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer !== null) clearInterval(timer);
|
||||
timer = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
276
src/components/pageHome/ph-pool-card.vue
Normal file
276
src/components/pageHome/ph-pool-card.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="ph-pool-card">
|
||||
<div class="ph-pool-cover">
|
||||
<TMiImg v-if="cover" :src="cover" alt="cover" :ori="true" />
|
||||
<img src="/source/UI/empty.webp" class="empty" v-else alt="empty" />
|
||||
</div>
|
||||
<div class="ph-pool-bottom">
|
||||
<div class="ph-pool-avatars">
|
||||
<div
|
||||
v-for="avatar in avatars"
|
||||
:key="avatar.url"
|
||||
class="ph-pool-icon"
|
||||
@click="toAvatar(avatar)"
|
||||
>
|
||||
<TItemBox
|
||||
v-if="avatar.info"
|
||||
:title="avatar.info.name"
|
||||
:model-value="getBox(avatar.info)"
|
||||
/>
|
||||
<TMiImg v-else :src="avatar.icon" alt="icon" :ori="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="ph-pool-info">
|
||||
<div class="ph-pool-time">
|
||||
<v-icon>mdi-calendar-clock</v-icon>
|
||||
<span>{{ props.pool.start_time }} ~ {{ props.pool.end_time }}</span>
|
||||
</div>
|
||||
<v-progress-linear color="var(--tgc-od-green)" :model-value="percent" :rounded="true" />
|
||||
<div v-if="restTs > durationTs" class="ph-pool-stat">未开始</div>
|
||||
<div v-else-if="restTs > 0" class="ph-pool-stat">
|
||||
剩余时间:{{ stamp2LastTime(restTs) }}
|
||||
</div>
|
||||
<div v-else class="ph-pool-stat">已结束</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import TItemBox, { TItemBoxData } from "@comp/app/t-itemBox.vue";
|
||||
import TMiImg from "@comp/app/t-mi-img.vue";
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, onMounted, ref, shallowRef } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import { AppCharacterData } from "@/data/index.js";
|
||||
import { useHomeStore } from "@/store/modules/home.js";
|
||||
import TGLogger from "@/utils/TGLogger.js";
|
||||
import { createTGWindow } from "@/utils/TGWindow.js";
|
||||
import { stamp2LastTime } from "@/utils/toolFunc.js";
|
||||
import postReq from "@/web/request/postReq.js";
|
||||
|
||||
type PhPoolCardProps = { pool: TGApp.BBS.Obc.GachaItem };
|
||||
type PhPoolAvatar = TGApp.BBS.Obc.GachaPool & { info?: TGApp.App.Character.WikiBriefInfo };
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
const props = defineProps<PhPoolCardProps>();
|
||||
const router = useRouter();
|
||||
const { poolCover } = storeToRefs(useHomeStore());
|
||||
const cover = ref<string>();
|
||||
const endTs = ref<number>(0);
|
||||
const restTs = ref<number>(0);
|
||||
const durationTs = ref<number>(0);
|
||||
const avatars = shallowRef<Array<PhPoolAvatar>>([]);
|
||||
const percent = computed<number>(() => {
|
||||
if (restTs.value > durationTs.value) return 100;
|
||||
return (restTs.value * 100) / durationTs.value;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await loadCover();
|
||||
const avTmp: Array<PhPoolAvatar> = [];
|
||||
for (const av of props.pool.pool) {
|
||||
const contentId = av.url.match(/(?<=content\/)\d+/)?.[0];
|
||||
if (contentId) {
|
||||
const infoFind = AppCharacterData.find((a) => a.contentId.toString() === contentId);
|
||||
if (infoFind) {
|
||||
avTmp.push({ ...av, info: infoFind });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
avTmp.push(av);
|
||||
}
|
||||
avatars.value = avTmp;
|
||||
endTs.value = new Date(props.pool.end_time).getTime();
|
||||
restTs.value = endTs.value - Date.now();
|
||||
durationTs.value = endTs.value - new Date(props.pool.start_time).getTime();
|
||||
if (restTs.value > 0) {
|
||||
if (timer !== null) clearInterval(timer);
|
||||
timer = setInterval(handlePosition, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadCover(): Promise<void> {
|
||||
const postId: number | undefined = Number(props.pool.activity_url.split("/").pop()) || undefined;
|
||||
if (postId === undefined || isNaN(postId)) return;
|
||||
if (poolCover.value && poolCover.value[postId]) {
|
||||
cover.value = poolCover.value[postId];
|
||||
return;
|
||||
}
|
||||
const resp = await postReq.post(postId);
|
||||
if ("retcode" in resp) {
|
||||
showSnackbar.error(`[PhPoolCard][${resp.retcode}] ${resp.message}`);
|
||||
await TGLogger.Error(`[PhPoolCard][${resp.retcode}] ${resp.message}`);
|
||||
return;
|
||||
}
|
||||
cover.value = resp.post.cover;
|
||||
if (!poolCover.value) poolCover.value = { [postId]: resp.post.cover };
|
||||
else poolCover.value[postId] = resp.post.cover;
|
||||
}
|
||||
|
||||
function handlePosition(): void {
|
||||
if (restTs.value < 1) {
|
||||
if (timer !== null) clearInterval(timer);
|
||||
timer = null;
|
||||
restTs.value = 0;
|
||||
return;
|
||||
}
|
||||
restTs.value = endTs.value - Date.now();
|
||||
}
|
||||
|
||||
async function toAvatar(avatar: PhPoolAvatar): Promise<void> {
|
||||
if (avatar.info !== undefined) {
|
||||
await router.push({ name: "角色图鉴", params: { id: avatar.info.id } });
|
||||
return;
|
||||
}
|
||||
const url = avatar.url;
|
||||
if (url === "") {
|
||||
showSnackbar.warn("链接为空!");
|
||||
return;
|
||||
}
|
||||
await createTGWindow(url, "Sub_window", `Pool_${props.pool.title}`, 1200, 800, true, true);
|
||||
}
|
||||
|
||||
function getBox(info: TGApp.App.Character.WikiBriefInfo): TItemBoxData {
|
||||
return {
|
||||
bg: `/icon/bg/${info.star}-Star.webp`,
|
||||
icon: `/WIKI/character/${info.id}.webp`,
|
||||
size: "60px",
|
||||
height: "60px",
|
||||
display: "inner",
|
||||
clickable: true,
|
||||
lt: `/icon/element/${info.element}元素.webp`,
|
||||
ltSize: "15px",
|
||||
innerHeight: 20,
|
||||
innerIcon: `/icon/weapon/${info.weapon}.webp`,
|
||||
innerText: info.name,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ph-pool-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
aspect-ratio: 69 / 32;
|
||||
box-shadow: 2px 2px 4px var(--common-shadow-2);
|
||||
}
|
||||
|
||||
.ph-pool-cover {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
.empty {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
img {
|
||||
transform: scale(1.1);
|
||||
transition: all 0.5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ph-pool-bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.ph-pool-avatars {
|
||||
display: flex;
|
||||
overflow: hidden auto;
|
||||
width: auto;
|
||||
max-width: 280px;
|
||||
height: 60px;
|
||||
margin: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 8px;
|
||||
background: var(--common-shadow-t-4);
|
||||
}
|
||||
}
|
||||
|
||||
.ph-pool-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
transition: all ease-in-out 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(0.95);
|
||||
transition: all ease-in-out 0.3s;
|
||||
}
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ph-pool-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
margin-right: 10px;
|
||||
color: var(--tgc-white-1);
|
||||
font-size: 12px;
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ph-pool-time {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
column-gap: 4px;
|
||||
|
||||
:first-child {
|
||||
color: var(--tgc-od-orange);
|
||||
}
|
||||
}
|
||||
|
||||
.ph-pool-stat {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +1,31 @@
|
||||
<template>
|
||||
<div class="ph-pool-card">
|
||||
<div class="ph-position-card">
|
||||
<div class="top">
|
||||
<div class="main">
|
||||
<div class="left" @click="openPosition(props.position)">
|
||||
<TMiImg :ori="true" :src="props.position.icon" alt="icon" />
|
||||
<div class="left" @click="openPosition()">
|
||||
<TMiImg :ori="true" :src="props.pos.icon" alt="icon" />
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="title">{{ props.position.title }}</div>
|
||||
<div class="sub">{{ props.position.abstract }}</div>
|
||||
<div class="title">{{ props.pos.title }}</div>
|
||||
<div class="sub">{{ props.pos.abstract }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn class="btn" @click="openPosition(props.position)">查看</v-btn>
|
||||
<v-btn class="btn" @click="openPosition()">查看</v-btn>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="period">
|
||||
<v-icon>mdi-calendar-clock</v-icon>
|
||||
<span class="time">
|
||||
{{ timestampToDate(props.position.time.startStamp) }}
|
||||
{{ props.pos.create_time }}
|
||||
~
|
||||
{{
|
||||
props.position.time.endStamp === 0
|
||||
? "未知"
|
||||
: timestampToDate(props.position.time.endStamp)
|
||||
}}
|
||||
{{ endTs === 0 ? "未知" : timestampToDate(endTs) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rest">
|
||||
<v-icon>mdi-clock-outline</v-icon>
|
||||
<span v-if="props.position.stat === 'past'">已结束</span>
|
||||
<span v-else-if="props.position.stat === 'unknown'">未知</span>
|
||||
<span v-else-if="props.position.stat === 'now'">
|
||||
剩余时间:{{ stamp2LastTime(props.position.timeRest) }}
|
||||
</span>
|
||||
<span v-if="!hasEndTime">未知</span>
|
||||
<span v-else-if="restTs === 0">已结束</span>
|
||||
<span v-else-if="restTs < durationTs">剩余时间:{{ stamp2LastTime(restTs) }}</span>
|
||||
<span v-else>未开始</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,30 +34,68 @@
|
||||
<script lang="ts" setup>
|
||||
import TMiImg from "@comp/app/t-mi-img.vue";
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import type { PositionCard } from "@comp/pageHome/ph-comp-position.vue";
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
import { parseLink } from "@/utils/linkParser.js";
|
||||
import { createPost } from "@/utils/TGWindow.js";
|
||||
import { stamp2LastTime, timestampToDate } from "@/utils/toolFunc.js";
|
||||
|
||||
type PhPositionCardProps = { position: PositionCard };
|
||||
const props = defineProps<PhPositionCardProps>();
|
||||
type PhPositionCardProps = { pos: TGApp.BBS.Obc.PositionItem };
|
||||
|
||||
async function openPosition(card: PositionCard): Promise<void> {
|
||||
const res = await parseLink(card.link);
|
||||
// eslint-disable-next-line no-undef
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
const props = defineProps<PhPositionCardProps>();
|
||||
const endTs = ref<number>(0);
|
||||
const restTs = ref<number>(0);
|
||||
const durationTs = ref<number>(0);
|
||||
const hasEndTime = computed<boolean>(() => endTs.value !== 0);
|
||||
|
||||
onMounted(() => {
|
||||
endTs.value = Number(props.pos.end_time);
|
||||
restTs.value = endTs.value === 0 ? 0 : endTs.value - Date.now();
|
||||
if (restTs.value > 0) {
|
||||
durationTs.value = endTs.value - new Date(props.pos.create_time).getTime();
|
||||
}
|
||||
if (timer !== null) clearInterval(timer);
|
||||
timer = setInterval(handlePosition, 1000);
|
||||
});
|
||||
|
||||
function handlePosition(): void {
|
||||
if (restTs.value < 1) {
|
||||
if (timer !== null) clearInterval(timer);
|
||||
timer = null;
|
||||
restTs.value = 0;
|
||||
return;
|
||||
}
|
||||
restTs.value = endTs.value - Date.now();
|
||||
}
|
||||
|
||||
async function openPosition(): Promise<void> {
|
||||
if (props.pos.url === "" && props.pos.content_id !== 0) {
|
||||
window.open(
|
||||
`https://bbs.mihoyo.com/ys/obc/content/${props.pos.content_id}/detail?bbs_presentation_style=no_header`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const res = await parseLink(props.pos.url);
|
||||
if (res === "post") {
|
||||
await createPost(card.postId, card.title);
|
||||
const postId = Number(props.pos.url.split("/").pop());
|
||||
await createPost(postId, props.pos.title);
|
||||
return;
|
||||
}
|
||||
if (res === false) {
|
||||
showSnackbar.warn(`未知链接:${card.link}`, 3000);
|
||||
showSnackbar.warn(`未知链接:${props.pos.url}`, 3000);
|
||||
return;
|
||||
}
|
||||
window.open(card.link);
|
||||
window.open(props.pos.url);
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer !== null) clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.ph-pool-card {
|
||||
.ph-position-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
2
src/types/BBS/Obc.d.ts
vendored
2
src/types/BBS/Obc.d.ts
vendored
@@ -112,6 +112,7 @@ declare namespace TGApp.BBS.Obc {
|
||||
* @interface PositionItem
|
||||
* @property {number} recommend_id 推荐ID
|
||||
* @property {number} content_id 内容ID
|
||||
* @property {string} corner_mark 角标
|
||||
* @property {string} title 标题
|
||||
* @property {string} ext 扩展信息
|
||||
* @property {number} type 类型
|
||||
@@ -128,6 +129,7 @@ declare namespace TGApp.BBS.Obc {
|
||||
type PositionItem = {
|
||||
recommend_id: number;
|
||||
content_id: number;
|
||||
corner_mark: string;
|
||||
title: string;
|
||||
ext: string;
|
||||
type: number;
|
||||
|
||||
@@ -153,7 +153,9 @@ async function getUserGameRolesByCookie(
|
||||
* @since Beta v0.7.2
|
||||
* @return {Promise<Array<TGApp.BBS.Obc.GachaItem>>}
|
||||
*/
|
||||
async function getObcGachaPool(): Promise<Array<TGApp.BBS.Obc.GachaItem>> {
|
||||
async function getObcGachaPool(): Promise<
|
||||
Array<TGApp.BBS.Obc.GachaItem> | TGApp.BBS.Response.Base
|
||||
> {
|
||||
const resp = await TGHttp<TGApp.BBS.Obc.GachaResp>(
|
||||
`${taBu}common/blackboard/ys_obc/v1/gacha_pool`,
|
||||
{
|
||||
@@ -162,6 +164,7 @@ async function getObcGachaPool(): Promise<Array<TGApp.BBS.Obc.GachaItem>> {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data.list;
|
||||
}
|
||||
|
||||
@@ -187,7 +190,9 @@ function DfsObc(
|
||||
* @since Beta v0.7.2
|
||||
* @return {Promise<Array<TGApp.BBS.Obc.PositionItem>>}
|
||||
*/
|
||||
async function getObcHomePosition(): Promise<Array<TGApp.BBS.Obc.PositionItem>> {
|
||||
async function getObcHomePosition(): Promise<
|
||||
Array<TGApp.BBS.Obc.PositionItem> | TGApp.BBS.Response.Base
|
||||
> {
|
||||
const resp = await TGHttp<TGApp.BBS.Obc.PositionResp>(
|
||||
`${taBu}common/blackboard/ys_obc/v1/home/position`,
|
||||
{
|
||||
@@ -196,6 +201,7 @@ async function getObcHomePosition(): Promise<Array<TGApp.BBS.Obc.PositionItem>>
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
const data = resp.data.list;
|
||||
return DfsObc(data);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user