index.jsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. import React, { useState, useEffect, useRef } from 'react'
  2. import ProTable from '@ant-design/pro-table'
  3. import { Row, Col, Select, Card, Skeleton } from 'antd'
  4. import { AddressBook, City, Finance, Comment } from '@icon-park/react'
  5. import consts from '@/consts'
  6. import { useModal } from '@/components/Modal'
  7. import { queryDashboard } from '@/services/dashboard'
  8. import ReminderList from './components/ReminderList'
  9. import LeaderBoard from './components/LeaderBoard'
  10. import SoftLeaderboard from './components/SoftLeaderboard'
  11. import { cardTypeMap, cyclicalOp } from './consts'
  12. import BusinessChar from './components/BusinessChar'
  13. import RatioPanels from './components/RatioPanels'
  14. import PermSelect, { PermDataTypeEunm } from '@/pages/Customer/Company/components/PermSelect'
  15. import { getAuthCache, getPermAuthCache, setAuthCache } from '@/utils/auth'
  16. import { WORKBENCH_CYCLICAL_KEY } from '@/utils/cache/cacheEnum'
  17. import styles from './index.less'
  18. const Dashboard = () => {
  19. const timer = useRef(null)
  20. const [defaultPermAuthCache, defaultStaffIds] = getPermAuthCache(PermDataTypeEunm.WORKBENCH)
  21. const cyclicalAuthCache = getAuthCache(WORKBENCH_CYCLICAL_KEY)
  22. let defaultCyclicalValue = 'week'
  23. if (cyclicalAuthCache) {
  24. defaultCyclicalValue = cyclicalAuthCache
  25. }
  26. const [state, setState] = useState({
  27. disabled: true,
  28. loading: true,
  29. staffIds: null,
  30. immediate: false,
  31. visible: false,
  32. reminderData: [],
  33. leaderboard: [],
  34. productLeaderBoard: [],
  35. clientChainRatio: {},
  36. customerChainRatio: {},
  37. businessChainRatio: {},
  38. serviceLogChainRatio: {},
  39. aggregation: {},
  40. businessChar: {},
  41. params: {
  42. businessGroupId: '',
  43. dataPermission: defaultPermAuthCache,
  44. staffIds: defaultStaffIds,
  45. cyclical: defaultCyclicalValue
  46. }
  47. })
  48. const { toggleModal, setModalProps } = useModal()
  49. // 展示服务弹窗详情
  50. const handleReminderList = (dataType, reminderCyclical) => {
  51. setModalProps({
  52. width: '90vw',
  53. modalRender: () => <ReminderList dataType={dataType} reminderCyclical={reminderCyclical} />,
  54. onCancel: () => toggleModal()
  55. })
  56. toggleModal()
  57. }
  58. // 展示卡片详情
  59. const handleRatioCard = dataType => {
  60. setModalProps({
  61. width: '90vw',
  62. modalRender: () => (
  63. <RatioPanels
  64. dataType={dataType}
  65. staffIds={state.params.staffIds}
  66. retioCyclical={state.params.cyclical}
  67. dataPermission={state.params.dataPermission}
  68. />
  69. ),
  70. onCancel: () => toggleModal()
  71. })
  72. toggleModal()
  73. }
  74. const initData = async payload => {
  75. if (timer.current) clearTimeout(timer.current)
  76. setState({ ...state, disabled: true })
  77. const {
  78. code = -1,
  79. data: {
  80. reminderData = [],
  81. leaderboard = [],
  82. productLeaderBoard = {},
  83. clientChainRatio = [],
  84. customerChainRatio = {},
  85. businessChainRatio = {},
  86. serviceLogChainRatio = {},
  87. aggregation = {},
  88. businessChar = {}
  89. }
  90. } = await queryDashboard(payload)
  91. if (code === consts.RET_CODE.SUCCESS) {
  92. if (state.immediate) {
  93. setState({
  94. ...state,
  95. loading: false,
  96. immediate: false,
  97. reminderData,
  98. leaderboard,
  99. productLeaderBoard,
  100. clientChainRatio,
  101. customerChainRatio,
  102. businessChainRatio,
  103. serviceLogChainRatio,
  104. aggregation,
  105. disabled: false,
  106. businessChar
  107. })
  108. return
  109. }
  110. timer.current = setTimeout(() => {
  111. setState({
  112. ...state,
  113. loading: false,
  114. reminderData,
  115. leaderboard,
  116. productLeaderBoard,
  117. clientChainRatio,
  118. customerChainRatio,
  119. businessChainRatio,
  120. serviceLogChainRatio,
  121. aggregation,
  122. disabled: false,
  123. businessChar
  124. })
  125. }, 300)
  126. }
  127. }
  128. useEffect(() => {
  129. initData(state.params)
  130. }, [
  131. state.params.businessGroupId,
  132. state.params.dataPermission,
  133. state.params.cyclical,
  134. state.params.staffIds
  135. ])
  136. const columns = [
  137. {
  138. title: '超过周期未服务提醒',
  139. dataIndex: 'name'
  140. },
  141. {
  142. title: '7天',
  143. dataIndex: 'day7',
  144. render: (name, record) => (
  145. <span
  146. onClick={() => handleReminderList(record.type, '7day')}
  147. className="text-primary cursor-pointer hover:text-[#967bbd]">
  148. {name}
  149. </span>
  150. )
  151. },
  152. {
  153. title: '15天',
  154. dataIndex: 'day15',
  155. render: (name, record) => (
  156. <span
  157. onClick={() => handleReminderList(record.type, '15day')}
  158. className="text-primary cursor-pointer hover:text-[#967bbd]">
  159. {name}
  160. </span>
  161. )
  162. },
  163. {
  164. title: '30天',
  165. dataIndex: 'day30',
  166. render: (name, record) => (
  167. <span
  168. onClick={() => handleReminderList(record.type, '30day')}
  169. className="text-primary cursor-pointer hover:text-[#967bbd]">
  170. {name}
  171. </span>
  172. )
  173. },
  174. {
  175. title: '3个月',
  176. dataIndex: 'month3',
  177. render: (name, record) => (
  178. <span
  179. onClick={() => handleReminderList(record.type, '3month')}
  180. className="text-primary cursor-pointer hover:text-[#967bbd]">
  181. {name}
  182. </span>
  183. )
  184. },
  185. {
  186. title: '6个月',
  187. dataIndex: 'month6',
  188. render: (name, record) => (
  189. <span
  190. onClick={() => handleReminderList(record.type, '6month')}
  191. className="text-primary cursor-pointer hover:text-[#967bbd]">
  192. {name}
  193. </span>
  194. )
  195. },
  196. {
  197. title: '备忘',
  198. dataIndex: 'remind',
  199. render: (name, record) => (
  200. <span
  201. onClick={() => handleReminderList(record.type, 'reminder')}
  202. className="text-primary cursor-pointer hover:text-[#967bbd]">
  203. {name}
  204. </span>
  205. )
  206. }
  207. ]
  208. const handleChangeGroupId = e =>
  209. setState({ ...state, immediate: true, params: { ...state.params, businessGroupId: e } })
  210. // 处理权限Select下拉组件OnChange事件
  211. const handlePermChange = ({ staffIds = [], dataPermission }) => {
  212. if (staffIds?.length) {
  213. setState({
  214. ...state,
  215. immediate: true,
  216. params: { ...state.params, staffIds, dataPermission: 'custom' }
  217. })
  218. return
  219. }
  220. setState({
  221. ...state,
  222. immediate: true,
  223. params: { ...state.params, staffIds: null, dataPermission }
  224. })
  225. }
  226. const handleCyclicalChange = value => {
  227. setAuthCache(WORKBENCH_CYCLICAL_KEY, value)
  228. setState({ ...state, immediate: true, params: { ...state.params, cyclical: value } })
  229. }
  230. return (
  231. <div className={styles.dashboard}>
  232. <div className="shadow-card">
  233. <ProTable
  234. bordered
  235. loading={state.loading}
  236. rowKey={record => record.type + Date.now()}
  237. columns={columns}
  238. dataSource={state.reminderData}
  239. search={false}
  240. toolBarRender={false}
  241. tableRender={(_, dom) => (
  242. <Card bodyStyle={{ padding: state.loading ? '24px' : 0 }} loading={state.loading}>
  243. {dom}
  244. </Card>
  245. )}
  246. pagination={false}
  247. />
  248. </div>
  249. <div className="mt-4">
  250. <PermSelect
  251. loading={state.loading}
  252. disabled={state.disabled}
  253. onConfirm={handlePermChange}
  254. dataType="workbench"
  255. />
  256. <span className="ml-4">
  257. <Select
  258. disabled={state.disabled}
  259. loading={state.loading}
  260. options={cyclicalOp}
  261. dropdownMatchSelectWidth={false}
  262. defaultValue={defaultCyclicalValue}
  263. onChange={handleCyclicalChange}
  264. />
  265. </span>
  266. </div>
  267. <div className="mt-4">
  268. <Row gutter={24}>
  269. <Col
  270. className="gutter-row"
  271. sm={{ span: 24 }}
  272. md={{ span: 24 }}
  273. lg={{ span: 6 }}
  274. xl={{ span: 6 }}>
  275. <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
  276. {state.loading ? (
  277. <Skeleton active avatar />
  278. ) : (
  279. <Row className="cursor-pointer" onClick={() => handleRatioCard(cardTypeMap.CLIENT)}>
  280. <Col span={6}>
  281. <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
  282. <AddressBook size="26" fill="#fff" />
  283. </div>
  284. </Col>
  285. <Col span={18} flex="wrap">
  286. <div className="flex items-center justify-between ">
  287. <div className="text-2xl">{state.clientChainRatio.count}</div>
  288. <span
  289. className={[
  290. 'text-xl',
  291. state.clientChainRatio.percentage.startsWith('-')
  292. ? 'text-green-500'
  293. : 'text-red-500'
  294. ].join(' ')}>
  295. {state.clientChainRatio.percentage}
  296. </span>
  297. </div>
  298. <div className="flex justify-between items-center">
  299. <div>新增客户</div>
  300. <span>
  301. {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
  302. </span>
  303. </div>
  304. </Col>
  305. </Row>
  306. )}
  307. </div>
  308. </Col>
  309. <Col
  310. className="gutter-row"
  311. sm={{ span: 24 }}
  312. md={{ span: 24 }}
  313. lg={{ span: 6 }}
  314. xl={{ span: 6 }}>
  315. <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
  316. {state.loading ? (
  317. <Skeleton active avatar />
  318. ) : (
  319. <Row
  320. className="cursor-pointer"
  321. onClick={() => handleRatioCard(cardTypeMap.COMPANY)}>
  322. <Col span={6}>
  323. <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
  324. <City size="26" fill="#fff" />
  325. </div>
  326. </Col>
  327. <Col span={18} flex="wrap">
  328. <div className="flex items-center justify-between ">
  329. <div className="text-2xl">{state.customerChainRatio.count}</div>
  330. <span
  331. className={[
  332. 'text-xl',
  333. state.customerChainRatio.percentage.startsWith('-')
  334. ? 'text-green-500'
  335. : 'text-red-500'
  336. ].join(' ')}>
  337. {state.customerChainRatio.percentage}
  338. </span>
  339. </div>
  340. <div className="flex justify-between items-center">
  341. <div>新增单位</div>
  342. <span>
  343. {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
  344. </span>
  345. </div>
  346. </Col>
  347. </Row>
  348. )}
  349. </div>
  350. </Col>
  351. <Col
  352. className="gutter-row"
  353. sm={{ span: 24 }}
  354. md={{ span: 24 }}
  355. lg={{ span: 6 }}
  356. xl={{ span: 6 }}>
  357. <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
  358. {state.loading ? (
  359. <Skeleton active avatar />
  360. ) : (
  361. <Row
  362. className="cursor-pointer"
  363. onClick={() => handleRatioCard(cardTypeMap.BUSINESS)}>
  364. <Col span={6}>
  365. <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
  366. <Finance size="26" fill="#fff" />
  367. </div>
  368. </Col>
  369. <Col span={18} flex="wrap">
  370. <div className="flex items-center justify-between ">
  371. <div className="text-2xl">{state.businessChainRatio.count}</div>
  372. <span
  373. className={[
  374. 'text-xl',
  375. state.businessChainRatio.percentage.startsWith('-')
  376. ? 'text-green-500'
  377. : 'text-red-500'
  378. ].join(' ')}>
  379. {state.businessChainRatio.percentage}
  380. </span>
  381. </div>
  382. <div className="flex justify-between items-center">
  383. <div>新增商机</div>
  384. <span>
  385. {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
  386. </span>
  387. </div>
  388. </Col>
  389. </Row>
  390. )}
  391. </div>
  392. </Col>
  393. <Col
  394. className="gutter-row"
  395. sm={{ span: 24 }}
  396. md={{ span: 24 }}
  397. lg={{ span: 6 }}
  398. xl={{ span: 6 }}>
  399. <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
  400. {state.loading ? (
  401. <Skeleton active avatar />
  402. ) : (
  403. <Row
  404. onClick={() => handleRatioCard(cardTypeMap.SERVICE)}
  405. className="cursor-pointer">
  406. <Col span={6}>
  407. <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
  408. <Comment size="26" fill="#fff" />
  409. </div>
  410. </Col>
  411. <Col span={18} flex="wrap">
  412. <div className="flex items-center justify-between ">
  413. <div className="text-2xl">{state.serviceLogChainRatio.count}</div>
  414. <span
  415. className={[
  416. 'text-xl',
  417. state.serviceLogChainRatio.percentage.startsWith('-')
  418. ? 'text-green-500'
  419. : 'text-red-500'
  420. ].join(' ')}>
  421. {state.serviceLogChainRatio.percentage}
  422. </span>
  423. </div>
  424. <div className="flex justify-between items-center">
  425. <div>服务记录</div>
  426. <span>
  427. {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
  428. </span>
  429. </div>
  430. </Col>
  431. </Row>
  432. )}
  433. </div>
  434. </Col>
  435. </Row>
  436. </div>
  437. <div className="mt-4">
  438. <Row gutter={24}>
  439. <Col
  440. className="gutter-row"
  441. sm={{ span: 24 }}
  442. md={{ span: 24 }}
  443. lg={{ span: 16 }}
  444. xl={{ span: 16 }}>
  445. <BusinessChar
  446. loading={state.loading}
  447. data={state.businessChar}
  448. changeGroupId={handleChangeGroupId}
  449. />
  450. </Col>
  451. <Col
  452. className="gutter-row"
  453. sm={{ span: 24 }}
  454. md={{ span: 24 }}
  455. lg={{ span: 8 }}
  456. xl={{ span: 8 }}>
  457. <Card
  458. headStyle={{ padding: '0 12px' }}
  459. title="数据汇总"
  460. className="shadow-card"
  461. bodyStyle={{ padding: state.loading ? '24px' : 0 }}
  462. loading={state.loading}>
  463. <ul>
  464. <li className="px-12px py-16px border-b-1">
  465. 新增客户<b className="px-1">{state.aggregation.clientCount}</b>个,服务
  466. <b className="px-1">{state.aggregation.clientServiceCount}</b> 条
  467. </li>
  468. <li className="px-12px py-16px border-b-1">
  469. 新增单位<b className="px-1">{state.aggregation.customerCount}</b>个,服务
  470. <b className="px-1">{state.aggregation.customerServiceCount}</b>条
  471. </li>
  472. <li className="px-12px py-16px">
  473. 新增商机<b className="px-1">{state.aggregation.businessCount}</b>个,金额
  474. <b className="px-1">{state.aggregation.businessSum}</b> 元,服务
  475. <b className="px-1">{state.aggregation.businessServiceCount}</b>
  476. 条,赢单<b className="px-1">{state.aggregation.businessWinCount}</b>个
  477. </li>
  478. </ul>
  479. </Card>
  480. </Col>
  481. </Row>
  482. </div>
  483. <div className="mt-4">
  484. <Row gutter={24}>
  485. <Col
  486. className="gutter-row"
  487. sm={{ span: 24 }}
  488. md={{ span: 24 }}
  489. lg={{ span: 12 }}
  490. xl={{ span: 12 }}>
  491. <LeaderBoard dataList={state.leaderboard} loading={state.loading} />
  492. </Col>
  493. <Col
  494. className="gutter-row"
  495. sm={{ span: 24 }}
  496. md={{ span: 24 }}
  497. lg={{ span: 12 }}
  498. xl={{ span: 12 }}>
  499. <SoftLeaderboard
  500. productLeaderBoard={state.productLeaderBoard}
  501. loading={state.loading}
  502. />
  503. </Col>
  504. </Row>
  505. </div>
  506. </div>
  507. )
  508. }
  509. export default Dashboard