Просмотр исходного кода

🐛 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
t0ng7u 7 месяцев назад
Родитель
Сommit
d96f846648
1 измененных файлов с 32 добавлено и 16 удалено
  1. 32 16
      web/src/components/common/ui/JSONEditor.js

+ 32 - 16
web/src/components/common/ui/JSONEditor.js

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