match.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /*
  2. * @Author: code4eat awesomedema@gmail.com
  3. * @Date: 2024-03-06 10:43:05
  4. * @LastEditors: code4eat awesomedema@gmail.com
  5. * @LastEditTime: 2024-06-20 21:03:12
  6. * @FilePath: /CostAccountingSys/src/pages/costLibraryManagement/projectCostManagement/chargeItemsMana/components/match.tsx
  7. * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  8. */
  9. import { KCIMTable } from "@/components/KCIMTable";
  10. import ProgressModal from "@/components/ProgressModal";
  11. import { createFromIconfontCN } from "@ant-design/icons";
  12. import { ProColumns, ProFormText } from "@ant-design/pro-components";
  13. import { Progress, message, Modal, Skeleton } from 'antd';
  14. import { useEffect, useState } from "react";
  15. import { getAllNeedMatchItems, getMatchCount, getReferralMatch, getSingleItem, mapItemAndStandItem, matchRequest } from "../service";
  16. import './style.less';
  17. const IconFont = createFromIconfontCN({
  18. scriptUrl: '',
  19. });
  20. interface SignalBarProps {
  21. level: number; // 信号强度级别,通常在0-4之间
  22. }
  23. const SignalBar: React.FC<SignalBarProps> = ({ level }) => {
  24. const maxLevel = 5;
  25. const signalLevel = Math.round(level * maxLevel); // 现在signalLevel是0到4之间的整数
  26. const signalBars = [];
  27. for (let i = 0; i < maxLevel; i++) {
  28. signalBars.push(
  29. <div
  30. key={i}
  31. className={`signal-bar ${i < signalLevel ? 'active' : ''}`}
  32. style={{
  33. marginLeft: i > 0 ? '2px' : undefined,
  34. height: `${8 + (i * 2)}px`, // 使每个条更高一些
  35. // 定义更多样式...
  36. }}
  37. />
  38. );
  39. }
  40. return <div className="signal-container">{signalBars}</div>;
  41. };
  42. export const MatchPage = (props: any) => {
  43. const { record, single = true, closeDrawer, switchBatchHandle } = props;
  44. const [matchItemInfo, set_matchItemInfo] = useState<undefined | any>(undefined);
  45. const [allNeedMatchList, set_allNeedMatchList] = useState<any[]>([]);
  46. const [allStandItem, set_allStandItem] = useState<any[]>([]);
  47. const [matchStandItem, set_matchStandItem] = useState<any[]>([]);
  48. const [dataSource, set_dataSource] = useState<any[]>([]);
  49. const [chargeItemTabledataSource, set_chargeItemTabledataSource] = useState<any[]>([]);
  50. const [searchText, setSearchText] = useState('');
  51. const [chargeItemSearchText, setChargeItemSearchText] = useState('');
  52. const [countInfo, set_countInfo] = useState<undefined | any>(undefined);
  53. const [currentTablePage, set_currentTablePage] = useState(1);
  54. const [openTable, set_openTable] = useState(false);
  55. const [isProgressModalVisible, set_isProgressModalVisible] = useState(false);
  56. const [loading, set_loading] = useState(true);
  57. const leftTableColumns = [
  58. {
  59. title: '标准项目编码',
  60. dataIndex: 'code',
  61. },
  62. {
  63. title: '标准项目名称',
  64. dataIndex: 'name',
  65. },
  66. {
  67. title: '康程分类',
  68. dataIndex: 'kcClassName',
  69. },
  70. {
  71. title: '操作',
  72. key: 'option',
  73. width: 80,
  74. valueType: 'option',
  75. render: (_: any, data: any) => {
  76. const { code } = data;
  77. return code != matchItemInfo?.standItemCode ? [
  78. <span key='btn' onClick={() => matchRequestHandle(data,1)} style={{
  79. display: 'inline-block', width: 56, height: 24, cursor: 'pointer',
  80. backgroundColor: '#FAFCFF', borderRadius: 4, border: '1px solid #DAE2F2', textAlign: 'center', color: '#17181A'
  81. }}>匹配</span>
  82. ] : [
  83. <span key='btn' style={{ fontWeight: 500, fontSize: 14, color: '#00BF8F' }}>当前匹配</span>
  84. ]
  85. },
  86. },
  87. ];
  88. const chargeTableColumns: ProColumns[] = [
  89. {
  90. title: '编号',
  91. width: 80,
  92. dataIndex: 'id',
  93. },
  94. {
  95. title: '收费项目编码',
  96. dataIndex: 'code',
  97. },
  98. {
  99. title: '收费项目名称',
  100. dataIndex: 'name',
  101. },
  102. {
  103. title: '状态',
  104. dataIndex: 'match',
  105. width: 90,
  106. renderText(bool, record, index, action) {
  107. return <>{bool ? <span style={{ fontSize: 14, color: '#17181A' }}>已匹配</span> : <span style={{ fontSize: 14, color: '#FF8C19' }}>待匹配</span>}{matchItemInfo?.code == record.code && <IconFont style={{ color: '#3377FF', display: 'inline-block', marginLeft: 16 }} type="iconqueren" />}</>
  108. },
  109. },
  110. ]
  111. const getMatchInfo = async () => {
  112. const { code } = record;
  113. const resp = await getSingleItem(code,record.departmentCode);
  114. if (resp) {
  115. set_matchItemInfo(resp);
  116. }
  117. }
  118. const getNeedMatchList = async () => {
  119. const resp = await getAllNeedMatchItems();
  120. if (resp) {
  121. set_allNeedMatchList(resp);
  122. }
  123. }
  124. const getReferralMatchHandle = async () => {
  125. set_loading(true);
  126. const { code,departmentCode } = matchItemInfo;
  127. const resp = await getReferralMatch(code,single?record.departmentCode:departmentCode);
  128. if (resp) {
  129. const { allStandItem: all = [], matchStandItem: match = [] } = resp;
  130. const temp = [...all];
  131. const index = temp.findIndex((item: any) => item.code == matchItemInfo.standItemCode);
  132. if (index !== -1) {
  133. // 如果找到了符合条件的项,先将其从数组中移除
  134. const [item] = temp.splice(index, 1);
  135. // 然后将其添加到数组的开头
  136. temp.unshift(item);
  137. }
  138. set_allStandItem([...temp]);
  139. set_matchStandItem(match?Array.from(new Map(match.map((a: any) => [a.code, a])).values()):[]);
  140. set_dataSource([...all]);
  141. set_loading(false);
  142. }
  143. }
  144. const switchNeddMatchItem = async (flag: number) => {
  145. const currentIndex = allNeedMatchList.findIndex((a) => a.code == matchItemInfo.code);
  146. if ((currentIndex == allNeedMatchList.length - 1) && flag > 0) {
  147. message.info('已经是最后一项!');
  148. return;
  149. }
  150. if ((currentIndex == 0) && flag < 0) {
  151. message.info('没有上一项!');
  152. return;
  153. }
  154. let next = undefined;
  155. if (flag > 0) {
  156. for (const item of [...allNeedMatchList].splice(currentIndex + 1, allNeedMatchList.length)) {
  157. if (!item.match) {
  158. next = item
  159. break; // 满足条件后退出循环
  160. }
  161. }
  162. }
  163. if (flag < 0) {
  164. for (const item of ([...allNeedMatchList].splice(0, currentIndex)).reverse()) {
  165. if (!item.match) {
  166. next = item
  167. break; // 满足条件后退出循环
  168. }
  169. }
  170. }
  171. // console.log({switchNeddMatchItem,currentIndex,next});
  172. if (next) {
  173. set_matchItemInfo(next);
  174. } else {
  175. message.info('未找到未匹配项!');
  176. getMatchInfo();
  177. getNeedMatchList();
  178. }
  179. }
  180. const matchRequestHandle = async (data: any,type:number) => {
  181. try {
  182. const { code: itemCode,departmentCode } = matchItemInfo;
  183. const { code: standItemCode } = data;
  184. const resp = await matchRequest(itemCode, standItemCode,type,departmentCode);
  185. if (resp) {
  186. if (!single) {
  187. switchNeddMatchItem(1);
  188. getNeedMatchList();
  189. } else {
  190. getMatchInfo();
  191. getNeedMatchList();
  192. }
  193. }
  194. } catch (error) {
  195. console.log('matchRequestHandle_error', error);
  196. }
  197. }
  198. const getRightsInfo = async () => {
  199. const resp = await getMatchCount();
  200. if (resp) {
  201. set_countInfo(resp);
  202. }
  203. }
  204. const openTableHandle = (bool: boolean) => {
  205. if (bool) {
  206. const currentIndex = allNeedMatchList.findIndex((a) => a.code == matchItemInfo.code);
  207. set_currentTablePage(Math.ceil((currentIndex + 1) / 10));
  208. set_chargeItemTabledataSource([...allNeedMatchList]);
  209. }
  210. set_openTable(bool);
  211. }
  212. const startBatchCalc = async () => {
  213. set_isProgressModalVisible(true);
  214. const resp = await mapItemAndStandItem();
  215. if (resp) {
  216. set_isProgressModalVisible(false);
  217. }
  218. }
  219. const mapBtnHandle = async () => {
  220. Modal.confirm({
  221. title: '注意',
  222. content: '对照操作只会自动匹配未对照的项目,是否继续操作?',
  223. okText: '确定',
  224. cancelText: '取消',
  225. onOk: (...args) => {
  226. startBatchCalc();
  227. },
  228. })
  229. }
  230. const goMatch = () => {
  231. getReferralMatchHandle()
  232. }
  233. useEffect(() => {
  234. if (matchItemInfo) {
  235. getReferralMatchHandle();
  236. getRightsInfo();
  237. }
  238. }, [matchItemInfo]);
  239. useEffect(() => {
  240. const filteredData = allStandItem.filter((item) => {
  241. return item.code.includes(searchText) || item.name.includes(searchText);
  242. });
  243. set_dataSource(filteredData);
  244. }, [searchText, allStandItem]);
  245. useEffect(() => {
  246. const filteredData = allNeedMatchList.filter((item) => {
  247. return item.code.includes(chargeItemSearchText) || item.name.includes(chargeItemSearchText);
  248. });
  249. set_chargeItemTabledataSource(filteredData);
  250. }, [chargeItemSearchText]);
  251. useEffect(() => {
  252. if (!single) getRightsInfo();
  253. }, [single]);
  254. useEffect(() => {
  255. if (record) {
  256. getMatchInfo();
  257. getNeedMatchList();
  258. }
  259. }, [record])
  260. return (
  261. <div className="matchPage">
  262. <ProgressModal
  263. title="正在匹配"
  264. type={'ITEM_BATCH_MATCH'}
  265. onComplete={() => {
  266. getMatchInfo();
  267. getNeedMatchList();
  268. }}
  269. visible={isProgressModalVisible}
  270. />
  271. {
  272. single && (
  273. <div className="header">
  274. <div className="title">{record ? record.standItemName : '项目'}</div>
  275. <div className="btnGroup">
  276. <IconFont onClick={() => { closeDrawer() }} type="iconquxiao" style={{ color: '#17181A', marginRight: 8, cursor: 'pointer', fontSize: 21 }} />
  277. </div>
  278. </div>
  279. )
  280. }
  281. <div className="content" style={{ padding: single ? 16 : 0 }}>
  282. <div className="left">
  283. <div className="leftRowOne">
  284. {
  285. !single && (
  286. <div className="leftRowOneHeader">
  287. <div className="menu">
  288. <IconFont type="iconliebiao" style={{ color: '#17181A', marginRight: 8, cursor: 'pointer' }} onClick={() => openTableHandle(!openTable)} />
  289. 当前项目:{matchItemInfo?.id}
  290. </div>
  291. <div className="btnGroup">
  292. <div className="backPrev btns" onClick={() => { switchBatchHandle && switchBatchHandle(false) }}>返回上一层</div>
  293. <div className="line"></div>
  294. <div className="prev btns" onClick={() => switchNeddMatchItem(-1)}>上一项</div>
  295. <div className="next btns" onClick={() => switchNeddMatchItem(1)}>下一项</div>
  296. </div>
  297. {
  298. openTable && (
  299. <div className="floatTable" style={{ width: 432 }}>
  300. <div style={{ marginBottom: 12 }}>
  301. <div style={{ marginBottom: 12 }}>
  302. <ProFormText noStyle placeholder={'收费项目编码、名称'}
  303. fieldProps={{
  304. // value: keyword,
  305. suffix: <IconFont style={{ color: '#99A6BF' }} type="iconsousuo" />,
  306. onChange: (e) => {
  307. if (e.target.value.length != 0) {
  308. setChargeItemSearchText(e.target.value);
  309. } else {
  310. setChargeItemSearchText('');
  311. }
  312. }
  313. }}
  314. />
  315. </div>
  316. <KCIMTable columns={chargeTableColumns}
  317. options={{
  318. density: true,
  319. setting: {
  320. listsHeight: 100,
  321. },
  322. }}
  323. dataSource={[...chargeItemTabledataSource]}
  324. rowKey={'id'}
  325. onRow={(record) => {
  326. return {
  327. onClick: () => { set_matchItemInfo(record); set_openTable(false) }
  328. }
  329. }}
  330. tableAlertRender={false}
  331. rowClassName={(record) => record.code == matchItemInfo.code ? 'match' : ''}
  332. pagination={{ showTitle: false, showSizeChanger: true, simple: true, defaultPageSize: 10, defaultCurrent: currentTablePage }}
  333. />
  334. </div>
  335. </div>
  336. )
  337. }
  338. </div>
  339. )
  340. }
  341. <div className="leftRowOneContent">
  342. <div className="blocks">
  343. <div className="value">{matchItemInfo?.code}</div>
  344. <div className="label">项目编码</div>
  345. </div>
  346. <div className="blocks">
  347. <div className="value">{matchItemInfo?.name}</div>
  348. <div className="label">项目名称</div>
  349. </div>
  350. <div className="blocks">
  351. <div className="value">{matchItemInfo?.type}</div>
  352. <div className="label">项目类别</div>
  353. </div>
  354. <div className="blocks">
  355. <div className="value">{(matchItemInfo?.match) ? '已匹配' : '未匹配'}</div>
  356. <div className="label">匹配状态</div>
  357. </div>
  358. </div>
  359. </div>
  360. <div className="leftTable">
  361. <div className="leftTableTitle">手动匹配</div>
  362. <div style={{ marginBottom: 12 }}>
  363. <ProFormText noStyle placeholder={'标准项目编码、名称'}
  364. fieldProps={{
  365. // value: keyword,
  366. suffix: <IconFont style={{ color: '#99A6BF' }} type="iconsousuo" />,
  367. onChange: (e) => {
  368. if (e.target.value.length != 0) {
  369. setSearchText(e.target.value);
  370. } else {
  371. setSearchText('');
  372. }
  373. }
  374. }}
  375. />
  376. </div>
  377. <KCIMTable columns={leftTableColumns}
  378. options={{
  379. density: true,
  380. setting: {
  381. listsHeight: 100,
  382. },
  383. }}
  384. loading={loading}
  385. dataSource={[...dataSource]}
  386. rowKey={'id'}
  387. tableAlertRender={false}
  388. pagination={{ showTitle: false, showSizeChanger: true, simple: true, defaultPageSize: 12 }}
  389. />
  390. </div>
  391. </div>
  392. <div className="right">
  393. {
  394. !single && (
  395. <div className="rightRowOne">
  396. <div className="processCycle">
  397. <Progress type="circle" format={percent => <span style={{ color: '52c41a !important' }}>{`${percent}%`}</span>} percent={countInfo ? Math.floor((countInfo.trueMatch / countInfo.allCount) * 100) : 0} width={64} />
  398. <div className="processInfo">
  399. <div className="total">全部项目:{countInfo ? countInfo.allCount : 0}</div>
  400. <div className="subInfo">
  401. <span>已匹配:<a>{countInfo ? countInfo.trueMatch : 0}</a></span> <span>待匹配:<a>{countInfo ? countInfo.falseMatch : 0}</a></span>
  402. </div>
  403. </div>
  404. </div>
  405. <div className="btn" onClick={() => mapBtnHandle()}>
  406. <IconFont style={{ color: '#fff',fontSize:14,marginRight:4 }} type="iconshuoming" />智能匹配
  407. <div className="description">
  408. <img src={require('../../../../../../static/tanhao.png')} alt="" style={{ width: 16, height: 16, marginRight: 4 }} />
  409. <div className="detail">
  410. <div className="detailTitle">匹配规则</div>
  411. <div className="detailContent">
  412. 1、逐个匹配未匹配的项目,已匹配的项目会自动跳过<br />
  413. 2、与标准项目国家编码精准匹配的项目状态设置成已匹配<br />
  414. 3、未精准匹配的项目会通过智能匹配算法形成推荐项,需用户逐个选择匹配项
  415. </div>
  416. </div>
  417. </div>
  418. </div>
  419. </div>
  420. )
  421. }
  422. <div className="rightContent">
  423. <div className="rightContentHeader">
  424. <span className="title">推荐匹配</span>
  425. <span className="btn" onClick={()=>switchNeddMatchItem(1)}><IconFont style={{ color: '#3376FE',paddingRight:4 }} type="iconshuangyou" />跳过该项</span>
  426. </div>
  427. <div className="content">
  428. {
  429. loading && (
  430. <Skeleton active />
  431. )
  432. }
  433. {
  434. !loading && matchStandItem.map((item, index) => {
  435. const { code } = item;
  436. return (
  437. <div className="list" key={index}>
  438. <div className="leftDetail">
  439. <div className="rowOne">
  440. <div className="title">{item.name}</div>
  441. <div className="Signal"><SignalBar level={item.percent ? item.percent : 0} />{item.percentDisplay}</div>
  442. </div>
  443. <div className="rowTwo">
  444. <div className="code">普通门诊诊察费</div>
  445. <div className="type">康程分类:诊察费/西医诊察费</div>
  446. </div>
  447. </div>
  448. {code != matchItemInfo?.standItemCode ? <div className="btn" onClick={() => matchRequestHandle(item,2)}>匹配</div> : <div className="btnText">当前匹配</div>}
  449. </div>
  450. )
  451. })
  452. }
  453. {
  454. !loading && matchStandItem.length == 0 && (
  455. <div className="empty">
  456. <img src={require('../../../../../../static/empty.png')} alt="" />
  457. <div className="title">暂无推荐项目</div>
  458. <div className="titleSub">当前暂无推荐匹配项目,您可尝试进行对照或</div>
  459. <a className="btn" onClick={() => goMatch()}>去匹配</a>
  460. </div>
  461. )
  462. }
  463. </div>
  464. </div>
  465. </div>
  466. </div>
  467. </div>
  468. )
  469. }