lanjianrong 4 years ago
parent
commit
09c360c870

+ 89 - 0
src/pages/Workbench/Dashboard/components/BusinessChar.jsx

@@ -0,0 +1,89 @@
+import { DualAxes } from '@ant-design/charts'
+import { Card, Select } from 'antd'
+import { connect } from 'umi'
+import React, { useEffect } from 'react'
+
+const BusinessChar = props => {
+  const { dispatch, groupList, loading, data, changeGroupId } = props
+  useEffect(() => {
+    if (!groupList) {
+      dispatch({
+        type: 'business/fetchGroup'
+      })
+    }
+  }, [groupList])
+  const { char, thread } = data
+  const config = {
+    data: [char, thread],
+    legend: { position: 'top' },
+    height: 230,
+    xField: 'time',
+    yField: ['value', 'increment'],
+    yAxis: {
+      value: {
+        label: {
+          formatter: text => `${text}个`
+        }
+      },
+      increment: {
+        label: {
+          formatter: text => `${text}%`
+        }
+      }
+    },
+    meta: {
+      increment: {
+        alias: '增幅'
+      }
+    },
+    geometryOptions: [
+      {
+        geometry: 'column',
+        isGroup: true,
+        seriesField: 'type',
+        color: ['#A088C4', '#605B67']
+      },
+      {
+        geometry: 'line',
+        lineStyle: { lineWidth: 2 },
+        smooth: true,
+        color: '#FEB51A',
+        point: {
+          size: 3,
+          style: {
+            fill: 'white',
+            stroke: '#5B8FF9',
+            lineWidth: 2
+          }
+        }
+      }
+    ]
+  }
+  return (
+    <Card
+      loading={loading}
+      bodyStyle={{ minHeight: '230px' }}
+      className="shadow-card"
+      title={
+        <div className="relative">
+          <span>销售漏斗</span>
+          <div className="absolute right-1 top-0">
+            {groupList?.length ? (
+              <Select
+                defaultValue={groupList[0]?.value}
+                options={groupList}
+                onChange={e => changeGroupId(e)}
+              />
+            ) : null}
+          </div>
+        </div>
+      }
+    >
+      {char && thread && <DualAxes {...config} />}
+    </Card>
+  )
+}
+
+export default connect(({ business }) => ({
+  groupList: business.groupList
+}))(BusinessChar)

+ 130 - 0
src/pages/Workbench/Dashboard/components/LeaderBoard.jsx

