115 lines
3.9 KiB
JavaScript
115 lines
3.9 KiB
JavaScript
import { useMemo, useState } from "react";
|
|
import TextField from "@mui/material/TextField";
|
|
import { Button } from "@/shared/ui/Button.jsx";
|
|
import styles from "@/shared/ui/JsonEditorField.module.css";
|
|
|
|
export function JsonEditorField({
|
|
className = "",
|
|
disabled = false,
|
|
label,
|
|
minRows = 6,
|
|
onChange,
|
|
required = false,
|
|
value,
|
|
}) {
|
|
const [validated, setValidated] = useState(false);
|
|
const validation = useMemo(() => validateJsonObject(value), [value]);
|
|
const showError = validated && !validation.valid;
|
|
const helperText = showError
|
|
? validation.message
|
|
: validated
|
|
? "JSON 格式正确"
|
|
: "支持格式化和校验,内容必须是 JSON 对象";
|
|
const lineCount = Math.max(1, String(value || "").split("\n").length);
|
|
|
|
const formatJson = () => {
|
|
const result = parseJsonObject(value);
|
|
setValidated(true);
|
|
if (!result.valid) {
|
|
return;
|
|
}
|
|
onChange(JSON.stringify(result.value, null, 2));
|
|
};
|
|
|
|
return (
|
|
<div className={[styles.root, className].filter(Boolean).join(" ")}>
|
|
<div className={styles.header}>
|
|
<div className={styles.title}>
|
|
<span>{label}</span>
|
|
{required ? <span className={styles.required}>*</span> : null}
|
|
</div>
|
|
<div className={styles.actions}>
|
|
<Button disabled={disabled} onClick={() => setValidated(true)}>
|
|
校验 JSON
|
|
</Button>
|
|
<Button disabled={disabled} onClick={formatJson}>
|
|
格式化 JSON
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className={[styles.editor, showError ? styles.editorInvalid : ""].filter(Boolean).join(" ")}>
|
|
<pre className={styles.gutter} aria-hidden="true">
|
|
{Array.from({ length: lineCount }, (_, index) => index + 1).join("\n")}
|
|
</pre>
|
|
<TextField
|
|
className={styles.editorField}
|
|
disabled={disabled}
|
|
minRows={minRows}
|
|
multiline
|
|
slotProps={{
|
|
input: { disableUnderline: true },
|
|
htmlInput: {
|
|
"aria-label": label,
|
|
autoCapitalize: "off",
|
|
autoCorrect: "off",
|
|
spellCheck: false,
|
|
},
|
|
}}
|
|
value={value}
|
|
variant="standard"
|
|
onBlur={() => setValidated(true)}
|
|
onChange={(event) => {
|
|
setValidated(false);
|
|
onChange(event.target.value);
|
|
}}
|
|
/>
|
|
</div>
|
|
<div
|
|
className={[styles.helper, showError ? styles.helperInvalid : validated ? styles.helperValid : ""].join(
|
|
" ",
|
|
)}
|
|
>
|
|
{helperText}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function normalizeJsonObjectString(raw) {
|
|
const result = parseJsonObject(raw);
|
|
if (!result.valid) {
|
|
throw new Error(result.message);
|
|
}
|
|
return JSON.stringify(result.value);
|
|
}
|
|
|
|
function validateJsonObject(raw) {
|
|
return parseJsonObject(raw);
|
|
}
|
|
|
|
function parseJsonObject(raw) {
|
|
const value = String(raw || "").trim();
|
|
if (!value) {
|
|
return { valid: true, value: {} };
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
return { valid: false, message: '配置 JSON 必须是对象,例如 {"app_id": 123}' };
|
|
}
|
|
return { valid: true, value: parsed };
|
|
} catch (err) {
|
|
return { valid: false, message: `JSON 格式错误:${err.message}` };
|
|
}
|
|
}
|