index.jsx 20 KB

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