@@ -0,0 +1,130 @@
+import ProTable from '@ant-design/pro-table'
+import { Select, Card } from 'antd'
+import Iconfont from '@/components/SvgIcon'
+import React, { useState, useMemo } from 'react'
+
+const { Option } = Select
+
+const opMap = {
+  customer: '新增单位',
+  customerService: '单位服务',
+  client: '新增客户',
+  clientService: '客户服务',
+  business: '新增商机',
+  businessService: '商机服务'
+}
+const colorMap = {
+  1: 'text-hex-ffc241',
+  2: 'text-hex-1dc9b7',
+  3: 'text-hex-868e96'
+}
+const LeaderBoard = ({ loading = false, dataList = [] }) => {
+  const [activeOp, setActiveOp] = useState('customer')
+  const columns = useMemo(() => {
+    const oColumns = [
+      {
+        title: '排名',
+        dataIndex: 'rank',
+        align: 'center',
+        width: '30%',
+        render: (_, record) =>
+          record.rank <= 3 ? <Iconfont type="icon-trophy" className={colorMap[record.rank]} /> : ''
+      },
+      {
+        title: '姓名',
+        dataIndex: `${activeOp}Name`,
+        align: 'center',
+        width: '40%'
+      },
+      {
+        title: '新增单位数',
+        dataIndex: 'customer',
+        align: 'center',
+        width: '30%'
+      },
+      {
+        title: '单位服务',
+        dataIndex: 'customerService',
+        align: 'center',
+        width: '30%'
+      },
+      {
+        title: '新增客户',
+        dataIndex: 'client',
+        align: 'center',
+        width: '30%'
+      },
+      {
+        title: '客户服务',
+        dataIndex: 'clientService',
+        align: 'center',
+        width: '30%'
+      },
+      {
+        title: '新增商机',
+        dataIndex: 'business',
+        align: 'center',
+        width: '30%'
+      },
+      {
+        title: '商机服务',
+        dataIndex: 'businessService',
+        align: 'center',
+        width: '30%'
+      }
+    ]
+
+    let stop = false
+    const bColumns = oColumns.reduce((prev, curr, idx) => {
+      if (idx < 2) {
+        prev.push(curr)
+        return prev
+      }
+
+      if (!stop && curr.dataIndex.startsWith(activeOp)) {
+        stop = true
+        prev.push(curr)
+        return prev
+      }
+      return prev
+    }, [])
+    return bColumns
+  }, [activeOp])
+
+  return (
+    <Card
+      loading={loading}
+      bodyStyle={{ padding: loading ? '24px' : 0 }}
+      className="shadow-card"
+      title={
+        <div className="px-4 pt-2 pb-0 flex items-center justify-between">
+          <span>排行榜</span>
+          <Select defaultValue="client" onChange={e => setActiveOp(e)}>
+            <Option value="client">新增客户</Option>
+            <Option value="clientService">客户服务</Option>
+            <Option value="customer">新增单位</Option>
+            <Option value="customerService">单位服务</Option>
+            <Option value="business">新增商机</Option>
+            <Option value="businessService">商机服务</Option>
+            {/* <Option value="开票金额">开票金额</Option>
+      <Option value="回款金额">回款金额</Option> */}
+          </Select>
+        </div>
+      }
+    >
+      <div className="text-center border border-x-0 p-2">{opMap[activeOp]}排行榜</div>
+      <ProTable
+        border={true}
+        rowKey={record => record.rank}
+        columns={columns}
+        dataSource={dataList}
+        search={false}
+        size="small"
+        toolBarRender={false}
+        pagination={false}
+      />
+    </Card>
+  )
+}
+
+export default LeaderBoard

+ 409 - 0
src/pages/Workbench/Dashboard/components/RatioPanels.jsx

@@ -0,0 +1,409 @@
+import React, { useState } from 'react'
+import ProTable from '@ant-design/pro-table'
+import CustomModal from '@/components/Modal/src/components/CustomModal'
+import { queryRatioPanelsList, queryServiceRatioPanelsList } from '@/services/dashboard'
+import consts from '@/consts'
+import { servicelogEnum } from '@/pages/Customer/Company/components/CompanyDetail/LowerList'
+import ClientDetail from '@/pages/Customer/Client/components/ClientDetail'
+import CompanyDetail from '@/pages/Customer/Company/components/CompanyDetail'
+import { cardTypeMap } from '../consts'
+import { useModal } from '@/components/Modal'
+import { Badge } from 'antd'
+
+const dataTypeEunm = {
+  0: {
+    key: 'client',
+    title: '新增客户'
+  },
+  1: {
+    key: 'customer',
+    title: '新增单位'
+  },
+  2: {
+    key: 'business',
+    title: '新增商机'
+  },
+  3: {
+    key: 'service',
+    title: '新增服务记录'
+  }
+}
+
+export const renderBadge = (count, active = false) => {
+  return active ? (
+    <Badge
+      count={count}
+      style={{
+        marginTop: -2,
+        marginLeft: 4,
+        color: '#531dab',
+        backgroundColor: '#f9f0ff'
+      }}
+    />
+  ) : null
+}
+
+/** 服务环比数据Tab切换表格 */
+const ServiceRatioPanels = ({ staffIds, cyclical, dataPermission }) => {
+  const { toggleDrawer, setDrawerProps } = useModal()
+  const activeKeyMap = {
+    0: { key: 'clientName', title: '客户', id: 'clientId' },
+    1: { key: 'customerName', title: '单位', id: 'customerId' },
+    2: { key: 'businessName', title: '商机', id: 'businessId' }
+  }
+  const [state, setState] = useState({
+    orderIds: [],
+    activeKey: '0',
+    total: 0
+  })
+  const showDrawer = id => {
+    let children = null
+    if (state.activeKey === '0') {
+      children = <ClientDetail dataId={id} orderIds={state.orderIds} />
+    }
+    if (state.activeKey === '1') {
+      children = <CompanyDetail dataId={id} orderIds={state.orderIds} />
+    }
+    setDrawerProps({
+      zIndex: 1001,
+      closable: true,
+      onClose: () => toggleDrawer(),
+      bodyStyle: {
+        height: '100vh',
+        overflowY: 'hidden'
+      },
+      children
+    })
+    toggleDrawer()
+  }
+  const columns = [
+    {
+      dataIndex: activeKeyMap[state.activeKey].key,
+      title: `${activeKeyMap[state.activeKey].title}名称`,
+      width: '15%',
+      render: (text, record) => (
+        <span
+          onClick={() => showDrawer(record[activeKeyMap[state.activeKey].id])}
+          className="text-primary cursor-pointer hover:text-[#967bbd]"
+        >
+          {text}
+        </span>
+      )
+    },
+    {
+      dataIndex: 'status',
+      title: '服务类型',
+      width: '8%',
+      render: type => servicelogEnum[type]
+    },
+    {
+      dataIndex: 'date',
+      title: '服务日期',
+      width: '9%',
+      valueType: 'date'
+    },
+    {
+      dataIndex: 'mark',
+      width: '38%',
+      ellipsis: true,
+      title: '服务内容'
+    },
+    {
+      dataIndex: 'staffName',
+      title: '创建人',
+      width: '7%'
+    }
+  ]
+
+  return (
+    <ProTable
+      size="small"
+      params={{ dataType: state.activeKey, cyclical, dataPermission, staffIds }}
+      rowKey={record => record.id}
+      scroll={{ y: 400 }}
+      bordered={true}
+      columns={columns}
+      search={false}
+      toolbar={{
+        settings: false,
+        menu: {
+          type: 'tab',
+          activeKey: state.activeKey,
+          items: [
+            {
+              key: '0',
+              label: (
+                <span>
+                  客户
+                  {renderBadge(state.total, state.activeKey === '0')}
+                </span>
+              )
+            },
+            {
+              key: '1',
+              label: (
+                <span>
+                  单位
+                  {renderBadge(state.total, state.activeKey === '1')}
+                </span>
+              )
+            },
+            {
+              key: '2',
+              label: (
+                <span>
+                  商机
+                  {renderBadge(state.total, state.activeKey === '2')}
+                </span>
+              )
+            }
+          ],
+          onChange: key => {
+            setState({ ...state, activeKey: key })
+            // redoHeight()
+          }
+        }
+      }}
+      pagination={{ defaultPageSize: 10, showQuickJumper: true }}
+      request={async params => {
+        const { code = -1, data } = await queryServiceRatioPanelsList(params)
+        setState({ ...state, total: data.total || 0 })
+        return {
+          data: data.log,
+          success: code === consts.RET_CODE.SUCCESS,
+          total: data.total
+        }
+      }}
+    />
+  )
+}
+
+const RatioPanels = ({ dataType, staffIds, retioCyclical, dataPermission }) => {
+  const { toggleDrawer, setDrawerProps } = useModal()
+  const state = {
+    orderIds: []
+  }
+  const showDrawer = id => {
+    setDrawerProps({
+      zIndex: 1001,
+      closable: true,
+      onClose: () => toggleDrawer(),
+      bodyStyle: {
+        height: '100vh',
+        overflowY: 'hidden'
+      },
+      children: <ClientDetail dataId={id} orderIds={state.orderIds} />
+    })
+    toggleDrawer()
+  }
+  // 展示单位
+  const showDrawerComapny = id => {
+    setDrawerProps({
+      zIndex: 1001,
+      closable: true,
+      onClose: () => toggleDrawer(),
+      bodyStyle: {
+        height: '100vh',
+        overflowY: 'hidden'
+      },
+      children: <CompanyDetail dataId={id} orderIds={state.orderIds} />
+    })
+    toggleDrawer()
+  }
+  const columnsMap = {
+    0: [
+      {
+        dataIndex: 'clientName',
+        title: '客户名称',
+        width: '10%',
+        render: (clientName, record) => (
+          <span
+            onClick={() => showDrawer(record.id)}
+            className="text-primary cursor-pointer hover:text-[#967bbd]"
+          >
+            {clientName}
+          </span>
+        )
+      },
+      {
+        dataIndex: 'companyName',
+        title: '单位名称',
+        width: '16%',
+        ellipsis: true,
+        render: (companyName, record) => (
+          <span
+            className="text-primary cursor-pointer hover:text-[#967bbd]"
+            onClick={() => showDrawerComapny(record.companyId)}
+          >
+            {companyName}
+          </span>
+        )
+      },
+      {
+        dataIndex: 'districtName',
+        title: '地区',
+        width: '15%',
+        ellipsis: true,
+        renderText: text => text && text?.replaceAll(' / ', ' , ')
+      },
+      {
+        dataIndex: 'phone',
+        title: '手机',
+        ellipsis: true,
+        width: '5%'
+      },
+      {
+        dataIndex: 'email',
+        title: '邮箱',
+        ellipsis: true,
+        width: '7%'
+      },
+      {
+        dataIndex: 'qq',
+        title: 'QQ',
+        ellipsis: true,
+        width: '5%'
+      },
+      {
+        dataIndex: 'staffName',
+        title: '创建人',
+        ellipsis: true,
+        width: '7%'
+      },
+      {
+        dataIndex: 'createTime',
+        title: '创建时间',
+        ellipsis: true,
+        width: '7%'
+      }
+    ],
+    1: [
+      {
+        dataIndex: 'companyName',
+        title: '单位名称',
+        width: '16%',
+        render: (companyName, record) => (
+          <span
+            onClick={() => showDrawerComapny(record.id)}
+            className="text-primary cursor-pointer hover:text-[#967bbd]"
+          >
+            {companyName}
+          </span>
+        )
+      },
+      {
+        dataIndex: 'districtName',
+        title: '地区',
+        width: '15%',
+        ellipsis: true,
+        renderText: text => text && text?.replaceAll(' / ', ' , ')
+      },
+      {
+        dataIndex: 'nature',
+        title: '单位性质',
+        ellipsis: true,
+        width: '14%'
+      },
+      {
+        dataIndex: 'phone',
+        title: '单位电话',
+        ellipsis: true,
+        width: '5%'
+      },
+      {
+        dataIndex: 'staffName',
+        title: '创建人',
+        ellipsis: true,
+        width: '7%'
+      },
+      {
+        dataIndex: 'createTime',
+        title: '创建时间',
+        ellipsis: true,
+        width: '7%'
+      }
+    ],
+    2: [
+      {
+        dataIndex: 'name',
+        title: '商机名称',
+        width: '15%'
+      },
+      {
+        dataIndex: 'customerName',
+        title: '单位名称',
+        ellipsis: true,
+        width: '20%'
+      },
+      {
+        dataIndex: 'businessStatusName',
+        title: '商机状态',
+        ellipsis: true,
+        width: '5%'
+      },
+      {
+        dataIndex: 'price',
+        title: '商机金额',
+        ellipsis: true,
+        width: '5%'
+      },
+      {
+        dataIndex: 'staffName',
+        title: '创建人',
+        ellipsis: true,
+        width: '7%'
+      },
+      {
+        dataIndex: 'createTime',
+        title: '创建时间',
+        ellipsis: true,
+        width: '7%'
+      }
+    ]
+  }
+
+  return (
+    <CustomModal title={`卡片详情(${dataTypeEunm[dataType].title})`}>
+      <div className="pb-4">
+        {dataType === cardTypeMap.SERVICE ? (
+          <ServiceRatioPanels
+            cyclical={retioCyclical}
+            dataPermission={dataPermission}
+            staffIds={staffIds}
+          />
+        ) : (
+          <div className="mx-4">
+            <ProTable
+              size="small"
+              bordered={true}
+              pagination={{ defaultPageSize: 10 }}
+              rowKey={record => record.id}
+              scroll={{ y: 400 }}
+              columns={columnsMap[dataType]}
+              params={{ dataType, dataPermission, cyclical: retioCyclical, staffIds }}
+              request={async params => {
+                const { code = -1, data } = await queryRatioPanelsList(params)
+                return {
+                  data: data[dataTypeEunm[dataType].key],
+                  success: code === consts.RET_CODE.SUCCESS,
+                  total: data.total
+                }
+              }}
+              search={false}
+              toolBarRender={false}
+            />
+          </div>
+        )}
+      </div>
+      {/* <div className={styles.modalFooter}>
+        <div className="text-right">
+          <Button type="button" onClick={closeModal}>
+            关闭
+          </Button>
+        </div>
+      </div> */}
+    </CustomModal>
+  )
+}
+
+export default RatioPanels

