From b51da785af6104e918fed939d1646f40fd0032bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=E8=B6=A3=E4=BF=9D=E7=BD=97?= Date: Thu, 29 Jan 2026 12:01:09 +0800 Subject: [PATCH] =?UTF-8?q?Fix:=20=E9=87=8D=E6=9E=84=E9=83=A8=E5=88=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E7=BC=96=E8=BE=91=E5=99=A8=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E9=83=A8=E5=88=86=E9=80=BB=E8=BE=91=E6=8B=86=E5=88=86?= =?UTF-8?q?=EF=BC=8C=E6=95=B0=E6=8D=AE=E6=BA=90=E6=94=B9=E4=B8=BA=E9=80=82?= =?UTF-8?q?=E9=85=8D=E5=99=A8=E6=A8=A1=E5=BC=8F=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=94=AE=E7=9B=98=E5=BF=AB=E6=8D=B7=E9=94=AE=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/biz/editor/common-menubar.tsx | 14 +- src/components/biz/editor/table.tsx | 388 ++++++++ .../biz/editor/use-table-option-state.ts | 16 + src/components/biz/project-sources-wizard.tsx | 10 +- src/components/biz/sync-from-files-button.tsx | 106 --- .../biz/use-editor-keyboard-shortcuts.ts | 87 ++ src/hooks/biz/use-translation-inline-edit.ts | 2 + src/hooks/use-language-adapter.ts | 30 + src/lib/language-source/browser-adapter.ts | 45 + src/lib/language-source/manager.ts | 15 + src/lib/language-source/tauri-adapter.ts | 61 ++ src/lib/language-source/types.ts | 22 + src/pages/editor.tsx | 877 +++++++----------- src/store/sources-store.ts | 7 +- 14 files changed, 1019 insertions(+), 661 deletions(-) create mode 100644 src/components/biz/editor/table.tsx create mode 100644 src/components/biz/editor/use-table-option-state.ts delete mode 100644 src/components/biz/sync-from-files-button.tsx create mode 100644 src/hooks/biz/use-editor-keyboard-shortcuts.ts create mode 100644 src/hooks/use-language-adapter.ts create mode 100644 src/lib/language-source/browser-adapter.ts create mode 100644 src/lib/language-source/manager.ts create mode 100644 src/lib/language-source/tauri-adapter.ts create mode 100644 src/lib/language-source/types.ts diff --git a/src/components/biz/editor/common-menubar.tsx b/src/components/biz/editor/common-menubar.tsx index 7a477bf..e3f093a 100644 --- a/src/components/biz/editor/common-menubar.tsx +++ b/src/components/biz/editor/common-menubar.tsx @@ -3,6 +3,7 @@ import { MenubarContent, MenubarItem, MenubarMenu, + MenubarSeparator, MenubarShortcut, MenubarTrigger, } from "@/components/ui/menubar"; @@ -10,6 +11,7 @@ import { ChevronDown } from "lucide-react"; export type CommonMenubarItem = | "read" + | "read-all" | "save" | "import-with-directory" | "import-with-files" @@ -29,10 +31,14 @@ function CommonMenubar({ disabledItems, onClickItem }: CommonMenubarProps) { onClickItem("read")} disabled={disabledItems?.read}> - 读取 ⌘R + 读取单个文件 ⌘R + onClickItem("read-all")} disabled={disabledItems?.read}> + 自动读取并更新结构 ⌘⇧R + + onClickItem("save")} disabled={disabledItems?.save}> - 保存 ⌘S + 保存所有文件 ⌘S @@ -54,7 +60,9 @@ function CommonMenubar({ disabledItems, onClickItem }: CommonMenubarProps) { 导出 - onClickItem("export")} disabled={disabledItems?.export}>导出 JSON + onClickItem("export")} disabled={disabledItems?.export}> + 手动导出 JSON + diff --git a/src/components/biz/editor/table.tsx b/src/components/biz/editor/table.tsx new file mode 100644 index 0000000..85983a6 --- /dev/null +++ b/src/components/biz/editor/table.tsx @@ -0,0 +1,388 @@ +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Textarea } from "@/components/ui/textarea"; +import { ArrowBigDownDash, ArrowBigUpDash, Brackets, CaseSensitive, Filter, Languages, LocateFixed, MoreVertical, PencilLine, Trash2 } from "lucide-react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type React from "react"; +import { TableVirtuoso, type TableVirtuosoHandle } from "react-virtuoso"; +import { toast } from "sonner"; +import { useClipboard } from "@/hooks/use-clipboard"; +import { useTableOptionState } from "./use-table-option-state"; +import type { FlatEntry } from "@/lib/i18n-structure"; +import type { TranslationInlineEditHandlers } from "@/hooks/biz/use-translation-inline-edit"; + +interface EditorTableProps { + entries: FlatEntry[]; + languages: string[]; + selected: Set; + setSelected: React.Dispatch>>; + inlineEdit: TranslationInlineEditHandlers; + onOpenAddEntry: (path: string, position: "above" | "below") => void; + onOpenAiTranslate: (path: string) => void; + onOpenBulkAiTranslate: () => void; + onOpenRenameEntry: (path: string) => void; + onMoveEntry: (path: string, offset: number) => Promise | void; + onDeleteEntry: (path: string) => Promise | void; + onDeleteSelected: (paths: string[]) => Promise | void; + maxAiItems?: number; +} + +export default function EditorTable({ + entries, + languages, + selected, + setSelected, + inlineEdit, + onOpenAddEntry, + onOpenAiTranslate, + onOpenBulkAiTranslate, + onOpenRenameEntry, + onMoveEntry, + onDeleteEntry, + onDeleteSelected, + maxAiItems = 50, +}: EditorTableProps) { + const { copy } = useClipboard(); + + const [visibleLangs, setVisibleLangs] = useState>(new Set()); + + useEffect(() => { + setVisibleLangs(new Set(languages)); + }, [languages]); + + const virtuosoRef = useRef(null); + const scrollerRootRef = useRef(null); + + const { + query, setQuery, + caseSensitive, setCaseSensitive, + fullMatch, setFullMatch, + } = useTableOptionState(); + + const [moveCountUp, setMoveCountUp] = useState(1); + const [moveCountDown, setMoveCountDown] = useState(1); + + const allSelected = useMemo(() => entries.length > 0 && selected.size === entries.length, [entries, selected]); + const MAX_AI_ITEMS = maxAiItems; + + const highlightRow = useCallback((index: number) => { + const tryFindAndAnimate = (attempt = 0) => { + const root = scrollerRootRef.current as HTMLElement | null; + if (!root) return; + const row = root.querySelector(`tr[data-item-index="${index}"]`) as HTMLTableRowElement | null; + if (row) { + row.animate( + [ + { backgroundColor: "rgba(250, 204, 21, 0.6)" }, + { backgroundColor: "transparent" }, + { backgroundColor: "rgba(250, 204, 21, 0.6)" }, + { backgroundColor: "transparent" }, + ], + { duration: 1200, easing: "ease-in-out" } + ); + return; + } + if (attempt < 10) { + setTimeout(() => tryFindAndAnimate(attempt + 1), 50); + } + }; + requestAnimationFrame(() => tryFindAndAnimate(0)); + }, []); + + const scrollToQuery = useCallback((ev: React.FormEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + if (!query) return; + const idx = entries.findIndex((e) => { + const hay = caseSensitive ? e.path : e.path.toLowerCase(); + const needle = caseSensitive ? query : query.toLowerCase(); + return fullMatch ? hay === needle : hay.includes(needle); + }); + if (idx >= 0) { + virtuosoRef.current?.scrollIntoView({ index: idx, align: "center", done: () => highlightRow(idx) }); + } + }, [caseSensitive, entries, fullMatch, highlightRow, query]); + + const displayedLanguages = useMemo(() => languages.filter((l) => visibleLangs.has(l)), [languages, visibleLangs]); + const tableWidth = useMemo(() => (displayedLanguages.length * 200) + 36 + 60 + 300, [displayedLanguages.length]); + + const virtuosoComponents = useMemo(() => ({ + Table: ({ style, ...props }: React.TableHTMLAttributes) => , + TableHead: (props: React.HTMLAttributes) => , + TableRow: (props: React.HTMLAttributes) => , + TableBody: (props: React.HTMLAttributes) => , + TableFoot: (props: React.HTMLAttributes) => , + }), [tableWidth]); + + const headerContent = useCallback(() => ( + + + + {displayedLanguages.map((lang) => ( + + ))} + + + ), [allSelected, displayedLanguages, entries, setSelected]); + + const renderItemContent = useCallback((_idx: number, entry: FlatEntry) => { + const handleCopy = () => { + copy(entry.path); + toast.success("复制成功"); + }; + + return ( + <> + + + {displayedLanguages.map((lang) => { + const isEditing = inlineEdit.isEditingCell(entry.path, lang); + const isSaving = inlineEdit.isSavingCell(entry.path, lang); + const displayValue = inlineEdit.getDisplayValue(entry.path, lang); + + return ( +
+ { + const next = new Set(); + if (checked) { + for (const en of entries) next.add(en.path); + } + setSelected(next); + }} + /> + 翻译条目名称{lang}操作
+ { + setSelected((prev) => { + const next = new Set(prev); + if (checked) next.add(entry.path); else next.delete(entry.path); + return next; + }); + }} + /> + + + + {isEditing ? ( +