设置页添加胡桃云相关模块

#202
This commit is contained in:
BTMuli
2026-01-04 16:53:34 +08:00
parent 5834eee6fc
commit 1cbcdbb31d
10 changed files with 400 additions and 175 deletions

View File

@@ -70,6 +70,7 @@
"doc": "docs"
},
"dependencies": {
"@date-fns/tz": "^1.4.1",
"@mdi/font": "7.4.47",
"@sentry/vite-plugin": "^4.6.1",
"@sentry/vue": "^10.32.1",
@@ -89,6 +90,7 @@
"ajv": "^8.17.1",
"artplayer": "^5.3.0",
"colord": "^2.9.3",
"date-fns": "^4.1.0",
"echarts": "^6.0.0",
"html2canvas": "^1.4.1",
"js-md5": "^0.8.3",

16
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@date-fns/tz':
specifier: ^1.4.1
version: 1.4.1
'@mdi/font':
specifier: 7.4.47
version: 7.4.47
@@ -65,6 +68,9 @@ importers:
colord:
specifier: ^2.9.3
version: 2.9.3
date-fns:
specifier: ^4.1.0
version: 4.1.0
echarts:
specifier: ^6.0.0
version: 6.0.0
@@ -720,6 +726,9 @@ packages:
peerDependencies:
postcss: ^8.4
'@date-fns/tz@1.4.1':
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
'@dual-bundle/import-meta-resolve@4.2.1':
resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==}
@@ -2026,6 +2035,9 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -4867,6 +4879,8 @@ snapshots:
dependencies:
postcss: 8.5.6
'@date-fns/tz@1.4.1': {}
'@dual-bundle/import-meta-resolve@4.2.1': {}
'@emnapi/core@1.7.1':
@@ -6209,6 +6223,8 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
date-fns@4.1.0: {}
debug@3.2.7:
dependencies:
ms: 2.1.3

View File

