|
|
@@ -0,0 +1,516 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { Search, Plus, RefreshCw, Edit2, Trash2 } from 'lucide-react';
|
|
|
+import { keyApi, KeyVO, PageParam } from '../api/Key';
|
|
|
+import { hardwareApi } from '../api/Hardware';
|
|
|
+import { toast } from 'sonner';
|
|
|
+import { Modal, Button, Input, Space, Table, Select, Form, Switch, Radio } from 'antd';
|
|
|
+import { ExclamationCircleOutlined } from '@ant-design/icons';
|
|
|
+import type { ColumnsType } from 'antd/es/table';
|
|
|
+import { DICT_TYPE, getStrDictOptions } from '../utils/dict';
|
|
|
+import { dateFormatter } from '../utils/formatTime';
|
|
|
+import { Button as UIButton } from './ui/button';
|
|
|
+
|
|
|
+interface KeyManagementProps {
|
|
|
+ subMenu?: string;
|
|
|
+}
|
|
|
+
|
|
|
+export default function KeyManagement({ subMenu }: KeyManagementProps) {
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [list, setList] = useState<KeyVO[]>([]);
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
+ const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
|
+ const [queryParams, setQueryParams] = useState<PageParam>({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ keyName: undefined,
|
|
|
+ });
|
|
|
+ const [showKeyForm, setShowKeyForm] = useState(false);
|
|
|
+ const [editingKey, setEditingKey] = useState<KeyVO | null>(null);
|
|
|
+
|
|
|
+ // 获取钥匙列表
|
|
|
+ const getList = async (params?: PageParam) => {
|
|
|
+ const currentParams = params || queryParams;
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const response = await keyApi.listKey(currentParams);
|
|
|
+ setList(response.list || []);
|
|
|
+ setTotal(response.total || 0);
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '获取钥匙列表失败');
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ getList();
|
|
|
+ }, [queryParams.pageNo, queryParams.pageSize, queryParams.keyName]);
|
|
|
+
|
|
|
+ // 搜索钥匙
|
|
|
+ const handleQuery = () => {
|
|
|
+ const newParams = { ...queryParams, pageNo: 1 };
|
|
|
+ setQueryParams(newParams);
|
|
|
+ getList(newParams);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重置搜索
|
|
|
+ const resetQuery = () => {
|
|
|
+ const resetParams: PageParam = {
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ keyName: undefined,
|
|
|
+ };
|
|
|
+ setQueryParams(resetParams);
|
|
|
+ getList(resetParams);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 删除钥匙
|
|
|
+ const handleDelete = async (id?: number) => {
|
|
|
+ const ids = id ? [id] : selectedRowKeys.map(key => Number(key));
|
|
|
+ if (ids.length === 0) {
|
|
|
+ toast.error('请选择要删除的数据');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Modal.confirm({
|
|
|
+ title: '确认删除',
|
|
|
+ icon: <ExclamationCircleOutlined />,
|
|
|
+ content: (
|
|
|
+ <div>
|
|
|
+ <p>确定要删除选中的 {ids.length} 条钥匙数据吗?</p>
|
|
|
+ <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ okText: '确定删除',
|
|
|
+ okType: 'danger',
|
|
|
+ cancelText: '取消',
|
|
|
+ onOk: async () => {
|
|
|
+ try {
|
|
|
+ await keyApi.delKey(ids);
|
|
|
+ toast.success('删除成功');
|
|
|
+ setSelectedRowKeys([]);
|
|
|
+ await getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '删除失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开钥匙表单
|
|
|
+ const openKeyForm = (type: 'create' | 'update', id?: number) => {
|
|
|
+ if (type === 'create') {
|
|
|
+ setEditingKey(null);
|
|
|
+ setShowKeyForm(true);
|
|
|
+ } else if (type === 'update' && id) {
|
|
|
+ keyApi.getKeyInfo(id).then((data) => {
|
|
|
+ setEditingKey(data);
|
|
|
+ setShowKeyForm(true);
|
|
|
+ }).catch((error: any) => {
|
|
|
+ toast.error(error.message || '获取钥匙信息失败');
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 保存钥匙
|
|
|
+ const saveKey = async (formData: KeyVO) => {
|
|
|
+ try {
|
|
|
+ if (formData.keyId || formData.id) {
|
|
|
+ await keyApi.updateKey({
|
|
|
+ ...formData,
|
|
|
+ keyId: formData.keyId || formData.id,
|
|
|
+ });
|
|
|
+ toast.success('修改成功');
|
|
|
+ } else {
|
|
|
+ await keyApi.addKey(formData);
|
|
|
+ toast.success('新增成功');
|
|
|
+ }
|
|
|
+ setShowKeyForm(false);
|
|
|
+ setEditingKey(null);
|
|
|
+ await getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '保存失败');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 钥匙列表表格列
|
|
|
+ const keyColumns: ColumnsType<KeyVO> = [
|
|
|
+ {
|
|
|
+ title: '钥匙名称',
|
|
|
+ dataIndex: 'keyName',
|
|
|
+ width: 180,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '钥匙NFC',
|
|
|
+ dataIndex: 'keyNfc',
|
|
|
+ width: 180,
|
|
|
+ ellipsis: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '钥匙型号',
|
|
|
+ dataIndex: 'keySpec',
|
|
|
+ width: 180,
|
|
|
+ ellipsis: true,
|
|
|
+ render: (text: string) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: 'MAC地址',
|
|
|
+ dataIndex: 'macAddress',
|
|
|
+ width: 150,
|
|
|
+ render: (text: string) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ dataIndex: 'exStatus',
|
|
|
+ width: 100,
|
|
|
+ align: 'center',
|
|
|
+ render: (status: string) => {
|
|
|
+ if (status === null || status === undefined) return '-';
|
|
|
+ return (
|
|
|
+ <Switch
|
|
|
+ checked={status === '1'}
|
|
|
+ disabled
|
|
|
+ checkedChildren="启用"
|
|
|
+ unCheckedChildren="禁用"
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '备注',
|
|
|
+ dataIndex: 'exRemark',
|
|
|
+ ellipsis: true,
|
|
|
+ render: (text: string) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '所属硬件',
|
|
|
+ dataIndex: 'hardwareName',
|
|
|
+ width: 150,
|
|
|
+ render: (text: string) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '创建时间',
|
|
|
+ dataIndex: 'createTime',
|
|
|
+ width: 180,
|
|
|
+ render: (text: string | Date) => text ? dateFormatter(text) : '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '操作',
|
|
|
+ width: 150,
|
|
|
+ align: 'center',
|
|
|
+ fixed: 'right',
|
|
|
+ render: (_: any, record: KeyVO) => (
|
|
|
+ <div className="flex items-center gap-2 justify-center">
|
|
|
+ <UIButton
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => openKeyForm('update', record.keyId || record.id)}
|
|
|
+ className="h-8 px-2"
|
|
|
+ >
|
|
|
+ <Edit2 className="w-4 h-4" />
|
|
|
+ <span className="ml-1">编辑</span>
|
|
|
+ </UIButton>
|
|
|
+ <UIButton
|
|
|
+ variant="ghost"
|
|
|
+ size="sm"
|
|
|
+ onClick={() => handleDelete(record.keyId || record.id)}
|
|
|
+ className="h-8 px-2 text-red-600 hover:text-red-700"
|
|
|
+ >
|
|
|
+ <Trash2 className="w-4 h-4" />
|
|
|
+ <span className="ml-1">删除</span>
|
|
|
+ </UIButton>
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-4">
|
|
|
+ {/* 钥匙列表 - 搜索栏和表格放在一起 */}
|
|
|
+ <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm overflow-hidden">
|
|
|
+ {/* 搜索栏 */}
|
|
|
+ <div className="p-5 border-b border-gray-200">
|
|
|
+ <div className="flex items-center justify-between gap-4 flex-wrap">
|
|
|
+ {/* 搜索输入框 */}
|
|
|
+ <div className="flex items-center gap-3 flex-wrap flex-1">
|
|
|
+ <div className="flex items-center gap-3">
|
|
|
+ <label className="text-sm font-medium text-gray-700 whitespace-nowrap">钥匙名称:</label>
|
|
|
+ <Input
|
|
|
+ value={queryParams.keyName || ''}
|
|
|
+ onChange={(e) => setQueryParams({ ...queryParams, keyName: e.target.value })}
|
|
|
+ onPressEnter={handleQuery}
|
|
|
+ placeholder="请输入钥匙名称"
|
|
|
+ style={{ width: 192 }}
|
|
|
+ allowClear
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 操作按钮组 */}
|
|
|
+ <Space>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<Search className="w-4 h-4" />}
|
|
|
+ onClick={handleQuery}
|
|
|
+ >
|
|
|
+ 搜索
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ icon={<RefreshCw className="w-4 h-4" />}
|
|
|
+ onClick={resetQuery}
|
|
|
+ >
|
|
|
+ 重置
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<Plus className="w-4 h-4" />}
|
|
|
+ onClick={() => openKeyForm('create')}
|
|
|
+ >
|
|
|
+ 新增
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ <Button
|
|
|
+ danger
|
|
|
+ icon={<Trash2 className="w-4 h-4" />}
|
|
|
+ onClick={() => handleDelete()}
|
|
|
+ disabled={selectedRowKeys.length === 0}
|
|
|
+ >
|
|
|
+ 批量删除
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 表格 */}
|
|
|
+ <div>
|
|
|
+ <Table
|
|
|
+ columns={keyColumns}
|
|
|
+ dataSource={list}
|
|
|
+ rowKey={(record) => record.keyId || record.id || ''}
|
|
|
+ loading={loading}
|
|
|
+ pagination={false}
|
|
|
+ scroll={{ x: 'max-content' }}
|
|
|
+ rowSelection={{
|
|
|
+ selectedRowKeys,
|
|
|
+ onChange: setSelectedRowKeys,
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 分页 */}
|
|
|
+ {!loading && list.length > 0 && (
|
|
|
+ <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div className="text-sm text-gray-600">
|
|
|
+ 共 <span className="text-blue-600 font-medium">{total}</span> 条记录
|
|
|
+ </div>
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <Button
|
|
|
+ onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
|
|
|
+ disabled={queryParams.pageNo! <= 1}
|
|
|
+ >
|
|
|
+ 上一页
|
|
|
+ </Button>
|
|
|
+ <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
|
|
|
+ {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
|
|
|
+ </span>
|
|
|
+ <Button
|
|
|
+ onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
|
|
|
+ disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
|
|
|
+ >
|
|
|
+ 下一页
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+
|
|
|
+ {/* 钥匙表单弹窗 */}
|
|
|
+ {showKeyForm && (
|
|
|
+ <KeyFormModal
|
|
|
+ visible={showKeyForm}
|
|
|
+ editingKey={editingKey}
|
|
|
+ onCancel={() => {
|
|
|
+ setShowKeyForm(false);
|
|
|
+ setEditingKey(null);
|
|
|
+ }}
|
|
|
+ onSave={saveKey}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// 钥匙表单弹窗组件
|
|
|
+interface KeyFormModalProps {
|
|
|
+ visible: boolean;
|
|
|
+ editingKey: KeyVO | null;
|
|
|
+ onCancel: () => void;
|
|
|
+ onSave: (data: KeyVO) => void;
|
|
|
+}
|
|
|
+
|
|
|
+function KeyFormModal({ visible, editingKey, onCancel, onSave }: KeyFormModalProps) {
|
|
|
+ const [form] = Form.useForm();
|
|
|
+ const [formLoading, setFormLoading] = useState(false);
|
|
|
+ const [hardwareOptions, setHardwareOptions] = useState<{ label: string; value: number }[]>([]);
|
|
|
+ const [statusOptions] = useState(() => {
|
|
|
+ return getStrDictOptions(DICT_TYPE.KEY_STATUS);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取硬件列表
|
|
|
+ const getHardwareList = async () => {
|
|
|
+ try {
|
|
|
+ const response = await hardwareApi.listHardware({ pageNo: 1, pageSize: -1 });
|
|
|
+ const options = response.list.map(item => ({
|
|
|
+ label: item.hardwareName,
|
|
|
+ value: item.id!,
|
|
|
+ }));
|
|
|
+ setHardwareOptions(options);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取硬件列表失败:', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (visible) {
|
|
|
+ getHardwareList();
|
|
|
+
|
|
|
+ if (editingKey) {
|
|
|
+ form.setFieldsValue({
|
|
|
+ hardwareId: editingKey.hardwareId,
|
|
|
+ keyName: editingKey.keyName || '',
|
|
|
+ keyNfc: editingKey.keyNfc || '',
|
|
|
+ macAddress: editingKey.macAddress || '',
|
|
|
+ keySpec: editingKey.keySpec || '',
|
|
|
+ exStatus: editingKey.exStatus || '1',
|
|
|
+ exRemark: editingKey.exRemark || '',
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ form.resetFields();
|
|
|
+ form.setFieldsValue({
|
|
|
+ hardwareId: undefined,
|
|
|
+ keyName: '',
|
|
|
+ keyNfc: '',
|
|
|
+ macAddress: '',
|
|
|
+ keySpec: '',
|
|
|
+ exStatus: '1',
|
|
|
+ exRemark: '',
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 弹窗关闭时重置表单
|
|
|
+ form.resetFields();
|
|
|
+ }
|
|
|
+ }, [visible, editingKey, form]);
|
|
|
+
|
|
|
+ const handleSubmit = async () => {
|
|
|
+ try {
|
|
|
+ const values = await form.validateFields();
|
|
|
+ setFormLoading(true);
|
|
|
+
|
|
|
+ const submitData: KeyVO = {
|
|
|
+ ...values,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 如果是编辑模式,添加 keyId
|
|
|
+ if (editingKey) {
|
|
|
+ submitData.keyId = editingKey.keyId || editingKey.id;
|
|
|
+ submitData.id = editingKey.keyId || editingKey.id;
|
|
|
+ }
|
|
|
+
|
|
|
+ onSave(submitData);
|
|
|
+ } catch (error) {
|
|
|
+ // 表单验证失败
|
|
|
+ } finally {
|
|
|
+ setFormLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Modal
|
|
|
+ title={editingKey ? '编辑钥匙' : '新增钥匙'}
|
|
|
+ open={visible}
|
|
|
+ onOk={handleSubmit}
|
|
|
+ onCancel={onCancel}
|
|
|
+ okText="确定"
|
|
|
+ cancelText="取消"
|
|
|
+ confirmLoading={formLoading}
|
|
|
+ width={600}
|
|
|
+ >
|
|
|
+ <Form
|
|
|
+ form={form}
|
|
|
+ layout="vertical"
|
|
|
+ className="mt-4"
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ label="所属硬件"
|
|
|
+ name="hardwareId"
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ placeholder="请选择所属硬件"
|
|
|
+ allowClear
|
|
|
+ options={hardwareOptions}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="钥匙名称"
|
|
|
+ name="keyName"
|
|
|
+ rules={[{ required: true, message: '请输入钥匙名称' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入钥匙名称" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="钥匙NFC"
|
|
|
+ name="keyNfc"
|
|
|
+ rules={[{ required: true, message: '请输入钥匙NFC' }]}
|
|
|
+ >
|
|
|
+ <Input
|
|
|
+ placeholder="请输入钥匙NFC"
|
|
|
+ maxLength={16}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="MAC地址"
|
|
|
+ name="macAddress"
|
|
|
+ rules={[{ required: true, message: '请输入MAC地址' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入MAC地址" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="钥匙型号"
|
|
|
+ name="keySpec"
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入钥匙型号" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="状态"
|
|
|
+ name="exStatus"
|
|
|
+ >
|
|
|
+ <Radio.Group>
|
|
|
+ {statusOptions.map(option => (
|
|
|
+ <Radio key={option.value} value={option.value}>
|
|
|
+ {option.label}
|
|
|
+ </Radio>
|
|
|
+ ))}
|
|
|
+ </Radio.Group>
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ label="备注"
|
|
|
+ name="exRemark"
|
|
|
+ >
|
|
|
+ <Input.TextArea placeholder="请输入备注" rows={3} />
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|