Files
CC-Switch/src/components/JsonEditor.tsx
T
Jason 01da9a1eac - i18n: complete remaining internationalization across the UI
- Locales: add and align keys (common.enterValidValue, apiKeyInput.*, jsonEditor.*, claudeConfig.*); fix zh common.unknown mapping
- ProviderForm: localize labels/placeholders/hints/errors; unify JSON/auth validation to providerForm.*; add wizard CTA for Codex custom with i18n; cancel uses common.cancel
- CodexConfigEditor: i18n for quick wizard, labels/placeholders/hints, common config modal (title/help/buttons)
- ClaudeConfigEditor: i18n for main label, common-config toggle/button, modal title/help, footer buttons
- EndpointSpeedTest: localize failed/noEndpoints/done and aria labels
- ApiKeyInput: i18n for placeholder and show/hide aria
- JsonEditor: i18n linter messages
- PresetSelector: remove hardcoded defaults, use i18n keys
- UpdateBadge: i18n close aria
- Build/typecheck: pass; scan shows no visible hardcoded Chinese strings outside locales
2025-10-07 23:31:00 +08:00

143 lines
3.8 KiB
TypeScript

import React, { useRef, useEffect, useMemo } from "react";
import { EditorView, basicSetup } from "codemirror";
import { json } from "@codemirror/lang-json";
import { oneDark } from "@codemirror/theme-one-dark";
import { EditorState } from "@codemirror/state";
import { placeholder } from "@codemirror/view";
import { linter, Diagnostic } from "@codemirror/lint";
import { useTranslation } from "react-i18next";
interface JsonEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
darkMode?: boolean;
rows?: number;
showValidation?: boolean;
}
const JsonEditor: React.FC<JsonEditorProps> = ({
value,
onChange,
placeholder: placeholderText = "",
darkMode = false,
rows = 12,
showValidation = true,
}) => {
const { t } = useTranslation();
const editorRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
// JSON linter 函数
const jsonLinter = useMemo(
() =>
linter((view) => {
const diagnostics: Diagnostic[] = [];
if (!showValidation) return diagnostics;
const doc = view.state.doc.toString();
if (!doc.trim()) return diagnostics;
try {
const parsed = JSON.parse(doc);
// 检查是否是JSON对象
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
// 格式正确
} else {
diagnostics.push({
from: 0,
to: doc.length,
severity: "error",
message: t("jsonEditor.mustBeObject"),
});
}
} catch (e) {
// 简单处理JSON解析错误
const message = e instanceof SyntaxError ? e.message : t("jsonEditor.invalidJson");
diagnostics.push({
from: 0,
to: doc.length,
severity: "error",
message,
});
}
return diagnostics;
}),
[showValidation, t],
);
useEffect(() => {
if (!editorRef.current) return;
// 创建编辑器扩展
const minHeightPx = Math.max(1, rows) * 18; // 降低最小高度以减少抖动
const sizingTheme = EditorView.theme({
"&": { minHeight: `${minHeightPx}px` },
".cm-scroller": { overflow: "auto" },
".cm-content": {
fontFamily:
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
fontSize: "14px",
},
});
const extensions = [
basicSetup,
json(),
placeholder(placeholderText || ""),
sizingTheme,
jsonLinter,
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const newValue = update.state.doc.toString();
onChange(newValue);
}
}),
];
// 如果启用深色模式,添加深色主题
if (darkMode) {
extensions.push(oneDark);
}
// 创建初始状态
const state = EditorState.create({
doc: value,
extensions,
});
// 创建编辑器视图
const view = new EditorView({
state,
parent: editorRef.current,
});
viewRef.current = view;
// 清理函数
return () => {
view.destroy();
viewRef.current = null;
};
}, [darkMode, rows, jsonLinter]); // 依赖项中不包含 onChange 和 placeholder,避免不必要的重建
// 当 value 从外部改变时更新编辑器内容
useEffect(() => {
if (viewRef.current && viewRef.current.state.doc.toString() !== value) {
const transaction = viewRef.current.state.update({
changes: {
from: 0,
to: viewRef.current.state.doc.length,
insert: value,
},
});
viewRef.current.dispatch(transaction);
}
}, [value]);
return <div ref={editorRef} style={{ width: "100%" }} />;
};
export default JsonEditor;