♻️ 全面整理重构

This commit is contained in:
目棃
2024-12-13 16:15:01 +08:00
parent 86bfc80b57
commit 3ed6d503d4
229 changed files with 3105 additions and 4127 deletions

View File

@@ -28,17 +28,18 @@
</div>
</template>
<script lang="ts" setup>
import { onBeforeMount, ref } from "vue";
import TSAvatarBirth from "@Sqlite/modules/avatarBirth.js";
import { storeToRefs } from "pinia";
import { onBeforeMount, ref, shallowRef } from "vue";
import { useRouter } from "vue-router";
import TSAvatarBirth from "../../plugins/Sqlite/modules/avatarBirth.js";
import { useAppStore } from "../../store/modules/app.js";
import { useAppStore } from "@/store/modules/app.js";
const isBirthday = ref<boolean>(false);
const router = useRouter();
const cur = ref<TGApp.Archive.Birth.CalendarItem[]>([]);
const next = ref<TGApp.Archive.Birth.RoleItem[]>([]);
const appStore = useAppStore();
const { recentNewsType } = storeToRefs(useAppStore());
const isBirthday = ref<boolean>(false);
const cur = shallowRef<Array<TGApp.Archive.Birth.CalendarItem>>([]);
const next = shallowRef<Array<TGApp.Archive.Birth.RoleItem>>([]);
onBeforeMount(async () => {
const check = TSAvatarBirth.isAvatarBirth();
@@ -64,7 +65,7 @@ function toBirth(type: TGApp.Archive.Birth.RoleItem | true): void {
return;
}
if (cur.value.length > 0 && !cur.value[0].is_subscribe) {
appStore.recentNewsType = "news";
recentNewsType.value = "news";
router.push("/news/2/news");
return;
}

View File

@@ -1,21 +1,17 @@
<template>
<div class="tcm-box">
<div class="tcm-left">
<div class="tcm-bg"><img :src="props.item.bg" alt="bg" /></div>
<div class="tcm-icon"><img :src="props.item.icon" alt="icon" /></div>
<div class="tcm-star" v-if="props.item.star !== 0">
<img :src="props.item.starIcon" alt="element" />
<div class="tcm-bg"><img :src="item.bg" alt="bg" /></div>
<div class="tcm-icon"><img :src="item.icon" alt="icon" /></div>
<div class="tcm-star" v-if="item.star !== 0">
<img :src="item.starIcon" alt="element" />
</div>
</div>
<div class="tcm-right">{{ props.item.name }}</div>
<div class="tcm-right">{{ item.name }}</div>
</div>
</template>
<script lang="ts" setup>
interface TMiniWeaponProps {
item: TGApp.App.Calendar.Material;
}
const props = defineProps<TMiniWeaponProps>();
defineProps<{ item: TGApp.App.Calendar.Material }>();
</script>
<style lang="css" scoped>
.tcm-box {

View File

@@ -6,7 +6,7 @@
<TItemBox :model-value="boxData" />
<div class="toc-material-grid">
<TibCalendarMaterial
v-for="(item, index) in itemVal.materials"
v-for="(item, index) in props.dataVal.materials"
:key="index"
:item="item"
/>
@@ -16,28 +16,29 @@
<div class="toc-bottom">
<div class="toc-src-box">
<div class="toc-src-text">来源</div>
<img :src="`/icon/nation/${itemVal.source.area}.webp`" alt="icon" />
<div class="toc-src-text">{{ itemVal.source.area }} - {{ itemVal.source.name }}</div>
<img :src="`/icon/nation/${props.dataVal.source.area}.webp`" alt="icon" />
<div class="toc-src-text">
{{ props.dataVal.source.area }} - {{ props.dataVal.source.name }}
</div>
</div>
<v-btn variant="outlined" @click="toDetail(itemVal)">详情</v-btn>
<v-btn variant="outlined" @click="toDetail(props.dataVal)">详情</v-btn>
</div>
</div>
</div>
</TOverlay>
</template>
<script lang="ts" setup>
import TItemBox, { type TItemBoxData } from "@comp/app/t-itemBox.vue";
import TOverlay from "@comp/app/t-overlay.vue";
import showSnackbar from "@comp/func/snackbar.js";
import { computed } from "vue";
import { useRouter } from "vue-router";
import TItemBox, { TItemBoxData } from "../app/t-item-box.vue";
import TOverlay from "../app/t-overlay.vue";
import showSnackbar from "../func/snackbar.js";
import TibCalendarMaterial from "./ph-calendar-material.vue";
type ToCalendarProps = {
modelValue: boolean;
dataType: "weapon" | "avatar";
dataType: "weapon" | "character";
dataVal: TGApp.App.Calendar.Item;
};
type ToCalendarEmits = (e: "update:modelValue", v: boolean) => void;
@@ -49,22 +50,19 @@ const visible = computed<boolean>({
get: () => props.modelValue,
set: (v) => emits("update:modelValue", v),
});
const itemVal = computed<TGApp.App.Calendar.Item>(() => props.dataVal);
const boxData = computed<TItemBoxData>(() => {
return {
bg: itemVal.value.bg,
icon: itemVal.value.icon,
size: "100px",
height: "100px",
display: "inner",
clickable: false,
lt: props.dataType === "avatar" ? (itemVal.value.elementIcon ?? "") : itemVal.value.weaponIcon,
ltSize: "20px",
innerHeight: 25,
innerIcon: props.dataType === "avatar" ? itemVal.value.weaponIcon : undefined,
innerText: itemVal.value.name,
};
});
const boxData = computed<TItemBoxData>(() => ({
bg: props.dataVal.bg,
icon: props.dataVal.icon,
size: "100px",
height: "100px",
display: "inner",
clickable: false,
lt: props.dataType === "character" ? (props.dataVal.elementIcon ?? "") : props.dataVal.weaponIcon,
ltSize: "20px",
innerHeight: 25,
innerIcon: props.dataType === "character" ? props.dataVal.weaponIcon : undefined,
innerText: props.dataVal.name,
}));
async function toDetail(item: TGApp.App.Calendar.Item): Promise<void> {
if (!["character", "weapon"].includes(item.itemType)) {

View File

@@ -2,11 +2,8 @@
<THomeCard append>
<template #title>今日素材 {{ dateNow }}</template>
<template #title-append>
<v-switch
class="tc-switch"
@change="switchType = switchType === 'avatar' ? 'weapon' : 'avatar'"
/>
{{ switchType === "avatar" ? "角色" : "武器" }}
<v-switch class="tc-switch" @change="switchType()" />
<span>{{ selectedType === "character" ? "角色" : "武器" }}</span>
</template>
<template #default>
<div class="tc-top">
@@ -15,22 +12,9 @@
v-for="text of btnText"
:key="text.week"
rounded
:style="{
border: text.week === weekNow ? '1px solid var(--tgc-yellow-1)' : 'none',
backgroundColor:
text.week === btnNow
? 'var(--tgc-yellow-1)'
: text.week === weekNow
? 'transparent'
: 'var(--tgc-btn-1)',
color:
text.week === btnNow
? 'var(--box-text-4)'
: text.week === weekNow
? 'var(--tgc-yellow-1)'
: 'var(--btn-text)',
}"
@click="getContents(text.week)"
class="tc-btn"
:class="{ selected: text.week === btnNow, today: text.week === weekNow }"
@click="btnNow = text.week"
>
{{ text.text }}
</v-btn>
@@ -53,38 +37,19 @@
<ToCalendar v-model="showItem" :data-type="selectedType" :data-val="selectedItem" />
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from "vue";
import { AppCalendarData } from "../../data/index.js";
import { timestampToDate } from "../../utils/toolFunc.js";
import TItemBox, { type TItemBoxData } from "../app/t-item-box.vue";
import TItemBox, { type TItemBoxData } from "@comp/app/t-itemBox.vue";
import { computed, onMounted, ref, shallowRef } from "vue";
import TCalendarBirth from "./ph-calendar-birth.vue";
import ToCalendar from "./ph-calendar-overlay.vue";
import THomeCard from "./ph-comp-card.vue";
const weekNow = ref<number>(0);
const btnNow = ref<number>(0);
const dateNow = ref<string>("");
import { AppCalendarData } from "@/data/index.js";
import { timestampToDate } from "@/utils/toolFunc.js";
// page
const page = ref<number>(1);
const length = ref<number>(0);
const visible = 16;
// calendar
const calendarNow = ref<TGApp.App.Calendar.Item[]>([]);
const characterCards = ref<TGApp.App.Calendar.Item[]>([]);
const weaponCards = ref<TGApp.App.Calendar.Item[]>([]);
// calendar item
const showItem = ref<boolean>(false);
const switchType = ref<"avatar" | "weapon">("avatar");
const selectedItem = ref<TGApp.App.Calendar.Item>(<TGApp.App.Calendar.Item>{});
const selectedType = ref<"avatar" | "weapon">("avatar");
const renderItems = ref<TGApp.App.Calendar.Item[]>([]);
const btnText = [
type BtnItem = { week: 1 | 2 | 3 | 4 | 5 | 6 | 7; text: string };
type TCalendarEmits = (e: "success") => void;
const btnText: Array<BtnItem> = [
{ week: 7, text: "周日" },
{ week: 1, text: "周一" },
{ week: 2, text: "周二" },
@@ -93,98 +58,59 @@ const btnText = [
{ week: 5, text: "周五" },
{ week: 6, text: "周六" },
];
interface TCalendarEmits {
(e: "success"): void;
}
const emits = defineEmits<TCalendarEmits>();
const visible = 16;
onMounted(async () => {
const weekNow = ref<number>(0);
const btnNow = ref<number>(0);
const dateNow = ref<string>("");
const page = ref<number>(1);
const showItem = ref<boolean>(false);
const selectedType = ref<"character" | "weapon">("character");
const calendarTotal = computed<Array<TGApp.App.Calendar.Item>>(() =>
AppCalendarData.filter(
(i) => i.dropDays.includes(btnNow.value) && i.itemType === selectedType.value,
),
);
const length = computed<number>(() => Math.ceil(calendarTotal.value.length / visible));
const renderItems = computed<Array<TGApp.App.Calendar.Item>>(() =>
calendarTotal.value.slice((page.value - 1) * visible, page.value * visible),
);
const selectedItem = shallowRef<TGApp.App.Calendar.Item>(renderItems.value[0]);
onMounted(() => {
const dayNow = new Date().getDay() === 0 ? 7 : new Date().getDay();
const week = <{ week: number; text: string }>btnText.find((item) => item.week === dayNow);
const week = btnText.find((item) => item.week === dayNow) ?? { text: "周日", week: 7 };
dateNow.value = `${timestampToDate(new Date().getTime())} ${week.text}`;
weekNow.value = dayNow;
btnNow.value = dayNow;
calendarNow.value = getCalendar(dayNow);
characterCards.value = calendarNow.value.filter((item) => item.itemType === "character");
weaponCards.value = calendarNow.value.filter((item) => item.itemType === "weapon");
renderItems.value = getGrid();
emits("success");
});
watch(
() => page.value,
() => {
renderItems.value = getGrid();
},
);
watch(
() => switchType.value,
() => {
if (page.value !== 1) page.value = 1;
else renderItems.value = getGrid();
},
);
// 获取当前日历
function getCalendar(day: number): TGApp.App.Calendar.Item[] {
return AppCalendarData.filter((item) => item.dropDays.includes(day));
}
function getGrid(): TGApp.App.Calendar.Item[] {
let selectedCards: TGApp.App.Calendar.Item[];
if (switchType.value === "avatar") selectedCards = characterCards.value;
else selectedCards = weaponCards.value;
length.value = Math.ceil(selectedCards.length / visible);
return selectedCards.slice((page.value - 1) * visible, page.value * visible);
function switchType(): void {
selectedType.value = selectedType.value === "character" ? "weapon" : "character";
page.value = 1;
}
function selectItem(item: TGApp.App.Calendar.Item): void {
selectedItem.value = item;
selectedType.value = switchType.value;
showItem.value = true;
}
function getContents(day: number): void {
btnNow.value = day;
calendarNow.value = getCalendar(day);
characterCards.value = calendarNow.value.filter((item) => item.itemType === "character");
weaponCards.value = calendarNow.value.filter((item) => item.itemType === "weapon");
if (page.value !== 1) page.value = 1;
else renderItems.value = getGrid();
}
function getBoxData(item: TGApp.App.Calendar.Item): TItemBoxData {
if (switchType.value === "avatar") {
return {
bg: item.bg,
icon: item.icon,
size: "100px",
height: "100px",
display: "inner",
clickable: true,
lt: item.elementIcon ?? "",
ltSize: "20px",
innerHeight: 25,
innerIcon: item.weaponIcon,
innerText: item.name,
};
} else {
return {
bg: item.bg,
icon: item.icon,
size: "100px",
height: "100px",
display: "inner",
clickable: true,
lt: item.weaponIcon,
ltSize: "20px",
innerHeight: 25,
innerText: item.name,
};
}
return {
bg: item.bg,
icon: item.icon,
size: "100px",
height: "100px",
display: "inner",
clickable: true,
lt: selectedType.value === "weapon" ? item.weaponIcon : (item.elementIcon ?? ""),
ltSize: "20px",
innerHeight: 25,
innerIcon: selectedType.value === "character" ? item.weaponIcon : undefined,
innerText: item.name,
};
}
</script>
<style lang="css" scoped>
@@ -212,6 +138,25 @@ function getBoxData(item: TGApp.App.Calendar.Item): TItemBoxData {
column-gap: 5px;
}
.tc-btn {
background: var(--tgc-btn-1);
color: var(--btn-text);
&.today {
border: 1px solid var(--tgc-yellow-1);
}
&.selected {
background-color: var(--tgc-yellow-1);
color: var(--box-text-4);
}
&.today:not(.selected) {
background-color: transparent;
color: var(--tgc-yellow-1);
}
}
.tc-content {
position: relative;
display: flex;

View File

@@ -3,7 +3,7 @@
<div class="thc-title">
<slot name="title"></slot>
</div>
<div v-if="props.append" class="thc-append">
<div v-if="append" class="thc-append">
<slot name="title-append"></slot>
</div>
<div class="thc-box">
@@ -12,8 +12,7 @@
</div>
</template>
<script lang="ts" setup>
type THomeCardProps = { append?: boolean };
const props = defineProps<THomeCardProps>();
defineProps<{ append?: boolean }>();
</script>
<style lang="css" scoped>
.thc-container {

View File

@@ -2,8 +2,8 @@
<THomeCard :append="hasNew">
<template #title>限时祈愿</template>
<template #title-append>
<v-switch class="pool-switch" @change="switchPool" />
{{ showNew ? "查看当前祈愿" : "查看后续祈愿" }}
<v-switch class="pool-switch" @change="showNew = !showNew" />
<span>{{ showNew ? "查看当前祈愿" : "查看后续祈愿" }}</span>
</template>
<template #default>
<div class="pool-grid">
@@ -30,20 +30,22 @@
</div>
</div>
<div class="pool-time">
<div>
<div class="left">
<v-icon>mdi-calendar-clock</v-icon>
{{ pool.time.str }}
<span>{{ pool.time.str }}</span>
</div>
<v-progress-linear :model-value="poolTimePass[pool.postId]" :rounded="true">
</v-progress-linear>
<div v-if="poolTimeGet[pool.postId] === '已结束'">
{{ poolTimeGet[pool.postId] }}
<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>
<div v-else class="time">
<span>剩余时间</span>
<span>
{{ poolTimeGet[pool.postId] }}
</span>
<span>{{ stamp2LastTime(pool.timeRest) }}</span>
</div>
</div>
</div>
@@ -53,122 +55,95 @@
</THomeCard>
</template>
<script lang="ts" setup>
import TItembox, { type TItemBoxData } from "@comp/app/t-itemBox.vue";
import showSnackbar from "@comp/func/snackbar.js";
import Mys from "@Mys/index.js";
import { storeToRefs } from "pinia";
import { ref, onMounted, onUnmounted } from "vue";
import { computed, onMounted, onUnmounted, ref } from "vue";
import { useRouter } from "vue-router";
import Mys from "../../plugins/Mys/index.js";
import { useHomeStore } from "../../store/modules/home.js";
import { createPost, createTGWindow } from "../../utils/TGWindow.js";
import { stamp2LastTime } from "../../utils/toolFunc.js";
import TItembox, { TItemBoxData } from "../app/t-item-box.vue";
import showSnackbar from "../func/snackbar.js";
import THomeCard from "./ph-comp-card.vue";
// store
const homeStore = storeToRefs(useHomeStore());
import { useHomeStore } from "@/store/modules/home.js";
import { createPost, createTGWindow } from "@/utils/TGWindow.js";
import { stamp2LastTime } from "@/utils/toolFunc.js";
const router = useRouter();
const hasNew = ref<boolean>(false);
const showNew = ref<boolean>(false);
// data
const poolCards = ref<TGApp.Plugins.Mys.Gacha.RenderCard[]>([]);
const poolSelect = ref<TGApp.Plugins.Mys.Gacha.RenderCard[]>([]);
const poolTimeGet = ref<Record<number, string>>({});
const poolTimePass = ref<Record<number, number>>({});
const timer = ref<Record<number, any>>({});
interface TPoolEmits {
(e: "success"): void;
}
type TPoolEmits = (e: "success") => void;
type PoolStat = "future" | "now" | "past"; // 未开始 | 进行中 | 已结束
type PoolItem = TGApp.Plugins.Mys.Gacha.RenderCard & { 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;
function poolLastInterval(postId: number): TGApp.Plugins.Mys.Gacha.RenderCard | undefined {
const pool = poolCards.value.find((pool) => pool.postId === postId);
if (!pool) return;
if (poolTimeGet.value[postId] === "未开始") {
const isStart = pool.time.startStamp - Date.now();
if (isStart > 0) return;
poolTimeGet.value[postId] = stamp2LastTime(pool.time.endStamp - Date.now());
poolTimePass.value[postId] = pool.time.endStamp - Date.now();
} else {
const isEnd = pool.time.endStamp - Date.now();
poolTimeGet.value[postId] = stamp2LastTime(isEnd);
poolTimePass.value[postId] =
((pool.time.endStamp - Date.now()) / (pool.time.endStamp - pool.time.startStamp)) * 100;
if (isEnd >= 0) return;
clearInterval(timer.value[postId]);
timer.value[postId] = null;
poolTimePass.value[postId] = 100;
poolTimeGet.value[postId] = "已结束";
}
return pool;
}
const showNew = ref<boolean>(false);
const poolCards = ref<Array<PoolItem>>([]);
const hasNew = computed<boolean>(
() => poolCards.value.find((pool) => pool.stat === "future") !== undefined,
);
const poolSelect = computed<Array<PoolItem>>(() => {
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");
});
onMounted(async () => {
const gachaData = await Mys.Gacha.get();
if (!gachaData) {
console.error("获取限时祈愿数据失败");
return;
}
console.log("获取限时祈愿数据成功");
console.info(gachaData);
let cards: Array<TGApp.Plugins.Mys.Gacha.RenderCard>;
if (!checkCover(gachaData)) {
poolCards.value = await Mys.Gacha.card(gachaData);
cards = await Mys.Gacha.card(gachaData);
const coverData: Record<string, string> = {};
poolCards.value.map((pool) => {
coverData[pool.id] = pool.cover;
return pool;
});
homeStore.poolCover.value = coverData;
poolCover.value = coverData;
} else {
poolCards.value = await Mys.Gacha.card(gachaData, homeStore.poolCover.value);
cards = await Mys.Gacha.card(gachaData, poolCover.value);
}
poolCards.value.map((pool) => {
poolTimeGet.value[pool.postId] = stamp2LastTime(pool.time.endStamp - Date.now());
poolTimePass.value[pool.postId] = pool.time.endStamp - Date.now();
if (poolTimePass.value[pool.postId] <= 0) {
poolTimeGet.value[pool.postId] = "已结束";
poolTimePass.value[pool.postId] = 100;
showNew.value = false;
} else if (pool.time.startStamp - Date.now() > 0) {
poolTimeGet.value[pool.postId] = "未开始";
poolTimePass.value[pool.postId] = 100;
}
timer.value[pool.postId] = setInterval(() => {
poolLastInterval(pool.postId);
}, 1000);
return pool;
});
if (poolCards.value.length > 2) {
poolSelect.value = poolCards.value.filter(
(pool) => poolTimeGet.value[pool.postId] !== "未开始",
);
hasNew.value =
poolCards.value.filter((pool) => poolTimeGet.value[pool.postId] === "未开始").length > 0;
} else {
poolSelect.value = poolCards.value;
hasNew.value = false;
for (const pool of cards) {
const timeRest = pool.time.endStamp - Date.now();
poolCards.value.push({
...pool,
stat: timeRest > pool.time.totalStamp ? "future" : timeRest > 0 ? "now" : "past",
timeRest: timeRest,
});
}
if (timer !== null) clearInterval(timer);
timer = setInterval(poolTimeout, 1000);
emits("success");
});
// 检测新卡池
function checkCover(data: TGApp.Plugins.Mys.Gacha.Data[]): boolean {
if (!homeStore.poolCover || Object.keys(homeStore.poolCover).length === 0) {
return false;
}
const cover = homeStore.poolCover;
if (cover.value === undefined) return false;
let checkList = data.length;
Object.entries(cover).forEach(([key, value]: [string, unknown]) => {
const pool = data.find((item: TGApp.Plugins.Mys.Gacha.Data) => item.id.toString() === key);
if (pool && value !== "/source/UI/empty.webp") {
checkList--;
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.Plugins.Mys.Gacha.Data>): 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;
}
@@ -205,25 +180,9 @@ function getCBox(info: TGApp.App.Character.WikiBriefInfo): TItemBoxData {
};
}
// 更换显示的卡池
async function switchPool(): Promise<void> {
showNew.value = !showNew.value;
if (showNew.value) {
poolSelect.value = poolCards.value.filter(
(pool) => poolTimeGet.value[pool.postId] === "未开始",
);
} else {
poolSelect.value = poolCards.value.filter(
(pool) => poolTimeGet.value[pool.postId] !== "未开始",
);
}
}
onUnmounted(() => {
Object.keys(timer.value).forEach((key) => {
clearInterval(timer.value[Number(key)]);
});
timer.value = {};
if (timer !== null) clearInterval(timer);
timer = null;
});
</script>

View File

@@ -3,110 +3,76 @@
<template #title>近期活动</template>
<template #default>
<div class="position-grid">
<v-card v-for="(card, index) in positionCards" :key="index" class="position-card" rounded>
<v-list class="position-list">
<v-list-item :subtitle="card.abstract" :title="card.title">
<template #prepend>
<v-avatar rounded="0" @click="openPosition(card)">
<v-img :src="card.icon" class="position-icon" />
</v-avatar>
</template>
<template #append>
<v-btn class="position-card-btn" @click="openPosition(card)"> 查看</v-btn>
</template>
</v-list-item>
</v-list>
<v-divider />
<v-card-text>
<div class="position-card-text">
<v-icon>mdi-calendar-clock</v-icon>
<span>{{ card.time.start }}~{{ card.time.end }}</span>
</div>
<div class="position-card-text">
<v-icon>mdi-clock-outline</v-icon>
<span v-if="positionTimeGet[card.postId] !== '已结束'">{{
positionTimeGet[card.postId]
}}</span>
<span v-else>已结束</span>
</div>
</v-card-text>
</v-card>
<PhPositionCard v-for="(card, index) in positionCards" :key="index" :position="card" />
</div>
</template>
</THomeCard>
</template>
<script lang="ts" setup>
import PhPositionCard from "@comp/pageHome/ph-position-card.vue";
import Mys from "@Mys/index.js";
import { onMounted, onUnmounted, ref } from "vue";
import Mys from "../../plugins/Mys/index.js";
import { parseLink } from "../../utils/linkParser.js";
import { createPost } from "../../utils/TGWindow.js";
import { stamp2LastTime } from "../../utils/toolFunc.js";
import showSnackbar from "../func/snackbar.js";
import THomeCard from "./ph-comp-card.vue";
// data
const positionCards = ref<TGApp.Plugins.Mys.Position.RenderCard[]>([]);
const positionTimeGet = ref<Record<number, string>>({}); // 剩余时间/已结束/未知
const positionTimeEnd = ref<Record<number, number>>({}); // 结束时间戳
const positionTimer = ref<Record<number, any>>({}); // 定时器
interface TPositionEmits {
(e: "success"): void;
}
type TPositionEmits = (e: "success") => void;
type PositionStat = "past" | "now" | "future" | "unknown"; // 已结束 | 进行中 | 未开始 | 未知
export type PositionItem = TGApp.Plugins.Mys.Position.RenderCard & {
timeRest: number;
stat: PositionStat;
};
const emits = defineEmits<TPositionEmits>();
function positionLastInterval(postId: number): void {
const timeGet = positionTimeGet.value[postId];
if (timeGet === "未知" || timeGet === "已结束") {
clearInterval(positionTimer.value[postId]);
positionTimer.value[postId] = null;
return;
}
const timeLast = positionTimeEnd.value[postId] - Date.now();
if (timeLast <= 0) {
positionTimeGet.value[postId] = "已结束";
} else {
positionTimeGet.value[postId] = stamp2LastTime(timeLast);
}
}
// eslint-disable-next-line no-undef
let timer: NodeJS.Timeout | null = null;
const positionCards = ref<Array<PositionItem>>([]);
onMounted(async () => {
const positionData = await Mys.Position.get();
if (!positionData) {
console.error("获取近期活动失败");
return;
}
positionCards.value = Mys.Position.card(positionData);
positionCards.value.forEach((card) => {
if (card.time.endStamp === 0) {
positionTimeGet.value[card.postId] = "未知";
} else {
positionTimeGet.value[card.postId] = stamp2LastTime(card.time.endStamp - Date.now());
console.log(positionData);
const cards = Mys.Position.card(positionData);
for (const position of cards) {
if (position.time.endStamp === 0 || position.time.totalStamp < 0) {
positionCards.value.push({
...position,
timeRest: 0,
stat: "unknown",
});
continue;
}
positionTimeEnd.value[card.postId] = card.time.endStamp;
positionTimer.value[card.postId] = setInterval(() => {
positionLastInterval(card.postId);
}, 1000);
});
const timeRest = position.time.endStamp - Date.now();
positionCards.value.push({
...position,
timeRest,
stat: timeRest > position.time.totalStamp ? "future" : timeRest > 0 ? "now" : "past",
});
}
if (timer !== null) clearInterval(timer);
timer = setInterval(getPositionTimer, 1000);
emits("success");
});
async function openPosition(card: TGApp.Plugins.Mys.Position.RenderCard): Promise<void> {
const res = await parseLink(card.link);
if (res === "post") await createPost(card.postId, card.title);
if (res === false) {
showSnackbar.warn(`未知链接:${card.link}`, 3000);
return;
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(() => {
Object.keys(positionTimer.value).forEach((key) => {
clearInterval(positionTimer.value[Number(key)]);
});
if (timer !== null) clearInterval(timer);
timer = null;
});
</script>
@@ -117,54 +83,4 @@ onUnmounted(() => {
grid-gap: 20px;
grid-template-columns: repeat(auto-fill, minmax(calc(400px + 2rem), 0.5fr));
}
.position-card {
border: none;
background: var(--box-bg-1);
}
.position-list {
background: inherit;
color: inherit;
font-family: var(--font-title);
}
.position-icon {
overflow: hidden;
width: 100%;
height: 100%;
border-radius: 5px;
object-fit: contain;
}
.position-icon :deep(img) {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.5s;
}
.position-icon :hover {
cursor: pointer;
scale: 1.2;
}
.position-card-btn {
border: 1px solid var(--common-shadow-4);
border-radius: 5px;
background: var(--tgc-btn-1);
color: var(--btn-text);
}
.position-card-text {
display: inline-block;
min-width: 200px;
align-items: flex-start;
margin-right: 5px;
}
.position-card-text :nth-child(1) {
color: var(--btn-text);
filter: brightness(0.8);
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<div class="ph-pool-card">
<div class="top">
<div class="main">
<div class="left"><img :src="props.position.icon" alt="icon" /></div>
<div class="right">
<div class="title">{{ props.position.title }}</div>
<div class="sub">{{ props.position.abstract }}</div>
</div>
</div>
<v-btn class="btn" @click="openPosition(props.position)">查看</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.position.time.endStamp === 0
? "未知"
: timestampToDate(props.position.time.endStamp)
}}
</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-else>未开始</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import showSnackbar from "@comp/func/snackbar.js";
import type { PositionItem } from "@comp/pageHome/ph-comp-position.vue";
import { parseLink } from "@/utils/linkParser.js";
import { createPost } from "@/utils/TGWindow.js";
import { stamp2LastTime, timestampToDate } from "@/utils/toolFunc.js";
type PhPositionCardProps = { position: PositionItem };
const props = defineProps<PhPositionCardProps>();
async function openPosition(card: TGApp.Plugins.Mys.Position.RenderCard): Promise<void> {
const res = await parseLink(card.link);
if (res === "post") await createPost(card.postId, card.title);
if (res === false) {
showSnackbar.warn(`未知链接:${card.link}`, 3000);
return;
}
}
</script>
<style lang="css" scoped>
.ph-pool-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-radius: 4px;
background: var(--box-bg-1);
box-shadow: 0 0 5px var(--common-shadow-2);
color: var(--box-text-1);
}
.dark .ph-pool-card {
border: 1px solid var(--common-shadow-1);
box-shadow: unset;
}
.top {
position: relative;
display: flex;
width: 100%;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
padding: 8px;
border-bottom: 2px solid var(--common-shadow-2);
.main {
position: relative;
display: flex;
align-items: center;
justify-content: flex-start;
column-gap: 8px;
.left {
position: relative;
overflow: hidden;
width: 40px;
height: 40px;
border-radius: 3px;
object-fit: contain;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: all 0.5s;
}
&:hover img {
cursor: pointer;
scale: 1.2;
}
}
.right {
position: relative;
display: flex;
height: 100%;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
.title {
font-family: var(--font-title);
font-size: 16px;
}
.sub {
font-size: 14px;
opacity: 0.8;
}
}
}
.btn {
border: 1px solid var(--common-shadow-4);
border-radius: 5px;
background: var(--tgc-btn-1);
color: var(--btn-text);
font-family: var(--font-title);
}
}
.bottom {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
font-size: 14px;
gap: 6px;
.period {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: flex-start;
column-gap: 4px;
:first-child {
color: var(--tgc-od-orange);
}
}
.rest {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: flex-end;
column-gap: 4px;
:first-child {
color: var(--tgc-od-green);
}
}
}
</style>