import React, { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useApiMutation } from '../hooks/useApiMutation'; import { validateResponse } from '../utils/validate'; import { attendanceDevicesSchema, classroomOptionsSchema } from '../api/schemas'; import { Form, Input, Modal, Popconfirm, Select, Space, Table, Tag } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { PlusOutlined } from '@ant-design/icons'; import api from '../api'; import PermissionButton from '../components/PermissionButton'; import EditableCell from '../components/EditableCell'; import { QueryErrorState, QueryEmpty } from '../components/QueryState'; import { message } from '../ui/app-message'; import { useDirtyGuard } from '../hooks/useDirtyGuard'; import { usePermission } from '../hooks/usePermission'; interface ClassroomOption { id: number; name: string; building?: string | null; status?: string; } interface AttendanceDeviceRow { id: number; deviceSn: string; deviceName: string; classroomId: number; classroom?: ClassroomOption | null; status: 'active' | 'disabled'; location?: string | null; notes?: string | null; } const statusMeta = { active: { text: '启用', color: 'green' }, disabled: { text: '停用', color: 'default' }, } as const; const AttendanceDevicesPage: React.FC = () => { const { hasPermission } = usePermission(); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [saving, setSaving] = useState(false); const [keyword, setKeyword] = useState(''); const [form] = Form.useForm(); const formGuard = useDirtyGuard(form); const { data: fetchResult = { devices: [], classrooms: [] }, isLoading, isFetching, isError, refetch, } = useQuery<{ devices: AttendanceDeviceRow[]; classrooms: ClassroomOption[] }>({ queryKey: ['attendance-devices'], queryFn: async () => { const [devices, classroomList] = await Promise.all([ api.get('/attendance-devices'), api.get('/classrooms'), ]); return { devices: validateResponse(attendanceDevicesSchema, devices), classrooms: validateResponse( classroomOptionsSchema, classroomList, ).filter((item: ClassroomOption) => item.status !== 'archived'), }; }, }); const data = fetchResult.devices; const classrooms = fetchResult.classrooms; const loading = isLoading || isFetching; const saveMutation = useApiMutation( async (values: Record) => editing ? api.put(`/attendance-devices/${editing.id}`, values) : api.post('/attendance-devices', values), { invalidate: [['attendance-devices']] }, ); const saveCellMutation = useApiMutation( async ({ record, field, value }: { record: AttendanceDeviceRow; field: string; value: unknown }) => api.put(`/attendance-devices/${record.id}`, { [field]: value }), { invalidate: [['attendance-devices']] }, ); const deleteMutation = useApiMutation( async (id: number) => api.delete(`/attendance-devices/${id}`), { invalidate: [['attendance-devices']] }, ); const classroomOptions = useMemo( () => classrooms.map((item) => ({ value: item.id, label: item.building ? `${item.name}(${item.building})` : item.name, })), [classrooms], ); const filteredData = useMemo(() => { const text = keyword.trim().toLocaleLowerCase('zh-CN'); if (!text) return data; return data.filter((item) => [item.deviceSn, item.deviceName, item.classroom?.name, item.location].some((value) => (value || '').toLocaleLowerCase('zh-CN').includes(text), ), ); }, [data, keyword]); const openCreate = () => { setEditing(null); form.resetFields(); form.setFieldsValue({ status: 'active' }); formGuard.snapshot(); setModalOpen(true); }; const openEdit = (record: AttendanceDeviceRow) => { setEditing(record); form.setFieldsValue({ deviceSn: record.deviceSn, deviceName: record.deviceName, classroomId: record.classroomId, status: record.status, location: record.location, notes: record.notes, }); formGuard.snapshot(); setModalOpen(true); }; const handleSave = async () => { const values = await form.validateFields(); setSaving(true); try { await saveMutation.mutateAsync(values); message.success(editing ? '考勤机绑定已更新' : '考勤机绑定已创建'); setModalOpen(false); setEditing(null); form.resetFields(); } catch { // 错误提示由 useApiMutation 统一处理 } finally { setSaving(false); } }; const saveCell = async (record: AttendanceDeviceRow, field: string, value: unknown) => { try { await saveCellMutation.mutateAsync({ record, field, value }); message.success('已保存'); } catch { // 错误提示由 useApiMutation 统一处理 } }; const handleDelete = async (id: number) => { try { await deleteMutation.mutateAsync(id); message.success('已停用绑定'); } catch { // 错误提示由 useApiMutation 统一处理 } }; const columns: ColumnsType = [ { title: '设备名称', dataIndex: 'deviceName', width: 180, render: (value: string, record) => ( saveCell(record, 'deviceName', next)} > {value} ), }, { title: 'SN 码', dataIndex: 'deviceSn', width: 220, render: (value, record) => ( saveCell(record, 'deviceSn', next)} > {value} ), }, { title: '绑定教室', dataIndex: ['classroom', 'name'], width: 160, render: (_value, record) => ( ({ value: item.id, label: item.building ? `${item.building} · ${item.name}` : item.name, }))} permission="classroom:edit" required onSave={(next) => saveCell(record, 'classroomId', next)} > {record.classroom?.name || `教室 ${record.classroomId}`} ), }, { title: '位置', dataIndex: 'location', render: (value, record) => ( saveCell(record, 'location', next)} > {value || } ), }, { title: '状态', dataIndex: 'status', width: 90, render: (value: keyof typeof statusMeta, record) => ( saveCell(record, 'status', next)} > {statusMeta[value]?.text || value} ), }, { title: '备注', dataIndex: 'notes', ellipsis: true, render: (value, record) => ( saveCell(record, 'notes', next)} > {value || } ), }, { title: '操作', width: 150, render: (_, record) => ( openEdit(record)} > 编辑 handleDelete(record.id)}> 停用 ), }, ]; return (
setKeyword(event.target.value)} /> } onClick={openCreate} > 添加考勤机
{isError ? ( void refetch()} /> ) : ( rowKey="id" columns={columns} dataSource={filteredData} loading={loading} locale={{ emptyText: ( , onClick: openCreate } : undefined } /> ), }} pagination={{ defaultPageSize: 20, showSizeChanger: true }} /> )} formGuard.confirmClose(() => { setModalOpen(false); setEditing(null); }) } confirmLoading={saving} okText="保存" >
); }; export default AttendanceDevicesPage;