Feat: 新增 AI 翻译功能,导出 JSON 复制到剪贴板
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { memo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { requestTranslations } from "@/lib/ai";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (next: boolean) => void;
|
||||
languages: string[];
|
||||
path: string;
|
||||
onConfirm: (result: Record<string, string>) => Promise<void> | void;
|
||||
};
|
||||
|
||||
function AiTranslateModalImpl({ open, onOpenChange, languages, path, onConfirm }: Props) {
|
||||
const [text, setText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const payload = text.trim();
|
||||
if (!payload) {
|
||||
setError("请输入待翻译文本");
|
||||
return;
|
||||
}
|
||||
if (languages.length === 0) {
|
||||
setError("当前项目暂无目标语言");
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await requestTranslations({ text: payload, languages });
|
||||
// 二次校验 keys 完整性
|
||||
for (const l of languages) {
|
||||
if (!Object.prototype.hasOwnProperty.call(result, l)) {
|
||||
throw new Error("AI 返回的 key 不完整");
|
||||
}
|
||||
}
|
||||
await onConfirm(result);
|
||||
setText("");
|
||||
onOpenChange(false);
|
||||
} catch (e) {
|
||||
setError((e as Error)?.message ?? "AI 翻译失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(v) => {
|
||||
if (!loading) {
|
||||
onOpenChange(v);
|
||||
if (!v) setError(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{`AI 翻译 — ${path || "条目"}`}</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>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
目标语言:{languages.join(", ") || "无"}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-sm text-red-600" role="alert">{error}</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
setError(null);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "翻译中..." : "生成翻译"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export const AiTranslateModal = memo(AiTranslateModalImpl);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { Check } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { unflattenValues } from "@/lib/i18n-structure";
|
||||
import { useClipboard } from "@/hooks/use-clipboard";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -25,6 +27,7 @@ function ExportLanguageModalImpl({
|
||||
orderedPaths,
|
||||
}: Props) {
|
||||
const [selected, setSelected] = useState<string>("");
|
||||
const { copied, copy } = useClipboard({ resetAfterMs: 1500 });
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
@@ -102,6 +105,12 @@ function ExportLanguageModalImpl({
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
if (!jsonText) return;
|
||||
|
||||
copy(jsonText);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl">
|
||||
@@ -141,6 +150,9 @@ function ExportLanguageModalImpl({
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
<Button onClick={handleCopy} variant="outline" disabled={!selected || !jsonText}>
|
||||
{copied ? (<><Check /> 已复制</>) : "复制 JSON"}
|
||||
</Button>
|
||||
<Button onClick={handleDownload} disabled={!selected || !jsonText}>
|
||||
导出 JSON
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user