| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- 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, Image, Input, Select, Space, Button as AntButton, Tooltip } from 'antd';
- import { ExclamationCircleOutlined, SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
- import { Button } from '../ui/button';
- import type { ColumnsType } from 'antd/es/table';
- import LockCabinetForm, { LockCabinetFormRef } from './LockCabinetForm';
- import { ImageWithFallback } from '../figma/ImageWithFallback';
- import { useNavigate } from 'react-router-dom';
- import PermissionWrapper from '../PermissionWrapper';
- import { useTranslation } from 'react-i18next';
- export default function SystemLockCabinetManagement() {
- const { t } = useTranslation();
- 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(t('common.formNotInitialized'));
- }
- };
- // 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(t('common.pleaseSelect'));
- return;
- }
- Modal.confirm({
- title: t('common.confirmDelete'),
- icon: <ExclamationCircleOutlined />,
- content: (
- <div>
- <p>{t('common.confirmDeleteText')} {ids.length} {t('common.items')}?</p>
- <p style={{ color: '#ff4d4f', marginTop: '8px' }}>{t('common.confirmDeleteWarning')}</p>
- </div>
- ),
- okText: t('common.confirmDelete'),
- okType: 'danger',
- cancelText: t('common.cancel'),
- onOk: async () => {
- try {
- await lockCabinetApi.deleteIsLockCabinetByCabinetIds(ids.join(','));
- toast.success(t('common.deleteSuccess'));
- setSelectedIds([]);
- await getList();
- } catch (error: any) {
- toast.error(error.message || t('common.deleteFailed'));
- }
- },
- });
- };
- // 查看详情
- const lookDetail = (row: LockCabinetVO) => {
- // 使用 id 或 cabinetId,优先使用 id
- const cabinetId = (row as any).id || row.cabinetId;
- if (cabinetId) {
- // 记录来源菜单信息,用于返回时恢复菜单状态
- const sourceMenu = { menu: 'systemConfig', subMenu: 'cabinetManagement' };
- sessionStorage.setItem('cabinetDetailSource', JSON.stringify(sourceMenu));
- // 直接使用 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-4 lg:p-5">
- <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
- {/* 搜索条件 */}
- <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
- <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.cabinetName')}:</label>
- <Input
- placeholder={t('form.cabinetNamePlaceholder')}
- value={queryParams.cabinetName || ''}
- onChange={(e) => setQueryParams(prev => ({ ...prev, cabinetName: e.target.value }))}
- onPressEnter={handleQuery}
- className="min-w-[150px] max-w-[200px]"
- allowClear
- />
- </div>
- <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
- <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('common.onlineStatus')}:</label>
- <Select
- placeholder={t('common.pleaseSelect')}
- value={queryParams.isOnline || undefined}
- onChange={(value) => setQueryParams(prev => ({ ...prev, isOnline: value }))}
- className="min-w-[150px] max-w-[200px]"
- 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">{t('common.offline')}</Select.Option>
- <Select.Option value="1">{t('common.online')}</Select.Option>
- </>
- )}
- </Select>
- </div>
- </div>
- {/* 操作按钮组 */}
- <Space className="flex-shrink-0">
- <PermissionWrapper permission="iscs:lock-cabinet:query">
- <AntButton
- type="primary"
- icon={<SearchOutlined />}
- onClick={handleQuery}
- >
- {t('common.search')}
- </AntButton>
- </PermissionWrapper>
-
- <PermissionWrapper permission="iscs:lock-cabinet:query">
- <AntButton
- icon={<ReloadOutlined />}
- onClick={resetQuery}
- >
- {t('common.reset')}
- </AntButton>
- </PermissionWrapper>
- <PermissionWrapper permission="iscs:lock-cabinet:create">
- <AntButton
- type="primary"
- icon={<PlusOutlined />}
- onClick={() => openForm('create')}
- >
- {t('common.addNew')}
- </AntButton>
- </PermissionWrapper>
- {/* <PermissionWrapper permission="iscs:lock-cabinet:delete">
- <AntButton
- danger
- icon={<DeleteOutlined />}
- onClick={handleDelete}
- disabled={selectedIds.length === 0}
- >
- 批量删除
- </AntButton>
- </PermissionWrapper> */}
- </Space>
- </div>
- </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: t('table.cabinetName'),
- dataIndex: 'cabinetName',
- key: 'cabinetName',
- width: 150,
- align: 'center',
- },
- {
- title: t('table.hardwareSerial'),
- dataIndex: 'serialNumber',
- key: 'serialNumber',
- width: 150,
- align: 'center',
- render: (text) => text || '-',
- },
- {
- title: t('table.position'),
- dataIndex: 'workstationName',
- key: 'workstationName',
- width: 150,
- align: 'center',
- render: (text) => text || '-',
- },
- {
- title: t('table.image'),
- dataIndex: 'cabinetPicture',
- key: 'cabinetPicture',
- width: 100,
- align: 'center',
- render: (url: string) => {
- if (!url) return '-';
- return (
- <div className="flex items-center justify-center">
- <Image
- src={url}
- alt={t('table.image')}
- width={50}
- height={50}
- style={{ objectFit: 'cover', cursor: 'pointer' }}
- preview={{
- mask: t('common.view'),
- }}
- />
- </div>
- );
- },
- },
- {
- title: t('table.icon'),
- dataIndex: 'cabinetIcon',
- key: 'cabinetIcon',
- width: 100,
- align: 'center',
- render: (url: string) => {
- if (!url) return '-';
- return (
- <div className="flex items-center justify-center">
- <Image
- src={url}
- alt={t('table.icon')}
- width={50}
- height={50}
- style={{ objectFit: 'cover', cursor: 'pointer' }}
- preview={{
- mask: t('common.view'),
- }}
- />
- </div>
- );
- },
- },
- {
- title: t('common.onlineStatus'),
- dataIndex: 'isOnline',
- key: 'isOnline',
- width: 100,
- align: 'center',
- render: (value: string) => getDictLabel(DICT_TYPE.ISONLINE_STATUS, value) || '-',
- },
- {
- title: t('table.status'),
- dataIndex: 'status',
- key: 'status',
- width: 100,
- align: 'center',
- render: (value: string) => {
- if (value === null || value === undefined) return '-';
- const statusText = getDictLabel(DICT_TYPE.CANBINET_STATUS, value) || '-';
- if (statusText === '-') return '-';
- // 判断是否为正常状态(根据字典值或文本判断)
- const isNormal = value === '1' || statusText.includes('正常');
- return (
- <div className="flex items-center justify-center">
- <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
- isNormal ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'
- }`}>
- {statusText}
- </span>
- </div>
- );
- },
- },
- {
- title: t('table.remark'),
- dataIndex: 'remark',
- key: 'remark',
- width: 150,
- align: 'center',
- render: (text: string) => {
- const remarkText = text || '-';
- const maxLength = 20;
- const shouldTruncate = remarkText.length > maxLength;
- const displayText = shouldTruncate ? remarkText.slice(0, maxLength) + '...' : remarkText;
-
- return (
- <Tooltip placement="topLeft" title={remarkText}>
- <span>{displayText}</span>
- </Tooltip>
- );
- },
- },
- {
- title: t('table.createTime'),
- dataIndex: 'createTime',
- key: 'createTime',
- width: 180,
- align: 'center',
- render: (text) => text ? dateFormatter(text) : '-',
- },
- {
- title: t('common.detail'),
- key: 'detail',
- width: 80,
- align: 'center',
- render: (_: any, record: LockCabinetVO) => (
- <div className="flex items-center justify-center">
- <PermissionWrapper permission="iscs:lock-cabinet:query">
- <Button
- variant="ghost"
- size="sm"
- onClick={() => lookDetail(record)}
- className="h-8 px-2"
- >
- <Eye className="w-4 h-4" />
- <span className="ml-1">{t('common.view')}</span>
- </Button>
- </PermissionWrapper>
- </div>
- ),
- },
- {
- title: t('table.operation'),
- key: 'action',
- width: 150,
- align: 'center',
- render: (_: any, record: LockCabinetVO) => (
- <div className="flex items-center gap-2 justify-center">
- <PermissionWrapper permission="iscs:lock-cabinet:update">
- <Button
- variant="ghost"
- size="sm"
- onClick={() => openForm('update', record.cabinetId || (record as any).id)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-1">{t('common.edit')}</span>
- </Button>
- </PermissionWrapper>
- <PermissionWrapper permission="iscs:lock-cabinet:delete">
- <Button
- variant="ghost"
- size="sm"
- onClick={() => handleDelete(record.cabinetId || (record as any).id)}
- className="h-8 px-2 transition-colors hover:underline"
- style={{ color: '#000000' }}
- onMouseEnter={(e) => {
- e.currentTarget.style.color = '#1677ff';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.color = '#000000';
- e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
- }}
- >
- <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
- <span className="ml-1">{t('common.delete')}</span>
- </Button>
- </PermissionWrapper>
- </div>
- ),
- },
- ]}
- pagination={false}
- scroll={{ x: 'max-content' }}
- />
- </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">
- {t('common.total')} <span className="text-blue-600 font-medium">{total}</span> {t('common.records')}
- </div>
- <div className="flex gap-2">
- <Button
- onClick={() => {
- const newParams = { ...queryParams, pageNo: queryParams.pageNo - 1 };
- setQueryParams(newParams);
- getList(newParams);
- }}
- disabled={queryParams.pageNo <= 1}
- >
- {t('common.prevPage')}
- </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={() => {
- const newParams = { ...queryParams, pageNo: queryParams.pageNo + 1 };
- setQueryParams(newParams);
- getList(newParams);
- }}
- disabled={queryParams.pageNo >= Math.ceil(total / queryParams.pageSize)}
- >
- {t('common.nextPage')}
- </Button>
- </div>
- </div>
- </div>
- )}
- {/* 表单弹窗 */}
- <LockCabinetForm ref={formRef} onSuccess={() => {
- // 编辑成功后刷新列表
- getList();
- }} />
- </div>
- );
- }
|