Repository navigation
Expand file tree
/
Copy pathUpdateNotification.tsx
More file actions
153 lines (141 loc) · 6.78 KB
/
Copy pathUpdateNotification.tsx
File metadata and controls
153 lines (141 loc) · 6.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/**
* 更新通知组件
*
* 监听 autoUpdater 的 update-status 事件,
* 显示更新状态(发现更新 → 下载进度 → 安装确认)。
* 发现新版本时额外展示 releaseNotes 折叠面板。
*
* @ai-context: UI 基础组件(shadcn/radix 封装):UpdateNotification。
*/
import { useState, useEffect } from 'react';
import DOMPurify from 'dompurify';
import { Download, RefreshCw, X, ChevronDown, AlertCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
interface UpdateStatus {
status: 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
version?: string;
percent?: number;
message?: string;
releaseNotes?: string | null;
/** 重试剩余等待毫秒数(下载重试中) */
retryIn?: number;
/** 当前重试次数 */
retryCount?: number;
/** 最大重试次数 */
maxRetries?: number;
}
export default function UpdateNotification() {
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null);
const [dismissed, setDismissed] = useState(false);
const [notesOpen, setNotesOpen] = useState(false);
useEffect(() => {
if (!window.electronAPI?.on) return;
const cleanup = window.electronAPI.on('update-status', (data: unknown) => {
setUpdateStatus(data as UpdateStatus);
setDismissed(false);
});
return cleanup;
}, []);
if (!updateStatus || dismissed) return null;
if (updateStatus.status === 'not-available' || updateStatus.status === 'checking') return null;
const handleDownload = () => {
window.electronAPI?.invoke('update:download');
};
const handleInstall = () => {
window.electronAPI?.invoke('update:install');
};
/** 简易 Markdown → HTML(仅处理常见格式,不做完整解析) */
const renderReleaseNotes = (notes: string): string => {
return notes
// h2 / h3
.replace(/^### (.+)$/gm, '<h3 class="text-b3 font-semibold text-text-primary mt-2 mb-1">$1</h3>')
.replace(/^## (.+)$/gm, '<h2 class="text-b2 font-semibold text-text-primary mt-2 mb-1">$1</h2>')
// 无序列表
.replace(/^- (.+)$/gm, '<li class="ml-4 list-disc text-b3 text-text-secondary">$1</li>')
// 行内代码
.replace(/`([^`]+)`/g, '<code class="px-1 py-0.5 rounded bg-bg-tertiary text-b3 font-mono">$1</code>')
// 粗体
.replace(/\*\*([^*]+)\*\*/g, '<strong class="font-semibold text-text-primary">$1</strong>')
// 换行
.replace(/\n/g, '<br/>');
};
return (
<div className="fixed bottom-4 right-4 z-50 max-w-sm bg-bg-elevated border border-border rounded-kb-lg shadow-kb-lg p-kb-md animate-fade-in-up">
<div className="flex items-start justify-between gap-kb-sm">
<div className="flex-1">
{updateStatus.status === 'available' && (
<>
<p className="text-b1 font-medium text-text-primary">
发现新版本 v{updateStatus.version}
</p>
<p className="text-b3 text-text-secondary mt-1">点击下方按钮开始下载</p>
{/* releaseNotes 折叠面板 */}
{updateStatus.releaseNotes && (
<div className="mt-2">
<button
onClick={() => setNotesOpen(!notesOpen)}
className={cn(
'flex items-center gap-1 text-b3 text-text-tertiary hover:text-text-secondary transition-colors',
)}
>
<ChevronDown className={cn('w-icon-xs h-icon-xs transition-transform', notesOpen && 'rotate-180')} />
{notesOpen ? '收起更新内容' : '查看更新内容'}
</button>
{notesOpen && (
<div
className="mt-1.5 p-2.5 rounded-kb-md bg-bg-card text-text-secondary text-b3 leading-relaxed max-h-48 overflow-y-auto"
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(renderReleaseNotes(String(updateStatus.releaseNotes))) }}
/>
)}
</div>
)}
<button onClick={handleDownload} className="mt-2 inline-flex items-center gap-1 px-3 py-1.5 bg-brand-500 text-white rounded-kb-md text-b3 hover:bg-brand-600 transition-colors">
<Download className="w-icon-xs h-icon-xs" /> 下载更新
</button>
</>
)}
{updateStatus.status === 'downloading' && (
<>
<p className="text-b1 font-medium text-text-primary">
{updateStatus.retryCount ? `下载失败,正在重试 (${updateStatus.retryCount}/${updateStatus.maxRetries})...` : '正在下载更新...'}
</p>
<div className="mt-2 w-full bg-bg-tertiary rounded-kb-full h-2">
<div className="h-full bg-brand-500 rounded-kb-full transition-all" style={{ width: `${updateStatus.percent || 0}%` }} />
</div>
<p className="text-b3 text-text-secondary mt-1">{updateStatus.percent || 0}%</p>
</>
)}
{updateStatus.status === 'downloaded' && (
<>
<p className="text-b1 font-medium text-text-primary">更新已就绪 v{updateStatus.version}</p>
<button onClick={handleInstall} className="mt-2 inline-flex items-center gap-1 px-3 py-1.5 bg-brand-500 text-white rounded-kb-md text-b3 hover:bg-brand-600 transition-colors">
<RefreshCw className="w-icon-xs h-icon-xs" /> 安装并重启
</button>
</>
)}
{updateStatus.status === 'error' && (
<>
<p className="flex items-center gap-1.5 text-b1 font-medium text-text-primary">
<AlertCircle className="w-icon-sm h-icon-sm shrink-0 text-semantic-error" />
更新检查失败
</p>
<p className="text-b3 text-text-secondary mt-1 break-words">
{updateStatus.message}
</p>
<p className="text-b3 text-text-tertiary mt-1">不影响正常使用,可稍后在设置中手动检查更新。</p>
<button
onClick={() => setDismissed(true)}
className="mt-2 inline-flex items-center gap-1 px-3 py-1.5 bg-bg-tertiary text-text-primary rounded-kb-md text-b3 hover:bg-bg-card transition-colors"
>
<X className="w-icon-xs h-icon-xs" /> 关闭
</button>
</>
)}
</div>
<button onClick={() => setDismissed(true)} className="text-text-tertiary hover:text-text-secondary">
<X className="w-icon-sm h-icon-sm" />
</button>
</div>
</div>
);
}