Feat: 上下移动条目功能(非拖拽
This commit is contained in:
parent
71424c8770
commit
cd4dd441a4
|
|
@ -118,7 +118,11 @@ export function insertEntrySibling(
|
||||||
): StructureNode {
|
): StructureNode {
|
||||||
const cloned = cloneNode(root);
|
const cloned = cloneNode(root);
|
||||||
const info = findParentAndIndex(cloned, targetPath);
|
const info = findParentAndIndex(cloned, targetPath);
|
||||||
if (!info) return cloned;
|
if (!info) {
|
||||||
|
console.log("未能找到父节点", targetPath);
|
||||||
|
return cloned;
|
||||||
|
}
|
||||||
|
|
||||||
const { parent, index } = info;
|
const { parent, index } = info;
|
||||||
if (!parent.children) parent.children = [];
|
if (!parent.children) parent.children = [];
|
||||||
const exists = parent.children.some((c) => c.key === newKey);
|
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(".") };
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import {
|
||||||
} from "@/lib/db";
|
} from "@/lib/db";
|
||||||
import { ArrowBigDownDash, ArrowBigUpDash, ArrowLeft, Brackets, CaseSensitive, Download, Filter, Languages, LocateFixed, MoreVertical, PencilLine, Reply, Save, Settings, Trash2 } from "lucide-react";
|
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 { 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 { ImportLanguageModal } from "@/components/biz/import-language-modal";
|
||||||
import { ExportLanguageModal } from "@/components/biz/export-language-modal";
|
import { ExportLanguageModal } from "@/components/biz/export-language-modal";
|
||||||
import { EntryNameModal } from "@/components/biz/entry-name-modal";
|
import { EntryNameModal } from "@/components/biz/entry-name-modal";
|
||||||
|
|
@ -64,6 +64,8 @@ export default function Editor() {
|
||||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
const [visibleLangs, setVisibleLangs] = useState<Set<string>>(new Set());
|
const [visibleLangs, setVisibleLangs] = useState<Set<string>>(new Set());
|
||||||
|
const [moveCountUp, setMoveCountUp] = useState(1);
|
||||||
|
const [moveCountDown, setMoveCountDown] = useState(1);
|
||||||
const [savingAll, setSavingAll] = useState(false);
|
const [savingAll, setSavingAll] = useState(false);
|
||||||
|
|
||||||
const { copy } = useClipboard();
|
const { copy } = useClipboard();
|
||||||
|
|
@ -94,6 +96,22 @@ export default function Editor() {
|
||||||
requestAnimationFrame(() => tryFindAndAnimate(0));
|
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<HTMLFormElement>) {
|
function scrollToQuery(ev: React.FormEvent<HTMLFormElement>) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
@ -317,13 +335,13 @@ export default function Editor() {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<td className="sticky right-0 px-3 py-2 align-top bg-white">
|
<td className="sticky right-0 px-3 py-2 align-top bg-white">
|
||||||
<DropdownMenu>
|
<DropdownMenu modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button size="sm" variant="outline">
|
<Button size="sm" variant="outline">
|
||||||
<MoreVertical />
|
<MoreVertical />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-44">
|
<DropdownMenuContent align="end" className="w-64">
|
||||||
<DropdownMenuItem onClick={() => setAddModal({ open: true, path: entry.path, position: "below" })}>
|
<DropdownMenuItem onClick={() => setAddModal({ open: true, path: entry.path, position: "below" })}>
|
||||||
<ArrowBigDownDash />
|
<ArrowBigDownDash />
|
||||||
在下面新增
|
在下面新增
|
||||||
|
|
@ -342,6 +360,52 @@ export default function Editor() {
|
||||||
<PencilLine />
|
<PencilLine />
|
||||||
重命名
|
重命名
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
handleMove(entry.path, -moveCountUp)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowBigUpDash />
|
||||||
|
<span className="whitespace-nowrap">向上移动</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={moveCountUp}
|
||||||
|
onChange={(e) => {
|
||||||
|
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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
个条目
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => handleMove(entry.path, moveCountDown)}
|
||||||
|
>
|
||||||
|
<ArrowBigDownDash />
|
||||||
|
<span className="whitespace-nowrap">向下移动</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={moveCountDown}
|
||||||
|
onChange={(e) => {
|
||||||
|
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();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
个条目
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|
@ -374,7 +438,7 @@ export default function Editor() {
|
||||||
</td>
|
</td>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, [displayedLanguages, inline, projectId, structure, setStructure, setValuesByLang, copy, selected]);
|
}, [displayedLanguages, inline, projectId, structure, setStructure, setValuesByLang, copy, selected, moveCountUp, moveCountDown, handleMove]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectId || languages.length === 0) return;
|
if (!projectId || languages.length === 0) return;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue