| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- import React, { useState, useEffect, useRef } from 'react'
- import ProTable from '@ant-design/pro-table'
- import { Row, Col, Select, Card, Skeleton, TreeSelect } from 'antd'
- import { AddressBook, City, Comment } from '@icon-park/react'
- import consts from '@/consts'
- import { connect, useModel } from 'umi'
- import { queryDashboard } from '@/services/dashboard'
- import { queryStaff } from '@/services/staff'
- import LeaderBoard from './components/LeaderBoard'
- import SoftLeaderboard from './components/SoftLeaderboard'
- import { cardTypeMap, cyclicalOp } from './consts'
- // import BusinessChar from './components/BusinessChar'
- 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'
- import { isDevMode } from '@/utils/env'
- import { isMobile } from '@/utils/is'
- import { useModal } from '@/components/ModalStore'
- import classNames from 'classnames'
- const Dashboard = ({ dispatch, departments = [] }) => {
- const { initialState: { currentUser } = { currentUser: {} } } = useModel('@@initialState')
- 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({
- selectId: '',
- disabled: true,
- loading: true,
- immediate: false,
- visible: false,
- reminderData: [],
- leaderboard: [],
- productLeaderBoard: [],
- clientChainRatio: {},
- customerChainRatio: {},
- businessChainRatio: {},
- serviceLogChainRatio: {},
- aggregation: {},
- businessChar: {},
- staffList: [],
- params: {
- businessGroupId: '',
- dataPermission: defaultPermAuthCache, // 'all' 'custom'
- staffIds: defaultStaffIds, // ['xxxx'] || null
- cyclical: defaultCyclicalValue
- }
- })
- const dispatchModal = useModal()
- // 展示服务弹窗详情
- const handleReminderList = (dataType, reminderCyclical) => {
- dispatchModal('M_REMINDER_LIST', { dataType, reminderCyclical })
- }
- // 展示卡片详情
- const handleRatioCard = dataType => {
- dispatchModal('M_RATIO_PANELS', {
- dataType,
- staffIds: state.params.staffIds,
- retioCyclical: state.params.cyclical,
- dataPermission: state.params.dataPermission
- })
- }
- // dataPermission-all 拉取部门下员工列表
- const queryStaffList = async (departmentId, canSetState = true) => {
- const { code = -1, data: { list = [] } = { list: [] } } = await queryStaff({
- departmentId,
- current: 1,
- pageSize: 100
- })
- if (code === consts.RET_CODE.SUCCESS && canSetState) {
- // list存起来,同时将员工列表id整个塞进params获取当前部门所有人的数据(变向)
- setState({
- ...state,
- staffList: list,
- params: { ...state.params, staffIds: list.map(item => item.id) }
- })
- }
- return list
- }
- const handleStaffClick = id => {
- const sId = `s_${id}`
- // 反选,调整回选择部门下的所有人
- if (state.params.staffIds?.length === 1 && state.params.staffIds?.includes(sId)) {
- setState({
- ...state,
- selectId: null,
- immediate: true,
- params: { ...state.params, staffIds: state.staffList.map(item => `s_${item.id}`) }
- })
- return
- }
- setState({
- ...state,
- selectId: id,
- immediate: true,
- params: { ...state.params, staffIds: [sId] }
- })
- }
- 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) {
- const otherState = {}
- if (!state.staffList.length && state.params.dataPermission === 'department' && currentUser) {
- otherState.staffList = await queryStaffList(currentUser.departmentId, false)
- }
- if (state.immediate) {
- setState({
- ...state,
- ...otherState,
- loading: false,
- immediate: false,
- reminderData,
- leaderboard,
- productLeaderBoard,
- clientChainRatio,
- customerChainRatio,
- businessChainRatio,
- serviceLogChainRatio,
- aggregation,
- disabled: false,
- businessChar
- })
- return
- }
- timer.current = setTimeout(() => {
- setState({
- ...state,
- ...otherState,
- loading: false,
- reminderData,
- leaderboard,
- productLeaderBoard,
- clientChainRatio,
- customerChainRatio,
- businessChainRatio,
- serviceLogChainRatio,
- aggregation,
- disabled: false,
- businessChar
- })
- }, 300)
- }
- }
- useEffect(() => {
- initData(state.params)
- if (!departments?.length && state.params.dataPermission === 'all') {
- dispatch({
- type: 'staff/fetchDepartments'
- })
- }
- }, [
- 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-hex-967bbd">
- {name}
- </span>
- )
- },
- {
- title: '15天',
- dataIndex: 'day15',
- render: (name, record) => (
- <span
- onClick={() => handleReminderList(record.type, '15day')}
- className="text-primary cursor-pointer hover:text-hex-967bbd">
- {name}
- </span>
- )
- },
- {
- title: '30天',
- dataIndex: 'day30',
- render: (name, record) => (
- <span
- onClick={() => handleReminderList(record.type, '30day')}
- className="text-primary cursor-pointer hover:text-hex-967bbd">
- {name}
- </span>
- )
- },
- {
- title: '3个月',
- dataIndex: 'month3',
- render: (name, record) => (
- <span
- onClick={() => handleReminderList(record.type, '3month')}
- className="text-primary cursor-pointer hover:text-hex-967bbd">
- {name}
- </span>
- )
- },
- {
- title: '6个月',
- dataIndex: 'month9',
- render: (name, record) => (
- <span
- onClick={() => handleReminderList(record.type, '6month')}
- className="text-primary cursor-pointer hover:text-hex-967bbd">
- {name}
- </span>
- )
- }
- ]
- const columnsSmall = [
- {
- title: '已到期备忘',
- dataIndex: 'expiry',
- render: (_, record) => (
- <span
- onClick={() => handleReminderList(record.type, 'expiry')}
- className="text-primary cursor-pointer hover:text-hex-967bbd">
- {record.remind.expiry}
- </span>
- )
- },
- {
- title: '未到期备忘',
- dataIndex: 'unexpired',
- render: (_, record) => (
- <span
- onClick={() => handleReminderList(record.type, 'unexpired')}
- className="text-primary cursor-pointer hover:text-hex-967bbd">
- {record.remind.unexpired}
- </span>
- )
- }
- ]
- // 处理权限Select下拉组件OnChange事件
- const handlePermChange = ({ staffIds = [], dataPermission }) => {
- if (staffIds?.length) {
- setState({
- ...state,
- immediate: true,
- staffList: [],
- selectId: '',
- params: { ...state.params, staffIds, dataPermission: 'custom' }
- })
- return
- }
- setState({
- ...state,
- immediate: true,
- staffList: [],
- selectId: '',
- 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>
- <Row gutter={[8, 8]}>
- <Col xs={24} sm={24} md={24} lg={18} xl={18}>
- <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>
- </Col>
- <Col xs={24} sm={24} md={24} lg={6} xl={6}>
- <div className="shadow-card">
- <ProTable
- bordered
- loading={state.loading}
- rowKey={record => record.type + Date.now()}
- columns={columnsSmall}
- dataSource={state.reminderData}
- search={false}
- toolBarRender={false}
- tableRender={(_, dom) => (
- <Card bodyStyle={{ padding: state.loading ? '24px' : 0 }} loading={state.loading}>
- {dom}
- </Card>
- )}
- pagination={false}
- />
- </div>
- </Col>
- </Row>
- </div>
- <div className="flex flex-wrap">
- <div className="mr-4 my-4 flex">
- <Select
- disabled={state.disabled}
- loading={state.loading}
- options={cyclicalOp}
- dropdownMatchSelectWidth={false}
- defaultValue={defaultCyclicalValue}
- onChange={handleCyclicalChange}
- />
- <div className="mx-2">
- <PermSelect
- loading={state.loading}
- disabled={state.disabled}
- onConfirm={handlePermChange}
- dataType="workbench"
- />
- </div>
- {state.params.dataPermission === 'all' ? (
- <TreeSelect
- size="middle"
- style={{ width: 150 }}
- dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
- loading={state.loading}
- disabled={state.disabled}
- treeDefaultExpandAll
- placeholder="办事处"
- treeData={departments}
- allowClear
- onSelect={queryStaffList}
- onChange={e => {
- if (!e)
- setState({
- ...state,
- params: { ...state.params, staffIds: null },
- staffList: [],
- selectId: null
- })
- }}
- />
- ) : null}
- </div>
- <div className="flex items-center flex-row ml-2">
- {['all', 'department'].includes(state.params.dataPermission) &&
- state.staffList.map(item => (
- <div
- className={classNames('relative cursor-pointer', styles.staffItem, {
- [styles.active]: state.selectId === item.id
- })}
- key={item.id}
- onClick={() => handleStaffClick(item.id)}>
- <img
- className="w-full max-w-30px h-30px border rounded-30px"
- src={
- (isDevMode() ? 'http://cld2qa.com' : 'http://zhcld.com') +
- (item?.avatar ?? consts.DEFAULT_AVATAR)
- }
- />
- <span className={styles.extra}>{item.username}</span>
- </div>
- ))}
- </div>
- </div>
- <Row gutter={[8, 8]}>
- <Col xs={24} sm={24} md={24} lg={8} xl={8}>
- <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 xs={24} sm={24} md={24} lg={8} xl={8}>
- <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 xs={24} sm={24} md={24} lg={6} xl={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 xs={24} sm={24} md={24} lg={8} xl={8}>
- <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 className="mt-4">
- <Row gutter={[8, 8]}>
- {/* <Col xs={24} sm={24} md={24} lg={12} xl={12}>
- <BusinessChar
- loading={state.loading}
- data={state.businessChar}
- changeGroupId={handleChangeGroupId}
- />
- </Col> */}
- <Col xs={24} sm={24} md={24} lg={24} xl={24}>
- <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={[8, 8]}>
- <Col xs={24} sm={24} md={24} lg={12} xl={12}>
- <LeaderBoard dataList={state.leaderboard} loading={state.loading} />
- </Col>
- <Col xs={24} sm={24} md={24} lg={12} xl={12}>
- <SoftLeaderboard productLeaderBoard={state.productLeaderBoard} loading={state.loading} />
- </Col>
- </Row>
- </div>
- </div>
- )
- }
- // export default Dashboard
- export default connect(({ staff }) => ({
- departments: staff.departments
- }))(Dashboard)
|