@@ -54,16 +54,18 @@ async function toSite(): Promise<void> {
await openUrl("https://app.btmuli.ink/docs/TeyvatGuide/changelogs.html");
}
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
.tab-box {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
flex-shrink: 0;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 10px;
border-radius: 8px;
background-image: linear-gradient(to right, #f78ca0 0%, #f9748f 19%, #fd868c 60%, #fe9a8b 100%);
box-shadow: 0 0 10px var(--common-shadow-2);
}
.tab-icon {

View File

@@ -68,7 +68,9 @@ async function tryPlayGame(): Promise<void> {
.tgb-box {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
flex-shrink: 0;
align-items: flex-start;
justify-content: center;
padding: 10px;

View File

@@ -0,0 +1,141 @@
<!-- 胡桃账号 -->
<template>
<v-card class="tch-box">
<img alt="logo" class="logo" src="/platforms/other/hutao2.webp" />
<div class="tch-top">
<img alt="logo" class="tch-top-logo" src="/platforms/other/hutao.webp" />
<div class="tch-top-info">
<span>胡桃云账号({{ getAccountDesc() }})</span>
<span>{{ userName ?? "未登录" }}</span>
</div>
</div>
<div class="tch-mid">
<div v-if="isLogin && userInfo" class="info-list">
<div class="info-item">
<span>CDN到期时间</span>
<span>{{ timeStr2str(userInfo.CdnExpireAt) }}</span>
</div>
<div class="info-item">
<span>祈愿到期时间</span>
<span>{{ timeStr2str(userInfo.GachaLogExpireAt) }}</span>
</div>
</div>
<span v-else>未获取到用户信息</span>
</div>
<template #actions>
<v-spacer />
<v-btn icon="mdi-login" title="登录胡桃云" variant="outlined" @click="tryLogin()" />
<v-btn
:disabled="!userName"
icon="mdi-refresh"
title="刷新用户信息"
variant="outlined"
@click="hutaoStore.tryRefreshInfo()"
/>
</template>
</v-card>
</template>
<script lang="ts" setup>
import showDialog from "@comp/func/dialog.js";
import useHutaoStore from "@store/hutao.js";
import { timeStr2str } from "@utils/toolFunc.js";
import { storeToRefs } from "pinia";
const hutaoStore = useHutaoStore();
const { userName, userInfo, isLogin } = storeToRefs(useHutaoStore());
function getAccountDesc(): string {
if (!isLogin.value) return "未登录";
if (userInfo.value) {
if (userInfo.value.IsMaintainer) return "维护者";
if (userInfo.value.IsLicensedDeveloper) return "开发者";
return "普通用户";
}
return "未知";
}
async function tryLogin(): Promise<void> {
if (isLogin.value) {
const check = await showDialog.check("确认重新登录?");
if (!check) return;
}
isLogin.value = false;
await hutaoStore.tryLogin();
}
</script>
<style lang="scss" scoped>
.tch-box {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding: 8px;
border-radius: 8px;
background-image: linear-gradient(to right, #f78ca0 0%, #f9748f 19%, #fd868c 60%, #fe9a8b 100%);
color: var(--tgc-white-1);
row-gap: 8px;
}
.logo {
position: absolute;
z-index: 1;
right: -20px;
bottom: -20px;
width: 160px;
}
.tch-top {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: flex-start;
column-gap: 8px;
}
.tch-top-logo {
position: relative;
width: 48px;
}
.tch-top-info {
position: relative;
display: flex;
flex-direction: column;
:first-child {
font-family: var(--font-title);
}
:last-child {
font-size: 12px;
}
}
.tch-mid {
position: relative;
z-index: 2;
width: 100%;
}
.info-list {
position: relative;
display: flex;
width: 100%;
flex-direction: column;
align-items: center;
justify-content: center;
row-gap: 4px;
}
.info-item {
position: relative;
display: flex;
width: 100%;
align-items: center;
justify-content: space-between;
font-size: 12px;
}
</style>

View File

@@ -5,7 +5,7 @@
<v-avatar :image="userInfo.avatar" />
</template>
<template #title>{{ userInfo.nickname }}</template>
<template #subtitle>UID:{{ userInfo.uid }}</template>
<template #subtitle>{{ userInfo.uid }}</template>
<template #text>{{ userInfo.desc }}</template>
<template #append>
<v-menu location="start">
@@ -565,7 +565,10 @@ async function clearUser(user: TGApp.App.Account.User): Promise<void> {
</script>
<style lang="css" scoped>
.tcu-box {
border-radius: 10px;
position: relative;
width: 100%;
flex-shrink: 0;
border-radius: 8px;
background-image: linear-gradient(to right, #f78ca0 0%, #f9748f 19%, #fd868c 60%, #fe9a8b 100%);
color: var(--tgc-white-1);
}

View File

@@ -1,165 +1,168 @@
<template>
<div class="config-box">
<TcInfo />
<v-list class="config-list">
<v-list-subheader :inset="true" class="config-header" title="数据相关" />
<v-divider :inset="true" class="border-opacity-75" />
<v-list-item title="数据备份" @click="confirmBackup()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-export</v-icon>
</div>
</template>
</v-list-item>
<v-list-item title="数据恢复" @click="confirmRestore()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-import</v-icon>
</div>
</template>
</v-list-item>
<v-list-item title="数据更新" @click="confirmUpdate()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-arrow-up</v-icon>
</div>
</template>
</v-list-item>
<v-list-item>
<template #prepend>
<div class="config-icon">
<v-icon>mdi-refresh</v-icon>
</div>
</template>
<v-list-item-title @click="confirmUpdateDevice()">刷新设备信息</v-list-item-title>
<v-list-item-subtitle>
<!-- @ts-expect-error eslint-disable-next-line Deprecated symbol used -->
{{ deviceInfo.device_name }}({{ deviceInfo.product }}) - {{ deviceInfo.device_fp }}
</v-list-item-subtitle>
<template #append>
<v-icon title="强制刷新设备信息" @click="confirmUpdateDevice(true)">mdi-bug</v-icon>
</template>
</v-list-item>
<v-list-item title="清除缓存" @click="confirmDelCache">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-remove</v-icon>
</div>
</template>
<template #append>{{ bytesToSize(cacheSize) }}</template>
</v-list-item>
<v-list-item v-show="showReset" title="重置数据库" @click="confirmResetDB()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-settings</v-icon>
</div>
</template>
</v-list-item>
<v-list-item title="恢复默认设置" @click="confirmResetApp">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-cog-sync</v-icon>
</div>
</template>
</v-list-item>
<v-list-subheader :inset="true" class="config-header" title="调试" @click="tryShowReset" />
<v-divider :inset="true" class="border-opacity-75" />
<v-list-item v-if="isDevEnv" subtitle="开启后将显示调试信息" title="调试模式">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-bug-play</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="devMode"
:inset="true"
:label="devMode ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
@click="submitDevMode"
/>
</template>
</v-list-item>
<v-list-item subtitle="根据分辨率动态调整窗体大小" title="窗口回正">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-window-restore</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="isNeedResize"
:inset="true"
:label="isNeedResize ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
@click="submitResize"
/>
</template>
</v-list-item>
<v-list-item subtitle="关闭窗口时最小化到系统托盘而不是退出应用" title="关闭到托盘">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-tray-arrow-down</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="closeToTray"
:inset="true"
:label="closeToTray ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
/>
</template>
</v-list-item>
<v-list-item subtitle="关闭后将记录帖子浏览记录" title="无痕浏览">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-incognito</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="appStore.incognito"
:inset="true"
:label="appStore.incognito ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
@click="switchIncognito"
/>
</template>
</v-list-item>
<v-list-item v-if="platform() === 'windows'" title="分享设置">
<template #subtitle>默认保存到剪贴板超过{{ shareDefaultFile }}MB时保存到文件</template>
<template #prepend>
<div class="config-icon">
<v-icon>mdi-share-variant</v-icon>
</div>
</template>
<template #append>
<v-icon @click="confirmShare()">mdi-cog</v-icon>
</template>
</v-list-item>
<v-list-item title="图片质量调整">
<template #subtitle>当前图像质量{{ imageQualityPercent }}%</template>
<template #prepend>
<div class="config-icon">
<v-icon>mdi-image-filter-vintage</v-icon>
</div>
</template>
<template #append>
<v-icon @click="confirmImgQuality()">mdi-cog</v-icon>
</template>
</v-list-item>
</v-list>
<TcDataDir />
</div>
<div class="config-right">
<TcAppBadge />
<TcUserBadge />
<TcGameBadge v-if="platform() === 'windows'" />
<div class="pc-page">
<div class="config-box">
<TcInfo />
<v-list class="config-list">
<v-list-subheader :inset="true" class="config-header" title="数据相关" />
<v-divider :inset="true" class="border-opacity-75" />
<v-list-item title="数据备份" @click="confirmBackup()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-export</v-icon>
</div>
</template>
</v-list-item>
<v-list-item title="数据恢复" @click="confirmRestore()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-import</v-icon>
</div>
</template>
</v-list-item>
<v-list-item title="数据更新" @click="confirmUpdate()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-arrow-up</v-icon>
</div>
</template>
</v-list-item>
<v-list-item>
<template #prepend>
<div class="config-icon">
<v-icon>mdi-refresh</v-icon>
</div>
</template>
<v-list-item-title @click="confirmUpdateDevice()">刷新设备信息</v-list-item-title>
<v-list-item-subtitle>
<!-- @ts-expect-error eslint-disable-next-line Deprecated symbol used -->
{{ deviceInfo.device_name }}({{ deviceInfo.product }}) - {{ deviceInfo.device_fp }}
</v-list-item-subtitle>
<template #append>
<v-icon title="强制刷新设备信息" @click="confirmUpdateDevice(true)">mdi-bug</v-icon>
</template>
</v-list-item>
<v-list-item title="清除缓存" @click="confirmDelCache">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-remove</v-icon>
</div>
</template>
<template #append>{{ bytesToSize(cacheSize) }}</template>
</v-list-item>
<v-list-item v-show="showReset" title="重置数据库" @click="confirmResetDB()">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-database-settings</v-icon>
</div>
</template>
</v-list-item>
<v-list-item title="恢复默认设置" @click="confirmResetApp">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-cog-sync</v-icon>
</div>
</template>
</v-list-item>
<v-list-subheader :inset="true" class="config-header" title="调试" @click="tryShowReset" />
<v-divider :inset="true" class="border-opacity-75" />
<v-list-item v-if="isDevEnv" subtitle="开启后将显示调试信息" title="调试模式">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-bug-play</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="devMode"
:inset="true"
:label="devMode ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
@click="submitDevMode"
/>
</template>
</v-list-item>
<v-list-item subtitle="根据分辨率动态调整窗体大小" title="窗口回正">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-window-restore</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="isNeedResize"
:inset="true"
:label="isNeedResize ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
@click="submitResize"
/>
</template>
</v-list-item>
<v-list-item subtitle="关闭窗口时最小化到系统托盘而不是退出应用" title="关闭到托盘">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-tray-arrow-down</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="closeToTray"
:inset="true"
:label="closeToTray ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
/>
</template>
</v-list-item>
<v-list-item subtitle="关闭后将记录帖子浏览记录" title="无痕浏览">
<template #prepend>
<div class="config-icon">
<v-icon>mdi-incognito</v-icon>
</div>
</template>
<template #append>
<v-switch
v-model="appStore.incognito"
:inset="true"
:label="appStore.incognito ? '开启' : '关闭'"
class="config-switch"
color="#FAC51E"
@click="switchIncognito"
/>
</template>
</v-list-item>
<v-list-item v-if="platform() === 'windows'" title="分享设置">
<template #subtitle>默认保存到剪贴板超过{{ shareDefaultFile }}MB时保存到文件</template>
<template #prepend>
<div class="config-icon">
<v-icon>mdi-share-variant</v-icon>
</div>
</template>
<template #append>
<v-icon @click="confirmShare()">mdi-cog</v-icon>
</template>
</v-list-item>
<v-list-item title="图片质量调整">
<template #subtitle>当前图像质量{{ imageQualityPercent }}%</template>
<template #prepend>
<div class="config-icon">
<v-icon>mdi-image-filter-vintage</v-icon>
</div>
</template>
<template #append>
<v-icon @click="confirmImgQuality()">mdi-cog</v-icon>
</template>
</v-list-item>
</v-list>
<TcDataDir />
</div>
<div class="config-right">
<TcAppBadge />
<TcUserBadge />
<TcGameBadge v-if="platform() === 'windows'" />
<TcHutaoBadge />
</div>
</div>
<TcoImgQuality v-model="showImgQuality" />
</template>
@@ -170,6 +173,7 @@ import showSnackbar from "@comp/func/snackbar.js";
import TcAppBadge from "@comp/pageConfig/tc-appBadge.vue";
import TcDataDir from "@comp/pageConfig/tc-dataDir.vue";
import TcGameBadge from "@comp/pageConfig/tc-gameBadge.vue";
import TcHutaoBadge from "@comp/pageConfig/tc-hutaoBadge.vue";
import TcInfo from "@comp/pageConfig/tc-info.vue";
import TcUserBadge from "@comp/pageConfig/tc-userBadge.vue";
import TcoImgQuality from "@comp/pageConfig/tco-imgQuality.vue";
@@ -511,17 +515,33 @@ async function switchIncognito(): Promise<void> {
showSnackbar.success("已关闭无痕浏览!");
}
</script>
<style lang="css" scoped>
<style lang="scss" scoped>
.pc-page {
position: relative;
display: flex;
height: calc(100vh - 32px);
align-items: stretch;
column-gap: 8px;
}
.config-box {
position: relative;
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
row-gap: 10px;
align-items: center;
justify-content: flex-start;
padding-right: 8px;
overflow-y: auto;
row-gap: 8px;
}
.config-list {
border-radius: 10px;
margin-right: 260px;
position: relative;
width: 100%;
flex-shrink: 0;
border-radius: 8px;
background: var(--box-bg-1);
color: var(--box-text-4);
font-family: var(--font-text);
@@ -553,12 +573,15 @@ async function switchIncognito(): Promise<void> {
}
.config-right {
position: fixed;
top: 16px;
right: 10px;
position: relative;
display: flex;
overflow: hidden auto;
width: 256px;
height: 100%;
box-sizing: border-box;
flex-direction: column;
row-gap: 15px;
flex-shrink: 0;
justify-content: flex-start;
row-gap: 8px;
}
</style>

View File

@@ -90,7 +90,7 @@ declare namespace TGApp.Plugins.Hutao.Account {
*/
GachaLogExpireAt: string;
/** 是否是开发者 */
IsLicenseDeveloper: boolean;
IsLicensedDeveloper: boolean;
/** 是否是主开发 */
IsMaintainer: boolean;
/** 常规用户名 */

View File

@@ -13,6 +13,8 @@ import { ref } from "vue";
const useHutaoStore = defineStore(
"hutao",
() => {
/** 是否登录 */
const isLogin = ref<boolean>(false);
/** 账号 */
const userName = ref<string>();
/** token */
@@ -59,6 +61,7 @@ const useHutaoStore = defineStore(
await showLoading.end();
return;
}
isLogin.value = true;
userName.value = inputN;
accessToken.value = resp.AccessToken;
refreshToken.value = resp.RefreshToken;
@@ -70,6 +73,22 @@ const useHutaoStore = defineStore(
} finally {
await showLoading.end();
}
if (isLogin.value) {
await tryRefreshInfo();
}
}
async function tryRefreshInfo(): Promise<void> {
if (!checkIsValid()) {
await tryRefreshToken();
}
const resp = await hutao.Account.info(accessToken.value!);
if ("retcode" in resp) {
showSnackbar.warn(`刷新用户信息失败:${resp.retcode}-${resp.message}`);
return;
}
userInfo.value = resp;
showSnackbar.success("成功刷新用户信息");
}
async function tryRefreshToken(): Promise<void> {
@@ -95,6 +114,7 @@ const useHutaoStore = defineStore(
}
return {
isLogin,
userName,
accessToken,
refreshToken,
@@ -103,6 +123,7 @@ const useHutaoStore = defineStore(
checkIsValid,
tryLogin,
tryRefreshToken,
tryRefreshInfo,
};
},
{

View File

@@ -4,12 +4,14 @@
*/
import showSnackbar from "@comp/func/snackbar.js";
import { tz } from "@date-fns/tz";
import bbsEnum from "@enum/bbs.js";
import staticDataEnum from "@enum/staticData.js";
import { path } from "@tauri-apps/api";
import { invoke } from "@tauri-apps/api/core";
import { type } from "@tauri-apps/plugin-os";
import TGLogger from "@utils/TGLogger.js";
import { format, parseISO } from "date-fns";
import { v4 } from "uuid";
import { AppCalendarData, AppCharacterData, AppWeaponData } from "@/data/index.js";
@@ -366,3 +368,16 @@ export function getRcStar(cid: number, star: number): number {
const star105List = [10000062, 10000117, 10000118];
return star105List.includes(cid) ? 105 : star;
}
/**
* 接收时间字符串转成utf8时区
* @since Beta v0.9.1
* @param str - 时间字符串
* @example 2025-09-18T01:01:39+00:00
* @returns 转换后的时间
*/
export function timeStr2str(str: string): string {
return format(parseISO(str), "yyyy-MM-dd HH:mm:ss", {
in: tz("Asia/Shanghai"),
});
}