index.jsx 20 KB

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