+ 284 - 0
src/pages/Workbench/Dashboard/components/ReminderList.jsx

@@ -0,0 +1,284 @@
+import React, { useState } from 'react'
+import consts from '@/consts'
+import { connect } from 'umi'
+import ProTable from '@ant-design/pro-table'
+import { useModal } from '@/components/Modal'
+import { queryReminderList } from '@/services/dashboard'
+// import styles from '@/pages/Customer/Company/components/ConnectCompany/index.less'
+import CustomModal from '@/components/Modal/src/components/CustomModal'
+import ContactDetail from '@/pages/Customer/Client/components/ClientDetail'
+import CompanyDetail from '@/pages/Customer/Company/components/CompanyDetail'
+import Detail from '@/pages/Customer/Business/components/BusinessDetail'
+
+const ReminderList = props => {
+  // const dispatch = useDispatch()
+  const { dataType, reminderCyclical } = props
+  const { toggleDrawer, setDrawerProps } = useModal()
+  const [state] = useState({
+    orderIds: []
+  })
+  // const [state, setState] = useState({
+  //   params: { dataType, reminderCyclical }
+  // })
+  // 展示客户
+  const showDrawer = id => {
+    setDrawerProps({
+      zIndex: 1001,
+      closable: true,
+      onClose: () => toggleDrawer(),
+      bodyStyle: {
+        height: '100vh'
+        // overflowY: 'hidden'
+      },
+      children: <ContactDetail dataId={id} orderIds={state.orderIds} />
+    })
+    toggleDrawer()
+  }
+  // 展示单位
+  const showDrawerComapny = id => {
+    setDrawerProps({
+      zIndex: 1001,
+      closable: true,
+      onClose: () => toggleDrawer(),
+      bodyStyle: {
+        height: '100vh'
+        // overflowY: 'hidden'
+      },
+      children: <CompanyDetail dataId={id} orderIds={state.orderIds} />
+    })
+    toggleDrawer()
+  }
+  // 展示商机详情
+  const showDrawerBusiness = (id, isCompany = false) => {
+    setDrawerProps({
+      zIndex: 1001,
+      closable: true,
+      onClose: () => toggleDrawer(),
+      bodyStyle: {
+        height: '100vh',
+        overflowY: 'hidden'
+      },
+      children: isCompany ? (
+        <CompanyDetail dataId={id} />
+      ) : (
+        <Detail dataId={id} orderIds={state.orderIds} />
+      )
+    })
+    toggleDrawer()
+  }
+  const clientColumns = [
+    {
+      title: '客户名称',
+      dataIndex: 'clientName',
+      ellipsis: true,
+      width: '10%',
+      render: (clientName, record) => (
+        <span
+          onClick={() => showDrawer(record.id)}
+          className="text-primary cursor-pointer hover:text-[#967bbd]"
+        >
+          {clientName}
+        </span>
+      )
+    },
+    {
+      title: '地区',
+      dataIndex: 'districtName',
+      ellipsis: true,
+      width: '15%',
+      renderText: text => text?.replaceAll(' / ', ' , ')
+    },
+    {
+      title: '手机',
+      dataIndex: 'telephone',
+      ellipsis: true,
+      width: '10%'
+    },
+    {
+      title: '服务日期',
+      dataIndex: 'serviceDate',
+      ellipsis: true,
+      width: '8%'
+    },
+    {
+      title: '服务内容',
+      dataIndex: 'serviceContent',
+      width: '26%',
+      ellipsis: true
+    },
+    {
+      title: '备忘时间',
+      dataIndex: 'memoDate',
+      width: '10%',
+      ellipsis: true
+    },
+    {
+      title: '备忘内容',
+      dataIndex: 'memoContent',
+      width: '27%',
+      ellipsis: true
+    }
+  ]
+  const customerColumns = [
+    {
+      title: '单位名称',
+      dataIndex: 'companyName',
+      width: '15%',
+      ellipsis: true,
+      render: (companyName, record) => (
+        <span
+          onClick={() => showDrawerComapny(record.id)}
+          className="text-primary cursor-pointer hover:text-[#967bbd]"
+        >
+          {companyName}
+        </span>
+      )
+    },
+    {
+      title: '地区',
+      dataIndex: 'districtName',
+      width: '15%',
+      ellipsis: true,
+      renderText: text => text?.replaceAll(' / ', ' , ')
+    },
+    {
+      title: '电话',
+      dataIndex: 'phone',
+      width: '10%',
+      ellipsis: true
+    },
+    {
+      title: '服务日期',
+      dataIndex: 'serviceDate',
+      width: '8%',
+      ellipsis: true
+    },
+    {
+      title: '服务内容',
+      dataIndex: 'serviceContent',
+      width: '24%',
+      ellipsis: true
+    },
+    {
+      title: '备忘时间',
+      dataIndex: 'memoDate',
+      width: '10%',
+      ellipsis: true
+    },
+    {
+      title: '备忘内容',
+      dataIndex: 'memoContent',
+      width: '22%',
+      ellipsis: true
+    }
+  ]
+  const businessColumns = [
+    {
+      title: '商机名称',
+      dataIndex: 'name',
+      width: '15%',
+      ellipsis: true,
+      render: (text, record) => (
+        <span
+          onClick={() => showDrawerBusiness(record.id)}
+          className="text-primary cursor-pointer hover:text-[#967bbd]"
+        >
+          {text}
+        </span>
+      )
+    },
+    {
+      title: '单位名称',
+      dataIndex: 'customerName',
+      width: '15%',
+      ellipsis: true
+    },
+    {
+      title: '服务日期',
+      dataIndex: 'serviceDate',
+      width: '8%',
+      ellipsis: true
+    },
+    {
+      title: '服务内容',
+      dataIndex: 'serviceContent',
+      width: '27%',
+      ellipsis: true
+    },
+    {
+      title: '备忘时间',
+      dataIndex: 'memoDate',
+      width: '10%',
+      ellipsis: true
+    },
+    {
+      title: '备忘内容',
+      dataIndex: 'memoContent',
+      width: '25%',
+      ellipsis: true
+    }
+  ]
+  const dataMap = {
+    0: {
+      key: 'client',
+      columns: clientColumns
+    },
+    1: {
+      key: 'customer',
+      columns: customerColumns
+    },
+    2: {
+      key: 'business',
+      columns: businessColumns
+    }
+  }
+
+  const titleMap = {
+    0: { title: '客户' },
+    1: { title: '单位' },
+    2: { title: '商机' }
+  }
+
+  // const closeModal = () => {
+  //   dispatch({
+  //     type: 'single/changeModal'
+  //   })
+  // }
+
+  return (
+    <CustomModal title={titleMap[dataType].title}>
+      <div className="mx-4">
+        <ProTable
+          size="small"
+          rowKey={record => record.id}
+          columns={dataMap[dataType].columns}
+          params={{ dataType, reminderCyclical }}
+          scroll={{ y: 400 }}
+          request={async params => {
+            const { code = -1, data } = await queryReminderList({ ...params })
+            return {
+              data: data[dataMap[dataType].key],
+              success: code === consts.RET_CODE.SUCCESS,
+              total: data.total
+            }
+          }}
+          scroll={{ y: 400 }}
+          pagination={{ defaultPageSize: 10 }}
+          search={false}
+          toolBarRender={false}
+        />
+      </div>
+      {/* <div className={styles.modalFooter}>
+        <div className="text-right">
+          <Button type="button" onClick={closeModal}>
+            关闭
+          </Button>
+        </div>
+      </div> */}
+    </CustomModal>
+  )
+}
+
+export default connect(({ single }) => ({
+  visible: single.drawer.visible
+}))(ReminderList)

