From ff1cb22475126af427b659e58a9c6098169b51d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=E8=B6=A3=E4=BF=9D=E7=BD=97?= Date: Fri, 5 Dec 2025 11:23:19 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E6=96=B0=E5=A2=9E=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E6=8C=89=E9=92=AE=EF=BC=8C=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E6=96=87=E4=BB=B6=E5=86=85=E5=AE=B9=E8=80=8C?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=E4=B8=80=E4=B8=AA=E4=B8=80=E4=B8=AA=E5=86=8D?= =?UTF-8?q?=E6=AC=A1=E9=87=8D=E6=96=B0=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/biz/sync-from-files-button.tsx | 106 ++++++++++++++++++ src/pages/editor.tsx | 7 ++ 2 files changed, 113 insertions(+) create mode 100644 src/components/biz/sync-from-files-button.tsx diff --git a/src/components/biz/sync-from-files-button.tsx b/src/components/biz/sync-from-files-button.tsx new file mode 100644 index 0000000..db81e9f --- /dev/null +++ b/src/components/biz/sync-from-files-button.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { useFileConnections } from "@/store/file-connection"; +import { toast } from "sonner"; +import { upsertLanguageTranslations } from "@/lib/db"; +import { flattenValues } from "@/lib/i18n-structure"; +import { RefreshCw } from "lucide-react"; + +type Props = { + projectId: string; + onSynced: () => void | Promise; + disabled?: boolean; +}; + +async function ensureReadPermission(handle: FileSystemFileHandle): Promise { + try { + // @ts-expect-error - queryPermission/requestPermission exist in supporting browsers + const q = await handle.queryPermission?.({ mode: "read" }); + if (q === "granted") return true; + // @ts-expect-error - requestPermission exist in supporting browsers + const r = await handle.requestPermission?.({ mode: "read" }); + return r === "granted"; + } catch { + try { + // Fallback: try to read once; if it throws, we treat as no permission + const f = await handle.getFile(); + // Touch the file object so TS doesn't complain about unused variable + if (!f) return false; + return true; + } catch { + return false; + } + } +} + +export function SyncFromFilesButton({ projectId, onSynced, disabled }: Props) { + const [syncing, setSyncing] = useState(false); + const connSnap = useFileConnections(projectId); + + const hasConnections = Object.keys(connSnap.connections).length > 0; + + async function handleSync() { + if (!projectId) return; + if (!hasConnections) { + toast.info("没有已连接的语言"); + return; + } + + setSyncing(true); + + try { + const entries = Object.entries(connSnap.connections); + const results = await Promise.allSettled( + entries.map(async ([lang, conn]) => { + const canRead = await ensureReadPermission(conn.handle); + if (!canRead) throw new Error(`${lang}: 无读取权限`); + + const file = await conn.handle.getFile(); + const text = await file.text(); + + let json: unknown; + try { + json = JSON.parse(text); + } catch { + throw new Error(`${lang}: JSON 解析失败`); + } + + const values = flattenValues(json); + await upsertLanguageTranslations(projectId, lang, values); + return lang; + }) + ); + + const failed: string[] = []; + let success = 0; + + for (const r of results) { + if (r.status === "fulfilled") success += 1; + else failed.push((r.reason as Error)?.message || "未知语言"); + } + + if (failed.length === 0) { + toast.success(`同步完成(${success})`); + } else if (success === 0) { + toast.error(`全部失败(${failed.length}):${failed.join(",")}`); + } else { + toast.warning(`部分成功(成功 ${success},失败 ${failed.length}):${failed.join(",")}`); + } + } finally { + setSyncing(false); + await onSynced?.(); + } + } + + return ( + + ); +} diff --git a/src/pages/editor.tsx b/src/pages/editor.tsx index d9eaec6..8c1f8a7 100644 --- a/src/pages/editor.tsx +++ b/src/pages/editor.tsx @@ -41,6 +41,7 @@ import { Textarea } from "@/components/ui/textarea"; import { clearAllConnections, useFileConnections, writeLanguageToConnectedFile } from "@/store/file-connection"; import { generateLanguageJson } from "@/lib/utils"; import { HeaderConnectionIndicator } from "@/components/biz/header-connection-indicator"; +import { SyncFromFilesButton } from "@/components/biz/sync-from-files-button"; export default function Editor() { const { id: projectId } = useParams(); @@ -494,6 +495,12 @@ export default function Editor() { 导出 )} + {projectId && ( + + )} {projectId && (