PadLockManagement.tsx 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146
  1. import React, { useState, useEffect, useRef } 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 } 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 [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 || '获取挂锁列表失败');
  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 || '获取挂锁类型列表失败');
  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('请选择要删除的数据');
  154. return;
  155. }
  156. Modal.confirm({
  157. title: '确认删除',
  158. icon: <ExclamationCircleOutlined />,
  159. content: (
  160. <div>
  161. <p>确定要删除选中的 {ids.length} 条挂锁数据吗?</p>
  162. <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
  163. </div>
  164. ),
  165. okText: '确定删除',
  166. okType: 'danger',
  167. cancelText: '取消',
  168. onOk: async () => {
  169. try {
  170. await padLockApi.delPadLock(ids);
  171. toast.success('删除成功');
  172. setSelectedRowKeys([]);
  173. await getList();
  174. } catch (error: any) {
  175. toast.error(error.message || '删除失败');
  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 || '获取挂锁信息失败');
  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('修改成功');
  211. } else {
  212. await padLockApi.addPadLock(formData);
  213. toast.success('新增成功');
  214. }
  215. setShowPadLockForm(false);
  216. setEditingPadLock(null);
  217. await getList();
  218. } catch (error: any) {
  219. toast.error(error.message || '保存失败');
  220. }
  221. };
  222. // 删除挂锁类型
  223. const handleDeleteType = async (id: number, name?: string) => {
  224. Modal.confirm({
  225. title: '确认删除',
  226. icon: <ExclamationCircleOutlined />,
  227. content: (
  228. <div>
  229. <p>确定要删除挂锁类型 <strong>"{name || '该类型'}"</strong> 吗?</p>
  230. <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
  231. </div>
  232. ),
  233. okText: '确定删除',
  234. okType: 'danger',
  235. cancelText: '取消',
  236. onOk: async () => {
  237. try {
  238. await padLockTypeApi.delPadLockType(id);
  239. toast.success('删除成功');
  240. await getTypeList();
  241. } catch (error: any) {
  242. toast.error(error.message || '删除失败');
  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 || '获取挂锁类型信息失败');
  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('修改成功');
  302. } else {
  303. await padLockTypeApi.addPadLockType(formData);
  304. toast.success('新增成功');
  305. }
  306. setShowTypeForm(false);
  307. setEditingType(null);
  308. setParentTypeId(undefined);
  309. await getTypeList();
  310. } catch (error: any) {
  311. toast.error(error.message || '保存失败');
  312. }
  313. };
  314. // 挂锁列表表格列
  315. const padLockColumns: ColumnsType<PadLockVO> = [
  316. {
  317. title: '挂锁名称',
  318. dataIndex: 'lockName',
  319. width: 200,
  320. },
  321. {
  322. title: '挂锁NFC',
  323. dataIndex: 'lockNfc',
  324. width: 180,
  325. ellipsis: true,
  326. },
  327. {
  328. title: '所属硬件',
  329. dataIndex: 'hardwareName',
  330. width: 150,
  331. render: (text: string) => text || '-',
  332. },
  333. {
  334. title: '挂锁类型',
  335. dataIndex: 'lockTypeName',
  336. width: 150,
  337. render: (text: string) => text || '-',
  338. },
  339. {
  340. title: '挂锁型号',
  341. dataIndex: 'lockSpec',
  342. width: 150,
  343. render: (text: string) => text || '-',
  344. },
  345. {
  346. title: '状态',
  347. dataIndex: 'exStatus',
  348. width: 100,
  349. align: 'center',
  350. render: (status: string) => {
  351. if (status === null || status === undefined) return '-';
  352. return (
  353. <Switch
  354. checked={status === '1'}
  355. disabled
  356. checkedChildren="启用"
  357. unCheckedChildren="禁用"
  358. />
  359. );
  360. },
  361. },
  362. {
  363. title: '备注',
  364. dataIndex: 'exRemark',
  365. ellipsis: true,
  366. render: (text: string) => text || '-',
  367. },
  368. {
  369. title: '创建时间',
  370. dataIndex: 'createTime',
  371. width: 180,
  372. render: (text: string | Date) => text ? dateFormatter(text) : '-',
  373. },
  374. {
  375. title: '操作',
  376. width: 150,
  377. align: 'center',
  378. fixed: 'right',
  379. render: (_: any, record: PadLockVO) => (
  380. <div className="flex items-center gap-2 justify-center">
  381. <PermissionWrapper permission="iscs:lock:update">
  382. <UIButton
  383. variant="ghost"
  384. size="sm"
  385. onClick={() => openPadLockForm('update', record.lockId || record.id)}
  386. className="h-8 px-2"
  387. >
  388. <Edit2 className="w-4 h-4" />
  389. <span className="ml-1">编辑</span>
  390. </UIButton>
  391. </PermissionWrapper>
  392. <PermissionWrapper permission="iscs:lock:delete">
  393. <UIButton
  394. variant="ghost"
  395. size="sm"
  396. onClick={() => handleDelete(record.lockId || record.id)}
  397. className="h-8 px-2 text-red-600 hover:text-red-700"
  398. >
  399. <Trash2 className="w-4 h-4" />
  400. <span className="ml-1">删除</span>
  401. </UIButton>
  402. </PermissionWrapper>
  403. </div>
  404. ),
  405. },
  406. ];
  407. // 挂锁类型表格列(树形表格)
  408. const padLockTypeColumns: ColumnsType<PadLockTypeVO> = [
  409. {
  410. title: '挂锁类型名称',
  411. dataIndex: 'lockTypeName',
  412. width: 200,
  413. align: 'center',
  414. },
  415. {
  416. title: '挂锁类型图标',
  417. dataIndex: 'lockTypeIcon',
  418. width: 120,
  419. align: 'center',
  420. render: (url: string) => {
  421. if (!url) return '-';
  422. return (
  423. <Image
  424. src={url}
  425. alt="图标"
  426. width={50}
  427. height={50}
  428. style={{ objectFit: 'cover' }}
  429. preview={{
  430. mask: '查看',
  431. }}
  432. />
  433. );
  434. },
  435. },
  436. {
  437. title: '挂锁类型图片',
  438. dataIndex: 'lockTypeImg',
  439. width: 120,
  440. align: 'center',
  441. render: (url: string) => {
  442. if (!url) return '-';
  443. return (
  444. <Image
  445. src={url}
  446. alt="图片"
  447. width={50}
  448. height={50}
  449. style={{ objectFit: 'cover' }}
  450. preview={{
  451. mask: '查看',
  452. }}
  453. />
  454. );
  455. },
  456. },
  457. {
  458. title: '挂锁型号',
  459. dataIndex: 'lockTypeSpec',
  460. width: 150,
  461. align: 'center',
  462. render: (spec: string) => spec || '-',
  463. },
  464. {
  465. title: '操作',
  466. width: 200,
  467. align: 'center',
  468. fixed: 'right',
  469. render: (_: any, record: PadLockTypeVO) => (
  470. <div className="flex items-center gap-2 justify-center">
  471. <PermissionWrapper permission="iscs:lock:update">
  472. <UIButton
  473. variant="ghost"
  474. size="sm"
  475. onClick={() => openTypeForm('update', record.lockTypeId || record.id)}
  476. className="h-8 px-2"
  477. >
  478. <Edit2 className="w-4 h-4" />
  479. <span className="ml-1">修改</span>
  480. </UIButton>
  481. </PermissionWrapper>
  482. <UIButton
  483. variant="ghost"
  484. size="sm"
  485. onClick={() => openTypeForm('create', undefined, record.lockTypeId || record.id)}
  486. className="h-8 px-2"
  487. >
  488. <Plus className="w-4 h-4" />
  489. <span className="ml-1">新增</span>
  490. </UIButton>
  491. {record.parentTypeId !== 0 && (
  492. <UIButton
  493. variant="ghost"
  494. size="sm"
  495. onClick={() => handleDeleteType(record.lockTypeId || record.id!, record.lockTypeName)}
  496. className="h-8 px-2 text-red-600 hover:text-red-700"
  497. >
  498. <Trash2 className="w-4 h-4" />
  499. <span className="ml-1">删除</span>
  500. </UIButton>
  501. )}
  502. </div>
  503. ),
  504. },
  505. ];
  506. return (
  507. <div className="space-y-4">
  508. {viewMode === 'list' ? (
  509. <>
  510. {/* 挂锁列表 - 搜索栏和表格放在一起 */}
  511. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm overflow-hidden">
  512. {/* 搜索栏 */}
  513. <div className="p-4 lg:p-5 border-b border-gray-200">
  514. <div className="flex items-center justify-between gap-3 lg:gap-4 flex-wrap min-w-0">
  515. {/* 搜索输入框 */}
  516. <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
  517. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  518. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">挂锁名称:</label>
  519. <Input
  520. value={queryParams.lockName || ''}
  521. onChange={(e) => setQueryParams({ ...queryParams, lockName: e.target.value })}
  522. onPressEnter={handleQuery}
  523. placeholder="请输入挂锁名称"
  524. className="min-w-[150px] max-w-[200px]"
  525. allowClear
  526. />
  527. </div>
  528. </div>
  529. {/* 操作按钮组 */}
  530. <Space className="flex-shrink-0">
  531. <PermissionWrapper permission="iscs:lock:query">
  532. <Button
  533. type="primary"
  534. icon={<Search className="w-4 h-4" />}
  535. onClick={handleQuery}
  536. >
  537. 搜索
  538. </Button>
  539. </PermissionWrapper>
  540. <PermissionWrapper permission="iscs:lock:query">
  541. <Button
  542. icon={<RefreshCw className="w-4 h-4" />}
  543. onClick={resetQuery}
  544. >
  545. 重置
  546. </Button>
  547. </PermissionWrapper>
  548. <PermissionWrapper permission="iscs:lock:create">
  549. <Button
  550. type="primary"
  551. icon={<Plus className="w-4 h-4" />}
  552. onClick={() => openPadLockForm('create')}
  553. >
  554. 新增
  555. </Button>
  556. </PermissionWrapper>
  557. <PermissionWrapper permission="iscs:lock:delete">
  558. <Button
  559. danger
  560. icon={<Trash2 className="w-4 h-4" />}
  561. onClick={() => handleDelete()}
  562. disabled={selectedRowKeys.length === 0}
  563. >
  564. 批量删除
  565. </Button>
  566. </PermissionWrapper>
  567. <Button
  568. icon={<Settings className="w-4 h-4" />}
  569. onClick={() => setViewMode('type')}
  570. >
  571. 设置挂锁类型
  572. </Button>
  573. </Space>
  574. </div>
  575. </div>
  576. {/* 表格 */}
  577. <div className="min-w-0">
  578. <Table
  579. columns={padLockColumns}
  580. dataSource={list}
  581. rowKey={(record) => record.lockId || record.id || ''}
  582. loading={loading}
  583. pagination={false}
  584. scroll={{ x: 'max-content' }}
  585. rowSelection={{
  586. selectedRowKeys,
  587. onChange: setSelectedRowKeys,
  588. }}
  589. />
  590. </div>
  591. </div>
  592. {/* 分页 */}
  593. {!loading && list.length > 0 && (
  594. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  595. <div className="flex items-center justify-between">
  596. <div className="text-sm text-gray-600">
  597. 共 <span className="text-blue-600 font-medium">{total}</span> 条记录
  598. </div>
  599. <div className="flex gap-2">
  600. <Button
  601. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
  602. disabled={queryParams.pageNo! <= 1}
  603. >
  604. 上一页
  605. </Button>
  606. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  607. {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
  608. </span>
  609. <Button
  610. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
  611. disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
  612. >
  613. 下一页
  614. </Button>
  615. </div>
  616. </div>
  617. </div>
  618. )}
  619. {/* 挂锁表单弹窗 */}
  620. <PadLockFormModal
  621. visible={showPadLockForm}
  622. editingPadLock={editingPadLock}
  623. onCancel={() => {
  624. setShowPadLockForm(false);
  625. setEditingPadLock(null);
  626. }}
  627. onSave={savePadLock}
  628. />
  629. </>
  630. ) : (
  631. <>
  632. {/* 挂锁类型 - 搜索栏和表格放在一起 */}
  633. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm overflow-hidden">
  634. {/* 搜索栏 */}
  635. <div className="p-5 border-b border-gray-200">
  636. <div className="flex items-center justify-between gap-4 flex-wrap">
  637. {/* 搜索输入框 */}
  638. <div className="flex items-center gap-2 lg:gap-3 flex-wrap flex-1 min-w-0">
  639. <div className="flex items-center gap-2 lg:gap-3 flex-shrink-0">
  640. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">类型名称:</label>
  641. <Input
  642. value={typeQueryParams.lockTypeName || ''}
  643. onChange={(e) => setTypeQueryParams({ ...typeQueryParams, lockTypeName: e.target.value })}
  644. onPressEnter={handleTypeQuery}
  645. placeholder="请输入类型名称"
  646. className="min-w-[150px] max-w-[200px]"
  647. allowClear
  648. />
  649. </div>
  650. </div>
  651. {/* 操作按钮组 */}
  652. <Space className="flex-shrink-0">
  653. <PermissionWrapper permission="iscs:lock:query">
  654. <Button
  655. type="primary"
  656. icon={<Search className="w-4 h-4" />}
  657. onClick={handleTypeQuery}
  658. >
  659. 搜索
  660. </Button>
  661. </PermissionWrapper>
  662. <PermissionWrapper permission="iscs:lock:query">
  663. <Button
  664. icon={<RefreshCw className="w-4 h-4" />}
  665. onClick={resetTypeQuery}
  666. >
  667. 重置
  668. </Button>
  669. </PermissionWrapper>
  670. <PermissionWrapper permission="iscs:lock:create">
  671. <Button
  672. type="primary"
  673. icon={<Plus className="w-4 h-4" />}
  674. onClick={() => openTypeForm('create')}
  675. >
  676. 新增
  677. </Button>
  678. </PermissionWrapper>
  679. <Button
  680. icon={<ArrowUpDown className="w-4 h-4" />}
  681. onClick={toggleExpandAll}
  682. >
  683. 展开/折叠
  684. </Button>
  685. <Button
  686. onClick={() => setViewMode('list')}
  687. >
  688. 返回挂锁列表
  689. </Button>
  690. </Space>
  691. </div>
  692. </div>
  693. {/* 表格 - 树形表格 */}
  694. <div>
  695. <Table
  696. columns={padLockTypeColumns}
  697. dataSource={typeTreeList}
  698. rowKey={(record) => record.lockTypeId || record.id || ''}
  699. loading={typeLoading}
  700. pagination={false}
  701. scroll={{ x: 'max-content' }}
  702. expandable={{
  703. expandedRowKeys,
  704. onExpandedRowsChange: setExpandedRowKeys,
  705. defaultExpandAllRows: isExpandAll,
  706. }}
  707. />
  708. </div>
  709. </div>
  710. {/* 挂锁类型表单弹窗 */}
  711. {showTypeForm && (
  712. <TypeFormModal
  713. visible={showTypeForm}
  714. editingType={editingType}
  715. parentTypeId={parentTypeId}
  716. typeList={typeList}
  717. onCancel={() => {
  718. setShowTypeForm(false);
  719. setEditingType(null);
  720. setParentTypeId(undefined);
  721. }}
  722. onSave={saveType}
  723. />
  724. )}
  725. </>
  726. )}
  727. </div>
  728. );
  729. }
  730. // 挂锁类型表单弹窗组件
  731. interface TypeFormModalProps {
  732. visible: boolean;
  733. editingType: PadLockTypeVO | null;
  734. parentTypeId?: number;
  735. typeList: PadLockTypeVO[];
  736. onCancel: () => void;
  737. onSave: (data: PadLockTypeVO) => void;
  738. }
  739. function TypeFormModal({ visible, editingType, parentTypeId, typeList, onCancel, onSave }: TypeFormModalProps) {
  740. const { t } = useTranslation();
  741. const [form] = Form.useForm();
  742. const [formLoading, setFormLoading] = useState(false);
  743. const [typeTreeOptions, setTypeTreeOptions] = useState<any[]>([]);
  744. // 构建树形选择器数据
  745. useEffect(() => {
  746. if (typeList.length > 0) {
  747. const treeData = handleTree<PadLockTypeVO>(
  748. typeList.map(item => ({ ...item, id: item.lockTypeId || item.id })),
  749. 'id',
  750. 'parentTypeId',
  751. 'children'
  752. );
  753. const buildTreeSelectData = (nodes: PadLockTypeVO[]): any[] => {
  754. return nodes.map(node => ({
  755. title: node.lockTypeName,
  756. value: node.lockTypeId || node.id,
  757. children: node.children ? buildTreeSelectData(node.children) : undefined,
  758. }));
  759. };
  760. setTypeTreeOptions(buildTreeSelectData(treeData));
  761. }
  762. }, [typeList]);
  763. useEffect(() => {
  764. if (visible) {
  765. if (editingType) {
  766. form.setFieldsValue({
  767. lockTypeName: editingType.lockTypeName || '',
  768. parentTypeId: editingType.parentTypeId === 0 ? undefined : editingType.parentTypeId,
  769. lockTypeSpec: editingType.lockTypeSpec || '',
  770. lockTypeIcon: editingType.lockTypeIcon || '',
  771. lockTypeImg: editingType.lockTypeImg || '',
  772. });
  773. } else {
  774. form.setFieldsValue({
  775. lockTypeName: '',
  776. parentTypeId: parentTypeId || undefined,
  777. lockTypeSpec: '',
  778. lockTypeIcon: '',
  779. lockTypeImg: '',
  780. });
  781. }
  782. }
  783. }, [visible, editingType, parentTypeId, form]);
  784. const handleSubmit = async () => {
  785. try {
  786. const values = await form.validateFields();
  787. setFormLoading(true);
  788. // 编辑模式下,合并原始数据和表单数据,确保所有必要字段都被传递
  789. const submitData: PadLockTypeVO = editingType ? {
  790. // 保留原始数据中的字段(这些字段用户不能编辑但需要传递)
  791. ...editingType,
  792. // 用表单数据覆盖可编辑字段
  793. ...values,
  794. // 确保 parentTypeId 存在(如果表单中没有,使用原始数据或默认值 0)
  795. parentTypeId: values.parentTypeId !== undefined ? (values.parentTypeId || 0) : (editingType.parentTypeId || 0),
  796. // 确保 id 和 lockTypeId 都存在
  797. id: editingType.id || editingType.lockTypeId,
  798. lockTypeId: editingType.lockTypeId || editingType.id,
  799. } : {
  800. // 新增模式下,只传递表单数据
  801. ...values,
  802. parentTypeId: values.parentTypeId || 0,
  803. };
  804. onSave(submitData);
  805. } catch (error) {
  806. // 表单验证失败
  807. } finally {
  808. setFormLoading(false);
  809. }
  810. };
  811. return (
  812. <Modal
  813. title={editingType ? '编辑挂锁类型' : '新增挂锁类型'}
  814. open={visible}
  815. onOk={handleSubmit}
  816. onCancel={onCancel}
  817. okText={t('common.confirm')}
  818. cancelText={t('common.cancel')}
  819. confirmLoading={formLoading}
  820. width={600}
  821. >
  822. <Form
  823. form={form}
  824. layout="horizontal"
  825. labelCol={{ span: 6 }}
  826. wrapperCol={{ span: 18 }}
  827. className="mt-4"
  828. >
  829. {editingType?.parentTypeId !== 0 && (
  830. <Form.Item
  831. label="父类型"
  832. name="parentTypeId"
  833. >
  834. <TreeSelect
  835. treeData={typeTreeOptions}
  836. placeholder="选择父级"
  837. allowClear
  838. treeDefaultExpandAll
  839. />
  840. </Form.Item>
  841. )}
  842. <Form.Item
  843. label="挂锁类型名称"
  844. name="lockTypeName"
  845. rules={[{ required: true, message: '请输入挂锁类型名称' }]}
  846. >
  847. <Input placeholder="请输入挂锁类型名称" />
  848. </Form.Item>
  849. <Form.Item
  850. label="挂锁型号"
  851. name="lockTypeSpec"
  852. >
  853. <Input placeholder="请输入挂锁型号" />
  854. </Form.Item>
  855. <Form.Item
  856. label="挂锁类型图标"
  857. name="lockTypeIcon"
  858. >
  859. <UploadImg
  860. value={form.getFieldValue('lockTypeIcon')}
  861. onChange={(value) => form.setFieldsValue({ lockTypeIcon: value })}
  862. limit={1}
  863. height="100px"
  864. width="100px"
  865. />
  866. </Form.Item>
  867. <Form.Item
  868. label="挂锁类型图片"
  869. name="lockTypeImg"
  870. >
  871. <UploadImg
  872. value={form.getFieldValue('lockTypeImg')}
  873. onChange={(value) => form.setFieldsValue({ lockTypeImg: value })}
  874. limit={1}
  875. height="100px"
  876. width="100px"
  877. />
  878. </Form.Item>
  879. </Form>
  880. </Modal>
  881. );
  882. }
  883. // 挂锁表单弹窗组件
  884. interface PadLockFormModalProps {
  885. visible: boolean;
  886. editingPadLock: PadLockVO | null;
  887. onCancel: () => void;
  888. onSave: (data: PadLockVO) => void;
  889. }
  890. function PadLockFormModal({ visible, editingPadLock, onCancel, onSave }: PadLockFormModalProps) {
  891. const { t } = useTranslation();
  892. const [form] = Form.useForm();
  893. const [formLoading, setFormLoading] = useState(false);
  894. const [hardwareOptions, setHardwareOptions] = useState<{ label: string; value: number }[]>([]);
  895. const [lockTypeTreeOptions, setLockTypeTreeOptions] = useState<any[]>([]);
  896. const [statusOptions] = useState(() => {
  897. // 使用默认状态选项
  898. return [
  899. { dictType: 'common_status', label: '启用', value: '1' },
  900. { dictType: 'common_status', label: '禁用', value: '0' },
  901. ];
  902. });
  903. // 获取硬件列表
  904. const getHardwareList = async () => {
  905. try {
  906. const response = await hardwareApi.listHardware({ pageNo: 1, pageSize: -1 });
  907. const options = response.list.map(item => ({
  908. label: item.hardwareName,
  909. value: item.id!,
  910. }));
  911. setHardwareOptions(options);
  912. } catch (error) {
  913. console.error('获取硬件列表失败:', error);
  914. }
  915. };
  916. // 获取挂锁类型列表
  917. const getLockTypeList = async () => {
  918. try {
  919. const response = await padLockTypeApi.listPadLockType({ pageNo: 1, pageSize: -1 });
  920. const flatList = response.list || [];
  921. // 转换为树形结构
  922. const treeData = handleTree<PadLockTypeVO>(
  923. flatList.map(item => ({ ...item, id: item.lockTypeId || item.id })),
  924. 'id',
  925. 'parentTypeId',
  926. 'children'
  927. );
  928. const buildTreeSelectData = (nodes: PadLockTypeVO[]): any[] => {
  929. return nodes.map(node => ({
  930. title: node.lockTypeName,
  931. value: node.lockTypeId || node.id,
  932. children: node.children ? buildTreeSelectData(node.children) : undefined,
  933. }));
  934. };
  935. setLockTypeTreeOptions(buildTreeSelectData(treeData));
  936. } catch (error) {
  937. console.error('获取挂锁类型列表失败:', error);
  938. }
  939. };
  940. useEffect(() => {
  941. if (visible) {
  942. getHardwareList();
  943. getLockTypeList();
  944. if (editingPadLock) {
  945. form.setFieldsValue({
  946. hardwareId: editingPadLock.hardwareId,
  947. lockTypeId: editingPadLock.lockTypeId,
  948. lockName: editingPadLock.lockName || '',
  949. lockNfc: editingPadLock.lockNfc || '',
  950. lockSpec: editingPadLock.lockSpec || '',
  951. exStatus: editingPadLock.exStatus || '1',
  952. exRemark: editingPadLock.exRemark || '',
  953. });
  954. } else {
  955. form.setFieldsValue({
  956. hardwareId: undefined,
  957. lockTypeId: undefined,
  958. lockName: '',
  959. lockNfc: '',
  960. lockSpec: '',
  961. exStatus: '1',
  962. exRemark: '',
  963. });
  964. }
  965. }
  966. }, [visible, editingPadLock, form]);
  967. const handleSubmit = async () => {
  968. try {
  969. const values = await form.validateFields();
  970. setFormLoading(true);
  971. // 编辑模式下,合并原始数据和表单数据,确保所有必要字段都被传递
  972. const submitData: PadLockVO = editingPadLock ? {
  973. // 保留原始数据中的字段(这些字段用户不能编辑但需要传递)
  974. ...editingPadLock,
  975. // 用表单数据覆盖可编辑字段
  976. ...values,
  977. // 确保 id 和 lockId 都存在
  978. id: editingPadLock.id || editingPadLock.lockId,
  979. lockId: editingPadLock.lockId || editingPadLock.id,
  980. } : {
  981. // 新增模式下,只传递表单数据
  982. ...values,
  983. };
  984. onSave(submitData);
  985. } catch (error) {
  986. // 表单验证失败
  987. } finally {
  988. setFormLoading(false);
  989. }
  990. };
  991. return (
  992. <Modal
  993. title={editingPadLock ? '编辑挂锁' : '新增挂锁'}
  994. open={visible}
  995. onOk={handleSubmit}
  996. onCancel={onCancel}
  997. okText={t('common.confirm')}
  998. cancelText={t('common.cancel')}
  999. confirmLoading={formLoading}
  1000. width={600}
  1001. >
  1002. <Form
  1003. form={form}
  1004. layout="horizontal"
  1005. labelCol={{ span: 6 }}
  1006. wrapperCol={{ span: 18 }}
  1007. className="mt-4"
  1008. >
  1009. <Form.Item
  1010. label="所属硬件"
  1011. name="hardwareId"
  1012. >
  1013. <Select
  1014. placeholder="请选择所属硬件"
  1015. allowClear
  1016. options={hardwareOptions}
  1017. />
  1018. </Form.Item>
  1019. <Form.Item
  1020. label="挂锁类型"
  1021. name="lockTypeId"
  1022. >
  1023. <TreeSelect
  1024. treeData={lockTypeTreeOptions}
  1025. placeholder="请选择挂锁类型"
  1026. allowClear
  1027. treeDefaultExpandAll
  1028. />
  1029. </Form.Item>
  1030. <Form.Item
  1031. label="挂锁名称"
  1032. name="lockName"
  1033. rules={[{ required: true, message: '请输入挂锁名称' }]}
  1034. >
  1035. <Input placeholder="请输入挂锁名称" />
  1036. </Form.Item>
  1037. <Form.Item
  1038. label="挂锁NFC"
  1039. name="lockNfc"
  1040. rules={[{ required: true, message: '请输入挂锁NFC' }]}
  1041. >
  1042. <Input placeholder="请输入挂锁NFC" />
  1043. </Form.Item>
  1044. <Form.Item
  1045. label="挂锁型号"
  1046. name="lockSpec"
  1047. >
  1048. <Input placeholder="请输入挂锁型号" />
  1049. </Form.Item>
  1050. <Form.Item
  1051. label="状态"
  1052. name="exStatus"
  1053. >
  1054. <Radio.Group>
  1055. {statusOptions.map(option => (
  1056. <Radio key={option.value} value={option.value}>
  1057. {option.label}
  1058. </Radio>
  1059. ))}
  1060. </Radio.Group>
  1061. </Form.Item>
  1062. <Form.Item
  1063. label="备注"
  1064. name="exRemark"
  1065. >
  1066. <Input.TextArea placeholder="请输入备注" rows={3} />
  1067. </Form.Item>
  1068. </Form>
  1069. </Modal>
  1070. );
  1071. }