返回 oh-my-ppt
HtmlEditorHistoryDialog.tsx
根目录 / src / renderer / src / components / html-editor / HtmlEditorHistoryDialog.tsx
1 import { useEffect, useState } from 'react'
2 import { History, Loader2, RotateCcw } from 'lucide-react'
3 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/Dialog'
4 import { ipc } from '../../lib/ipc'
5 import { useT } from '../../i18n'
6 import { useToastStore } from '../../store/toastStore'
7
8 interface VersionRow {
9 id: string
10 commitSha: string
11 message: string
12 createdAt: number
13 }
14
15 /** HTML 编辑器版本历史:列出该文档的 git 提交(版本表记录),可恢复到任一版本。 */
16 export function HtmlEditorHistoryDialog({
17 open,
18 onOpenChange,
19 docId,
20 onRestored
21 }: {
22 open: boolean
23 onOpenChange: (open: boolean) => void
24 docId: string
25 onRestored: (html: string) => void
26 }): React.JSX.Element {
27 const t = useT()
28 const [versions, setVersions] = useState<VersionRow[]>([])
29 const [loading, setLoading] = useState(false)
30 const [restoring, setRestoring] = useState<string | null>(null)
31
32 useEffect(() => {
33 if (!open || !docId) return
34 setLoading(true)
35 ipc
36 .listHtmlVersions({ docId })
37 .then((r) => setVersions(r.versions))
38 .catch((error) => {
39 useToastStore
40 .getState()
41 .error(error instanceof Error ? error.message : t('common.retryLater'))
42 setVersions([])
43 })
44 .finally(() => setLoading(false))
45 }, [open, docId, t])
46
47 const handleRestore = async (versionId: string): Promise<void> => {
48 setRestoring(versionId)
49 try {
50 const { html } = await ipc.restoreHtmlVersion({ docId, versionId })
51 onRestored(html)
52 onOpenChange(false)
53 } catch (error) {
54 useToastStore
55 .getState()
56 .error(error instanceof Error ? error.message : t('common.retryLater'))
57 } finally {
58 setRestoring(null)
59 }
60 }
61
62 const fmtTime = (ms: number): string => new Date(ms).toLocaleString()
63
64 return (
65 <Dialog open={open} onOpenChange={onOpenChange}>
66 <DialogContent className="max-h-[80vh] w-[480px] overflow-y-auto">
67 <DialogHeader>
68 <DialogTitle className="flex items-center gap-2">
69 <History className="h-4 w-4" />
70 {t('htmlEditor.history')}
71 </DialogTitle>
72 </DialogHeader>
73 {loading ? (
74 <div className="flex items-center justify-center py-8 text-sm text-[#8a8676]">
75 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
76 {t('common.loading')}
77 </div>
78 ) : versions.length === 0 ? (
79 <div className="py-8 text-center text-sm text-[#8a8676]">
80 {t('htmlEditor.historyEmpty')}
81 </div>
82 ) : (
83 <ul className="space-y-1.5">
84 {versions.map((v) => (
85 <li
86 key={v.id}
87 className="flex items-center gap-2 rounded-md border border-[#e2dccf] bg-[#f5f1e8] px-3 py-2"
88 >
89 <div className="min-w-0 flex-1">
90 <div className="text-sm font-medium text-[#3e4a32]">{v.message}</div>
91 <div className="text-[11px] text-[#8a8676]">
92 {fmtTime(v.createdAt)} · {v.commitSha.slice(0, 7)}
93 </div>
94 </div>
95 <button
96 type="button"
97 onClick={() => void handleRestore(v.id)}
98 disabled={restoring !== null}
99 className="flex items-center gap-1 rounded-md border border-[#c9c0ad] px-2 py-1 text-[11px] text-[#3e4a32] hover:bg-[#ece5d6] disabled:opacity-50"
100 >
101 {restoring === v.id ? (
102 <Loader2 className="h-3 w-3 animate-spin" />
103 ) : (
104 <RotateCcw className="h-3 w-3" />
105 )}
106 {t('htmlEditor.restore')}
107 </button>
108 </li>
109 ))}
110 </ul>
111 )}
112 </DialogContent>
113 </Dialog>
114 )
115 }
116
116 lines Plain Text