diff --git a/web/src/pages/Setting/Ratio/components/GroupTable.jsx b/web/src/pages/Setting/Ratio/components/GroupTable.jsx index 9cf08b25..3594d3e8 100644 --- a/web/src/pages/Setting/Ratio/components/GroupTable.jsx +++ b/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({ 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={ - {t('暂无分组,点击下方按钮添加')} - } + empty={{t('暂无分组,点击下方按钮添加')}} />
{duplicateNames.size > 0 && ( - {t('存在重复的分组名称:')}{Array.from(duplicateNames).join(', ')} + {t('存在重复的分组名称:')} + {Array.from(duplicateNames).join(', ')} )}