feat: 添加定期更新检查功能并优化相关逻辑;调整 ESLint 配置以放宽类型检查规则

This commit is contained in:
Megghy
2025-10-07 22:00:25 +08:00
parent 96f6169a6c
commit 89c4c05faf
7 changed files with 229 additions and 72 deletions

View File

@@ -61,14 +61,31 @@ export default antfu(
// TypeScript 相关规则 // TypeScript 相关规则
'ts/no-explicit-any': 'off', 'ts/no-explicit-any': 'off',
'ts/ban-ts-comment': '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', 'no-console': 'off',
'unused-imports/no-unused-vars': 'warn', 'unused-imports/no-unused-vars': 'warn',
'eqeqeq': 'off', // 允许使用 == 和 !=
'no-eq-null': 'off', // 允许使用 == null
'@typescript-eslint/strict-boolean-expressions': 'off', // 允许宽松的布尔表达式
// 关闭一些过于严格的规则 // 关闭一些过于严格的规则
'antfu/if-newline': 'off', 'antfu/if-newline': 'off',
'style/brace-style': ['error', '1tbs'], '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 配置 // 集成 VueVine 配置

View File

@@ -14,6 +14,7 @@ import {
import { openUrl } from '@tauri-apps/plugin-opener' import { openUrl } from '@tauri-apps/plugin-opener'
import { relaunch } from '@tauri-apps/plugin-process' import { relaunch } from '@tauri-apps/plugin-process'
import { check } from '@tauri-apps/plugin-updater' import { check } from '@tauri-apps/plugin-updater'
import { h } from 'vue'
import { isLoggedIn, useAccount } from '@/api/account' import { isLoggedIn, useAccount } from '@/api/account'
import { CN_HOST, isDev } from '@/data/constants' import { CN_HOST, isDev } from '@/data/constants'
import { useWebFetcher } from '@/store/useWebFetcher' import { useWebFetcher } from '@/store/useWebFetcher'
@@ -30,6 +31,8 @@ const accountInfo = useAccount()
export const clientInited = ref(false) export const clientInited = ref(false)
let tray: TrayIcon let tray: TrayIcon
let heartbeatTimer: number | null = null let heartbeatTimer: number | null = null
let updateCheckTimer: number | null = null
let updateNotificationRef: any = null
async function sendHeartbeat() { async function sendHeartbeat() {
try { 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) { export async function initAll(isOnBoot: boolean) {
const setting = useSettings() const setting = useSettings()
if (clientInited.value) { if (clientInited.value) {
return return
} }
checkUpdate() // 初始检查更新(不阻塞初始化)
if (!isDev) {
void checkUpdate()
}
const appWindow = getCurrentWindow() const appWindow = getCurrentWindow()
let permissionGranted = await isPermissionGranted() let permissionGranted = await isPermissionGranted()
@@ -86,7 +239,7 @@ export async function initAll(isOnBoot: boolean) {
} }
} }
initNotificationHandler() initNotificationHandler()
const detach = await attachConsole() await attachConsole()
const settings = useSettings() const settings = useSettings()
const biliCookie = useBiliCookie() const biliCookie = useBiliCookie()
await settings.init() await settings.init()
@@ -135,13 +288,9 @@ export async function initAll(isOnBoot: boolean) {
tooltip: 'VTsuru 事件收集器', tooltip: 'VTsuru 事件收集器',
icon: iconData, icon: iconData,
action: (event) => { action: (event) => {
switch (event.type) { if (event.type === 'DoubleClick' || event.type === 'Click') {
case 'DoubleClick': appWindow.show()
appWindow.show() appWindow.setFocus()
break
case 'Click':
appWindow.show()
break
} }
}, },
} }
@@ -180,6 +329,12 @@ export async function initAll(isOnBoot: boolean) {
useBiliFunction().init() useBiliFunction().init()
//startHeartbeat() //startHeartbeat()
// 启动定期更新检查
if (!isDev) {
startUpdateCheck()
}
clientInited.value = true clientInited.value = true
} }
export function OnClientUnmounted() { export function OnClientUnmounted() {
@@ -188,39 +343,14 @@ export function OnClientUnmounted() {
} }
stopHeartbeat() stopHeartbeat()
stopUpdateCheck()
tray.close() tray.close()
// useDanmakuWindow().closeWindow(); // useDanmakuWindow().closeWindow();
} }
async function checkUpdate() { export async function checkUpdate() {
const update = await check() // 手动检查更新(保留用于手动触发)
console.log(update) await checkUpdatePeriodically()
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 const isInitedDanmakuClient = ref(false) export const isInitedDanmakuClient = ref(false)

View File

@@ -48,7 +48,7 @@ export function InitVTsuru() {
} }
async function InitOther() { 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 mod = await import('@hyperdx/browser')
const HyperDX = (mod as any).default ?? mod const HyperDX = (mod as any).default ?? mod
HyperDX.init({ HyperDX.init({
@@ -59,7 +59,7 @@ async function InitOther() {
advancedNetworkCapture: true, // Capture full HTTP request/response headers and bodies (default false) advancedNetworkCapture: true, // Capture full HTTP request/response headers and bodies (default false)
ignoreUrls: [/localhost/i], ignoreUrls: [/localhost/i],
}) })
// 将实例挂到窗口便于后续设置全局属性可选 // 将实例挂到窗口,便于后续设置全局属性(可选)
;(window as any).__HyperDX__ = HyperDX ;(window as any).__HyperDX__ = HyperDX
} }
// 加载其他数据 // 加载其他数据

View File

@@ -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 const isTauri = () => (window as any).__TAURI__ !== undefined || (window as any).__TAURI_INTERNAL__ !== undefined || '__TAURI__' in window
if (isTauri()) { if (isTauri()) {
// 仅在 Tauri 环境下才动态加载相关初始化,避免把 @tauri-apps/* 打入入口 // 仅在 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 window.$mitt = emitter

View File

@@ -410,7 +410,7 @@ onMounted(async () => {
background: cardBgMedium, background: cardBgMedium,
border: borderSystem.medium, border: borderSystem.medium,
borderRadius: borderRadius.large, borderRadius: borderRadius.large,
boxShadow: shadowSystem.light, boxShadow: 'none',
cursor: item.route ? 'pointer' : 'default', cursor: item.route ? 'pointer' : 'default',
}" hoverable class="feature-card" @click="handleFunctionClick(item)"> }" hoverable class="feature-card" @click="handleFunctionClick(item)">
<NFlex vertical> <NFlex vertical>
@@ -643,7 +643,7 @@ onMounted(async () => {
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding-bottom: 60px; padding-bottom: 60px;
&::before &::before
content: ''; content: '';
position: absolute; position: absolute;
@@ -664,7 +664,7 @@ onMounted(async () => {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&::before &::before
content: ''; content: '';
position: absolute; position: absolute;
@@ -674,11 +674,11 @@ onMounted(async () => {
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transition: left 0.6s; transition: left 0.6s;
&:hover &:hover
transform: translateY(-4px); transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
&::before &::before
left: 100%; left: 100%;
@@ -697,14 +697,14 @@ onMounted(async () => {
.feature-card .feature-card
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover &:hover
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
.entry-card .entry-card
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover &:hover
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
@@ -736,7 +736,7 @@ onMounted(async () => {
:deep(.n-button) :deep(.n-button)
border-radius: 12px; border-radius: 12px;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover &:hover
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@@ -751,13 +751,13 @@ onMounted(async () => {
.main-container .main-container
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;
.section-title .section-title
font-size: 1.1rem; font-size: 1.1rem;
.feature-card:hover .feature-card:hover
transform: translateY(-1px); transform: translateY(-1px);
.entry-card:hover .entry-card:hover
transform: translateY(-1px); transform: translateY(-1px);
@@ -794,7 +794,7 @@ onMounted(async () => {
/* 新增样式 */ /* 新增样式 */
.section-header .section-header
text-align: center; text-align: center;
.section-subtitle .section-subtitle
margin-top: 8px; margin-top: 8px;
@@ -840,13 +840,13 @@ onMounted(async () => {
min-width: 85px; min-width: 85px;
max-width: 95px; max-width: 95px;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
&:hover &:hover
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.15);
border-color: rgba(255, 255, 255, 0.25); border-color: rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
.streamer-avatar-wrapper img .streamer-avatar-wrapper img
transform: scale(1.08); transform: scale(1.08);
@@ -855,7 +855,7 @@ onMounted(async () => {
width: 50px; width: 50px;
height: 50px; height: 50px;
flex-shrink: 0; flex-shrink: 0;
img img
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -897,10 +897,10 @@ onMounted(async () => {
border-radius: 50%; border-radius: 50%;
background: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.5);
animation: pulse-dot 1.8s ease-in-out infinite; animation: pulse-dot 1.8s ease-in-out infinite;
&:nth-child(2) &:nth-child(2)
animation-delay: 0.3s; animation-delay: 0.3s;
&:nth-child(3) &:nth-child(3)
animation-delay: 0.6s; animation-delay: 0.6s;
@@ -917,12 +917,12 @@ onMounted(async () => {
.streamers-grid-modern .streamers-grid-modern
gap: 8px; gap: 8px;
padding: 0 4px; padding: 0 4px;
.streamer-card-modern .streamer-card-modern
min-width: 80px; min-width: 80px;
max-width: 90px; max-width: 90px;
padding: 8px 6px; padding: 8px 6px;
&:hover &:hover
transform: translateY(-2px); transform: translateY(-2px);
@@ -930,16 +930,16 @@ onMounted(async () => {
.streamers-grid-modern .streamers-grid-modern
gap: 6px; gap: 6px;
padding: 0 4px; padding: 0 4px;
.streamer-card-modern .streamer-card-modern
min-width: 75px; min-width: 75px;
max-width: 85px; max-width: 85px;
padding: 8px 6px; padding: 8px 6px;
.streamer-avatar-wrapper .streamer-avatar-wrapper
width: 45px; width: 45px;
height: 45px; height: 45px;
.streamer-name .streamer-name
font-size: 0.8rem; font-size: 0.8rem;

View File

@@ -516,7 +516,7 @@ onUnmounted(() => {
</NSpace> </NSpace>
</NSpace> </NSpace>
<NDivider /> <NDivider />
<NSpin :show="loading"> <NSpin :show="loading">
<!-- 空状态 --> <!-- 空状态 -->
<NEmpty <NEmpty
@@ -637,7 +637,7 @@ onUnmounted(() => {
</div> </div>
</NCard> </NCard>
</NGridItem> </NGridItem>
<NGridItem> <NGridItem>
<NCard <NCard
title="近30天统计" title="近30天统计"
@@ -734,7 +734,7 @@ onUnmounted(() => {
</div> </div>
</NCard> </NCard>
</NGridItem> </NGridItem>
<NGridItem> <NGridItem>
<NCard <NCard
title="关键指标" title="关键指标"
@@ -809,9 +809,9 @@ onUnmounted(() => {
</NGridItem> </NGridItem>
</NGrid> </NGrid>
</div> </div>
<NDivider /> <NDivider />
<!-- 图表选择器 --> <!-- 图表选择器 -->
<div class="chart-selector"> <div class="chart-selector">
<NTabs <NTabs

View File

@@ -12,13 +12,20 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"types": ["node", "vue-vine/macros", "jszip"], "types": ["node", "vue-vine/macros", "jszip"],
"allowJs": false, "allowJs": false,
"strict": true, "strict": false,
"alwaysStrict": false,
"noImplicitAny": false,
"noImplicitThis": false,
"strictBindCallApply": false,
"strictFunctionTypes": false,
"strictNullChecks": false,
"strictPropertyInitialization": false,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"sourceMap": true, "isolatedModules": true,
"isolatedModules": true "skipLibCheck": true,
"sourceMap": true
}, },
"references": [{ "path": "./tsconfig.node.json" }], "references": [{ "path": "./tsconfig.node.json" }],
"include": [ "include": [