From cd4dd441a4c1e44f35f2c70ed7c054673bffd203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=87=E8=B6=A3=E4=BF=9D=E7=BD=97?= Date: Wed, 26 Nov 2025 18:01:17 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E4=B8=8A=E4=B8=8B=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E6=9D=A1=E7=9B=AE=E5=8A=9F=E8=83=BD=EF=BC=88=E9=9D=9E=E6=8B=96?= =?UTF-8?q?=E6=8B=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/i18n-structure.ts | 24 ++++++++++++- src/pages/editor.tsx | 72 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 5 deletions(-) diff --git a/src/lib/i18n-structure.ts b/src/lib/i18n-structure.ts index c2532fd..d534c81 100644 --- a/src/lib/i18n-structure.ts +++ b/src/lib/i18n-structure.ts @@ -118,7 +118,11 @@ export function insertEntrySibling( ): StructureNode { const cloned = cloneNode(root); const info = findParentAndIndex(cloned, targetPath); - if (!info) return cloned; + if (!info) { + console.log("未能找到父节点", targetPath); + return cloned; + } + const { parent, index } = info; if (!parent.children) parent.children = []; const exists = parent.children.some((c) => c.key === newKey); @@ -153,4 +157,22 @@ export function renameEntryAtPath(root: StructureNode, path: string, newKey: str return { root: cloned, newPath: segments.join(".") }; } +export function moveEntryByOffset(root: StructureNode, path: string, offset: number): StructureNode { + const cloned = cloneNode(root); + if (!offset) return cloned; + const info = findParentAndIndex(cloned, path); + if (!info) return cloned; + const { parent, index } = info; + const siblings = parent.children ?? []; + if (siblings.length <= 1) return cloned; + const nextIndex = Math.max(0, Math.min(siblings.length - 1, index + offset)); + if (nextIndex === index) return cloned; + const [node] = siblings.splice(index, 1); + siblings.splice(nextIndex, 0, node); + + console.log("cloned", cloned); + + return cloned; +} + diff --git a/src/pages/editor.tsx b/src/pages/editor.tsx index 01758f2..d9eaec6 100644 --- a/src/pages/editor.tsx +++ b/src/pages/editor.tsx @@ -19,7 +19,7 @@ import { } from "@/lib/db"; import { ArrowBigDownDash, ArrowBigUpDash, ArrowLeft, Brackets, CaseSensitive, Download, Filter, Languages, LocateFixed, MoreVertical, PencilLine, Reply, Save, Settings, Trash2 } from "lucide-react"; import { useTranslationInlineEdit } from "@/hooks/biz/use-translation-inline-edit"; -import { flattenEntries, type FlatEntry, insertEntrySibling, removeEntryAtPath, renameEntryAtPath } from "@/lib/i18n-structure"; +import { flattenEntries, type FlatEntry, insertEntrySibling, removeEntryAtPath, renameEntryAtPath, moveEntryByOffset } from "@/lib/i18n-structure"; import { ImportLanguageModal } from "@/components/biz/import-language-modal"; import { ExportLanguageModal } from "@/components/biz/export-language-modal"; import { EntryNameModal } from "@/components/biz/entry-name-modal"; @@ -64,6 +64,8 @@ export default function Editor() { const [settingsOpen, setSettingsOpen] = useState(false); const [selected, setSelected] = useState>(new Set()); const [visibleLangs, setVisibleLangs] = useState>(new Set()); + const [moveCountUp, setMoveCountUp] = useState(1); + const [moveCountDown, setMoveCountDown] = useState(1); const [savingAll, setSavingAll] = useState(false); const { copy } = useClipboard(); @@ -94,6 +96,22 @@ export default function Editor() { requestAnimationFrame(() => tryFindAndAnimate(0)); } + const handleMove = useCallback(async (path: string, offset: number) => { + if (!projectId || !structure) return; + try { + const nextRoot = moveEntryByOffset(structure.root, path, offset); + await upsertStructure({ projectId, root: nextRoot }); + setStructure({ projectId, root: nextRoot }); + const nextEntries = flattenEntries(nextRoot); + const idx = nextEntries.findIndex((e) => e.path === path); + if (idx >= 0) { + virtuosoRef.current?.scrollIntoView({ index: idx, align: "center", done: () => highlightRow(idx) }); + } + } catch (e) { + setPageError((e as Error)?.message ?? "移动失败"); + } + }, [projectId, structure]); + function scrollToQuery(ev: React.FormEvent) { ev.preventDefault(); ev.stopPropagation(); @@ -317,13 +335,13 @@ export default function Editor() { ); })} - + - + setAddModal({ open: true, path: entry.path, position: "below" })}> 在下面新增 @@ -342,6 +360,52 @@ export default function Editor() { 重命名 + + { + handleMove(entry.path, -moveCountUp) + }} + > + + 向上移动 + { + const n = parseInt(e.target.value, 10); + setMoveCountUp(Number.isFinite(n) && n > 0 ? n : 1); + }} + className="h-7 w-16" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + /> + 个条目 + + handleMove(entry.path, moveCountDown)} + > + + 向下移动 + { + const n = parseInt(e.target.value, 10); + setMoveCountDown(Number.isFinite(n) && n > 0 ? n : 1); + }} + className="h-7 w-16" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + }} + /> + 个条目 + + { @@ -374,7 +438,7 @@ export default function Editor() { ); - }, [displayedLanguages, inline, projectId, structure, setStructure, setValuesByLang, copy, selected]); + }, [displayedLanguages, inline, projectId, structure, setStructure, setValuesByLang, copy, selected, moveCountUp, moveCountDown, handleMove]); useEffect(() => { if (!projectId || languages.length === 0) return;