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

fix(GroupTable): prevent Input cursor jumping to end on keystroke (#4208)

Refactor updateRow/addRow/removeRow to use functional setRows(prev => ...)
and ref-based onChange/duplicateNames access, making columns useMemo stable
across keystrokes so Semi UI Table does not re-mount Input components.
萧邦 3 недель назад
Родитель
Сommit
c20060931b
1 измененных файлов с 52 добавлено и 43 удалено
  1. 52 43
      web/src/pages/Setting/Ratio/components/GroupTable.jsx

+ 52 - 43
web/src/pages/Setting/Ratio/components/GroupTable.jsx

@@ -1,4 +1,4 @@
-import React, { useState, useCallback, useMemo } from 'react';
+import React, { useState, useCallback, useMemo, useRef } from 'react';
 import {
   Button,
   Input,
@@ -61,60 +61,63 @@ export function serializeGroupTable(rows) {
   };
 }
 
-export default function GroupTable({
-  groupRatio,
-  userUsableGroups,
-  onChange,
-}) {
+export default function GroupTable({ groupRatio, userUsableGroups, onChange }) {
   const { t } = useTranslation();
 
   const [rows, setRows] = useState(() =>
     buildRows(groupRatio, userUsableGroups),
   );
 
-  const emitChange = useCallback(
-    (newRows) => {
-      setRows(newRows);
-      onChange?.(serializeGroupTable(newRows));
-    },
-    [onChange],
-  );
+  // Use functional setRows to keep updateRow/addRow/removeRow referentially
+  // stable, preventing columns useMemo from rebuilding on every keystroke
+  // which causes the Input cursor to jump to end (cursor reset bug).
+  const onChangeRef = useRef(onChange);
+  onChangeRef.current = onChange;
+
+  const emitAndSet = useCallback((updater) => {
+    setRows((prev) => {
+      const next = typeof updater === 'function' ? updater(prev) : updater;
+      onChangeRef.current?.(serializeGroupTable(next));
+      return next;
+    });
+  }, []);
 
   const updateRow = useCallback(
     (id, field, value) => {
-      const next = rows.map((r) =>
-        r._id === id ? { ...r, [field]: value } : r,
+      emitAndSet((prev) =>
+        prev.map((r) => (r._id === id ? { ...r, [field]: value } : r)),
       );
-      emitChange(next);
     },
-    [rows, emitChange],
+    [emitAndSet],
   );
 
   const addRow = useCallback(() => {
-    const existingNames = new Set(rows.map((r) => r.name));
-    let counter = 1;
-    let newName = `group_${counter}`;
-    while (existingNames.has(newName)) {
-      counter++;
-      newName = `group_${counter}`;
-    }
-    emitChange([
-      ...rows,
-      {
-        _id: uid(),
-        name: newName,
-        ratio: 1,
-        selectable: true,
-        description: '',
-      },
-    ]);
-  }, [rows, emitChange]);
+    emitAndSet((prev) => {
+      const existingNames = new Set(prev.map((r) => r.name));
+      let counter = 1;
+      let newName = `group_${counter}`;
+      while (existingNames.has(newName)) {
+        counter++;
+        newName = `group_${counter}`;
+      }
+      return [
+        ...prev,
+        {
+          _id: uid(),
+          name: newName,
+          ratio: 1,
+          selectable: true,
+          description: '',
+        },
+      ];
+    });
+  }, [emitAndSet]);
 
   const removeRow = useCallback(
     (id) => {
-      emitChange(rows.filter((r) => r._id !== id));
+      emitAndSet((prev) => prev.filter((r) => r._id !== id));
     },
-    [rows, emitChange],
+    [emitAndSet],
   );
 
   const groupNames = useMemo(() => rows.map((r) => r.name), [rows]);
@@ -127,6 +130,11 @@ export default function GroupTable({
     return new Set(Object.keys(counts).filter((k) => counts[k] > 1));
   }, [groupNames]);
 
+  // Use ref so column render functions always read the latest duplicate set
+  // without adding duplicateNames to columns deps (which would break cursor).
+  const duplicateNamesRef = useRef(duplicateNames);
+  duplicateNamesRef.current = duplicateNames;
+
   const columns = useMemo(
     () => [
       {
@@ -138,7 +146,9 @@ export default function GroupTable({
           <Input
             size='small'
             value={record.name}
-            status={duplicateNames.has(record.name) ? 'warning' : undefined}
+            status={
+              duplicateNamesRef.current.has(record.name) ? 'warning' : undefined
+            }
             onChange={(v) => updateRow(record._id, 'name', v)}
           />
         ),
@@ -212,7 +222,7 @@ export default function GroupTable({
         ),
       },
     ],
-    [t, duplicateNames, updateRow, removeRow],
+    [t, updateRow, removeRow],
   );
 
   return (
@@ -223,9 +233,7 @@ export default function GroupTable({
         rowKey='_id'
         hidePagination
         size='small'
-        empty={
-          <Text type='tertiary'>{t('暂无分组,点击下方按钮添加')}</Text>
-        }
+        empty={<Text type='tertiary'>{t('暂无分组,点击下方按钮添加')}</Text>}
       />
       <div className='mt-3 flex justify-center'>
         <Button icon={<IconPlus />} theme='outline' onClick={addRow}>
@@ -234,7 +242,8 @@ export default function GroupTable({
       </div>
       {duplicateNames.size > 0 && (
         <Text type='warning' size='small' className='mt-2 block'>
-          {t('存在重复的分组名称:')}{Array.from(duplicateNames).join(', ')}
+          {t('存在重复的分组名称:')}
+          {Array.from(duplicateNames).join(', ')}
         </Text>
       )}
     </div>