diff --git a/src/components/biz/import-language-modal.tsx b/src/components/biz/import-language-modal.tsx
index b456b78..5a056db 100644
--- a/src/components/biz/import-language-modal.tsx
+++ b/src/components/biz/import-language-modal.tsx
@@ -10,6 +10,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
+import { Checkbox } from "../ui/checkbox";
type Props = {
open: boolean;
@@ -17,6 +18,7 @@ type Props = {
projectId: string;
hasStructure: boolean;
onImported: () => void | Promise;
+ languages: string[];
};
function ImportLanguageModalImpl({
@@ -25,11 +27,13 @@ function ImportLanguageModalImpl({
projectId,
hasStructure,
onImported,
+ languages,
}: Props) {
const [lang, setLang] = useState("");
const [importing, setImporting] = useState(false);
const [error, setError] = useState(null);
const fileRef = useRef(null);
+ const [forceOverride, setForceOverride] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -57,14 +61,20 @@ function ImportLanguageModalImpl({
} catch {
throw new Error("JSON 解析失败,请检查文件内容");
}
- if (!hasStructure) {
+
+ // 如果没有结构或勾选强制覆盖,则根据文件重建结构
+ if (!hasStructure || forceOverride) {
const root = buildStructureFromObject(json);
await upsertStructure({ projectId, root });
+ console.log("构建结构成功", root);
}
+
+ // 导入翻译(可能包含结构外的键,注意需要保留额外处理)
const values = flattenValues(json);
await upsertLanguageTranslations(projectId, language, values);
setLang("");
if (fileRef.current) fileRef.current.value = "";
+ setForceOverride(false);
onOpenChange(false);
await onImported();
} catch (e) {
@@ -81,13 +91,14 @@ function ImportLanguageModalImpl({
if (!importing) {
onOpenChange(v);
if (!v) setError(null);
+ if (!v) setForceOverride(false);
}
}}
>
- {hasStructure ? "导入语言 JSON" : "导入翻译 JSON 并构建结构"}
+ {hasStructure ? "导入 JSON" : "导入 JSON 并构建结构"}
+ {hasStructure && (
+
+
+
+ 勾选后将使用上传文件的结构覆盖当前项目结构,此操作不可撤销。
+
+
+ )}
{error && (
{error}
@@ -125,6 +156,7 @@ function ImportLanguageModalImpl({
onClick={() => {
onOpenChange(false);
setError(null);
+ setForceOverride(false);
}}
disabled={importing}
>
diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx
new file mode 100644
index 0000000..0e2a6cd
--- /dev/null
+++ b/src/components/ui/checkbox.tsx
@@ -0,0 +1,30 @@
+import * as React from "react"
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
+import { CheckIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Checkbox({
+ className,
+ ...props
+}: React.ComponentProps
) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { Checkbox }
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
new file mode 100644
index 0000000..c9cd094
--- /dev/null
+++ b/src/components/ui/sonner.tsx
@@ -0,0 +1,38 @@
+import {
+ CircleCheckIcon,
+ InfoIcon,
+ Loader2Icon,
+ OctagonXIcon,
+ TriangleAlertIcon,
+} from "lucide-react";
+import { useTheme } from "next-themes";
+import { Toaster as Sonner, type ToasterProps } from "sonner";
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme();
+
+ return (
+ ,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ );
+};
+
+export { Toaster };
diff --git a/src/main.tsx b/src/main.tsx
index af42b23..b3f80de 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -5,6 +5,7 @@ import { RouterProvider } from "react-router/dom";
import "./index.css";
import Home from "./pages/home.tsx";
import Editor from "./pages/editor.tsx";
+import { Toaster } from "@/components/ui/sonner";
const router = createHashRouter([
{
@@ -19,6 +20,7 @@ const router = createHashRouter([
createRoot(document.getElementById("root")!).render(
- ,
+
+
);
diff --git a/src/pages/editor.tsx b/src/pages/editor.tsx
index 44b9a56..7beba1e 100644
--- a/src/pages/editor.tsx
+++ b/src/pages/editor.tsx
@@ -30,6 +30,8 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
+import { useClipboard } from "@/hooks/use-clipboard";
+import { toast } from "sonner";
export default function Editor() {
const { id: projectId } = useParams();
@@ -49,6 +51,8 @@ export default function Editor() {
const [fullMatch, setFullMatch] = useState(false);
const [aiModal, setAiModal] = useState<{ open: boolean; path: string } | null>(null);
+ const { copy } = useClipboard();
+
function highlightRow(index: number) {
const tryFindAndAnimate = (attempt = 0) => {
const root = scrollerRootRef.current as HTMLElement | null;
@@ -138,18 +142,27 @@ export default function Editor() {
const headerContent = useCallback(() => (
- | 翻译条目名称 |
+ 翻译条目名称 |
{languages.map((lang) => (
- {lang} |
+ {lang} |
))}
- 操作 |
+ 操作 |
), [languages, colWidth]);
const renderItemContent = useCallback((_idx: number, entry: FlatEntry) => {
+ const handleCopy = () => {
+ copy(entry.path);
+ toast.success("复制成功");
+ };
+
return (
<>
- {entry.path} |
+
+
+ |
{languages.map((lang) => {
const isEditing = inline.isEditingCell(entry.path, lang);
const isSaving = inline.isSavingCell(entry.path, lang);
@@ -183,7 +196,6 @@ export default function Editor() {
@@ -258,7 +270,7 @@ export default function Editor() {
return (
-
+