index.jsx 22 KB

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