PadLockManagement.tsx 40 KB

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