SegregationPointManagement.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. import React, { useState, useEffect, useRef, useMemo } from 'react';
  2. import { Plus, Search, RefreshCw, Edit2, Trash2 } from 'lucide-react';
  3. import { segregationPointApi, SegregationPointVO, PageParam } from '../api/spm/index';
  4. import { marsDeptApi, MarsDeptVO } from '../api/marsdept/index';
  5. import { lotoStationApi, LotoStationVO } from '../api/lotoStation/index';
  6. import { technologyApi, TechnologyVO } from '../api/technology/index';
  7. import { toast } from 'sonner';
  8. import { Modal, Table, Input, Button, Select, TreeSelect, Space, Image, Switch } from 'antd';
  9. import { ExclamationCircleOutlined } from '@ant-design/icons';
  10. import type { ColumnsType } from 'antd/es/table';
  11. import { handleTree } from '../utils/tree';
  12. import { getStrDictOptions, DICT_TYPE } from '../utils/dict';
  13. import SegregationPointForm, { SegregationPointFormRef } from './SegregationPointForm';
  14. import { Button as UIButton } from './ui/button';
  15. import PermissionWrapper from './PermissionWrapper';
  16. import { useTranslation } from 'react-i18next';
  17. interface SegregationPointManagementProps {
  18. subMenu?: string;
  19. }
  20. export default function SegregationPointManagement({ subMenu }: SegregationPointManagementProps) {
  21. const { t, i18n } = useTranslation();
  22. // 顶部查询条件:先隐藏(不删除代码),后续需要再打开改为 true 即可
  23. const showAdvancedQueryFilters = false;
  24. const [loading, setLoading] = useState(true);
  25. const [list, setList] = useState<SegregationPointVO[]>([]);
  26. const [total, setTotal] = useState(0);
  27. const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  28. const [queryParams, setQueryParams] = useState<PageParam>({
  29. current: 1,
  30. size: 10,
  31. pointName: undefined,
  32. workstationId: undefined,
  33. machineryId: undefined,
  34. lotoId: undefined,
  35. powerType: undefined,
  36. });
  37. // 下拉选项数据
  38. const [deptOptions, setDeptOptions] = useState<any[]>([]);
  39. const [machineryOptions, setMachineryOptions] = useState<any[]>([]);
  40. const [lotoOptions, setLotoOptions] = useState<Array<{ label: string; value: number }>>([]);
  41. const powerTypeOptions = getStrDictOptions(DICT_TYPE.POWER_TYPE);
  42. const formRef = useRef<SegregationPointFormRef>(null);
  43. // 获取隔离点列表
  44. const getList = async (params?: PageParam) => {
  45. const currentParams = params || queryParams;
  46. setLoading(true);
  47. try {
  48. console.log('SegregationPointManagement: 开始获取隔离点列表', currentParams);
  49. const response = await segregationPointApi.getIsIsolationPointPage(currentParams);
  50. console.log('SegregationPointManagement: API 响应', response);
  51. // 处理响应数据
  52. let data;
  53. if (response && typeof response === 'object') {
  54. // 如果响应有 data 属性,使用 data;否则直接使用 response
  55. if ('data' in response && response.data) {
  56. data = response.data;
  57. } else if ('list' in response || 'total' in response) {
  58. data = response;
  59. } else {
  60. data = response;
  61. }
  62. } else {
  63. data = response;
  64. }
  65. setList(data?.list || []);
  66. setTotal(data?.total || 0);
  67. console.log('SegregationPointManagement: 设置列表数据', { list: data?.list || [], total: data?.total || 0 });
  68. } catch (error: any) {
  69. console.error('SegregationPointManagement: 获取列表失败', error);
  70. toast.error(error.message || t('form.getSegregationPointListFailed'));
  71. } finally {
  72. setLoading(false);
  73. }
  74. };
  75. useEffect(() => {
  76. getList();
  77. // eslint-disable-next-line react-hooks/exhaustive-deps
  78. }, [queryParams.current, queryParams.size]);
  79. // 初始化数据
  80. useEffect(() => {
  81. const initData = async () => {
  82. try {
  83. // 获取岗位数据
  84. const deptRes = await marsDeptApi.listMarsDept({ pageNo: 1, pageSize: -1 });
  85. const deptTreeData = handleTree(deptRes.list, 'id', 'parentId');
  86. setDeptOptions(convertToTreeSelectData(deptTreeData, 'workstationName'));
  87. // 获取锁定站数据
  88. try {
  89. const lotoRes = await lotoStationApi.listLoto({ pageNo: 1, pageSize: -1 });
  90. const data = (lotoRes as any)?.data || lotoRes;
  91. const lotoList = data?.list || [];
  92. setLotoOptions(lotoList.map((item: any) => ({
  93. value: item.id!,
  94. label: item.lotoName,
  95. })));
  96. } catch (lotoError) {
  97. console.error('获取锁定站数据失败:', lotoError);
  98. // 锁定站是非必填的,如果接口不存在或失败,设置为空数组即可
  99. setLotoOptions([]);
  100. }
  101. // 获取设备/工艺数据
  102. const techRes = await technologyApi.listTechnology({ pageNo: 1, pageSize: -1 });
  103. const techData = techRes.list.filter(item => item.machineryType === '工艺');
  104. const techTreeData = handleTree(techData, 'id', 'parentId');
  105. setMachineryOptions(convertToTreeSelectData(techTreeData, 'machineryName'));
  106. } catch (error) {
  107. console.error('初始化数据失败:', error);
  108. }
  109. };
  110. initData();
  111. }, []);
  112. // 转换树形数据为 TreeSelect 格式
  113. const convertToTreeSelectData = (treeData: any[], labelKey: string): any[] => {
  114. return treeData.map(item => ({
  115. title: item[labelKey],
  116. value: item.id,
  117. key: item.id,
  118. children: item.children ? convertToTreeSelectData(item.children, labelKey) : undefined,
  119. }));
  120. };
  121. // 搜索
  122. const handleQuery = () => {
  123. setQueryParams({ ...queryParams, current: 1 });
  124. getList({ ...queryParams, current: 1 });
  125. };
  126. // 重置搜索
  127. const resetQuery = () => {
  128. const resetParams: PageParam = {
  129. current: 1,
  130. size: 10,
  131. pointName: undefined,
  132. workstationId: undefined,
  133. machineryId: undefined,
  134. lotoId: undefined,
  135. powerType: undefined,
  136. };
  137. setQueryParams(resetParams);
  138. getList(resetParams);
  139. };
  140. // 打开表单
  141. const openForm = (type: string, id?: number) => {
  142. formRef.current?.open(type, id);
  143. };
  144. // 删除隔离点
  145. const handleDelete = async (id?: number) => {
  146. const pointIds = id ? [id] : selectedRowKeys.map(key => Number(key));
  147. Modal.confirm({
  148. title: t('common.confirmDelete'),
  149. icon: <ExclamationCircleOutlined />,
  150. content: `${t('common.confirmDeleteText')} ${pointIds.length} ${t('common.records')}?`,
  151. okText: t('common.confirmDelete'),
  152. okType: 'danger',
  153. cancelText: t('common.cancel'),
  154. onOk: async () => {
  155. try {
  156. await segregationPointApi.deleteIsIsolationPointByPointIds(pointIds);
  157. toast.success(t('common.deleteSuccess'));
  158. setSelectedRowKeys([]);
  159. await getList();
  160. } catch (error: any) {
  161. toast.error(error.message || t('common.deleteFailed'));
  162. }
  163. },
  164. });
  165. };
  166. // 表格列配置
  167. const columns: ColumnsType<SegregationPointVO> = useMemo(() => [
  168. {
  169. title: t('table.segregationPointId'),
  170. dataIndex: 'id',
  171. width: 100,
  172. align: 'center',
  173. },
  174. {
  175. title: t('table.segregationPointName'),
  176. dataIndex: 'pointName',
  177. align: 'center',
  178. },
  179. {
  180. title: t('table.icon'),
  181. dataIndex: 'pointIcon',
  182. width: 100,
  183. align: 'center',
  184. render: (text: string) => {
  185. if (text) {
  186. return <Image src={text} width={50} height={50} style={{ objectFit: 'cover' }} />;
  187. }
  188. return '-';
  189. },
  190. },
  191. {
  192. title: t('table.switchStatus'),
  193. dataIndex: 'switchStatus',
  194. width: 100,
  195. align: 'center',
  196. render: (status: number | string | null) => {
  197. if (status === null || status === undefined) {
  198. return '-';
  199. }
  200. const isChecked = String(status) === '1';
  201. return (
  202. <Switch
  203. checked={isChecked}
  204. checkedChildren="ON"
  205. unCheckedChildren="OFF"
  206. disabled
  207. style={{ pointerEvents: 'none' }}
  208. />
  209. );
  210. },
  211. },
  212. {
  213. title: t('table.segregationPointNfc'),
  214. dataIndex: 'pointNfc',
  215. align: 'center',
  216. },
  217. {
  218. title: t('table.position'),
  219. dataIndex: 'workstationName',
  220. align: 'center',
  221. },
  222. {
  223. title: t('table.deviceProcess'),
  224. dataIndex: 'machineryName',
  225. width: 180,
  226. align: 'center',
  227. },
  228. {
  229. title: t('table.lotoStation'),
  230. dataIndex: 'lotoName',
  231. width: 120,
  232. align: 'center',
  233. },
  234. {
  235. title: t('table.segregationPointSerial'),
  236. dataIndex: 'pointSerialNumber',
  237. width: 120,
  238. align: 'center',
  239. },
  240. {
  241. title: t('table.function'),
  242. dataIndex: 'remark',
  243. align: 'center',
  244. },
  245. {
  246. title: t('table.image'),
  247. dataIndex: 'pointPicture',
  248. width: 100,
  249. align: 'center',
  250. render: (text: string) => {
  251. if (text) {
  252. return <Image src={text} width={50} height={50} style={{ objectFit: 'cover' }} />;
  253. }
  254. return '-';
  255. },
  256. },
  257. {
  258. title: t('table.energySource'),
  259. dataIndex: 'powerType',
  260. align: 'center',
  261. render: (value: string) => {
  262. const option = powerTypeOptions.find(opt => opt.value === value);
  263. return option ? option.label : value || '-';
  264. },
  265. },
  266. {
  267. title: t('table.operation'),
  268. width: 150,
  269. align: 'center',
  270. fixed: 'right',
  271. render: (_: any, record: SegregationPointVO) => (
  272. <div className="flex items-center gap-2 justify-center">
  273. <PermissionWrapper permission="iscs:point:update">
  274. <UIButton
  275. variant="ghost"
  276. size="sm"
  277. onClick={() => openForm('update', record.pointId)}
  278. className="h-8 px-2 transition-colors hover:underline"
  279. style={{ color: '#000000' }}
  280. onMouseEnter={(e) => {
  281. e.currentTarget.style.color = '#1677ff';
  282. e.currentTarget.style.textDecoration = 'underline';
  283. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  284. }}
  285. onMouseLeave={(e) => {
  286. e.currentTarget.style.color = '#000000';
  287. e.currentTarget.style.textDecoration = 'none';
  288. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  289. }}
  290. >
  291. <Edit2 className="w-4 h-4" style={{ color: '#000000' }} />
  292. <span className="ml-1">{t('common.edit')}</span>
  293. </UIButton>
  294. </PermissionWrapper>
  295. <PermissionWrapper permission="iscs:point:delete">
  296. <UIButton
  297. variant="ghost"
  298. size="sm"
  299. onClick={() => handleDelete(record.pointId)}
  300. className="h-8 px-2 transition-colors hover:underline"
  301. style={{ color: '#000000' }}
  302. onMouseEnter={(e) => {
  303. e.currentTarget.style.color = '#1677ff';
  304. e.currentTarget.style.textDecoration = 'underline';
  305. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #1677ff');
  306. }}
  307. onMouseLeave={(e) => {
  308. e.currentTarget.style.color = '#000000';
  309. e.currentTarget.style.textDecoration = 'none';
  310. e.currentTarget.querySelector('svg')?.setAttribute('style', 'color: #000000');
  311. }}
  312. >
  313. <Trash2 className="w-4 h-4" style={{ color: '#000000' }} />
  314. <span className="ml-1">{t('common.delete')}</span>
  315. </UIButton>
  316. </PermissionWrapper>
  317. </div>
  318. ),
  319. },
  320. ], [t, i18n.language, powerTypeOptions]);
  321. return (
  322. <div className="p-6 space-y-4">
  323. {/* 搜索栏 */}
  324. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
  325. <div className="flex flex-col gap-4">
  326. {/* 搜索字段 + 操作按钮:同一行,按钮靠右 */}
  327. <div className="flex items-center justify-between gap-3 flex-wrap">
  328. <div className="flex items-center gap-3 flex-wrap">
  329. <div className="flex items-center gap-2">
  330. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.segregationPointName')}:</label>
  331. <Input
  332. value={queryParams.pointName}
  333. onChange={(e) => setQueryParams({ ...queryParams, pointName: e.target.value })}
  334. onPressEnter={handleQuery}
  335. placeholder={t('form.segregationPointNamePlaceholder')}
  336. className="w-[180px]"
  337. allowClear
  338. />
  339. </div>
  340. {showAdvancedQueryFilters && (
  341. <>
  342. <div className="flex items-center gap-2">
  343. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.workstation')}:</label>
  344. <TreeSelect
  345. value={queryParams.workstationId}
  346. onChange={(value) => setQueryParams({ ...queryParams, workstationId: value })}
  347. treeData={deptOptions}
  348. placeholder={t('form.workstationPlaceholder')}
  349. allowClear
  350. className="w-[180px]"
  351. />
  352. </div>
  353. <div className="flex items-center gap-2">
  354. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.deviceProcess')}:</label>
  355. <TreeSelect
  356. value={queryParams.machineryId}
  357. onChange={(value) => setQueryParams({ ...queryParams, machineryId: value })}
  358. treeData={machineryOptions}
  359. placeholder={t('form.deviceProcessPlaceholder')}
  360. allowClear
  361. className="w-[180px]"
  362. />
  363. </div>
  364. <div className="flex items-center gap-2">
  365. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.lotoStation')}:</label>
  366. <Select
  367. value={queryParams.lotoId}
  368. onChange={(value) => setQueryParams({ ...queryParams, lotoId: value })}
  369. placeholder={t('form.lotoStationPlaceholder')}
  370. allowClear
  371. className="w-[180px]"
  372. options={lotoOptions}
  373. />
  374. </div>
  375. <div className="flex items-center gap-2">
  376. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">{t('form.energySource')}:</label>
  377. <Select
  378. value={queryParams.powerType}
  379. onChange={(value) => setQueryParams({ ...queryParams, powerType: value })}
  380. placeholder={t('form.energySourcePlaceholder')}
  381. allowClear
  382. className="w-[180px]"
  383. options={powerTypeOptions}
  384. />
  385. </div>
  386. </>
  387. )}
  388. </div>
  389. <div className="flex justify-end">
  390. <Space>
  391. <PermissionWrapper permission="iscs:point:query">
  392. <Button
  393. type="primary"
  394. icon={<Search />}
  395. onClick={handleQuery}
  396. >
  397. {t('common.search')}
  398. </Button>
  399. </PermissionWrapper>
  400. <PermissionWrapper permission="iscs:point:query">
  401. <Button
  402. icon={<RefreshCw />}
  403. onClick={resetQuery}
  404. >
  405. {t('common.reset')}
  406. </Button>
  407. </PermissionWrapper>
  408. <PermissionWrapper permission="iscs:point:create">
  409. <Button
  410. type="primary"
  411. icon={<Plus />}
  412. onClick={() => openForm('create')}
  413. >
  414. {t('common.addNew')}
  415. </Button>
  416. </PermissionWrapper>
  417. <PermissionWrapper permission="iscs:point:delete">
  418. <Button
  419. danger
  420. icon={<Trash2 className="w-4 h-4" />}
  421. disabled={selectedRowKeys.length === 0}
  422. onClick={() => handleDelete()}
  423. >
  424. {t('common.batchDelete')}
  425. </Button>
  426. </PermissionWrapper>
  427. </Space>
  428. </div>
  429. </div>
  430. </div>
  431. </div>
  432. {/* 表格 */}
  433. <div className="bg-white rounded-lg border border-gray-200">
  434. <Table
  435. columns={columns}
  436. dataSource={list}
  437. rowKey={(record, index) => {
  438. // 优先使用 pointId,如果没有则使用 id,最后使用索引
  439. const key = record.pointId ?? record.id ?? `row-${index}`;
  440. return key;
  441. }}
  442. loading={loading}
  443. pagination={false}
  444. scroll={{ x: 'max-content' }}
  445. rowSelection={{
  446. selectedRowKeys,
  447. onChange: (keys) => {
  448. setSelectedRowKeys(keys);
  449. },
  450. onSelect: (record, selected) => {
  451. const recordKey = record.pointId ?? record.id;
  452. if (!recordKey) return;
  453. if (selected) {
  454. // 选中时,只添加当前行的 key
  455. setSelectedRowKeys(prev => {
  456. if (prev.includes(recordKey)) {
  457. return prev;
  458. }
  459. return [...prev, recordKey];
  460. });
  461. } else {
  462. // 取消选中时,只移除当前行的 key
  463. setSelectedRowKeys(prev => prev.filter(key => key !== recordKey));
  464. }
  465. },
  466. onSelectAll: (selected, selectedRows, changeRows) => {
  467. if (selected) {
  468. // 全选时,只选中当前页的数据
  469. const currentPageKeys = list
  470. .map(item => item.pointId ?? item.id)
  471. .filter((id): id is number => id !== undefined && id !== null);
  472. setSelectedRowKeys(prev => {
  473. const newKeys = new Set(prev);
  474. currentPageKeys.forEach(key => newKeys.add(key));
  475. return Array.from(newKeys);
  476. });
  477. } else {
  478. // 取消全选时,只取消当前页的选中
  479. const currentPageKeys = list
  480. .map(item => item.pointId ?? item.id)
  481. .filter((id): id is number => id !== undefined && id !== null);
  482. setSelectedRowKeys(prev => prev.filter(key => !currentPageKeys.includes(key)));
  483. }
  484. },
  485. getCheckboxProps: (record) => ({
  486. name: record.pointName,
  487. }),
  488. }}
  489. />
  490. </div>
  491. {/* 分页 */}
  492. {!loading && list.length > 0 && (
  493. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  494. <div className="flex items-center justify-between">
  495. <div className="text-sm text-gray-600">
  496. {t('common.total')} <span className="text-blue-600 font-medium">{total}</span> {t('common.records')}
  497. </div>
  498. <div className="flex gap-2">
  499. <Button
  500. onClick={() => setQueryParams({ ...queryParams, current: (queryParams.current || 1) - 1 })}
  501. disabled={(queryParams.current || 1) <= 1}
  502. >
  503. {t('common.prevPage')}
  504. </Button>
  505. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  506. {queryParams.current || 1} / {Math.ceil(total / (queryParams.size || 10)) || 1}
  507. </span>
  508. <Button
  509. onClick={() => setQueryParams({ ...queryParams, current: (queryParams.current || 1) + 1 })}
  510. disabled={(queryParams.current || 1) >= Math.ceil(total / (queryParams.size || 10))}
  511. >
  512. {t('common.nextPage')}
  513. </Button>
  514. </div>
  515. </div>
  516. </div>
  517. )}
  518. {/* 表单弹窗 */}
  519. <SegregationPointForm ref={formRef} onSuccess={getList} />
  520. </div>
  521. );
  522. }