index.tsx 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367
  1. /*
  2. * @Author: code4eat awesomedema@gmail.com
  3. * @Date: 2023-03-03 11:30:33
  4. * @LastEditors: code4eat awesomedema@gmail.com
  5. * @LastEditTime: 2025-12-15 11:18:35
  6. * @FilePath: /KC-MiddlePlatform/src/pages/platform/setting/pubDicTypeMana/index.tsx
  7. * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  8. */
  9. import React, { useEffect, useRef, useState } from 'react';
  10. import {
  11. Input,
  12. message,
  13. Card,
  14. Button,
  15. Modal,
  16. Tabs,
  17. Radio,
  18. Popconfirm,
  19. Dropdown,
  20. Menu,
  21. Checkbox,
  22. } from 'antd';
  23. import { ActionType, ProColumns } from '@ant-design/pro-table';
  24. import KCIMPagecontainer from '@/components/KCIMPageContainer';
  25. import { KCIMTable } from '@/components/KCIMTable';
  26. import {
  27. ModalForm,
  28. ProFormText,
  29. ProFormTextArea,
  30. ProFormSelect,
  31. } from '@ant-design/pro-form';
  32. import { PlusOutlined, SearchOutlined, MoreOutlined } from '@ant-design/icons';
  33. import { createFromIconfontCN } from '@ant-design/icons';
  34. import './style.less';
  35. import {
  36. getChackGroupData,
  37. addData,
  38. editData,
  39. delData,
  40. getCheckGroupEmployees,
  41. addCheckGroupEmployee,
  42. delCheckGroupEmployee,
  43. getAvailableEmployees,
  44. setGroupManager,
  45. getCheckGroupCheckpoints,
  46. getGroupPendingCheckpoints,
  47. addCheckGroupCheckpoints,
  48. delCheckGroupCheckpoints,
  49. } from './service';
  50. const IconFont = createFromIconfontCN({
  51. scriptUrl: '',
  52. });
  53. // 添加查核要点弹窗组件
  54. const AddCheckpointModal: React.FC<{
  55. selectedGroup: any;
  56. onSuccess: () => void;
  57. }> = ({ selectedGroup, onSuccess }) => {
  58. const [modalVisible, setModalVisible] = useState(false);
  59. const [selectedCheckpointIds, setSelectedCheckpointIds] = useState<
  60. React.Key[]
  61. >([]);
  62. const [searchKeyword, setSearchKeyword] = useState('');
  63. const [allCheckpointSelectData, setAllCheckpointSelectData] = useState<any[]>(
  64. [],
  65. );
  66. const checkpointSelectTableRef = useRef<ActionType>();
  67. // 获取可添加的查核要点列表
  68. const getCheckpointSelectData = async (params: any) => {
  69. if (!selectedGroup?.id) {
  70. return {
  71. data: [],
  72. success: true,
  73. total: 0,
  74. };
  75. }
  76. try {
  77. const { current = 1, pageSize = 10 } = params;
  78. const res = await getGroupPendingCheckpoints(
  79. selectedGroup.id,
  80. searchKeyword,
  81. current,
  82. pageSize,
  83. );
  84. // 保存所有数据,用于后续选择时获取完整信息
  85. if (current === 1) {
  86. setAllCheckpointSelectData(res.list || []);
  87. } else {
  88. setAllCheckpointSelectData((prev) => [...prev, ...(res.list || [])]);
  89. }
  90. return {
  91. data: res.list || [],
  92. success: res.success,
  93. total: res.total || 0,
  94. };
  95. } catch (error) {
  96. return {
  97. data: [],
  98. success: false,
  99. total: 0,
  100. };
  101. }
  102. };
  103. // 打开弹窗时重置状态
  104. const handleOpenModal = () => {
  105. setModalVisible(true);
  106. setSelectedCheckpointIds([]);
  107. setSearchKeyword('');
  108. setAllCheckpointSelectData([]);
  109. // 延迟触发表格重新加载
  110. setTimeout(() => {
  111. checkpointSelectTableRef.current?.reload();
  112. }, 100);
  113. };
  114. // 提交添加
  115. const handleSubmit = async () => {
  116. if (selectedCheckpointIds.length === 0) {
  117. message.warning('请至少选择一个查核要点');
  118. return;
  119. }
  120. try {
  121. // 直接按“要点ID”提交(与接口分页口径一致)
  122. const selectedIds = Array.from(
  123. new Set(
  124. selectedCheckpointIds
  125. .map((k) => Number(k))
  126. .filter((n) => !Number.isNaN(n)),
  127. ),
  128. ) as number[];
  129. const result = await addCheckGroupCheckpoints(
  130. selectedGroup.id,
  131. selectedIds,
  132. );
  133. // 检查返回值,如果是false说明业务失败
  134. if (result === false) {
  135. return; // 错误消息已在响应拦截器中显示,直接返回
  136. }
  137. message.success(`已添加 ${selectedIds.length} 个查核要点`);
  138. setModalVisible(false);
  139. onSuccess(); // 调用成功回调
  140. } catch (error) {
  141. message.error('添加失败');
  142. }
  143. };
  144. // 定义表格列
  145. const checkpointSelectColumns: ProColumns[] = [
  146. {
  147. title: '要点名称',
  148. dataIndex: 'name',
  149. width: 260,
  150. align: 'left' as 'left',
  151. },
  152. {
  153. title: '查核项数',
  154. dataIndex: 'checkItemList',
  155. width: 100,
  156. align: 'center' as 'center',
  157. render: (_: any, record: any) =>
  158. Array.isArray(record?.checkItemList) ? record.checkItemList.length : 0,
  159. },
  160. ];
  161. return (
  162. <>
  163. <Button
  164. type="primary"
  165. className="add-btn"
  166. size="small"
  167. onClick={handleOpenModal}
  168. >
  169. 添加
  170. </Button>
  171. <Modal
  172. title={`选择查核要点(${selectedGroup?.name || ''})`}
  173. open={modalVisible}
  174. onCancel={() => setModalVisible(false)}
  175. width={700}
  176. footer={[
  177. <Button key="cancel" onClick={() => setModalVisible(false)}>
  178. 取消
  179. </Button>,
  180. <Button
  181. key="submit"
  182. type="primary"
  183. onClick={handleSubmit}
  184. disabled={selectedCheckpointIds.length === 0}
  185. style={{
  186. backgroundColor: '#3377FF',
  187. borderColor: '#3377FF',
  188. color: '#FFFFFF',
  189. }}
  190. >
  191. 确定
  192. {selectedCheckpointIds.length > 0
  193. ? ` (${selectedCheckpointIds.length})`
  194. : ''}
  195. </Button>,
  196. ]}
  197. className="checkpoint-select-modal"
  198. >
  199. <div style={{ marginBottom: 8 }}>
  200. <Input
  201. placeholder="要点名称"
  202. value={searchKeyword}
  203. allowClear
  204. onChange={(e) => {
  205. setSearchKeyword(e.target.value);
  206. if (e.target.value === '') {
  207. // 清空搜索时自动刷新
  208. checkpointSelectTableRef.current?.reload();
  209. }
  210. }}
  211. onPressEnter={() => checkpointSelectTableRef.current?.reload()}
  212. suffix={
  213. <SearchOutlined
  214. onClick={() => checkpointSelectTableRef.current?.reload()}
  215. style={{ cursor: 'pointer', color: '#99A6BF' }}
  216. />
  217. }
  218. style={{ width: '100%' }}
  219. />
  220. </div>
  221. <KCIMTable
  222. actionRef={checkpointSelectTableRef}
  223. columns={checkpointSelectColumns}
  224. request={getCheckpointSelectData}
  225. rowKey="id"
  226. bordered
  227. search={false}
  228. pagination={{
  229. simple: true,
  230. pageSize: 10,
  231. size: 'small',
  232. showTotal: () => null,
  233. }}
  234. rowSelection={{
  235. selectedRowKeys: selectedCheckpointIds,
  236. onChange: (selectedRowKeys) => {
  237. setSelectedCheckpointIds(selectedRowKeys);
  238. },
  239. preserveSelectedRowKeys: true,
  240. }}
  241. tableAlertRender={false}
  242. scroll={{ y: 360 }}
  243. expandable={{
  244. expandedRowRender: (record: any) => (
  245. <div style={{ paddingLeft: 24 }}>
  246. {Array.isArray(record?.checkItemList) &&
  247. record.checkItemList.length > 0 ? (
  248. <ul style={{ margin: 0, paddingLeft: 16 }}>
  249. {record.checkItemList.map((ci: any) => (
  250. <li key={ci.id} style={{ lineHeight: '22px' }}>
  251. {ci.name}
  252. </li>
  253. ))}
  254. </ul>
  255. ) : (
  256. <span>无明细</span>
  257. )}
  258. </div>
  259. ),
  260. }}
  261. />
  262. </Modal>
  263. </>
  264. );
  265. };
  266. // 添加人员弹窗组件
  267. const AddEmployeeModal: React.FC<{
  268. selectedGroup: any;
  269. onSuccess: () => void;
  270. }> = ({ selectedGroup, onSuccess }) => {
  271. const [modalVisible, setModalVisible] = useState(false);
  272. const [selectedEmployeeIds, setSelectedEmployeeIds] = useState<React.Key[]>(
  273. [],
  274. );
  275. const [selectedEmployeeData, setSelectedEmployeeData] = useState<any[]>([]);
  276. const [searchKeyword, setSearchKeyword] = useState('');
  277. const [allEmployeeData, setAllEmployeeData] = useState<any[]>([]);
  278. const employeeTableRef = useRef<ActionType>();
  279. // 获取可添加的人员列表
  280. const getEmployeeSelectData = async (params: any) => {
  281. if (!selectedGroup?.id) {
  282. return {
  283. data: [],
  284. success: true,
  285. total: 0,
  286. };
  287. }
  288. try {
  289. const { current = 1, pageSize = 10 } = params;
  290. const res = await getAvailableEmployees(
  291. selectedGroup.id,
  292. searchKeyword,
  293. current,
  294. pageSize,
  295. );
  296. // 保存所有数据,用于后续选择时获取完整信息
  297. if (current === 1) {
  298. setAllEmployeeData(res.list || []);
  299. } else {
  300. setAllEmployeeData((prev) => [...prev, ...(res.list || [])]);
  301. }
  302. return {
  303. data: res.list || [],
  304. success: res.success,
  305. total: res.total || 0,
  306. };
  307. } catch (error) {
  308. return {
  309. data: [],
  310. success: false,
  311. total: 0,
  312. };
  313. }
  314. };
  315. // 打开弹窗时重置状态
  316. const handleOpenModal = () => {
  317. setModalVisible(true);
  318. setSelectedEmployeeIds([]);
  319. setSelectedEmployeeData([]);
  320. setSearchKeyword('');
  321. setAllEmployeeData([]);
  322. // 延迟触发表格重新加载
  323. setTimeout(() => {
  324. employeeTableRef.current?.reload();
  325. }, 100);
  326. };
  327. // 提交添加
  328. const handleSubmit = async () => {
  329. if (selectedEmployeeIds.length === 0) {
  330. message.warning('请至少选择一个人员');
  331. return;
  332. }
  333. try {
  334. // 根据选中的ID获取完整的人员信息
  335. const selectedEmployees = allEmployeeData.filter((emp) =>
  336. selectedEmployeeIds.includes(emp.id),
  337. );
  338. const result = await addCheckGroupEmployee(
  339. selectedGroup.id,
  340. selectedEmployees,
  341. );
  342. // 检查返回值,如果是false说明业务失败
  343. if (result === false) {
  344. return; // 错误消息已在响应拦截器中显示,直接返回
  345. }
  346. message.success(`已添加 ${selectedEmployeeIds.length} 个人员`);
  347. setModalVisible(false);
  348. onSuccess(); // 调用成功回调
  349. } catch (error) {
  350. message.error('添加失败');
  351. }
  352. };
  353. // 定义表格列
  354. const employeeSelectColumns: ProColumns[] = [
  355. {
  356. title: '工号',
  357. dataIndex: 'code',
  358. width: 120,
  359. },
  360. {
  361. title: '姓名',
  362. dataIndex: 'name',
  363. width: 120,
  364. },
  365. ];
  366. return (
  367. <>
  368. <Button
  369. type="primary"
  370. className="add-btn"
  371. size="small"
  372. onClick={handleOpenModal}
  373. >
  374. 添加
  375. </Button>
  376. <Modal
  377. title={`选择人员(${selectedGroup?.name || ''})`}
  378. open={modalVisible}
  379. onCancel={() => setModalVisible(false)}
  380. width={352}
  381. footer={[
  382. <Button key="cancel" onClick={() => setModalVisible(false)}>
  383. 取消
  384. </Button>,
  385. <Button
  386. key="submit"
  387. type="primary"
  388. onClick={handleSubmit}
  389. disabled={selectedEmployeeIds.length === 0}
  390. style={{
  391. backgroundColor: '#3377FF',
  392. borderColor: '#3377FF',
  393. color: '#FFFFFF',
  394. }}
  395. >
  396. 确定
  397. {selectedEmployeeIds.length > 0
  398. ? ` (${selectedEmployeeIds.length})`
  399. : ''}
  400. </Button>,
  401. ]}
  402. className="employee-select-modal"
  403. >
  404. <div style={{ marginBottom: 8 }}>
  405. <Input
  406. placeholder="请输入姓名或工号"
  407. value={searchKeyword}
  408. allowClear
  409. onChange={(e) => {
  410. setSearchKeyword(e.target.value);
  411. if (e.target.value === '') {
  412. // 清空搜索时自动刷新
  413. employeeTableRef.current?.reload();
  414. }
  415. }}
  416. onPressEnter={() => employeeTableRef.current?.reload()}
  417. suffix={
  418. <SearchOutlined
  419. onClick={() => employeeTableRef.current?.reload()}
  420. style={{ cursor: 'pointer', color: '#99A6BF' }}
  421. />
  422. }
  423. style={{ width: '100%' }}
  424. />
  425. </div>
  426. <KCIMTable
  427. actionRef={employeeTableRef}
  428. columns={employeeSelectColumns}
  429. request={getEmployeeSelectData}
  430. rowKey="id"
  431. search={false}
  432. pagination={{
  433. simple: true,
  434. pageSize: 10,
  435. size: 'small',
  436. showTotal: () => null,
  437. }}
  438. rowSelection={{
  439. selectedRowKeys: selectedEmployeeIds,
  440. onChange: (selectedRowKeys) => {
  441. setSelectedEmployeeIds(selectedRowKeys);
  442. },
  443. }}
  444. tableAlertRender={false}
  445. scroll={{ y: 360 }}
  446. />
  447. </Modal>
  448. </>
  449. );
  450. };
  451. // 主页面组件
  452. const CheckGroupMana: React.FC = () => {
  453. // 左侧查核组数据
  454. const [groupData, setGroupData] = useState<any[]>([]);
  455. const [filteredGroupData, setFilteredGroupData] = useState<any[]>([]);
  456. const [selectedGroup, setSelectedGroup] = useState<any>(null);
  457. const [groupSearchKeywords, setGroupSearchKeywords] = useState<string>('');
  458. const [hoveredNode, setHoveredNode] = useState<string | null>(null);
  459. const [openDropdownNode, setOpenDropdownNode] = useState<string | null>(null);
  460. // 分页加载相关状态
  461. const [currentPage, setCurrentPage] = useState(1);
  462. const [totalCount, setTotalCount] = useState(0);
  463. const [loading, setLoading] = useState(false);
  464. const [hasMore, setHasMore] = useState(true);
  465. // 右侧人员表格相关
  466. const [employeeSearchKeywords, setEmployeeSearchKeywords] =
  467. useState<string>('');
  468. const [selectedEmployeeKeys, setSelectedEmployeeKeys] = useState<string[]>(
  469. [],
  470. );
  471. const [allEmployeeList, setAllEmployeeList] = useState<any[]>([]); // 存储原始组员数据
  472. const [filteredEmployeeList, setFilteredEmployeeList] = useState<any[]>([]); // 存储过滤后的组员数据
  473. const employeeTableRef = useRef<ActionType>();
  474. // 查核要点相关状态
  475. const [checkpointSearchKeywords, setCheckpointSearchKeywords] =
  476. useState<string>('');
  477. const [selectedCheckpointKeys, setSelectedCheckpointKeys] = useState<
  478. string[]
  479. >([]);
  480. const [allCheckpointList, setAllCheckpointList] = useState<any[]>([]); // 存储原始查核要点数据
  481. const [filteredCheckpointList, setFilteredCheckpointList] = useState<any[]>(
  482. [],
  483. ); // 存储过滤后的查核要点数据
  484. const checkpointTableRef = useRef<ActionType>();
  485. // 右侧内容区ref和操作栏定位状态
  486. const deptContentRef = useRef<HTMLDivElement>(null);
  487. const [actionBarStyle, setActionBarStyle] = useState<{
  488. left: number;
  489. width: number;
  490. }>({ left: 0, width: 0 });
  491. // 更新操作栏定位,支持ResizeObserver
  492. const updateActionBarPosition = () => {
  493. if (deptContentRef.current) {
  494. const rect = deptContentRef.current.getBoundingClientRect();
  495. setActionBarStyle({ left: rect.left, width: rect.width });
  496. }
  497. };
  498. useEffect(() => {
  499. updateActionBarPosition();
  500. window.addEventListener('resize', updateActionBarPosition);
  501. window.addEventListener('scroll', updateActionBarPosition, true);
  502. let resizeObserver: ResizeObserver | null = null;
  503. if (deptContentRef.current && typeof ResizeObserver !== 'undefined') {
  504. resizeObserver = new ResizeObserver(() => {
  505. updateActionBarPosition();
  506. });
  507. resizeObserver.observe(deptContentRef.current);
  508. }
  509. return () => {
  510. window.removeEventListener('resize', updateActionBarPosition);
  511. window.removeEventListener('scroll', updateActionBarPosition, true);
  512. if (resizeObserver && deptContentRef.current) {
  513. resizeObserver.unobserve(deptContentRef.current);
  514. resizeObserver.disconnect();
  515. }
  516. };
  517. }, []);
  518. // 获取左侧查核组数据
  519. useEffect(() => {
  520. fetchGroupData();
  521. }, []);
  522. const fetchGroupData = async (
  523. page: number = 1,
  524. isLoadMore: boolean = false,
  525. keyword?: string,
  526. ) => {
  527. try {
  528. setLoading(true);
  529. // 如果传入了keyword参数,使用它;否则使用状态中的groupSearchKeywords
  530. const searchKeyword =
  531. keyword !== undefined ? keyword : groupSearchKeywords;
  532. const res = await getChackGroupData({
  533. current: page,
  534. pageSize: 100,
  535. groupName: searchKeyword,
  536. });
  537. if (res) {
  538. // 转换数据格式,符合树形组件需要的格式,同时保留原始数据
  539. const transformedData = res.list.map((item: any) => ({
  540. title: item.name,
  541. key: item.id,
  542. type: 'checkGroup', // 标记为查核组
  543. // 保留原始数据
  544. id: item.id,
  545. name: item.name,
  546. groupManagerName: item.groupManagerName,
  547. remark: item.remark,
  548. createUserName: item.createUserName,
  549. }));
  550. if (isLoadMore) {
  551. // 加载更多时,追加到现有数据
  552. setGroupData((prev) => [...prev, ...transformedData]);
  553. setFilteredGroupData((prev) => [...prev, ...transformedData]);
  554. } else {
  555. // 首次加载或搜索时,替换数据
  556. setGroupData(transformedData);
  557. setFilteredGroupData(transformedData);
  558. // 默认选中第一个
  559. if (transformedData.length > 0) {
  560. setSelectedGroup(transformedData[0]);
  561. }
  562. }
  563. // 更新分页信息
  564. setTotalCount(res.totalCount);
  565. setCurrentPage(page);
  566. setHasMore(res.list.length === 100 && page * 100 < res.totalCount);
  567. }
  568. } catch (error) {
  569. message.error('获取查核组数据失败');
  570. } finally {
  571. setLoading(false);
  572. }
  573. };
  574. // 本地过滤组员数据
  575. const filterEmployeeData = (data: any[], keyword: string) => {
  576. if (!keyword.trim()) {
  577. return data;
  578. }
  579. return data.filter(
  580. (emp) =>
  581. emp.employeeId?.toString().includes(keyword) ||
  582. emp.employeeName?.includes(keyword),
  583. );
  584. };
  585. // 监听搜索关键词变化,进行本地过滤
  586. useEffect(() => {
  587. const filtered = filterEmployeeData(
  588. allEmployeeList,
  589. employeeSearchKeywords,
  590. );
  591. setFilteredEmployeeList(filtered);
  592. }, [allEmployeeList, employeeSearchKeywords]);
  593. // 监听选中项变化,加载组员数据和查核要点数据
  594. useEffect(() => {
  595. if (selectedGroup?.key) {
  596. loadEmployeeData();
  597. loadCheckpointData();
  598. } else {
  599. setAllEmployeeList([]);
  600. setFilteredEmployeeList([]);
  601. setAllCheckpointList([]);
  602. setFilteredCheckpointList([]);
  603. }
  604. }, [selectedGroup?.key]);
  605. // 加载组员数据
  606. const loadEmployeeData = async () => {
  607. if (!selectedGroup?.id) return;
  608. try {
  609. const result = await getCheckGroupEmployees({
  610. groupId: selectedGroup.id,
  611. });
  612. if (result.success) {
  613. setAllEmployeeList(result.data || []);
  614. }
  615. } catch (error) {
  616. console.error('加载组员数据失败:', error);
  617. setAllEmployeeList([]);
  618. }
  619. };
  620. // 本地过滤查核要点数据
  621. const filterCheckpointData = (data: any[], keyword: string) => {
  622. if (!keyword.trim()) {
  623. return data;
  624. }
  625. return data.filter(
  626. (item) =>
  627. item.pointName?.includes(keyword) ||
  628. item.checkItemName?.includes(keyword),
  629. );
  630. };
  631. // 监听查核要点搜索关键词变化,进行本地过滤
  632. useEffect(() => {
  633. const filtered = filterCheckpointData(
  634. allCheckpointList,
  635. checkpointSearchKeywords,
  636. );
  637. setFilteredCheckpointList(filtered);
  638. }, [allCheckpointList, checkpointSearchKeywords]);
  639. // 加载查核要点数据
  640. const loadCheckpointData = async () => {
  641. if (!selectedGroup?.id) return;
  642. try {
  643. const result = await getCheckGroupCheckpoints({
  644. groupId: selectedGroup.id,
  645. });
  646. if (result.success) {
  647. setAllCheckpointList(result.data || []);
  648. }
  649. } catch (error) {
  650. console.error('加载查核要点数据失败:', error);
  651. setAllCheckpointList([]);
  652. }
  653. };
  654. // 处理查核组搜索 - 点击搜索图标或回车时触发
  655. const handleGroupSearch = () => {
  656. setCurrentPage(1);
  657. setHasMore(true);
  658. fetchGroupData(1, false);
  659. };
  660. // 处理搜索输入框的变化
  661. const handleSearchInputChange = (value: string) => {
  662. setGroupSearchKeywords(value);
  663. // 如果清空搜索,立即重新加载数据,传入空字符串确保清空搜索
  664. if (!value.trim()) {
  665. setCurrentPage(1);
  666. setHasMore(true);
  667. // 直接传入空字符串,而不是依赖状态更新
  668. fetchGroupData(1, false, '');
  669. }
  670. };
  671. // 加载更多数据
  672. const loadMoreData = () => {
  673. if (!loading && hasMore) {
  674. const nextPage = currentPage + 1;
  675. fetchGroupData(nextPage, true);
  676. }
  677. };
  678. // 滚动事件处理
  679. const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
  680. const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
  681. // 当滚动到底部附近时加载更多数据
  682. if (scrollHeight - scrollTop <= clientHeight + 10) {
  683. loadMoreData();
  684. }
  685. };
  686. // 新增查核组弹窗组件
  687. const AddCheckGroupModal = () => {
  688. return (
  689. <ModalForm
  690. title="新增查核组"
  691. width={400}
  692. trigger={<Button icon={<PlusOutlined />} className="add-button" />}
  693. onFinish={async (val: any) => {
  694. try {
  695. await addData(val);
  696. message.success('新增成功!');
  697. fetchGroupData(1, false); // 重新获取数据
  698. return true;
  699. } catch (error) {
  700. message.error('新增失败');
  701. return false;
  702. }
  703. }}
  704. modalProps={{
  705. destroyOnClose: true,
  706. maskClosable: false,
  707. }}
  708. >
  709. <ProFormText
  710. name="name"
  711. label="查核组"
  712. placeholder="请输入"
  713. rules={[{ required: true, message: '查核组不能为空!' }]}
  714. />
  715. </ModalForm>
  716. );
  717. };
  718. // 更多操作菜单
  719. const getMoreMenu = (node: any) => (
  720. <Menu>
  721. <Menu.Item key="edit">
  722. <ModalForm
  723. title="编辑查核组"
  724. width={400}
  725. initialValues={{
  726. name: node.name,
  727. }}
  728. trigger={<span>编辑</span>}
  729. onFinish={async (val: any) => {
  730. try {
  731. await editData({
  732. id: node.id,
  733. name: val.name,
  734. });
  735. message.success('编辑成功!');
  736. fetchGroupData(1, false);
  737. return true;
  738. } catch (error) {
  739. message.error('编辑失败');
  740. return false;
  741. }
  742. }}
  743. modalProps={{ destroyOnClose: true }}
  744. >
  745. <ProFormText
  746. name="name"
  747. label="查核组"
  748. placeholder="请输入"
  749. rules={[{ required: true, message: '查核组不能为空!' }]}
  750. />
  751. </ModalForm>
  752. </Menu.Item>
  753. <Menu.Item key="delete">
  754. <a
  755. onClick={() => {
  756. // 删除确认
  757. Modal.confirm({
  758. title: '确认删除',
  759. content: `确定要删除查核组"${node.name}"吗?`,
  760. okText: '确定',
  761. cancelText: '取消',
  762. onOk: async () => {
  763. try {
  764. await delData(node.id);
  765. message.success('删除成功!');
  766. fetchGroupData(1, false);
  767. // 如果删除的是当前选中的组,清空选中状态
  768. if (selectedGroup?.id === node.id) {
  769. setSelectedGroup(null);
  770. }
  771. } catch (error) {
  772. message.error('删除失败');
  773. }
  774. },
  775. });
  776. }}
  777. >
  778. 删除
  779. </a>
  780. </Menu.Item>
  781. </Menu>
  782. );
  783. // 左侧查核组树形组件渲染
  784. const renderGroupTree = () => {
  785. return (
  786. <div className="dept-tree">
  787. <div className="search-wrapper">
  788. <Input
  789. placeholder="查核组"
  790. suffix={
  791. <SearchOutlined
  792. style={{ color: '#99A6BF', cursor: 'pointer' }}
  793. onClick={handleGroupSearch}
  794. />
  795. }
  796. className="search-input"
  797. value={groupSearchKeywords}
  798. onChange={(e) => handleSearchInputChange(e.target.value)}
  799. onPressEnter={handleGroupSearch}
  800. allowClear
  801. />
  802. <AddCheckGroupModal />
  803. </div>
  804. <div className="dept-list" onScroll={handleScroll}>
  805. {filteredGroupData.map((item) => (
  806. <div key={item.key} className="dept-group">
  807. <div
  808. className={`dept-leaf ${
  809. selectedGroup?.key === item.key ? 'selected' : ''
  810. }`}
  811. onClick={() => {
  812. setSelectedGroup(item);
  813. }}
  814. onMouseEnter={() => setHoveredNode(item.key)}
  815. onMouseLeave={() => setHoveredNode(null)}
  816. >
  817. <span>{item.title}</span>
  818. {(selectedGroup?.key === item.key ||
  819. hoveredNode === item.key ||
  820. openDropdownNode === item.key) && (
  821. <Dropdown
  822. overlay={getMoreMenu(item)}
  823. trigger={['click']}
  824. placement="bottomRight"
  825. overlayClassName="check-unit-add-dropdown"
  826. onVisibleChange={(visible) => {
  827. if (visible) {
  828. setOpenDropdownNode(item.key);
  829. } else {
  830. setOpenDropdownNode(null);
  831. }
  832. }}
  833. >
  834. <Button
  835. type="text"
  836. icon={<MoreOutlined />}
  837. className="more-button"
  838. onClick={(e) => e.stopPropagation()}
  839. />
  840. </Dropdown>
  841. )}
  842. </div>
  843. </div>
  844. ))}
  845. {/* 加载更多提示 */}
  846. {loading && (
  847. <div className="loading-more">
  848. <span>加载中...</span>
  849. </div>
  850. )}
  851. {!hasMore && filteredGroupData.length > 0 && (
  852. <div className="no-more-data">
  853. <span>已加载全部数据(共{totalCount}条)</span>
  854. </div>
  855. )}
  856. </div>
  857. </div>
  858. );
  859. };
  860. // 设置组长
  861. const handleSetManager = async (employeeId: number) => {
  862. if (!selectedGroup?.id) return;
  863. try {
  864. const result = await setGroupManager(selectedGroup.id, employeeId);
  865. // 检查返回值,如果是false说明业务失败
  866. if (result === false) {
  867. return; // 错误消息已在响应拦截器中显示,直接返回
  868. }
  869. message.success('设置组长成功!');
  870. // 只需要刷新组员表格数据
  871. loadEmployeeData();
  872. } catch (error) {
  873. message.error('设置组长失败');
  874. }
  875. };
  876. // 处理删除查核要点
  877. const handleDeleteCheckpoint = async (record: any) => {
  878. try {
  879. const result = await delCheckGroupCheckpoints([record.originalPointId]);
  880. // 检查返回值,如果是false说明业务失败
  881. if (result === false) {
  882. return; // 错误消息已在响应拦截器中显示,直接返回
  883. }
  884. message.success('删除要点成功');
  885. loadCheckpointData();
  886. } catch (error) {
  887. message.error('删除要点失败');
  888. }
  889. };
  890. // 计算全选状态
  891. const getAllSelectableKeys = () => {
  892. return filteredCheckpointList
  893. .filter((item) => item.rowSpan > 0) // 只获取合并行的ID
  894. .map((item) => item.id);
  895. };
  896. const allSelectableKeys = getAllSelectableKeys();
  897. const isAllSelected =
  898. allSelectableKeys.length > 0 &&
  899. allSelectableKeys.every((key) => selectedCheckpointKeys.includes(key));
  900. const isIndeterminate =
  901. selectedCheckpointKeys.length > 0 &&
  902. selectedCheckpointKeys.length < allSelectableKeys.length;
  903. // 处理全选/取消全选
  904. const handleSelectAll = (checked: boolean) => {
  905. if (checked) {
  906. setSelectedCheckpointKeys(allSelectableKeys);
  907. } else {
  908. setSelectedCheckpointKeys([]);
  909. }
  910. };
  911. // 查核要点表格列定义
  912. const checkpointColumns: ProColumns[] = [
  913. {
  914. title: (
  915. <div style={{ display: 'flex', alignItems: 'center' }}>
  916. <Checkbox
  917. checked={isAllSelected}
  918. indeterminate={isIndeterminate}
  919. onChange={(e) => handleSelectAll(e.target.checked)}
  920. />
  921. <span style={{ marginLeft: 8 }}>要点名称</span>
  922. </div>
  923. ),
  924. dataIndex: 'pointName',
  925. width: 200,
  926. align: 'left' as 'left',
  927. render: (dom: React.ReactNode, record: any) => {
  928. if (record.rowSpan > 0) {
  929. return {
  930. children: (
  931. <div style={{ display: 'flex', alignItems: 'center' }}>
  932. <Checkbox
  933. checked={selectedCheckpointKeys.includes(record.id)}
  934. onChange={() => {
  935. if (selectedCheckpointKeys.includes(record.id)) {
  936. setSelectedCheckpointKeys(
  937. selectedCheckpointKeys.filter((k) => k !== record.id),
  938. );
  939. } else {
  940. setSelectedCheckpointKeys([
  941. ...selectedCheckpointKeys,
  942. record.id,
  943. ]);
  944. }
  945. }}
  946. />
  947. <span style={{ marginLeft: 8 }}>{dom}</span>
  948. </div>
  949. ),
  950. props: { rowSpan: record.rowSpan },
  951. };
  952. }
  953. return { children: null, props: { rowSpan: 0 } };
  954. },
  955. },
  956. {
  957. title: '查核项名称',
  958. dataIndex: 'checkItemName',
  959. align: 'left' as 'left',
  960. },
  961. {
  962. title: '操作',
  963. dataIndex: 'option',
  964. width: 80,
  965. align: 'center' as 'center',
  966. render: (_: any, record: any) => {
  967. if (record.rowSpan > 0) {
  968. return {
  969. children: (
  970. <Popconfirm
  971. title="确认删除该要点及其所有查核项?"
  972. okText="确定"
  973. cancelText="取消"
  974. onConfirm={() => handleDeleteCheckpoint(record)}
  975. >
  976. <a style={{ color: '#3377ff' }}>删除</a>
  977. </Popconfirm>
  978. ),
  979. props: { rowSpan: record.rowSpan },
  980. };
  981. }
  982. return { children: null, props: { rowSpan: 0 } };
  983. },
  984. },
  985. ];
  986. // 人员表格列定义
  987. const employeeColumns: ProColumns[] = [
  988. {
  989. title: '工号',
  990. dataIndex: 'employeeCode',
  991. width: 120,
  992. },
  993. {
  994. title: '姓名',
  995. dataIndex: 'employeeName',
  996. width: 120,
  997. },
  998. {
  999. title: '组长',
  1000. dataIndex: 'isManager',
  1001. width: 100,
  1002. render: (text: any, record: any) => (
  1003. <Radio
  1004. checked={text === 1}
  1005. onChange={() => handleSetManager(record.employeeId)}
  1006. />
  1007. ),
  1008. },
  1009. {
  1010. title: '操作',
  1011. key: 'option',
  1012. width: 100,
  1013. valueType: 'option',
  1014. render: (_: any, record: any) => [
  1015. <a
  1016. key="delete"
  1017. style={{ color: '#3377FF' }}
  1018. onClick={async () => {
  1019. Modal.confirm({
  1020. title: '确认移除该人员吗?',
  1021. onOk: async () => {
  1022. try {
  1023. const result = await delCheckGroupEmployee([record.id]);
  1024. // 检查返回值,如果是false说明业务失败
  1025. if (result === false) {
  1026. return; // 错误消息已在响应拦截器中显示,直接返回
  1027. }
  1028. message.success('移除成功!');
  1029. loadEmployeeData();
  1030. } catch (error) {
  1031. message.error('移除失败');
  1032. }
  1033. },
  1034. okText: '确定',
  1035. cancelText: '取消',
  1036. });
  1037. }}
  1038. >
  1039. 删除
  1040. </a>,
  1041. ],
  1042. },
  1043. ];
  1044. return (
  1045. <KCIMPagecontainer title={false}>
  1046. <div className="check-unit-mana">
  1047. <div className="check-unit-container">
  1048. {/* 左侧查核组树 */}
  1049. {renderGroupTree()}
  1050. {/* 右侧内容区 */}
  1051. <div className="dept-content" ref={deptContentRef}>
  1052. {selectedGroup && (
  1053. <Card className="pfm-ant-card" bordered={false}>
  1054. <Tabs defaultActiveKey="members">
  1055. <Tabs.TabPane tab="组员" key="members">
  1056. <div className="tab-content">
  1057. <div className="toolbar">
  1058. <div
  1059. style={{
  1060. display: 'flex',
  1061. alignItems: 'center',
  1062. gap: 8,
  1063. }}
  1064. >
  1065. <span style={{ color: '#17181A', fontSize: 14 }}>
  1066. 检索:
  1067. </span>
  1068. <Input
  1069. placeholder="工号、姓名"
  1070. allowClear
  1071. suffix={
  1072. <SearchOutlined style={{ color: '#99A6BF' }} />
  1073. }
  1074. style={{ width: 250 }}
  1075. value={employeeSearchKeywords}
  1076. onChange={(e) => {
  1077. setEmployeeSearchKeywords(e.target.value);
  1078. }}
  1079. />
  1080. </div>
  1081. <AddEmployeeModal
  1082. selectedGroup={selectedGroup}
  1083. onSuccess={() => loadEmployeeData()}
  1084. />
  1085. </div>
  1086. <KCIMTable
  1087. columns={employeeColumns as ProColumns[]}
  1088. actionRef={employeeTableRef}
  1089. rowKey="id"
  1090. dataSource={filteredEmployeeList}
  1091. search={false}
  1092. options={false}
  1093. pagination={false}
  1094. scroll={{ y: 'calc(100vh - 232px)' }}
  1095. rowSelection={{
  1096. selectedRowKeys: selectedEmployeeKeys,
  1097. onChange: (keys) =>
  1098. setSelectedEmployeeKeys(keys as string[]),
  1099. }}
  1100. tableAlertRender={false}
  1101. />
  1102. </div>
  1103. </Tabs.TabPane>
  1104. <Tabs.TabPane tab="查核要点" key="checkpoints">
  1105. <div className="tab-content">
  1106. <div className="toolbar">
  1107. <div
  1108. style={{
  1109. display: 'flex',
  1110. alignItems: 'center',
  1111. gap: 8,
  1112. }}
  1113. >
  1114. <span style={{ color: '#17181A', fontSize: 14 }}>
  1115. 检索:
  1116. </span>
  1117. <Input
  1118. placeholder="要点名称"
  1119. allowClear
  1120. suffix={
  1121. <SearchOutlined style={{ color: '#99A6BF' }} />
  1122. }
  1123. style={{ width: 250 }}
  1124. value={checkpointSearchKeywords}
  1125. onChange={(e) => {
  1126. setCheckpointSearchKeywords(e.target.value);
  1127. }}
  1128. />
  1129. </div>
  1130. <AddCheckpointModal
  1131. selectedGroup={selectedGroup}
  1132. onSuccess={() => loadCheckpointData()}
  1133. />
  1134. </div>
  1135. <KCIMTable
  1136. columns={checkpointColumns as ProColumns[]}
  1137. actionRef={checkpointTableRef}
  1138. rowKey="id"
  1139. dataSource={filteredCheckpointList}
  1140. search={false}
  1141. options={false}
  1142. pagination={false}
  1143. bordered
  1144. scroll={{ y: 'calc(100vh - 236px)' }}
  1145. tableAlertRender={false}
  1146. />
  1147. </div>
  1148. </Tabs.TabPane>
  1149. </Tabs>
  1150. </Card>
  1151. )}
  1152. {!selectedGroup && (
  1153. <div className="empty-state">
  1154. <div className="empty-content">
  1155. <div className="empty-text">请选择左侧查核组查看详情</div>
  1156. </div>
  1157. </div>
  1158. )}
  1159. </div>
  1160. </div>
  1161. </div>
  1162. {/* 批量操作栏 - 固定在页面底部,宽度跟dept-content一致 */}
  1163. {(selectedEmployeeKeys.length > 0 ||
  1164. selectedCheckpointKeys.length > 0) && (
  1165. <div
  1166. style={{
  1167. position: 'fixed',
  1168. left: actionBarStyle.left + 16,
  1169. width: actionBarStyle.width - 16,
  1170. bottom: 16,
  1171. height: 56,
  1172. backgroundColor: '#fff',
  1173. zIndex: 1000,
  1174. boxShadow: 'rgba(0, 0, 0, 0.04) 0px -2px 8px',
  1175. borderRadius: 4,
  1176. display: 'flex',
  1177. alignItems: 'center',
  1178. justifyContent: 'space-between',
  1179. padding: '0 16px',
  1180. transition: 'left 0.2s, width 0.2s',
  1181. }}
  1182. >
  1183. <span style={{ color: '#3377FF', fontWeight: 500 }}>
  1184. 已选 {selectedEmployeeKeys.length + selectedCheckpointKeys.length}{' '}
  1185. </span>
  1186. <div style={{ display: 'flex', gap: 8 }}>
  1187. <Button
  1188. onClick={() => {
  1189. setSelectedEmployeeKeys([]);
  1190. setSelectedCheckpointKeys([]);
  1191. }}
  1192. style={{ height: 32, borderRadius: 4, fontSize: 14 }}
  1193. size="small"
  1194. >
  1195. 取消选择
  1196. </Button>
  1197. {selectedEmployeeKeys.length > 0 && (
  1198. <Popconfirm
  1199. title="确认批量移除所选人员?"
  1200. okText="确定"
  1201. cancelText="取消"
  1202. onConfirm={async () => {
  1203. try {
  1204. const result = await delCheckGroupEmployee(
  1205. selectedEmployeeKeys,
  1206. );
  1207. // 检查返回值,如果是false说明业务失败
  1208. if (result === false) {
  1209. return; // 错误消息已在响应拦截器中显示,直接返回
  1210. }
  1211. message.success('批量移除成功');
  1212. setSelectedEmployeeKeys([]);
  1213. loadEmployeeData();
  1214. } catch (e) {
  1215. message.error('批量移除失败');
  1216. }
  1217. }}
  1218. >
  1219. <Button
  1220. type="primary"
  1221. style={{
  1222. background: '#3377FF',
  1223. borderColor: '#3377FF',
  1224. height: 32,
  1225. borderRadius: 4,
  1226. fontSize: 14,
  1227. }}
  1228. size="small"
  1229. >
  1230. 批量移除人员
  1231. </Button>
  1232. </Popconfirm>
  1233. )}
  1234. {selectedCheckpointKeys.length > 0 && (
  1235. <Popconfirm
  1236. title="确认批量删除所选查核要点?"
  1237. okText="确定"
  1238. cancelText="取消"
  1239. onConfirm={async () => {
  1240. try {
  1241. // 从选中的ID中提取原始要点ID(去重)
  1242. const originalIds = Array.from(
  1243. new Set(
  1244. selectedCheckpointKeys
  1245. .map((id) => {
  1246. const record = filteredCheckpointList.find(
  1247. (item: any) => item.id === id,
  1248. );
  1249. return record?.originalPointId;
  1250. })
  1251. .filter((id) => id !== undefined),
  1252. ),
  1253. );
  1254. const result = await delCheckGroupCheckpoints(originalIds);
  1255. // 检查返回值,如果是false说明业务失败
  1256. if (result === false) {
  1257. return; // 错误消息已在响应拦截器中显示,直接返回
  1258. }
  1259. message.success('批量删除成功');
  1260. setSelectedCheckpointKeys([]);
  1261. loadCheckpointData();
  1262. } catch (e) {
  1263. message.error('批量删除失败');
  1264. }
  1265. }}
  1266. >
  1267. <Button
  1268. type="primary"
  1269. style={{
  1270. background: '#3377FF',
  1271. borderColor: '#3377FF',
  1272. height: 32,
  1273. borderRadius: 4,
  1274. fontSize: 14,
  1275. }}
  1276. size="small"
  1277. >
  1278. 批量删除要点
  1279. </Button>
  1280. </Popconfirm>
  1281. )}
  1282. </div>
  1283. </div>
  1284. )}
  1285. </KCIMPagecontainer>
  1286. );
  1287. };
  1288. export default CheckGroupMana;