+ 103 - 0
src/pages/Workbench/Dashboard/components/SoftLeaderboard.jsx

@@ -0,0 +1,103 @@
+import ProTable from '@ant-design/pro-table'
+import { Select, Card } from 'antd'
+import Iconfont from '@/components/SvgIcon'
+import React, { useState, useMemo } from 'react'
+
+const { Option } = Select
+
+const opMap = {
+  soft: '软件锁'
+}
+const colorMap = {
+  1: 'text-hex-ffc241',
+  2: 'text-hex-1dc9b7',
+  3: 'text-hex-868e96'
+}
+const SoftLeaderboard = ({ loading = false, productLeaderBoard = [] }) => {
+  const [activeOp, setActiveOp] = useState('soft')
+  const columns = useMemo(() => {
+    const oColumns = [
+      {
+        title: '排名',
+        dataIndex: 'index',
+        valueType: 'indexBorder',
+        align: 'center',
+        width: '20%',
+        render: (_, record, index) =>
+          index + 1 <= 3 ? <Iconfont type="icon-trophy" className={colorMap[index + 1]} /> : ''
+      },
+      {
+        title: '姓名',
+        dataIndex: `${activeOp}Name`,
+        align: 'center',
+        width: '20%'
+      },
+      {
+        title: '销售',
+        dataIndex: 'softSales',
+        align: 'center',
+        width: '20%'
+      },
+      {
+        title: '借出',
+        dataIndex: 'softLend',
+        align: 'center',
+        width: '20%'
+      },
+      {
+        title: '赠送',
+        dataIndex: 'softGift',
+        align: 'center',
+        width: '20%'
+      }
+    ]
+
+    let stop = false
+    const bColumns = oColumns.reduce((prev, curr, idx) => {
+      if (idx < 4) {
+        prev.push(curr)
+        return prev
+      }
+
+      if (!stop && curr.dataIndex.startsWith(activeOp)) {
+        stop = true
+        prev.push(curr)
+        return prev
+      }
+      return prev
+    }, [])
+    return bColumns
+  }, [activeOp])
+
+  return (
+    <Card
+      loading={loading}
+      bodyStyle={{ padding: loading ? '24px' : 0 }}
+      className="shadow-card"
+      title={
+        <div className="px-4 pt-2 pb-0 flex items-center justify-between">
+          <span>产品排行榜</span>
+          <Select defaultValue="soft" onChange={e => setActiveOp(e)}>
+            <Option value="soft">软件锁</Option>
+          </Select>
+        </div>
+      }
+    >
+      <div className="text-center border border-x-0 p-2">{opMap[activeOp]}排行榜</div>
+      <div className="">
+        <ProTable
+          border={true}
+          rowKey={record => record.rank}
+          columns={columns}
+          dataSource={productLeaderBoard}
+          search={false}
+          size="small"
+          toolBarRender={false}
+          pagination={false}
+        />
+      </div>
+    </Card>
+  )
+}
+
+export default SoftLeaderboard

