mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2025-12-16 09:58:13 +08:00
♻️ 调整目录结构
This commit is contained in:
150
src/components/pageHome/ph-calendar-birth.vue
Normal file
150
src/components/pageHome/ph-calendar-birth.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<div class="tcb-container">
|
||||
<div class="tcb-top-none" v-if="!isBirthday">
|
||||
<img src="/source/UI/empty.webp" alt="empty" />
|
||||
<span>今天没有角色过生日哦~</span>
|
||||
</div>
|
||||
<div class="tcb-top-active" v-else>
|
||||
<span>今天是</span>
|
||||
<img
|
||||
v-for="i in cur"
|
||||
:key="i.role_id"
|
||||
class="tcb-cur"
|
||||
:alt="i.name"
|
||||
:src="i.head_icon"
|
||||
:title="i.name"
|
||||
/>
|
||||
<span>的生日哦~</span>
|
||||
<img @click="toBirth(true)" src="/source/UI/act_birthday.png" alt="empty" class="active" />
|
||||
</div>
|
||||
<div>即将到来:{{ next[0].role_birthday }}</div>
|
||||
<div v-for="i in next" :key="i.role_id" class="tcb-item">
|
||||
<img :src="i.head_icon" :alt="i.name" @click="toBirth(i)" :title="i.name" />
|
||||
<div class="tcb-item-info">
|
||||
<span>{{ i.name }} 所属:{{ i.belong === "" ? "未知" : i.belong }}</span>
|
||||
<span>{{ parseDesc(i.introduce) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
import TSAvatarBirth from "../../plugins/Sqlite/modules/avatarBirth.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();
|
||||
|
||||
onBeforeMount(async () => {
|
||||
const check = TSAvatarBirth.isAvatarBirth();
|
||||
if (check.length !== 0) {
|
||||
isBirthday.value = true;
|
||||
cur.value = check;
|
||||
}
|
||||
next.value = TSAvatarBirth.getNextAvatarBirth();
|
||||
});
|
||||
|
||||
function toBirth(type: TGApp.Archive.Birth.RoleItem | true): void {
|
||||
let dateStr;
|
||||
if (type === true) {
|
||||
const date = new Date();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
dateStr = `${month}/${day}`;
|
||||
} else {
|
||||
dateStr = type.role_birthday;
|
||||
}
|
||||
if (type !== true) {
|
||||
router.push({ name: "留影叙佳期", params: { date: dateStr } });
|
||||
return;
|
||||
}
|
||||
if (cur.value.length > 0 && !cur.value[0].is_subscribe) {
|
||||
appStore.recentNewsType = "news";
|
||||
router.push("/news/2/news");
|
||||
return;
|
||||
}
|
||||
router.push({ name: "留影叙佳期", params: { date: dateStr } });
|
||||
}
|
||||
|
||||
function parseDesc(intro: string): string {
|
||||
const dom = new DOMParser().parseFromString(intro, "text/html");
|
||||
return dom.body.textContent || "";
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tcb-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 5px inset var(--common-shadow-1);
|
||||
overflow-y: auto;
|
||||
row-gap: 5px;
|
||||
}
|
||||
|
||||
.tcb-top-none,
|
||||
.tcb-top-active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tcb-top-none img,
|
||||
.tcb-top-active img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.tcb-top-active img.active {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tcb-top-active img.tcb-cur {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: var(--common-shadow-1);
|
||||
}
|
||||
|
||||
.tcb-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
background: var(--box-bg-1);
|
||||
}
|
||||
|
||||
.tcb-item img {
|
||||
height: 100px;
|
||||
aspect-ratio: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tcb-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tcb-item-info :first-child {
|
||||
font-family: var(--font-title);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tcb-item-info :last-child {
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
75
src/components/pageHome/ph-calendar-material.vue
Normal file
75
src/components/pageHome/ph-calendar-material.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<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>
|
||||
</div>
|
||||
<div class="tcm-right">{{ props.item.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
interface TMiniWeaponProps {
|
||||
item: TGApp.App.Calendar.Material;
|
||||
}
|
||||
|
||||
const props = defineProps<TMiniWeaponProps>();
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tcm-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 45px;
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
border-radius: 5px;
|
||||
background: var(--box-bg-1);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.tcm-left {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
.tcm-bg,
|
||||
.tcm-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
.tcm-bg img,
|
||||
.tcm-icon img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-top-left-radius: 5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.tcm-star {
|
||||
position: absolute;
|
||||
bottom: -8px;
|
||||
width: 45px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tcm-star img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tcm-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--box-text-2);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
181
src/components/pageHome/ph-calendar-overlay.vue
Normal file
181
src/components/pageHome/ph-calendar-overlay.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<TOverlay v-model="visible" :to-click="onCancel" blur-val="20px" hide>
|
||||
<div class="toc-box">
|
||||
<div class="box-div">
|
||||
<div class="toc-top">
|
||||
<TItemBox :model-value="getBoxData()" />
|
||||
<div class="toc-material-grid">
|
||||
<TibCalendarMaterial
|
||||
v-for="(item, index) in itemVal.materials"
|
||||
:key="index"
|
||||
:item="item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<img alt="line" class="toc-line" src="/source/UI/item-line.webp" />
|
||||
<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>
|
||||
</div>
|
||||
<v-btn variant="outlined" @click="toDetail(itemVal)">详情</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TOverlay>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
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 TibCalendarMaterial from "./ph-calendar-material.vue";
|
||||
|
||||
interface ToCalendarProps {
|
||||
modelValue: boolean;
|
||||
dataType: "weapon" | "avatar";
|
||||
dataVal: TGApp.App.Calendar.Item;
|
||||
}
|
||||
|
||||
interface ToCalendarEmits {
|
||||
(e: "update:modelValue", value: boolean): void;
|
||||
|
||||
(e: "cancel"): void;
|
||||
}
|
||||
|
||||
const emits = defineEmits<ToCalendarEmits>();
|
||||
const props = defineProps<ToCalendarProps>();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => {
|
||||
emits("update:modelValue", value);
|
||||
},
|
||||
});
|
||||
const itemVal = computed<TGApp.App.Calendar.Item>(() => props.dataVal);
|
||||
|
||||
const onCancel = (): void => {
|
||||
visible.value = false;
|
||||
emits("cancel");
|
||||
};
|
||||
|
||||
async function toDetail(item: TGApp.App.Calendar.Item): Promise<void> {
|
||||
if (item.itemType === "character") {
|
||||
await router.push(`/wiki/character/${item.id}`);
|
||||
} else {
|
||||
await router.push(`/wiki/weapon/${item.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getBoxData(): TItemBoxData {
|
||||
if (props.dataType === "avatar") {
|
||||
return {
|
||||
bg: props.dataVal.bg,
|
||||
icon: props.dataVal.icon,
|
||||
size: "100px",
|
||||
height: "100px",
|
||||
display: "inner",
|
||||
clickable: false,
|
||||
lt: props.dataVal.elementIcon ?? "",
|
||||
ltSize: "20px",
|
||||
innerHeight: 25,
|
||||
innerIcon: props.dataVal.weaponIcon,
|
||||
innerText: props.dataVal.name,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
bg: props.dataVal.bg,
|
||||
icon: props.dataVal.icon,
|
||||
size: "100px",
|
||||
height: "100px",
|
||||
display: "inner",
|
||||
clickable: false,
|
||||
lt: props.dataVal.weaponIcon,
|
||||
ltSize: "20px",
|
||||
innerHeight: 25,
|
||||
innerText: props.dataVal.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.toc-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.box-div {
|
||||
display: flex;
|
||||
width: 500px;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
background-color: var(--app-page-bg);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toc-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toc-material-grid {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
font-family: var(--font-title);
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.toc-line {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.toc-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 10px;
|
||||
padding-left: 10px;
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
border-radius: 5px;
|
||||
background: var(--box-bg-1);
|
||||
}
|
||||
|
||||
.toc-src-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.toc-src-box :nth-child(2) {
|
||||
height: 30px;
|
||||
margin: 5px;
|
||||
aspect-ratio: 1;
|
||||
filter: invert(87%) sepia(14%) saturate(216%) hue-rotate(180deg) brightness(81%) contrast(87%);
|
||||
}
|
||||
|
||||
.dark .toc-src-box :nth-child(2) {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.toc-src-text {
|
||||
display: flex;
|
||||
height: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-title);
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
231
src/components/pageHome/ph-comp-calendar.vue
Normal file
231
src/components/pageHome/ph-comp-calendar.vue
Normal file
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<THomeCard append>
|
||||
<template #title>今日素材 {{ dateNow }}</template>
|
||||
<template #title-append>
|
||||
<v-switch
|
||||
class="tc-switch"
|
||||
@change="switchType = switchType === 'avatar' ? 'weapon' : 'avatar'"
|
||||
/>
|
||||
{{ switchType === "avatar" ? "角色" : "武器" }}
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="tc-top">
|
||||
<div class="tc-btn-list">
|
||||
<v-btn
|
||||
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)"
|
||||
>
|
||||
{{ text.text }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-pagination class="tc-page" v-model="page" :total-visible="visible" :length="length" />
|
||||
</div>
|
||||
<div class="tc-content">
|
||||
<TCalendarBirth />
|
||||
<div class="calendar-grid">
|
||||
<TItemBoxData
|
||||
v-for="item in renderItems"
|
||||
:key="item.id"
|
||||
@click="selectItem(item)"
|
||||
:model-value="getBoxData(item)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</THomeCard>
|
||||
<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 type { TItemBoxData } from "../app/t-item-box.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>("");
|
||||
|
||||
// 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 = [
|
||||
{ week: 7, text: "周日" },
|
||||
{ week: 1, text: "周一" },
|
||||
{ week: 2, text: "周二" },
|
||||
{ week: 3, text: "周三" },
|
||||
{ week: 4, text: "周四" },
|
||||
{ week: 5, text: "周五" },
|
||||
{ week: 6, text: "周六" },
|
||||
];
|
||||
|
||||
interface TCalendarEmits {
|
||||
(e: "success"): void;
|
||||
}
|
||||
|
||||
const emits = defineEmits<TCalendarEmits>();
|
||||
|
||||
onMounted(async () => {
|
||||
const dayNow = new Date().getDay() === 0 ? 7 : new Date().getDay();
|
||||
const week = <{ week: number; text: string }>btnText.find((item) => item.week === dayNow);
|
||||
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 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.tc-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tc-switch {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tc-btn-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: 5px;
|
||||
}
|
||||
|
||||
.tc-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 210px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
column-gap: 10px;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(8, 100px);
|
||||
place-items: flex-start flex-start;
|
||||
}
|
||||
</style>
|
||||
56
src/components/pageHome/ph-comp-card.vue
Normal file
56
src/components/pageHome/ph-comp-card.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="thc-container">
|
||||
<div class="thc-title">
|
||||
<slot name="title"></slot>
|
||||
</div>
|
||||
<div v-if="props.append" class="thc-append">
|
||||
<slot name="title-append"></slot>
|
||||
</div>
|
||||
<div class="thc-box">
|
||||
<slot name="default"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
interface THomeCardProps {
|
||||
append?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<THomeCardProps>();
|
||||
</script>
|
||||
<style lang="css" scoped>
|
||||
.thc-container {
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
padding: 20px 10px 10px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 5px;
|
||||
margin-top: 30px;
|
||||
box-shadow: 2px 2px 5px var(--common-shadow-1);
|
||||
}
|
||||
|
||||
.thc-title,
|
||||
.thc-append {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 10px;
|
||||
border-radius: 5px;
|
||||
background: var(--tgc-od-blue);
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
|
||||
.thc-title {
|
||||
left: 10px;
|
||||
color: var(--tgc-white-4);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.thc-append {
|
||||
right: 10px;
|
||||
color: var(--tgc-white-1);
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
345
src/components/pageHome/ph-comp-pool.vue
Normal file
345
src/components/pageHome/ph-comp-pool.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<THomeCard :append="hasNew">
|
||||
<template #title>限时祈愿</template>
|
||||
<template #title-append>
|
||||
<v-switch class="pool-switch" @change="switchPool" />
|
||||
{{ showNew ? "查看当前祈愿" : "查看后续祈愿" }}
|
||||
</template>
|
||||
<template #default>
|
||||
<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)">
|
||||
<img :src="pool.cover" alt="cover" />
|
||||
</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>
|
||||
<v-icon>mdi-calendar-clock</v-icon>
|
||||
{{ pool.time.str }}
|
||||
</div>
|
||||
<v-progress-linear :model-value="poolTimePass[pool.postId]" :rounded="true">
|
||||
</v-progress-linear>
|
||||
<div v-if="poolTimeGet[pool.postId] === '已结束'">
|
||||
{{ poolTimeGet[pool.postId] }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<span>剩余时间:</span>
|
||||
<span>
|
||||
{{ poolTimeGet[pool.postId] }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</THomeCard>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ref, onMounted, onUnmounted } 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());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const emits = defineEmits<TPoolEmits>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const gachaData = await Mys.Gacha.get();
|
||||
if (!gachaData) {
|
||||
console.error("获取限时祈愿数据失败");
|
||||
return;
|
||||
}
|
||||
console.log("获取限时祈愿数据成功");
|
||||
console.info(gachaData);
|
||||
if (!checkCover(gachaData)) {
|
||||
poolCards.value = 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;
|
||||
} else {
|
||||
poolCards.value = await Mys.Gacha.card(gachaData, homeStore.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;
|
||||
}
|
||||
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--;
|
||||
}
|
||||
});
|
||||
return checkList === 0;
|
||||
}
|
||||
|
||||
async function toOuter(
|
||||
character: TGApp.Plugins.Mys.Gacha.RenderItem,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// 更换显示的卡池
|
||||
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 = {};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.pool-switch {
|
||||
display: flex;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
170
src/components/pageHome/ph-comp-position.vue
Normal file
170
src/components/pageHome/ph-comp-position.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<THomeCard>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</THomeCard>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
positionTimeEnd.value[card.postId] = card.time.endStamp;
|
||||
positionTimer.value[card.postId] = setInterval(() => {
|
||||
positionLastInterval(card.postId);
|
||||
}, 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;
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
Object.keys(positionTimer.value).forEach((key) => {
|
||||
clearInterval(positionTimer.value[Number(key)]);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.position-grid {
|
||||
display: grid;
|
||||
margin-top: 10px;
|
||||
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>
|
||||
Reference in New Issue
Block a user