diff --git a/src/renderer/src/components/MaterialTypeManagementDialog.tsx b/src/renderer/src/components/MaterialTypeManagementDialog.tsx index 2e649b1..6083c4e 100644 --- a/src/renderer/src/components/MaterialTypeManagementDialog.tsx +++ b/src/renderer/src/components/MaterialTypeManagementDialog.tsx @@ -1,19 +1,30 @@ /** - * Material Type Management Dialog + * Material Type Management Dialog (Modern UI Refactor) * * Provides a dialog for managing material type keywords used to identify * materials for deletion. Admin users can see all records and filter by manager. * Regular users can only see and edit their own records. */ -import React, { useState, useEffect, useCallback, useRef } from 'react' -import { Plus, Trash2, Save, RotateCcw, Users } from 'lucide-react' -import { Modal } from './ui/Modal' +import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react' +import { + X, + Plus, + RotateCcw, + Search, + Trash2, + User, + CloudUpload, + CheckCircle2, + AlertCircle, + Users +} from 'lucide-react' import { showSuccess, showError, showInfo } from '../stores/useAppStore' import { ConfirmDialog } from './ui/ConfirmDialog' import { useConfirmDialog } from './ui/useConfirmDialog' import { useLogger } from '../hooks/useLogger' +// --- Interfaces --- interface MaterialTypeRecord { id?: number materialName: string @@ -21,6 +32,7 @@ interface MaterialTypeRecord { } interface RowState { + localId: string // Stable ID for React mapping during edits/filters record: MaterialTypeRecord state: 'original' | 'new' | 'modified' | 'deleted' originalRecord?: MaterialTypeRecord @@ -34,39 +46,172 @@ interface MaterialTypeManagementDialogProps { triggerRef?: React.RefObject } +// --- KeywordCard Component (Handles individual items) --- +interface KeywordCardProps { + item: RowState + isAdmin: boolean + managers: string[] + onUpdate: (localId: string, newRecord: Partial) => void + onDelete: (localId: string) => void + onRestore: (localId: string) => void +} + +const KeywordCard: React.FC = ({ item, isAdmin, managers, onUpdate, onDelete, onRestore }) => { + const [isEditing, setIsEditing] = useState(item.state === 'new'); + const [text, setText] = useState(item.record.materialName); + const [manager, setManager] = useState(item.record.managerName); + const inputRef = useRef(null); + + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + if (item.state !== 'new') { + inputRef.current.select(); + } + } + }, [isEditing, item.state]); + + const handleSave = () => { + const trimmedText = text.trim(); + if (!trimmedText) { + onDelete(item.localId); // Delete if empty + } else { + setIsEditing(false); + if (trimmedText !== item.record.materialName || manager !== item.record.managerName) { + onUpdate(item.localId, { materialName: trimmedText, managerName: manager }); + } + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') handleSave(); + if (e.key === 'Escape') { + setText(item.record.materialName); + setManager(item.record.managerName); + setIsEditing(false); + if (item.state === 'new') onDelete(item.localId); + } + }; + + // Deleted State View + if (item.state === 'deleted') { + return ( +
+ + {item.record.materialName} + + +
+ ); + } + + // Editing State View + if (isEditing) { + return ( +
{ + // Delay save to allow dropdown clicks. Checks if new focus is outside this container. + if (!e.currentTarget.contains(e.relatedTarget as Node)) { + handleSave(); + } + }} + > +
+ setText(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="关键词..." + className="flex-1 w-full min-w-[80px] bg-transparent outline-none text-sm font-medium text-indigo-900 placeholder-indigo-300" + /> + {isAdmin && ( + + )} +
+ ); + } + + // Normal / Modified State View + const isModified = item.state === 'modified' || item.state === 'new'; + return ( +
setIsEditing(true)} + className={`group relative flex items-center h-10 px-3 bg-white border rounded-lg cursor-pointer transition-all duration-200 hover:shadow-md hover:border-indigo-300 + ${isModified ? 'border-amber-300 bg-amber-50/30' : 'border-slate-200'} + `} + > +
+ + + {item.record.materialName} + + +
+ {isAdmin && ( + + {item.record.managerName || '未指定'} + + )} + +
+
+ ); +}; + + +// --- Main Dialog Component --- export const MaterialTypeManagementDialog: React.FC = ({ isOpen, onClose, isAdmin, currentUsername, - triggerRef + // triggerRef // Retained for compatibility, but layout uses custom overlay }) => { const [rows, setRows] = useState([]) const [managers, setManagers] = useState([]) const [selectedManagers, setSelectedManagers] = useState>(new Set()) const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) - const [editingCell, setEditingCell] = useState<{ rowIndex: number; field: string } | null>(null) - const [editValue, setEditValue] = useState('') - const [selectedRowIndex, setSelectedRowIndex] = useState(null) + const [searchQuery, setSearchQuery] = useState('') const logger = useLogger('MaterialType') - const tableRef = useRef(null) - const inputRef = useRef(null) - const selectRef = useRef(null) - - // Confirmation dialog hook const { confirm, dialog: confirmDialog } = useConfirmDialog() - // Calculate pending changes count const pendingCount = rows.filter( (r) => r.state === 'new' || r.state === 'modified' || r.state === 'deleted' ).length + const isSynced = pendingCount === 0; + + // UUID Generator for stable React keys + const generateId = () => Math.random().toString(36).substring(2, 9) + Date.now().toString(36); const loadData = useCallback(async () => { setLoading(true) try { - // Load managers list const managersResult = await window.electron.materialType.getManagers() if (managersResult.success && managersResult.data) { setManagers(managersResult.data) @@ -75,28 +220,23 @@ export const MaterialTypeManagementDialog: React.FC ({ + localId: generateId(), record, state: 'original' as const, originalRecord: { ...record } })) ) - setSelectedRowIndex(null) } catch (error) { logger.error('Failed to load material types', { error: error instanceof Error ? error.message : String(error), @@ -108,137 +248,74 @@ export const MaterialTypeManagementDialog: React.FC { - if (isOpen) { - void loadData() - } + if (isOpen) void loadData() }, [isOpen, loadData]) - // Focus input when editing starts - useEffect(() => { - const activeElement = inputRef.current ?? selectRef.current - if (editingCell && activeElement) { - activeElement.focus() - if (activeElement instanceof HTMLInputElement) { - activeElement.select() - } + const filteredRows = useMemo(() => { + let result = rows; + if (isAdmin && selectedManagers.size > 0) { + result = result.filter(row => selectedManagers.has(row.record.managerName) || row.state === 'new'); } - }, [editingCell]) + if (searchQuery) { + const lowerQuery = searchQuery.toLowerCase(); + result = result.filter(row => row.record.materialName.toLowerCase().includes(lowerQuery)); + } + return result; + }, [rows, isAdmin, selectedManagers, searchQuery]) - // Filter rows by selected managers (admin only) - const filteredRows = React.useMemo(() => { - if (!isAdmin) return rows - if (selectedManagers.size === 0) return rows - return rows.filter((row) => selectedManagers.has(row.record.managerName) || row.state === 'new') - }, [rows, isAdmin, selectedManagers]) - // Insert new row - const insertNewRow = useCallback(() => { + // --- Row Actions --- + const handleAdd = () => { const newRow: RowState = { - record: { - materialName: '', - managerName: isAdmin ? '' : currentUsername - }, + localId: generateId(), + record: { materialName: '', managerName: isAdmin ? '' : currentUsername }, state: 'new' } + setRows(prev => [newRow, ...prev]) + setSearchQuery('') + } - // 在 setRows 回调中计算索引并设置编辑状态 - setRows((prev) => { - const newIndex = prev.length - setTimeout(() => { - setEditingCell({ rowIndex: newIndex, field: 'materialName' }) - setEditValue('') - }, 0) - return [...prev, newRow] - }) - }, [isAdmin, currentUsername]) - - // Delete row - const deleteRow = useCallback((index: number) => { - setRows((prev) => { - const newRows = [...prev] - const row = newRows[index] - if (row.state === 'new') { - // Remove new rows directly - newRows.splice(index, 1) - } else { - // Mark existing rows as deleted - newRows[index] = { ...row, state: 'deleted' } - } - return newRows - }) - setSelectedRowIndex(null) - }, []) - - // Start editing a cell - const startEdit = useCallback( - (rowIndex: number, field: string) => { - const row = rows[rowIndex] - if (row.state === 'deleted') return - - setEditingCell({ rowIndex, field }) - setEditValue(row.record[field as keyof MaterialTypeRecord] as string) - }, - [rows] - ) - - // Save edit - const saveEdit = useCallback(() => { - if (!editingCell) return - - const { rowIndex, field } = editingCell - setRows((prev) => { - const newRows = [...prev] - const row = newRows[rowIndex] - const newValue = editValue.trim() - - // Update the record - newRows[rowIndex] = { + const handleUpdate = (localId: string, updates: Partial) => { + setRows(prev => prev.map(row => { + if (row.localId !== localId) return row; + return { ...row, - record: { - ...row.record, - [field]: newValue - }, + record: { ...row.record, ...updates }, state: row.state === 'new' ? 'new' : 'modified' } - return newRows + })) + } + + const handleDelete = (localId: string) => { + setRows(prev => { + const row = prev.find(r => r.localId === localId); + if (!row) return prev; + if (row.state === 'new') return prev.filter(r => r.localId !== localId); + return prev.map(r => r.localId === localId ? { ...r, state: 'deleted' } : r); }) + } - setEditingCell(null) - setEditValue('') - }, [editingCell, editValue]) + const handleRestore = (localId: string) => { + setRows(prev => prev.map(r => { + if (r.localId !== localId) return r; + // Determine if it was modified before deletion + const wasModified = r.originalRecord?.materialName !== r.record.materialName || + r.originalRecord?.managerName !== r.record.managerName; + return { ...r, state: wasModified ? 'modified' : 'original' } + })) + } - // Cancel edit - const cancelEdit = useCallback(() => { - setEditingCell(null) - setEditValue('') - }, []) + const handleReset = async () => { + if (pendingCount === 0) return + const confirmed = await confirm({ + title: '确认重置', + message: '确定要放弃所有未保存的更改吗?', + variant: 'warning' + }) + if (confirmed) void loadData() + } - // Handle keyboard events - const handleKeyDown = useCallback( - (event: React.KeyboardEvent) => { - if (editingCell) { - if (event.key === 'Enter') { - saveEdit() - } else if (event.key === 'Escape') { - cancelEdit() - } - return - } - - if (event.key === 'Insert') { - event.preventDefault() - insertNewRow() - } else if (event.key === 'Delete' && selectedRowIndex !== null) { - event.preventDefault() - deleteRow(selectedRowIndex) - } - }, - [editingCell, selectedRowIndex, insertNewRow, deleteRow, saveEdit, cancelEdit] - ) - - // Save all changes const handleSave = async () => { const toInsert: MaterialTypeRecord[] = [] const toUpdate: { old: MaterialTypeRecord; new: MaterialTypeRecord }[] = [] @@ -273,19 +350,11 @@ export const MaterialTypeManagementDialog: React.FC { - if (pendingCount === 0) return - const confirmed = await confirm({ - title: '确认重置', - message: '确定要放弃所有未保存的更改吗?', - variant: 'warning' - }) - if (confirmed) { - void loadData() - } - } - - // Handle close with unsaved changes warning const handleClose = async () => { if (pendingCount > 0) { const confirmed = await confirm({ @@ -329,253 +382,170 @@ export const MaterialTypeManagementDialog: React.FC { - if (row.state === 'deleted') { - return { backgroundColor: '#fee2e2', textDecoration: 'line-through', opacity: 0.6 } - } - if (row.state === 'new') { - return { backgroundColor: '#dcfce7' } - } - if (row.state === 'modified') { - return { backgroundColor: '#fef9c3' } - } - return {} - } + if (!isOpen) return null; return ( - -
- {/* Manager filter (admin only) */} - {isAdmin && ( -
-
-
- - 按负责人筛选 +
+
+ + {/* --- Header --- */} +
+
+
+ Material Type Management +
+

物料类型管理

+

+ {isAdmin ? '集中维护和管理所有负责人的物料关键词。' : '管理属于您的物料关键词。'} +

+
+ +
+ + {/* --- Toolbar --- */} +
+
+ + + +
+
+
-
- - + setSearchQuery(e.target.value)} + className="pl-9 pr-4 py-2 bg-slate-50 border border-transparent outline-none focus:border-indigo-200 focus:bg-white rounded-lg text-sm text-slate-700 w-56 transition-all" + /> +
+
+ +
+
+
+ 记录 + {filteredRows.length} +
+
+ + {isAdmin ? '管理员视图' : currentUsername} +
+
+ {isSynced ? <> 已同步 : <> 待保存 {pendingCount}}
-
- {managers.map((manager) => ( -
+
+ + {/* --- Admin Manager Filter --- */} + {isAdmin && managers.length > 0 && ( +
+
+ 负责人筛选 +
+
+ + {managers.map((manager) => { + const isSelected = selectedManagers.has(manager); + return ( + + ) + })}
)} - {/* Toolbar */} -
-
- - - -
-
- {pendingCount > 0 && ( - - {pendingCount} 项待保存 - - )} - -
-
- - {/* Table */} -
+ {/* --- Main Content Grid --- */} +
{loading ? ( -
加载中...
+
+
+ + 加载数据中... +
+
+ ) : filteredRows.length > 0 ? ( +
+ {filteredRows.map(row => ( + + ))} +
) : ( - - - - - - - - - {filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? ( - - - - ) : ( - filteredRows - .filter((r) => r.state !== 'deleted') - .map((row, index) => { - const originalIndex = rows.indexOf(row) - const isSelected = selectedRowIndex === originalIndex - const isEditingMaterial = - editingCell?.rowIndex === originalIndex && - editingCell?.field === 'materialName' - const isEditingManager = - editingCell?.rowIndex === originalIndex && - editingCell?.field === 'managerName' - - return ( - setSelectedRowIndex(originalIndex)} - > - - - - ) - }) - )} - -
- 物料名称关键词 - 负责人
- 暂无数据,点击"新增"按钮添加物料类型关键词 -
- {isEditingMaterial ? ( - setEditValue(e.target.value)} - onBlur={saveEdit} - onKeyDown={(e) => { - if (e.key === 'Enter') saveEdit() - if (e.key === 'Escape') cancelEdit() - }} - className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - ) : ( -
startEdit(originalIndex, 'materialName')} - > - {row.record.materialName || ( - 双击编辑 - )} -
- )} -
- {isEditingManager ? ( - isAdmin ? ( - - ) : ( - setEditValue(e.target.value)} - onBlur={saveEdit} - onKeyDown={(e) => { - if (e.key === 'Enter') saveEdit() - if (e.key === 'Escape') cancelEdit() - }} - className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" - /> - ) - ) : ( -
startEdit(originalIndex, 'managerName')} - > - {row.record.managerName || ( - 双击编辑 - )} -
- )} -
+
+ +

未找到匹配的关键词记录

+
)}
- {/* Footer info */} -
- - 双击单元格编辑 | Insert 新增 | Delete 删除 - {isAdmin && ' | 绿色=新增 | 黄色=已修改'} - - 共 {filteredRows.filter((r) => r.state !== 'deleted').length} 条记录 -
-
+ {/* --- Footer --- */} +
+

支持直接点击卡片编辑名称,回车快速保存,清空内容即为删除。

+

展示 {filteredRows.length} / {rows.length} 条记录

+
- {/* Confirmation Dialog */} +
+ + {/* Retain the original confirmation dialog */} {confirmDialog && } - +
) } -export default MaterialTypeManagementDialog +export default MaterialTypeManagementDialog \ No newline at end of file