|
|
@@ -0,0 +1,450 @@
|
|
|
+import React, { useState, useEffect, useRef } from 'react';
|
|
|
+import { Search, Plus, RefreshCw, Edit2, Trash2, Eye } from 'lucide-react';
|
|
|
+import { lockCabinetApi, LockCabinetVO, PageParam } from '../../api/lockCabinet';
|
|
|
+import { toast } from 'sonner';
|
|
|
+import { dateFormatter } from '../../utils/formatTime';
|
|
|
+import { DICT_TYPE, getDictLabel, getStrDictOptions } from '../../utils/dict';
|
|
|
+import { Modal, Table, Button as AntButton, Image, Input, Select, Space } from 'antd';
|
|
|
+import { ExclamationCircleOutlined, SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
|
+import type { ColumnsType } from 'antd/es/table';
|
|
|
+import LockCabinetForm, { LockCabinetFormRef } from './LockCabinetForm';
|
|
|
+import { ImageWithFallback } from '../figma/ImageWithFallback';
|
|
|
+import { useNavigate } from 'react-router-dom';
|
|
|
+
|
|
|
+export default function SystemLockCabinetManagement() {
|
|
|
+ const navigate = useNavigate();
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
+ const [list, setList] = useState<LockCabinetVO[]>([]);
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
+ const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
|
|
+ const [queryParams, setQueryParams] = useState<PageParam>({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ cabinetName: undefined,
|
|
|
+ isOnline: undefined,
|
|
|
+ });
|
|
|
+ const formRef = useRef<LockCabinetFormRef>(null);
|
|
|
+ const [isOnlineOptions] = useState(() => {
|
|
|
+ try {
|
|
|
+ const options = getStrDictOptions(DICT_TYPE.ISONLINE_STATUS);
|
|
|
+ console.log('是否在线选项:', options);
|
|
|
+ return options || [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取是否在线选项失败:', error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取机柜列表
|
|
|
+ const getList = async (params?: PageParam) => {
|
|
|
+ const currentParams = params || queryParams;
|
|
|
+ // 过滤掉 undefined 的参数
|
|
|
+ const cleanParams: PageParam = {};
|
|
|
+ Object.keys(currentParams).forEach(key => {
|
|
|
+ if (currentParams[key] !== undefined && currentParams[key] !== null && currentParams[key] !== '') {
|
|
|
+ cleanParams[key] = currentParams[key];
|
|
|
+ }
|
|
|
+ });
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ console.log('SystemLockCabinetManagement: 开始获取机柜列表', cleanParams);
|
|
|
+ const response = await lockCabinetApi.getIsLockCabinetPage(cleanParams);
|
|
|
+ console.log('SystemLockCabinetManagement: API 响应', response);
|
|
|
+
|
|
|
+ // 处理响应数据 - 兼容不同的响应格式
|
|
|
+ let data;
|
|
|
+ if (response && typeof response === 'object') {
|
|
|
+ // 如果响应有 data 属性,使用 data;否则直接使用 response
|
|
|
+ data = (response as any).data !== undefined ? (response as any).data : response;
|
|
|
+ } else {
|
|
|
+ data = response;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保 data 是对象
|
|
|
+ if (!data || typeof data !== 'object') {
|
|
|
+ console.warn('SystemLockCabinetManagement: 响应数据格式异常', data);
|
|
|
+ setList([]);
|
|
|
+ setTotal(0);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setList(data.list || []);
|
|
|
+ setTotal(data.total || 0);
|
|
|
+ console.log('SystemLockCabinetManagement: 设置列表数据', data.list, '总数', data.total);
|
|
|
+ // 调试:打印第一条数据的结构,检查 ID 字段名
|
|
|
+ if (data.list && data.list.length > 0) {
|
|
|
+ console.log('SystemLockCabinetManagement: 第一条数据示例', data.list[0]);
|
|
|
+ }
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('SystemLockCabinetManagement: 获取机柜列表失败', error);
|
|
|
+ console.error('错误详情:', {
|
|
|
+ message: error?.message,
|
|
|
+ response: error?.response,
|
|
|
+ status: error?.response?.status,
|
|
|
+ data: error?.response?.data,
|
|
|
+ code: error?.response?.data?.code,
|
|
|
+ config: error?.config,
|
|
|
+ url: error?.config?.url
|
|
|
+ });
|
|
|
+ setList([]);
|
|
|
+ setTotal(0);
|
|
|
+
|
|
|
+ // 显示更详细的错误信息
|
|
|
+ let errorMessage = '获取机柜列表失败';
|
|
|
+ if (error?.response?.data?.message) {
|
|
|
+ errorMessage = error.response.data.message;
|
|
|
+ } else if (error?.message) {
|
|
|
+ errorMessage = error.message;
|
|
|
+ } else if (error?.response?.data) {
|
|
|
+ // 如果响应数据存在但没有 message,尝试显示整个响应
|
|
|
+ errorMessage = `请求失败: ${JSON.stringify(error.response.data)}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只在非静默模式下显示错误(避免重复显示)
|
|
|
+ if (!error?.silent) {
|
|
|
+ toast.error(errorMessage);
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 组件挂载和分页参数变化时获取数据
|
|
|
+ useEffect(() => {
|
|
|
+ getList();
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ }, [queryParams.pageNo, queryParams.pageSize]);
|
|
|
+
|
|
|
+ // 搜索
|
|
|
+ const handleQuery = () => {
|
|
|
+ const newParams = { ...queryParams, pageNo: 1 };
|
|
|
+ setQueryParams(newParams);
|
|
|
+ getList(newParams);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重置搜索
|
|
|
+ const resetQuery = () => {
|
|
|
+ const resetParams: PageParam = {
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ cabinetName: undefined,
|
|
|
+ isOnline: undefined,
|
|
|
+ };
|
|
|
+ setQueryParams(resetParams);
|
|
|
+ getList(resetParams);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 打开表单
|
|
|
+ const openForm = (type: string, id?: number) => {
|
|
|
+ if (formRef.current) {
|
|
|
+ formRef.current.open(type, id);
|
|
|
+ } else {
|
|
|
+ toast.error('表单组件未初始化,请刷新页面重试');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Ant Design Table 的行选择配置
|
|
|
+ const rowSelection = {
|
|
|
+ selectedRowKeys: selectedIds,
|
|
|
+ onChange: (selectedRowKeys: React.Key[]) => {
|
|
|
+ setSelectedIds(selectedRowKeys as number[]);
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // 删除机柜
|
|
|
+ const handleDelete = async (id?: number) => {
|
|
|
+ const ids = id ? [id] : selectedIds;
|
|
|
+ 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 lockCabinetApi.deleteIsLockCabinetByCabinetIds(ids.join(','));
|
|
|
+ toast.success('删除成功');
|
|
|
+ setSelectedIds([]);
|
|
|
+ await getList();
|
|
|
+ } catch (error: any) {
|
|
|
+ toast.error(error.message || '删除失败');
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 查看详情
|
|
|
+ const lookDetail = (row: LockCabinetVO) => {
|
|
|
+ // 使用 id 或 cabinetId,优先使用 id
|
|
|
+ const cabinetId = (row as any).id || row.cabinetId;
|
|
|
+ if (cabinetId) {
|
|
|
+ // 直接使用 navigate 跳转,Dashboard 会检测路径变化
|
|
|
+ navigate(`/lock-cabinet/detail?cabinetId=${cabinetId}`);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="space-y-4">
|
|
|
+ {/* 搜索栏 */}
|
|
|
+ <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
|
|
|
+ <Space size="middle" wrap>
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-sm font-medium text-gray-700 whitespace-nowrap">锁柜名称:</span>
|
|
|
+ <Input
|
|
|
+ placeholder="请输入锁柜名称"
|
|
|
+ value={queryParams.cabinetName || ''}
|
|
|
+ onChange={(e) => setQueryParams(prev => ({ ...prev, cabinetName: e.target.value }))}
|
|
|
+ onPressEnter={handleQuery}
|
|
|
+ style={{ width: 200 }}
|
|
|
+ allowClear
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <span className="text-sm font-medium text-gray-700 whitespace-nowrap">是否在线:</span>
|
|
|
+ <Select
|
|
|
+ placeholder="请选择是否在线"
|
|
|
+ value={queryParams.isOnline || undefined}
|
|
|
+ onChange={(value) => setQueryParams(prev => ({ ...prev, isOnline: value }))}
|
|
|
+ style={{ width: 200 }}
|
|
|
+ allowClear
|
|
|
+ >
|
|
|
+ {isOnlineOptions && isOnlineOptions.length > 0 ? (
|
|
|
+ isOnlineOptions.map(option => (
|
|
|
+ <Select.Option key={option.value} value={String(option.value)}>
|
|
|
+ {option.label}
|
|
|
+ </Select.Option>
|
|
|
+ ))
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ <Select.Option value="0">离线</Select.Option>
|
|
|
+ <Select.Option value="1">在线</Select.Option>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 操作按钮组 */}
|
|
|
+ <AntButton
|
|
|
+ type="primary"
|
|
|
+ icon={<SearchOutlined />}
|
|
|
+ onClick={handleQuery}
|
|
|
+ >
|
|
|
+ 搜索
|
|
|
+ </AntButton>
|
|
|
+
|
|
|
+ <AntButton
|
|
|
+ icon={<ReloadOutlined />}
|
|
|
+ onClick={resetQuery}
|
|
|
+ >
|
|
|
+ 重置
|
|
|
+ </AntButton>
|
|
|
+
|
|
|
+ <AntButton
|
|
|
+ type="primary"
|
|
|
+ icon={<PlusOutlined />}
|
|
|
+ onClick={() => openForm('create')}
|
|
|
+ >
|
|
|
+ 新增
|
|
|
+ </AntButton>
|
|
|
+
|
|
|
+ <AntButton
|
|
|
+ danger
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
+ onClick={handleDelete}
|
|
|
+ disabled={selectedIds.length === 0}
|
|
|
+ >
|
|
|
+ 批量删除
|
|
|
+ </AntButton>
|
|
|
+ </Space>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 表格 */}
|
|
|
+ <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
|
+ <Table
|
|
|
+ rowKey={(record) => {
|
|
|
+ // 兼容不同的 ID 字段名
|
|
|
+ const id = record.cabinetId || (record as any).id || (record as any).cabinetId;
|
|
|
+ return id || `row-${Math.random()}`;
|
|
|
+ }}
|
|
|
+ loading={loading}
|
|
|
+ dataSource={list}
|
|
|
+ rowSelection={rowSelection}
|
|
|
+ columns={[
|
|
|
+ {
|
|
|
+ title: '锁柜名称',
|
|
|
+ dataIndex: 'cabinetName',
|
|
|
+ key: 'cabinetName',
|
|
|
+ width: 150,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '硬件ID',
|
|
|
+ dataIndex: 'hardwareId',
|
|
|
+ key: 'hardwareId',
|
|
|
+ width: 120,
|
|
|
+ render: (text) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '硬件序列号',
|
|
|
+ dataIndex: 'serialNumber',
|
|
|
+ key: 'serialNumber',
|
|
|
+ width: 150,
|
|
|
+ render: (text) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '岗位',
|
|
|
+ dataIndex: 'workstationName',
|
|
|
+ key: 'workstationName',
|
|
|
+ width: 150,
|
|
|
+ render: (text) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '图片',
|
|
|
+ dataIndex: 'cabinetPicture',
|
|
|
+ key: 'cabinetPicture',
|
|
|
+ width: 100,
|
|
|
+ render: (url: string) => {
|
|
|
+ if (!url) return '-';
|
|
|
+ return (
|
|
|
+ <Image
|
|
|
+ src={url}
|
|
|
+ alt="图片"
|
|
|
+ width={50}
|
|
|
+ height={50}
|
|
|
+ style={{ objectFit: 'cover', cursor: 'pointer' }}
|
|
|
+ preview={{
|
|
|
+ mask: '查看',
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '图标',
|
|
|
+ dataIndex: 'cabinetIcon',
|
|
|
+ key: 'cabinetIcon',
|
|
|
+ width: 100,
|
|
|
+ render: (url: string) => {
|
|
|
+ if (!url) return '-';
|
|
|
+ return (
|
|
|
+ <Image
|
|
|
+ src={url}
|
|
|
+ alt="图标"
|
|
|
+ width={50}
|
|
|
+ height={50}
|
|
|
+ style={{ objectFit: 'cover', cursor: 'pointer' }}
|
|
|
+ preview={{
|
|
|
+ mask: '查看',
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '是否在线',
|
|
|
+ dataIndex: 'isOnline',
|
|
|
+ key: 'isOnline',
|
|
|
+ width: 100,
|
|
|
+ render: (value: string) => getDictLabel(DICT_TYPE.ISONLINE_STATUS, value) || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ dataIndex: 'status',
|
|
|
+ key: 'status',
|
|
|
+ width: 100,
|
|
|
+ render: (value: string) => {
|
|
|
+ if (value === null || value === undefined) return '-';
|
|
|
+ return getDictLabel(DICT_TYPE.CANBINET_STATUS, value) || '-';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '备注',
|
|
|
+ dataIndex: 'remark',
|
|
|
+ key: 'remark',
|
|
|
+ width: 150,
|
|
|
+ render: (text) => text || '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '创建时间',
|
|
|
+ dataIndex: 'createTime',
|
|
|
+ key: 'createTime',
|
|
|
+ width: 180,
|
|
|
+ render: (text) => text ? dateFormatter(text) : '-',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '详情',
|
|
|
+ key: 'detail',
|
|
|
+ width: 80,
|
|
|
+ align: 'center',
|
|
|
+ render: (_: any, record: LockCabinetVO) => (
|
|
|
+ <AntButton
|
|
|
+ type="link"
|
|
|
+ size="small"
|
|
|
+ onClick={() => lookDetail(record)}
|
|
|
+ >
|
|
|
+ 查看
|
|
|
+ </AntButton>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '操作',
|
|
|
+ key: 'action',
|
|
|
+ width: 150,
|
|
|
+ align: 'center',
|
|
|
+ render: (_: any, record: LockCabinetVO) => (
|
|
|
+ <div className="flex items-center gap-2 justify-center">
|
|
|
+ <AntButton
|
|
|
+ type="link"
|
|
|
+ size="small"
|
|
|
+ icon={<Edit2 className="w-4 h-4" />}
|
|
|
+ onClick={() => openForm('update', record.cabinetId || (record as any).id)}
|
|
|
+ >
|
|
|
+ 编辑
|
|
|
+ </AntButton>
|
|
|
+ <AntButton
|
|
|
+ type="link"
|
|
|
+ size="small"
|
|
|
+ danger
|
|
|
+ icon={<Trash2 className="w-4 h-4" />}
|
|
|
+ onClick={() => handleDelete(record.cabinetId || (record as any).id)}
|
|
|
+ >
|
|
|
+ 删除
|
|
|
+ </AntButton>
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ pagination={{
|
|
|
+ current: queryParams.pageNo,
|
|
|
+ pageSize: queryParams.pageSize,
|
|
|
+ total: total,
|
|
|
+ showTotal: (total) => `共 ${total} 条记录`,
|
|
|
+ showSizeChanger: true,
|
|
|
+ showQuickJumper: true,
|
|
|
+ onChange: (page, pageSize) => {
|
|
|
+ setQueryParams(prev => ({ ...prev, pageNo: page, pageSize: pageSize || 10 }));
|
|
|
+ },
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 表单弹窗 */}
|
|
|
+ <LockCabinetForm ref={formRef} onSuccess={() => {
|
|
|
+ // 编辑成功后刷新列表
|
|
|
+ getList();
|
|
|
+ }} />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|