PadLockManagement.tsx 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  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 { hardwareApi } from '../api/Hardware';
  6. import { toast } from 'sonner';
  7. import { Modal, Button, Input, Space, Table, Select, TreeSelect, Form, Image, Switch, Radio, Tooltip } from 'antd';
  8. import { ExclamationCircleOutlined } from '@ant-design/icons';
  9. import type { ColumnsType } from 'antd/es/table';
  10. import { handleTree } from '../utils/tree';
  11. import { DICT_TYPE, getStrDictOptions } from '../utils/dict';
  12. import { dateFormatter } from '../utils/formatTime';
  13. import UploadImg from './lockCabinet/UploadImg';
  14. import { Button as UIButton } from './ui/button';
  15. import { useTranslation } from 'react-i18next';
  16. import PermissionWrapper from './PermissionWrapper';
  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.hardwareName'),
  330. dataIndex: 'hardwareName',
  331. width: 150,
  332. render: (text: string) => text || '-',
  333. },
  334. {
  335. title: t('table.padLockType'),
  336. dataIndex: 'lockTypeName',
  337. width: 150,
  338. render: (text: string) => text || '-',
  339. },
  340. {
  341. title: t('table.padLockModel'),
  342. dataIndex: 'lockSpec',
  343. width: 150,
  344. render: (text: string) => text || '-',
  345. },
  346. {
  347. title: t('table.status'),
  348. dataIndex: 'exStatus',
  349. width: 100,
  350. align: 'center',
  351. render: (status: string) => {
  352. if (status === null || status === undefined) return '-';
  353. const isEnabled = status === '1';
  354. return (
  355. <span className={`inline-flex px-3 py-1 rounded-lg text-xs ${
  356. isEnabled ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'
  357. }`}>
  358. {isEnabled ? t('common.enabled') : t('common.disabled')}
  359. </span>
  360. );
  361. },
  362. },
  363. {
  364. title: t('table.remark'),
  365. dataIndex: 'exRemark',
  366. render: (text: string) => {
  367. const remarkText = text || '-';
  368. const maxLength = 20;
  369. const shouldTruncate = remarkText.length > maxLength;
  370. const displayText = shouldTruncate ? remarkText.slice(0, maxLength) + '...' : remarkText;
  371. return (
  372. <Tooltip placement="topLeft" title={remarkText}>
  373. <span>{displayText}</span>
  374. </Tooltip>
  375. );
  376. },
  377. },
  378. {
  379. title: t('table.createTime'),
  380. dataIndex: 'createTime',
  381. width: 180,
  382. render: (text: string | Date) => text ? dateFormatter(text) : '-',
  383. },
  384. {
  385. title: t('table.operation'),
  386. width: 150,
  387. align: 'center',
  388. fixed: 'right',
  389. render: (_: any, record: PadLockVO) => (
  390. <div className="flex items-center gap-2 justify-center">
  391. <PermissionWrapper permission="iscs:lock:update">
  392. <UIButton
  393. variant="ghost"
  394. size="sm"
  395. onClick={() => openPadLockForm('update', record.lockId || record.id)}
  396. className="h-8 px-2 transition-colors hover:underline"
  397. style={{ color: '#000000' }}
  398. onMouseEnter={(e) => {
  399. e.currentTarget.style.color = '#1677ff';
  400. e.currentTarget.style.textDecoration = 'underline';
  401. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  402. }}
  403. onMouseLeave={(e) => {
  404. e.currentTarget.style.color = '#000000';
  405. e.currentTarget.style.textDecoration = 'none';
  406. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  407. }}
  408. >
  409. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  410. <span className="ml-1">{t('common.edit')}</span>
  411. </UIButton>
  412. </PermissionWrapper>
  413. <PermissionWrapper permission="iscs:lock:delete">
  414. <UIButton
  415. variant="ghost"
  416. size="sm"
  417. onClick={() => handleDelete(record.lockId || record.id)}
  418. className="h-8 px-2 transition-colors hover:underline"
  419. style={{ color: '#000000' }}
  420. onMouseEnter={(e) => {
  421. e.currentTarget.style.color = '#1677ff';
  422. e.currentTarget.style.textDecoration = 'underline';
  423. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  424. }}
  425. onMouseLeave={(e) => {
  426. e.currentTarget.style.color = '#000000';
  427. e.currentTarget.style.textDecoration = 'none';
  428. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  429. }}
  430. >
  431. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  432. <span className="ml-1">{t('common.delete')}</span>
  433. </UIButton>
  434. </PermissionWrapper>
  435. </div>
  436. ),
  437. },
  438. ];
  439. // 挂锁类型表格列(树形表格)
  440. const padLockTypeColumns: ColumnsType<PadLockTypeVO> = [
  441. {
  442. title: t('table.padLockTypeName'),
  443. dataIndex: 'lockTypeName',
  444. width: 200,
  445. align: 'center',
  446. },
  447. {
  448. title: t('table.padLockTypeIcon'),
  449. dataIndex: 'lockTypeIcon',
  450. width: 120,
  451. align: 'center',
  452. render: (url: string) => {
  453. if (!url) return '-';
  454. return (
  455. <Image
  456. src={url}
  457. alt={t('padLockManagement.icon')}
  458. width={50}
  459. height={50}
  460. style={{ objectFit: 'cover' }}
  461. preview={{
  462. mask: t('padLockManagement.view'),
  463. }}
  464. />
  465. );
  466. },
  467. },
  468. {
  469. title: t('table.padLockTypeImage'),
  470. dataIndex: 'lockTypeImg',
  471. width: 120,
  472. align: 'center',
  473. render: (url: string) => {
  474. if (!url) return '-';
  475. return (
  476. <Image
  477. src={url}
  478. alt={t('padLockManagement.image')}
  479. width={50}
  480. height={50}
  481. style={{ objectFit: 'cover' }}
  482. preview={{
  483. mask: t('padLockManagement.view'),
  484. }}
  485. />
  486. );
  487. },
  488. },
  489. {
  490. title: t('table.padLockModel'),
  491. dataIndex: 'lockTypeSpec',
  492. width: 150,
  493. align: 'center',
  494. render: (spec: string) => spec || '-',
  495. },
  496. {
  497. title: t('table.operation'),
  498. width: 200,
  499. align: 'center',
  500. fixed: 'right',
  501. render: (_: any, record: PadLockTypeVO) => (
  502. <div className="flex items-center gap-2 justify-center">
  503. <PermissionWrapper permission="iscs:lock:update">
  504. <UIButton
  505. variant="ghost"
  506. size="sm"
  507. onClick={() => openTypeForm('update', record.lockTypeId || record.id)}
  508. className="h-8 px-2 transition-colors hover:underline"
  509. style={{ color: '#000000' }}
  510. onMouseEnter={(e) => {
  511. e.currentTarget.style.color = '#1677ff';
  512. e.currentTarget.style.textDecoration = 'underline';
  513. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  514. }}
  515. onMouseLeave={(e) => {
  516. e.currentTarget.style.color = '#000000';
  517. e.currentTarget.style.textDecoration = 'none';
  518. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  519. }}
  520. >
  521. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  522. <span className="ml-1">{t('common.edit')}</span>
  523. </UIButton>
  524. </PermissionWrapper>
  525. <UIButton
  526. variant="ghost"
  527. size="sm"
  528. onClick={() => openTypeForm('create', undefined, record.lockTypeId || record.id)}
  529. className="h-8 px-2 transition-colors hover:underline"
  530. style={{ color: '#000000' }}
  531. onMouseEnter={(e) => {
  532. e.currentTarget.style.color = '#1677ff';
  533. e.currentTarget.style.textDecoration = 'underline';
  534. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  535. }}
  536. onMouseLeave={(e) => {
  537. e.currentTarget.style.color = '#000000';
  538. e.currentTarget.style.textDecoration = 'none';
  539. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  540. }}
  541. >
  542. <Plus className="w-4 h-4" style={{ color: '#000000' }} />
  543. <span className="ml-1">{t('common.addNew')}</span>
  544. </UIButton>
  545. <PermissionWrapper permission="iscs:lock:delete">
  546. <UIButton
  547. variant="ghost"
  548. size="sm"
  549. onClick={() => handleDeleteType(record.lockTypeId || record.id!, record.lockTypeName)}
  550. className="h-8 px-2 transition-colors hover:underline"
  551. style={{ color: '#000000' }}
  552. onMouseEnter={(e) => {
  553. e.currentTarget.style.color = '#1677ff';
  554. e.currentTarget.style.textDecoration = 'underline';
  555. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  556. }}
  557. onMouseLeave={(e) => {
  558. e.currentTarget.style.color = '#000000';
  559. e.currentTarget.style.textDecoration = 'none';
  560. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  561. }}
  562. >
  563. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  564. <span className="ml-1">{t('common.delete')}</span>
  565. </UIButton>
  566. </PermissionWrapper>
  567. </div>
  568. ),
  569. },
  570. ];
  571. return (
  572. <div className="space-y-4">
  573. {viewMode === 'list' ? (
  574. <>
  575. {/* 挂锁列表 - 搜索栏和表格放在一起 */}
  576. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm overflow-hidden">
  577. {/* 搜索栏 */}
  578. <div className="p-4 lg:p-5 border-b border-gray-200">
  579. <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
  580. {/* 搜索输入框 */}
  581. <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
  582. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  583. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.padLockName')}:</label>
  584. <Input
  585. value={queryParams.lockName || ''}
  586. onChange={(e) => setQueryParams({ ...queryParams, lockName: e.target.value })}
  587. onPressEnter={handleQuery}
  588. placeholder={t('form.padLockNamePlaceholder')}
  589. className="min-w-[150px] max-w-[200px]"
  590. allowClear
  591. />
  592. </div>
  593. </div>
  594. {/* 操作按钮组 */}
  595. <Space className="flex-shrink-0">
  596. <PermissionWrapper permission="iscs:lock:query">
  597. <Button
  598. type="primary"
  599. icon={<Search className="w-4 h-4" />}
  600. onClick={handleQuery}
  601. >
  602. {t('common.search')}
  603. </Button>
  604. </PermissionWrapper>
  605. <PermissionWrapper permission="iscs:lock:query">
  606. <Button
  607. icon={<RefreshCw className="w-4 h-4" />}
  608. onClick={resetQuery}
  609. >
  610. {t('common.reset')}
  611. </Button>
  612. </PermissionWrapper>
  613. <PermissionWrapper permission="iscs:lock:create">
  614. <Button
  615. type="primary"
  616. icon={<Plus className="w-4 h-4" />}
  617. onClick={() => openPadLockForm('create')}
  618. >
  619. {t('common.addNew')}
  620. </Button>
  621. </PermissionWrapper>
  622. <PermissionWrapper permission="iscs:lock:delete">
  623. <Button
  624. danger
  625. icon={<Trash2 className="w-4 h-4" />}
  626. onClick={() => handleDelete()}
  627. disabled={selectedRowKeys.length === 0}
  628. >
  629. {t('common.batchDelete')}
  630. </Button>
  631. </PermissionWrapper>
  632. <Button
  633. icon={<Settings className="w-4 h-4" />}
  634. onClick={() => setViewMode('type')}
  635. >
  636. {t('common.setPadLockType')}
  637. </Button>
  638. </Space>
  639. </div>
  640. </div>
  641. {/* 表格 */}
  642. <div className="min-w-0">
  643. <Table
  644. columns={padLockColumns}
  645. dataSource={list}
  646. rowKey={(record) => record.lockId || record.id || ''}
  647. loading={loading}
  648. pagination={false}
  649. scroll={{ x: 'max-content' }}
  650. rowSelection={{
  651. selectedRowKeys,
  652. onChange: setSelectedRowKeys,
  653. }}
  654. />
  655. </div>
  656. </div>
  657. {/* 分页 */}
  658. {!loading && list.length > 0 && (
  659. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  660. <div className="flex items-center justify-between">
  661. <div className="text-sm text-gray-600">
  662. {t('common.total')} <span className="text-blue-600 font-medium">{total}</span> {t('common.records')}
  663. </div>
  664. <div className="flex gap-2">
  665. <Button
  666. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
  667. disabled={queryParams.pageNo! <= 1}
  668. >
  669. {t('common.prevPage')}
  670. </Button>
  671. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  672. {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
  673. </span>
  674. <Button
  675. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
  676. disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
  677. >
  678. {t('common.nextPage')}
  679. </Button>
  680. </div>
  681. </div>
  682. </div>
  683. )}
  684. {/* 挂锁表单弹窗 */}
  685. <PadLockFormModal
  686. visible={showPadLockForm}
  687. editingPadLock={editingPadLock}
  688. onCancel={() => {
  689. setShowPadLockForm(false);
  690. setEditingPadLock(null);
  691. }}
  692. onSave={savePadLock}
  693. />
  694. </>
  695. ) : (
  696. <>
  697. {/* 挂锁类型 - 搜索栏和表格放在一起 */}
  698. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm overflow-hidden">
  699. {/* 搜索栏 */}
  700. <div className="p-5 border-b border-gray-200">
  701. <div className="flex items-center justify-between gap-4 flex-wrap">
  702. {/* 搜索输入框 */}
  703. <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
  704. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  705. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.padLockTypeName')}:</label>
  706. <Input
  707. value={typeQueryParams.lockTypeName || ''}
  708. onChange={(e) => setTypeQueryParams({ ...typeQueryParams, lockTypeName: e.target.value })}
  709. onPressEnter={handleTypeQuery}
  710. placeholder={t('form.padLockTypeNamePlaceholder')}
  711. className="min-w-[150px] max-w-[200px]"
  712. allowClear
  713. />
  714. </div>
  715. </div>
  716. {/* 操作按钮组 */}
  717. <Space className="flex-shrink-0">
  718. <PermissionWrapper permission="iscs:lock:query">
  719. <Button
  720. type="primary"
  721. icon={<Search className="w-4 h-4" />}
  722. onClick={handleTypeQuery}
  723. >
  724. {t('common.search')}
  725. </Button>
  726. </PermissionWrapper>
  727. <PermissionWrapper permission="iscs:lock:query">
  728. <Button
  729. icon={<RefreshCw className="w-4 h-4" />}
  730. onClick={resetTypeQuery}
  731. >
  732. {t('common.reset')}
  733. </Button>
  734. </PermissionWrapper>
  735. <PermissionWrapper permission="iscs:lock:create">
  736. <Button
  737. type="primary"
  738. icon={<Plus className="w-4 h-4" />}
  739. onClick={() => openTypeForm('create')}
  740. >
  741. {t('common.addNew')}
  742. </Button>
  743. </PermissionWrapper>
  744. <Button
  745. icon={<ArrowUpDown className="w-4 h-4" />}
  746. onClick={toggleExpandAll}
  747. >
  748. {t('padLockManagement.expandCollapse')}
  749. </Button>
  750. <Button
  751. onClick={() => setViewMode('list')}
  752. >
  753. {t('padLockManagement.backToPadLockList')}
  754. </Button>
  755. </Space>
  756. </div>
  757. </div>
  758. {/* 表格 - 树形表格 */}
  759. <div>
  760. <Table
  761. columns={padLockTypeColumns}
  762. dataSource={typeTreeList}
  763. rowKey={(record) => record.lockTypeId || record.id || ''}
  764. loading={typeLoading}
  765. pagination={false}
  766. scroll={{ x: 'max-content' }}
  767. expandable={{
  768. expandedRowKeys,
  769. onExpandedRowsChange: setExpandedRowKeys,
  770. defaultExpandAllRows: isExpandAll,
  771. }}
  772. />
  773. </div>
  774. </div>
  775. {/* 挂锁类型表单弹窗 */}
  776. {showTypeForm && (
  777. <TypeFormModal
  778. visible={showTypeForm}
  779. editingType={editingType}
  780. parentTypeId={parentTypeId}
  781. typeList={typeList}
  782. onCancel={() => {
  783. setShowTypeForm(false);
  784. setEditingType(null);
  785. setParentTypeId(undefined);
  786. }}
  787. onSave={saveType}
  788. />
  789. )}
  790. </>
  791. )}
  792. </div>
  793. );
  794. }
  795. // 挂锁类型表单弹窗组件
  796. interface TypeFormModalProps {
  797. visible: boolean;
  798. editingType: PadLockTypeVO | null;
  799. parentTypeId?: number;
  800. typeList: PadLockTypeVO[];
  801. onCancel: () => void;
  802. onSave: (data: PadLockTypeVO) => void;
  803. }
  804. function TypeFormModal({ visible, editingType, parentTypeId, typeList, onCancel, onSave }: TypeFormModalProps) {
  805. const { t } = useTranslation();
  806. const [form] = Form.useForm();
  807. const [formLoading, setFormLoading] = useState(false);
  808. const [typeTreeOptions, setTypeTreeOptions] = useState<any[]>([]);
  809. // 构建树形选择器数据
  810. useEffect(() => {
  811. if (typeList.length > 0) {
  812. const treeData = handleTree<PadLockTypeVO>(
  813. typeList.map(item => ({ ...item, id: item.lockTypeId || item.id })),
  814. 'id',
  815. 'parentTypeId',
  816. 'children'
  817. );
  818. const buildTreeSelectData = (nodes: PadLockTypeVO[]): any[] => {
  819. return nodes.map(node => ({
  820. title: node.lockTypeName,
  821. value: node.lockTypeId || node.id,
  822. children: node.children ? buildTreeSelectData(node.children) : undefined,
  823. }));
  824. };
  825. setTypeTreeOptions(buildTreeSelectData(treeData));
  826. }
  827. }, [typeList]);
  828. useEffect(() => {
  829. if (visible) {
  830. if (editingType) {
  831. form.setFieldsValue({
  832. lockTypeName: editingType.lockTypeName || '',
  833. parentTypeId: editingType.parentTypeId === 0 ? undefined : editingType.parentTypeId,
  834. lockTypeSpec: editingType.lockTypeSpec || '',
  835. lockTypeIcon: editingType.lockTypeIcon || '',
  836. lockTypeImg: editingType.lockTypeImg || '',
  837. });
  838. } else {
  839. form.setFieldsValue({
  840. lockTypeName: '',
  841. parentTypeId: parentTypeId || undefined,
  842. lockTypeSpec: '',
  843. lockTypeIcon: '',
  844. lockTypeImg: '',
  845. });
  846. }
  847. }
  848. }, [visible, editingType, parentTypeId, form]);
  849. const handleSubmit = async () => {
  850. try {
  851. const values = await form.validateFields();
  852. setFormLoading(true);
  853. // 编辑模式下,合并原始数据和表单数据,确保所有必要字段都被传递
  854. const submitData: PadLockTypeVO = editingType ? {
  855. // 保留原始数据中的字段(这些字段用户不能编辑但需要传递)
  856. ...editingType,
  857. // 用表单数据覆盖可编辑字段
  858. ...values,
  859. // 确保 parentTypeId 存在(如果表单中没有,使用原始数据或默认值 0)
  860. parentTypeId: values.parentTypeId !== undefined ? (values.parentTypeId || 0) : (editingType.parentTypeId || 0),
  861. // 确保 id 和 lockTypeId 都存在
  862. id: editingType.id || editingType.lockTypeId,
  863. lockTypeId: editingType.lockTypeId || editingType.id,
  864. } : {
  865. // 新增模式下,只传递表单数据
  866. ...values,
  867. parentTypeId: values.parentTypeId || 0,
  868. };
  869. onSave(submitData);
  870. } catch (error) {
  871. // 表单验证失败
  872. } finally {
  873. setFormLoading(false);
  874. }
  875. };
  876. return (
  877. <Modal
  878. title={editingType ? t('form.editPadLockType') : t('form.addPadLockType')}
  879. open={visible}
  880. onOk={handleSubmit}
  881. onCancel={onCancel}
  882. okText={t('common.confirm')}
  883. cancelText={t('common.cancel')}
  884. confirmLoading={formLoading}
  885. width={600}
  886. >
  887. <Form
  888. form={form}
  889. layout="horizontal"
  890. labelCol={{ span: 6 }}
  891. wrapperCol={{ span: 18 }}
  892. className="mt-4"
  893. >
  894. {editingType?.parentTypeId !== 0 && (
  895. <Form.Item
  896. label={t('form.parentType')}
  897. name="parentTypeId"
  898. >
  899. <TreeSelect
  900. treeData={typeTreeOptions}
  901. placeholder={t('form.parentTypePlaceholder')}
  902. allowClear
  903. treeDefaultExpandAll
  904. />
  905. </Form.Item>
  906. )}
  907. <Form.Item
  908. label={t('form.padLockTypeName')}
  909. name="lockTypeName"
  910. rules={[{ required: true, message: t('form.padLockTypeNameRequired') }]}
  911. >
  912. <Input placeholder={t('form.padLockTypeNamePlaceholder')} />
  913. </Form.Item>
  914. <Form.Item
  915. label={t('form.padLockTypeSpec')}
  916. name="lockTypeSpec"
  917. >
  918. <Input placeholder={t('form.padLockTypeSpecPlaceholder')} />
  919. </Form.Item>
  920. <Form.Item
  921. label={t('form.padLockTypeIcon')}
  922. name="lockTypeIcon"
  923. >
  924. <UploadImg
  925. value={form.getFieldValue('lockTypeIcon')}
  926. onChange={(value) => form.setFieldsValue({ lockTypeIcon: value })}
  927. limit={1}
  928. height="100px"
  929. width="100px"
  930. />
  931. </Form.Item>
  932. <Form.Item
  933. label={t('form.padLockTypeImage')}
  934. name="lockTypeImg"
  935. >
  936. <UploadImg
  937. value={form.getFieldValue('lockTypeImg')}
  938. onChange={(value) => form.setFieldsValue({ lockTypeImg: value })}
  939. limit={1}
  940. height="100px"
  941. width="100px"
  942. />
  943. </Form.Item>
  944. </Form>
  945. </Modal>
  946. );
  947. }
  948. // 挂锁表单弹窗组件
  949. interface PadLockFormModalProps {
  950. visible: boolean;
  951. editingPadLock: PadLockVO | null;
  952. onCancel: () => void;
  953. onSave: (data: PadLockVO) => void;
  954. }
  955. function PadLockFormModal({ visible, editingPadLock, onCancel, onSave }: PadLockFormModalProps) {
  956. const { t } = useTranslation();
  957. const [form] = Form.useForm();
  958. const [formLoading, setFormLoading] = useState(false);
  959. const [hardwareOptions, setHardwareOptions] = useState<{ label: string; value: number }[]>([]);
  960. const [lockTypeTreeOptions, setLockTypeTreeOptions] = useState<any[]>([]);
  961. const statusOptions = useMemo(() => {
  962. // 使用默认状态选项
  963. return [
  964. { dictType: 'common_status', label: t('common.enabled'), value: '1' },
  965. { dictType: 'common_status', label: t('common.disabled'), value: '0' },
  966. ];
  967. }, [t]);
  968. // 获取硬件列表
  969. const getHardwareList = async () => {
  970. try {
  971. const response = await hardwareApi.listHardware({ pageNo: 1, pageSize: -1 });
  972. const options = response.list.map(item => ({
  973. label: item.hardwareName,
  974. value: item.id!,
  975. }));
  976. setHardwareOptions(options);
  977. } catch (error) {
  978. console.error(t('padLockManagement.getHardwareListFailed'), error);
  979. }
  980. };
  981. // 获取挂锁类型列表
  982. const getLockTypeList = async () => {
  983. try {
  984. const response = await padLockTypeApi.listPadLockType({ pageNo: 1, pageSize: -1 });
  985. const flatList = response.list || [];
  986. // 转换为树形结构
  987. const treeData = handleTree<PadLockTypeVO>(
  988. flatList.map(item => ({ ...item, id: item.lockTypeId || item.id })),
  989. 'id',
  990. 'parentTypeId',
  991. 'children'
  992. );
  993. const buildTreeSelectData = (nodes: PadLockTypeVO[]): any[] => {
  994. return nodes.map(node => ({
  995. title: node.lockTypeName,
  996. value: node.lockTypeId || node.id,
  997. children: node.children ? buildTreeSelectData(node.children) : undefined,
  998. }));
  999. };
  1000. setLockTypeTreeOptions(buildTreeSelectData(treeData));
  1001. } catch (error) {
  1002. console.error(t('padLockManagement.getPadLockTypeListFailed'), error);
  1003. }
  1004. };
  1005. useEffect(() => {
  1006. if (visible) {
  1007. getHardwareList();
  1008. getLockTypeList();
  1009. if (editingPadLock) {
  1010. form.setFieldsValue({
  1011. hardwareId: editingPadLock.hardwareId,
  1012. lockTypeId: editingPadLock.lockTypeId,
  1013. lockName: editingPadLock.lockName || '',
  1014. lockNfc: editingPadLock.lockNfc || '',
  1015. lockSpec: editingPadLock.lockSpec || '',
  1016. exStatus: editingPadLock.exStatus || '1',
  1017. exRemark: editingPadLock.exRemark || '',
  1018. });
  1019. } else {
  1020. form.setFieldsValue({
  1021. hardwareId: undefined,
  1022. lockTypeId: undefined,
  1023. lockName: '',
  1024. lockNfc: '',
  1025. lockSpec: '',
  1026. exStatus: '1',
  1027. exRemark: '',
  1028. });
  1029. }
  1030. }
  1031. }, [visible, editingPadLock, form]);
  1032. const handleSubmit = async () => {
  1033. try {
  1034. const values = await form.validateFields();
  1035. setFormLoading(true);
  1036. // 编辑模式下,合并原始数据和表单数据,确保所有必要字段都被传递
  1037. const submitData: PadLockVO = editingPadLock ? {
  1038. // 保留原始数据中的字段(这些字段用户不能编辑但需要传递)
  1039. ...editingPadLock,
  1040. // 用表单数据覆盖可编辑字段
  1041. ...values,
  1042. // 确保 id 和 lockId 都存在
  1043. id: editingPadLock.id || editingPadLock.lockId,
  1044. lockId: editingPadLock.lockId || editingPadLock.id,
  1045. } : {
  1046. // 新增模式下,只传递表单数据
  1047. ...values,
  1048. };
  1049. onSave(submitData);
  1050. } catch (error) {
  1051. // 表单验证失败
  1052. } finally {
  1053. setFormLoading(false);
  1054. }
  1055. };
  1056. return (
  1057. <Modal
  1058. title={editingPadLock ? t('form.editPadLock') : t('form.addPadLock')}
  1059. open={visible}
  1060. onOk={handleSubmit}
  1061. onCancel={onCancel}
  1062. okText={t('common.confirm')}
  1063. cancelText={t('common.cancel')}
  1064. confirmLoading={formLoading}
  1065. width={600}
  1066. >
  1067. <Form
  1068. form={form}
  1069. layout="horizontal"
  1070. labelCol={{ span: 6 }}
  1071. wrapperCol={{ span: 18 }}
  1072. className="mt-4"
  1073. >
  1074. <Form.Item
  1075. label={t('table.hardwareName')}
  1076. name="hardwareId"
  1077. >
  1078. <Select
  1079. placeholder={t('form.hardwareNamePlaceholder')}
  1080. allowClear
  1081. options={hardwareOptions}
  1082. />
  1083. </Form.Item>
  1084. <Form.Item
  1085. label={t('form.padLockType')}
  1086. name="lockTypeId"
  1087. >
  1088. <TreeSelect
  1089. treeData={lockTypeTreeOptions}
  1090. placeholder={t('form.padLockTypePlaceholder')}
  1091. allowClear
  1092. treeDefaultExpandAll
  1093. />
  1094. </Form.Item>
  1095. <Form.Item
  1096. label={t('form.padLockName')}
  1097. name="lockName"
  1098. rules={[{ required: true, message: t('form.padLockNameRequired') }]}
  1099. >
  1100. <Input placeholder={t('form.padLockNamePlaceholder')} />
  1101. </Form.Item>
  1102. <Form.Item
  1103. label={t('form.padLockNfc')}
  1104. name="lockNfc"
  1105. rules={[{ required: true, message: t('form.padLockNfcRequired') }]}
  1106. >
  1107. <Input placeholder={t('form.padLockNfcPlaceholder')} />
  1108. </Form.Item>
  1109. <Form.Item
  1110. label={t('form.padLockSpec')}
  1111. name="lockSpec"
  1112. >
  1113. <Input placeholder={t('form.padLockSpecPlaceholder')} />
  1114. </Form.Item>
  1115. <Form.Item
  1116. label={t('form.status')}
  1117. name="exStatus"
  1118. >
  1119. <Radio.Group>
  1120. {statusOptions.map(option => (
  1121. <Radio key={option.value} value={option.value}>
  1122. {option.label}
  1123. </Radio>
  1124. ))}
  1125. </Radio.Group>
  1126. </Form.Item>
  1127. <Form.Item
  1128. label={t('form.remark')}
  1129. name="exRemark"
  1130. >
  1131. <Input.TextArea placeholder={t('form.remarkPlaceholder')} rows={3} />
  1132. </Form.Item>
  1133. </Form>
  1134. </Modal>
  1135. );
  1136. }