index.jsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import React, { useState, useEffect, useRef } from 'react'
  2. import ProTable from '@ant-design/pro-table'
  3. import { Row, Col, Select, Card, Skeleton, TreeSelect } from 'antd'
  4. import { AddressBook, City, Comment } from '@icon-park/react'
  5. import consts from '@/consts'
  6. import { connect, useModel } from 'umi'
  7. import { queryDashboard } from '@/services/dashboard'
  8. import { queryStaff } from '@/services/staff'
  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 PermSelect, { PermDataTypeEunm } from '@/pages/Customer/Company/components/PermSelect'
  14. import { getAuthCache, getPermAuthCache, setAuthCache } from '@/utils/auth'
  15. import { WORKBENCH_CYCLICAL_KEY } from '@/utils/cache/cacheEnum'
  16. import styles from './index.less'
  17. import { isDevMode } from '@/utils/env'
  18. import { isMobile } from '@/utils/is'
  19. import { useModal } from '@/components/ModalStore'
  20. const Dashboard = ({ dispatch, departments = [] }) => {
  21. const { initialState: { currentUser } = { currentUser: {} } } = useModel('@@initialState')
  22. const timer = useRef(null)
  23. const [defaultPermAuthCache, defaultStaffIds] = getPermAuthCache(PermDataTypeEunm.WORKBENCH)
  24. const cyclicalAuthCache = getAuthCache(WORKBENCH_CYCLICAL_KEY)
  25. let defaultCyclicalValue = 'week'
  26. if (cyclicalAuthCache) {
  27. defaultCyclicalValue = cyclicalAuthCache
  28. }
  29. const [state, setState] = useState({
  30. selectId: '',
  31. disabled: true,
  32. loading: true,
  33. immediate: false,
  34. visible: false,
  35. reminderData: [],
  36. leaderboard: [],
  37. productLeaderBoard: [],
  38. clientChainRatio: {},
  39. customerChainRatio: {},
  40. businessChainRatio: {},
  41. serviceLogChainRatio: {},
  42. aggregation: {},
  43. businessChar: {},
  44. staffList: [],
  45. params: {
  46. businessGroupId: '',
  47. dataPermission: defaultPermAuthCache, // 'all' 'custom'
  48. staffIds: defaultStaffIds, // ['xxxx'] || null
  49. cyclical: defaultCyclicalValue
  50. }
  51. })
  52. const dispatchModal = useModal()
  53. // 展示服务弹窗详情
  54. const handleReminderList = (dataType, reminderCyclical) => {
  55. dispatchModal('M_REMINDER_LIST', { dataType, reminderCyclical })
  56. }
  57. // 展示卡片详情
  58. const handleRatioCard = dataType => {
  59. dispatchModal('M_RATIO_PANELS', {
  60. dataType,
  61. staffIds: state.params.staffIds,
  62. retioCyclical: state.params.cyclical,
  63. dataPermission: state.params.dataPermission
  64. })
  65. }
  66. // dataPermission-all 拉取部门下员工列表
  67. const queryStaffList = async (departmentId, canSetState = true) => {
  68. const { code = -1, data: { list = [] } = { list: [] } } = await queryStaff({
  69. departmentId,
  70. current: 1,
  71. pageSize: 100
  72. })
  73. if (code === consts.RET_CODE.SUCCESS && canSetState) {
  74. // list存起来
  75. setState({
  76. ...state,
  77. staffList: list
  78. })
  79. }
  80. return list
  81. }
  82. const handleStaffClick = id => {
  83. const sId = `s_${id}`
  84. if (state.params.staffIds?.includes(sId)) {
  85. setState({
  86. ...state,
  87. selectId: null,
  88. immediate: true,
  89. params: { ...state.params, staffIds: null }
  90. })
  91. return
  92. }
  93. setState({
  94. ...state,
  95. selectId: id,
  96. immediate: true,
  97. params: { ...state.params, staffIds: [sId] }
  98. })
  99. }
  100. const initData = async payload => {
  101. if (timer.current) clearTimeout(timer.current)
  102. setState({ ...state, disabled: true })
  103. const {
  104. code = -1,
  105. data: {
  106. reminderData = [],
  107. leaderboard = [],
  108. productLeaderBoard = {},
  109. clientChainRatio = [],
  110. customerChainRatio = {},
  111. businessChainRatio = {},
  112. serviceLogChainRatio = {},
  113. aggregation = {},
  114. businessChar = {}
  115. }
  116. } = await queryDashboard(payload)
  117. if (code === consts.RET_CODE.SUCCESS) {
  118. const otherState = {}
  119. if (!state.staffList.length && state.params.dataPermission === 'department' && currentUser) {
  120. otherState.staffList = await queryStaffList(currentUser.departmentId, false)
  121. }
  122. if (state.immediate) {
  123. setState({
  124. ...state,
  125. ...otherState,
  126. loading: false,
  127. immediate: false,
  128. reminderData,
  129. leaderboard,
  130. productLeaderBoard,
  131. clientChainRatio,
  132. customerChainRatio,
  133. businessChainRatio,
  134. serviceLogChainRatio,
  135. aggregation,
  136. disabled: false,
  137. businessChar
  138. })
  139. return
  140. }
  141. timer.current = setTimeout(() => {
  142. setState({
  143. ...state,
  144. ...otherState,
  145. loading: false,
  146. reminderData,
  147. leaderboard,
  148. productLeaderBoard,
  149. clientChainRatio,
  150. customerChainRatio,
  151. businessChainRatio,
  152. serviceLogChainRatio,
  153. aggregation,
  154. disabled: false,
  155. businessChar
  156. })
  157. }, 300)
  158. }
  159. }
  160. useEffect(() => {
  161. initData(state.params)
  162. if (!departments?.length && state.params.dataPermission === 'all') {
  163. dispatch({
  164. type: 'staff/fetchDepartments'
  165. })
  166. }
  167. }, [
  168. state.params.businessGroupId,
  169. state.params.dataPermission,
  170. state.params.cyclical,
  171. state.params.staffIds
  172. ])
  173. const columns = [
  174. {
  175. title: '超过周期未服务提醒',
  176. dataIndex: 'name'
  177. },
  178. {
  179. title: '7天',
  180. dataIndex: 'day7',
  181. render: (name, record) => (
  182. <span
  183. onClick={() => handleReminderList(record.type, '7day')}
  184. className="text-primary cursor-pointer hover:text-hex-967bbd"
  185. >
  186. {name}
  187. </span>
  188. )
  189. },
  190. {
  191. title: '15天',
  192. dataIndex: 'day15',
  193. render: (name, record) => (
  194. <span
  195. onClick={() => handleReminderList(record.type, '15day')}
  196. className="text-primary cursor-pointer hover:text-hex-967bbd"
  197. >
  198. {name}
  199. </span>
  200. )
  201. },
  202. {
  203. title: '30天',
  204. dataIndex: 'day30',
  205. render: (name, record) => (
  206. <span
  207. onClick={() => handleReminderList(record.type, '30day')}
  208. className="text-primary cursor-pointer hover:text-hex-967bbd"
  209. >
  210. {name}
  211. </span>
  212. )
  213. },
  214. {
  215. title: '3个月',
  216. dataIndex: 'month3',
  217. render: (name, record) => (
  218. <span
  219. onClick={() => handleReminderList(record.type, '3month')}
  220. className="text-primary cursor-pointer hover:text-hex-967bbd"
  221. >
  222. {name}
  223. </span>
  224. )
  225. },
  226. {
  227. title: '9个月',
  228. dataIndex: 'month9',
  229. render: (name, record) => (
  230. <span
  231. onClick={() => handleReminderList(record.type, '9month')}
  232. className="text-primary cursor-pointer hover:text-hex-967bbd"
  233. >
  234. {name}
  235. </span>
  236. )
  237. }
  238. ]
  239. const columnsSmall = [
  240. {
  241. title: '已到期备忘',
  242. dataIndex: 'expiry',
  243. render: (_, record) => (
  244. <span
  245. onClick={() => handleReminderList(record.type, 'expiry')}
  246. className="text-primary cursor-pointer hover:text-hex-967bbd"
  247. >
  248. {record.remind.expiry}
  249. </span>
  250. )
  251. },
  252. {
  253. title: '未到期备忘',
  254. dataIndex: 'unexpired',
  255. render: (_, record) => (
  256. <span
  257. onClick={() => handleReminderList(record.type, 'unexpired')}
  258. className="text-primary cursor-pointer hover:text-hex-967bbd"
  259. >
  260. {record.remind.unexpired}
  261. </span>
  262. )
  263. }
  264. ]
  265. // 处理权限Select下拉组件OnChange事件
  266. const handlePermChange = ({ staffIds = [], dataPermission }) => {
  267. if (staffIds?.length) {
  268. setState({
  269. ...state,
  270. immediate: true,
  271. staffList: [],
  272. selectId: '',
  273. params: { ...state.params, staffIds, dataPermission: 'custom' }
  274. })
  275. return
  276. }
  277. setState({
  278. ...state,
  279. immediate: true,
  280. staffList: [],
  281. selectId: '',
  282. params: { ...state.params, staffIds: null, dataPermission }
  283. })
  284. }
  285. const handleCyclicalChange = value => {
  286. setAuthCache(WORKBENCH_CYCLICAL_KEY, value)
  287. setState({ ...state, immediate: true, params: { ...state.params, cyclical: value } })
  288. }
  289. return (
  290. <div className={styles.dashboard}>
  291. <div>
  292. <div className="shadow-card">
  293. <ProTable
  294. bordered
  295. loading={state.loading}
  296. rowKey={record => record.type + Date.now()}
  297. columns={columns}
  298. dataSource={state.reminderData}
  299. search={false}
  300. toolBarRender={false}
  301. tableRender={(_, dom) => (
  302. <Card bodyStyle={{ padding: state.loading ? '24px' : 0 }} loading={state.loading}>
  303. {dom}
  304. </Card>
  305. )}
  306. pagination={false}
  307. />
  308. </div>
  309. {/* <Row gutter={[8, 8]}>
  310. <Col xs={24} sm={24} md={24} lg={18} xl={18}>
  311. <div className="shadow-card">
  312. <ProTable
  313. bordered
  314. loading={state.loading}
  315. rowKey={record => record.type + Date.now()}
  316. columns={columns}
  317. dataSource={state.reminderData}
  318. search={false}
  319. toolBarRender={false}
  320. tableRender={(_, dom) => (
  321. <Card bodyStyle={{ padding: state.loading ? '24px' : 0 }} loading={state.loading}>
  322. {dom}
  323. </Card>
  324. )}
  325. pagination={false}
  326. />
  327. </div>
  328. </Col>
  329. <Col xs={24} sm={24} md={24} lg={6} xl={6}>
  330. <div className="shadow-card">
  331. <ProTable
  332. bordered
  333. loading={state.loading}
  334. rowKey={record => record.type + Date.now()}
  335. columns={columnsSmall}
  336. dataSource={state.reminderData}
  337. search={false}
  338. toolBarRender={false}
  339. tableRender={(_, dom) => (
  340. <Card bodyStyle={{ padding: state.loading ? '24px' : 0 }} loading={state.loading}>
  341. {dom}
  342. </Card>
  343. )}
  344. pagination={false}
  345. />
  346. </div>
  347. </Col>
  348. </Row> */}
  349. </div>
  350. <div className="mt-4 flex flex-row ">
  351. <span className="mx-4 my-4">
  352. <Select
  353. disabled={state.disabled}
  354. loading={state.loading}
  355. options={cyclicalOp}
  356. dropdownMatchSelectWidth={false}
  357. defaultValue={defaultCyclicalValue}
  358. onChange={handleCyclicalChange}
  359. />
  360. <span className="mx-4">
  361. <PermSelect
  362. loading={state.loading}
  363. disabled={state.disabled}
  364. onConfirm={handlePermChange}
  365. dataType="workbench"
  366. />
  367. </span>
  368. {state.params.dataPermission === 'all' ? (
  369. <TreeSelect
  370. size="middle"
  371. style={{ width: 150 }}
  372. dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
  373. loading={state.loading}
  374. disabled={state.disabled}
  375. treeDefaultExpandAll
  376. placeholder="办事处"
  377. treeData={departments}
  378. allowClear
  379. onSelect={queryStaffList}
  380. onChange={e => {
  381. if (!e)
  382. setState({
  383. ...state,
  384. params: { ...state.params, staffIds: null },
  385. staffList: [],
  386. selectId: null
  387. })
  388. }}
  389. />
  390. ) : null}
  391. </span>
  392. <div className="flex items-center flex-row ml-2">
  393. {['all', 'department'].includes(state.params.dataPermission) &&
  394. state.staffList.map(item => (
  395. <div key={item.id} onClick={() => handleStaffClick(item.id)}>
  396. <div
  397. className={[
  398. 'relative',
  399. 'cursor-pointer',
  400. state.selectId === item.id ? styles.imgBg : styles.paddingBg
  401. ].join(' ')}
  402. >
  403. <img
  404. className="w-full max-w-30px h-30px border rounded-30px"
  405. src={
  406. (isDevMode() ? 'http://cld2qa.com' : 'http://zhcld.com') +
  407. (item?.avatar ? item.avatar : '/global/img/avtra_2.jpg')
  408. }
  409. />
  410. <span className={['absolute', styles.avtraName].join(' ')}>
  411. <span className={styles.bb}>{item.username}</span>
  412. </span>
  413. </div>
  414. </div>
  415. ))}
  416. </div>
  417. </div>
  418. <div className="mt-4">
  419. <Row gutter={[8, 8]}>
  420. <Col xs={24} sm={24} md={24} lg={8} xl={8}>
  421. <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
  422. {state.loading ? (
  423. <Skeleton active avatar />
  424. ) : (
  425. <Row className="cursor-pointer" onClick={() => handleRatioCard(cardTypeMap.CLIENT)}>
  426. <Col span={6}>
  427. <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
  428. <AddressBook size="26" fill="#fff" />
  429. </div>
  430. </Col>
  431. <Col span={18} flex="wrap">
  432. <div className="flex items-center justify-between ">
  433. <div className="text-2xl">{state.clientChainRatio.count}</div>
  434. <span
  435. className={[
  436. 'text-xl',
  437. state.clientChainRatio.percentage.startsWith('-')
  438. ? 'text-green-500'
  439. : 'text-red-500'
  440. ].join(' ')}
  441. >
  442. {state.clientChainRatio.percentage}
  443. </span>
  444. </div>
  445. <div className="flex justify-between items-center">
  446. <div>新增客户</div>
  447. <span>{cyclicalOp.find(item => item.value === state.params.cyclical)?.title}</span>
  448. </div>
  449. </Col>
  450. </Row>
  451. )}
  452. </div>
  453. </Col>
  454. <Col xs={24} sm={24} md={24} lg={8} xl={8}>
  455. <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
  456. {state.loading ? (
  457. <Skeleton active avatar />
  458. ) : (
  459. <Row className="cursor-pointer" onClick={() => handleRatioCard(cardTypeMap.COMPANY)}>
  460. <Col span={6}>
  461. <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
  462. <City size="26" fill="#fff" />
  463. </div>
  464. </Col>
  465. <Col span={18} flex="wrap">
  466. <div className="flex items-center justify-between ">
  467. <div className="text-2xl">{state.customerChainRatio.count}</div>
  468. <span
  469. className={[
  470. 'text-xl',
  471. state.customerChainRatio.percentage.startsWith('-')
  472. ? 'text-green-500'
  473. : 'text-red-500'
  474. ].join(' ')}
  475. >
  476. {state.customerChainRatio.percentage}
  477. </span>
  478. </div>
  479. <div className="flex justify-between items-center">
  480. <div>新增单位</div>
  481. <span>{cyclicalOp.find(item => item.value === state.params.cyclical)?.title}</span>
  482. </div>
  483. </Col>
  484. </Row>
  485. )}
  486. </div>
  487. </Col>
  488. {/* <Col xs={24} sm={24} md={24} lg={6} xl={6}>
  489. <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
  490. {state.loading ? (
  491. <Skeleton active avatar />
  492. ) : (
  493. <Row
  494. className="cursor-pointer"
  495. onClick={() => handleRatioCard(cardTypeMap.BUSINESS)}>
  496. <Col span={6}>
  497. <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
  498. <Finance size="26" fill="#fff" />
  499. </div>
  500. </Col>
  501. <Col span={18} flex="wrap">
  502. <div className="flex items-center justify-between ">
  503. <div className="text-2xl">{state.businessChainRatio.count}</div>
  504. <span
  505. className={[
  506. 'text-xl',
  507. state.businessChainRatio.percentage.startsWith('-')
  508. ? 'text-green-500'
  509. : 'text-red-500'
  510. ].join(' ')}>
  511. {state.businessChainRatio.percentage}
  512. </span>
  513. </div>
  514. <div className="flex justify-between items-center">
  515. <div>新增商机</div>
  516. <span>
  517. {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
  518. </span>
  519. </div>
  520. </Col>
  521. </Row>
  522. )}
  523. </div>
  524. </Col> */}
  525. <Col xs={24} sm={24} md={24} lg={8} xl={8}>
  526. <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
  527. {state.loading ? (
  528. <Skeleton active avatar />
  529. ) : (
  530. <Row onClick={() => handleRatioCard(cardTypeMap.SERVICE)} className="cursor-pointer">
  531. <Col span={6}>
  532. <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
  533. <Comment size="26" fill="#fff" />
  534. </div>
  535. </Col>
  536. <Col span={18} flex="wrap">
  537. <div className="flex items-center justify-between ">
  538. <div className="text-2xl">{state.serviceLogChainRatio.count}</div>
  539. <span
  540. className={[
  541. 'text-xl',
  542. state.serviceLogChainRatio.percentage.startsWith('-')
  543. ? 'text-green-500'
  544. : 'text-red-500'
  545. ].join(' ')}
  546. >
  547. {state.serviceLogChainRatio.percentage}
  548. </span>
  549. </div>
  550. <div className="flex justify-between items-center">
  551. <div>服务记录</div>
  552. <span>{cyclicalOp.find(item => item.value === state.params.cyclical)?.title}</span>
  553. </div>
  554. </Col>
  555. </Row>
  556. )}
  557. </div>
  558. </Col>
  559. </Row>
  560. </div>
  561. <div className="mt-4">
  562. <Row gutter={[8, 8]}>
  563. {/* <Col xs={24} sm={24} md={24} lg={12} xl={12}>
  564. <BusinessChar
  565. loading={state.loading}
  566. data={state.businessChar}
  567. changeGroupId={handleChangeGroupId}
  568. />
  569. </Col> */}
  570. <Col xs={24} sm={24} md={24} lg={24} xl={24}>
  571. <Card
  572. headStyle={{ padding: '0 12px' }}
  573. title="数据汇总"
  574. className="shadow-card"
  575. bodyStyle={{ padding: state.loading ? '24px' : 0 }}
  576. loading={state.loading}
  577. >
  578. <ul>
  579. <li className="px-12px py-16px border-b-1">
  580. 新增客户<b className="px-1">{state.aggregation.clientCount}</b>个,服务
  581. <b className="px-1">{state.aggregation.clientServiceCount}</b> 条
  582. </li>
  583. <li className="px-12px py-16px border-b-1">
  584. 新增单位<b className="px-1">{state.aggregation.customerCount}</b>个,服务
  585. <b className="px-1">{state.aggregation.customerServiceCount}</b>条
  586. </li>
  587. <li className="px-12px py-16px">
  588. 新增商机<b className="px-1">{state.aggregation.businessCount}</b>个,金额
  589. <b className="px-1">{state.aggregation.businessSum}</b> 元,服务
  590. <b className="px-1">{state.aggregation.businessServiceCount}</b>
  591. 条,赢单<b className="px-1">{state.aggregation.businessWinCount}</b>个
  592. </li>
  593. </ul>
  594. </Card>
  595. </Col>
  596. </Row>
  597. </div>
  598. <div className="mt-4">
  599. <Row gutter={[8, 8]}>
  600. <Col xs={24} sm={24} md={24} lg={12} xl={12}>
  601. <LeaderBoard dataList={state.leaderboard} loading={state.loading} />
  602. </Col>
  603. <Col xs={24} sm={24} md={24} lg={12} xl={12}>
  604. <SoftLeaderboard productLeaderBoard={state.productLeaderBoard} loading={state.loading} />
  605. </Col>
  606. </Row>
  607. </div>
  608. </div>
  609. )
  610. }
  611. // export default Dashboard
  612. export default connect(({ staff }) => ({
  613. departments: staff.departments
  614. }))(Dashboard)