index.tsx 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498
  1. /*
  2. * @Author: code4eat awesomedema@gmail.com
  3. * @Date: 2025-06-10 12:15:25
  4. * @LastEditors: code4eat awesomedema@gmail.com
  5. * @LastEditTime: 2025-07-25 16:09:17
  6. * @FilePath: /pfmBackMana/src/pages/checkUnitMana/index.tsx
  7. * @Description: 查核单位管理
  8. */
  9. import {
  10. createFromIconfontCN,
  11. SearchOutlined,
  12. PlusOutlined,
  13. MoreOutlined,
  14. InboxOutlined,
  15. } from '@ant-design/icons';
  16. import {
  17. ActionType,
  18. ProColumns,
  19. ModalForm,
  20. ProFormText,
  21. ProFormRadio,
  22. ProFormUploadDragger,
  23. ProFormDigit,
  24. } from '@ant-design/pro-components';
  25. import {
  26. Button,
  27. Input,
  28. message,
  29. Tabs,
  30. Radio,
  31. Empty,
  32. Card,
  33. Dropdown,
  34. Menu,
  35. Modal,
  36. Select,
  37. Checkbox,
  38. Pagination,
  39. Popconfirm,
  40. Tooltip,
  41. } from 'antd';
  42. import React, { useRef, useState, useEffect } from 'react';
  43. import { request } from 'umi';
  44. import {
  45. getDeptClassList,
  46. addDeptClass,
  47. addDepartmentByClass,
  48. getDeptClassTree,
  49. getDeptClassFilterConditionList,
  50. applyDeptClassFilterCondition,
  51. updateDeptClass,
  52. delDeptClass,
  53. updateDepartmentByClass,
  54. delDepartmentByClass,
  55. getDeptEmployee,
  56. getDeptPendingEmployee,
  57. addDeptEmployee,
  58. setDeptManager,
  59. delDeptEmp,
  60. getDeptCheckpoint,
  61. getDeptPendingCheckpoint,
  62. addDeptCheckpoint,
  63. delDeptCheckpoint,
  64. } from './service';
  65. import './style.less';
  66. import KCIMPagecontainer from '@/components/KCIMPageContainer';
  67. import { KCIMTable } from '@/components/KCIMTable';
  68. const IconFont = createFromIconfontCN({
  69. scriptUrl: '',
  70. });
  71. const { TabPane } = Tabs;
  72. // 下载模板方法
  73. const downloadTemplate = async (type: number) => {
  74. try {
  75. // 获取token(如有)
  76. let token = '';
  77. const userDataStr = localStorage.getItem('userData');
  78. if (userDataStr) {
  79. try {
  80. token = JSON.parse(userDataStr).token || '';
  81. } catch (e) {
  82. token = '';
  83. }
  84. }
  85. const fetchOptions: any = { method: 'GET' };
  86. if (token) {
  87. fetchOptions.headers = { token };
  88. }
  89. const res = await fetch(`/gateway/manager/Department/exportPfmKnowledgeTemp?type=${type}`, fetchOptions);
  90. if (!res.ok) {
  91. message.error('下载模板失败');
  92. return;
  93. }
  94. const blob = await res.blob();
  95. // 尝试从header获取文件名
  96. let fileName = '';
  97. const disposition = res.headers.get('content-disposition');
  98. if (disposition && disposition.indexOf('filename=') !== -1) {
  99. fileName = decodeURIComponent(disposition.split('filename=')[1].replace(/"/g, ''));
  100. } else {
  101. fileName = type === 1 ? '系统追踪模板.xlsx' : '自查督查模板.xlsx';
  102. }
  103. const url = window.URL.createObjectURL(blob);
  104. const a = document.createElement('a');
  105. a.href = url;
  106. a.download = fileName;
  107. document.body.appendChild(a);
  108. a.click();
  109. a.remove();
  110. window.URL.revokeObjectURL(url);
  111. } catch (e) {
  112. message.error('下载模板失败');
  113. }
  114. };
  115. export default function CheckUnitMana() {
  116. // 状态定义
  117. const [selectedDept, setSelectedDept] = useState<any>(null);
  118. const [deptInfo, setDeptInfo] = useState<string>('');
  119. const [searchKeywords, setSearchKeywords] = useState<string>('');
  120. const [personnelSearchInput, setPersonnelSearchInput] = useState<string>(''); // 人员搜索输入框的值
  121. const [activeTab, setActiveTab] = useState('personnel');
  122. const tableRef = useRef<ActionType>();
  123. const conditionTableRef = useRef<ActionType>();
  124. const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
  125. const [treeData, setTreeData] = useState<any[]>([]);
  126. const [treeSearchKeywords, setTreeSearchKeywords] = useState<string>(''); // 树形搜索关键词
  127. const [filteredTreeData, setFilteredTreeData] = useState<any[]>([]); // 过滤后的树形数据
  128. const [hoveredNode, setHoveredNode] = useState<string | null>(null); // 当前悬停的节点
  129. const [openDropdownNode, setOpenDropdownNode] = useState<string | null>(null); // 当前打开下拉菜单的节点
  130. // 查核条件弹窗相关状态
  131. const [conditionModalVisible, setConditionModalVisible] = useState(false); // 条件弹窗显示状态
  132. const [conditionSearchType, setConditionSearchType] = useState('个案'); // 条件搜索类型
  133. const [conditionSearchKeyword, setConditionSearchKeyword] = useState(''); // 条件搜索关键词
  134. const [selectedConditions, setSelectedConditions] = useState<string[]>([]); // 已选择的条件ID列表
  135. const [currentNode, setCurrentNode] = useState<any>(null); // 当前操作的节点
  136. // 右侧内容区ref和操作栏定位状态
  137. const cardRef = useRef<HTMLDivElement>(null);
  138. const [actionBarStyle, setActionBarStyle] = useState<{
  139. left: number;
  140. width: number;
  141. }>({ left: 0, width: 0 });
  142. // 1. 新增弹窗相关状态
  143. const [checkPointModalVisible, setCheckPointModalVisible] = useState(false);
  144. const [checkPointTableRef, setCheckPointTableRef] = useState<any>(null);
  145. const [selectedCheckPointKeys, setSelectedCheckPointKeys] = useState<
  146. string[]
  147. >([]);
  148. // 记录当前页数据用于全选逻辑
  149. const [currentCheckPointPageData, setCurrentCheckPointPageData] = useState<
  150. any[]
  151. >([]);
  152. // 获取当前页面所有可选的要点ID(用于全选逻辑)
  153. const getCurrentPageCheckPointIds = () => {
  154. return currentCheckPointPageData
  155. .filter((item) => item.rowSpan > 0) // 只选择有rowSpan的行(要点行)
  156. .map((item) => item.id);
  157. };
  158. // 更新操作栏定位,支持ResizeObserver
  159. const updateActionBarPosition = () => {
  160. if (cardRef.current) {
  161. const rect = cardRef.current.getBoundingClientRect();
  162. setActionBarStyle({ left: rect.left, width: rect.width });
  163. }
  164. };
  165. useEffect(() => {
  166. updateActionBarPosition();
  167. window.addEventListener('resize', updateActionBarPosition);
  168. window.addEventListener('scroll', updateActionBarPosition, true);
  169. let resizeObserver: ResizeObserver | null = null;
  170. if (cardRef.current && typeof ResizeObserver !== 'undefined') {
  171. resizeObserver = new ResizeObserver(() => {
  172. updateActionBarPosition();
  173. });
  174. resizeObserver.observe(cardRef.current);
  175. }
  176. return () => {
  177. window.removeEventListener('resize', updateActionBarPosition);
  178. window.removeEventListener('scroll', updateActionBarPosition, true);
  179. if (resizeObserver && cardRef.current) {
  180. resizeObserver.unobserve(cardRef.current);
  181. resizeObserver.disconnect();
  182. }
  183. };
  184. }, []);
  185. // 获取单位分类树
  186. const fetchDeptClassList = async () => {
  187. try {
  188. const res = await getDeptClassTree();
  189. // 尝试多种数据结构格式
  190. let dataArray = null;
  191. if (res && res.data && Array.isArray(res.data)) {
  192. // 格式1: { data: [...] }
  193. dataArray = res.data;
  194. } else if (Array.isArray(res)) {
  195. // 格式2: 直接返回数组
  196. dataArray = res;
  197. } else if (res && Array.isArray(res.items)) {
  198. // 格式3: { items: [...] }
  199. dataArray = res.items;
  200. }
  201. if (dataArray && dataArray.length > 0) {
  202. const transformedData = dataArray.map((item: any) => {
  203. // 处理子节点数据 - 只处理children字段,不处理filterConditionList
  204. let children = [];
  205. // 只使用 children 字段作为子节点
  206. if (item.children && Array.isArray(item.children)) {
  207. children = item.children.map((child: any) => ({
  208. title: child.name,
  209. key: child.id || child.code,
  210. type: child.type || 'department', // 标记为查核单位
  211. visitType: child.visitType,
  212. deptType: child.deptType ?? 0, // 确保有默认值
  213. sort: child.sort,
  214. filterConditionList: child.filterConditionList || [], // 保留查核条件
  215. }));
  216. }
  217. // 如果没有children,说明这是叶子节点,不需要处理filterConditionList作为子节点
  218. return {
  219. title: item.name,
  220. key: item.id,
  221. type: item.type, // 类型:1单位分类 2查核单位
  222. visitType: item.visitType, // 诊别:1门诊 2急诊
  223. deptType: item.deptType ?? 0, // 单位类型:0普通单位 1职能科室,确保有默认值
  224. sort: item.sort,
  225. children: children,
  226. filterConditionList: item.filterConditionList || [], // 保留查核条件
  227. };
  228. });
  229. setTreeData(transformedData);
  230. setFilteredTreeData(transformedData); // 初始化过滤数据
  231. // 默认展开所有父节点
  232. setExpandedKeys(transformedData.map((item: any) => item.key));
  233. // 默认选中逻辑:选择第一个叶子节点(没有子节点的节点本身就是叶子节点)
  234. if (transformedData.length > 0) {
  235. let defaultSelected = null;
  236. // 遍历所有节点,找到第一个叶子节点
  237. for (const parent of transformedData) {
  238. if (parent.children && parent.children.length > 0) {
  239. // 如果有子节点,选择第一个子节点
  240. const firstChild = parent.children[0];
  241. defaultSelected = {
  242. ...firstChild,
  243. parentTitle: parent.title,
  244. filterConditionList: firstChild.filterConditionList || [],
  245. };
  246. break;
  247. } else {
  248. // 如果没有子节点,说明它本身就是叶子节点,直接选中
  249. defaultSelected = {
  250. ...parent,
  251. parentTitle: '单位分类',
  252. filterConditionList: parent.filterConditionList || [],
  253. };
  254. break;
  255. }
  256. }
  257. setSelectedDept(defaultSelected);
  258. }
  259. } else {
  260. message.error('获取单位分类树失败');
  261. }
  262. } catch (error) {
  263. console.error('接口调用异常:', error);
  264. message.error('获取单位分类树失败');
  265. }
  266. };
  267. useEffect(() => {
  268. fetchDeptClassList();
  269. }, []);
  270. // 监听selectedDept变化,加载人员数据
  271. useEffect(() => {
  272. if (selectedDept?.key) {
  273. if (activeTab === 'personnel') {
  274. tableRef.current?.reload();
  275. } else if (activeTab === 'checkPoints') {
  276. checkPointsTableRef.current?.reload();
  277. }
  278. }
  279. }, [selectedDept?.key, activeTab]);
  280. // 树形数据搜索过滤函数
  281. const filterTreeData = (data: any[], keywords: string) => {
  282. if (!keywords.trim()) {
  283. return data;
  284. }
  285. const filtered = data
  286. .map((parent) => {
  287. // 检查父节点是否匹配
  288. const parentMatches = parent.title
  289. .toLowerCase()
  290. .includes(keywords.toLowerCase());
  291. // 过滤子节点
  292. const filteredChildren = parent.children.filter((child: any) =>
  293. child.title.toLowerCase().includes(keywords.toLowerCase()),
  294. );
  295. // 如果父节点匹配或有匹配的子节点,则保留该父节点
  296. if (parentMatches || filteredChildren.length > 0) {
  297. return {
  298. ...parent,
  299. children: parentMatches ? parent.children : filteredChildren, // 如果父节点匹配,显示所有子节点;否则只显示匹配的子节点
  300. };
  301. }
  302. return null;
  303. })
  304. .filter(Boolean); // 移除 null 值
  305. return filtered;
  306. };
  307. // 处理树形搜索
  308. const handleTreeSearch = (value: string) => {
  309. setTreeSearchKeywords(value);
  310. const filtered = filterTreeData(treeData, value);
  311. setFilteredTreeData(filtered);
  312. // 如果有搜索关键词,自动展开所有匹配的父节点
  313. if (value.trim()) {
  314. const expandKeys = filtered.map((item: any) => item.key);
  315. setExpandedKeys(expandKeys);
  316. } else {
  317. // 如果清空搜索,恢复默认展开状态
  318. setExpandedKeys(treeData.map((item: any) => item.key));
  319. }
  320. };
  321. // 获取人员数据
  322. const getPersonnelData = async (params: any) => {
  323. try {
  324. // 如果没有选中的部门,返回空数据
  325. if (!selectedDept?.key) {
  326. return {
  327. data: [],
  328. success: true,
  329. total: 0,
  330. };
  331. }
  332. // 调用真实API获取人员列表(不传filter参数,获取全部数据)
  333. const res = await getDeptEmployee({
  334. deptId: Number(selectedDept.key), // 当前选中的查核单位ID
  335. filter: '', // 不传搜索关键词,获取全部数据
  336. deptType: selectedDept.deptType ?? 0, // 单位类型,使用空值合并操作符,默认为0
  337. });
  338. // 尝试多种数据结构格式
  339. let dataArray: any[] | null = null;
  340. if (res) {
  341. if (Array.isArray(res.data)) {
  342. dataArray = res.data; // { data: [...] }
  343. } else if (Array.isArray(res.items)) {
  344. dataArray = res.items; // { items: [...] }
  345. } else if (res.data && Array.isArray(res.data.items)) {
  346. dataArray = res.data.items; // { data: { items: [...] } }
  347. } else if (Array.isArray(res)) {
  348. dataArray = res; // 直接返回数组
  349. }
  350. }
  351. if (dataArray && dataArray.length >= 0) {
  352. // 保留接口字段,不做映射,仅把 id 转成字符串,避免 rowKey 警告
  353. let formattedData = dataArray.map((item: any) => ({
  354. ...item,
  355. id: String(item.id),
  356. }));
  357. // 前端本地搜索过滤
  358. if (searchKeywords && searchKeywords.trim()) {
  359. formattedData = formattedData.filter((item: any) =>
  360. (item.empCode && item.empCode.includes(searchKeywords)) ||
  361. (item.empName && item.empName.includes(searchKeywords))
  362. );
  363. }
  364. return {
  365. data: formattedData,
  366. success: true,
  367. total: formattedData.length,
  368. };
  369. }
  370. return {
  371. data: [],
  372. success: true,
  373. total: 0,
  374. };
  375. } catch (error) {
  376. console.error('获取人员列表失败:', error);
  377. message.error('获取人员列表失败');
  378. return {
  379. data: [],
  380. success: false,
  381. total: 0,
  382. };
  383. }
  384. };
  385. // 查核条件表格列定义
  386. const conditionColumns: ProColumns[] = [
  387. {
  388. title: '条件名称',
  389. dataIndex: 'name',
  390. width: 200,
  391. align: 'left' as 'left',
  392. },
  393. {
  394. title: '说明',
  395. dataIndex: 'description',
  396. align: 'left' as 'left',
  397. ellipsis: true,
  398. },
  399. ];
  400. // 人员表格列定义
  401. const [selectedPersonnelKeys, setSelectedPersonnelKeys] = useState<string[]>(
  402. [],
  403. );
  404. const personnelColumns: ProColumns[] = [
  405. {
  406. title: '工号',
  407. dataIndex: 'empCode',
  408. width: 120,
  409. align: 'center',
  410. },
  411. {
  412. title: '姓名',
  413. dataIndex: 'empName',
  414. width: 120,
  415. align: 'center',
  416. },
  417. {
  418. title: '负责人',
  419. dataIndex: 'isManager',
  420. width: 120,
  421. align: 'center',
  422. render: (_, record) => {
  423. return (
  424. <Radio
  425. checked={record.isManager}
  426. disabled={settingManagerId === record.id}
  427. onChange={() => handleSetManager(record)}
  428. />
  429. );
  430. },
  431. },
  432. {
  433. title: '操作',
  434. key: 'option',
  435. width: 100,
  436. align: 'center',
  437. render: (_, record) => {
  438. return (
  439. <Popconfirm
  440. title="确认删除?"
  441. okText="确定"
  442. cancelText="取消"
  443. onConfirm={() => handleDeletePersonnel(record)}
  444. >
  445. <a className="delete-btn">删除</a>
  446. </Popconfirm>
  447. );
  448. },
  449. },
  450. ];
  451. // 处理科室选择
  452. const handleSelectDept = (dept: string) => {
  453. setSelectedDept(dept);
  454. };
  455. // 处理部门展开/收起
  456. const toggleExpand = (dept: string) => {
  457. if (expandedKeys.includes(dept)) {
  458. setExpandedKeys(expandedKeys.filter((key) => key !== dept));
  459. } else {
  460. setExpandedKeys([...expandedKeys, dept]);
  461. }
  462. };
  463. // 人员选择相关状态
  464. const [employeeSearchKeyword, setEmployeeSearchKeyword] = useState('');
  465. const [selectedEmployees, setSelectedEmployees] = useState<string[]>([]);
  466. const [employeeModalVisible, setEmployeeModalVisible] = useState(false);
  467. const employeeTableRef = useRef<ActionType>();
  468. // 查核要点相关状态
  469. const [checkPointsSearchKeyword, setCheckPointsSearchKeyword] = useState('');
  470. const checkPointsTableRef = useRef<ActionType>();
  471. // 处理添加人员
  472. const handleAddPersonnel = () => {
  473. // 如果没有选择单位,提示先选择单位
  474. if (!selectedDept?.key) {
  475. message.warning('请先选择一个查核单位');
  476. return;
  477. }
  478. // 重置状态
  479. setEmployeeSearchKeyword('');
  480. setSelectedEmployees([]);
  481. // 打开选择人员弹窗
  482. setEmployeeModalVisible(true);
  483. // 延迟触发表格重新加载,确保弹窗打开后重新请求数据
  484. setTimeout(() => {
  485. employeeTableRef.current?.reload();
  486. }, 100);
  487. };
  488. // 获取选择人员列表的数据
  489. const getEmployeeSelectData = async (params: any) => {
  490. try {
  491. // 如果没有选中的部门,返回空数据
  492. if (!selectedDept?.key) {
  493. return {
  494. data: [],
  495. success: true,
  496. total: 0,
  497. };
  498. }
  499. // 调用真实API获取可选人员列表
  500. const res = await getDeptPendingEmployee({
  501. deptId: Number(selectedDept.key), // 当前选中的查核单位ID
  502. filter: employeeSearchKeyword || '', // 搜索关键词
  503. deptType: selectedDept.deptType ?? 0, // 单位类型,使用空值合并操作符,默认为0
  504. });
  505. if (res && res.data) {
  506. // 尝试多种数据结构格式
  507. let dataArray = null;
  508. if (Array.isArray(res.data)) {
  509. // 格式1: { data: [...] }
  510. dataArray = res.data;
  511. } else if (res.data.items && Array.isArray(res.data.items)) {
  512. // 格式2: { data: { items: [...] } }
  513. dataArray = res.data.items;
  514. } else if (res.data.list && Array.isArray(res.data.list)) {
  515. // 格式3: { data: { list: [...] } }
  516. dataArray = res.data.list;
  517. }
  518. if (dataArray && dataArray.length >= 0) {
  519. // 格式化返回的数据
  520. const formattedData = dataArray.map((item: any) => ({
  521. id: String(item.id || item.empId), // 人员ID
  522. empCode: item.code || item.empCode, // 人员工号
  523. empName: item.name || item.empName, // 人员姓名
  524. userId: item.userId, // 人员中台ID
  525. }));
  526. return {
  527. data: formattedData,
  528. success: true,
  529. total: formattedData.length,
  530. };
  531. }
  532. } else if (Array.isArray(res)) {
  533. // 格式4: 直接返回数组
  534. const formattedData = res.map((item: any) => ({
  535. id: String(item.id || item.empId), // 人员ID
  536. empCode: item.code || item.empCode, // 人员工号
  537. empName: item.name || item.empName, // 人员姓名
  538. userId: item.userId, // 人员中台ID
  539. }));
  540. return {
  541. data: formattedData,
  542. success: true,
  543. total: formattedData.length,
  544. };
  545. }
  546. return {
  547. data: [],
  548. success: true,
  549. total: 0,
  550. };
  551. } catch (error) {
  552. console.error('获取可选人员列表失败:', error);
  553. message.error('获取可选人员列表失败');
  554. return {
  555. data: [],
  556. success: false,
  557. total: 0,
  558. };
  559. }
  560. };
  561. // 处理员工选择
  562. const handleEmployeeSelect = (employeeId: string, checked: boolean) => {
  563. if (checked) {
  564. setSelectedEmployees([...selectedEmployees, employeeId]);
  565. } else {
  566. setSelectedEmployees(selectedEmployees.filter((id) => id !== employeeId));
  567. }
  568. };
  569. // 确认添加选择的人员
  570. const handleConfirmAddEmployees = async () => {
  571. if (selectedEmployees.length === 0) {
  572. message.warning('请至少选择一名人员');
  573. return;
  574. }
  575. try {
  576. // 调用真实API保存添加的人员
  577. const res = await addDeptEmployee({
  578. id: selectedDept.key, // 查核单位ID
  579. type: String(selectedDept.deptType ?? 0), // 单位类型:0普通单位1职能科室
  580. mapIdList: selectedEmployees, // 人员ID列表
  581. });
  582. if (res) {
  583. message.success(`已添加 ${selectedEmployees.length} 名人员`);
  584. setEmployeeModalVisible(false);
  585. // 重置选择状态
  586. setSelectedEmployees([]);
  587. // 刷新人员列表
  588. tableRef.current?.reload();
  589. } else {
  590. message.error('添加人员失败');
  591. }
  592. } catch (error) {
  593. console.error('添加人员失败:', error);
  594. message.error('添加人员失败');
  595. }
  596. };
  597. // 处理搜索
  598. const handleSearch = (value: string) => {
  599. setSearchKeywords(value);
  600. // 使用新接口时,不需要本地过滤,直接重新请求API
  601. tableRef.current?.reload();
  602. };
  603. // 处理标签页切换
  604. const handleTabChange = (key: string) => {
  605. setActiveTab(key);
  606. // 切换tab时清空相关选择状态
  607. if (key !== 'personnel') {
  608. setSelectedPersonnelKeys([]);
  609. }
  610. if (key !== 'checkPoints') {
  611. setSelectedCheckPointKeys([]);
  612. }
  613. };
  614. // 获取查核要点数据
  615. const getCheckPointsData = async (params: any) => {
  616. try {
  617. // 如果没有选中的部门,返回空数据
  618. if (!selectedDept?.key) {
  619. return {
  620. data: [],
  621. success: true,
  622. total: 0,
  623. };
  624. }
  625. // 模拟数据,后续替换为真实API调用
  626. const mockData = [
  627. {
  628. id: '1',
  629. pointName: '麻醉科管理规程',
  630. checkItemName: '审阅麻醉前评估记录内容完整、(10分)',
  631. rowSpan: 4, // 麻醉科管理规程有4行
  632. },
  633. {
  634. id: '2',
  635. pointName: '麻醉科管理规程',
  636. checkItemName: '审阅麻醉记录单内手术时间与用药记录(10分)',
  637. rowSpan: 0, // 合并到上一行
  638. },
  639. {
  640. id: '3',
  641. pointName: '麻醉科管理规程',
  642. checkItemName: '审阅麻醉前评估记录内容完整、(10分)',
  643. rowSpan: 0, // 合并到上一行
  644. },
  645. {
  646. id: '4',
  647. pointName: '麻醉科管理规程',
  648. checkItemName: '审阅麻醉记录单内手术时间与用药记录(10分)',
  649. rowSpan: 0, // 合并到上一行
  650. },
  651. {
  652. id: '5',
  653. pointName: '药品管理规程',
  654. checkItemName: '审阅麻醉前评估记录内容完整、(10分)',
  655. rowSpan: 2, // 药品管理规程有2行
  656. },
  657. {
  658. id: '6',
  659. pointName: '药品管理规程',
  660. checkItemName: '审阅麻醉记录单内手术时间与用药记录(10分)',
  661. rowSpan: 0, // 合并到上一行
  662. },
  663. {
  664. id: '7',
  665. pointName: '急救药品管理规程',
  666. checkItemName: '如果发现冰箱温度不正常,应该如何处理?(15分)',
  667. rowSpan: 1, // 急救药品管理规程有1行
  668. },
  669. ];
  670. // 根据搜索关键词过滤数据
  671. const filteredData = checkPointsSearchKeyword
  672. ? mockData.filter(
  673. (item) =>
  674. item.pointName.includes(checkPointsSearchKeyword) ||
  675. item.checkItemName.includes(checkPointsSearchKeyword),
  676. )
  677. : mockData;
  678. return {
  679. data: filteredData,
  680. success: true,
  681. total: filteredData.length,
  682. };
  683. } catch (error) {
  684. console.error('获取查核要点失败:', error);
  685. message.error('获取查核要点失败');
  686. return {
  687. data: [],
  688. success: false,
  689. total: 0,
  690. };
  691. }
  692. };
  693. // 处理查核要点搜索
  694. const handleSearchCheckPoints = (value: string) => {
  695. setCheckPointsSearchKeyword(value);
  696. checkPointsTableRef.current?.reload();
  697. };
  698. // 处理添加查核要点
  699. const handleAddCheckPoint = () => {
  700. if (!selectedDept?.key) {
  701. message.warning('请先选择一个查核单位');
  702. return;
  703. }
  704. // TODO: 实现添加查核要点的弹窗
  705. message.info('添加查核要点功能待实现');
  706. };
  707. // 处理删除查核要点
  708. const handleDeleteCheckPoint = async (record: any) => {
  709. try {
  710. await delDeptCheckpoint([record.id]);
  711. message.success('删除要点成功');
  712. checkPointsTableRef.current?.reload();
  713. } catch (e) {
  714. message.error('删除要点失败');
  715. }
  716. };
  717. // 获取查核条件列表 - 用于KCIMTable
  718. const getConditionData = async (params: any) => {
  719. try {
  720. const { current = 1, pageSize = 10 } = params;
  721. // 调用真实API获取条件列表(不传filter,获取所有数据)
  722. const requestParams = {
  723. deptClassId: currentNode?.key, // 当前选中的部门分类ID
  724. conditionType: conditionSearchType === '个案' ? 0 : 1, // 条件类型:0个案1系统
  725. filter: '', // 不传搜索关键词,获取所有数据
  726. };
  727. const res = await getDeptClassFilterConditionList(requestParams);
  728. if (res && res.itemList) {
  729. // 直接从res中获取数据
  730. const { itemList = [], mapIdList = [] } = res;
  731. // 更新已选择的条件ID列表 - 确保ID类型一致
  732. const selectedIds = (mapIdList || []).map((id: any) => String(id));
  733. setSelectedConditions(selectedIds);
  734. // 转换数据格式,保持树形结构
  735. const transformConditions = (conditions: any[]): any[] => {
  736. return conditions.map((item: any) => {
  737. const transformed: any = {
  738. id: String(item.id), // 确保ID为字符串类型
  739. name: item.name,
  740. description: item.description || '暂无说明',
  741. };
  742. // 如果有子条件,递归处理
  743. if (
  744. item.children &&
  745. Array.isArray(item.children) &&
  746. item.children.length > 0
  747. ) {
  748. transformed.children = transformConditions(item.children);
  749. }
  750. return transformed;
  751. });
  752. };
  753. // 保持树形结构的条件数据
  754. const allConditions = transformConditions(itemList);
  755. // 递归搜索树形数据
  756. const searchInTree = (data: any[], keyword: string): any[] => {
  757. if (!keyword.trim()) return data;
  758. const result: any[] = [];
  759. data.forEach((item) => {
  760. // 检查当前节点是否匹配
  761. const currentMatches =
  762. item.name.includes(keyword) || item.description.includes(keyword);
  763. // 递归搜索子节点
  764. let filteredChildren: any[] = [];
  765. if (item.children && Array.isArray(item.children)) {
  766. filteredChildren = searchInTree(item.children, keyword);
  767. }
  768. // 如果当前节点匹配或有匹配的子节点,则保留
  769. if (currentMatches || filteredChildren.length > 0) {
  770. const newItem = { ...item };
  771. if (filteredChildren.length > 0) {
  772. newItem.children = filteredChildren;
  773. } else if (!currentMatches) {
  774. // 如果当前节点不匹配但有匹配的子节点,保留原有children
  775. // 这种情况在上面的逻辑中已经处理了
  776. }
  777. result.push(newItem);
  778. }
  779. });
  780. return result;
  781. };
  782. // 根据搜索关键词过滤(本地搜索)
  783. const filteredData = searchInTree(
  784. allConditions,
  785. conditionSearchKeyword,
  786. );
  787. // 前端分页处理
  788. const startIndex = (current - 1) * pageSize;
  789. const endIndex = startIndex + pageSize;
  790. const pageData = filteredData.slice(startIndex, endIndex);
  791. return {
  792. data: pageData,
  793. success: true,
  794. total: filteredData.length,
  795. };
  796. } else {
  797. return {
  798. data: [],
  799. success: true,
  800. total: 0,
  801. };
  802. }
  803. } catch (error) {
  804. console.error('进入了catch块,获取条件列表失败:', error);
  805. message.error('获取条件列表失败');
  806. return {
  807. data: [],
  808. success: false,
  809. total: 0,
  810. };
  811. }
  812. };
  813. // 打开条件选择弹窗
  814. const handleOpenConditionModal = (node: any) => {
  815. setCurrentNode(node);
  816. setConditionModalVisible(true);
  817. setSelectedConditions([]); // 重置选择,实际选择状态会在API调用后更新
  818. setConditionSearchKeyword(''); // 重置搜索关键词
  819. setConditionSearchType('个案'); // 重置搜索类型
  820. // 延迟触发表格重新加载,确保弹窗打开后重新请求数据
  821. setTimeout(() => {
  822. conditionTableRef.current?.reload();
  823. }, 100);
  824. };
  825. // 处理条件选择
  826. const handleConditionSelect = (conditionId: string, checked: boolean) => {
  827. if (checked) {
  828. setSelectedConditions([...selectedConditions, conditionId]);
  829. } else {
  830. setSelectedConditions(
  831. selectedConditions.filter((id) => id !== conditionId),
  832. );
  833. }
  834. };
  835. // 处理级联选择(父子关联)
  836. const handleConditionSelectWithCascade = (record: any, selected: boolean) => {
  837. // 获取当前记录及其所有子记录的ID
  838. const getAllIds = (item: any): string[] => {
  839. const ids = [item.id];
  840. if (
  841. item.children &&
  842. Array.isArray(item.children) &&
  843. item.children.length > 0
  844. ) {
  845. item.children.forEach((child: any) => {
  846. ids.push(...getAllIds(child));
  847. });
  848. }
  849. return ids;
  850. };
  851. const allIds = getAllIds(record);
  852. if (selected) {
  853. // 选中:添加当前记录及其所有子记录
  854. const newSelectedIds = [...new Set([...selectedConditions, ...allIds])];
  855. setSelectedConditions(newSelectedIds);
  856. } else {
  857. // 取消选中:移除当前记录及其所有子记录
  858. const newSelectedIds = selectedConditions.filter(
  859. (id) => !allIds.includes(id),
  860. );
  861. setSelectedConditions(newSelectedIds);
  862. }
  863. };
  864. // 确认选择条件
  865. const handleConfirmConditions = async () => {
  866. try {
  867. // 调用保存接口
  868. const res = await applyDeptClassFilterCondition({
  869. id: currentNode?.key, // 单位分类ID
  870. mapIdList: selectedConditions, // 查核条件ID列表
  871. });
  872. if (res) {
  873. message.success(
  874. `已为 ${currentNode?.title} 保存了 ${selectedConditions.length} 个查核条件`,
  875. );
  876. setConditionModalVisible(false);
  877. // 重新获取列表,更新条件信息
  878. await fetchDeptClassList();
  879. // 查询选中的条件名称
  880. if (res.items && Array.isArray(res.items)) {
  881. // 如果API返回了条件列表,可以直接从返回值获取条件名称
  882. const conditionNames = res.items
  883. .map((item: any) => item.name)
  884. .join('、');
  885. // 更新treeData中对应节点的conditions属性
  886. const updatedTreeData = treeData.map((item: any) => {
  887. if (item.key === currentNode?.key) {
  888. return {
  889. ...item,
  890. conditions: conditionNames || '手术室、重症加护病房、医学影像',
  891. };
  892. }
  893. return item;
  894. });
  895. setTreeData(updatedTreeData);
  896. setFilteredTreeData(updatedTreeData);
  897. }
  898. } else {
  899. message.error('保存查核条件失败');
  900. }
  901. } catch (error) {
  902. console.error('保存查核条件失败:', error);
  903. message.error('保存查核条件失败');
  904. }
  905. };
  906. // 处理删除节点
  907. const handleDeleteNode = (node: any, isParent: boolean) => {
  908. Modal.confirm({
  909. title: '确认删除',
  910. content: `确定要删除${isParent ? '单位分类' : '查核单位'} "${node.title
  911. }" 吗?`,
  912. okText: '确定',
  913. cancelText: '取消',
  914. onOk: async () => {
  915. try {
  916. if (isParent) {
  917. // 删除单位分类
  918. const res = await delDeptClass([Number(node.key)]);
  919. if (res) {
  920. message.success('删除单位分类成功');
  921. // 重新获取列表数据
  922. await fetchDeptClassList();
  923. } else {
  924. message.error('删除单位分类失败');
  925. }
  926. } else {
  927. // 删除查核单位
  928. const res = await delDepartmentByClass({
  929. deptId: Number(node.key), // 查核单位ID
  930. deptType: node.deptType ?? 0, // 单位类型:0普通单位 1职能科室
  931. });
  932. if (res) {
  933. message.success('删除查核单位成功');
  934. // 重新获取列表数据
  935. await fetchDeptClassList();
  936. } else {
  937. message.error('删除查核单位失败');
  938. }
  939. }
  940. } catch (error) {
  941. message.error('删除失败');
  942. }
  943. },
  944. });
  945. };
  946. // 自定义空状态
  947. const customizeRenderEmpty = () => (
  948. <div style={{ textAlign: 'center', padding: '50px 0' }}>
  949. <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无数据" />
  950. </div>
  951. );
  952. // 生成更多按钮的菜单
  953. const getMoreMenu = (
  954. node: any,
  955. isParent: boolean = false,
  956. parentNode?: any,
  957. ) => {
  958. const menuItems = [];
  959. if (isParent) {
  960. // 父级节点菜单:新增单位、条件、修改、删除
  961. menuItems.push(
  962. <Menu.Item key="addUnit" style={{ padding: 0 }}>
  963. <ModalForm
  964. title="新增查核单位"
  965. trigger={<div style={{ padding: '5px 12px' }}>新增单位</div>}
  966. width={352}
  967. onFinish={async (values) => {
  968. try {
  969. const { name, sort, deptType } = values;
  970. const res = await addDepartmentByClass({
  971. deptClassId: node.key, // 使用当前选中的单位分类ID
  972. name,
  973. sort,
  974. deptType, // 使用表单中用户选择的职能科室值
  975. });
  976. if (res) {
  977. message.success('新增单位成功');
  978. // 重新获取列表数据
  979. await fetchDeptClassList();
  980. return true;
  981. } else {
  982. message.error('新增单位失败');
  983. return false;
  984. }
  985. } catch (error) {
  986. message.error('新增单位失败');
  987. return false;
  988. }
  989. }}
  990. modalProps={{
  991. destroyOnClose: true,
  992. }}
  993. >
  994. <ProFormText
  995. name="name"
  996. label="单位名称"
  997. placeholder="请输入"
  998. rules={[{ required: true, message: '请输入单位名称' }]}
  999. />
  1000. <ProFormDigit
  1001. name="sort"
  1002. label="序号"
  1003. placeholder="请输入"
  1004. initialValue={0}
  1005. min={0}
  1006. rules={[{ required: true, message: '请输入序号' }]}
  1007. />
  1008. <ProFormRadio.Group
  1009. name="deptType"
  1010. label="职能科室"
  1011. initialValue={0}
  1012. rules={[{ required: true, message: '请选择是否为职能科室' }]}
  1013. options={[
  1014. { label: '否', value: 0 },
  1015. { label: '是', value: 1 },
  1016. ]}
  1017. />
  1018. </ModalForm>
  1019. </Menu.Item>,
  1020. <Menu.Item
  1021. key="condition"
  1022. onClick={() => handleOpenConditionModal(node)}
  1023. >
  1024. 条件
  1025. </Menu.Item>,
  1026. );
  1027. }
  1028. // 公共菜单项:修改、删除
  1029. menuItems.push(
  1030. <Menu.Item key="edit" style={{ padding: 0 }}>
  1031. <ModalForm
  1032. title={isParent ? '修改单位分类' : '修改查核单位'}
  1033. trigger={<div style={{ padding: '5px 12px' }}>修改</div>}
  1034. width={352}
  1035. initialValues={{
  1036. name: node.title,
  1037. depType: node.visitType || 1,
  1038. sort: node.sort || 0,
  1039. deptType: node.deptType ?? 0, // 单位类型:0普通单位 1职能科室
  1040. }}
  1041. onFinish={async (values) => {
  1042. try {
  1043. if (isParent) {
  1044. // 修改单位分类
  1045. const { name, depType } = values;
  1046. const res = await updateDeptClass({
  1047. id: Number(node.key), // 单位分类ID
  1048. name,
  1049. depType, // 门急诊类型:1-门诊 2-急诊
  1050. });
  1051. if (res) {
  1052. message.success('修改单位分类成功');
  1053. } else {
  1054. message.error('修改单位分类失败');
  1055. return false;
  1056. }
  1057. } else {
  1058. // 修改查核单位
  1059. const { name, sort, deptType } = values;
  1060. const res = await updateDepartmentByClass({
  1061. deptClassId: Number(parentNode?.key), // 单位分类ID
  1062. id: Number(node.key), // 查核单位ID
  1063. name,
  1064. sort,
  1065. deptType, // 使用表单中用户选择的职能科室值
  1066. });
  1067. if (res) {
  1068. message.success('修改查核单位成功');
  1069. } else {
  1070. message.error('修改查核单位失败');
  1071. return false;
  1072. }
  1073. }
  1074. // 重新获取列表数据
  1075. await fetchDeptClassList();
  1076. return true;
  1077. } catch (error) {
  1078. message.error('修改失败');
  1079. return false;
  1080. }
  1081. }}
  1082. modalProps={{
  1083. destroyOnClose: true,
  1084. }}
  1085. >
  1086. <ProFormText
  1087. name="name"
  1088. label={isParent ? '单位分类名称' : '单位名称'}
  1089. placeholder="请输入"
  1090. rules={[
  1091. {
  1092. required: true,
  1093. message: `请输入${isParent ? '单位分类名称' : '单位名称'}`,
  1094. },
  1095. ]}
  1096. />
  1097. {isParent ? (
  1098. <ProFormRadio.Group
  1099. name="depType"
  1100. label="诊别"
  1101. rules={[{ required: true, message: '请选择诊别' }]}
  1102. options={[
  1103. { label: '门诊', value: 1 },
  1104. { label: '急诊', value: 2 },
  1105. ]}
  1106. />
  1107. ) : (
  1108. <>
  1109. <ProFormDigit
  1110. name="sort"
  1111. label="序号"
  1112. placeholder="请输入"
  1113. min={0}
  1114. rules={[{ required: true, message: '请输入序号' }]}
  1115. />
  1116. <ProFormRadio.Group
  1117. name="deptType"
  1118. label="职能科室"
  1119. disabled={true} // 编辑时不可修改
  1120. rules={[{ required: true, message: '请选择是否为职能科室' }]}
  1121. options={[
  1122. { label: '否', value: 0 },
  1123. { label: '是', value: 1 },
  1124. ]}
  1125. />
  1126. </>
  1127. )}
  1128. </ModalForm>
  1129. </Menu.Item>,
  1130. <Menu.Item key="delete" onClick={() => handleDeleteNode(node, isParent)}>
  1131. 删除
  1132. </Menu.Item>,
  1133. );
  1134. return <Menu>{menuItems}</Menu>;
  1135. };
  1136. // 新增按钮的菜单
  1137. const menu = (
  1138. <Menu>
  1139. <Menu.Item key="1" style={{ padding: 0 }}>
  1140. <ModalForm
  1141. title="新增单位分类"
  1142. trigger={<div style={{ padding: '5px 12px' }}>新增单位分类</div>}
  1143. width={352}
  1144. onFinish={async (values) => {
  1145. try {
  1146. const { name, depType } = values;
  1147. const res = await addDeptClass({ name, depType });
  1148. if (res) {
  1149. message.success('新增单位分类成功');
  1150. // 重新获取列表数据
  1151. await fetchDeptClassList();
  1152. return true;
  1153. } else {
  1154. message.error('新增失败');
  1155. return false;
  1156. }
  1157. } catch (error) {
  1158. message.error('新增失败');
  1159. return false;
  1160. }
  1161. }}
  1162. modalProps={{
  1163. destroyOnClose: true,
  1164. }}
  1165. >
  1166. <ProFormText
  1167. name="name"
  1168. label="单位分类名称"
  1169. placeholder="请输入"
  1170. rules={[{ required: true, message: '请输入单位分类名称' }]}
  1171. />
  1172. <ProFormRadio.Group
  1173. name="depType"
  1174. label="诊别"
  1175. initialValue={1}
  1176. rules={[{ required: true, message: '请选择诊别' }]}
  1177. options={[
  1178. { label: '门诊', value: 1 },
  1179. { label: '急诊', value: 2 },
  1180. ]}
  1181. />
  1182. </ModalForm>
  1183. </Menu.Item>
  1184. <Menu.Item key="2" style={{ padding: 0 }}>
  1185. <ModalForm
  1186. title="导入系统追踪知识库"
  1187. trigger={<div style={{ padding: '5px 12px' }}>导入系统追踪</div>}
  1188. width={600}
  1189. onFinish={async (values) => {
  1190. try {
  1191. let token = '';
  1192. const userDataStr = localStorage.getItem('userData');
  1193. if (userDataStr) {
  1194. try {
  1195. token = JSON.parse(userDataStr).token || '';
  1196. } catch (e) {
  1197. token = '';
  1198. }
  1199. }
  1200. const formData = new FormData();
  1201. formData.append('type', '1');
  1202. if (values.systemTrackingFile && values.systemTrackingFile[0]?.originFileObj) {
  1203. formData.append('file', values.systemTrackingFile[0].originFileObj);
  1204. } else {
  1205. message.error('请上传文件');
  1206. return false;
  1207. }
  1208. const res = await fetch('/gateway/manager/Department/importPfmKnowledge', {
  1209. method: 'POST',
  1210. headers: {
  1211. token: token,
  1212. },
  1213. body: formData,
  1214. });
  1215. let result = null;
  1216. try {
  1217. result = await res.json();
  1218. } catch (e) {
  1219. message.error('导入失败');
  1220. return false;
  1221. }
  1222. if (result && result.status === 200) {
  1223. message.success('导入成功');
  1224. // 重新获取列表数据,刷新左侧树形结构
  1225. await fetchDeptClassList();
  1226. return true;
  1227. } else {
  1228. message.error(result.errorMessage || '导入失败');
  1229. return false;
  1230. }
  1231. } catch (error) {
  1232. message.error('导入失败');
  1233. return false;
  1234. }
  1235. }}
  1236. modalProps={{
  1237. destroyOnClose: true,
  1238. }}
  1239. >
  1240. <div
  1241. style={{
  1242. display: 'flex',
  1243. justifyContent: 'space-between',
  1244. alignItems: 'center',
  1245. marginBottom: 16,
  1246. }}
  1247. >
  1248. <span style={{ fontSize: 16, fontWeight: 500 }}>文件上传</span>
  1249. <Button type="link" style={{ padding: 0 }} onClick={() => downloadTemplate(1)}>
  1250. 模板下载
  1251. </Button>
  1252. </div>
  1253. <ProFormUploadDragger
  1254. name="systemTrackingFile"
  1255. icon={<InboxOutlined />}
  1256. title="点击或将文件拖拽到这里上传"
  1257. description="支持扩展名:.rar .zip .doc .docx .pdf .jpg..."
  1258. fieldProps={{
  1259. name: 'file',
  1260. multiple: false,
  1261. accept: '.rar,.zip,.doc,.docx,.pdf,.jpg,.jpeg,.png,.xls,.xlsx',
  1262. beforeUpload: () => false, // 阻止自动上传
  1263. }}
  1264. rules={[{ required: true, message: '请上传文件' }]}
  1265. />
  1266. </ModalForm>
  1267. </Menu.Item>
  1268. {/* <Menu.Item key="3" style={{ padding: 0 }}>
  1269. <ModalForm
  1270. title="导入自查督查知识库"
  1271. trigger={<div style={{ padding: '5px 12px' }}>导入自查督查</div>}
  1272. width={600}
  1273. onFinish={async (values) => {
  1274. try {
  1275. let token = '';
  1276. const userDataStr = localStorage.getItem('userData');
  1277. if (userDataStr) {
  1278. try {
  1279. token = JSON.parse(userDataStr).token || '';
  1280. } catch (e) {
  1281. token = '';
  1282. }
  1283. }
  1284. const formData = new FormData();
  1285. formData.append('type', '2');
  1286. if (values.selfInspectionFile && values.selfInspectionFile[0]?.originFileObj) {
  1287. formData.append('file', values.selfInspectionFile[0].originFileObj);
  1288. } else {
  1289. message.error('请上传文件');
  1290. return false;
  1291. }
  1292. const res = await fetch('/manager/Department/importPfmKnowledge', {
  1293. method: 'POST',
  1294. headers: {
  1295. token: token,
  1296. },
  1297. body: formData,
  1298. });
  1299. let result = null;
  1300. try {
  1301. result = await res.json();
  1302. } catch (e) {
  1303. message.error('导入失败');
  1304. return false;
  1305. }
  1306. if (result && result.status === 200) {
  1307. message.success('导入成功');
  1308. return true;
  1309. } else {
  1310. message.error(result.errorMessage || '导入失败');
  1311. return false;
  1312. }
  1313. } catch (error) {
  1314. message.error('导入失败');
  1315. return false;
  1316. }
  1317. }}
  1318. modalProps={{
  1319. destroyOnClose: true,
  1320. }}
  1321. >
  1322. <div
  1323. style={{
  1324. display: 'flex',
  1325. justifyContent: 'space-between',
  1326. alignItems: 'center',
  1327. marginBottom: 16,
  1328. }}
  1329. >
  1330. <span style={{ fontSize: 16, fontWeight: 500 }}>文件上传</span>
  1331. <Button type="link" style={{ padding: 0 }} onClick={() => downloadTemplate(2)}>
  1332. 模板下载
  1333. </Button>
  1334. </div>
  1335. <ProFormUploadDragger
  1336. name="selfInspectionFile"
  1337. icon={<InboxOutlined />}
  1338. title="点击或将文件拖拽到这里上传"
  1339. description="支持扩展名:.rar .zip .doc .docx .pdf .jpg..."
  1340. fieldProps={{
  1341. name: 'file',
  1342. multiple: false,
  1343. accept: '.rar,.zip,.doc,.docx,.pdf,.jpg,.jpeg,.png,.xls,.xlsx',
  1344. beforeUpload: () => false, // 阻止自动上传
  1345. }}
  1346. rules={[{ required: true, message: '请上传文件' }]}
  1347. />
  1348. </ModalForm>
  1349. </Menu.Item> */}
  1350. </Menu>
  1351. );
  1352. // 渲染左侧部门树
  1353. const renderDeptTree = () => {
  1354. return (
  1355. <div className="dept-tree">
  1356. <div className="search-wrapper">
  1357. <Input
  1358. placeholder="单位分类、名称"
  1359. suffix={<SearchOutlined style={{ color: '#99A6BF' }} />}
  1360. className="search-input"
  1361. value={treeSearchKeywords}
  1362. onChange={(e) => handleTreeSearch(e.target.value)}
  1363. allowClear
  1364. />
  1365. <Dropdown
  1366. overlay={menu}
  1367. trigger={['hover']}
  1368. overlayClassName="check-unit-add-dropdown"
  1369. >
  1370. <Button icon={<PlusOutlined />} className="add-button" />
  1371. </Dropdown>
  1372. </div>
  1373. <div className="dept-list">
  1374. {filteredTreeData.map((parent) => {
  1375. // 判断是否有子项
  1376. const hasChildren = parent.children && parent.children.length > 0;
  1377. return (
  1378. <div key={parent.key} className="dept-group">
  1379. {hasChildren ? (
  1380. <div
  1381. className="dept-parent"
  1382. onClick={() => toggleExpand(parent.key)}
  1383. onMouseEnter={() => setHoveredNode(parent.key)}
  1384. onMouseLeave={() => setHoveredNode(null)}
  1385. >
  1386. <Button
  1387. type="text"
  1388. icon={
  1389. expandedKeys.includes(parent.key) ? (
  1390. <IconFont type="icon-shouqi1" />
  1391. ) : (
  1392. <IconFont type="icon-zhankai1" />
  1393. )
  1394. }
  1395. className="toggle-button"
  1396. />
  1397. <Tooltip title={parent.title}>
  1398. <span className="dept-title">{parent.title}</span>
  1399. </Tooltip>
  1400. {(selectedDept?.key === parent.key ||
  1401. hoveredNode === parent.key ||
  1402. openDropdownNode === parent.key) && (
  1403. <Dropdown
  1404. overlay={getMoreMenu(parent, true)}
  1405. trigger={['click']}
  1406. placement="bottomRight"
  1407. overlayClassName="check-unit-add-dropdown"
  1408. onVisibleChange={(visible) => {
  1409. if (visible) {
  1410. setOpenDropdownNode(parent.key);
  1411. } else {
  1412. setOpenDropdownNode(null);
  1413. }
  1414. }}
  1415. >
  1416. <Button
  1417. type="text"
  1418. icon={<MoreOutlined />}
  1419. className="more-button"
  1420. onClick={(e) => e.stopPropagation()}
  1421. />
  1422. </Dropdown>
  1423. )}
  1424. </div>
  1425. ) : (
  1426. <div
  1427. className={`dept-leaf ${selectedDept?.key === parent.key ? 'selected' : ''
  1428. }`}
  1429. onClick={() => {
  1430. // 设置分类自身的信息
  1431. setSelectedDept({
  1432. ...parent,
  1433. parentTitle: '单位分类',
  1434. // 保留原始的filterConditionList数据
  1435. filterConditionList: parent.filterConditionList || [],
  1436. });
  1437. // 不需要手动重新加载,useEffect会监听selectedDept变化
  1438. }}
  1439. onMouseEnter={() => setHoveredNode(parent.key)}
  1440. onMouseLeave={() => setHoveredNode(null)}
  1441. >
  1442. <Tooltip title={parent.title}>
  1443. <span className="dept-title">{parent.title}</span>
  1444. </Tooltip>
  1445. {(selectedDept?.key === parent.key ||
  1446. hoveredNode === parent.key ||
  1447. openDropdownNode === parent.key) && (
  1448. <Dropdown
  1449. overlay={getMoreMenu(parent, true)}
  1450. trigger={['click']}
  1451. placement="bottomRight"
  1452. overlayClassName="check-unit-add-dropdown"
  1453. onVisibleChange={(visible) => {
  1454. if (visible) {
  1455. setOpenDropdownNode(parent.key);
  1456. } else {
  1457. setOpenDropdownNode(null);
  1458. }
  1459. }}
  1460. >
  1461. <Button
  1462. type="text"
  1463. icon={<MoreOutlined />}
  1464. className="more-button"
  1465. onClick={(e) => e.stopPropagation()}
  1466. />
  1467. </Dropdown>
  1468. )}
  1469. </div>
  1470. )}
  1471. {hasChildren && expandedKeys.includes(parent.key) && (
  1472. <div className="dept-children">
  1473. {parent.children.map((child: any) => (
  1474. <div
  1475. key={child.key}
  1476. className={`dept-item ${selectedDept?.key === child.key ? 'selected' : ''
  1477. }`}
  1478. onClick={() => {
  1479. // 添加父节点信息,保留原始查核条件数据
  1480. setSelectedDept({
  1481. ...child,
  1482. parentTitle: parent.title,
  1483. // 保留原始的filterConditionList数据
  1484. filterConditionList:
  1485. child.filterConditionList || [],
  1486. });
  1487. // 不需要手动重新加载,useEffect会监听selectedDept变化
  1488. }}
  1489. onMouseEnter={() => setHoveredNode(child.key)}
  1490. onMouseLeave={() => setHoveredNode(null)}
  1491. >
  1492. <Tooltip title={child.title}>
  1493. <span className="dept-title-text">
  1494. {child.title}
  1495. {child.deptType === 1 && (
  1496. <span
  1497. style={{
  1498. marginLeft: 4,
  1499. fontSize: 12,
  1500. padding: '0 4px',
  1501. background: '#f0f0f0',
  1502. color: '#666',
  1503. borderRadius: 2,
  1504. display: 'inline-block',
  1505. lineHeight: '18px',
  1506. verticalAlign: 'text-top',
  1507. }}
  1508. >
  1509. </span>
  1510. )}
  1511. </span>
  1512. </Tooltip>
  1513. {(selectedDept?.key === child.key ||
  1514. hoveredNode === child.key ||
  1515. openDropdownNode === child.key) && (
  1516. <Dropdown
  1517. overlay={getMoreMenu(child, false, parent)}
  1518. trigger={['click']}
  1519. placement="bottomRight"
  1520. overlayClassName="check-unit-add-dropdown"
  1521. onVisibleChange={(visible) => {
  1522. if (visible) {
  1523. setOpenDropdownNode(child.key);
  1524. } else {
  1525. setOpenDropdownNode(null);
  1526. }
  1527. }}
  1528. >
  1529. <Button
  1530. type="text"
  1531. icon={<MoreOutlined />}
  1532. className="more-button"
  1533. onClick={(e) => e.stopPropagation()}
  1534. />
  1535. </Dropdown>
  1536. )}
  1537. </div>
  1538. ))}
  1539. </div>
  1540. )}
  1541. </div>
  1542. );
  1543. })}
  1544. </div>
  1545. </div>
  1546. );
  1547. };
  1548. // 处理设置负责人
  1549. const [settingManagerId, setSettingManagerId] = useState<string>('');
  1550. const handleSetManager = async (record: any) => {
  1551. if (record.isManager) return; // 已是负责人
  1552. setSettingManagerId(record.id);
  1553. try {
  1554. await setDeptManager({
  1555. empId: Number(record.empId || record.id),
  1556. empName: record.empName,
  1557. deptId: Number(selectedDept.key),
  1558. deptType: selectedDept.deptType ?? 0, // 使用空值合并操作符,默认为0
  1559. });
  1560. message.success(`已设置 ${record.empName} 为负责人`);
  1561. tableRef.current?.reload();
  1562. } catch (e) {
  1563. message.error('设置负责人失败');
  1564. } finally {
  1565. setSettingManagerId('');
  1566. }
  1567. };
  1568. const handleDeletePersonnel = async (record: any) => {
  1569. try {
  1570. await delDeptEmp([
  1571. { id: String(record.id), type: String(selectedDept.deptType ?? 0) },
  1572. ]);
  1573. message.success('删除成功');
  1574. tableRef.current?.reload();
  1575. } catch (e) {
  1576. message.error('删除失败');
  1577. }
  1578. };
  1579. // Tab下table专用数据获取方法
  1580. const getDeptCheckpointData = async (params: any) => {
  1581. try {
  1582. if (!selectedDept?.key) {
  1583. return { data: [], success: true, total: 0 };
  1584. }
  1585. // 不传搜索关键词,获取全部数据
  1586. const res = await getDeptCheckpoint({
  1587. deptId: selectedDept.key,
  1588. filter: '', // 获取全部数据
  1589. deptType: selectedDept.deptType ?? 0,
  1590. });
  1591. // 这里根据接口返回结构处理数据
  1592. if (Array.isArray(res)) {
  1593. const flatData: any[] = [];
  1594. res.forEach((point: any) => {
  1595. if (
  1596. Array.isArray(point.checkItemList) &&
  1597. point.checkItemList.length > 0
  1598. ) {
  1599. point.checkItemList.forEach((item: any, idx: number) => {
  1600. flatData.push({
  1601. id: idx === 0 ? String(point.id) : `${point.id}_item_${idx}`,
  1602. checkPointName: point.checkPointName, // 直接用接口字段
  1603. checkItemName: item.name,
  1604. rowSpan: idx === 0 ? point.checkItemList.length : 0,
  1605. isFirst: idx === 0,
  1606. });
  1607. });
  1608. } else {
  1609. flatData.push({
  1610. id: String(point.id),
  1611. checkPointName: point.checkPointName, // 直接用接口字段
  1612. checkItemName: '-',
  1613. rowSpan: 1,
  1614. isFirst: true,
  1615. });
  1616. }
  1617. });
  1618. // 前端本地搜索过滤
  1619. let filteredData = flatData;
  1620. if (checkPointsSearchKeyword && checkPointsSearchKeyword.trim()) {
  1621. filteredData = flatData.filter((item: any) =>
  1622. (item.checkPointName && item.checkPointName.includes(checkPointsSearchKeyword)) ||
  1623. (item.checkItemName && item.checkItemName.includes(checkPointsSearchKeyword))
  1624. );
  1625. }
  1626. // 更新当前页数据用于全选逻辑
  1627. setCurrentCheckPointPageData(filteredData);
  1628. return {
  1629. data: filteredData,
  1630. success: true,
  1631. total: filteredData.length,
  1632. };
  1633. }
  1634. return { data: [], success: true, total: 0 };
  1635. } catch (e) {
  1636. return { data: [], success: false, total: 0 };
  1637. }
  1638. };
  1639. // Tab页columns
  1640. const checkPointTabColumns = [
  1641. {
  1642. title: (
  1643. <Checkbox
  1644. indeterminate={
  1645. getCurrentPageCheckPointIds().length > 0 &&
  1646. selectedCheckPointKeys.length > 0 &&
  1647. selectedCheckPointKeys.length < getCurrentPageCheckPointIds().length
  1648. }
  1649. checked={
  1650. getCurrentPageCheckPointIds().length > 0 &&
  1651. selectedCheckPointKeys.length === getCurrentPageCheckPointIds().length
  1652. }
  1653. onChange={(e) => {
  1654. const ids = getCurrentPageCheckPointIds();
  1655. if (e.target.checked) {
  1656. setSelectedCheckPointKeys(Array.from(new Set([...selectedCheckPointKeys, ...ids])));
  1657. } else {
  1658. setSelectedCheckPointKeys(selectedCheckPointKeys.filter((id) => !ids.includes(id)));
  1659. }
  1660. }}
  1661. >
  1662. 要点名称
  1663. </Checkbox>
  1664. ),
  1665. dataIndex: 'checkPointName',
  1666. width: 200,
  1667. align: 'left' as 'left',
  1668. render: (dom: React.ReactNode, record: any) => {
  1669. if (record.rowSpan > 0) {
  1670. return {
  1671. children: (
  1672. <div style={{ display: 'flex', alignItems: 'center' }}>
  1673. <Checkbox
  1674. checked={selectedCheckPointKeys.includes(record.id)}
  1675. onChange={() => {
  1676. if (selectedCheckPointKeys.includes(record.id)) {
  1677. setSelectedCheckPointKeys(
  1678. selectedCheckPointKeys.filter((k) => k !== record.id),
  1679. );
  1680. } else {
  1681. setSelectedCheckPointKeys([
  1682. ...selectedCheckPointKeys,
  1683. record.id,
  1684. ]);
  1685. }
  1686. }}
  1687. />
  1688. <span style={{ marginLeft: 8 }}>{dom}</span>
  1689. </div>
  1690. ),
  1691. props: { rowSpan: record.rowSpan },
  1692. };
  1693. }
  1694. return { children: null, props: { rowSpan: 0 } };
  1695. },
  1696. },
  1697. {
  1698. title: '查核项名称',
  1699. dataIndex: 'checkItemName',
  1700. align: 'left' as 'left',
  1701. },
  1702. {
  1703. title: '操作',
  1704. dataIndex: 'option',
  1705. width: 80,
  1706. align: 'center' as 'center',
  1707. render: (_: any, record: any) => {
  1708. if (record.rowSpan > 0) {
  1709. return {
  1710. children: (
  1711. <Popconfirm
  1712. title="确认删除该要点及其所有查核项?"
  1713. okText="确定"
  1714. cancelText="取消"
  1715. onConfirm={() => handleDeleteCheckPoint(record)}
  1716. >
  1717. <a style={{ color: '#3377ff' }}>删除</a>
  1718. </Popconfirm>
  1719. ),
  1720. props: { rowSpan: record.rowSpan },
  1721. };
  1722. }
  1723. return { children: null, props: { rowSpan: 0 } };
  1724. },
  1725. },
  1726. ];
  1727. // 4. 打开弹窗方法
  1728. const handleOpenCheckPointModal = () => {
  1729. setCheckPointModalVisible(true);
  1730. setSelectedCheckPointKeys([]);
  1731. setTimeout(() => {
  1732. checkPointTableRef?.reload?.();
  1733. }, 100);
  1734. };
  1735. // 选择查核要点弹窗表格数据获取方法
  1736. const getDeptPendingCheckpointData = async (params: any) => {
  1737. try {
  1738. if (!selectedDept?.key) {
  1739. return { data: [], success: true, total: 0 };
  1740. }
  1741. const res = await getDeptPendingCheckpoint({
  1742. deptId: selectedDept.key,
  1743. filter: '',
  1744. deptType: selectedDept.deptType ?? 0,
  1745. });
  1746. if (Array.isArray(res)) {
  1747. const flatData: any[] = [];
  1748. res.forEach((point: any) => {
  1749. if (
  1750. Array.isArray(point.checkItemList) &&
  1751. point.checkItemList.length > 0
  1752. ) {
  1753. point.checkItemList.forEach((item: any, idx: number) => {
  1754. flatData.push({
  1755. id: idx === 0 ? String(point.id) : `${point.id}_item_${idx}`,
  1756. name: point.name,
  1757. checkItemName: item.name,
  1758. rowSpan: idx === 0 ? point.checkItemList.length : 0,
  1759. isFirst: idx === 0,
  1760. });
  1761. });
  1762. } else {
  1763. flatData.push({
  1764. id: String(point.id),
  1765. name: point.name,
  1766. checkItemName: '-',
  1767. rowSpan: 1,
  1768. isFirst: true,
  1769. });
  1770. }
  1771. });
  1772. return {
  1773. data: flatData,
  1774. success: true,
  1775. total: flatData.length,
  1776. };
  1777. }
  1778. return { data: [], success: true, total: 0 };
  1779. } catch (e) {
  1780. return { data: [], success: false, total: 0 };
  1781. }
  1782. };
  1783. // 选择查核要点弹窗"确定"按钮逻辑
  1784. const handleConfirmAddCheckPoints = async () => {
  1785. if (!selectedDept?.key) {
  1786. message.warning('请先选择查核单位');
  1787. return;
  1788. }
  1789. if (selectedCheckPointKeys.length === 0) {
  1790. message.warning('请至少选择一个查核要点');
  1791. return;
  1792. }
  1793. try {
  1794. await addDeptCheckpoint({
  1795. id: String(selectedDept.key),
  1796. type: String(selectedDept.deptType ?? 0),
  1797. mapIdList: selectedCheckPointKeys,
  1798. });
  1799. message.success('添加查核要点成功');
  1800. setCheckPointModalVisible(false);
  1801. // 可选:刷新Tab下表格
  1802. checkPointsTableRef.current?.reload();
  1803. } catch (e) {
  1804. message.error('添加查核要点失败');
  1805. }
  1806. };
  1807. // 弹窗columns
  1808. const checkPointModalColumns = [
  1809. {
  1810. title: '要点名称',
  1811. dataIndex: 'name',
  1812. width: 200,
  1813. align: 'left' as 'left',
  1814. render: (dom: React.ReactNode, record: any) => {
  1815. if (record.rowSpan > 0) {
  1816. return {
  1817. children: (
  1818. <div style={{ display: 'flex', alignItems: 'center' }}>
  1819. <Checkbox
  1820. checked={selectedCheckPointKeys.includes(record.id)}
  1821. onChange={() => {
  1822. if (selectedCheckPointKeys.includes(record.id)) {
  1823. setSelectedCheckPointKeys(
  1824. selectedCheckPointKeys.filter((k) => k !== record.id),
  1825. );
  1826. } else {
  1827. setSelectedCheckPointKeys([
  1828. ...selectedCheckPointKeys,
  1829. record.id,
  1830. ]);
  1831. }
  1832. }}
  1833. />
  1834. <span style={{ marginLeft: 8 }}>{dom}</span>
  1835. </div>
  1836. ),
  1837. props: { rowSpan: record.rowSpan },
  1838. };
  1839. }
  1840. return { children: null, props: { rowSpan: 0 } };
  1841. },
  1842. },
  1843. {
  1844. title: '查核项名称',
  1845. dataIndex: 'checkItemName',
  1846. align: 'left' as 'left',
  1847. },
  1848. ];
  1849. return (
  1850. <KCIMPagecontainer className="check-unit-mana" title={false} style={{ position: 'relative' }}>
  1851. <div className="check-unit-container" style={{ position: 'relative' }}>
  1852. {/* 左侧科室树 */}
  1853. {renderDeptTree()}
  1854. {/* 右侧内容区 */}
  1855. <div className="dept-content" style={{ position: 'relative', paddingBottom: (activeTab === 'personnel' && selectedPersonnelKeys.length > 0) || (activeTab === 'checkPoints' && selectedCheckPointKeys.length > 0) ? 68 : 0 }}>
  1856. <Card bordered={false} ref={cardRef}>
  1857. <div className="dept-header content-padding">
  1858. {selectedDept && (
  1859. <>
  1860. <span className="dept-title">{selectedDept.title}</span>
  1861. <span className="dept-info">
  1862. 单位分类:{selectedDept.parentTitle || '暂无'} &nbsp;
  1863. 分类查核条件:
  1864. {selectedDept.filterConditionList
  1865. ? selectedDept.filterConditionList
  1866. .map((item: any) => item.name)
  1867. .join('、')
  1868. : '手术室、重症加护病房、医学影像'}
  1869. </span>
  1870. </>
  1871. )}
  1872. </div>
  1873. <Tabs activeKey={activeTab} onChange={handleTabChange}>
  1874. <TabPane tab="单位人员" key="personnel">
  1875. <div className="tab-content">
  1876. <div className="toolbar">
  1877. <Input
  1878. placeholder="工号、姓名"
  1879. allowClear
  1880. value={personnelSearchInput}
  1881. suffix={
  1882. <SearchOutlined
  1883. onClick={() => handleSearch(personnelSearchInput)}
  1884. style={{ cursor: 'pointer', color: '#99A6BF' }}
  1885. />
  1886. }
  1887. style={{ width: 250 }}
  1888. onChange={(e) => {
  1889. setPersonnelSearchInput(e.target.value);
  1890. if (e.target.value === '') {
  1891. setSearchKeywords('');
  1892. tableRef.current?.reload();
  1893. }
  1894. }}
  1895. onPressEnter={(e) => handleSearch(e.currentTarget.value)}
  1896. />
  1897. <Button
  1898. type="primary"
  1899. onClick={handleAddPersonnel}
  1900. className="add-btn"
  1901. size="small"
  1902. >
  1903. 添加
  1904. </Button>
  1905. </div>
  1906. <KCIMTable
  1907. actionRef={tableRef}
  1908. columns={personnelColumns}
  1909. request={getPersonnelData}
  1910. rowKey="id"
  1911. search={false}
  1912. rowSelection={{
  1913. selectedRowKeys: selectedPersonnelKeys,
  1914. onChange: (keys) => setSelectedPersonnelKeys(keys as string[]),
  1915. }}
  1916. locale={{ emptyText: customizeRenderEmpty() }}
  1917. pagination={false}
  1918. tableAlertRender={false}
  1919. />
  1920. </div>
  1921. </TabPane>
  1922. <TabPane tab="单位查核要点" key="checkPoints">
  1923. <div className="tab-content">
  1924. <div className="toolbar" style={{ display: 'flex', alignItems: 'center' }}>
  1925. <Input
  1926. placeholder="要点名称"
  1927. allowClear
  1928. suffix={
  1929. <SearchOutlined
  1930. onClick={() => handleSearchCheckPoints(checkPointsSearchKeyword)}
  1931. style={{ cursor: 'pointer', color: '#99A6BF' }}
  1932. />
  1933. }
  1934. style={{ width: 250 }}
  1935. value={checkPointsSearchKeyword}
  1936. onChange={(e) => {
  1937. setCheckPointsSearchKeyword(e.target.value);
  1938. if (e.target.value === '') {
  1939. // 清空搜索时自动刷新
  1940. checkPointsTableRef.current?.reload();
  1941. }
  1942. }}
  1943. onPressEnter={(e) =>
  1944. handleSearchCheckPoints(e.currentTarget.value)
  1945. }
  1946. />
  1947. <Button
  1948. type="primary"
  1949. onClick={handleOpenCheckPointModal}
  1950. className="add-btn"
  1951. size="small"
  1952. style={{ marginLeft: 8 }}
  1953. >
  1954. 添加
  1955. </Button>
  1956. </div>
  1957. <KCIMTable
  1958. actionRef={checkPointsTableRef}
  1959. columns={checkPointTabColumns}
  1960. request={getDeptCheckpointData}
  1961. rowKey="id"
  1962. bordered
  1963. search={false}
  1964. pagination={false}
  1965. tableAlertRender={false}
  1966. scroll={{ y: 360 }}
  1967. />
  1968. </div>
  1969. </TabPane>
  1970. </Tabs>
  1971. </Card>
  1972. </div>
  1973. </div>
  1974. {/* 批量操作栏 - 固定在页面底部,参考empMana样式 */}
  1975. {activeTab === 'personnel' && selectedPersonnelKeys.length > 0 && (
  1976. <div
  1977. style={{
  1978. position: 'absolute',
  1979. bottom: 0,
  1980. left: 0,
  1981. right: 0,
  1982. height: 48,
  1983. background: '#FFFFFF',
  1984. boxShadow: '0px -8px 16px 0px rgba(64,85,128,0.1)',
  1985. display: 'flex',
  1986. justifyContent: 'space-between',
  1987. alignItems: 'center',
  1988. padding: '0 16px',
  1989. zIndex: 1000,
  1990. }}
  1991. >
  1992. <span style={{
  1993. color: '#333333',
  1994. fontSize: 14,
  1995. fontFamily: 'SourceHanSansCN-Normal, SourceHanSansCN',
  1996. fontWeight: 400,
  1997. }}>
  1998. 已选择 <span style={{ color: '#3377ff' }}>{selectedPersonnelKeys.length}</span> 项,将进行批量删除
  1999. </span>
  2000. <div style={{ display: 'flex', gap: 12 }}>
  2001. <div
  2002. onClick={() => {
  2003. setSelectedPersonnelKeys([]);
  2004. }}
  2005. style={{
  2006. cursor: 'pointer',
  2007. display: 'inline-block',
  2008. fontSize: 14,
  2009. fontFamily: 'SourceHanSansCN-Normal, SourceHanSansCN',
  2010. fontWeight: 400,
  2011. color: '#666666',
  2012. lineHeight: '24px',
  2013. padding: '0 14px',
  2014. background: '#ffffff',
  2015. border: '1px solid #dae2f2',
  2016. borderRadius: 4,
  2017. }}
  2018. >
  2019. 取消选择
  2020. </div>
  2021. <Popconfirm
  2022. title="确认批量删除所选人员?"
  2023. okText="确定"
  2024. cancelText="取消"
  2025. onConfirm={async () => {
  2026. try {
  2027. await delDeptEmp(
  2028. selectedPersonnelKeys.map((id) => ({
  2029. id: String(id),
  2030. type: String(selectedDept.deptType ?? 0),
  2031. })),
  2032. );
  2033. message.success('批量删除成功');
  2034. setSelectedPersonnelKeys([]);
  2035. tableRef.current?.reload();
  2036. } catch (e) {
  2037. message.error('批量删除失败');
  2038. }
  2039. }}
  2040. >
  2041. <div
  2042. style={{
  2043. cursor: 'pointer',
  2044. display: 'inline-block',
  2045. fontSize: 14,
  2046. fontFamily: 'SourceHanSansCN-Normal, SourceHanSansCN',
  2047. fontWeight: 400,
  2048. color: '#ffffff',
  2049. lineHeight: '24px',
  2050. padding: '0 14px',
  2051. background: '#3377ff',
  2052. borderRadius: 4,
  2053. }}
  2054. >
  2055. 批量删除
  2056. </div>
  2057. </Popconfirm>
  2058. </div>
  2059. </div>
  2060. )}
  2061. {activeTab === 'checkPoints' && !checkPointModalVisible && selectedCheckPointKeys.length > 0 && (
  2062. <div
  2063. style={{
  2064. position: 'absolute',
  2065. bottom: 0,
  2066. left: 0,
  2067. right: 0,
  2068. height: 48,
  2069. background: '#FFFFFF',
  2070. boxShadow: '0px -8px 16px 0px rgba(64,85,128,0.1)',
  2071. display: 'flex',
  2072. justifyContent: 'space-between',
  2073. alignItems: 'center',
  2074. padding: '0 16px',
  2075. zIndex: 1000,
  2076. }}
  2077. >
  2078. <span style={{
  2079. color: '#333333',
  2080. fontSize: 14,
  2081. fontFamily: 'SourceHanSansCN-Normal, SourceHanSansCN',
  2082. fontWeight: 400,
  2083. }}>
  2084. 已选择 <span style={{ color: '#3377ff' }}>{selectedCheckPointKeys.length}</span> 项,将进行批量删除
  2085. </span>
  2086. <div style={{ display: 'flex', gap: 12 }}>
  2087. <div
  2088. onClick={() => {
  2089. setSelectedCheckPointKeys([]);
  2090. }}
  2091. style={{
  2092. cursor: 'pointer',
  2093. display: 'inline-block',
  2094. fontSize: 14,
  2095. fontFamily: 'SourceHanSansCN-Normal, SourceHanSansCN',
  2096. fontWeight: 400,
  2097. color: '#666666',
  2098. lineHeight: '24px',
  2099. padding: '0 14px',
  2100. background: '#ffffff',
  2101. border: '1px solid #dae2f2',
  2102. borderRadius: 4,
  2103. }}
  2104. >
  2105. 取消选择
  2106. </div>
  2107. <Popconfirm
  2108. title="确认批量删除所选查核要点?"
  2109. okText="确定"
  2110. cancelText="取消"
  2111. onConfirm={async () => {
  2112. try {
  2113. await delDeptCheckpoint(selectedCheckPointKeys);
  2114. message.success('批量删除成功');
  2115. setSelectedCheckPointKeys([]);
  2116. checkPointsTableRef.current?.reload();
  2117. } catch (e) {
  2118. message.error('批量删除失败');
  2119. }
  2120. }}
  2121. >
  2122. <div
  2123. style={{
  2124. cursor: 'pointer',
  2125. display: 'inline-block',
  2126. fontSize: 14,
  2127. fontFamily: 'SourceHanSansCN-Normal, SourceHanSansCN',
  2128. fontWeight: 400,
  2129. color: '#ffffff',
  2130. lineHeight: '24px',
  2131. padding: '0 14px',
  2132. background: '#3377ff',
  2133. borderRadius: 4,
  2134. }}
  2135. >
  2136. 批量删除
  2137. </div>
  2138. </Popconfirm>
  2139. </div>
  2140. </div>
  2141. )}
  2142. {/* 查核条件选择弹窗 */}
  2143. <Modal
  2144. title="选择查核条件(病房)"
  2145. open={conditionModalVisible}
  2146. onCancel={() => setConditionModalVisible(false)}
  2147. width={532}
  2148. footer={[
  2149. <Button key="cancel" onClick={() => setConditionModalVisible(false)}>
  2150. 取消
  2151. </Button>,
  2152. <Button
  2153. key="confirm"
  2154. type="primary"
  2155. onClick={handleConfirmConditions}
  2156. >
  2157. 确定
  2158. {selectedConditions.length > 0
  2159. ? `(${selectedConditions.length})`
  2160. : ''}
  2161. </Button>,
  2162. ]}
  2163. >
  2164. <div style={{ marginBottom: 8 }}>
  2165. <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
  2166. <Select
  2167. value={conditionSearchType}
  2168. onChange={(value) => {
  2169. setConditionSearchType(value);
  2170. // 切换类型时重置选择状态和搜索关键词
  2171. setSelectedConditions([]);
  2172. setConditionSearchKeyword('');
  2173. // 切换类型时重新拉取数据
  2174. conditionTableRef.current?.reload();
  2175. }}
  2176. style={{ width: 120 }}
  2177. options={[
  2178. { label: '个案', value: '个案' },
  2179. { label: '系统', value: '系统' },
  2180. ]}
  2181. />
  2182. <Input
  2183. placeholder="条件名称"
  2184. value={conditionSearchKeyword}
  2185. allowClear
  2186. onChange={(e) => {
  2187. setConditionSearchKeyword(e.target.value);
  2188. // 实时搜索,触发表格重新渲染
  2189. conditionTableRef.current?.reload();
  2190. }}
  2191. onPressEnter={() => conditionTableRef.current?.reload()}
  2192. suffix={
  2193. <SearchOutlined
  2194. onClick={() => conditionTableRef.current?.reload()}
  2195. style={{ cursor: 'pointer', color: '#99A6BF' }}
  2196. />
  2197. }
  2198. style={{ flex: 1 }}
  2199. />
  2200. </div>
  2201. </div>
  2202. <KCIMTable
  2203. actionRef={conditionTableRef}
  2204. columns={conditionColumns}
  2205. request={getConditionData}
  2206. rowKey="id"
  2207. search={false}
  2208. expandable={{
  2209. defaultExpandAllRows: true, // 默认展开所有行
  2210. childrenColumnName: 'children', // 指定子数据的字段名
  2211. }}
  2212. pagination={{
  2213. simple: true,
  2214. pageSize: 10,
  2215. size: 'small',
  2216. showTotal: () => null,
  2217. }}
  2218. rowSelection={{
  2219. selectedRowKeys: selectedConditions,
  2220. onChange: (selectedRowKeys) => {
  2221. setSelectedConditions(selectedRowKeys as string[]);
  2222. },
  2223. checkStrictly: false, // 启用父子关联选择
  2224. }}
  2225. tableAlertRender={false}
  2226. scroll={{ y: 360 }}
  2227. />
  2228. </Modal>
  2229. {/* 选择人员弹窗 */}
  2230. <Modal
  2231. title="选择人员(产科病房)"
  2232. open={employeeModalVisible}
  2233. onCancel={() => setEmployeeModalVisible(false)}
  2234. width={372}
  2235. footer={[
  2236. <Button key="cancel" onClick={() => setEmployeeModalVisible(false)}>
  2237. 取消
  2238. </Button>,
  2239. <Button
  2240. key="confirm"
  2241. type="primary"
  2242. onClick={handleConfirmAddEmployees}
  2243. style={{ backgroundColor: '#3377FF', borderColor: '#3377FF' }}
  2244. >
  2245. 确定
  2246. {selectedEmployees.length > 0
  2247. ? ` (${selectedEmployees.length})`
  2248. : ''}
  2249. </Button>,
  2250. ]}
  2251. >
  2252. <div style={{ marginBottom: 8 }}>
  2253. <Input
  2254. placeholder="请输入"
  2255. value={employeeSearchKeyword}
  2256. allowClear
  2257. onChange={(e) => {
  2258. setEmployeeSearchKeyword(e.target.value);
  2259. if (e.target.value === '') {
  2260. // 清空搜索时自动刷新
  2261. employeeTableRef.current?.reload();
  2262. }
  2263. }}
  2264. onPressEnter={() => employeeTableRef.current?.reload()}
  2265. suffix={
  2266. <SearchOutlined
  2267. onClick={() => employeeTableRef.current?.reload()}
  2268. style={{ cursor: 'pointer', color: '#99A6BF' }}
  2269. />
  2270. }
  2271. style={{ width: '100%' }}
  2272. />
  2273. </div>
  2274. <KCIMTable
  2275. actionRef={employeeTableRef}
  2276. columns={[
  2277. {
  2278. title: '工号',
  2279. dataIndex: 'empCode',
  2280. width: 120,
  2281. align: 'left',
  2282. },
  2283. {
  2284. title: '姓名',
  2285. dataIndex: 'empName',
  2286. align: 'left',
  2287. },
  2288. ]}
  2289. request={getEmployeeSelectData}
  2290. rowKey="id"
  2291. search={false}
  2292. pagination={{
  2293. simple: true,
  2294. pageSize: 10,
  2295. size: 'small',
  2296. showTotal: () => null,
  2297. }}
  2298. rowSelection={{
  2299. selectedRowKeys: selectedEmployees,
  2300. onChange: (selectedRowKeys) => {
  2301. setSelectedEmployees(selectedRowKeys as string[]);
  2302. },
  2303. }}
  2304. tableAlertRender={false}
  2305. scroll={{ y: 360 }}
  2306. />
  2307. </Modal>
  2308. {/* 选择查核要点弹窗 */}
  2309. <Modal
  2310. title={`选择查核要点(${selectedDept?.title || ''})`}
  2311. open={checkPointModalVisible}
  2312. onCancel={() => setCheckPointModalVisible(false)}
  2313. width={700}
  2314. footer={[
  2315. <Button key="cancel" onClick={() => setCheckPointModalVisible(false)}>
  2316. 取消
  2317. </Button>,
  2318. <Button
  2319. key="confirm"
  2320. type="primary"
  2321. onClick={handleConfirmAddCheckPoints}
  2322. >
  2323. 确定
  2324. {selectedCheckPointKeys.length > 0
  2325. ? `(${selectedCheckPointKeys.length})`
  2326. : ''}
  2327. </Button>,
  2328. ]}
  2329. >
  2330. {/* fragment 包裹,防止JSX语法错误 */}
  2331. <>
  2332. <KCIMTable
  2333. actionRef={checkPointTableRef}
  2334. columns={checkPointModalColumns}
  2335. request={getDeptPendingCheckpointData}
  2336. rowKey="id"
  2337. bordered
  2338. search={false}
  2339. pagination={{
  2340. simple: true,
  2341. pageSize: 10,
  2342. size: 'small',
  2343. showTotal: () => null,
  2344. }}
  2345. tableAlertRender={false}
  2346. scroll={{ y: 360 }}
  2347. />
  2348. </>
  2349. </Modal>
  2350. </KCIMPagecontainer>
  2351. );
  2352. }