Feat: 多选条目翻译,最多同时翻译 50 条
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { memo, useState } from "react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -14,21 +14,36 @@ type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (next: boolean) => void;
|
||||
languages: string[];
|
||||
path: string;
|
||||
paths: string[];
|
||||
prompt: string | undefined;
|
||||
onConfirm: (result: Record<string, string>) => Promise<void> | void;
|
||||
onConfirm: (result: Record<string, Record<string, string>>) => Promise<void> | void;
|
||||
};
|
||||
|
||||
function AiTranslateModalImpl({ open, onOpenChange, languages, path, prompt, onConfirm }: Props) {
|
||||
const [text, setText] = useState("");
|
||||
function AiTranslateModalImpl({ open, onOpenChange, languages, paths, prompt, onConfirm }: Props) {
|
||||
const [inputsByKey, setInputsByKey] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const MAX_ITEMS = 50;
|
||||
const overLimit = paths.length > MAX_ITEMS;
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const init: Record<string, string> = {};
|
||||
for (const p of paths) init[p] = inputsByKey[p] ?? "";
|
||||
setInputsByKey(init);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, paths.join("|")]);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const payload = text.trim();
|
||||
if (!payload) {
|
||||
setError("请输入待翻译文本");
|
||||
if (overLimit) {
|
||||
setError(`一次最多支持 ${MAX_ITEMS} 条`);
|
||||
return;
|
||||
}
|
||||
const items = paths.map((key) => ({ key, text: (inputsByKey[key] ?? "").trim() }));
|
||||
if (items.some((it) => !it.text)) {
|
||||
setError("请为所有条目输入原文");
|
||||
return;
|
||||
}
|
||||
if (languages.length === 0) {
|
||||
@@ -38,15 +53,9 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, path, prompt, onC
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await requestTranslations({ text: payload, languages, prompt });
|
||||
// 二次校验 keys 完整性
|
||||
for (const l of languages) {
|
||||
if (!Object.prototype.hasOwnProperty.call(result, l)) {
|
||||
throw new Error("AI 返回的 key 不完整");
|
||||
}
|
||||
}
|
||||
const result = await requestTranslations({ items, languages, prompt });
|
||||
await onConfirm(result);
|
||||
setText("");
|
||||
setInputsByKey({});
|
||||
onOpenChange(false);
|
||||
} catch (e) {
|
||||
setError((e as Error)?.message ?? "AI 翻译失败");
|
||||
@@ -65,24 +74,29 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, path, prompt, onC
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{`AI 翻译 — ${path || "条目"}`}</DialogTitle>
|
||||
<DialogTitle>{`AI 翻译 — 已选 ${paths.length} 条`}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-3">
|
||||
<div>
|
||||
<label className="text-sm text-muted-foreground">待翻译文本</label>
|
||||
<Input
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="请输入原文本"
|
||||
aria-label="待翻译文本"
|
||||
/>
|
||||
<div className="space-y-4 max-h-[50vh] overflow-auto pr-1">
|
||||
{paths.map((p) => (
|
||||
<div key={p} className="flex flex-col gap-2">
|
||||
<div className="truncate font-mono text-xs text-muted-foreground">{p}</div>
|
||||
<Input
|
||||
className="grow"
|
||||
value={inputsByKey[p] ?? ""}
|
||||
onChange={(e) => setInputsByKey((old) => ({ ...old, [p]: e.target.value }))}
|
||||
placeholder="请输入原文本"
|
||||
aria-label={`原文 ${p}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
目标语言:{languages.join(", ") || "无"}
|
||||
</div>
|
||||
{error && (
|
||||
{(error || overLimit) && (
|
||||
<div className="text-sm text-red-600" role="alert">{error}</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
@@ -97,7 +111,7 @@ function AiTranslateModalImpl({ open, onOpenChange, languages, path, prompt, onC
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
<Button type="submit" disabled={loading || overLimit}>
|
||||
{loading ? "翻译中..." : "生成翻译"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -14,6 +14,8 @@ const buttonVariants = cva(
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
"outline-destructive":
|
||||
"border border-destructive text-destructive hover:bg-destructive/10 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
|
||||
Reference in New Issue
Block a user