I18n-Translate-It/src/components/biz/add-entry-modal.tsx

177 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { memo, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { toast } from "sonner";
import { ClipboardPaste } from "lucide-react";
type Props = {
open: boolean;
onOpenChange: (v: boolean) => void;
position: "above" | "below";
onConfirm: (name: string, values?: Record<string, string>) => void | Promise<void>;
validate?: (name: string) => string | null;
availableLanguages: string[];
};
function AddEntryModalImpl({ open, onOpenChange, position, onConfirm, validate, availableLanguages }: Props) {
const [name, setName] = useState("");
const [err, setErr] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const [clipboardValues, setClipboardValues] = useState<Record<string, string> | null>(null);
const [useClipboard, setUseClipboard] = useState(false);
useEffect(() => {
if (open) {
setName("");
setErr(null);
setSaving(false);
setUseClipboard(false);
// 尝试读取剪贴板内容
checkClipboard();
}
}, [open]);
async function checkClipboard() {
try {
const text = await navigator.clipboard.readText();
const parsed = JSON.parse(text);
// 验证是否是有效的语言值对象
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
const keys = Object.keys(parsed);
const allStrings = keys.every(key => typeof parsed[key] === 'string');
if (allStrings && keys.length > 0) {
setClipboardValues(parsed);
setUseClipboard(true);
return;
}
}
} catch {
// 剪贴板内容不是有效的 JSON 或无法访问,忽略
}
setClipboardValues(null);
setUseClipboard(false);
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const trimmed = name.trim();
if (!trimmed) return setErr("名称不能为空");
if (trimmed.includes(".")) return setErr("名称不能包含 '.'");
if (validate) {
const msg = validate(trimmed);
if (msg) return setErr(msg);
}
setSaving(true);
try {
const values = useClipboard && clipboardValues ? clipboardValues : undefined;
await onConfirm(trimmed, values);
onOpenChange(false);
} catch (e) {
setErr((e as Error)?.message ?? "操作失败");
} finally {
setSaving(false);
}
}
const title = position === "above" ? "在上方新增条目" : "在下方新增条目";
return (
<Dialog open={open} onOpenChange={(v) => { if (!saving) onOpenChange(v); }}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Input
placeholder="请输入条目名称(不含点)"
value={name}
onChange={(e) => setName(e.target.value)}
aria-label="名称"
/>
</div>
{clipboardValues && (
<div className="space-y-3 p-4 border rounded-md bg-muted/30">
<div className="flex items-center gap-2">
<Checkbox
id="use-clipboard"
checked={useClipboard}
onCheckedChange={(checked) => setUseClipboard(!!checked)}
/>
<label
htmlFor="use-clipboard"
className="text-sm font-medium cursor-pointer flex items-center gap-2"
>
<ClipboardPaste className="w-4 h-4" />
使
</label>
</div>
{useClipboard && (
<div className="space-y-2 pl-6">
<div className="text-xs text-muted-foreground"></div>
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-y-auto">
{Object.entries(clipboardValues).map(([lang, value]) => {
const isAvailable = availableLanguages.includes(lang);
return (
<div
key={lang}
className={`text-xs p-2 rounded border ${
isAvailable
? "bg-background border-border"
: "bg-muted/50 border-muted text-muted-foreground"
}`}
>
<div className="font-mono font-semibold mb-1">
{lang}
{!isAvailable && (
<span className="ml-1 text-[10px] text-orange-600">()</span>
)}
</div>
<div className="break-all line-clamp-2">{value}</div>
</div>
);
})}
</div>
{Object.keys(clipboardValues).some(lang => !availableLanguages.includes(lang)) && (
<div className="text-xs text-orange-600">
</div>
)}
</div>
)}
</div>
)}
{err && <div className="text-sm text-red-600">{err}</div>}
<DialogFooter>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>
</Button>
<Button type="submit" disabled={saving}>
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
export const AddEntryModal = memo(AddEntryModalImpl);