import React, { useEffect, useMemo, useState } from 'react'; import { Empty, 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 { message } from '../ui/app-message'; interface ClassroomOption { id: number; name: string; building?: string | null; } 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 [data, setData] = useState([]); const [classrooms, setClassrooms] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [editing, setEditing] = useState(null); const [saving, setSaving] = useState(false); const [keyword, setKeyword] = useState(''); const [form] = Form.useForm(); const loadData = async () => { setLoading(true); try { const [devices, classroomList] = await Promise.all([ api.get('/attendance-devices'), api.get('/classrooms'), ]); setData(devices); setClassrooms(classroomList.filter((item: any) => item.status !== 'archived')); } catch (error: any) { message.error(error?.message || '加载考勤机绑定失败'); } finally { setLoading(false); } }; useEffect(() => { void loadData(); }, []); 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' }); 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, }); setModalOpen(true); }; const handleSave = async () => { const values = await form.validateFields(); setSaving(true); try { if (editing) { await api.put(`/attendance-devices/${editing.id}`, values); message.success('考勤机绑定已更新'); } else { await api.post('/attendance-devices', values); message.success('考勤机绑定已创建'); } setModalOpen(false); setEditing(null); form.resetFields(); await loadData(); } catch (error: any) { message.error(error?.message || '保存失败'); } finally { setSaving(false); } }; const handleDelete = async (id: number) => { try { await api.delete(`/attendance-devices/${id}`); message.success('已删除绑定'); await loadData(); } catch (error: any) { message.error(error?.message || '删除失败'); } }; const columns: ColumnsType = [ { title: '设备名称', dataIndex: 'deviceName', width: 180 }, { title: 'SN 码', dataIndex: 'deviceSn', width: 220, render: (value) => {value} }, { title: '绑定教室', dataIndex: ['classroom', 'name'], width: 160, render: (_value, record) => record.classroom?.name || `教室 ${record.classroomId}` }, { title: '位置', dataIndex: 'location', render: (value) => value || }, { title: '状态', dataIndex: 'status', width: 90, render: (value: keyof typeof statusMeta) => {statusMeta[value]?.text || value} }, { title: '备注', dataIndex: 'notes', ellipsis: true, render: (value) => value || }, { title: '操作', width: 150, render: (_, record) => ( openEdit(record)}> 编辑 handleDelete(record.id)}> 删除 ), }, ]; return (
setKeyword(event.target.value)} /> } onClick={openCreate}> 添加考勤机
rowKey="id" columns={columns} dataSource={filteredData} loading={loading} locale={{ emptyText: }} pagination={{ defaultPageSize: 20, showSizeChanger: true }} /> { setModalOpen(false); setEditing(null); }} confirmLoading={saving} okText="保存" >
); }; export default AttendanceDevicesPage;