PadLockManagement.tsx 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187
  1. import React, { useState, useEffect, useRef, useMemo } from 'react';
  2. import { Search, Plus, RefreshCw, Edit2, Trash2, Settings, ArrowUpDown } from 'lucide-react';
  3. import { padLockApi, PadLockVO, PageParam } from '../api/PadLock';
  4. import { padLockTypeApi, PadLockTypeVO, PageParam as PadLockTypePageParam } from '../api/PadLockType';
  5. import { toast } from 'sonner';
  6. import { Modal, Button, Input, Space, Table, TreeSelect, Form, Image, Switch, Radio, Tooltip } from 'antd';
  7. import { ExclamationCircleOutlined } from '@ant-design/icons';
  8. import type { ColumnsType } from 'antd/es/table';
  9. import { handleTree } from '../utils/tree';
  10. import { DICT_TYPE, getStrDictOptions } from '../utils/dict';
  11. import { dateFormatter } from '../utils/formatTime';
  12. import UploadImg from './lockCabinet/UploadImg';
  13. import { Button as UIButton } from './ui/button';
  14. import { useTranslation } from 'react-i18next';
  15. import PermissionWrapper from './PermissionWrapper';
  16. import './IsolationWorkListTable.css';
  17. interface PadLockManagementProps {
  18. subMenu?: string;
  19. }
  20. export default function PadLockManagement({ subMenu }: PadLockManagementProps) {
  21. const { t } = useTranslation();
  22. const [viewMode, setViewMode] = useState<'list' | 'type'>('list'); // 'list' 挂锁列表, 'type' 挂锁类型
  23. const [loading, setLoading] = useState(true);
  24. const [list, setList] = useState<PadLockVO[]>([]);
  25. const [total, setTotal] = useState(0);
  26. const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  27. const [queryParams, setQueryParams] = useState<PageParam>({
  28. pageNo: 1,
  29. pageSize: 10,
  30. lockCode: undefined,
  31. lockName: undefined,
  32. enableFlag: undefined,
  33. });
  34. const [showPadLockForm, setShowPadLockForm] = useState(false);
  35. const [editingPadLock, setEditingPadLock] = useState<PadLockVO | null>(null);
  36. // 挂锁类型相关状态
  37. const [typeLoading, setTypeLoading] = useState(true);
  38. const [typeList, setTypeList] = useState<PadLockTypeVO[]>([]);
  39. const [typeTreeList, setTypeTreeList] = useState<PadLockTypeVO[]>([]);
  40. const [typeTotal, setTypeTotal] = useState(0);
  41. const [typeQueryParams, setTypeQueryParams] = useState<PadLockTypePageParam>({
  42. pageNo: 1,
  43. pageSize: -1, // 使用 -1 获取全部数据用于树形结构
  44. lockTypeCode: undefined,
  45. lockTypeName: undefined,
  46. enableFlag: undefined,
  47. });
  48. const [showTypeForm, setShowTypeForm] = useState(false);
  49. const [editingType, setEditingType] = useState<PadLockTypeVO | null>(null);
  50. const [parentTypeId, setParentTypeId] = useState<number | undefined>(undefined);
  51. const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([]);
  52. const [isExpandAll, setIsExpandAll] = useState(true);
  53. // 获取挂锁列表
  54. const getList = async (params?: PageParam) => {
  55. const currentParams = params || queryParams;
  56. setLoading(true);
  57. try {
  58. const response = await padLockApi.listPadLock(currentParams);
  59. setList(response.list || []);
  60. setTotal(response.total || 0);
  61. } catch (error: any) {
  62. toast.error(error.message || t('padLockManagement.getPadLockListFailed'));
  63. } finally {
  64. setLoading(false);
  65. }
  66. };
  67. // 获取挂锁类型列表
  68. const getTypeList = async (params?: PadLockTypePageParam) => {
  69. const currentParams = params || typeQueryParams;
  70. setTypeLoading(true);
  71. try {
  72. const response = await padLockTypeApi.listPadLockType(currentParams);
  73. const flatList = response.list || [];
  74. setTypeList(flatList);
  75. setTypeTotal(response.total || 0);
  76. // 转换为树形结构
  77. const treeData = handleTree<PadLockTypeVO>(
  78. flatList.map(item => ({ ...item, id: item.lockTypeId || item.id })),
  79. 'id',
  80. 'parentTypeId',
  81. 'children'
  82. );
  83. setTypeTreeList(treeData);
  84. // 默认展开所有节点
  85. if (isExpandAll && treeData.length > 0) {
  86. const getAllIds = (nodes: PadLockTypeVO[]): React.Key[] => {
  87. const ids: React.Key[] = [];
  88. nodes.forEach(node => {
  89. const id = node.lockTypeId || node.id;
  90. if (id) {
  91. ids.push(id);
  92. if (node.children && node.children.length > 0) {
  93. ids.push(...getAllIds(node.children));
  94. }
  95. }
  96. });
  97. return ids;
  98. };
  99. setExpandedRowKeys(getAllIds(treeData));
  100. }
  101. } catch (error: any) {
  102. toast.error(error.message || t('padLockManagement.getPadLockTypeListFailed'));
  103. } finally {
  104. setTypeLoading(false);
  105. }
  106. };
  107. useEffect(() => {
  108. if (viewMode === 'list') {
  109. getList();
  110. } else {
  111. getTypeList();
  112. }
  113. }, [viewMode, queryParams.pageNo, queryParams.pageSize, typeQueryParams.pageNo, typeQueryParams.pageSize]);
  114. // 搜索挂锁
  115. const handleQuery = () => {
  116. const newParams = { ...queryParams, pageNo: 1 };
  117. setQueryParams(newParams);
  118. getList(newParams);
  119. };
  120. // 重置挂锁搜索
  121. const resetQuery = () => {
  122. const resetParams: PageParam = {
  123. pageNo: 1,
  124. pageSize: 10,
  125. lockCode: undefined,
  126. lockName: undefined,
  127. enableFlag: undefined,
  128. };
  129. setQueryParams(resetParams);
  130. getList(resetParams);
  131. };
  132. // 搜索挂锁类型
  133. const handleTypeQuery = () => {
  134. const newParams = { ...typeQueryParams, pageNo: 1 };
  135. setTypeQueryParams(newParams);
  136. getTypeList(newParams);
  137. };
  138. // 重置挂锁类型搜索
  139. const resetTypeQuery = () => {
  140. const resetParams: PadLockTypePageParam = {
  141. pageNo: 1,
  142. pageSize: 10,
  143. lockTypeCode: undefined,
  144. lockTypeName: undefined,
  145. enableFlag: undefined,
  146. };
  147. setTypeQueryParams(resetParams);
  148. getTypeList(resetParams);
  149. };
  150. // 删除挂锁
  151. const handleDelete = async (id?: number) => {
  152. const ids = id ? [id] : selectedRowKeys.map(key => Number(key));
  153. if (ids.length === 0) {
  154. toast.error(t('common.pleaseSelect'));
  155. return;
  156. }
  157. Modal.confirm({
  158. title: t('common.confirmDelete'),
  159. icon: <ExclamationCircleOutlined />,
  160. content: (
  161. <div>
  162. <p>{t('common.confirmDeleteText')} {ids.length} {t('common.items')}?</p>
  163. <p style={{ color: '#ff4d4f', marginTop: '8px' }}>{t('common.confirmDeleteWarning')}</p>
  164. </div>
  165. ),
  166. okText: t('common.confirmDelete'),
  167. okType: 'danger',
  168. cancelText: t('common.cancel'),
  169. onOk: async () => {
  170. try {
  171. await padLockApi.delPadLock(ids);
  172. toast.success(t('common.deleteSuccess'));
  173. setSelectedRowKeys([]);
  174. await getList();
  175. } catch (error: any) {
  176. toast.error(error.message || t('common.deleteFailed'));
  177. }
  178. },
  179. });
  180. };
  181. // 打开挂锁表单
  182. const openPadLockForm = (type: 'create' | 'update', id?: number) => {
  183. if (type === 'create') {
  184. setEditingPadLock(null);
  185. setShowPadLockForm(true);
  186. } else if (type === 'update' && id) {
  187. padLockApi.getPadLockInfo(id).then((data) => {
  188. setEditingPadLock(data);
  189. setShowPadLockForm(true);
  190. }).catch((error: any) => {
  191. toast.error(error.message || t('padLockManagement.getPadLockInfoFailed'));
  192. });
  193. }
  194. };
  195. // 保存挂锁
  196. const savePadLock = async (formData: PadLockVO) => {
  197. try {
  198. if (editingPadLock?.lockId || editingPadLock?.id) {
  199. // 编辑模式下,合并原始数据和表单数据,确保所有必要字段都被传递
  200. const updateData: PadLockVO = {
  201. // 保留原始数据中的字段(这些字段用户不能编辑但需要传递)
  202. ...editingPadLock,
  203. // 用表单数据覆盖可编辑字段
  204. ...formData,
  205. // 确保 id 和 lockId 都存在
  206. id: editingPadLock.id || editingPadLock.lockId,
  207. lockId: editingPadLock.lockId || editingPadLock.id,
  208. };
  209. console.log('提交的挂锁数据:', updateData); // 调试用
  210. await padLockApi.updatePadLock(updateData);
  211. toast.success(t('common.updateSuccess'));
  212. } else {
  213. await padLockApi.addPadLock(formData);
  214. toast.success(t('common.addSuccess'));
  215. }
  216. setShowPadLockForm(false);
  217. setEditingPadLock(null);
  218. await getList();
  219. } catch (error: any) {
  220. toast.error(error.message || t('common.saveFailed'));
  221. }
  222. };
  223. // 删除挂锁类型
  224. const handleDeleteType = async (id: number, name?: string) => {
  225. Modal.confirm({
  226. title: t('common.confirmDelete'),
  227. icon: <ExclamationCircleOutlined />,
  228. content: (
  229. <div>
  230. <p>{t('common.confirmDeleteText')} <strong>"{name || t('common.type')}"</strong>?</p>
  231. <p style={{ color: '#ff4d4f', marginTop: '8px' }}>{t('common.confirmDeleteWarning')}</p>
  232. </div>
  233. ),
  234. okText: t('common.confirmDeleteButton'),
  235. okType: 'danger',
  236. cancelText: t('common.cancel'),
  237. onOk: async () => {
  238. try {
  239. await padLockTypeApi.delPadLockType(id);
  240. toast.success(t('common.deleteSuccess'));
  241. await getTypeList();
  242. } catch (error: any) {
  243. toast.error(error.message || t('common.deleteFailed'));
  244. }
  245. },
  246. });
  247. };
  248. // 打开挂锁类型表单
  249. const openTypeForm = (type?: 'create' | 'update', id?: number, parentId?: number) => {
  250. if (type === 'create') {
  251. setEditingType(null);
  252. setParentTypeId(parentId);
  253. setShowTypeForm(true);
  254. } else if (type === 'update' && id) {
  255. padLockTypeApi.getPadLockTypeInfo(id).then((data) => {
  256. setEditingType(data);
  257. setParentTypeId(undefined);
  258. setShowTypeForm(true);
  259. }).catch((error: any) => {
  260. toast.error(error.message || t('padLockManagement.getPadLockTypeInfoFailed'));
  261. });
  262. }
  263. };
  264. // 展开/折叠所有
  265. const toggleExpandAll = () => {
  266. if (isExpandAll) {
  267. setExpandedRowKeys([]);
  268. } else {
  269. const getAllIds = (nodes: PadLockTypeVO[]): React.Key[] => {
  270. const ids: React.Key[] = [];
  271. nodes.forEach(node => {
  272. const id = node.lockTypeId || node.id;
  273. if (id) {
  274. ids.push(id);
  275. if (node.children && node.children.length > 0) {
  276. ids.push(...getAllIds(node.children));
  277. }
  278. }
  279. });
  280. return ids;
  281. };
  282. setExpandedRowKeys(getAllIds(typeTreeList));
  283. }
  284. setIsExpandAll(!isExpandAll);
  285. };
  286. // 保存挂锁类型
  287. const saveType = async (formData: PadLockTypeVO) => {
  288. try {
  289. if (editingType?.lockTypeId || editingType?.id) {
  290. // 编辑模式下,合并原始数据和表单数据,确保所有必要字段都被传递
  291. const updateData: PadLockTypeVO = {
  292. // 保留原始数据中的字段(这些字段用户不能编辑但需要传递)
  293. ...editingType,
  294. // 用表单数据覆盖可编辑字段
  295. ...formData,
  296. // 确保 id 和 lockTypeId 都存在
  297. id: editingType.id || editingType.lockTypeId,
  298. lockTypeId: editingType.lockTypeId || editingType.id,
  299. };
  300. console.log('提交的挂锁类型数据:', updateData); // 调试用
  301. await padLockTypeApi.updatePadLockType(updateData);
  302. toast.success(t('common.updateSuccess'));
  303. } else {
  304. await padLockTypeApi.addPadLockType(formData);
  305. toast.success(t('common.addSuccess'));
  306. }
  307. setShowTypeForm(false);
  308. setEditingType(null);
  309. setParentTypeId(undefined);
  310. await getTypeList();
  311. } catch (error: any) {
  312. toast.error(error.message || t('common.saveFailed'));
  313. }
  314. };
  315. // 挂锁列表表格列
  316. const padLockColumns: ColumnsType<PadLockVO> = [
  317. {
  318. title: t('table.padLockName'),
  319. dataIndex: 'lockName',
  320. width: 200,
  321. },
  322. {
  323. title: t('table.padLockNfc'),
  324. dataIndex: 'lockNfc',
  325. width: 180,
  326. ellipsis: true,
  327. },
  328. {
  329. title: t('table.padLockType'),
  330. dataIndex: 'lockTypeName',
  331. width: 150,
  332. render: (text: string) => text || '-',
  333. },
  334. {
  335. title: t('table.padLockModel'),
  336. dataIndex: 'lockSpec',
  337. width: 150,
  338. render: (text: string) => text || '-',
  339. },
  340. {
  341. title: t('table.status'),
  342. dataIndex: 'exStatus',
  343. width: 100,
  344. align: 'center',
  345. render: (status: string) => {
  346. if (status === null || status === undefined) return '-';
  347. const isEnabled = status === '1';
  348. return (
  349. <span className={`inline-flex px-3 py-1 rounded-lg text-xs ${
  350. isEnabled ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'
  351. }`}>
  352. {isEnabled ? t('common.enabled') : t('common.disabled')}
  353. </span>
  354. );
  355. },
  356. },
  357. {
  358. title: t('table.remark'),
  359. dataIndex: 'exRemark',
  360. render: (text: string) => {
  361. const remarkText = text || '-';
  362. const maxLength = 20;
  363. const shouldTruncate = remarkText.length > maxLength;
  364. const displayText = shouldTruncate ? remarkText.slice(0, maxLength) + '...' : remarkText;
  365. return (
  366. <Tooltip placement="topLeft" title={remarkText}>
  367. <span>{displayText}</span>
  368. </Tooltip>
  369. );
  370. },
  371. },
  372. {
  373. title: t('table.createTime'),
  374. dataIndex: 'createTime',
  375. width: 180,
  376. render: (text: string | Date) => text ? dateFormatter(text) : '-',
  377. },
  378. {
  379. title: t('table.operation'),
  380. width: 150,
  381. align: 'center',
  382. fixed: 'right',
  383. render: (_: any, record: PadLockVO) => (
  384. <div className="flex items-center gap-2 justify-center">
  385. <PermissionWrapper permission="iscs:lock:update">
  386. <UIButton
  387. variant="ghost"
  388. size="sm"
  389. onClick={() => openPadLockForm('update', record.lockId || record.id)}
  390. className="h-8 px-2 transition-colors hover:underline"
  391. style={{ color: '#000000' }}
  392. onMouseEnter={(e) => {
  393. e.currentTarget.style.color = '#1677ff';
  394. e.currentTarget.style.textDecoration = 'underline';
  395. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  396. }}
  397. onMouseLeave={(e) => {
  398. e.currentTarget.style.color = '#000000';
  399. e.currentTarget.style.textDecoration = 'none';
  400. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  401. }}
  402. >
  403. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  404. <span className="ml-1">{t('common.edit')}</span>
  405. </UIButton>
  406. </PermissionWrapper>
  407. <PermissionWrapper permission="iscs:lock:delete">
  408. <UIButton
  409. variant="ghost"
  410. size="sm"
  411. onClick={() => handleDelete(record.lockId || record.id)}
  412. className="h-8 px-2 transition-colors hover:underline"
  413. style={{ color: '#000000' }}
  414. onMouseEnter={(e) => {
  415. e.currentTarget.style.color = '#1677ff';
  416. e.currentTarget.style.textDecoration = 'underline';
  417. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  418. }}
  419. onMouseLeave={(e) => {
  420. e.currentTarget.style.color = '#000000';
  421. e.currentTarget.style.textDecoration = 'none';
  422. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  423. }}
  424. >
  425. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  426. <span className="ml-1">{t('common.delete')}</span>
  427. </UIButton>
  428. </PermissionWrapper>
  429. </div>
  430. ),
  431. },
  432. ];
  433. // 挂锁类型表格列(树形表格)
  434. const padLockTypeColumns: ColumnsType<PadLockTypeVO> = [
  435. {
  436. title: t('table.padLockTypeName'),
  437. dataIndex: 'lockTypeName',
  438. width: 200,
  439. align: 'center',
  440. },
  441. {
  442. title: t('table.padLockTypeIcon'),
  443. dataIndex: 'lockTypeIcon',
  444. width: 120,
  445. align: 'center',
  446. render: (url: string) => {
  447. if (!url) return '-';
  448. return (
  449. <Image
  450. src={url}
  451. alt={t('padLockManagement.icon')}
  452. width={50}
  453. height={50}
  454. style={{ objectFit: 'cover' }}
  455. preview={{
  456. mask: t('padLockManagement.view'),
  457. }}
  458. />
  459. );
  460. },
  461. },
  462. {
  463. title: t('table.padLockTypeImage'),
  464. dataIndex: 'lockTypeImg',
  465. width: 120,
  466. align: 'center',
  467. render: (url: string) => {
  468. if (!url) return '-';
  469. return (
  470. <Image
  471. src={url}
  472. alt={t('padLockManagement.image')}
  473. width={50}
  474. height={50}
  475. style={{ objectFit: 'cover' }}
  476. preview={{
  477. mask: t('padLockManagement.view'),
  478. }}
  479. />
  480. );
  481. },
  482. },
  483. {
  484. title: t('table.padLockModel'),
  485. dataIndex: 'lockTypeSpec',
  486. width: 150,
  487. align: 'center',
  488. render: (spec: string) => spec || '-',
  489. },
  490. {
  491. title: t('table.operation'),
  492. width: 200,
  493. align: 'center',
  494. fixed: 'right',
  495. render: (_: any, record: PadLockTypeVO) => (
  496. <div className="flex items-center gap-2 justify-center">
  497. <PermissionWrapper permission="iscs:lock:update">
  498. <UIButton
  499. variant="ghost"
  500. size="sm"
  501. onClick={() => openTypeForm('update', record.lockTypeId || record.id)}
  502. className="h-8 px-2 transition-colors hover:underline"
  503. style={{ color: '#000000' }}
  504. onMouseEnter={(e) => {
  505. e.currentTarget.style.color = '#1677ff';
  506. e.currentTarget.style.textDecoration = 'underline';
  507. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  508. }}
  509. onMouseLeave={(e) => {
  510. e.currentTarget.style.color = '#000000';
  511. e.currentTarget.style.textDecoration = 'none';
  512. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  513. }}
  514. >
  515. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  516. <span className="ml-1">{t('common.edit')}</span>
  517. </UIButton>
  518. </PermissionWrapper>
  519. <UIButton
  520. variant="ghost"
  521. size="sm"
  522. onClick={() => openTypeForm('create', undefined, record.lockTypeId || record.id)}
  523. className="h-8 px-2 transition-colors hover:underline"
  524. style={{ color: '#000000' }}
  525. onMouseEnter={(e) => {
  526. e.currentTarget.style.color = '#1677ff';
  527. e.currentTarget.style.textDecoration = 'underline';
  528. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  529. }}
  530. onMouseLeave={(e) => {
  531. e.currentTarget.style.color = '#000000';
  532. e.currentTarget.style.textDecoration = 'none';
  533. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  534. }}
  535. >
  536. <Plus className="w-4 h-4" style={{ color: '#000000' }} />
  537. <span className="ml-1">{t('common.addNew')}</span>
  538. </UIButton>
  539. <PermissionWrapper permission="iscs:lock:delete">
  540. <UIButton
  541. variant="ghost"
  542. size="sm"
  543. onClick={() => handleDeleteType(record.lockTypeId || record.id!, record.lockTypeName)}
  544. className="h-8 px-2 transition-colors hover:underline"
  545. style={{ color: '#000000' }}
  546. onMouseEnter={(e) => {
  547. e.currentTarget.style.color = '#1677ff';
  548. e.currentTarget.style.textDecoration = 'underline';
  549. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  550. }}
  551. onMouseLeave={(e) => {
  552. e.currentTarget.style.color = '#000000';
  553. e.currentTarget.style.textDecoration = 'none';
  554. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  555. }}
  556. >
  557. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  558. <span className="ml-1">{t('common.delete')}</span>
  559. </UIButton>
  560. </PermissionWrapper>
  561. </div>
  562. ),
  563. },
  564. ];
  565. return (
  566. <div className="space-y-4">
  567. {viewMode === 'list' ? (
  568. <>
  569. {/* 挂锁列表 - 搜索栏和表格放在一起 */}
  570. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm overflow-hidden">
  571. {/* 搜索栏 */}
  572. <div className="p-4 lg:p-5 border-b border-gray-200">
  573. <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
  574. {/* 搜索条件:按原宽度 200px,不超出,可缩小 */}
  575. <div className="flex items-center gap-2 lg:gap-3 flex-wrap min-w-0">
  576. <div className="flex items-center gap-2 lg:gap-3 w-[200px] min-w-[100px] flex-shrink">
  577. <label className="text-sm font-medium text-gray-700 whitespace-nowrap flex-shrink-0">{t('form.padLockName')}:</label>
  578. <Input
  579. value={queryParams.lockName || ''}
  580. onChange={(e) => setQueryParams({ ...queryParams, lockName: e.target.value })}
  581. onPressEnter={handleQuery}
  582. placeholder={t('form.padLockNamePlaceholder')}
  583. className="min-w-0 w-full"
  584. allowClear
  585. />
  586. </div>
  587. <Space className="flex-shrink-0" size="small">
  588. <PermissionWrapper permission="iscs:lock:query">
  589. <Button
  590. type="primary"
  591. icon={<Search className="w-4 h-4" />}
  592. onClick={handleQuery}
  593. >
  594. {t('common.search')}
  595. </Button>
  596. </PermissionWrapper>
  597. <PermissionWrapper permission="iscs:lock:query">
  598. <Button
  599. icon={<RefreshCw className="w-4 h-4" />}
  600. onClick={resetQuery}
  601. >
  602. {t('common.reset')}
  603. </Button>
  604. </PermissionWrapper>
  605. </Space>
  606. </div>
  607. {/* 新增 / 批量删除 / 设置类型 等按钮组保持在最右侧 */}
  608. <Space className="flex-shrink-0" size="small">
  609. <PermissionWrapper permission="iscs:lock:create">
  610. <Button
  611. type="primary"
  612. icon={<Plus className="w-4 h-4" />}
  613. onClick={() => openPadLockForm('create')}
  614. >
  615. {t('common.addNew')}
  616. </Button>
  617. </PermissionWrapper>
  618. <PermissionWrapper permission="iscs:lock:delete">
  619. <Button
  620. danger
  621. icon={<Trash2 className="w-4 h-4" />}
  622. onClick={() => handleDelete()}
  623. disabled={selectedRowKeys.length === 0}
  624. >
  625. {t('common.batchDelete')}
  626. </Button>
  627. </PermissionWrapper>
  628. <Button
  629. icon={<Settings className="w-4 h-4" />}
  630. onClick={() => setViewMode('type')}
  631. >
  632. {t('common.setPadLockType')}
  633. </Button>
  634. </Space>
  635. </div>
  636. </div>
  637. {/* 表格 */}
  638. <div className="isolation-work-list-table min-w-0">
  639. <Table
  640. columns={padLockColumns}
  641. dataSource={list}
  642. rowKey={(record) => record.lockId || record.id || ''}
  643. loading={loading}
  644. pagination={false}
  645. scroll={{ x: 'max-content' }}
  646. rowSelection={{
  647. selectedRowKeys,
  648. onChange: setSelectedRowKeys,
  649. }}
  650. rowClassName={(_, index) =>
  651. index !== undefined && index % 2 === 1 ? 'isolation-work-list-row-alt' : ''
  652. }
  653. />
  654. </div>
  655. </div>
  656. {/* 分页 */}
  657. {!loading && list.length > 0 && (
  658. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  659. <div className="flex items-center justify-between">
  660. <div className="text-sm text-gray-600">
  661. {t('common.total')} <span className="text-blue-600 font-medium">{total}</span> {t('common.records')}
  662. </div>
  663. <div className="flex gap-2">
  664. <Button
  665. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
  666. disabled={queryParams.pageNo! <= 1}
  667. >
  668. {t('common.prevPage')}
  669. </Button>
  670. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  671. {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
  672. </span>
  673. <Button
  674. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
  675. disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
  676. >
  677. {t('common.nextPage')}
  678. </Button>
  679. </div>
  680. </div>
  681. </div>
  682. )}
  683. {/* 挂锁表单弹窗 */}
  684. <PadLockFormModal
  685. visible={showPadLockForm}
  686. editingPadLock={editingPadLock}
  687. onCancel={() => {
  688. setShowPadLockForm(false);
  689. setEditingPadLock(null);
  690. }}
  691. onSave={savePadLock}
  692. />
  693. </>
  694. ) : (
  695. <>
  696. {/* 挂锁类型 - 搜索栏和表格放在一起 */}
  697. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm overflow-hidden">
  698. {/* 搜索栏 */}
  699. <div className="p-5 border-b border-gray-200">
  700. <div className="flex items-center justify-between gap-4 flex-wrap">
  701. {/* 搜索条件:按原宽度 200px,不超出,可缩小 */}
  702. <div className="flex items-center gap-2 lg:gap-3 flex-wrap min-w-0">
  703. <div className="flex items-center gap-2 lg:gap-3 w-[200px] min-w-[100px] flex-shrink">
  704. <label className="text-sm font-medium text-gray-700 whitespace-nowrap flex-shrink-0">{t('form.padLockTypeName')}:</label>
  705. <Input
  706. value={typeQueryParams.lockTypeName || ''}
  707. onChange={(e) => setTypeQueryParams({ ...typeQueryParams, lockTypeName: e.target.value })}
  708. onPressEnter={handleTypeQuery}
  709. placeholder={t('form.padLockTypeNamePlaceholder')}
  710. className="min-w-0 w-full"
  711. allowClear
  712. />
  713. </div>
  714. <Space className="flex-shrink-0" size="small">
  715. <PermissionWrapper permission="iscs:lock:query">
  716. <Button
  717. type="primary"
  718. icon={<Search className="w-4 h-4" />}
  719. onClick={handleTypeQuery}
  720. >
  721. {t('common.search')}
  722. </Button>
  723. </PermissionWrapper>
  724. <PermissionWrapper permission="iscs:lock:query">
  725. <Button
  726. icon={<RefreshCw className="w-4 h-4" />}
  727. onClick={resetTypeQuery}
  728. >
  729. {t('common.reset')}
  730. </Button>
  731. </PermissionWrapper>
  732. </Space>
  733. </div>
  734. {/* 新增 / 展开收起 / 返回列表 等按钮组保持在最右侧 */}
  735. <Space className="flex-shrink-0" size="small">
  736. <PermissionWrapper permission="iscs:lock:create">
  737. <Button
  738. type="primary"
  739. icon={<Plus className="w-4 h-4" />}
  740. onClick={() => openTypeForm('create')}
  741. >
  742. {t('common.addNew')}
  743. </Button>
  744. </PermissionWrapper>
  745. <Button
  746. icon={<ArrowUpDown className="w-4 h-4" />}
  747. onClick={toggleExpandAll}
  748. >
  749. {t('padLockManagement.expandCollapse')}
  750. </Button>
  751. <Button
  752. onClick={() => setViewMode('list')}
  753. >
  754. {t('padLockManagement.backToPadLockList')}
  755. </Button>
  756. </Space>
  757. </div>
  758. </div>
  759. {/* 表格 - 树形表格 */}
  760. <div className="isolation-work-list-table">
  761. <Table
  762. columns={padLockTypeColumns}
  763. dataSource={typeTreeList}
  764. rowKey={(record) => record.lockTypeId || record.id || ''}
  765. loading={typeLoading}
  766. pagination={false}
  767. scroll={{ x: 'max-content' }}
  768. expandable={{
  769. expandedRowKeys,
  770. onExpandedRowsChange: setExpandedRowKeys,
  771. defaultExpandAllRows: isExpandAll,
  772. }}
  773. rowClassName={(_, index) =>
  774. index !== undefined && index % 2 === 1 ? 'isolation-work-list-row-alt' : ''
  775. }
  776. />
  777. </div>
  778. </div>
  779. {/* 挂锁类型表单弹窗 */}
  780. {showTypeForm && (
  781. <TypeFormModal
  782. visible={showTypeForm}
  783. editingType={editingType}
  784. parentTypeId={parentTypeId}
  785. typeList={typeList}
  786. onCancel={() => {
  787. setShowTypeForm(false);
  788. setEditingType(null);
  789. setParentTypeId(undefined);
  790. }}
  791. onSave={saveType}
  792. />
  793. )}
  794. </>
  795. )}
  796. </div>
  797. );
  798. }
  799. // 挂锁类型表单弹窗组件
  800. interface TypeFormModalProps {
  801. visible: boolean;
  802. editingType: PadLockTypeVO | null;
  803. parentTypeId?: number;
  804. typeList: PadLockTypeVO[];
  805. onCancel: () => void;
  806. onSave: (data: PadLockTypeVO) => void;
  807. }
  808. function TypeFormModal({ visible, editingType, parentTypeId, typeList, onCancel, onSave }: TypeFormModalProps) {
  809. const { t } = useTranslation();
  810. const [form] = Form.useForm();
  811. const [formLoading, setFormLoading] = useState(false);
  812. const [typeTreeOptions, setTypeTreeOptions] = useState<any[]>([]);
  813. // 构建树形选择器数据
  814. useEffect(() => {
  815. if (typeList.length > 0) {
  816. const treeData = handleTree<PadLockTypeVO>(
  817. typeList.map(item => ({ ...item, id: item.lockTypeId || item.id })),
  818. 'id',
  819. 'parentTypeId',
  820. 'children'
  821. );
  822. const buildTreeSelectData = (nodes: PadLockTypeVO[]): any[] => {
  823. return nodes.map(node => ({
  824. title: node.lockTypeName,
  825. value: node.lockTypeId || node.id,
  826. children: node.children ? buildTreeSelectData(node.children) : undefined,
  827. }));
  828. };
  829. setTypeTreeOptions(buildTreeSelectData(treeData));
  830. }
  831. }, [typeList]);
  832. useEffect(() => {
  833. if (visible) {
  834. if (editingType) {
  835. form.setFieldsValue({
  836. lockTypeName: editingType.lockTypeName || '',
  837. parentTypeId: editingType.parentTypeId === 0 ? undefined : editingType.parentTypeId,
  838. lockTypeSpec: editingType.lockTypeSpec || '',
  839. lockTypeIcon: editingType.lockTypeIcon || '',
  840. lockTypeImg: editingType.lockTypeImg || '',
  841. });
  842. } else {
  843. form.setFieldsValue({
  844. lockTypeName: '',
  845. parentTypeId: parentTypeId || undefined,
  846. lockTypeSpec: '',
  847. lockTypeIcon: '',
  848. lockTypeImg: '',
  849. });
  850. }
  851. }
  852. }, [visible, editingType, parentTypeId, form]);
  853. const handleSubmit = async () => {
  854. try {
  855. const values = await form.validateFields();
  856. setFormLoading(true);
  857. // 编辑模式下,合并原始数据和表单数据,确保所有必要字段都被传递
  858. const submitData: PadLockTypeVO = editingType ? {
  859. // 保留原始数据中的字段(这些字段用户不能编辑但需要传递)
  860. ...editingType,
  861. // 用表单数据覆盖可编辑字段
  862. ...values,
  863. // 确保 parentTypeId 存在(如果表单中没有,使用原始数据或默认值 0)
  864. parentTypeId: values.parentTypeId !== undefined ? (values.parentTypeId || 0) : (editingType.parentTypeId || 0),
  865. // 确保 id 和 lockTypeId 都存在
  866. id: editingType.id || editingType.lockTypeId,
  867. lockTypeId: editingType.lockTypeId || editingType.id,
  868. } : {
  869. // 新增模式下,只传递表单数据
  870. ...values,
  871. parentTypeId: values.parentTypeId || 0,
  872. };
  873. onSave(submitData);
  874. } catch (error) {
  875. // 表单验证失败
  876. } finally {
  877. setFormLoading(false);
  878. }
  879. };
  880. return (
  881. <Modal
  882. title={editingType ? t('form.editPadLockType') : t('form.addPadLockType')}
  883. open={visible}
  884. onOk={handleSubmit}
  885. onCancel={onCancel}
  886. okText={t('common.confirm')}
  887. cancelText={t('common.cancel')}
  888. confirmLoading={formLoading}
  889. width={600}
  890. >
  891. <Form
  892. form={form}
  893. layout="horizontal"
  894. labelCol={{ span: 6 }}
  895. wrapperCol={{ span: 18 }}
  896. className="mt-4"
  897. >
  898. {editingType?.parentTypeId !== 0 && (
  899. <Form.Item
  900. label={t('form.parentType')}
  901. name="parentTypeId"
  902. >
  903. <TreeSelect
  904. treeData={typeTreeOptions}
  905. placeholder={t('form.parentTypePlaceholder')}
  906. allowClear
  907. treeDefaultExpandAll
  908. />
  909. </Form.Item>
  910. )}
  911. <Form.Item
  912. label={t('form.padLockTypeName')}
  913. name="lockTypeName"
  914. rules={[{ required: true, message: t('form.padLockTypeNameRequired') }]}
  915. >
  916. <Input placeholder={t('form.padLockTypeNamePlaceholder')} />
  917. </Form.Item>
  918. <Form.Item
  919. label={t('form.padLockTypeSpec')}
  920. name="lockTypeSpec"
  921. >
  922. <Input placeholder={t('form.padLockTypeSpecPlaceholder')} />
  923. </Form.Item>
  924. <Form.Item
  925. label={t('form.padLockTypeIcon')}
  926. name="lockTypeIcon"
  927. >
  928. <UploadImg
  929. value={form.getFieldValue('lockTypeIcon')}
  930. onChange={(value) => form.setFieldsValue({ lockTypeIcon: value })}
  931. limit={1}
  932. height="100px"
  933. width="100px"
  934. />
  935. </Form.Item>
  936. <Form.Item
  937. label={t('form.padLockTypeImage')}
  938. name="lockTypeImg"
  939. >
  940. <UploadImg
  941. value={form.getFieldValue('lockTypeImg')}
  942. onChange={(value) => form.setFieldsValue({ lockTypeImg: value })}
  943. limit={1}
  944. height="100px"
  945. width="100px"
  946. />
  947. </Form.Item>
  948. </Form>
  949. </Modal>
  950. );
  951. }
  952. // 挂锁表单弹窗组件
  953. interface PadLockFormModalProps {
  954. visible: boolean;
  955. editingPadLock: PadLockVO | null;
  956. onCancel: () => void;
  957. onSave: (data: PadLockVO) => void;
  958. }
  959. function PadLockFormModal({ visible, editingPadLock, onCancel, onSave }: PadLockFormModalProps) {
  960. const { t } = useTranslation();
  961. const [form] = Form.useForm();
  962. const [formLoading, setFormLoading] = useState(false);
  963. const [lockTypeTreeOptions, setLockTypeTreeOptions] = useState<any[]>([]);
  964. const statusOptions = useMemo(() => {
  965. // 使用默认状态选项
  966. return [
  967. { dictType: 'common_status', label: t('common.enabled'), value: '1' },
  968. { dictType: 'common_status', label: t('common.disabled'), value: '0' },
  969. ];
  970. }, [t]);
  971. // 获取挂锁类型列表
  972. const getLockTypeList = async () => {
  973. try {
  974. const response = await padLockTypeApi.listPadLockType({ pageNo: 1, pageSize: -1 });
  975. const flatList = response.list || [];
  976. // 转换为树形结构
  977. const treeData = handleTree<PadLockTypeVO>(
  978. flatList.map(item => ({ ...item, id: item.lockTypeId || item.id })),
  979. 'id',
  980. 'parentTypeId',
  981. 'children'
  982. );
  983. const buildTreeSelectData = (nodes: PadLockTypeVO[]): any[] => {
  984. return nodes.map(node => ({
  985. title: node.lockTypeName,
  986. value: node.lockTypeId || node.id,
  987. children: node.children ? buildTreeSelectData(node.children) : undefined,
  988. }));
  989. };
  990. setLockTypeTreeOptions(buildTreeSelectData(treeData));
  991. } catch (error) {
  992. console.error(t('padLockManagement.getPadLockTypeListFailed'), error);
  993. }
  994. };
  995. useEffect(() => {
  996. if (visible) {
  997. getLockTypeList();
  998. if (editingPadLock) {
  999. form.setFieldsValue({
  1000. hardwareId: editingPadLock.hardwareId,
  1001. lockTypeId: editingPadLock.lockTypeId,
  1002. lockName: editingPadLock.lockName || '',
  1003. lockNfc: editingPadLock.lockNfc || '',
  1004. lockSpec: editingPadLock.lockSpec || '',
  1005. exStatus: editingPadLock.exStatus || '1',
  1006. exRemark: editingPadLock.exRemark || '',
  1007. });
  1008. } else {
  1009. form.setFieldsValue({
  1010. hardwareId: undefined,
  1011. lockTypeId: undefined,
  1012. lockName: '',
  1013. lockNfc: '',
  1014. lockSpec: '',
  1015. exStatus: '1',
  1016. exRemark: '',
  1017. });
  1018. }
  1019. }
  1020. }, [visible, editingPadLock, form]);
  1021. const handleSubmit = async () => {
  1022. try {
  1023. const values = await form.validateFields();
  1024. setFormLoading(true);
  1025. // 编辑模式下,合并原始数据和表单数据,确保所有必要字段都被传递
  1026. const submitData: PadLockVO = editingPadLock ? {
  1027. // 保留原始数据中的字段(这些字段用户不能编辑但需要传递)
  1028. ...editingPadLock,
  1029. // 用表单数据覆盖可编辑字段
  1030. ...values,
  1031. // 确保 id 和 lockId 都存在
  1032. id: editingPadLock.id || editingPadLock.lockId,
  1033. lockId: editingPadLock.lockId || editingPadLock.id,
  1034. } : {
  1035. // 新增模式下,只传递表单数据
  1036. ...values,
  1037. };
  1038. onSave(submitData);
  1039. } catch (error) {
  1040. // 表单验证失败
  1041. } finally {
  1042. setFormLoading(false);
  1043. }
  1044. };
  1045. return (
  1046. <Modal
  1047. title={editingPadLock ? t('form.editPadLock') : t('form.addPadLock')}
  1048. open={visible}
  1049. onOk={handleSubmit}
  1050. onCancel={onCancel}
  1051. okText={t('common.confirm')}
  1052. cancelText={t('common.cancel')}
  1053. confirmLoading={formLoading}
  1054. width={600}
  1055. >
  1056. <Form
  1057. form={form}
  1058. layout="horizontal"
  1059. labelCol={{ span: 6 }}
  1060. wrapperCol={{ span: 18 }}
  1061. className="mt-4"
  1062. >
  1063. <Form.Item
  1064. label={t('form.padLockType')}
  1065. name="lockTypeId"
  1066. >
  1067. <TreeSelect
  1068. treeData={lockTypeTreeOptions}
  1069. placeholder={t('form.padLockTypePlaceholder')}
  1070. allowClear
  1071. treeDefaultExpandAll
  1072. />
  1073. </Form.Item>
  1074. <Form.Item
  1075. label={t('form.padLockName')}
  1076. name="lockName"
  1077. rules={[{ required: true, message: t('form.padLockNameRequired') }]}
  1078. >
  1079. <Input placeholder={t('form.padLockNamePlaceholder')} />
  1080. </Form.Item>
  1081. <Form.Item
  1082. label={t('form.padLockNfc')}
  1083. name="lockNfc"
  1084. rules={[{ required: true, message: t('form.padLockNfcRequired') }]}
  1085. >
  1086. <Input placeholder={t('form.padLockNfcPlaceholder')} />
  1087. </Form.Item>
  1088. <Form.Item
  1089. label={t('form.padLockSpec')}
  1090. name="lockSpec"
  1091. >
  1092. <Input placeholder={t('form.padLockSpecPlaceholder')} />
  1093. </Form.Item>
  1094. <Form.Item
  1095. label={t('form.status')}
  1096. name="exStatus"
  1097. >
  1098. <Radio.Group>
  1099. {statusOptions.map(option => (
  1100. <Radio key={option.value} value={option.value}>
  1101. {option.label}
  1102. </Radio>
  1103. ))}
  1104. </Radio.Group>
  1105. </Form.Item>
  1106. <Form.Item
  1107. label={t('form.remark')}
  1108. name="exRemark"
  1109. >
  1110. <Input.TextArea placeholder={t('form.remarkPlaceholder')} rows={3} />
  1111. </Form.Item>
  1112. </Form>
  1113. </Modal>
  1114. );
  1115. }