From 89c4c05faf4d6be504cef9f607d9ba6bc4fe4898 Mon Sep 17 00:00:00 2001 From: Megghy Date: Tue, 7 Oct 2025 22:00:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=AE=9A=E6=9C=9F?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91=EF=BC=9B?= =?UTF-8?q?=E8=B0=83=E6=95=B4=20ESLint=20=E9=85=8D=E7=BD=AE=E4=BB=A5?= =?UTF-8?q?=E6=94=BE=E5=AE=BD=E7=B1=BB=E5=9E=8B=E6=A3=80=E6=9F=A5=E8=A7=84?= =?UTF-8?q?=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- eslint.config.mjs | 17 +++ src/client/data/initialize.ts | 206 +++++++++++++++++++++++++------ src/data/Initializer.ts | 4 +- src/main.ts | 5 +- src/views/IndexView.vue | 44 +++---- src/views/manage/AnalyzeView.vue | 10 +- tsconfig.json | 15 ++- 7 files changed, 229 insertions(+), 72 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index dfe33ff..922e009 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -61,14 +61,31 @@ export default antfu( // TypeScript 相关规则 'ts/no-explicit-any': 'off', 'ts/ban-ts-comment': 'off', + 'ts/no-floating-promises': 'off', // 允许不 await Promise + 'ts/no-misused-promises': 'off', // 允许在条件表达式中使用 Promise + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-misused-promises': 'off', // 通用规则 'no-console': 'off', 'unused-imports/no-unused-vars': 'warn', + 'eqeqeq': 'off', // 允许使用 == 和 != + 'no-eq-null': 'off', // 允许使用 == null + '@typescript-eslint/strict-boolean-expressions': 'off', // 允许宽松的布尔表达式 // 关闭一些过于严格的规则 'antfu/if-newline': 'off', 'style/brace-style': ['error', '1tbs'], + 'prefer-promise-reject-errors': 'off', // 允许 reject 任何值 + 'no-throw-literal': 'off', // 允许 throw 任何值 + 'ts/no-unsafe-assignment': 'off', // 允许不安全的赋值 + 'ts/no-unsafe-member-access': 'off', // 允许不安全的成员访问 + 'ts/no-unsafe-call': 'off', // 允许不安全的调用 + 'ts/switch-exhaustiveness-check': 'warn', // 允许 switch 不覆盖所有情况 + 'ts/restrict-template-expressions': 'off', // 允许模板字符串表达式不受限制 + + // JSON 相关规则 + 'jsonc/sort-keys': 'off', // 关闭 JSON key 排序要求 }, }, // 集成 VueVine 配置 diff --git a/src/client/data/initialize.ts b/src/client/data/initialize.ts index fa87a29..f57d807 100644 --- a/src/client/data/initialize.ts +++ b/src/client/data/initialize.ts @@ -14,6 +14,7 @@ import { import { openUrl } from '@tauri-apps/plugin-opener' import { relaunch } from '@tauri-apps/plugin-process' import { check } from '@tauri-apps/plugin-updater' +import { h } from 'vue' import { isLoggedIn, useAccount } from '@/api/account' import { CN_HOST, isDev } from '@/data/constants' import { useWebFetcher } from '@/store/useWebFetcher' @@ -30,6 +31,8 @@ const accountInfo = useAccount() export const clientInited = ref(false) let tray: TrayIcon let heartbeatTimer: number | null = null +let updateCheckTimer: number | null = null +let updateNotificationRef: any = null async function sendHeartbeat() { try { @@ -58,12 +61,162 @@ export function stopHeartbeat() { } } +export function startUpdateCheck() { + // 立即检查一次更新 + void checkUpdatePeriodically() + + // 之后每 6 小时检查一次更新 + updateCheckTimer = window.setInterval(() => { + void checkUpdatePeriodically() + }, 6 * 60 * 60 * 1000) // 6 hours + info('[更新检查] 定时器已启动,间隔 6 小时') +} + +export function stopUpdateCheck() { + if (updateCheckTimer !== null) { + clearInterval(updateCheckTimer) + updateCheckTimer = null + info('[更新检查] 定时器已停止') + } + if (updateNotificationRef) { + updateNotificationRef.destroy() + updateNotificationRef = null + } +} + +async function checkUpdatePeriodically() { + try { + info('[更新检查] 开始检查更新...') + const update = await check() + + if (update) { + info(`[更新检查] 发现新版本: ${update.version}`) + + // 发送 Windows 通知 + const permissionGranted = await isPermissionGranted() + if (permissionGranted) { + sendNotification({ + title: 'VTsuru.Client 更新可用', + body: `发现新版本 ${update.version},点击通知查看详情`, + }) + } + + // 显示不可关闭的 NaiveUI notification + if (!updateNotificationRef) { + updateNotificationRef = window.$notification.warning({ + title: '发现新版本', + content: `VTsuru.Client ${update.version} 现已可用`, + meta: update.date, + action: () => { + return h('div', { style: 'display: flex; gap: 8px; margin-top: 8px;' }, [ + h( + 'button', + { + class: 'n-button n-button--primary-type n-button--small-type', + onClick: () => { void handleUpdateInstall(update) }, + }, + '立即更新' + ), + h( + 'button', + { + class: 'n-button n-button--default-type n-button--small-type', + onClick: () => handleUpdateDismiss(), + }, + '稍后提醒' + ), + ]) + }, + closable: false, + duration: 0, // 不自动关闭 + }) + } + } else { + info('[更新检查] 当前已是最新版本') + } + } catch (error) { + warn(`[更新检查] 检查更新失败: ${error}`) + } +} + +async function handleUpdateInstall(update: any) { + try { + // 关闭提示 + if (updateNotificationRef) { + updateNotificationRef.destroy() + updateNotificationRef = null + } + + // 显示下载进度通知 + let downloaded = 0 + let contentLength = 0 + const progressNotification = window.$notification.info({ + title: '正在下载更新', + content: '更新下载中,请稍候...', + closable: false, + duration: 0, + }) + + info('[更新] 开始下载并安装更新') + await update.downloadAndInstall((event: any) => { + switch (event.event) { + case 'Started': + contentLength = event.data.contentLength || 0 + info(`[更新] 开始下载 ${contentLength} 字节`) + break + case 'Progress': { + downloaded += event.data.chunkLength + const progress = contentLength > 0 ? Math.round((downloaded / contentLength) * 100) : 0 + progressNotification.content = `下载进度: ${progress}% (${Math.round(downloaded / 1024 / 1024)}MB / ${Math.round(contentLength / 1024 / 1024)}MB)` + info(`[更新] 已下载 ${downloaded} / ${contentLength} 字节`) + break + } + case 'Finished': + info('[更新] 下载完成') + progressNotification.content = '下载完成,正在安装...' + break + } + }) + + progressNotification.destroy() + info('[更新] 更新安装完成,准备重启应用') + + window.$notification.success({ + title: '更新完成', + content: '应用将在 3 秒后重启', + duration: 3000, + }) + + // 延迟 3 秒后重启 + await new Promise(resolve => setTimeout(resolve, 3000)) + await relaunch() + } catch (error) { + warn(`[更新] 安装更新失败: ${error}`) + window.$notification.error({ + title: '更新失败', + content: `更新安装失败: ${error}`, + duration: 5000, + }) + } +} + +function handleUpdateDismiss() { + if (updateNotificationRef) { + updateNotificationRef.destroy() + updateNotificationRef = null + } + info('[更新] 用户选择稍后更新') +} + export async function initAll(isOnBoot: boolean) { const setting = useSettings() if (clientInited.value) { return } - checkUpdate() + // 初始检查更新(不阻塞初始化) + if (!isDev) { + void checkUpdate() + } const appWindow = getCurrentWindow() let permissionGranted = await isPermissionGranted() @@ -86,7 +239,7 @@ export async function initAll(isOnBoot: boolean) { } } initNotificationHandler() - const detach = await attachConsole() + await attachConsole() const settings = useSettings() const biliCookie = useBiliCookie() await settings.init() @@ -135,13 +288,9 @@ export async function initAll(isOnBoot: boolean) { tooltip: 'VTsuru 事件收集器', icon: iconData, action: (event) => { - switch (event.type) { - case 'DoubleClick': - appWindow.show() - break - case 'Click': - appWindow.show() - break + if (event.type === 'DoubleClick' || event.type === 'Click') { + appWindow.show() + appWindow.setFocus() } }, } @@ -180,6 +329,12 @@ export async function initAll(isOnBoot: boolean) { useBiliFunction().init() //startHeartbeat() + + // 启动定期更新检查 + if (!isDev) { + startUpdateCheck() + } + clientInited.value = true } export function OnClientUnmounted() { @@ -188,39 +343,14 @@ export function OnClientUnmounted() { } stopHeartbeat() + stopUpdateCheck() tray.close() // useDanmakuWindow().closeWindow(); } -async function checkUpdate() { - const update = await check() - console.log(update) - if (update) { - console.log( - `found update ${update.version} from ${update.date} with notes ${update.body}`, - ) - let downloaded = 0 - let contentLength = 0 - // alternatively we could also call update.download() and update.install() separately - await update.downloadAndInstall((event) => { - switch (event.event) { - case 'Started': - contentLength = event.data.contentLength || 0 - console.log(`started downloading ${event.data.contentLength} bytes`) - break - case 'Progress': - downloaded += event.data.chunkLength - console.log(`downloaded ${downloaded} from ${contentLength}`) - break - case 'Finished': - console.log('download finished') - break - } - }) - - console.log('update installed') - await relaunch() - } +export async function checkUpdate() { + // 手动检查更新(保留用于手动触发) + await checkUpdatePeriodically() } export const isInitedDanmakuClient = ref(false) diff --git a/src/data/Initializer.ts b/src/data/Initializer.ts index 2f4bb22..7e692a7 100644 --- a/src/data/Initializer.ts +++ b/src/data/Initializer.ts @@ -48,7 +48,7 @@ export function InitVTsuru() { } async function InitOther() { - if (process.env.NODE_ENV !== 'development' && !window.$route.path.startsWith('/obs')) { + if (import.meta.env.MODE !== 'development' && !window.$route.path.startsWith('/obs')) { const mod = await import('@hyperdx/browser') const HyperDX = (mod as any).default ?? mod HyperDX.init({ @@ -59,7 +59,7 @@ async function InitOther() { advancedNetworkCapture: true, // Capture full HTTP request/response headers and bodies (default false) ignoreUrls: [/localhost/i], }) - // 将实例挂到窗口,便于后续设置全局属性(可选) + // 将实例挂到窗口,便于后续设置全局属性(可选) ;(window as any).__HyperDX__ = HyperDX } // 加载其他数据 diff --git a/src/main.ts b/src/main.ts index 661fd98..36d3f1a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,10 @@ void import('./data/Initializer').then(m => m.InitVTsuru()) const isTauri = () => (window as any).__TAURI__ !== undefined || (window as any).__TAURI_INTERNAL__ !== undefined || '__TAURI__' in window if (isTauri()) { // 仅在 Tauri 环境下才动态加载相关初始化,避免把 @tauri-apps/* 打入入口 - void import('./client/data/initialize').then(m => m.startHeartbeat()) + void import('./client/data/initialize').then(m => { + m.startHeartbeat(); + m.checkUpdate(); + }) } window.$mitt = emitter diff --git a/src/views/IndexView.vue b/src/views/IndexView.vue index a07dd4b..8746f96 100644 --- a/src/views/IndexView.vue +++ b/src/views/IndexView.vue @@ -410,7 +410,7 @@ onMounted(async () => { background: cardBgMedium, border: borderSystem.medium, borderRadius: borderRadius.large, - boxShadow: shadowSystem.light, + boxShadow: 'none', cursor: item.route ? 'pointer' : 'default', }" hoverable class="feature-card" @click="handleFunctionClick(item)"> @@ -643,7 +643,7 @@ onMounted(async () => { overflow-x: hidden; overflow-y: auto; padding-bottom: 60px; - + &::before content: ''; position: absolute; @@ -664,7 +664,7 @@ onMounted(async () => { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; - + &::before content: ''; position: absolute; @@ -674,11 +674,11 @@ onMounted(async () => { height: 100%; background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); transition: left 0.6s; - + &:hover transform: translateY(-4px); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); - + &::before left: 100%; @@ -697,14 +697,14 @@ onMounted(async () => { .feature-card transition: all 0.2s ease; - + &:hover transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); .entry-card transition: all 0.2s ease; - + &:hover transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); @@ -736,7 +736,7 @@ onMounted(async () => { :deep(.n-button) border-radius: 12px; transition: all 0.2s ease; - + &:hover box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); @@ -751,13 +751,13 @@ onMounted(async () => { .main-container padding-top: 20px; padding-bottom: 20px; - + .section-title font-size: 1.1rem; - + .feature-card:hover transform: translateY(-1px); - + .entry-card:hover transform: translateY(-1px); @@ -794,7 +794,7 @@ onMounted(async () => { /* 新增样式 */ .section-header text-align: center; - + .section-subtitle margin-top: 8px; @@ -840,13 +840,13 @@ onMounted(async () => { min-width: 85px; max-width: 95px; backdrop-filter: blur(10px); - + &:hover transform: translateY(-3px); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15); border-color: rgba(255, 255, 255, 0.25); background: rgba(255, 255, 255, 0.15); - + .streamer-avatar-wrapper img transform: scale(1.08); @@ -855,7 +855,7 @@ onMounted(async () => { width: 50px; height: 50px; flex-shrink: 0; - + img width: 100%; height: 100%; @@ -897,10 +897,10 @@ onMounted(async () => { border-radius: 50%; background: rgba(255, 255, 255, 0.5); animation: pulse-dot 1.8s ease-in-out infinite; - + &:nth-child(2) animation-delay: 0.3s; - + &:nth-child(3) animation-delay: 0.6s; @@ -917,12 +917,12 @@ onMounted(async () => { .streamers-grid-modern gap: 8px; padding: 0 4px; - + .streamer-card-modern min-width: 80px; max-width: 90px; padding: 8px 6px; - + &:hover transform: translateY(-2px); @@ -930,16 +930,16 @@ onMounted(async () => { .streamers-grid-modern gap: 6px; padding: 0 4px; - + .streamer-card-modern min-width: 75px; max-width: 85px; padding: 8px 6px; - + .streamer-avatar-wrapper width: 45px; height: 45px; - + .streamer-name font-size: 0.8rem; diff --git a/src/views/manage/AnalyzeView.vue b/src/views/manage/AnalyzeView.vue index 98be65d..09ada28 100644 --- a/src/views/manage/AnalyzeView.vue +++ b/src/views/manage/AnalyzeView.vue @@ -516,7 +516,7 @@ onUnmounted(() => { - + { - + { - + { - + - +