SystemLockCabinetManagement.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { Search, Plus, RefreshCw, Edit2, Trash2, Eye } from 'lucide-react';
  3. import { lockCabinetApi, LockCabinetVO, PageParam } from '../../api/lockCabinet';
  4. import { toast } from 'sonner';
  5. import { dateFormatter } from '../../utils/formatTime';
  6. import { DICT_TYPE, getDictLabel, getStrDictOptions } from '../../utils/dict';
  7. import { Modal, Table, Image, Input, Select, Space, Button as AntButton, Tooltip } from 'antd';
  8. import { ExclamationCircleOutlined, SearchOutlined, ReloadOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons';
  9. import { Button } from '../ui/button';
  10. import type { ColumnsType } from 'antd/es/table';
  11. import LockCabinetForm, { LockCabinetFormRef } from './LockCabinetForm';
  12. import { ImageWithFallback } from '../figma/ImageWithFallback';
  13. import { useNavigate } from 'react-router-dom';
  14. import PermissionWrapper from '../PermissionWrapper';
  15. import { useTranslation } from 'react-i18next';
  16. export default function SystemLockCabinetManagement() {
  17. const { t } = useTranslation();
  18. const navigate = useNavigate();
  19. const [loading, setLoading] = useState(true);
  20. const [list, setList] = useState<LockCabinetVO[]>([]);
  21. const [total, setTotal] = useState(0);
  22. const [selectedIds, setSelectedIds] = useState<number[]>([]);
  23. const [queryParams, setQueryParams] = useState<PageParam>({
  24. pageNo: 1,
  25. pageSize: 10,
  26. cabinetName: undefined,
  27. isOnline: undefined,
  28. });
  29. const formRef = useRef<LockCabinetFormRef>(null);
  30. const [isOnlineOptions] = useState(() => {
  31. try {
  32. const options = getStrDictOptions(DICT_TYPE.ISONLINE_STATUS);
  33. console.log('是否在线选项:', options);
  34. return options || [];
  35. } catch (error) {
  36. console.error('获取是否在线选项失败:', error);
  37. return [];
  38. }
  39. });
  40. // 获取机柜列表
  41. const getList = async (params?: PageParam) => {
  42. const currentParams = params || queryParams;
  43. // 过滤掉 undefined 的参数
  44. const cleanParams: PageParam = {};
  45. Object.keys(currentParams).forEach(key => {
  46. if (currentParams[key] !== undefined && currentParams[key] !== null && currentParams[key] !== '') {
  47. cleanParams[key] = currentParams[key];
  48. }
  49. });
  50. setLoading(true);
  51. try {
  52. console.log('SystemLockCabinetManagement: 开始获取机柜列表', cleanParams);
  53. const response = await lockCabinetApi.getIsLockCabinetPage(cleanParams);
  54. console.log('SystemLockCabinetManagement: API 响应', response);
  55. // 处理响应数据 - 兼容不同的响应格式
  56. let data;
  57. if (response && typeof response === 'object') {
  58. // 如果响应有 data 属性,使用 data;否则直接使用 response
  59. data = (response as any).data !== undefined ? (response as any).data : response;
  60. } else {
  61. data = response;
  62. }
  63. // 确保 data 是对象
  64. if (!data || typeof data !== 'object') {
  65. console.warn('SystemLockCabinetManagement: 响应数据格式异常', data);
  66. setList([]);
  67. setTotal(0);
  68. return;
  69. }
  70. setList(data.list || []);
  71. setTotal(data.total || 0);
  72. console.log('SystemLockCabinetManagement: 设置列表数据', data.list, '总数', data.total);
  73. // 调试:打印第一条数据的结构,检查 ID 字段名
  74. if (data.list && data.list.length > 0) {
  75. console.log('SystemLockCabinetManagement: 第一条数据示例', data.list[0]);
  76. }
  77. } catch (error: any) {
  78. console.error('SystemLockCabinetManagement: 获取机柜列表失败', error);
  79. console.error('错误详情:', {
  80. message: error?.message,
  81. response: error?.response,
  82. status: error?.response?.status,
  83. data: error?.response?.data,
  84. code: error?.response?.data?.code,
  85. config: error?.config,
  86. url: error?.config?.url
  87. });
  88. setList([]);
  89. setTotal(0);
  90. // 显示更详细的错误信息
  91. let errorMessage = '获取机柜列表失败';
  92. if (error?.response?.data?.message) {
  93. errorMessage = error.response.data.message;
  94. } else if (error?.message) {
  95. errorMessage = error.message;
  96. } else if (error?.response?.data) {
  97. // 如果响应数据存在但没有 message,尝试显示整个响应
  98. errorMessage = `请求失败: ${JSON.stringify(error.response.data)}`;
  99. }
  100. // 只在非静默模式下显示错误(避免重复显示)
  101. if (!error?.silent) {
  102. toast.error(errorMessage);
  103. }
  104. } finally {
  105. setLoading(false);
  106. }
  107. };
  108. // 组件挂载和分页参数变化时获取数据
  109. useEffect(() => {
  110. getList();
  111. // eslint-disable-next-line react-hooks/exhaustive-deps
  112. }, [queryParams.pageNo, queryParams.pageSize]);
  113. // 搜索
  114. const handleQuery = () => {
  115. const newParams = { ...queryParams, pageNo: 1 };
  116. setQueryParams(newParams);
  117. getList(newParams);
  118. };
  119. // 重置搜索
  120. const resetQuery = () => {
  121. const resetParams: PageParam = {
  122. pageNo: 1,
  123. pageSize: 10,
  124. cabinetName: undefined,
  125. isOnline: undefined,
  126. };
  127. setQueryParams(resetParams);
  128. getList(resetParams);
  129. };
  130. // 打开表单
  131. const openForm = (type: string, id?: number) => {
  132. if (formRef.current) {
  133. formRef.current.open(type, id);
  134. } else {
  135. toast.error(t('common.formNotInitialized'));
  136. }
  137. };
  138. // Ant Design Table 的行选择配置
  139. const rowSelection = {
  140. selectedRowKeys: selectedIds,
  141. onChange: (selectedRowKeys: React.Key[]) => {
  142. setSelectedIds(selectedRowKeys as number[]);
  143. },
  144. };
  145. // 删除机柜
  146. const handleDelete = async (id?: number) => {
  147. const ids = id ? [id] : selectedIds;
  148. if (ids.length === 0) {
  149. toast.error(t('common.pleaseSelect'));
  150. return;
  151. }
  152. Modal.confirm({
  153. title: t('common.confirmDelete'),
  154. icon: <ExclamationCircleOutlined />,
  155. content: (
  156. <div>
  157. <p>{t('common.confirmDeleteText')} {ids.length} {t('common.items')}?</p>
  158. <p style={{ color: '#ff4d4f', marginTop: '8px' }}>{t('common.confirmDeleteWarning')}</p>
  159. </div>
  160. ),
  161. okText: t('common.confirmDelete'),
  162. okType: 'danger',
  163. cancelText: t('common.cancel'),
  164. onOk: async () => {
  165. try {
  166. await lockCabinetApi.deleteIsLockCabinetByCabinetIds(ids.join(','));
  167. toast.success(t('common.deleteSuccess'));
  168. setSelectedIds([]);
  169. await getList();
  170. } catch (error: any) {
  171. toast.error(error.message || t('common.deleteFailed'));
  172. }
  173. },
  174. });
  175. };
  176. // 查看详情
  177. const lookDetail = (row: LockCabinetVO) => {
  178. // 使用 id 或 cabinetId,优先使用 id
  179. const cabinetId = (row as any).id || row.cabinetId;
  180. if (cabinetId) {
  181. // 记录来源菜单信息,用于返回时恢复菜单状态
  182. const sourceMenu = { menu: 'systemConfig', subMenu: 'cabinetManagement' };
  183. sessionStorage.setItem('cabinetDetailSource', JSON.stringify(sourceMenu));
  184. // 直接使用 navigate 跳转,Dashboard 会检测路径变化
  185. navigate(`/lock-cabinet/detail?cabinetId=${cabinetId}`);
  186. }
  187. };
  188. return (
  189. <div className="space-y-4">
  190. {/* 搜索栏 */}
  191. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-4 lg:p-5">
  192. <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
  193. {/* 搜索条件 */}
  194. <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
  195. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  196. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.cabinetName')}:</label>
  197. <Input
  198. placeholder={t('form.cabinetNamePlaceholder')}
  199. value={queryParams.cabinetName || ''}
  200. onChange={(e) => setQueryParams(prev => ({ ...prev, cabinetName: e.target.value }))}
  201. onPressEnter={handleQuery}
  202. className="min-w-[150px] max-w-[200px]"
  203. allowClear
  204. />
  205. </div>
  206. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  207. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('common.onlineStatus')}:</label>
  208. <Select
  209. placeholder={t('common.pleaseSelect')}
  210. value={queryParams.isOnline || undefined}
  211. onChange={(value) => setQueryParams(prev => ({ ...prev, isOnline: value }))}
  212. className="min-w-[150px] max-w-[200px]"
  213. allowClear
  214. >
  215. {isOnlineOptions && isOnlineOptions.length > 0 ? (
  216. isOnlineOptions.map(option => (
  217. <Select.Option key={option.value} value={String(option.value)}>
  218. {option.label}
  219. </Select.Option>
  220. ))
  221. ) : (
  222. <>
  223. <Select.Option value="0">{t('common.offline')}</Select.Option>
  224. <Select.Option value="1">{t('common.online')}</Select.Option>
  225. </>
  226. )}
  227. </Select>
  228. </div>
  229. </div>
  230. {/* 操作按钮组 */}
  231. <Space className="flex-shrink-0">
  232. <PermissionWrapper permission="iscs:lock-cabinet:query">
  233. <AntButton
  234. type="primary"
  235. icon={<SearchOutlined />}
  236. onClick={handleQuery}
  237. >
  238. {t('common.search')}
  239. </AntButton>
  240. </PermissionWrapper>
  241. <PermissionWrapper permission="iscs:lock-cabinet:query">
  242. <AntButton
  243. icon={<ReloadOutlined />}
  244. onClick={resetQuery}
  245. >
  246. {t('common.reset')}
  247. </AntButton>
  248. </PermissionWrapper>
  249. <PermissionWrapper permission="iscs:lock-cabinet:create">
  250. <AntButton
  251. type="primary"
  252. icon={<PlusOutlined />}
  253. onClick={() => openForm('create')}
  254. >
  255. {t('common.addNew')}
  256. </AntButton>
  257. </PermissionWrapper>
  258. {/* <PermissionWrapper permission="iscs:lock-cabinet:delete">
  259. <AntButton
  260. danger
  261. icon={<DeleteOutlined />}
  262. onClick={handleDelete}
  263. disabled={selectedIds.length === 0}
  264. >
  265. 批量删除
  266. </AntButton>
  267. </PermissionWrapper> */}
  268. </Space>
  269. </div>
  270. </div>
  271. {/* 表格 */}
  272. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  273. <Table
  274. rowKey={(record) => {
  275. // 兼容不同的 ID 字段名
  276. const id = record.cabinetId || (record as any).id || (record as any).cabinetId;
  277. return id || `row-${Math.random()}`;
  278. }}
  279. loading={loading}
  280. dataSource={list}
  281. // rowSelection={rowSelection}
  282. columns={[
  283. {
  284. title: t('table.cabinetName'),
  285. dataIndex: 'cabinetName',
  286. key: 'cabinetName',
  287. width: 150,
  288. align: 'center',
  289. },
  290. {
  291. title: t('table.hardwareSerial'),
  292. dataIndex: 'serialNumber',
  293. key: 'serialNumber',
  294. width: 150,
  295. align: 'center',
  296. render: (text) => text || '-',
  297. },
  298. {
  299. title: t('table.position'),
  300. dataIndex: 'workstationName',
  301. key: 'workstationName',
  302. width: 150,
  303. align: 'center',
  304. render: (text) => text || '-',
  305. },
  306. {
  307. title: t('table.image'),
  308. dataIndex: 'cabinetPicture',
  309. key: 'cabinetPicture',
  310. width: 100,
  311. align: 'center',
  312. render: (url: string) => {
  313. if (!url) return '-';
  314. return (
  315. <div className="flex items-center justify-center">
  316. <Image
  317. src={url}
  318. alt={t('table.image')}
  319. width={50}
  320. height={50}
  321. style={{ objectFit: 'cover', cursor: 'pointer' }}
  322. preview={{
  323. mask: t('common.view'),
  324. }}
  325. />
  326. </div>
  327. );
  328. },
  329. },
  330. {
  331. title: t('table.icon'),
  332. dataIndex: 'cabinetIcon',
  333. key: 'cabinetIcon',
  334. width: 100,
  335. align: 'center',
  336. render: (url: string) => {
  337. if (!url) return '-';
  338. return (
  339. <div className="flex items-center justify-center">
  340. <Image
  341. src={url}
  342. alt={t('table.icon')}
  343. width={50}
  344. height={50}
  345. style={{ objectFit: 'cover', cursor: 'pointer' }}
  346. preview={{
  347. mask: t('common.view'),
  348. }}
  349. />
  350. </div>
  351. );
  352. },
  353. },
  354. {
  355. title: t('common.onlineStatus'),
  356. dataIndex: 'isOnline',
  357. key: 'isOnline',
  358. width: 100,
  359. align: 'center',
  360. render: (value: string) => getDictLabel(DICT_TYPE.ISONLINE_STATUS, value) || '-',
  361. },
  362. {
  363. title: t('table.status'),
  364. dataIndex: 'status',
  365. key: 'status',
  366. width: 100,
  367. align: 'center',
  368. render: (value: string) => {
  369. if (value === null || value === undefined) return '-';
  370. const statusText = getDictLabel(DICT_TYPE.CANBINET_STATUS, value) || '-';
  371. if (statusText === '-') return '-';
  372. // 判断是否为正常状态(根据字典值或文本判断)
  373. const isNormal = value === '1' || statusText.includes('正常');
  374. return (
  375. <div className="flex items-center justify-center">
  376. <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
  377. isNormal ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'
  378. }`}>
  379. {statusText}
  380. </span>
  381. </div>
  382. );
  383. },
  384. },
  385. {
  386. title: t('table.remark'),
  387. dataIndex: 'remark',
  388. key: 'remark',
  389. width: 150,
  390. align: 'center',
  391. render: (text: string) => {
  392. const remarkText = text || '-';
  393. const maxLength = 20;
  394. const shouldTruncate = remarkText.length > maxLength;
  395. const displayText = shouldTruncate ? remarkText.slice(0, maxLength) + '...' : remarkText;
  396. return (
  397. <Tooltip placement="topLeft" title={remarkText}>
  398. <span>{displayText}</span>
  399. </Tooltip>
  400. );
  401. },
  402. },
  403. {
  404. title: t('table.createTime'),
  405. dataIndex: 'createTime',
  406. key: 'createTime',
  407. width: 180,
  408. align: 'center',
  409. render: (text) => text ? dateFormatter(text) : '-',
  410. },
  411. {
  412. title: t('common.detail'),
  413. key: 'detail',
  414. width: 80,
  415. align: 'center',
  416. render: (_: any, record: LockCabinetVO) => (
  417. <div className="flex items-center justify-center">
  418. <PermissionWrapper permission="iscs:lock-cabinet:query">
  419. <Button
  420. variant="ghost"
  421. size="sm"
  422. onClick={() => lookDetail(record)}
  423. className="h-8 px-2"
  424. >
  425. <Eye className="w-4 h-4" />
  426. <span className="ml-1">{t('common.view')}</span>
  427. </Button>
  428. </PermissionWrapper>
  429. </div>
  430. ),
  431. },
  432. {
  433. title: t('table.operation'),
  434. key: 'action',
  435. width: 150,
  436. align: 'center',
  437. render: (_: any, record: LockCabinetVO) => (
  438. <div className="flex items-center gap-2 justify-center">
  439. <PermissionWrapper permission="iscs:lock-cabinet:update">
  440. <Button
  441. variant="ghost"
  442. size="sm"
  443. onClick={() => openForm('update', record.cabinetId || (record as any).id)}
  444. className="h-8 px-2 transition-colors hover:underline"
  445. style={{ color: '#000000' }}
  446. onMouseEnter={(e) => {
  447. e.currentTarget.style.color = '#1677ff';
  448. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  449. }}
  450. onMouseLeave={(e) => {
  451. e.currentTarget.style.color = '#000000';
  452. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  453. }}
  454. >
  455. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  456. <span className="ml-1">{t('common.edit')}</span>
  457. </Button>
  458. </PermissionWrapper>
  459. <PermissionWrapper permission="iscs:lock-cabinet:delete">
  460. <Button
  461. variant="ghost"
  462. size="sm"
  463. onClick={() => handleDelete(record.cabinetId || (record as any).id)}
  464. className="h-8 px-2 transition-colors hover:underline"
  465. style={{ color: '#000000' }}
  466. onMouseEnter={(e) => {
  467. e.currentTarget.style.color = '#1677ff';
  468. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  469. }}
  470. onMouseLeave={(e) => {
  471. e.currentTarget.style.color = '#000000';
  472. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  473. }}
  474. >
  475. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  476. <span className="ml-1">{t('common.delete')}</span>
  477. </Button>
  478. </PermissionWrapper>
  479. </div>
  480. ),
  481. },
  482. ]}
  483. pagination={false}
  484. scroll={{ x: 'max-content' }}
  485. />
  486. </div>
  487. {/* 分页 */}
  488. {!loading && list.length > 0 && (
  489. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  490. <div className="flex items-center justify-between">
  491. <div className="text-sm text-gray-600">
  492. {t('common.total')} <span className="text-blue-600 font-medium">{total}</span> {t('common.records')}
  493. </div>
  494. <div className="flex gap-2">
  495. <Button
  496. onClick={() => {
  497. const newParams = { ...queryParams, pageNo: queryParams.pageNo - 1 };
  498. setQueryParams(newParams);
  499. getList(newParams);
  500. }}
  501. disabled={queryParams.pageNo <= 1}
  502. >
  503. {t('common.prevPage')}
  504. </Button>
  505. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  506. {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize) || 1}
  507. </span>
  508. <Button
  509. onClick={() => {
  510. const newParams = { ...queryParams, pageNo: queryParams.pageNo + 1 };
  511. setQueryParams(newParams);
  512. getList(newParams);
  513. }}
  514. disabled={queryParams.pageNo >= Math.ceil(total / queryParams.pageSize)}
  515. >
  516. {t('common.nextPage')}
  517. </Button>
  518. </div>
  519. </div>
  520. </div>
  521. )}
  522. {/* 表单弹窗 */}
  523. <LockCabinetForm ref={formRef} onSuccess={() => {
  524. // 编辑成功后刷新列表
  525. getList();
  526. }} />
  527. </div>
  528. );
  529. }