♻️ 卡池&活动组件重构

This commit is contained in:
目棃
2025-03-08 15:39:27 +08:00
parent fcb16c9299
commit 214dec29d8
6 changed files with 368 additions and 440 deletions

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}