初始化
This commit is contained in:
1023
src/pages/ConfigPage.tsx
Normal file
1023
src/pages/ConfigPage.tsx
Normal file
File diff suppressed because it is too large
Load Diff
269
src/pages/LoginPage.tsx
Normal file
269
src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { isElectron, getAppVersion } from '../utils/electronBridge'
|
||||
import logo from '../assets/logo.png'
|
||||
import illustration from '../assets/illustration.png'
|
||||
|
||||
const STORAGE_KEY = 'login_credentials'
|
||||
const USER_KEY = 'user_data'
|
||||
|
||||
interface LoginCredentials {
|
||||
tenantName: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
interface LoginPageProps {
|
||||
onLoginSuccess: () => void
|
||||
}
|
||||
|
||||
function LoginPage({ onLoginSuccess }: LoginPageProps) {
|
||||
const [credentials, setCredentials] = useState<LoginCredentials>({
|
||||
tenantName: '',
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [version, setVersion] = useState('')
|
||||
|
||||
// 获取应用版本号
|
||||
useEffect(() => {
|
||||
const fetchVersion = async () => {
|
||||
const v = await getAppVersion()
|
||||
setVersion(v)
|
||||
}
|
||||
fetchVersion()
|
||||
}, [])
|
||||
|
||||
// 加载保存的凭据
|
||||
useEffect(() => {
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved)
|
||||
// 兼容旧数据格式:userId → username
|
||||
setCredentials({
|
||||
tenantName: data.tenantName || '',
|
||||
username: data.username || data.userId || '',
|
||||
password: data.password || '',
|
||||
})
|
||||
}
|
||||
} catch { }
|
||||
}, [])
|
||||
|
||||
const handleChange = (field: keyof LoginCredentials, value: string) => {
|
||||
setCredentials(prev => ({ ...prev, [field]: value }))
|
||||
setError('')
|
||||
}
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent) => {
|
||||
e?.preventDefault()
|
||||
|
||||
if (!credentials.tenantName || !credentials.username || !credentials.password) {
|
||||
setError('请填写所有字段')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
// 保存凭据
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(credentials))
|
||||
|
||||
console.log('[LoginPage] 开始登录...', credentials)
|
||||
|
||||
if (!isElectron()) {
|
||||
// 非 Electron 环境,模拟登录成功
|
||||
setError('非 Electron 环境,无法进行真实登录')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用登录 API
|
||||
const result = await window.electronAPI!.login(credentials)
|
||||
console.log('[LoginPage] 登录结果:', result)
|
||||
|
||||
if (result.success && result.user) {
|
||||
// 保存用户信息
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(result.user))
|
||||
onLoginSuccess()
|
||||
} else {
|
||||
setError(result.error || '登录失败')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '登录失败')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSubmit()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F0F4F8] flex items-center justify-center font-sans antialiased relative overflow-hidden transition-colors duration-300">
|
||||
{/* Background Shapes */}
|
||||
<div
|
||||
className="absolute top-[-200px] right-[-200px] w-[800px] h-[800px] rounded-full mix-blend-multiply filter blur-3xl opacity-70 animate-pulse"
|
||||
style={{ background: 'radial-gradient(circle, rgba(79, 129, 230, 0.2) 0%, rgba(79, 129, 230, 0) 70%)' }}
|
||||
/>
|
||||
<div
|
||||
className="absolute bottom-[-100px] left-[-100px] w-[600px] h-[600px] rounded-full mix-blend-multiply filter blur-3xl opacity-70 animate-pulse"
|
||||
style={{
|
||||
background: 'radial-gradient(circle, rgba(236, 72, 153, 0.15) 0%, rgba(236, 72, 153, 0) 70%)',
|
||||
animationDuration: '4s'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="container mx-auto px-4 z-10 relative flex justify-center items-center h-full">
|
||||
<div className="bg-white/70 backdrop-blur-xl w-full max-w-5xl rounded-[2rem] overflow-hidden flex flex-col md:flex-row shadow-2xl border border-white/20">
|
||||
|
||||
{/* Left Side: Form */}
|
||||
<div className="w-full md:w-1/2 p-8 md:p-12 lg:p-16 flex flex-col justify-center">
|
||||
{/* Header / Logo */}
|
||||
<div className="flex justify-center">
|
||||
<img src={logo} alt="Logo" className="w-[200px] h-auto" />
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-800 mb-2">欢迎回来</h1>
|
||||
<p className="text-gray-500 text-sm">请输入您的账户信息以登录平台</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
{/* 租户号 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">租户号</label>
|
||||
<div className="relative rounded-md shadow-sm">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={credentials.tenantName}
|
||||
onChange={(e) => handleChange('tenantName', e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="请输入租户号"
|
||||
className="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 账号 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">账号</label>
|
||||
<div className="relative rounded-md shadow-sm">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={credentials.username}
|
||||
onChange={(e) => handleChange('username', e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="请输入账号"
|
||||
className="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
||||
<div className="relative rounded-md shadow-sm">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
value={credentials.password}
|
||||
onChange={(e) => handleChange('password', e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="请输入密码"
|
||||
className="focus:ring-[#4F81E6] focus:border-[#4F81E6] block w-full pl-10 sm:text-sm border-gray-300 rounded-lg py-3 transition-colors bg-gray-50 focus:bg-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误提示 */}
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 text-red-600 text-xs bg-red-50 p-2.5 rounded-lg">
|
||||
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 登录按钮 */}
|
||||
<div className="pt-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-500 to-[#4F81E6] hover:from-blue-600 hover:to-[#3A6BC7] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#4F81E6] transition-all transform hover:scale-[1.01] disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
登录中
|
||||
</span>
|
||||
) : '登 录'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
<div className="mt-8 text-center">
|
||||
<span className="text-gray-300 text-xs font-mono">v{version}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Side: Illustration */}
|
||||
<div className="hidden md:flex w-1/2 bg-blue-50/50 relative items-center justify-center p-12 overflow-hidden">
|
||||
{/* Decorative Circle matches login.html style */}
|
||||
<div className="absolute w-96 h-96 bg-blue-100 rounded-full blur-3xl opacity-30 top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2"></div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-sm">
|
||||
<img
|
||||
src={illustration}
|
||||
alt="Illustration"
|
||||
className="w-full h-auto drop-shadow-xl animate-float"
|
||||
style={{ animation: 'float 6s ease-in-out infinite' }}
|
||||
/>
|
||||
<div className="text-center mt-8">
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-2">连接全球创意</h3>
|
||||
<p className="text-gray-500 text-sm">高效管理您的TikTok矩阵,释放无限潜能</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating Animation Keyframe Style */}
|
||||
<style>{`
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-15px); }
|
||||
100% { transform: translateY(0px); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
365
src/pages/UpdateChecker.tsx
Normal file
365
src/pages/UpdateChecker.tsx
Normal file
@@ -0,0 +1,365 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useUpdate } from '../hooks/useUpdate'
|
||||
import { isElectron } from '../utils/electronBridge'
|
||||
|
||||
interface UpdateCheckerProps {
|
||||
onReady: () => void // 无更新或更新完成后调用
|
||||
}
|
||||
|
||||
const CHECK_TIMEOUT = 15000 // 15秒超时
|
||||
const MAX_RETRIES = 3 // 最大重试次数
|
||||
const AUTO_INSTALL_DELAY = 3 // 自动安装倒计时秒数
|
||||
|
||||
/**
|
||||
* 自动安装倒计时组件
|
||||
*/
|
||||
function AutoInstallCountdown({ installUpdate }: { installUpdate: () => void }) {
|
||||
const [countdown, setCountdown] = useState(AUTO_INSTALL_DELAY)
|
||||
|
||||
useEffect(() => {
|
||||
if (countdown <= 0) {
|
||||
installUpdate()
|
||||
return
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setCountdown(prev => prev - 1)
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [countdown, installUpdate])
|
||||
|
||||
return (
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-green-50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900">下载完成</h2>
|
||||
<p className="text-green-600 text-sm mt-2">
|
||||
{countdown > 0 ? `${countdown} 秒后自动重启安装...` : '正在重启安装...'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={installUpdate}
|
||||
className="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>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制更新检查页面
|
||||
* 程序启动时显示,必须完成更新才能进入主程序
|
||||
* 注意:仅在 Electron 环境中有效
|
||||
*/
|
||||
export default function UpdateChecker({ onReady }: UpdateCheckerProps) {
|
||||
const {
|
||||
status,
|
||||
updateInfo,
|
||||
progress,
|
||||
error,
|
||||
currentVersion,
|
||||
checkForUpdates,
|
||||
downloadUpdate,
|
||||
installUpdate
|
||||
} = useUpdate()
|
||||
|
||||
const [checkComplete, setCheckComplete] = useState(false)
|
||||
const [retryCount, setRetryCount] = useState(0)
|
||||
const [isTimeout, setIsTimeout] = useState(false)
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const hasStartedRef = useRef(false)
|
||||
|
||||
// 非 Electron 环境直接跳过更新检查
|
||||
useEffect(() => {
|
||||
if (!isElectron()) {
|
||||
onReady()
|
||||
}
|
||||
}, [onReady])
|
||||
|
||||
// 启动检查更新(带超时)
|
||||
const startCheck = useCallback(() => {
|
||||
if (!isElectron()) return
|
||||
|
||||
setIsTimeout(false)
|
||||
checkForUpdates()
|
||||
|
||||
// 设置超时
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
if (status === 'checking') {
|
||||
setIsTimeout(true)
|
||||
// 超时后自动重试
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
setRetryCount(prev => prev + 1)
|
||||
}
|
||||
}
|
||||
}, CHECK_TIMEOUT)
|
||||
}, [checkForUpdates, status, retryCount])
|
||||
|
||||
// 清理超时定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 启动时自动检查更新(只执行一次)
|
||||
useEffect(() => {
|
||||
if (!hasStartedRef.current && isElectron()) {
|
||||
hasStartedRef.current = true
|
||||
startCheck()
|
||||
}
|
||||
}, [startCheck])
|
||||
|
||||
// 超时处理状态
|
||||
const [showTimeoutError, setShowTimeoutError] = useState(false)
|
||||
|
||||
// 超时后自动重试,重试次数用完后显示错误
|
||||
useEffect(() => {
|
||||
if (isTimeout) {
|
||||
if (retryCount >= MAX_RETRIES) {
|
||||
// 重试次数用完,显示超时错误
|
||||
console.log('[UpdateChecker] 更新检查超时,显示错误')
|
||||
setShowTimeoutError(true)
|
||||
} else if (retryCount > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
startCheck()
|
||||
}, 2000) // 2秒后重试
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}, [isTimeout, retryCount, startCheck])
|
||||
|
||||
// 监听状态变化
|
||||
useEffect(() => {
|
||||
// 状态不再是 checking,清除超时
|
||||
if (status !== 'checking' && timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current)
|
||||
timeoutRef.current = null
|
||||
setIsTimeout(false)
|
||||
}
|
||||
|
||||
// 检查完成且无更新,直接进入程序
|
||||
if (status === 'idle' && checkComplete) {
|
||||
onReady()
|
||||
}
|
||||
}, [status, checkComplete, onReady])
|
||||
|
||||
// 标记检查已完成(从 checking 变为其他状态时)
|
||||
useEffect(() => {
|
||||
if (status !== 'checking' && status !== 'idle') {
|
||||
setCheckComplete(true)
|
||||
}
|
||||
// 如果检查后直接变成 idle(无更新),也标记完成
|
||||
if (status === 'idle') {
|
||||
const timer = setTimeout(() => {
|
||||
setCheckComplete(true)
|
||||
}, 500) // 等待0.5秒确认无更新
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [status])
|
||||
|
||||
// 自动开始下载(发现更新后)
|
||||
const handleDownload = useCallback(() => {
|
||||
downloadUpdate()
|
||||
}, [downloadUpdate])
|
||||
|
||||
// 非 Electron 环境不渲染
|
||||
if (!isElectron()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center p-6">
|
||||
{/* 背景装饰 */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -right-40 w-96 h-96 bg-blue-100 rounded-full blur-3xl opacity-50" />
|
||||
<div className="absolute -bottom-40 -left-40 w-96 h-96 bg-purple-100 rounded-full blur-3xl opacity-50" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-md">
|
||||
{/* Logo 和标题 */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="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 className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-2">应用更新检查</h1>
|
||||
<p className="text-gray-500 text-sm">当前版本: v{currentVersion || '...'}</p>
|
||||
</div>
|
||||
|
||||
{/* 更新卡片 */}
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-xl p-6">
|
||||
|
||||
{/* 检查中 */}
|
||||
{status === 'checking' && !showTimeoutError && (
|
||||
<div className="text-center py-8">
|
||||
<div className="w-12 h-12 mx-auto mb-4 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-gray-900 font-medium">正在检查更新...</p>
|
||||
<p className="text-gray-500 text-sm mt-2">
|
||||
{retryCount > 0 ? `第 ${retryCount}/${MAX_RETRIES} 次重试...` : '请稍候'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 超时错误 */}
|
||||
{showTimeoutError && (
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-orange-50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900">检查更新超时</h2>
|
||||
<p className="text-orange-600 text-sm mt-2">无法连接到更新服务器</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowTimeoutError(false)
|
||||
setRetryCount(0)
|
||||
hasStartedRef.current = false
|
||||
startCheck()
|
||||
}}
|
||||
className="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 className="text-center text-gray-400 text-xs">
|
||||
请检查网络连接后重试
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 发现新版本 */}
|
||||
{status === 'available' && updateInfo && (
|
||||
<div className="space-y-6">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-green-50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900">发现新版本</h2>
|
||||
<p className="text-green-600 font-mono mt-2">v{updateInfo.version}</p>
|
||||
</div>
|
||||
|
||||
{updateInfo.releaseNotes && (
|
||||
<div className="bg-gray-50 rounded-lg p-3 max-h-32 overflow-y-auto">
|
||||
<p className="text-gray-600 text-sm whitespace-pre-wrap">{updateInfo.releaseNotes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="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 className="text-center text-gray-400 text-xs">
|
||||
必须更新后才能使用程序
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 下载中 */}
|
||||
{status === 'downloading' && progress && (
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-900 font-medium mb-1">正在下载更新</p>
|
||||
<p className="text-4xl font-bold text-blue-600">{progress.percent.toFixed(0)}%</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="h-3 bg-gray-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-blue-600 transition-all duration-300 rounded-full"
|
||||
style={{ width: `${progress.percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="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 className="text-center text-gray-400 text-sm">
|
||||
请勿关闭程序...
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 下载完成 - 自动重启安装 */}
|
||||
{status === 'downloaded' && (
|
||||
<AutoInstallCountdown installUpdate={installUpdate} />
|
||||
)}
|
||||
|
||||
{/* 错误 */}
|
||||
{status === 'error' && (
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 bg-red-50 rounded-full flex items-center justify-center">
|
||||
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-gray-900">检查更新失败</h2>
|
||||
<p className="text-red-500 text-sm mt-2">{error}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={startCheck}
|
||||
className="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
|
||||
onClick={onReady}
|
||||
className="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 className="text-center text-gray-400 text-xs">
|
||||
更新检查失败不影响正常使用
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部版权 */}
|
||||
<p className="text-center text-gray-400 text-xs mt-6">
|
||||
© 2025 Yolo
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节数
|
||||
*/
|
||||
function formatBytes(bytes: number): string {
|
||||
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]}`
|
||||
}
|
||||
Reference in New Issue
Block a user