index.jsx 20 KB

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