Feat: 粘贴剪贴板中不同语言的翻译值

This commit is contained in:
奇趣保罗 2026-05-11 18:16:37 +08:00
parent 8335970e2e
commit d3f78f40d8
2 changed files with 71 additions and 1 deletions

View File

@ -3,7 +3,7 @@ import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMe
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { ArrowBigDownDash, ArrowBigUpDash, Brackets, CaseSensitive, Copy, Filter, Languages, LocateFixed, MoreVertical, PencilLine, Trash2 } from "lucide-react";
import { ArrowBigDownDash, ArrowBigUpDash, Brackets, CaseSensitive, ClipboardPaste, Copy, 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";
@ -145,6 +145,46 @@ export default function EditorTable({
toast.success("复制成功");
};
const handlePasteAllValues = async () => {
if (!navigator.clipboard?.readText) {
toast.error("当前环境不支持读取剪贴板");
return;
}
let parsed: unknown;
try {
const text = await navigator.clipboard.readText();
parsed = JSON.parse(text);
} catch {
toast.error("读取或解析剪贴板失败");
return;
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
toast.error("剪贴板内容不是有效的语言值 JSON");
return;
}
const valuesToPaste: Record<string, string> = {};
for (const [lang, value] of Object.entries(parsed)) {
if (languages.includes(lang) && typeof value === "string") {
valuesToPaste[lang] = value;
}
}
try {
const pastedCount = await inlineEdit.updateValuesForPath(entry.path, valuesToPaste);
if (pastedCount === 0) {
toast.error("剪贴板中没有可粘贴的语言值");
return;
}
toast.success(`已粘贴 ${pastedCount} 种语言的值`);
} catch {
toast.error("粘贴语言值失败");
}
};
return (
<>
<td className="sticky left-0 px-3 py-2 bg-white">
@ -236,6 +276,10 @@ export default function EditorTable({
<Copy />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { void handlePasteAllValues(); }}>
<ClipboardPaste />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onSelect={async () => {

View File

@ -45,6 +45,31 @@ export function useTranslationInlineEdit(opts: {
return valuesByLang[lang]?.[path] ?? "";
}
async function updateValuesForPath(path: string, values: Record<string, string>) {
if (!projectId) return 0;
const entries = Object.entries(values);
if (entries.length === 0) return 0;
const updates: ValuesByLang = {};
try {
await Promise.all(
entries.map(async ([lang, value]) => {
const prev = valuesByLang[lang] ?? {};
const next = { ...prev, [path]: value };
updates[lang] = next;
await upsertLanguageTranslations(projectId, lang, next);
})
);
setValuesByLang((old) => ({ ...old, ...updates }));
cancelEdit();
return entries.length;
} catch (e) {
onError?.((e as Error)?.message ?? "保存失败");
throw e;
}
}
async function saveEdit() {
if (!projectId || !editingKey) return;
if (savingKey === editingKey) return;
@ -87,6 +112,7 @@ export function useTranslationInlineEdit(opts: {
startEdit,
cancelEdit,
setEditingValue,
updateValuesForPath,
saveEdit,
handleKeyDown,
} as const;