🐛 fix(model, web): robust JSON handling; remove datatypes dep; stabilize JSONEditor manual mode

- Why:
  - Eliminate `gorm.io/datatypes` for a single field and fix scan errors when drivers return JSON as string.
  - Prevent JSONEditor manual mode from locking on invalid JSON and from appending stray characters after “Fill Template”.

- What:
  - Backend (`model/prefill_group.go`):
    - Replaced `datatypes.JSON` with `JSONValue` (based on `json.RawMessage`) for `PrefillGroup.Items`.
    - Implemented `sql.Scanner` and `driver.Valuer` to accept both `[]byte` and `string`.
    - Implemented `MarshalJSON`/`UnmarshalJSON` to preserve raw JSON in API without base64.
    - Converted comments to Chinese.
  - Frontend (`web/src/components/common/ui/JSONEditor.js`):
    - Added `manualText` buffer for manual mode to avoid input being overridden by external value.
    - Only propagate `onChange` when manual text is valid JSON; otherwise show error but do not block typing.
    - Safe manual-mode rendering: derive rows from `manualText` and avoid calling `split` on non-strings.
    - Improved mode toggle: populate `manualText` from visual data; validate before switching back to visual.
    - Fixed “Fill Template” to sync `manualText`, `jsonData`, and `onChange` to avoid stray trailing characters.

- Impact:
  - Resolves: “unsupported Scan, storing driver.Value type string into type *json.RawMessage”.
  - Resolves: `value.split is not a function` in manual mode.
  - Resolves: extra `s` appended after inserting template.
  - API shape and DB column type remain the same (`gorm:"type:json"`); no `go.mod` changes.
  - Lints pass for modified files.

Files changed:
- model/prefill_group.go
- web/src/components/common/ui/JSONEditor.js
This commit is contained in:
t0ng7u
2025-08-08 04:21:50 +08:00
parent 473f3b6f3e
commit d96f846648

View File

@@ -60,6 +60,13 @@ const JSONEditor = ({
return {}; return {};
}); });
// 手动模式下的本地文本缓冲,避免无效 JSON 时被外部值重置
const [manualText, setManualText] = useState(() => {
if (typeof value === 'string') return value;
if (value && typeof value === 'object') return JSON.stringify(value, null, 2);
return '';
});
// 根据键数量决定默认编辑模式 // 根据键数量决定默认编辑模式
const [editMode, setEditMode] = useState(() => { const [editMode, setEditMode] = useState(() => {
// 如果初始JSON数据的键数量大于10个则默认使用手动模式 // 如果初始JSON数据的键数量大于10个则默认使用手动模式
@@ -95,6 +102,15 @@ const JSONEditor = ({
} }
}, [value]); }, [value]);
// 外部 value 变化时,若不在手动模式,则同步手动文本;在手动模式下不打断用户输入
useEffect(() => {
if (editMode !== 'manual') {
if (typeof value === 'string') setManualText(value);
else if (value && typeof value === 'object') setManualText(JSON.stringify(value, null, 2));
else setManualText('');
}
}, [value, editMode]);
// 处理可视化编辑的数据变化 // 处理可视化编辑的数据变化
const handleVisualChange = useCallback((newData) => { const handleVisualChange = useCallback((newData) => {
setJsonData(newData); setJsonData(newData);
@@ -109,21 +125,21 @@ const JSONEditor = ({
onChange?.(jsonString); onChange?.(jsonString);
}, [onChange, formApi, field]); }, [onChange, formApi, field]);
// 处理手动编辑的数据变化 // 处理手动编辑的数据变化(无效 JSON 不阻断输入,也不立刻回传上游)
const handleManualChange = useCallback((newValue) => { const handleManualChange = useCallback((newValue) => {
onChange?.(newValue); setManualText(newValue);
// 验证JSON格式
if (newValue && newValue.trim()) { if (newValue && newValue.trim()) {
try { try {
const parsed = JSON.parse(newValue); JSON.parse(newValue);
setJsonError(''); setJsonError('');
// 预先准备可视化数据,但不立即应用 onChange?.(newValue);
// 这样切换到可视化模式时数据已经准备好了
} catch (error) { } catch (error) {
setJsonError(error.message); setJsonError(error.message);
// 无效 JSON 时不回传,避免外部值把输入重置
} }
} else { } else {
setJsonError(''); setJsonError('');
onChange?.('');
} }
}, [onChange]); }, [onChange]);
@@ -131,12 +147,15 @@ const JSONEditor = ({
const toggleEditMode = useCallback(() => { const toggleEditMode = useCallback(() => {
if (editMode === 'visual') { if (editMode === 'visual') {
// 从可视化模式切换到手动模式 // 从可视化模式切换到手动模式
setManualText(Object.keys(jsonData).length === 0 ? '' : JSON.stringify(jsonData, null, 2));
setEditMode('manual'); setEditMode('manual');
} else { } else {
// 从手动模式切换到可视化模式需要验证JSON // 从手动模式切换到可视化模式需要验证JSON
try { try {
let parsed = {}; let parsed = {};
if (typeof value === 'string' && value.trim()) { if (manualText && manualText.trim()) {
parsed = JSON.parse(manualText);
} else if (typeof value === 'string' && value.trim()) {
parsed = JSON.parse(value); parsed = JSON.parse(value);
} else if (typeof value === 'object' && value !== null) { } else if (typeof value === 'object' && value !== null) {
parsed = value; parsed = value;
@@ -150,7 +169,7 @@ const JSONEditor = ({
return; return;
} }
} }
}, [editMode, value]); }, [editMode, value, manualText, jsonData]);
// 添加键值对 // 添加键值对
const addKeyValue = useCallback(() => { const addKeyValue = useCallback(() => {
@@ -204,14 +223,11 @@ const JSONEditor = ({
formApi.setValue(field, templateString); formApi.setValue(field, templateString);
} }
// 无论哪种模式都要更新值 // 同步内部与外部值,避免出现杂字符
setManualText(templateString);
setJsonData(template);
onChange?.(templateString); onChange?.(templateString);
// 如果是可视化模式同时更新jsonData
if (editMode === 'visual') {
setJsonData(template);
}
// 清除错误状态 // 清除错误状态
setJsonError(''); setJsonError('');
} }
@@ -617,10 +633,10 @@ const JSONEditor = ({
<div> <div>
<TextArea <TextArea
placeholder={placeholder} placeholder={placeholder}
value={value} value={manualText}
onChange={handleManualChange} onChange={handleManualChange}
showClear={showClear} showClear={showClear}
rows={Math.max(8, value ? value.split('\n').length : 8)} rows={Math.max(8, manualText ? manualText.split('\n').length : 8)}
/> />
{/* 隐藏的Form字段用于验证和数据绑定 */} {/* 隐藏的Form字段用于验证和数据绑定 */}
<Form.Input <Form.Input