166 lines
4.8 KiB
TypeScript
166 lines
4.8 KiB
TypeScript
import { memo, useEffect, useMemo, useState } from "react";
|
|
import { Check } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { unflattenValues } from "@/lib/i18n-structure";
|
|
import { useClipboard } from "@/hooks/use-clipboard";
|
|
|
|
type Props = {
|
|
open: boolean;
|
|
onOpenChange: (next: boolean) => void;
|
|
languages: string[];
|
|
valuesByLang: Record<string, Record<string, string>>;
|
|
orderedPaths?: string[]; // 优先按照结构顺序导出
|
|
};
|
|
|
|
function ExportLanguageModalImpl({
|
|
open,
|
|
onOpenChange,
|
|
languages,
|
|
valuesByLang,
|
|
orderedPaths,
|
|
}: Props) {
|
|
const [selected, setSelected] = useState<string>("");
|
|
const { copied, copy } = useClipboard({ resetAfterMs: 1500 });
|
|
|
|
useEffect(() => {
|
|
if (open) {
|
|
setSelected((prev) =>
|
|
prev && languages.includes(prev) ? prev : languages[0] ?? ""
|
|
);
|
|
}
|
|
}, [open, languages]);
|
|
|
|
const jsonText = useMemo(() => {
|
|
if (!selected) return "";
|
|
const flat = valuesByLang[selected] ?? {};
|
|
|
|
// 若提供结构顺序,则按结构顺序构建嵌套对象,保证键序与表格一致
|
|
if (orderedPaths && orderedPaths.length > 0) {
|
|
const root: Record<string, unknown> = {};
|
|
const added = new Set<string>();
|
|
const setByPath = (path: string, val: string) => {
|
|
const parts = path.split(".");
|
|
let cur: Record<string, unknown> = root;
|
|
for (let i = 0; i < parts.length; i += 1) {
|
|
const key = parts[i]!;
|
|
const isLeaf = i === parts.length - 1;
|
|
if (isLeaf) {
|
|
cur[key] = val;
|
|
} else {
|
|
const next = cur[key];
|
|
if (next && typeof next === "object" && !Array.isArray(next)) {
|
|
cur = next as Record<string, unknown>;
|
|
} else {
|
|
const obj: Record<string, unknown> = {};
|
|
cur[key] = obj;
|
|
cur = obj;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
for (const p of orderedPaths) {
|
|
const v = Object.prototype.hasOwnProperty.call(flat, p) ? flat[p] : "";
|
|
setByPath(p, v);
|
|
added.add(p);
|
|
}
|
|
// 追加结构外的剩余键,避免丢数据
|
|
for (const p of Object.keys(flat)) {
|
|
if (!added.has(p)) setByPath(p, flat[p]!);
|
|
}
|
|
try {
|
|
return JSON.stringify(root, null, 2);
|
|
} catch {
|
|
return "{}";
|
|
}
|
|
}
|
|
|
|
// 回退:无结构顺序时使用普通反扁平
|
|
try {
|
|
return JSON.stringify(unflattenValues(flat), null, 2);
|
|
} catch {
|
|
return "{}";
|
|
}
|
|
}, [selected, valuesByLang, orderedPaths]);
|
|
|
|
function handleDownload() {
|
|
if (!selected) return;
|
|
const blob = new Blob([jsonText || "{}"], {
|
|
type: "application/json;charset=utf-8",
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `${selected}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
const handleCopy = () => {
|
|
if (!jsonText) return;
|
|
|
|
copy(jsonText);
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-3xl">
|
|
<DialogHeader>
|
|
<DialogTitle>导出语言 JSON</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-3 min-w-0">
|
|
<div className="flex flex-wrap gap-2">
|
|
{languages.length === 0 ? (
|
|
<div className="text-sm text-muted-foreground">
|
|
暂无可导出的语言
|
|
</div>
|
|
) : (
|
|
languages.map((lang) => (
|
|
<button
|
|
key={lang}
|
|
type="button"
|
|
onClick={() => setSelected(lang)}
|
|
className={`px-3 h-8 rounded border text-sm ${
|
|
selected === lang ? "bg-accent" : ""
|
|
}`}
|
|
>
|
|
{lang}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
<div className="border rounded-md overflow-hidden">
|
|
<textarea
|
|
className="w-full h-[50vh] p-3 font-mono text-sm whitespace-pre"
|
|
readOnly
|
|
value={jsonText}
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<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>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
export const ExportLanguageModal = memo(ExportLanguageModalImpl);
|