mirror of
https://github.com/BTMuli/TeyvatGuide.git
synced 2026-04-03 06:55:06 +08:00
✨首页添加游戏签到组件,支持补签 (#182)
* Initial plan * Add sign-in card component to homepage Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Fix v-icon usage in sign-in card buttons Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Fix: Merge new home components into existing localStorage Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Refactor: Remove redundant code in getShowItems Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Add error handling for localStorage parsing Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Redesign sign-in card to show multiple game accounts with compact UI Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Fix reward item spacing with flex properties Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Redesign sign-in card: grid layout, account switching, light mode fixes Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Add forceReload parameter to prevent unnecessary API calls Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Show multiple game accounts with grid layout and MiHoYo account in append Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Use unique keys and concurrent API requests for better performance Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Refactor sign-in component: split into reusable parts with user switcher Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Fix sign-in logic: highlight based on count not date, enable resign button Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Remove duplicate code and fix comment Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Add reward cell component, extra rewards support, and improved visual distinction Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * 🏷️ 添加Store类型,调整首页选项框宽度 * 🏷️签到 → 游戏签到 * ♻️ 首页组件重构,支持组件分享 * ♻️ 调整组件UI * 💄 调整loading标题文本大小 * 💄 微调样式 * Refactor sign-in component: sequential loading, progress bar, internal data processing Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * Fix code review issues: correct gameInfo reference and add refresh event Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * 🎨 调整逻辑 * Refactor: self-contained data updates and numeric state enums Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * 🚸 调整排序逻辑 * 🚸 调整loading文本 * ✏️ 调整类型,修复打包失败 * ♻️ 添加补签相关请求 * 💄 调整脚本页布局 * 💄 调整首页布局 * Implement resign feature with local data updates and confirmation dialog Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * ✏️ 补充类型描述 * Fix resign logic: check is_sub, optimize resign info loading, improve success message Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * 🎨 调整逻辑,微调样式 * 🎨 调整唤起位置 * Add click handlers to reward cells for sign-in and resign actions Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * 🐛 对ID进行限制 * 💄 微调顶部gameNav样式 * Fix resign logic: only first missed day is clickable Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> * 🎨 放宽补签限制 * 🐛 修正删除逻辑判断 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: BTMuli <72692909+BTMuli@users.noreply.github.com> Co-authored-by: BTMuli <bt-muli@outlook.com>
This commit is contained in:
@@ -188,6 +188,7 @@ async function toBBS(link: URL): Promise<void> {
|
||||
color: var(--common-text-title);
|
||||
font-family: var(--font-title);
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover span {
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
<!-- Loading 组件 -->
|
||||
<template>
|
||||
<transition name="func-loading">
|
||||
<div v-show="showBox || showOuter" class="loading-overlay">
|
||||
<transition name="func-loading-inner">
|
||||
<div v-show="showInner" class="loading-container">
|
||||
<div class="loading-box">
|
||||
<!-- TODO: 适配长title -->
|
||||
<div class="loading-title">
|
||||
<span>{{ data.title }}</span>
|
||||
<div class="loading-circle" v-show="!data.empty">
|
||||
<div v-show="!data.empty" class="loading-circle">
|
||||
<div />
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-subtitle" v-show="data.subtitle && data.subtitle !== ''">
|
||||
<div v-show="data.subtitle && data.subtitle !== ''" class="loading-subtitle">
|
||||
{{ data.subtitle }}
|
||||
</div>
|
||||
<div class="loading-img">
|
||||
<img v-if="!data.empty" src="/source/UI/loading.webp" alt="loading" />
|
||||
<img v-else src="/source/UI/empty.webp" alt="empty" />
|
||||
<img v-if="!data.empty" alt="loading" src="/source/UI/loading.webp" />
|
||||
<img v-else alt="empty" src="/source/UI/empty.webp" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,12 +152,12 @@ defineExpose({ displayBox });
|
||||
.loading-title {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: 5px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 2rem;
|
||||
font-size: 1.75rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.loading-subtitle {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<!-- 首页今日素材组件 -->
|
||||
<template>
|
||||
<THomeCard append>
|
||||
<template #title>今日素材 {{ dateNow }}</template>
|
||||
<THomeCard :title="`今日素材 ${dateNow}`" append>
|
||||
<template #title-append>
|
||||
<v-switch class="tc-switch" @change="switchType()" />
|
||||
<span>{{ selectedType === "character" ? "角色" : "武器" }}</span>
|
||||
@@ -11,15 +11,15 @@
|
||||
<v-btn
|
||||
v-for="text of btnText"
|
||||
:key="text.week"
|
||||
rounded
|
||||
class="tc-btn"
|
||||
:class="{ selected: text.week === btnNow, today: text.week === weekNow }"
|
||||
class="tc-btn"
|
||||
rounded
|
||||
@click="switchDay(text.week)"
|
||||
>
|
||||
{{ text.text }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-pagination class="tc-page" v-model="page" :total-visible="9" :length="length" />
|
||||
<v-pagination v-model="page" :length="length" :total-visible="9" class="tc-page" />
|
||||
</div>
|
||||
<div class="tc-content">
|
||||
<TCalendarBirth />
|
||||
@@ -27,8 +27,8 @@
|
||||
<TItemBox
|
||||
v-for="item in renderItems"
|
||||
:key="item.id"
|
||||
@click="selectItem(item)"
|
||||
:model-value="getBoxData(item)"
|
||||
@click="selectItem(item)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,7 +176,7 @@ function getBoxData(item: TGApp.App.Calendar.Item): TItemBoxData {
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
height: 100%;
|
||||
grid-gap: 8px;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(8, 100px);
|
||||
place-items: flex-start flex-start;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="thc-container">
|
||||
<div class="thc-title">
|
||||
<slot name="title" />
|
||||
<div ref="thcRef" class="thc-container">
|
||||
<div class="thc-title" title="点击生成分享" @click="share()">
|
||||
<slot name="title">{{ props.title }}</slot>
|
||||
</div>
|
||||
<div v-if="append" class="thc-append">
|
||||
<slot name="title-append" />
|
||||
@@ -12,7 +12,25 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
defineProps<{ append?: boolean }>();
|
||||
import { generateShareImg } from "@utils/TGShare.js";
|
||||
import { useTemplateRef } from "vue";
|
||||
|
||||
/** 首页组件参数 */
|
||||
type PhCompCardProps = {
|
||||
/** 标题 */
|
||||
title?: string;
|
||||
/** 是否显示append */
|
||||
append?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<PhCompCardProps>();
|
||||
|
||||
const thcEl = useTemplateRef<HTMLDivElement>("thcRef");
|
||||
|
||||
async function share(): Promise<void> {
|
||||
if (!thcEl.value) return;
|
||||
await generateShareImg(`HomeComp_${props.title}`, thcEl.value);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@styles/github.styles.scss" as github-styles;
|
||||
@@ -51,6 +69,7 @@ defineProps<{ append?: boolean }>();
|
||||
.thc-title {
|
||||
left: 8px;
|
||||
color: var(--tgc-white-1);
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<!-- 首页限时祈愿组件 -->
|
||||
<template>
|
||||
<THomeCard :append="false">
|
||||
<template #title>限时祈愿</template>
|
||||
<THomeCard :append="false" title="限时祈愿">
|
||||
<template #default>
|
||||
<div class="pool-grid" v-if="pools.length < 3">
|
||||
<div v-if="pools.length < 3" class="pool-grid">
|
||||
<PhPoolCard v-for="(pool, idx) in pools" :key="idx" :pool="pool" />
|
||||
</div>
|
||||
<!-- TODO: 优化Swiper效果 -->
|
||||
<Swiper
|
||||
v-else
|
||||
:autoplay="{ delay: 3000, disableOnInteraction: false }"
|
||||
:centered-slides="true"
|
||||
:loop="true"
|
||||
:modules="swiperModules"
|
||||
:navigation="true"
|
||||
:slides-per-view="2"
|
||||
:space-between="12"
|
||||
:loop="true"
|
||||
:centered-slides="true"
|
||||
:navigation="true"
|
||||
:autoplay="{ delay: 3000, disableOnInteraction: false }"
|
||||
:modules="swiperModules"
|
||||
class="pool-swiper"
|
||||
>
|
||||
<SwiperSlide v-for="(pool, idx) in pools" :key="idx">
|
||||
@@ -33,7 +33,7 @@ import showSnackbar from "@comp/func/snackbar.js";
|
||||
import PhPoolCard from "@comp/pageHome/ph-pool-card.vue";
|
||||
import takumiReq from "@req/takumiReq.js";
|
||||
import TGLogger from "@utils/TGLogger.js";
|
||||
import { Autoplay, A11y } from "swiper/modules";
|
||||
import { A11y, Autoplay } from "swiper/modules";
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import { onMounted, shallowRef } from "vue";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<!-- 首页近期活动组件 -->
|
||||
<template>
|
||||
<THomeCard :append="isLogin">
|
||||
<template #title>近期活动</template>
|
||||
<THomeCard :append="isLogin" title="近期活动">
|
||||
<template v-if="isLogin" #title-append>
|
||||
<v-switch v-model="isUserPos" class="tp-switch"></v-switch>
|
||||
<span>{{ isUserPos ? "用户" : "百科" }}</span>
|
||||
@@ -63,6 +62,16 @@ watch(
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => account.value.gameUid,
|
||||
async () => {
|
||||
if (isUserPos.value && isInit.value) {
|
||||
userPos.value = [];
|
||||
await loadUserPosition(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
if (isLogin.value) await loadUserPosition();
|
||||
else await loadWikiPosition();
|
||||
@@ -70,8 +79,8 @@ onMounted(async () => {
|
||||
isInit.value = true;
|
||||
});
|
||||
|
||||
async function loadUserPosition(): Promise<void> {
|
||||
if (userPos.value.length > 0) return;
|
||||
async function loadUserPosition(forceReload: boolean = false): Promise<void> {
|
||||
if (userPos.value.length > 0 && !forceReload) return;
|
||||
if (!cookie.value) {
|
||||
showSnackbar.warn("获取近期活动失败:未登录");
|
||||
isLogin.value = false;
|
||||
|
||||
184
src/components/pageHome/ph-comp-sign.vue
Normal file
184
src/components/pageHome/ph-comp-sign.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<!-- 首页签到卡片 -->
|
||||
<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="sign-not-login">请先登录</div>
|
||||
<div v-else-if="gameAccounts.length === 0" class="sign-not-login">暂无游戏账户</div>
|
||||
<div v-else-if="loading" class="sign-loading">
|
||||
<div class="loading-content">
|
||||
<v-progress-linear :model-value="loadingProgress" color="primary" height="6" rounded />
|
||||
<div class="loading-text">{{ loadingText }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="sign-container">
|
||||
<PhSignItem
|
||||
v-for="item in signAccounts"
|
||||
:key="`${item.account.gameBiz}_${item.account.gameUid}`"
|
||||
:account="item.account"
|
||||
:info="item.info"
|
||||
:stat="item.stat"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</THomeCard>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import lunaReq from "@req/lunaReq.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 PhSignItem from "./ph-sign-item.vue";
|
||||
import PhUserSwitch from "./ph-user-switch.vue";
|
||||
|
||||
type SignAccount = {
|
||||
account: TGApp.Sqlite.Account.Game;
|
||||
info?: TGApp.BBS.Sign.HomeRes;
|
||||
stat?: TGApp.BBS.Sign.InfoRes;
|
||||
};
|
||||
|
||||
type TSignEmits = {
|
||||
(e: "success"): void;
|
||||
(e: "delete", gameUid: string): void;
|
||||
};
|
||||
|
||||
const emits = defineEmits<TSignEmits>();
|
||||
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<TGApp.Sqlite.Account.Game[]>([]);
|
||||
const signAccounts = ref<SignAccount[]>([]);
|
||||
|
||||
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 = [];
|
||||
signAccounts.value = [];
|
||||
return;
|
||||
}
|
||||
signAccounts.value = [];
|
||||
try {
|
||||
const accounts = await TSUserAccount.game.getAccount(uid.value);
|
||||
gameAccounts.value = accounts;
|
||||
if (accounts.length === 0) {
|
||||
await TGLogger.Warn("[Sign Card] No game accounts found");
|
||||
emits("success");
|
||||
return;
|
||||
}
|
||||
emits("success");
|
||||
loading.value = true;
|
||||
loadingProgress.value = 0;
|
||||
const ck = { cookie_token: cookie.value.cookie_token, account_id: cookie.value.account_id };
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
const account = accounts[i];
|
||||
loadingText.value = `正在加载 ${account.gameBiz} - ${account.regionName} - ${account.gameUid}...`;
|
||||
loadingProgress.value = (i / accounts.length) * 100;
|
||||
let info, stat;
|
||||
try {
|
||||
const infoResp = await lunaReq.sign.info(account, ck);
|
||||
if ("retcode" in infoResp) {
|
||||
await TGLogger.Error(
|
||||
`[Sign Card] Failed to get rewards for ${account.gameBiz}: ${infoResp.message}`,
|
||||
);
|
||||
} else info = infoResp;
|
||||
const statResp = await lunaReq.sign.stat(account, ck);
|
||||
if ("retcode" in statResp) {
|
||||
await TGLogger.Error(
|
||||
`[Sign Card] Failed to get status for ${account.gameBiz}: ${statResp.message}`,
|
||||
);
|
||||
} else stat = statResp;
|
||||
} catch (error) {
|
||||
await TGLogger.Error(`[Sign Card] Error loading data for ${account.gameBiz}: ${error}`);
|
||||
}
|
||||
signAccounts.value.push({ account, info, stat });
|
||||
}
|
||||
} catch (error) {
|
||||
await TGLogger.Error(`[Sign 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(`[Sign Card] User switched to ${newUid}`);
|
||||
}
|
||||
|
||||
async function handleDelete(account: TGApp.Sqlite.Account.Game): Promise<void> {
|
||||
try {
|
||||
await TSUserAccount.game.deleteAccount(account);
|
||||
signAccounts.value = signAccounts.value.filter(
|
||||
(item) =>
|
||||
item.account.gameUid !== account.gameUid || item.account.gameBiz !== account.gameBiz,
|
||||
);
|
||||
showSnackbar.success("账号已删除");
|
||||
} catch (error) {
|
||||
await TGLogger.Error(`[Sign Card] Delete account error: ${error}`);
|
||||
showSnackbar.error("删除账号失败");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.sign-not-login {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--box-text-1);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sign-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;
|
||||
}
|
||||
|
||||
.sign-container {
|
||||
display: grid;
|
||||
padding: 8px;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
}
|
||||
</style>
|
||||
606
src/components/pageHome/ph-sign-item.vue
Normal file
606
src/components/pageHome/ph-sign-item.vue
Normal file
@@ -0,0 +1,606 @@
|
||||
<!-- 单个游戏账户签到卡片 -->
|
||||
<template>
|
||||
<div ref="signItemRef" class="ph-si-box">
|
||||
<div class="ph-si-top">
|
||||
<div :title="gameInfo.title" class="ph-sit-icon" @click="shareItem()">
|
||||
<img :alt="gameInfo.title" :src="gameInfo.icon" />
|
||||
</div>
|
||||
<div class="ph-sit-info">
|
||||
<div class="ph-sit-title">
|
||||
<span class="hint">{{ signInfo?.month }}</span>
|
||||
<span>月</span>
|
||||
<span>累计签到</span>
|
||||
<span class="hint">{{ signStat?.total_sign_day ?? 0 }}</span>
|
||||
<span>天</span>
|
||||
<span class="hint">{{ signStat?.is_sign ? "已签到" : "未签到" }}</span>
|
||||
</div>
|
||||
<div class="ph-sit-sub">
|
||||
{{ account.nickname }} - {{ account.gameUid }} ({{ account.regionName }})
|
||||
</div>
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="props.account.gameBiz !== 'hk4e_cn'"
|
||||
class="ph-sit-delete"
|
||||
icon="mdi-delete"
|
||||
size="x-small"
|
||||
@click="tryDelete()"
|
||||
/>
|
||||
</div>
|
||||
<!-- 额外签到奖励部分 -->
|
||||
<div v-if="hasExtraRewards" class="sign-extra-rewards">
|
||||
<div class="extra-header">
|
||||
<v-icon color="orange" size="small">mdi-star</v-icon>
|
||||
<span class="extra-title">额外奖励</span>
|
||||
<span class="extra-days">({{ extraSignedDays }}/{{ extraRewards.length }})</span>
|
||||
<span class="extra-time">{{ extraTimeStr }}</span>
|
||||
</div>
|
||||
<div class="extra-grid">
|
||||
<PhSignRewardCell
|
||||
v-for="(reward, idx) in extraRewards"
|
||||
:key="`extra-${idx}`"
|
||||
:day-number="idx + 1"
|
||||
:is-extra="true"
|
||||
:reward="reward"
|
||||
:state="getExtraRewardState(idx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 签到日历 -->
|
||||
<div v-if="rewards.length > 0" class="sign-rewards">
|
||||
<PhSignRewardCell
|
||||
v-for="(reward, ridx) in rewards"
|
||||
:key="ridx"
|
||||
:day-number="ridx + 1"
|
||||
:is-clickable="getRewardClickable(ridx)"
|
||||
:reward="reward"
|
||||
:state="getRewardState(ridx)"
|
||||
@click="handleRewardCellClick(ridx)"
|
||||
/>
|
||||
</div>
|
||||
<!-- 签到操作 -->
|
||||
<div class="sign-actions" data-html2canvas-ignore>
|
||||
<v-btn
|
||||
:disabled="isTodaySigned || isSign || isResign"
|
||||
:loading="isSign"
|
||||
class="sign-btn"
|
||||
prepend-icon="mdi-calendar-check"
|
||||
size="small"
|
||||
@click="handleSign"
|
||||
>
|
||||
签到
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:disabled="!canResign || isSign || isResign"
|
||||
:loading="isResign"
|
||||
class="sign-btn resign-btn"
|
||||
prepend-icon="mdi-calendar-refresh"
|
||||
size="small"
|
||||
@click="handleResign"
|
||||
>
|
||||
补签{{ resignCount > 0 ? ` (${resignCount})` : "" }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import showDialog from "@comp/func/dialog.js";
|
||||
import showGeetest from "@comp/func/geetest.js";
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import PhSignRewardCell, { RewardStateEnum } from "@comp/pageHome/ph-sign-reward-cell.vue";
|
||||
import lunaReq from "@req/lunaReq.js";
|
||||
import miscReq from "@req/miscReq.js";
|
||||
import useBBSStore from "@store/bbs.js";
|
||||
import useUserStore from "@store/user.js";
|
||||
import TGLogger from "@utils/TGLogger.js";
|
||||
import { generateShareImg } from "@utils/TGShare.js";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, ref, useTemplateRef } from "vue";
|
||||
|
||||
const RewardState = <const>{
|
||||
NORMAL: 0,
|
||||
SIGNED: 1,
|
||||
NEXT_REWARD: 2,
|
||||
MISSED: 3,
|
||||
};
|
||||
|
||||
type SignGameInfo = { title: string; icon: string; gid: number };
|
||||
type PhSignItemProps = {
|
||||
account: TGApp.Sqlite.Account.Game;
|
||||
info?: TGApp.BBS.Sign.HomeRes;
|
||||
stat?: TGApp.BBS.Sign.InfoRes;
|
||||
};
|
||||
type PhSignItemEmits = {
|
||||
(e: "delete", account: TGApp.Sqlite.Account.Game): void;
|
||||
};
|
||||
|
||||
const props = defineProps<PhSignItemProps>();
|
||||
const emits = defineEmits<PhSignItemEmits>();
|
||||
|
||||
const { cookie } = storeToRefs(useUserStore());
|
||||
const { gameList } = storeToRefs(useBBSStore());
|
||||
|
||||
const signItemEl = useTemplateRef<HTMLDivElement>("signItemRef");
|
||||
|
||||
const isSign = ref<boolean>(false);
|
||||
const isResign = ref<boolean>(false);
|
||||
const signInfo = ref<TGApp.BBS.Sign.HomeRes | undefined>(props.info);
|
||||
const signStat = ref<TGApp.BBS.Sign.InfoRes | undefined>(props.stat);
|
||||
const resignInfo = ref<TGApp.BBS.Sign.ResignInfoRes | undefined>(undefined);
|
||||
|
||||
const gameInfo = computed<SignGameInfo>(() => {
|
||||
const biz = props.account.gameBiz;
|
||||
const enName = biz.split("_")[0];
|
||||
if (!enName) return { title: biz, icon: "/platforms/mhy/mys.webp", gid: 0 };
|
||||
const findGame = gameList.value.find((i) => i.op_name === enName);
|
||||
if (findGame) return { title: findGame.name, icon: findGame.app_icon, gid: findGame.id };
|
||||
return { title: biz, icon: "/platforms/mhy/mys.webp", gid: 0 };
|
||||
});
|
||||
const rewards = computed<Array<TGApp.BBS.Sign.HomeAward>>(() => signInfo.value?.awards ?? []);
|
||||
const extraRewards = computed<Array<TGApp.BBS.Sign.HomeAward>>(() => {
|
||||
if (!signInfo.value) return [];
|
||||
if (
|
||||
signInfo.value.short_extra_award.has_extra_award &&
|
||||
signInfo.value.short_extra_award.list.length > 0
|
||||
) {
|
||||
return signInfo.value.short_extra_award.list;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const hasExtraRewards = computed<boolean>(() => extraRewards.value.length > 0);
|
||||
const extraTimeStr = computed<string>(() => {
|
||||
if (!signInfo.value) return "";
|
||||
if (!signInfo.value.short_extra_award.has_extra_award) return "";
|
||||
return `${signInfo.value.short_extra_award.start_time}~${signInfo.value.short_extra_award.end_time}`;
|
||||
});
|
||||
const currentDay = computed<number>(() => new Date().getDate());
|
||||
const totalSignedDays = computed<number>(() => signStat.value?.total_sign_day ?? 0);
|
||||
const extraSignedDays = computed<number>(() => signStat.value?.short_sign_day ?? 0);
|
||||
const isTodaySigned = computed<boolean>(() => signStat.value?.is_sign ?? false);
|
||||
const canResign = computed<boolean>(() => {
|
||||
if (!signStat.value?.is_sign) return false;
|
||||
if (resignInfo.value?.quality_cnt === 0) return false;
|
||||
const missed = currentDay.value - 1 - totalSignedDays.value;
|
||||
return missed > 0 && isTodaySigned.value;
|
||||
});
|
||||
const resignCount = computed<number>(() => resignInfo.value?.quality_cnt ?? 0);
|
||||
|
||||
// Get reward state for regular rewards
|
||||
function getRewardState(index: number): RewardStateEnum {
|
||||
const signedDays = totalSignedDays.value;
|
||||
const today = currentDay.value;
|
||||
// Already signed
|
||||
if (index < signedDays) {
|
||||
return RewardState.SIGNED;
|
||||
}
|
||||
// Next reward to receive
|
||||
if (index === signedDays && !isTodaySigned.value) {
|
||||
return RewardState.NEXT_REWARD;
|
||||
}
|
||||
// Missed days (between signed count and current date)
|
||||
if (index < today - 1 && index >= signedDays) {
|
||||
return RewardState.MISSED;
|
||||
}
|
||||
return RewardState.NORMAL;
|
||||
}
|
||||
|
||||
// Get reward state for extra rewards
|
||||
function getExtraRewardState(index: number): RewardStateEnum {
|
||||
const signedDays = extraSignedDays.value;
|
||||
// Already signed
|
||||
if (index < signedDays) return RewardState.SIGNED;
|
||||
// Next reward to receive (extra rewards don't have missed state, only available during event)
|
||||
if (index === signedDays && !isTodaySigned.value) return RewardState.NEXT_REWARD;
|
||||
return RewardState.NORMAL;
|
||||
}
|
||||
|
||||
// Get if reward cell is clickable (for missed days, only first missed day is clickable)
|
||||
function getRewardClickable(index: number): boolean {
|
||||
const state = getRewardState(index);
|
||||
if (state === RewardState.NEXT_REWARD) {
|
||||
return true;
|
||||
} else if (state === RewardState.MISSED) {
|
||||
// Only the first missed day is clickable for resign
|
||||
// According to MiHoYo rules, you can only make up the first missed day
|
||||
const signedDays = signStat.value?.total_sign_day ?? 0;
|
||||
return index === signedDays;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle reward cell click
|
||||
function handleRewardCellClick(index: number): void {
|
||||
const state = getRewardState(index);
|
||||
if (state === RewardState.NEXT_REWARD) {
|
||||
// Click on next reward cell triggers sign-in
|
||||
handleSign();
|
||||
} else if (state === RewardState.MISSED) {
|
||||
// Only allow clicking the first missed day for resign
|
||||
// According to MiHoYo rules, you can only make up the first missed day
|
||||
const signedDays = signStat.value?.total_sign_day ?? 0;
|
||||
if (index === signedDays) {
|
||||
// This is the first missed day, allow resign
|
||||
handleResign();
|
||||
}
|
||||
// If it's not the first missed day, do nothing (clicking won't trigger resign)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadResignInfo(): Promise<void> {
|
||||
if (!cookie.value) return;
|
||||
try {
|
||||
const ck = { cookie_token: cookie.value.cookie_token, account_id: cookie.value.account_id };
|
||||
const resignResp = await lunaReq.resign.info(props.account, ck);
|
||||
console.log(resignResp);
|
||||
if ("retcode" in resignResp) {
|
||||
await TGLogger.Warn(`[Sign Item] Failed to load resign info: ${resignResp.message}`);
|
||||
} else {
|
||||
resignInfo.value = resignResp;
|
||||
await TGLogger.Info(`[Sign Item] Resign info loaded for ${props.account.gameUid}`);
|
||||
}
|
||||
} catch (error) {
|
||||
await TGLogger.Error(`[Sign Item] Load resign info error: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update local data after successful sign/resign without API call
|
||||
function updateLocalDataAfterSign(): void {
|
||||
if (!signStat.value) return;
|
||||
// Increment sign count
|
||||
signStat.value.total_sign_day += 1;
|
||||
// Mark as signed today
|
||||
signStat.value.is_sign = true;
|
||||
// Update extra sign day if has extra award
|
||||
if (signInfo.value?.short_extra_award.has_extra_award) {
|
||||
signStat.value.short_sign_day += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function updateLocalDataAfterResign(): void {
|
||||
if (!signStat.value || !resignInfo.value) return;
|
||||
// Increment sign count
|
||||
signStat.value.total_sign_day += 1;
|
||||
// Decrement missed count
|
||||
if (signStat.value.sign_cnt_missed > 0) {
|
||||
signStat.value.sign_cnt_missed -= 1;
|
||||
}
|
||||
// Decrement resign count
|
||||
if (resignInfo.value.quality_cnt > 0) {
|
||||
resignInfo.value.quality_cnt -= 1;
|
||||
}
|
||||
if (signInfo.value) signInfo.value.resign = true;
|
||||
}
|
||||
|
||||
async function handleSign(): Promise<void> {
|
||||
if (!cookie.value) {
|
||||
showSnackbar.warn("请先登录");
|
||||
return;
|
||||
}
|
||||
isSign.value = true;
|
||||
try {
|
||||
const ck = { cookie_token: cookie.value.cookie_token, account_id: cookie.value.account_id };
|
||||
const ckSign = {
|
||||
stoken: cookie.value.stoken,
|
||||
stuid: cookie.value.stuid,
|
||||
mid: cookie.value.mid,
|
||||
};
|
||||
|
||||
let check = false;
|
||||
let challenge: string | undefined = undefined;
|
||||
|
||||
while (!check) {
|
||||
const signResp = await lunaReq.sign.oper(props.account, ck, challenge);
|
||||
if (challenge !== undefined) challenge = undefined;
|
||||
if ("retcode" in signResp) {
|
||||
if (signResp.retcode === 1034) {
|
||||
await TGLogger.Info("[Sign Item] Captcha required");
|
||||
const challengeGet = await miscReq.challenge(ckSign);
|
||||
if (challengeGet === false) {
|
||||
showSnackbar.error("验证码验证失败");
|
||||
break;
|
||||
}
|
||||
challenge = challengeGet;
|
||||
continue;
|
||||
}
|
||||
await TGLogger.Error(`[Sign Item] Sign-in failed: ${signResp.message}`);
|
||||
showSnackbar.error(`签到失败: ${signResp.message}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (signResp.success === 0) {
|
||||
check = true;
|
||||
} else if (signResp.is_risk) {
|
||||
await TGLogger.Info("[Sign Item] Risk verification required");
|
||||
const gtRes = await showGeetest({
|
||||
gt: signResp.gt,
|
||||
challenge: signResp.challenge,
|
||||
new_captcha: 1,
|
||||
success: 1,
|
||||
});
|
||||
if (gtRes === false) {
|
||||
showSnackbar.error("验证码验证失败");
|
||||
break;
|
||||
}
|
||||
challenge = signResp.challenge;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (check) {
|
||||
showSnackbar.success("签到成功");
|
||||
updateLocalDataAfterSign();
|
||||
// Load resign info only if there are missed days
|
||||
const missedDays = signStat.value?.sign_cnt_missed ?? 0;
|
||||
if (missedDays > 0) {
|
||||
await loadResignInfo();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
await TGLogger.Error(`[Sign Item] Sign-in error: ${error}`);
|
||||
showSnackbar.error("签到失败,请重试");
|
||||
} finally {
|
||||
isSign.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResign(): Promise<void> {
|
||||
if (!cookie.value) {
|
||||
showSnackbar.warn("请先登录");
|
||||
return;
|
||||
}
|
||||
// Check if can resign
|
||||
const missedDays = signStat.value?.sign_cnt_missed ?? 0;
|
||||
if (missedDays === 0) {
|
||||
showSnackbar.warn("没有漏签天数,无需补签");
|
||||
return;
|
||||
}
|
||||
// Load resign info first
|
||||
await loadResignInfo();
|
||||
|
||||
if (!resignInfo.value) {
|
||||
showSnackbar.error("获取补签信息失败");
|
||||
return;
|
||||
}
|
||||
const resignCnt = resignInfo.value.quality_cnt;
|
||||
// Check resign count
|
||||
if (resignCnt <= 0) {
|
||||
showSnackbar.warn("补签次数不足");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check coin
|
||||
const coinCost = resignInfo.value.coin_cost;
|
||||
const coinCnt = resignInfo.value.coin_cnt;
|
||||
if (coinCnt < coinCost) {
|
||||
showSnackbar.warn(`米游币不足,需要 ${coinCost} 米游币`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm resign
|
||||
const confirmMsg = `漏签天数: ${missedDays} 剩余补签次数: ${resignInfo.value.quality_cnt}\n花费: ${coinCost} 米游币 当前米游币: ${coinCnt}`;
|
||||
const confirmed = await showDialog.check("确认补签?", confirmMsg);
|
||||
if (!confirmed) {
|
||||
showSnackbar.cancel("已取消补签");
|
||||
return;
|
||||
}
|
||||
|
||||
isResign.value = true;
|
||||
try {
|
||||
const ck = { cookie_token: cookie.value.cookie_token, account_id: cookie.value.account_id };
|
||||
const ckSign = {
|
||||
stoken: cookie.value.stoken,
|
||||
stuid: cookie.value.stuid,
|
||||
mid: cookie.value.mid,
|
||||
};
|
||||
|
||||
let check = false;
|
||||
let challenge: string | undefined = undefined;
|
||||
|
||||
while (!check) {
|
||||
const resignResp = await lunaReq.resign.oper(props.account, ck, challenge);
|
||||
if (challenge !== undefined) challenge = undefined;
|
||||
if ("retcode" in resignResp) {
|
||||
if (resignResp.retcode === 1034) {
|
||||
await TGLogger.Info("[Sign Item] Captcha required for resign");
|
||||
const challengeGet = await miscReq.challenge(ckSign);
|
||||
if (challengeGet === false) {
|
||||
showSnackbar.error("验证码验证失败");
|
||||
break;
|
||||
}
|
||||
challenge = challengeGet;
|
||||
continue;
|
||||
}
|
||||
await TGLogger.Error(`[Sign Item] Resign failed: ${resignResp.message}`);
|
||||
showSnackbar.error(`补签失败: ${resignResp.message}`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Resign successful
|
||||
check = true;
|
||||
updateLocalDataAfterResign();
|
||||
showSnackbar.success(
|
||||
`补签成功,剩余补签次数${resignCnt - 1}次,剩余米游币${coinCnt - coinCost}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
await TGLogger.Error(`[Sign Item] Resign error: ${error}`);
|
||||
showSnackbar.error("补签失败,请重试");
|
||||
} finally {
|
||||
isResign.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function tryDelete(): Promise<void> {
|
||||
const infoStr = `${gameInfo.value.title}-${props.account.regionName}-${props.account.gameUid}`;
|
||||
const check = await showDialog.check(`确定删除?`, `${infoStr}\n删除后仅能通过刷新游戏账号恢复`);
|
||||
if (!check) {
|
||||
showSnackbar.cancel(`已取消删除${infoStr}`);
|
||||
return;
|
||||
}
|
||||
emits("delete", props.account);
|
||||
}
|
||||
|
||||
async function shareItem(): Promise<void> {
|
||||
if (!signItemEl.value) return;
|
||||
const fileName = `游戏签到_${gameInfo.value.title}_${props.account.gameUid}`;
|
||||
await generateShareImg(fileName, signItemEl.value, 2);
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ph-si-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-1);
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.ph-si-top {
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--common-shadow-2);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ph-sit-delete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: var(--tgc-btn-1);
|
||||
color: var(--tgc-od-red);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ph-sit-icon {
|
||||
overflow: hidden;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.ph-sit-info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.ph-sit-title {
|
||||
display: flex;
|
||||
color: var(--box-text-1);
|
||||
column-gap: 4px;
|
||||
font-family: var(--font-title);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
|
||||
.hint {
|
||||
color: var(--tgc-od-red);
|
||||
}
|
||||
}
|
||||
|
||||
.ph-sit-sub {
|
||||
color: var(--box-text-1);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sign-extra-rewards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
border: 1px solid rgb(255 165 0 / 30%);
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, rgb(255 165 0 / 10%), rgb(255 215 0 / 10%));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.extra-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--box-text-1);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.extra-title {
|
||||
color: var(--tgc-od-orange);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.extra-days {
|
||||
color: var(--box-text-2);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.extra-time {
|
||||
margin-left: auto;
|
||||
color: var(--box-text-3);
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.extra-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sign-rewards {
|
||||
display: grid;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-2);
|
||||
gap: 4px;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
}
|
||||
|
||||
.sign-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sign-btn {
|
||||
flex: 1;
|
||||
background: var(--tgc-btn-1);
|
||||
color: var(--btn-text);
|
||||
font-weight: 500;
|
||||
|
||||
&.resign-btn {
|
||||
background: var(--box-bg-3);
|
||||
color: var(--box-text-2);
|
||||
}
|
||||
|
||||
&:not(:disabled).resign-btn {
|
||||
background: var(--tgc-od-orange);
|
||||
color: var(--tgc-white-1);
|
||||
}
|
||||
}
|
||||
|
||||
.dark .sign-btn.resign-btn {
|
||||
background: var(--box-bg-3);
|
||||
color: var(--box-text-2);
|
||||
|
||||
&:not(:disabled) {
|
||||
background: var(--tgc-od-orange);
|
||||
color: var(--tgc-white-1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
217
src/components/pageHome/ph-sign-reward-cell.vue
Normal file
217
src/components/pageHome/ph-sign-reward-cell.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<!-- 单个签到奖励格子 TODO:额外奖励格子需要测试 -->
|
||||
<template>
|
||||
<div
|
||||
:class="['reward-cell', stateClass, { extra: isExtra, clickable: isClickableComputed }]"
|
||||
:title="getTitle()"
|
||||
class="sign-reward-cell"
|
||||
@click="handleClick"
|
||||
>
|
||||
<img :alt="reward.name" :src="reward.icon" class="reward-icon" />
|
||||
<span class="reward-count">{{ reward.cnt }}</span>
|
||||
<div v-if="state === RewardState.SIGNED" class="reward-check">
|
||||
<img alt="finish" src="/source/UI/finish.webp" />
|
||||
</div>
|
||||
<div v-if="!isExtra" class="reward-day">{{ dayNumber }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue";
|
||||
|
||||
// Reward state enum TODO:完善类型
|
||||
const RewardState = <const>{
|
||||
NORMAL: 0,
|
||||
SIGNED: 1,
|
||||
NEXT_REWARD: 2,
|
||||
MISSED: 3,
|
||||
};
|
||||
export type RewardStateEnum = (typeof RewardState)[keyof typeof RewardState];
|
||||
|
||||
type Props = {
|
||||
reward: TGApp.BBS.Sign.HomeAward;
|
||||
dayNumber: number;
|
||||
state: RewardStateEnum;
|
||||
isExtra?: boolean;
|
||||
isClickable?: boolean; // Explicitly control if this cell is clickable
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
(e: "click"): void;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isExtra: false,
|
||||
isClickable: undefined, // Default to undefined, will be computed if not provided
|
||||
});
|
||||
const emits = defineEmits<Emits>();
|
||||
|
||||
const STATE_CLASS_MAP: Record<RewardStateEnum, string> = {
|
||||
[RewardState.NORMAL]: "state-normal",
|
||||
[RewardState.SIGNED]: "state-signed",
|
||||
[RewardState.NEXT_REWARD]: "state-next-reward",
|
||||
[RewardState.MISSED]: "state-missed",
|
||||
};
|
||||
|
||||
const stateClass = computed(() => {
|
||||
return STATE_CLASS_MAP[props.state];
|
||||
});
|
||||
|
||||
const isClickableComputed = computed(() => {
|
||||
// If isClickable prop is explicitly provided, use it
|
||||
if (props.isClickable !== undefined) {
|
||||
return props.isClickable;
|
||||
}
|
||||
// Otherwise, fall back to default logic
|
||||
return props.state === RewardState.NEXT_REWARD || props.state === RewardState.MISSED;
|
||||
});
|
||||
|
||||
function getTitle(): string {
|
||||
let title = `${props.reward.name}x${props.reward.cnt}`;
|
||||
if (props.state === RewardState.NEXT_REWARD) {
|
||||
title += " - 点击签到";
|
||||
} else if (props.state === RewardState.MISSED) {
|
||||
if (isClickableComputed.value) {
|
||||
title += " - 点击补签";
|
||||
} else {
|
||||
title += " - 漏签";
|
||||
}
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
function handleClick(): void {
|
||||
if (isClickableComputed.value) {
|
||||
emits("click");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.sign-reward-cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
border: 1px solid var(--common-shadow-2);
|
||||
border-radius: 4px;
|
||||
background: var(--box-bg-3);
|
||||
|
||||
&.extra {
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
&.state-signed {
|
||||
background: var(--box-bg-4);
|
||||
color: var(--box-text-4);
|
||||
|
||||
.reward-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&.state-next-reward {
|
||||
position: relative;
|
||||
border-width: 1px;
|
||||
border-color: var(--tgc-od-blue);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
background: var(--box-bg-3);
|
||||
box-shadow: 0 0 12px rgb(59 130 246 / 60%);
|
||||
}
|
||||
|
||||
&.state-missed {
|
||||
border-color: var(--tgc-od-red);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
z-index: 5;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.dark .sign-reward-cell {
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgb(255 255 255 / 10%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 12px rgb(59 130 246 / 60%);
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 20px rgb(59 130 246 / 90%);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
.reward-icon {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.extra .reward-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.reward-count {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: -2px;
|
||||
right: 2px;
|
||||
font-size: 9px;
|
||||
font-style: italic;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.extra .reward-count {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.reward-check {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.reward-day {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
bottom: -8px;
|
||||
left: -4px;
|
||||
color: var(--tgc-od-orange);
|
||||
font-family: var(--font-title);
|
||||
opacity: 0.4;
|
||||
}
|
||||
</style>
|
||||
102
src/components/pageHome/ph-user-switch.vue
Normal file
102
src/components/pageHome/ph-user-switch.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<!-- 米社用户切换菜单 -->
|
||||
<template>
|
||||
<v-menu location="bottom">
|
||||
<template v-slot:activator="{ props: menuProps }">
|
||||
<div class="sign-user-switch" v-bind="menuProps">
|
||||
<span>{{ nickname }}({{ currentUid }})</span>
|
||||
<v-icon size="small">mdi-chevron-down</v-icon>
|
||||
</div>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-for="ac in users" :key="ac.uid">
|
||||
<v-list-item-title>{{ ac.brief.nickname }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ ac.brief.uid }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div v-if="ac.uid === currentUid" title="当前登录账号">
|
||||
<v-icon color="green">mdi-account-check</v-icon>
|
||||
</div>
|
||||
<v-icon
|
||||
v-else
|
||||
icon="mdi-account-convert"
|
||||
size="small"
|
||||
title="切换用户"
|
||||
@click="switchUser(ac.uid)"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import showSnackbar from "@comp/func/snackbar.js";
|
||||
import TSUserAccount from "@Sqlm/userAccount.js";
|
||||
import useUserStore from "@store/user.js";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { onMounted, shallowRef } from "vue";
|
||||
|
||||
type Props = {
|
||||
nickname: string;
|
||||
currentUid: string;
|
||||
};
|
||||
|
||||
type Emits = {
|
||||
(e: "switch-user", uid: string): void;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emits = defineEmits<Emits>();
|
||||
|
||||
const { uid, briefInfo, cookie, account } = storeToRefs(useUserStore());
|
||||
const users = shallowRef<Array<TGApp.App.Account.User>>([]);
|
||||
|
||||
onMounted(async () => {
|
||||
await loadUsers();
|
||||
});
|
||||
|
||||
async function loadUsers(): Promise<void> {
|
||||
users.value = await TSUserAccount.account.getAllAccount();
|
||||
}
|
||||
|
||||
async function switchUser(targetUid: string): Promise<void> {
|
||||
if (targetUid === props.currentUid) {
|
||||
showSnackbar.warn("该账户已经登录,无需切换");
|
||||
return;
|
||||
}
|
||||
|
||||
const accountGet = await TSUserAccount.account.getAccount(targetUid);
|
||||
if (!accountGet) {
|
||||
showSnackbar.warn(`未找到${targetUid}的账号信息,请重新登录`);
|
||||
return;
|
||||
}
|
||||
|
||||
uid.value = targetUid;
|
||||
briefInfo.value = accountGet.brief;
|
||||
cookie.value = accountGet.cookie;
|
||||
|
||||
const gameAccount = await TSUserAccount.game.getCurAccount(targetUid);
|
||||
if (!gameAccount) {
|
||||
showSnackbar.warn(`未找到${targetUid}的游戏账号信息,请尝试刷新`);
|
||||
return;
|
||||
}
|
||||
|
||||
account.value = gameAccount;
|
||||
showSnackbar.success(`成功切换到用户${targetUid}`);
|
||||
|
||||
emits("switch-user", targetUid);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sign-user-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 4px;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -236,7 +236,7 @@ async function refreshState(ck: TGApp.App.Account.Cookie): Promise<void> {
|
||||
`[签到任务]刷新${item.info.title}-${item.account.regionName}-${item.account.gameUid}`,
|
||||
);
|
||||
if (item.reward === undefined) {
|
||||
const rewardResp = await lunaReq.home(item.account, cookie);
|
||||
const rewardResp = await lunaReq.sign.info(item.account, cookie);
|
||||
console.log("签到奖励", item, rewardResp);
|
||||
if ("retcode" in rewardResp) {
|
||||
await TGLogger.Script(
|
||||
@@ -245,7 +245,7 @@ async function refreshState(ck: TGApp.App.Account.Cookie): Promise<void> {
|
||||
showSnackbar.error(`[${rewardResp.retcode}] ${rewardResp.message}`);
|
||||
} else item.reward = rewardResp.awards[dayNow - 1];
|
||||
}
|
||||
const statResp = await lunaReq.info(item.account, cookie);
|
||||
const statResp = await lunaReq.sign.stat(item.account, cookie);
|
||||
console.log("签到状态", item, statResp);
|
||||
if ("retcode" in statResp) {
|
||||
await TGLogger.Script(`[签到任务]获取签到状态失败:${statResp.retcode} ${statResp.message}`);
|
||||
@@ -267,7 +267,7 @@ async function trySign(ac: SignAccount[], ck: TGApp.App.Account.Cookie): Promise
|
||||
let check = false;
|
||||
let challenge: string | undefined = undefined;
|
||||
while (!check) {
|
||||
const signResp = await lunaReq.sign(item.account, cookie, challenge);
|
||||
const signResp = await lunaReq.sign.oper(item.account, cookie, challenge);
|
||||
console.log("签到信息", item, signResp);
|
||||
if (challenge !== undefined) challenge = undefined;
|
||||
if ("retcode" in signResp) {
|
||||
|
||||
@@ -89,7 +89,9 @@
|
||||
},
|
||||
"item_id": {
|
||||
"type": "string",
|
||||
"description": "物品的内部 ID"
|
||||
"description": "物品的内部 ID",
|
||||
"minLength": 1,
|
||||
"pattern": "^[0-9]+$"
|
||||
},
|
||||
"count": {
|
||||
"type": "string",
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
<img alt="icon" src="/source/UI/toolbox.webp" />
|
||||
<span>实用脚本</span>
|
||||
<v-select
|
||||
density="compact"
|
||||
class="us-top-select"
|
||||
variant="outlined"
|
||||
v-model="curAccount"
|
||||
:items="accounts"
|
||||
item-title="uid"
|
||||
:hide-details="true"
|
||||
label="账号"
|
||||
:disabled="runScript || runAll"
|
||||
:hide-details="true"
|
||||
:items="accounts"
|
||||
class="us-top-select"
|
||||
density="compact"
|
||||
item-title="uid"
|
||||
label="账号"
|
||||
variant="outlined"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<div class="select-main">
|
||||
<img alt="icon" :src="item.raw.brief.avatar" />
|
||||
<img :src="item.raw.brief.avatar" alt="icon" />
|
||||
<div class="content">
|
||||
<span>{{ item.raw.brief.nickname }}</span>
|
||||
<span>UID:{{ item.raw.uid }}</span>
|
||||
@@ -26,7 +26,7 @@
|
||||
</template>
|
||||
<template #item="{ props, item }">
|
||||
<div class="select-item" v-bind="props">
|
||||
<img alt="icon" :src="item.raw.brief.avatar" />
|
||||
<img :src="item.raw.brief.avatar" alt="icon" />
|
||||
<div class="content">
|
||||
<span>{{ item.raw.brief.nickname }}</span>
|
||||
<span>UID:{{ item.raw.uid }}</span>
|
||||
@@ -37,8 +37,8 @@
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
size="small"
|
||||
icon="mdi-account-convert"
|
||||
size="small"
|
||||
title="切换用户"
|
||||
@click="loadAccount(item.raw.uid)"
|
||||
/>
|
||||
@@ -46,10 +46,13 @@
|
||||
</div>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-btn :loading="runAll" class="run-all-btn" variant="elevated" @click="tryExecAll()"
|
||||
>一键执行</v-btn
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template #append>
|
||||
<span class="top-hint" @click="tryCkVerify()" title="点击验证">
|
||||
<span class="top-hint" title="点击验证" @click="tryCkVerify()">
|
||||
需要验证码登录/游戏扫码登录所需cookie!!!
|
||||
</span>
|
||||
</template>
|
||||
@@ -57,10 +60,6 @@
|
||||
<div class="us-page-container">
|
||||
<!-- 左侧脚本列表 -->
|
||||
<div class="us-scripts">
|
||||
<div class="us-title">
|
||||
<span>脚本列表</span>
|
||||
<v-btn @click="tryExecAll()" class="btn" :loading="runAll">一键执行</v-btn>
|
||||
</div>
|
||||
<TusMission ref="mission" v-model="runScript" />
|
||||
<TusSign ref="sign" v-model="runScript" />
|
||||
</div>
|
||||
@@ -299,25 +298,12 @@ async function tryExecAll(): Promise<void> {
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.us-title {
|
||||
position: sticky;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--app-page-bg);
|
||||
.run-all-btn {
|
||||
background: var(--tgc-btn-1);
|
||||
color: var(--btn-text);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--common-text-title);
|
||||
font-family: var(--font-title);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: var(--tgc-btn-1);
|
||||
color: var(--btn-text);
|
||||
}
|
||||
.dark .run-all-btn {
|
||||
border: 1px solid var(--common-shadow-1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- 首页 -->
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<div class="home-top">
|
||||
<v-app-bar>
|
||||
<template #prepend>
|
||||
<div v-if="isLogin" class="home-tools">
|
||||
<v-select
|
||||
v-model="curGid"
|
||||
@@ -11,6 +11,7 @@
|
||||
item-value="gid"
|
||||
label="小工具(右侧)分区"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
>
|
||||
<template #selection="{ item }">
|
||||
<div class="select-item main">
|
||||
@@ -44,6 +45,8 @@
|
||||
</v-select>
|
||||
<TGameNav :model-value="curGid" />
|
||||
</div>
|
||||
</template>
|
||||
<template #append>
|
||||
<div class="home-select">
|
||||
<v-select
|
||||
v-model="showItems"
|
||||
@@ -53,11 +56,16 @@
|
||||
:multiple="true"
|
||||
label="首页组件显示"
|
||||
variant="outlined"
|
||||
width="300px"
|
||||
width="360px"
|
||||
density="compact"
|
||||
/>
|
||||
<v-btn :rounded="true" class="select-btn" @click="submitHome">确定</v-btn>
|
||||
<v-btn variant="elevated" :rounded="true" class="select-btn" @click="submitHome">
|
||||
确定
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-app-bar>
|
||||
<div class="home-container">
|
||||
<component :is="item" v-for="item in components" :key="item" @success="loadEnd(item)" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -69,6 +77,7 @@ import showSnackbar from "@comp/func/snackbar.js";
|
||||
import PhCompCalendar from "@comp/pageHome/ph-comp-calendar.vue";
|
||||
import PhCompPool from "@comp/pageHome/ph-comp-pool.vue";
|
||||
import PhCompPosition from "@comp/pageHome/ph-comp-position.vue";
|
||||
import PhCompSign from "@comp/pageHome/ph-comp-sign.vue";
|
||||
import useAppStore from "@store/app.js";
|
||||
import useBBSStore from "@store/bbs.js";
|
||||
import useHomeStore from "@store/home.js";
|
||||
@@ -76,15 +85,29 @@ import TGLogger from "@utils/TGLogger.js";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, defineComponent, onMounted, ref, shallowRef, watch } from "vue";
|
||||
|
||||
/**
|
||||
* 单文件组件类型
|
||||
*/
|
||||
type SFComp = ReturnType<typeof defineComponent>;
|
||||
type SelectItem = { icon: string; title: string; gid: number };
|
||||
/**
|
||||
* 选项类型
|
||||
*/
|
||||
type SelectItem = {
|
||||
/** 图标 */
|
||||
icon: string;
|
||||
/** 标题 */
|
||||
title: string;
|
||||
/** 分区ID */
|
||||
gid: number;
|
||||
};
|
||||
|
||||
const homeStore = useHomeStore();
|
||||
const bbsStore = useBBSStore();
|
||||
|
||||
const { devMode, isLogin } = storeToRefs(useAppStore());
|
||||
const { gameList } = storeToRefs(bbsStore);
|
||||
const homeStore = useHomeStore();
|
||||
|
||||
const showItemsAll: Array<string> = ["素材日历", "限时祈愿", "近期活动"];
|
||||
const showItemsAll: Array<string> = ["游戏签到", "素材日历", "限时祈愿", "近期活动"];
|
||||
|
||||
const curGid = ref<number>(2);
|
||||
|
||||
@@ -121,6 +144,9 @@ async function loadComp(): Promise<void> {
|
||||
const temp: Array<SFComp> = [];
|
||||
for (const item of showItems.value) {
|
||||
switch (item) {
|
||||
case "游戏签到":
|
||||
temp.push(PhCompSign);
|
||||
break;
|
||||
case "限时祈愿":
|
||||
temp.push(PhCompPool);
|
||||
break;
|
||||
@@ -150,6 +176,8 @@ async function submitHome(): Promise<void> {
|
||||
|
||||
function getName(name: string): string | undefined {
|
||||
switch (name) {
|
||||
case "ph-comp-sign":
|
||||
return "签到";
|
||||
case "ph-comp-pool":
|
||||
return "限时祈愿";
|
||||
case "ph-comp-position":
|
||||
@@ -194,15 +222,17 @@ async function loadEnd(item: ReturnType<typeof defineComponent>): Promise<void>
|
||||
}
|
||||
|
||||
.home-tool-select {
|
||||
width: 250px;
|
||||
width: 220px;
|
||||
max-width: 250px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.home-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-right: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.select-btn {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* @file plugins/Sqlite/modules/userAccounts.ts
|
||||
* @description 用户账户模块
|
||||
* @since Beta v0.7.8
|
||||
* 用户账户模块
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
|
||||
import { path } from "@tauri-apps/api";
|
||||
@@ -215,15 +214,15 @@ function copyCookie(cookie: TGApp.App.Account.Cookie): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取指定用户账号
|
||||
* @since Beta v0.7.2
|
||||
* 获取指定用户账号
|
||||
* @since Beta v0.9.0
|
||||
* @param {string} uid - 用户UID
|
||||
* @returns {Promise<TGApp.Sqlite.Account.Game[]>}
|
||||
* @returns {Promise<Array<TGApp.Sqlite.Account.Game>>}
|
||||
*/
|
||||
async function getGameAccount(uid: string): Promise<TGApp.Sqlite.Account.Game[]> {
|
||||
async function getGameAccount(uid: string): Promise<Array<TGApp.Sqlite.Account.Game>> {
|
||||
const db = await TGSqlite.getDB();
|
||||
return await db.select<TGApp.Sqlite.Account.Game[]>(
|
||||
"SELECT * FROM GameAccount WHERE uid = ? ORDER BY region, gameUid;",
|
||||
return await db.select<Array<TGApp.Sqlite.Account.Game>>(
|
||||
"SELECT * FROM GameAccount WHERE uid = ? ORDER BY gameBiz, gameUid;",
|
||||
[uid],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* @file request/lunaReq.ts
|
||||
* @description 签到模块请求
|
||||
* @since Beta v0.7.2
|
||||
* 签到模块请求
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
import { getRequestHeader } from "@utils/getRequestHeader.js";
|
||||
import TGBbs from "@utils/TGBbs.js";
|
||||
@@ -13,8 +12,8 @@ const telaBu: Readonly<string> = "https://api-takumi.mihoyo.com/event/luna/";
|
||||
type ReqParam = { host?: string; actId: string };
|
||||
|
||||
/**
|
||||
* @description 根据服务器获取actId跟host
|
||||
* @since Beta v0.7.2
|
||||
* 根据服务器获取actId跟host
|
||||
* @since Beta v0.9.0
|
||||
* @param {string} region - 服务器
|
||||
* @returns {string} actId
|
||||
*/
|
||||
@@ -22,10 +21,10 @@ function getActConf(region: string): ReqParam | false {
|
||||
switch (region) {
|
||||
// 崩坏2
|
||||
case "bh2_cn":
|
||||
return { actId: "e202203291431091" };
|
||||
return { actId: "e202203291431091", host: "bh2" };
|
||||
// 崩坏3
|
||||
case "bh3_cn":
|
||||
return { actId: "e202306201626331" };
|
||||
return { actId: "e202306201626331", host: "bh3" };
|
||||
// 原神
|
||||
case "hk4e_cn":
|
||||
return { actId: "e202311201442471", host: "hk4e" };
|
||||
@@ -34,17 +33,19 @@ function getActConf(region: string): ReqParam | false {
|
||||
return { actId: "e202304121516551", host: "hkrpg" };
|
||||
// 未定事件簿
|
||||
case "nxx_cn":
|
||||
return { actId: "e202202251749321" };
|
||||
return { actId: "e202202251749321", host: "nxx" };
|
||||
// 绝区零
|
||||
case "nap_cn":
|
||||
return { actId: "e202406242138391", host: "zzz" };
|
||||
// TODO: 崩坏·因缘精灵
|
||||
// TODO: 星布谷地
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取签到奖励列表
|
||||
* 获取签到奖励列表
|
||||
* @since Beta v0.7.2
|
||||
* @property {TGApp.Sqlite.Account.Game} account - 账号信息
|
||||
* @property {Record<string, string>} cookie - cookies
|
||||
@@ -78,7 +79,7 @@ async function getLunaHome(
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取签到信息
|
||||
* 获取签到信息
|
||||
* @since Beta v0.7.2
|
||||
* @property {TGApp.Sqlite.Account.Game} account - 账号信息
|
||||
* @property {Record<string, string>} cookie - cookies
|
||||
@@ -116,6 +117,14 @@ async function getLunaInfo(
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到
|
||||
* @since Beta v0.9.0
|
||||
* @param {TGApp.Sqlite.Account.Game} account - 账号信息
|
||||
* @param {Record<string, string>} cookie - cookies
|
||||
* @param {string} [challenge] 极验信息
|
||||
* @returns {Promise<TGApp.BBS.Sign.SignRes | TGApp.BBS.Response.Base>}
|
||||
*/
|
||||
async function lunaSign(
|
||||
account: TGApp.Sqlite.Account.Game,
|
||||
cookie: Record<string, string>,
|
||||
@@ -147,6 +156,94 @@ async function lunaSign(
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
const lunaReq = { home: getLunaHome, info: getLunaInfo, sign: lunaSign };
|
||||
/**
|
||||
* 获取补签信息
|
||||
* @since Beta v0.9.0
|
||||
* @param {TGApp.Sqlite.Account.Game} account - 账号信息
|
||||
* @param {Record<string, string>} cookie - cookies
|
||||
* @returns {Promise<TGApp.BBS.Sign.ResignInfoRes|TGApp.BBS.Response.Base>}
|
||||
*/
|
||||
async function getResignInfo(
|
||||
account: TGApp.Sqlite.Account.Game,
|
||||
cookie: Record<string, string>,
|
||||
): Promise<TGApp.BBS.Sign.ResignInfoRes | TGApp.BBS.Response.Base> {
|
||||
const conf = getActConf(account.gameBiz);
|
||||
if (conf === false) {
|
||||
return <TGApp.BBS.Response.Base>{ retcode: 1, message: "未知服务器" };
|
||||
}
|
||||
const url = conf.host ? `${telaBu}${conf.host}/resign_info` : `${telaBu}info`;
|
||||
const params = {
|
||||
lang: "zh-cn",
|
||||
act_id: conf.actId,
|
||||
region: account.region,
|
||||
uid: account.gameUid,
|
||||
};
|
||||
const header: Record<string, string> = {
|
||||
"user-agent": TGBbs.ua,
|
||||
referer: "https://act.mihoyo.com",
|
||||
cookie: Object.keys(cookie)
|
||||
.map((key) => `${key}=${cookie[key]}`)
|
||||
.join("; "),
|
||||
};
|
||||
if (conf.host) header["x-rpc-signgame"] = conf.host;
|
||||
const resp = await TGHttp<TGApp.BBS.Sign.ResignInfoResp>(url, {
|
||||
method: "GET",
|
||||
query: params,
|
||||
headers: header,
|
||||
});
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 补签
|
||||
* @since Beta v0.9.0
|
||||
* @param {TGApp.Sqlite.Account.Game} account - 账号信息
|
||||
* @param {Record<string, string>} cookie - cookies
|
||||
* @param {string} [challenge] 极验信息
|
||||
* @returns {Promise<TGApp.BBS.Sign.ResignRes | TGApp.BBS.Response.Base>}
|
||||
*/
|
||||
async function lunaResign(
|
||||
account: TGApp.Sqlite.Account.Game,
|
||||
cookie: Record<string, string>,
|
||||
challenge?: string,
|
||||
): Promise<TGApp.BBS.Sign.ResignRes | TGApp.BBS.Response.Base> {
|
||||
const conf = getActConf(account.gameBiz);
|
||||
if (conf === false) {
|
||||
return <TGApp.BBS.Response.Base>{ retcode: 1, message: "未知服务器" };
|
||||
}
|
||||
const url = conf.host ? `${telaBu}${conf.host}/resign` : `${telaBu}sign`;
|
||||
const data = {
|
||||
lang: "zh-cn",
|
||||
act_id: conf.actId,
|
||||
region: account.region,
|
||||
uid: account.gameUid,
|
||||
};
|
||||
const header: Record<string, string> = {
|
||||
...getRequestHeader(cookie, "POST", JSON.stringify(data), "X6"),
|
||||
"x-rpc-client_type": "2",
|
||||
};
|
||||
if (conf.host) header["x-rpc-signgame"] = conf.host;
|
||||
if (challenge) header["x-rpc-challenge"] = challenge;
|
||||
const resp = await TGHttp<TGApp.BBS.Sign.ResignResp>(url, {
|
||||
method: "POST",
|
||||
headers: header,
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (resp.retcode !== 0) return <TGApp.BBS.Response.Base>resp;
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
const lunaReq = {
|
||||
sign: {
|
||||
info: getLunaHome,
|
||||
stat: getLunaInfo,
|
||||
oper: lunaSign,
|
||||
},
|
||||
resign: {
|
||||
info: getResignInfo,
|
||||
oper: lunaResign,
|
||||
},
|
||||
};
|
||||
|
||||
export default lunaReq;
|
||||
|
||||
@@ -1,28 +1,55 @@
|
||||
/**
|
||||
* @file store/modules/home.ts
|
||||
* @description Home store module
|
||||
* @since Beta v0.7.6
|
||||
* 首页组件状态
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
export type ShowItem = { show: boolean; order: number; label: string };
|
||||
/**
|
||||
* 默认展示项
|
||||
*/
|
||||
const defaultHomeShow: Array<TGApp.Store.Home.ShowItem> = [
|
||||
{ show: false, order: 4, label: "游戏签到" },
|
||||
{ show: true, order: 1, label: "限时祈愿" },
|
||||
{ show: true, order: 2, label: "近期活动" },
|
||||
{ show: true, order: 3, label: "素材日历" },
|
||||
];
|
||||
|
||||
const useHomeStore = defineStore("home", () => {
|
||||
const homeShow = ref<Array<ShowItem>>([
|
||||
{ show: true, order: 1, label: "限时祈愿" },
|
||||
{ show: true, order: 2, label: "近期活动" },
|
||||
{ show: true, order: 3, label: "素材日历" },
|
||||
]);
|
||||
const homeShow = ref<Array<TGApp.Store.Home.ShowItem>>(defaultHomeShow);
|
||||
const poolCover = ref<Record<number, string>>();
|
||||
|
||||
function getShowItems(): Array<string> {
|
||||
const homeShowLocal = localStorage.getItem("homeShow");
|
||||
if (homeShowLocal === null || !Array.isArray(JSON.parse(homeShowLocal))) {
|
||||
if (homeShowLocal === null) {
|
||||
localStorage.setItem("homeShow", JSON.stringify(homeShow.value));
|
||||
} else {
|
||||
try {
|
||||
const storedItems: Array<TGApp.Store.Home.ShowItem> = JSON.parse(homeShowLocal);
|
||||
if (!Array.isArray(storedItems)) {
|
||||
// Invalid data, reset to default
|
||||
localStorage.setItem("homeShow", JSON.stringify(homeShow.value));
|
||||
} else {
|
||||
// Merge with default items to add new items
|
||||
const storedLabels = storedItems.map((i) => i.label);
|
||||
// Add new items from default that don't exist in stored
|
||||
for (const defaultItem of homeShow.value) {
|
||||
if (!storedLabels.includes(defaultItem.label)) {
|
||||
storedItems.push({ ...defaultItem });
|
||||
}
|
||||
}
|
||||
// Remove items that no longer exist in default
|
||||
const defaultLabels = homeShow.value.map((i) => i.label);
|
||||
homeShow.value = storedItems.filter((item) => defaultLabels.includes(item.label));
|
||||
localStorage.setItem("homeShow", JSON.stringify(homeShow.value));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Invalid JSON, reset to default
|
||||
localStorage.setItem("homeShow", JSON.stringify(homeShow.value));
|
||||
}
|
||||
}
|
||||
homeShow.value = JSON.parse(localStorage.getItem("homeShow")!);
|
||||
return homeShow.value
|
||||
.filter((item) => item.show)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
@@ -30,11 +57,7 @@ const useHomeStore = defineStore("home", () => {
|
||||
}
|
||||
|
||||
function init(): void {
|
||||
homeShow.value = [
|
||||
{ show: true, order: 1, label: "限时祈愿" },
|
||||
{ show: true, order: 2, label: "近期活动" },
|
||||
{ show: true, order: 3, label: "素材日历" },
|
||||
];
|
||||
homeShow.value = defaultHomeShow;
|
||||
localStorage.setItem("homeShow", JSON.stringify(homeShow.value));
|
||||
}
|
||||
|
||||
@@ -42,9 +65,13 @@ const useHomeStore = defineStore("home", () => {
|
||||
let order = 1;
|
||||
for (const item of items) {
|
||||
const findIdx = homeShow.value.findIndex((i) => i.label === item);
|
||||
if (findIdx === -1) continue;
|
||||
homeShow.value[findIdx].show = true;
|
||||
homeShow.value[findIdx].order = order++;
|
||||
if (findIdx === -1) {
|
||||
// Add new item if it doesn't exist
|
||||
homeShow.value.push({ show: true, order: order++, label: item });
|
||||
} else {
|
||||
homeShow.value[findIdx].show = true;
|
||||
homeShow.value[findIdx].order = order++;
|
||||
}
|
||||
}
|
||||
for (const item of homeShow.value) if (!items.includes(item.label)) item.show = false;
|
||||
localStorage.setItem("homeShow", JSON.stringify(homeShow.value));
|
||||
|
||||
58
src/types/BBS/Sign.d.ts
vendored
58
src/types/BBS/Sign.d.ts
vendored
@@ -21,7 +21,7 @@ declare namespace TGApp.BBS.Sign {
|
||||
awards: Array<HomeAward>;
|
||||
/** 业务标识 */
|
||||
biz: string;
|
||||
/** 是否补签 TODO:描述不清晰 */
|
||||
/** 是否补签 */
|
||||
resign: boolean;
|
||||
/** 活动额外奖励 */
|
||||
short_extra_award: HomeAwardExtra;
|
||||
@@ -77,7 +77,7 @@ declare namespace TGApp.BBS.Sign {
|
||||
today: string;
|
||||
/** 是否签到 */
|
||||
is_sign: boolean;
|
||||
/** TODO: 未知字段 */
|
||||
/** 是否补签 */
|
||||
is_sub: boolean;
|
||||
/**
|
||||
* 服务器
|
||||
@@ -116,4 +116,58 @@ declare namespace TGApp.BBS.Sign {
|
||||
/** 是否成功 */
|
||||
success: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取补签信息返回响应
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
type ResignInfoResp = TGApp.BBS.Response.BaseWithData<ResignInfoRes>;
|
||||
|
||||
/**
|
||||
* 补签信息返回
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
type ResignInfoRes = {
|
||||
/** 日 */
|
||||
resign_info_daily: number;
|
||||
/** 月 */
|
||||
resign_info_monthly: number;
|
||||
/** 日限制 */
|
||||
resign_limit_daily: number;
|
||||
/** 月限制 */
|
||||
resign_limit_monthly: number;
|
||||
/** 漏签天数 */
|
||||
sign_cnt_missed: number;
|
||||
/** 米游币 */
|
||||
coin_cnt: number;
|
||||
/** 补签花费 */
|
||||
coin_cost: number;
|
||||
/** 补签规则 */
|
||||
rule: string;
|
||||
/** 是否签到 */
|
||||
signed: boolean;
|
||||
/** 签到天数 */
|
||||
sign_days: number;
|
||||
/** 花费 */
|
||||
cost: number;
|
||||
/** 月补签次数 */
|
||||
month_quality_cnt: number;
|
||||
/** 剩余补签次数*/
|
||||
quality_cnt: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 执行补签返回响应
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
type ResignResp = TGApp.BBS.Response.BaseWithData<ResignRes>;
|
||||
|
||||
/**
|
||||
* 执行补签返回信息
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
type ResignRes = {
|
||||
/** 信息 */
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
19
src/types/Store/Home.d.ts
vendored
Normal file
19
src/types/Store/Home.d.ts
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 首页状态类型
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
|
||||
declare namespace TGApp.Store.Home {
|
||||
/**
|
||||
* 组件展示项
|
||||
* @since Beta v0.9.0
|
||||
*/
|
||||
type ShowItem = {
|
||||
/** 是否展示 */
|
||||
show: boolean;
|
||||
/** 文本 */
|
||||
label: string;
|
||||
/** 顺序 */
|
||||
order: number;
|
||||
};
|
||||
}
|
||||
@@ -85,7 +85,7 @@ function getShareImgBgColor(): string {
|
||||
|
||||
/**
|
||||
* 生成分享截图
|
||||
* @since Beta v0.8.7
|
||||
* @since Beta v0.9.0
|
||||
* @param {string} fileName - 文件名
|
||||
* @param {HTMLElement} element - 元素
|
||||
* @param {number} scale - 缩放比例
|
||||
@@ -99,7 +99,7 @@ export async function generateShareImg(
|
||||
scrollable: boolean = false,
|
||||
): Promise<void> {
|
||||
const canvas = document.createElement("canvas");
|
||||
const maxHeight = element.style.maxHeight;
|
||||
const maxHeight = element.style?.maxHeight;
|
||||
if (scrollable) element.style.maxHeight = "100%";
|
||||
const width = element.clientWidth + 30;
|
||||
const height = (scrollable ? element.scrollHeight : element.clientHeight) + 30;
|
||||
|
||||
Reference in New Issue
Block a user