+ 59 - 0
src/pages/Workbench/Dashboard/consts.js

@@ -0,0 +1,59 @@
+export const cyclicalOp = [
+  {
+    label: '今天',
+    value: 'today',
+    title: '较昨天'
+  },
+  {
+    label: '昨天',
+    value: 'yesterday',
+    title: '较前天'
+  },
+  {
+    label: '本周',
+    value: 'week',
+    title: '较上周'
+  },
+  {
+    label: '上周',
+    value: 'lastWeek',
+    title: '较前周'
+  },
+  {
+    label: '本月',
+    value: 'month',
+    title: '较上月'
+  },
+  {
+    label: '上月',
+    value: 'lastMonth',
+    title: '较前月'
+  },
+  {
+    label: '本季度',
+    value: 'quarter',
+    title: '较上季'
+  },
+  {
+    label: '上季度',
+    value: 'lastQuarter',
+    title: '较前季'
+  },
+  {
+    label: '本年',
+    value: 'year',
+    title: '较去年'
+  },
+  {
+    label: '去年',
+    value: 'lastYear',
+    title: '较前年'
+  }
+]
+
+export const cardTypeMap = {
+  CLIENT: 0, // 客户
+  COMPANY: 1, // 单位
+  BUSINESS: 2, // 商机
+  SERVICE: 3 // 服务记录
+}

+ 542 - 0
src/pages/Workbench/Dashboard/index.jsx

@@ -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

+ 11 - 0
src/pages/Workbench/Dashboard/index.less

@@ -0,0 +1,11 @@
+.dashboard {
+  height: calc(100vh - 48px - 48px);
+  overflow-y: scroll;
+
+  :global(.ant-pro-basicLayout-content) {
+    margin: 24px 24px 0 24px !important;
+  }
+  &::-webkit-scrollbar {
+    display: none;
+  }
+}