|
|
@@ -0,0 +1,542 @@
|
|
|
+import React, { useState, useEffect, useRef } from 'react'
|
|
|
+import ProTable from '@ant-design/pro-table'
|
|
|
+import { Row, Col, Select, Card, Skeleton } from 'antd'
|
|
|
+import { AddressBook, City, Finance, Comment } from '@icon-park/react'
|
|
|
+import consts from '@/consts'
|
|
|
+import { useModal } from '@/components/Modal'
|
|
|
+import { queryDashboard } from '@/services/dashboard'
|
|
|
+import ReminderList from './components/ReminderList'
|
|
|
+import LeaderBoard from './components/LeaderBoard'
|
|
|
+import SoftLeaderboard from './components/SoftLeaderboard'
|
|
|
+import { cardTypeMap, cyclicalOp } from './consts'
|
|
|
+import BusinessChar from './components/BusinessChar'
|
|
|
+import RatioPanels from './components/RatioPanels'
|
|
|
+import PermSelect, { PermDataTypeEunm } from '@/pages/Customer/Company/components/PermSelect'
|
|
|
+import { getAuthCache, getPermAuthCache, setAuthCache } from '@/utils/auth'
|
|
|
+import { WORKBENCH_CYCLICAL_KEY } from '@/utils/cache/cacheEnum'
|
|
|
+import styles from './index.less'
|
|
|
+
|
|
|
+const Dashboard = () => {
|
|
|
+ const timer = useRef(null)
|
|
|
+ const [defaultPermAuthCache, defaultStaffIds] = getPermAuthCache(PermDataTypeEunm.WORKBENCH)
|
|
|
+
|
|
|
+ const cyclicalAuthCache = getAuthCache(WORKBENCH_CYCLICAL_KEY)
|
|
|
+ let defaultCyclicalValue = 'week'
|
|
|
+ if (cyclicalAuthCache) {
|
|
|
+ defaultCyclicalValue = cyclicalAuthCache
|
|
|
+ }
|
|
|
+ const [state, setState] = useState({
|
|
|
+ disabled: true,
|
|
|
+ loading: true,
|
|
|
+ staffIds: null,
|
|
|
+ immediate: false,
|
|
|
+ visible: false,
|
|
|
+ reminderData: [],
|
|
|
+ leaderboard: [],
|
|
|
+ productLeaderBoard: [],
|
|
|
+ clientChainRatio: {},
|
|
|
+ customerChainRatio: {},
|
|
|
+ businessChainRatio: {},
|
|
|
+ serviceLogChainRatio: {},
|
|
|
+ aggregation: {},
|
|
|
+ businessChar: {},
|
|
|
+ params: {
|
|
|
+ businessGroupId: '',
|
|
|
+ dataPermission: defaultPermAuthCache,
|
|
|
+ staffIds: defaultStaffIds,
|
|
|
+ cyclical: defaultCyclicalValue
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const { toggleModal, setModalProps } = useModal()
|
|
|
+ // 展示服务弹窗详情
|
|
|
+ const handleReminderList = (dataType, reminderCyclical) => {
|
|
|
+ setModalProps({
|
|
|
+ width: '90vw',
|
|
|
+ modalRender: () => <ReminderList dataType={dataType} reminderCyclical={reminderCyclical} />,
|
|
|
+ onCancel: () => toggleModal()
|
|
|
+ })
|
|
|
+ toggleModal()
|
|
|
+ }
|
|
|
+ // 展示卡片详情
|
|
|
+ const handleRatioCard = dataType => {
|
|
|
+ setModalProps({
|
|
|
+ width: '90vw',
|
|
|
+ modalRender: () => (
|
|
|
+ <RatioPanels
|
|
|
+ dataType={dataType}
|
|
|
+ staffIds={state.params.staffIds}
|
|
|
+ retioCyclical={state.params.cyclical}
|
|
|
+ dataPermission={state.params.dataPermission}
|
|
|
+ />
|
|
|
+ ),
|
|
|
+ onCancel: () => toggleModal()
|
|
|
+ })
|
|
|
+ toggleModal()
|
|
|
+ }
|
|
|
+ const initData = async payload => {
|
|
|
+ if (timer.current) clearTimeout(timer.current)
|
|
|
+ setState({ ...state, disabled: true })
|
|
|
+ const {
|
|
|
+ code = -1,
|
|
|
+ data: {
|
|
|
+ reminderData = [],
|
|
|
+ leaderboard = [],
|
|
|
+ productLeaderBoard = {},
|
|
|
+ clientChainRatio = [],
|
|
|
+ customerChainRatio = {},
|
|
|
+ businessChainRatio = {},
|
|
|
+ serviceLogChainRatio = {},
|
|
|
+ aggregation = {},
|
|
|
+ businessChar = {}
|
|
|
+ }
|
|
|
+ } = await queryDashboard(payload)
|
|
|
+ if (code === consts.RET_CODE.SUCCESS) {
|
|
|
+ if (state.immediate) {
|
|
|
+ setState({
|
|
|
+ ...state,
|
|
|
+ loading: false,
|
|
|
+ immediate: false,
|
|
|
+ reminderData,
|
|
|
+ leaderboard,
|
|
|
+ productLeaderBoard,
|
|
|
+ clientChainRatio,
|
|
|
+ customerChainRatio,
|
|
|
+ businessChainRatio,
|
|
|
+ serviceLogChainRatio,
|
|
|
+ aggregation,
|
|
|
+ disabled: false,
|
|
|
+ businessChar
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ timer.current = setTimeout(() => {
|
|
|
+ setState({
|
|
|
+ ...state,
|
|
|
+ loading: false,
|
|
|
+ reminderData,
|
|
|
+ leaderboard,
|
|
|
+ productLeaderBoard,
|
|
|
+ clientChainRatio,
|
|
|
+ customerChainRatio,
|
|
|
+ businessChainRatio,
|
|
|
+ serviceLogChainRatio,
|
|
|
+ aggregation,
|
|
|
+ disabled: false,
|
|
|
+ businessChar
|
|
|
+ })
|
|
|
+ }, 300)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ initData(state.params)
|
|
|
+ }, [
|
|
|
+ state.params.businessGroupId,
|
|
|
+ state.params.dataPermission,
|
|
|
+ state.params.cyclical,
|
|
|
+ state.params.staffIds
|
|
|
+ ])
|
|
|
+ const columns = [
|
|
|
+ {
|
|
|
+ title: '超过周期未服务提醒',
|
|
|
+ dataIndex: 'name'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '7天',
|
|
|
+ dataIndex: 'day7',
|
|
|
+ render: (name, record) => (
|
|
|
+ <span
|
|
|
+ onClick={() => handleReminderList(record.type, '7day')}
|
|
|
+ className="text-primary cursor-pointer hover:text-[#967bbd]"
|
|
|
+ >
|
|
|
+ {name}
|
|
|
+ </span>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '15天',
|
|
|
+ dataIndex: 'day15',
|
|
|
+ render: (name, record) => (
|
|
|
+ <span
|
|
|
+ onClick={() => handleReminderList(record.type, '15day')}
|
|
|
+ className="text-primary cursor-pointer hover:text-[#967bbd]"
|
|
|
+ >
|
|
|
+ {name}
|
|
|
+ </span>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '30天',
|
|
|
+ dataIndex: 'day30',
|
|
|
+ render: (name, record) => (
|
|
|
+ <span
|
|
|
+ onClick={() => handleReminderList(record.type, '30day')}
|
|
|
+ className="text-primary cursor-pointer hover:text-[#967bbd]"
|
|
|
+ >
|
|
|
+ {name}
|
|
|
+ </span>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '3个月',
|
|
|
+ dataIndex: 'month3',
|
|
|
+ render: (name, record) => (
|
|
|
+ <span
|
|
|
+ onClick={() => handleReminderList(record.type, '3month')}
|
|
|
+ className="text-primary cursor-pointer hover:text-[#967bbd]"
|
|
|
+ >
|
|
|
+ {name}
|
|
|
+ </span>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '6个月',
|
|
|
+ dataIndex: 'month6',
|
|
|
+ render: (name, record) => (
|
|
|
+ <span
|
|
|
+ onClick={() => handleReminderList(record.type, '6month')}
|
|
|
+ className="text-primary cursor-pointer hover:text-[#967bbd]"
|
|
|
+ >
|
|
|
+ {name}
|
|
|
+ </span>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '备忘',
|
|
|
+ dataIndex: 'remind',
|
|
|
+ render: (name, record) => (
|
|
|
+ <span
|
|
|
+ onClick={() => handleReminderList(record.type, 'reminder')}
|
|
|
+ className="text-primary cursor-pointer hover:text-[#967bbd]"
|
|
|
+ >
|
|
|
+ {name}
|
|
|
+ </span>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ ]
|
|
|
+
|
|
|
+ const handleChangeGroupId = e =>
|
|
|
+ setState({ ...state, immediate: true, params: { ...state.params, businessGroupId: e } })
|
|
|
+
|
|
|
+ // 处理权限Select下拉组件OnChange事件
|
|
|
+ const handlePermChange = ({ staffIds = [], dataPermission }) => {
|
|
|
+ if (staffIds?.length) {
|
|
|
+ setState({
|
|
|
+ ...state,
|
|
|
+ immediate: true,
|
|
|
+ params: { ...state.params, staffIds, dataPermission: 'custom' }
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ setState({
|
|
|
+ ...state,
|
|
|
+ immediate: true,
|
|
|
+ params: { ...state.params, staffIds: null, dataPermission }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleCyclicalChange = value => {
|
|
|
+ setAuthCache(WORKBENCH_CYCLICAL_KEY, value)
|
|
|
+ setState({ ...state, immediate: true, params: { ...state.params, cyclical: value } })
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ <div className={styles.dashboard}>
|
|
|
+ <div className="shadow-card">
|
|
|
+ <ProTable
|
|
|
+ bordered
|
|
|
+ loading={state.loading}
|
|
|
+ rowKey={record => record.type + Date.now()}
|
|
|
+ columns={columns}
|
|
|
+ dataSource={state.reminderData}
|
|
|
+ search={false}
|
|
|
+ toolBarRender={false}
|
|
|
+ tableRender={(_, dom) => (
|
|
|
+ <Card bodyStyle={{ padding: state.loading ? '24px' : 0 }} loading={state.loading}>
|
|
|
+ {dom}
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+ pagination={false}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div className="mt-4">
|
|
|
+ <PermSelect
|
|
|
+ loading={state.loading}
|
|
|
+ disabled={state.disabled}
|
|
|
+ onConfirm={handlePermChange}
|
|
|
+ dataType="workbench"
|
|
|
+ />
|
|
|
+ <span className="ml-4">
|
|
|
+ <Select
|
|
|
+ disabled={state.disabled}
|
|
|
+ loading={state.loading}
|
|
|
+ options={cyclicalOp}
|
|
|
+ dropdownMatchSelectWidth={false}
|
|
|
+ defaultValue={defaultCyclicalValue}
|
|
|
+ onChange={handleCyclicalChange}
|
|
|
+ />
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div className="mt-4">
|
|
|
+ <Row gutter={24}>
|
|
|
+ <Col
|
|
|
+ className="gutter-row"
|
|
|
+ sm={{ span: 24 }}
|
|
|
+ md={{ span: 24 }}
|
|
|
+ lg={{ span: 6 }}
|
|
|
+ xl={{ span: 6 }}
|
|
|
+ >
|
|
|
+ <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
|
|
|
+ {state.loading ? (
|
|
|
+ <Skeleton active avatar />
|
|
|
+ ) : (
|
|
|
+ <Row className="cursor-pointer" onClick={() => handleRatioCard(cardTypeMap.CLIENT)}>
|
|
|
+ <Col span={6}>
|
|
|
+ <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
|
|
|
+ <AddressBook size="26" fill="#fff" />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col span={18} flex="wrap">
|
|
|
+ <div className="flex items-center justify-between ">
|
|
|
+ <div className="text-2xl">{state.clientChainRatio.count}</div>
|
|
|
+ <span
|
|
|
+ className={[
|
|
|
+ 'text-xl',
|
|
|
+ state.clientChainRatio.percentage.startsWith('-')
|
|
|
+ ? 'text-green-500'
|
|
|
+ : 'text-red-500'
|
|
|
+ ].join(' ')}
|
|
|
+ >
|
|
|
+ {state.clientChainRatio.percentage}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div>新增客户</div>
|
|
|
+ <span>
|
|
|
+ {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col
|
|
|
+ className="gutter-row"
|
|
|
+ sm={{ span: 24 }}
|
|
|
+ md={{ span: 24 }}
|
|
|
+ lg={{ span: 6 }}
|
|
|
+ xl={{ span: 6 }}
|
|
|
+ >
|
|
|
+ <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
|
|
|
+ {state.loading ? (
|
|
|
+ <Skeleton active avatar />
|
|
|
+ ) : (
|
|
|
+ <Row
|
|
|
+ className="cursor-pointer"
|
|
|
+ onClick={() => handleRatioCard(cardTypeMap.COMPANY)}
|
|
|
+ >
|
|
|
+ <Col span={6}>
|
|
|
+ <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
|
|
|
+ <City size="26" fill="#fff" />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col span={18} flex="wrap">
|
|
|
+ <div className="flex items-center justify-between ">
|
|
|
+ <div className="text-2xl">{state.customerChainRatio.count}</div>
|
|
|
+ <span
|
|
|
+ className={[
|
|
|
+ 'text-xl',
|
|
|
+ state.customerChainRatio.percentage.startsWith('-')
|
|
|
+ ? 'text-green-500'
|
|
|
+ : 'text-red-500'
|
|
|
+ ].join(' ')}
|
|
|
+ >
|
|
|
+ {state.customerChainRatio.percentage}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div>新增单位</div>
|
|
|
+ <span>
|
|
|
+ {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col
|
|
|
+ className="gutter-row"
|
|
|
+ sm={{ span: 24 }}
|
|
|
+ md={{ span: 24 }}
|
|
|
+ lg={{ span: 6 }}
|
|
|
+ xl={{ span: 6 }}
|
|
|
+ >
|
|
|
+ <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
|
|
|
+ {state.loading ? (
|
|
|
+ <Skeleton active avatar />
|
|
|
+ ) : (
|
|
|
+ <Row
|
|
|
+ className="cursor-pointer"
|
|
|
+ onClick={() => handleRatioCard(cardTypeMap.BUSINESS)}
|
|
|
+ >
|
|
|
+ <Col span={6}>
|
|
|
+ <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
|
|
|
+ <Finance size="26" fill="#fff" />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col span={18} flex="wrap">
|
|
|
+ <div className="flex items-center justify-between ">
|
|
|
+ <div className="text-2xl">{state.businessChainRatio.count}</div>
|
|
|
+ <span
|
|
|
+ className={[
|
|
|
+ 'text-xl',
|
|
|
+ state.businessChainRatio.percentage.startsWith('-')
|
|
|
+ ? 'text-green-500'
|
|
|
+ : 'text-red-500'
|
|
|
+ ].join(' ')}
|
|
|
+ >
|
|
|
+ {state.businessChainRatio.percentage}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div>新增商机</div>
|
|
|
+ <span>
|
|
|
+ {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col
|
|
|
+ className="gutter-row"
|
|
|
+ sm={{ span: 24 }}
|
|
|
+ md={{ span: 24 }}
|
|
|
+ lg={{ span: 6 }}
|
|
|
+ xl={{ span: 6 }}
|
|
|
+ >
|
|
|
+ <div className="p-4 bg-white border rounded-2px border-hex-f0f0f0 shadow-card">
|
|
|
+ {state.loading ? (
|
|
|
+ <Skeleton active avatar />
|
|
|
+ ) : (
|
|
|
+ <Row
|
|
|
+ onClick={() => handleRatioCard(cardTypeMap.SERVICE)}
|
|
|
+ className="cursor-pointer"
|
|
|
+ >
|
|
|
+ <Col span={6}>
|
|
|
+ <div className="w-full max-w-48px h-48px border rounded-48px bg-[#0c7cd5] flex items-center justify-center">
|
|
|
+ <Comment size="26" fill="#fff" />
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ <Col span={18} flex="wrap">
|
|
|
+ <div className="flex items-center justify-between ">
|
|
|
+ <div className="text-2xl">{state.serviceLogChainRatio.count}</div>
|
|
|
+ <span
|
|
|
+ className={[
|
|
|
+ 'text-xl',
|
|
|
+ state.serviceLogChainRatio.percentage.startsWith('-')
|
|
|
+ ? 'text-green-500'
|
|
|
+ : 'text-red-500'
|
|
|
+ ].join(' ')}
|
|
|
+ >
|
|
|
+ {state.serviceLogChainRatio.percentage}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div>服务记录</div>
|
|
|
+ <span>
|
|
|
+ {cyclicalOp.find(item => item.value === state.params.cyclical)?.title}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+ <div className="mt-4">
|
|
|
+ <Row gutter={24}>
|
|
|
+ <Col
|
|
|
+ className="gutter-row"
|
|
|
+ sm={{ span: 24 }}
|
|
|
+ md={{ span: 24 }}
|
|
|
+ lg={{ span: 16 }}
|
|
|
+ xl={{ span: 16 }}
|
|
|
+ >
|
|
|
+ <BusinessChar
|
|
|
+ loading={state.loading}
|
|
|
+ data={state.businessChar}
|
|
|
+ changeGroupId={handleChangeGroupId}
|
|
|
+ />
|
|
|
+ </Col>
|
|
|
+
|
|
|
+ <Col
|
|
|
+ className="gutter-row"
|
|
|
+ sm={{ span: 24 }}
|
|
|
+ md={{ span: 24 }}
|
|
|
+ lg={{ span: 8 }}
|
|
|
+ xl={{ span: 8 }}
|
|
|
+ >
|
|
|
+ <Card
|
|
|
+ headStyle={{ padding: '0 12px' }}
|
|
|
+ title="数据汇总"
|
|
|
+ className="shadow-card"
|
|
|
+ bodyStyle={{ padding: state.loading ? '24px' : 0 }}
|
|
|
+ loading={state.loading}
|
|
|
+ >
|
|
|
+ <ul>
|
|
|
+ <li className="px-12px py-16px border-b-1">
|
|
|
+ 新增客户<b className="px-1">{state.aggregation.clientCount}</b>个,服务
|
|
|
+ <b className="px-1">{state.aggregation.clientServiceCount}</b> 条
|
|
|
+ </li>
|
|
|
+ <li className="px-12px py-16px border-b-1">
|
|
|
+ 新增单位<b className="px-1">{state.aggregation.customerCount}</b>个,服务
|
|
|
+ <b className="px-1">{state.aggregation.customerServiceCount}</b>条
|
|
|
+ </li>
|
|
|
+ <li className="px-12px py-16px">
|
|
|
+ 新增商机<b className="px-1">{state.aggregation.businessCount}</b>个,金额
|
|
|
+ <b className="px-1">{state.aggregation.businessSum}</b> 元,服务
|
|
|
+ <b className="px-1">{state.aggregation.businessServiceCount}</b>
|
|
|
+ 条,赢单<b className="px-1">{state.aggregation.businessWinCount}</b>个
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+ </Card>
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+ <div className="mt-4">
|
|
|
+ <Row gutter={24}>
|
|
|
+ <Col
|
|
|
+ className="gutter-row"
|
|
|
+ sm={{ span: 24 }}
|
|
|
+ md={{ span: 24 }}
|
|
|
+ lg={{ span: 12 }}
|
|
|
+ xl={{ span: 12 }}
|
|
|
+ >
|
|
|
+ <LeaderBoard dataList={state.leaderboard} loading={state.loading} />
|
|
|
+ </Col>
|
|
|
+ <Col
|
|
|
+ className="gutter-row"
|
|
|
+ sm={{ span: 24 }}
|
|
|
+ md={{ span: 24 }}
|
|
|
+ lg={{ span: 12 }}
|
|
|
+ xl={{ span: 12 }}
|
|
|
+ >
|
|
|
+ <SoftLeaderboard
|
|
|
+ productLeaderBoard={state.productLeaderBoard}
|
|
|
+ loading={state.loading}
|
|
|
+ />
|
|
|
+ </Col>
|
|
|
+ </Row>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export default Dashboard
|