mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-04-22 21:59:49 +08:00
✨ 首页实时便笺组件
This commit is contained in:
187
src/components/pageHome/ph-comp-daily-note.vue
Normal file
187
src/components/pageHome/ph-comp-daily-note.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<!-- 首页实时便笺卡片 -->
|
||||
<template>
|
||||
<THomeCard :append="isLogin" title="实时便笺">
|
||||
<template v-if="isLogin" #title-append>
|
||||
<PhUserSwitch
|
||||
:current-uid="uid ?? ''"
|
||||
:nickname="briefInfo.nickname"
|
||||
@switch-user="handleUserSwitch"
|
||||
/>
|
||||
</template>
|
||||
<template #default>
|
||||
<div v-if="!isLogin" class="dn-not-login">请先登录</div>
|
||||
<div v-else-if="gameAccounts.length === 0" class="dn-not-login">暂无游戏账户</div>
|
||||
<div v-else-if="loading" class="dn-loading">
|
||||
<div class="loading-content">
|
||||
<v-progress-linear :model-value="loadingProgress" color="blue" height="6" rounded />
|
||||
<div class="loading-text">{{ loadingText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="dn-container">
|
||||
<PhDailyNoteItem
|
||||
v-for="item in dailyNoteAccounts"
|
||||
:key="`${item.account.gameBiz}_${item.account.gameUid}`"
|
||||
:account="item.account"
|
||||
:data="item.data"
|
||||
@refresh="handleRefresh(item.account)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</THomeCard>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import recordReq from "@req/recordReq.js";
|
||||
import TSUserAccount from "@Sqlm/userAccount.js";
|
||||
import useAppStore from "@store/app.js";
|
||||
import useUserStore from "@store/user.js";
|
||||
import TGLogger from "@utils/TGLogger.js";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
|
||||
import THomeCard from "./ph-comp-card.vue";
|
||||
import PhDailyNoteItem from "./ph-daily-note-item.vue";
|
||||
import PhUserSwitch from "./ph-user-switch.vue";
|
||||
|
||||
type DailyNoteAccount = {
|
||||
account: TGApp.Sqlite.Account.Game;
|
||||
data?: TGApp.Game.DailyNote.DnRes;
|
||||
};
|
||||
|
||||
type TDailyNoteEmits = {
|
||||
(e: "success"): void;
|
||||
(e: "delete", gameUid: string): void;
|
||||
};
|
||||
|
||||
const emits = defineEmits<TDailyNoteEmits>();
|
||||
const { cookie, uid, briefInfo } = storeToRefs(useUserStore());
|
||||
const { isLogin } = storeToRefs(useAppStore());
|
||||
|
||||
const loading = ref<boolean>(false);
|
||||
const loadingProgress = ref<number>(0);
|
||||
const loadingText = ref<string>("");
|
||||
const gameAccounts = ref<Array<TGApp.Sqlite.Account.Game>>([]);
|
||||
const dailyNoteAccounts = ref<Array<DailyNoteAccount>>([]);
|
||||
|
||||
watch(
|
||||
() => uid.value,
|
||||
async () => await loadData(),
|
||||
);
|
||||
|
||||
onMounted(async () => await loadData());
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
if (!isLogin.value || uid.value === undefined || !cookie.value) {
|
||||
gameAccounts.value = [];
|
||||
dailyNoteAccounts.value = [];
|
||||
return;
|
||||
}
|
||||
dailyNoteAccounts.value = [];
|
||||
try {
|
||||
const accounts = await TSUserAccount.game.getAccount(uid.value);
|
||||
const genshinAccounts = accounts.filter((ac) => ac.gameBiz === "hk4e_cn");
|
||||
gameAccounts.value = genshinAccounts;
|
||||
if (genshinAccounts.length === 0) {
|
||||
await TGLogger.Warn("[Daily Note Card] No Genshin Impact accounts found");
|
||||
emits("success");
|
||||
return;
|
||||
}
|
||||
emits("success");
|
||||
loading.value = true;
|
||||
loadingProgress.value = 0;
|
||||
for (let i = 0; i < genshinAccounts.length; i++) {
|
||||
const account = genshinAccounts[i];
|
||||
loadingText.value = `正在加载 ${account.gameBiz} - ${account.regionName} - ${account.gameUid}...`;
|
||||
loadingProgress.value = (i / genshinAccounts.length) * 100;
|
||||
let data: TGApp.Game.DailyNote.DnRes | undefined;
|
||||
try {
|
||||
const dataResp = await recordReq.daily(cookie.value, account);
|
||||
console.debug("dailyResp", account, dataResp);
|
||||
if (dataResp.retcode !== 0) {
|
||||
await TGLogger.Error(
|
||||
`[Daily Note Card] Failed to get daily note for ${account.gameBiz}: ${dataResp.message}`,
|
||||
);
|
||||
} else data = dataResp.data;
|
||||
} catch (error) {
|
||||
await TGLogger.Error(
|
||||
`[Daily Note Card] Error loading data for ${account.gameBiz}: ${error}`,
|
||||
);
|
||||
}
|
||||
dailyNoteAccounts.value.push({ account, data });
|
||||
}
|
||||
} catch (error) {
|
||||
await TGLogger.Error(`[Daily Note Card] Error loading data: ${error}`);
|
||||
} finally {
|
||||
loadingProgress.value = 100;
|
||||
loadingText.value = "加载完成";
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 200));
|
||||
loading.value = false;
|
||||
loadingProgress.value = 0;
|
||||
loadingText.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUserSwitch(newUid: string): Promise<void> {
|
||||
await TGLogger.Info(`[Daily Note Card] User switched to ${newUid}`);
|
||||
}
|
||||
|
||||
async function handleRefresh(account: TGApp.Sqlite.Account.Game): Promise<void> {
|
||||
try {
|
||||
const dataResp = await recordReq.daily(cookie.value!, account);
|
||||
if (dataResp.retcode !== 0) {
|
||||
await TGLogger.Error(`[Daily Note Card] Refresh failed: ${dataResp.message}`);
|
||||
showSnackbar.error("刷新失败");
|
||||
return;
|
||||
}
|
||||
const item = dailyNoteAccounts.value.find(
|
||||
(i) => i.account.gameUid === account.gameUid && i.account.gameBiz === account.gameBiz,
|
||||
);
|
||||
if (item) {
|
||||
item.data = dataResp.data;
|
||||
}
|
||||
showSnackbar.success("刷新成功");
|
||||
} catch (error) {
|
||||
await TGLogger.Error(`[Daily Note Card] Refresh error: ${error}`);
|
||||
showSnackbar.error("刷新失败");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.dn-not-login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--box-text-1);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.dn-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: var(--box-text-2);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dn-container {
|
||||
position: relative;
|
||||
display: grid;
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
</style>
|
||||
80
src/components/pageHome/ph-daily-note-boss.vue
Normal file
80
src/components/pageHome/ph-daily-note-boss.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<!-- 周本 BOSS 显示组件 -->
|
||||
<template>
|
||||
<div class="ph-dnb-box">
|
||||
<div class="pdb-boss-icon">
|
||||
<img alt="周本 BOSS" src="/UI/daily/domain.webp" />
|
||||
</div>
|
||||
<div class="pdb-boss-info">
|
||||
<div class="pdb-boss-title">周本 BOSS</div>
|
||||
<div class="pdb-boss-desc">剩余减半次数 {{ current }}/{{ max }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
type PhDailyNoteBossProps = {
|
||||
remainResinDiscountNum?: number;
|
||||
resinDiscountNumLimit?: number;
|
||||
};
|
||||
|
||||
const props = defineProps<PhDailyNoteBossProps>();
|
||||
|
||||
const current = computed((): number => {
|
||||
return props.remainResinDiscountNum ?? 0;
|
||||
});
|
||||
|
||||
const max = computed((): number => {
|
||||
return props.resinDiscountNumLimit ?? 0;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ph-dnb-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pdb-boss-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.pdb-boss-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pdb-boss-title {
|
||||
font-family: var(--font-title);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-boss-desc {
|
||||
overflow: hidden;
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
134
src/components/pageHome/ph-daily-note-coin.vue
Normal file
134
src/components/pageHome/ph-daily-note-coin.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<!-- 洞天宝钱显示组件 -->
|
||||
<template>
|
||||
<div class="ph-dnc-box">
|
||||
<div class="pdb-coin-icon">
|
||||
<img alt="洞天宝钱" src="/UI/daily/coin.webp" />
|
||||
</div>
|
||||
<div class="pdb-coin-info">
|
||||
<div class="pdb-coin-title">
|
||||
<span>洞天宝钱</span>
|
||||
<span>{{ current }}/{{ max }}</span>
|
||||
</div>
|
||||
<div class="pdb-coin-desc">
|
||||
{{ formattedTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
import { stamp2LastTime } from "@utils/toolFunc.js";
|
||||
|
||||
type PhDailyNoteCoinProps = {
|
||||
currentCoin: number;
|
||||
maxCoin: number;
|
||||
recoveryTime: string;
|
||||
};
|
||||
|
||||
const props = defineProps<PhDailyNoteCoinProps>();
|
||||
|
||||
const current = ref<number>(0);
|
||||
const max = ref<number>(0);
|
||||
const remainedTime = ref<number>(0);
|
||||
const formattedTime = ref<string>("");
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
initTime();
|
||||
startTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTimer();
|
||||
});
|
||||
|
||||
function initTime(): void {
|
||||
current.value = props.currentCoin || 0;
|
||||
max.value = props.maxCoin || 0;
|
||||
const time = props.recoveryTime;
|
||||
remainedTime.value = typeof time === "string" ? parseInt(time) : time || 0;
|
||||
updateFormattedTime();
|
||||
}
|
||||
|
||||
function startTimer(): void {
|
||||
if (remainedTime.value <= 0) return;
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (remainedTime.value > 0) {
|
||||
remainedTime.value -= 1;
|
||||
updateFormattedTime();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTimer(): void {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFormattedTime(): void {
|
||||
if (remainedTime.value <= 0) {
|
||||
formattedTime.value = "已存满";
|
||||
return;
|
||||
}
|
||||
|
||||
formattedTime.value = stamp2LastTime(remainedTime.value * 1000);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ph-dnc-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pdb-coin-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.pdb-coin-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pdb-coin-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-family: var(--font-title);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-coin-desc {
|
||||
overflow: hidden;
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
151
src/components/pageHome/ph-daily-note-expedition.vue
Normal file
151
src/components/pageHome/ph-daily-note-expedition.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<!-- 探索派遣单项 -->
|
||||
<template>
|
||||
<div class="dni-exp-item">
|
||||
<div class="dni-exp-icon">
|
||||
<img :src="expedition.avatar_side_icon" alt="角色" />
|
||||
</div>
|
||||
<div class="dni-exp-info">
|
||||
<div v-if="isFinished" class="dni-exp-status finished">已完成</div>
|
||||
<div v-else class="dni-exp-info-content">
|
||||
<div class="dni-exp-status">派遣中</div>
|
||||
<div class="dni-exp-time">{{ formattedTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from "vue";
|
||||
|
||||
import { stamp2LastTime } from "@utils/toolFunc.js";
|
||||
|
||||
type PhDailyNoteExpeditionProps = {
|
||||
expedition: TGApp.Game.DailyNote.Expedition;
|
||||
};
|
||||
|
||||
const props = defineProps<PhDailyNoteExpeditionProps>();
|
||||
|
||||
const remainedTime = ref<number>(0);
|
||||
const isFinished = ref<boolean>(false);
|
||||
const formattedTime = ref<string>("");
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
initTime();
|
||||
startTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTimer();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.expedition.remained_time,
|
||||
() => {
|
||||
initTime();
|
||||
stopTimer();
|
||||
startTimer();
|
||||
},
|
||||
);
|
||||
|
||||
function initTime(): void {
|
||||
const time = props.expedition.remained_time;
|
||||
remainedTime.value = typeof time === "string" ? parseInt(time) : time;
|
||||
isFinished.value = props.expedition.status === "Finished" || remainedTime.value <= 0;
|
||||
updateFormattedTime();
|
||||
}
|
||||
|
||||
function startTimer(): void {
|
||||
if (isFinished.value) return;
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (remainedTime.value > 0) {
|
||||
remainedTime.value -= 1;
|
||||
updateFormattedTime();
|
||||
|
||||
if (remainedTime.value <= 0) {
|
||||
isFinished.value = true;
|
||||
stopTimer();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTimer(): void {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFormattedTime(): void {
|
||||
if (remainedTime.value <= 0) {
|
||||
formattedTime.value = "0 小时 0 分钟 0 秒";
|
||||
return;
|
||||
}
|
||||
|
||||
formattedTime.value = stamp2LastTime(remainedTime.value * 1000);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.dni-exp-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
padding-right: 12px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(to right, var(--box-bg-2), transparent 120%);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dni-exp-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: var(--box-bg-4);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.dni-exp-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dni-exp-info-content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dni-exp-status {
|
||||
overflow: hidden;
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.finished {
|
||||
color: var(--tgc-od-green);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.dni-exp-time {
|
||||
overflow: hidden;
|
||||
color: var(--box-text-1);
|
||||
font-size: 10px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
248
src/components/pageHome/ph-daily-note-item.vue
Normal file
248
src/components/pageHome/ph-daily-note-item.vue
Normal file
@@ -0,0 +1,248 @@
|
||||
<!-- 实时便笺单项 -->
|
||||
<template>
|
||||
<div class="dni-container">
|
||||
<div class="dni-header">
|
||||
<div class="dni-header-title">
|
||||
<span>{{ props.account.nickname }}</span>
|
||||
<v-icon
|
||||
:size="16"
|
||||
color="var(--tgc-od-orange)"
|
||||
icon="mdi-refresh"
|
||||
variant="elevated"
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
</div>
|
||||
<div class="dni-header-append">
|
||||
<span>{{ props.account.gameUid }}</span>
|
||||
<span>{{ props.account.regionName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.data" class="dni-content">
|
||||
<div class="dni-grid">
|
||||
<div class="dni-row">
|
||||
<PhDailyNoteResin
|
||||
:current-resin="props.data.current_resin"
|
||||
:max-resin="props.data.max_resin"
|
||||
:recovery-time="props.data.resin_recovery_time"
|
||||
/>
|
||||
<PhDailyNoteCoin
|
||||
:current-coin="props.data.current_home_coin"
|
||||
:max-coin="props.data.max_home_coin"
|
||||
:recovery-time="props.data.home_coin_recovery_time"
|
||||
/>
|
||||
<PhDailyNoteTransformer :trans="props.data.transformer" />
|
||||
</div>
|
||||
<div class="dni-row">
|
||||
<PhDailyNoteQuest :quest="props.data.archon_quest_progress" />
|
||||
<PhDailyNoteTask :task="props.data.daily_task" />
|
||||
<PhDailyNoteBoss
|
||||
:remain-resin-discount-num="props.data.remain_resin_discount_num"
|
||||
:resin-discount-num-limit="props.data.resin_discount_num_limit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dni-exp-header">
|
||||
<span class="dni-exp-title">探索派遣</span>
|
||||
<span class="dni-exp-count">
|
||||
{{ props.data.current_expedition_num }}/{{ props.data.max_expedition_num }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dni-exp-grid">
|
||||
<PhDailyNoteExpedition
|
||||
v-for="expedition in props.data.expeditions"
|
||||
:key="expedition.avatar_side_icon"
|
||||
:expedition="expedition"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import PhDailyNoteExpedition from "./ph-daily-note-expedition.vue";
|
||||
import PhDailyNoteResin from "./ph-daily-note-resin.vue";
|
||||
import PhDailyNoteCoin from "./ph-daily-note-coin.vue";
|
||||
import PhDailyNoteTransformer from "./ph-daily-note-transformer.vue";
|
||||
import PhDailyNoteTask from "./ph-daily-note-task.vue";
|
||||
import PhDailyNoteQuest from "./ph-daily-note-quest.vue";
|
||||
import PhDailyNoteBoss from "./ph-daily-note-boss.vue";
|
||||
|
||||
type PhDailyNoteItemProps = {
|
||||
account: TGApp.Sqlite.Account.Game;
|
||||
data?: TGApp.Game.DailyNote.DnRes;
|
||||
};
|
||||
|
||||
type TDailyNoteItemEmits = {
|
||||
(e: "refresh"): void;
|
||||
};
|
||||
|
||||
const emits = defineEmits<TDailyNoteItemEmits>();
|
||||
const props = defineProps<PhDailyNoteItemProps>();
|
||||
|
||||
function handleRefresh(): void {
|
||||
emits("refresh");
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.dni-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-1);
|
||||
color: var(--box-text-1);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dni-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid var(--common-shadow-1);
|
||||
}
|
||||
|
||||
.dni-header-title {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: 4px;
|
||||
font-family: var(--font-title);
|
||||
}
|
||||
|
||||
.dni-header-append {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dni-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dni-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dni-row {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.dni-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dni-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.dni-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.dni-title {
|
||||
font-family: var(--font-title);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dni-desc {
|
||||
overflow: hidden;
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dni-value {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-3);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dni-exp-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 4px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dni-exp-title {
|
||||
color: var(--box-text-1);
|
||||
font-family: var(--font-title);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dni-exp-count {
|
||||
color: var(--box-text-2);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.dni-exp-grid {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.dni-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.dni-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
110
src/components/pageHome/ph-daily-note-quest.vue
Normal file
110
src/components/pageHome/ph-daily-note-quest.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<!-- 魔神任务显示组件 -->
|
||||
<template>
|
||||
<div class="ph-dnq-box">
|
||||
<div class="pdb-quest-icon">
|
||||
<img alt="魔神任务" src="/UI/daily/mission.webp" />
|
||||
</div>
|
||||
<div class="pdb-quest-info">
|
||||
<div class="pdb-quest-title">
|
||||
<span>魔神任务</span>
|
||||
<span>
|
||||
{{
|
||||
props.quest.is_finish_all_mainline && props.quest.is_finish_all_interchapter
|
||||
? "已完成"
|
||||
: `未完成:${props.quest.list.length}`
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pdb-quest-desc">
|
||||
<span v-for="q in props.quest.list" :key="q.id">
|
||||
{{ q.chapter_num }} {{ q.chapter_title }} {{ getQuestStatus(q.status) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
type PhDailyNoteQuestProps = { quest: TGApp.Game.DailyNote.ArchonQuestProgress };
|
||||
|
||||
const props = defineProps<PhDailyNoteQuestProps>();
|
||||
|
||||
function getQuestStatus(stat: string): string {
|
||||
switch (stat) {
|
||||
case "StatusNotOpen":
|
||||
return "未开启";
|
||||
default:
|
||||
return stat;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ph-dnq-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pdb-quest-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.pdb-quest-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.pdb-quest-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-family: var(--font-title);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-quest-desc {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-quest-value {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-3);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
142
src/components/pageHome/ph-daily-note-resin.vue
Normal file
142
src/components/pageHome/ph-daily-note-resin.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<!-- 原粹树脂显示组件 -->
|
||||
<template>
|
||||
<div class="ph-dnr-box">
|
||||
<div class="pdb-resin-icon">
|
||||
<img alt="原粹树脂" src="/UI/daily/resin.webp" />
|
||||
</div>
|
||||
<div class="pdb-resin-info">
|
||||
<div class="pdb-resin-title">
|
||||
<span>原粹树脂</span>
|
||||
<span>{{ current }}/{{ max }}</span>
|
||||
</div>
|
||||
<div class="pdb-resin-desc">
|
||||
{{ formattedTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
import { stamp2LastTime } from "@utils/toolFunc.js";
|
||||
|
||||
type PhDailyNoteResinProps = {
|
||||
currentResin: number;
|
||||
maxResin: number;
|
||||
recoveryTime: string;
|
||||
};
|
||||
|
||||
const props = defineProps<PhDailyNoteResinProps>();
|
||||
|
||||
const current = ref<number>(0);
|
||||
const max = ref<number>(0);
|
||||
const remainedTime = ref<number>(0);
|
||||
const formattedTime = ref<string>("");
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
initTime();
|
||||
startTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTimer();
|
||||
});
|
||||
|
||||
function initTime(): void {
|
||||
current.value = props.currentResin || 0;
|
||||
max.value = props.maxResin || 0;
|
||||
const time = props.recoveryTime;
|
||||
remainedTime.value = typeof time === "string" ? parseInt(time) : time || 0;
|
||||
updateFormattedTime();
|
||||
}
|
||||
|
||||
function startTimer(): void {
|
||||
if (remainedTime.value <= 0) return;
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (remainedTime.value > 0) {
|
||||
remainedTime.value -= 1;
|
||||
updateFormattedTime();
|
||||
// 每 9 分钟恢复 1 点树脂
|
||||
const totalSecondsPassed =
|
||||
(props.recoveryTime ? parseInt(props.recoveryTime) : 0) - remainedTime.value;
|
||||
const resinToRecover = Math.floor(totalSecondsPassed / 540); // 540 秒 = 9 分钟
|
||||
const newCurrent = Math.min(current.value + resinToRecover, max.value);
|
||||
if (newCurrent !== current.value) {
|
||||
current.value = newCurrent;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTimer(): void {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFormattedTime(): void {
|
||||
if (remainedTime.value <= 0) {
|
||||
formattedTime.value = "已恢复满";
|
||||
return;
|
||||
}
|
||||
|
||||
formattedTime.value = stamp2LastTime(remainedTime.value * 1000);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ph-dnr-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pdb-resin-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.pdb-resin-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pdb-resin-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-family: var(--font-title);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-resin-desc {
|
||||
overflow: hidden;
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
228
src/components/pageHome/ph-daily-note-task.vue
Normal file
228
src/components/pageHome/ph-daily-note-task.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<!-- 每日委托显示组件 -->
|
||||
<template>
|
||||
<div class="ph-dnta-box">
|
||||
<div class="pdb-task-icon">
|
||||
<img alt="每日委托" src="/UI/daily/task.webp" />
|
||||
</div>
|
||||
<div class="pdb-task-info">
|
||||
<div class="pdb-task-title-row">
|
||||
<div class="pdb-task-title">
|
||||
<span>每日委托</span>
|
||||
<span>{{ taskStatus }}</span>
|
||||
</div>
|
||||
<div v-if="task?.stored_attendance" class="pdb-task-stored" title="长效历练点">
|
||||
<v-icon color="var(--tgc-od-red)" size="12">mdi-circle</v-icon>
|
||||
<span>{{ task.stored_attendance }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdb-task-task-content">
|
||||
<div class="pdb-task-task-row">
|
||||
<span class="pdb-task-task-label">日常</span>
|
||||
<template v-for="i in props.task?.task_rewards" :key="i">
|
||||
<v-icon
|
||||
v-if="i.status === 'TaskRewardStatusFinished'"
|
||||
color="var(--tgc-od-green)"
|
||||
size="14"
|
||||
>
|
||||
mdi-check
|
||||
</v-icon>
|
||||
<v-icon v-else color="var(--tgc-od-white)" size="14"> mdi-square-outline</v-icon>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="attendanceVisible" class="pdb-task-task-row">
|
||||
<span class="pdb-task-task-label">历练</span>
|
||||
<div class="pdb-task-attendance-list">
|
||||
<div
|
||||
v-for="reward in attendanceRewards"
|
||||
:key="reward.progress"
|
||||
class="pdb-task-attendance-item"
|
||||
>
|
||||
<v-progress-circular
|
||||
:model-value="(Math.min(reward.progress, 2000) / 2000) * 100"
|
||||
:size="14"
|
||||
:width="2"
|
||||
class="pdb-task-attendance-progress"
|
||||
color="var(--tgc-od-orange)"
|
||||
/>
|
||||
<span
|
||||
v-if="reward.status !== 'AttendanceRewardStatusUnfinished'"
|
||||
class="pdb-task-attendance-icon"
|
||||
>
|
||||
<v-icon
|
||||
v-if="reward.status === 'AttendanceRewardStatusTakenAward'"
|
||||
color="var(--tgc-od-green)"
|
||||
size="8"
|
||||
>
|
||||
mdi-check
|
||||
</v-icon>
|
||||
<v-icon v-else color="var(--tgc-od-red)" size="8"> mdi-gift </v-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="hasUnclaimedAttendance" class="pdb-task-task-warn"> 可领取 </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pdb-task-value"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
type PhDailyNoteTaskProps = {
|
||||
task?: TGApp.Game.DailyNote.DailyTask;
|
||||
};
|
||||
|
||||
const props = defineProps<PhDailyNoteTaskProps>();
|
||||
|
||||
const attendanceVisible = computed((): boolean => {
|
||||
return props.task?.attendance_visible ?? false;
|
||||
});
|
||||
|
||||
const attendanceRewards = computed((): Array<TGApp.Game.DailyNote.AttendanceReward> => {
|
||||
return props.task?.attendance_rewards ?? [];
|
||||
});
|
||||
|
||||
const hasUnclaimedAttendance = computed((): boolean => {
|
||||
return (
|
||||
props.task?.attendance_rewards?.some(
|
||||
(r) => r.progress >= 2000 && r.status !== "AttendanceRewardStatusTakenAward",
|
||||
) ?? false
|
||||
);
|
||||
});
|
||||
|
||||
const taskStatus = computed((): string => {
|
||||
if (!props.task) return "";
|
||||
const { finished_num, total_num } = props.task;
|
||||
if (finished_num === total_num) {
|
||||
if (props.task.is_extra_task_reward_received) return "已完成";
|
||||
return `${finished_num}/${total_num} 可领取`;
|
||||
}
|
||||
return `${finished_num}/${total_num}`;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ph-dnta-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 64px;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pdb-task-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.pdb-task-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.pdb-task-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pdb-task-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: 4px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-task-task-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.pdb-task-task-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pdb-task-task-label {
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-task-task-warn {
|
||||
color: var(--tgc-od-orange);
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-task-attendance-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
}
|
||||
|
||||
.pdb-task-attendance-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdb-task-attendance-progress {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.pdb-task-attendance-icon {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdb-task-attendance-progress-text {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
color: var(--box-text-1);
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-task-stored {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
gap: 2px;
|
||||
}
|
||||
</style>
|
||||
152
src/components/pageHome/ph-daily-note-transformer.vue
Normal file
152
src/components/pageHome/ph-daily-note-transformer.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<!-- 参量质变仪显示组件 -->
|
||||
<template>
|
||||
<div class="ph-dnt-box">
|
||||
<div class="pdb-trans-icon">
|
||||
<img alt="参量质变仪" src="/UI/daily/trans.webp" />
|
||||
</div>
|
||||
<div class="pdb-trans-info">
|
||||
<div class="pdb-trans-title">参量质变仪</div>
|
||||
<div class="pdb-trans-desc">
|
||||
{{ valueText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
|
||||
import { stamp2LastTime } from "@utils/toolFunc.js";
|
||||
|
||||
type PhDailyNoteTransformerProps = {
|
||||
trans: TGApp.Game.DailyNote.TransformerData;
|
||||
};
|
||||
|
||||
const props = defineProps<PhDailyNoteTransformerProps>();
|
||||
|
||||
const remainedTime = ref<number>(0);
|
||||
const formattedTime = ref<string>("");
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const valueText = computed((): string => {
|
||||
if (!props.trans.obtained && !props.trans.recovery_time.reached) {
|
||||
return "恢复中";
|
||||
}
|
||||
if (props.trans.obtained || props.trans.recovery_time.reached) {
|
||||
return "可使用";
|
||||
}
|
||||
return formattedTime.value;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initTime();
|
||||
startTimer();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopTimer();
|
||||
});
|
||||
|
||||
function initTime(): void {
|
||||
const time = props.trans.recovery_time;
|
||||
if (time) {
|
||||
const hours = time.hour || 0;
|
||||
const minutes = time.minute || 0;
|
||||
const seconds = time.second || 0;
|
||||
remainedTime.value = hours * 3600 + minutes * 60 + seconds;
|
||||
} else {
|
||||
remainedTime.value = 0;
|
||||
}
|
||||
updateFormattedTime();
|
||||
}
|
||||
|
||||
function startTimer(): void {
|
||||
if (remainedTime.value <= 0 || props.trans.obtained || props.trans.recovery_time.reached) return;
|
||||
|
||||
timer = setInterval(() => {
|
||||
if (remainedTime.value > 0) {
|
||||
remainedTime.value -= 1;
|
||||
updateFormattedTime();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopTimer(): void {
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateFormattedTime(): void {
|
||||
if (remainedTime.value <= 0) {
|
||||
formattedTime.value = "0 小时 0 分钟 0 秒";
|
||||
return;
|
||||
}
|
||||
|
||||
formattedTime.value = stamp2LastTime(remainedTime.value * 1000);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ph-dnt-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pdb-trans-icon {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.pdb-trans-info {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pdb-trans-title {
|
||||
font-family: var(--font-title);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-trans-desc {
|
||||
overflow: hidden;
|
||||
color: var(--box-text-2);
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pdb-trans-value {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-3);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user