大哥 主播 即时消息 三合一

This commit is contained in:
2026-02-04 19:56:19 +08:00
parent 85e5d1ccb7
commit 791560af2e
52 changed files with 8324 additions and 4611 deletions

311
src/pages/UpdateChecker.vue Normal file
View File

@@ -0,0 +1,311 @@
<template>
<!-- Electron 环境不渲染 -->
<div v-if="isElectronEnv"
class="min-h-screen bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center p-6">
<!-- 背景装饰 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div class="absolute -top-40 -right-40 w-96 h-96 bg-blue-100 rounded-full blur-3xl opacity-50" />
<div class="absolute -bottom-40 -left-40 w-96 h-96 bg-purple-100 rounded-full blur-3xl opacity-50" />
</div>
<div class="relative z-10 w-full max-w-md">
<!-- Logo 和标题 -->
<div class="text-center mb-8">
<div
class="w-16 h-16 mx-auto mb-4 bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl flex items-center justify-center shadow-lg">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</div>
<h1 class="text-2xl font-bold text-gray-900 mb-2">应用更新检查</h1>
<p class="text-gray-500 text-sm">当前版本: v{{ currentVersion || '...' }}</p>
</div>
<!-- 更新卡片 -->
<div class="bg-white rounded-2xl border border-gray-100 shadow-xl p-6">
<!-- 检查中 -->
<div v-if="status === 'checking' && !showTimeoutError" class="text-center py-8">
<div
class="w-12 h-12 mx-auto mb-4 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
<p class="text-gray-900 font-medium">正在检查更新...</p>
<p class="text-gray-500 text-sm mt-2">
{{ retryCount > 0 ? `${retryCount}/${MAX_RETRIES} 次重试...` : '请稍候' }}
</p>
</div>
<!-- 超时错误 -->
<div v-if="showTimeoutError" class="space-y-6 py-4">
<div class="text-center">
<div
class="w-16 h-16 mx-auto mb-4 bg-orange-50 rounded-full flex items-center justify-center">
<svg class="w-8 h-8 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h2 class="text-xl font-bold text-gray-900">检查更新超时</h2>
<p class="text-orange-600 text-sm mt-2">无法连接到更新服务器</p>
</div>
<button @click="handleRetry"
class="w-full py-3 bg-gradient-to-r from-blue-600 to-blue-500 text-white rounded-lg font-medium hover:from-blue-700 hover:to-blue-600 transition-all shadow-sm">
🔄 重新检查
</button>
<p class="text-center text-gray-400 text-xs">
请检查网络连接后重试
</p>
</div>
<!-- 发现新版本 -->
<div v-if="status === 'available' && updateInfo" class="space-y-6">
<div class="text-center">
<div class="w-16 h-16 mx-auto mb-4 bg-green-50 rounded-full flex items-center justify-center">
<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 class="text-xl font-bold text-gray-900">发现新版本</h2>
<p class="text-green-600 font-mono mt-2">v{{ updateInfo.version }}</p>
</div>
<div v-if="updateInfo.releaseNotes" class="bg-gray-50 rounded-lg p-3 max-h-32 overflow-y-auto">
<p class="text-gray-600 text-sm whitespace-pre-wrap">{{ updateInfo.releaseNotes }}</p>
</div>
<button @click="downloadUpdate"
class="w-full py-3 bg-gradient-to-r from-blue-600 to-blue-500 text-white rounded-lg font-medium hover:from-blue-700 hover:to-blue-600 transition-all shadow-sm">
立即下载更新
</button>
<p class="text-center text-gray-400 text-xs">
必须更新后才能使用程序
</p>
</div>
<!-- 下载中 -->
<div v-if="status === 'downloading' && progress" class="space-y-6 py-4">
<div class="text-center">
<p class="text-gray-900 font-medium mb-1">正在下载更新</p>
<p class="text-4xl font-bold text-blue-600">{{ progress.percent.toFixed(0) }}%</p>
</div>
<div class="space-y-2">
<div class="h-3 bg-gray-100 rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-blue-500 to-blue-600 transition-all duration-300 rounded-full"
:style="{ width: `${progress.percent}%` }" />
</div>
<div class="flex justify-between text-xs text-gray-500">
<span>{{ formatBytes(progress.transferred) }} / {{ formatBytes(progress.total) }}</span>
<span>{{ formatBytes(progress.bytesPerSecond) }}/s</span>
</div>
</div>
<p class="text-center text-gray-400 text-sm">
请勿关闭程序...
</p>
</div>
<!-- 下载完成 - 自动重启安装 -->
<div v-if="status === 'downloaded'" class="space-y-6 py-4">
<div class="text-center">
<div class="w-16 h-16 mx-auto mb-4 bg-green-50 rounded-full flex items-center justify-center">
<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 class="text-xl font-bold text-gray-900">下载完成</h2>
<p class="text-green-600 text-sm mt-2">
{{ countdown > 0 ? `${countdown} 秒后自动重启安装...` : '正在重启安装...' }}
</p>
</div>
<button @click="installUpdate"
class="w-full py-3 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-lg font-medium hover:from-green-600 hover:to-green-700 transition-all shadow-sm">
🚀 立即重启安装
</button>
</div>
<!-- 错误 -->
<div v-if="status === 'error'" class="space-y-6 py-4">
<div class="text-center">
<div class="w-16 h-16 mx-auto mb-4 bg-red-50 rounded-full flex items-center justify-center">
<svg class="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h2 class="text-xl font-bold text-gray-900">检查更新失败</h2>
<p class="text-red-500 text-sm mt-2">{{ error }}</p>
</div>
<div class="flex gap-3">
<button @click="startCheck"
class="flex-1 py-3 bg-gradient-to-r from-blue-600 to-blue-500 text-white rounded-lg font-medium hover:from-blue-700 hover:to-blue-600 transition-all shadow-sm">
🔄 重试
</button>
<button @click="emit('ready')"
class="flex-1 py-3 bg-gray-100 text-gray-600 rounded-lg font-medium hover:bg-gray-200 transition-all border border-gray-200">
跳过继续
</button>
</div>
<p class="text-center text-gray-400 text-xs">
更新检查失败不影响正常使用
</p>
</div>
</div>
<!-- 底部版权 -->
<p class="text-center text-gray-400 text-xs mt-6">
© 2025 Yolo
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { useUpdate } from '../hooks/useUpdate'
import { isElectron } from '../utils/electronBridge'
const emit = defineEmits(['ready'])
const CHECK_TIMEOUT = 15000 // 15秒超时
const MAX_RETRIES = 3 // 最大重试次数
const AUTO_INSTALL_DELAY = 3 // 自动安装倒计时秒数
const {
status,
updateInfo,
progress,
error,
currentVersion,
checkForUpdates,
downloadUpdate,
installUpdate
} = useUpdate()
const isElectronEnv = isElectron()
const checkComplete = ref(false)
const retryCount = ref(0)
const isTimeout = ref(false)
const showTimeoutError = ref(false)
const countdown = ref(AUTO_INSTALL_DELAY)
let timeoutTimer = null
let hasStarted = false
let countdownTimer = null
// 非 Electron 环境直接 ready
onMounted(() => {
if (!isElectronEnv) {
emit('ready')
return
}
// 启动检查
if (!hasStarted) {
hasStarted = true
startCheck()
}
})
// 监听状态
watch(status, (newStatus) => {
if (newStatus !== 'checking' && timeoutTimer) {
clearTimeout(timeoutTimer)
timeoutTimer = null
isTimeout.value = false
}
if (newStatus === 'idle' && checkComplete.value) {
emit('ready')
}
if (newStatus !== 'checking' && newStatus !== 'idle') {
checkComplete.value = true
}
if (newStatus === 'idle') {
setTimeout(() => {
checkComplete.value = true
}, 500)
}
// Auto install timer
if (newStatus === 'downloaded') {
startCountdown()
}
})
watch(isTimeout, (val) => {
if (val) {
if (retryCount.value >= MAX_RETRIES) {
console.log('[UpdateChecker] 更新检查超时,显示错误')
showTimeoutError.value = true
} else if (retryCount.value > 0) {
setTimeout(() => {
startCheck()
}, 2000)
}
}
})
watch(checkComplete, (val) => {
if (val && status.value === 'idle') {
emit('ready')
}
})
function startCheck() {
if (!isElectronEnv) return
isTimeout.value = false
checkForUpdates()
timeoutTimer = setTimeout(() => {
if (status.value === 'checking') {
isTimeout.value = true
if (retryCount.value < MAX_RETRIES) {
retryCount.value++
}
}
}, CHECK_TIMEOUT)
}
function handleRetry() {
showTimeoutError.value = false
retryCount.value = 0
hasStarted = false
startCheck()
}
function startCountdown() {
if (countdownTimer) clearInterval(countdownTimer)
countdown.value = AUTO_INSTALL_DELAY
countdownTimer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(countdownTimer)
installUpdate()
}
}, 1000)
}
function formatBytes(bytes) {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`
}
onUnmounted(() => {
if (timeoutTimer) clearTimeout(timeoutTimer)
if (countdownTimer) clearInterval(countdownTimer)
})
</script>