Bladeren bron

feat: 增加发票收款入账趋势页面

lanjianrong 4 jaren geleden
bovenliggende
commit
3e92be71af

+ 5 - 0
config/routes.js

@@ -171,6 +171,11 @@ export default [
             path: 'invoice-aggregate',
             name: 'invoice-aggregate',
             component: './Statistic/Product/InvoiceAggregate'
+          },
+          {
+            path: 'invoice-trends',
+            name: 'invoice-trends',
+            component: './Statistic/Product/InvoiceTrends'
           }
         ]
       }

+ 1 - 1
package.json

@@ -53,7 +53,7 @@
     "@ant-design/pro-form": "1.42.1",
     "@ant-design/pro-layout": "6.19.3",
     "@ant-design/pro-table": "2.60.0",
-    "@arco-design/web-react": "^2.25.0",
+    "@arco-design/web-react": "2.26.2",
     "@icon-park/react": "^1.3.3",
     "@umijs/route-utils": "^1.0.33",
     "ahooks": "^2.10.0",

+ 1 - 1
src/app.tsx

@@ -1,7 +1,7 @@
 import { queryCurrentUser, queryPermData } from './services/user'
 import { history } from 'umi'
 import RightContent from '@/components/RightContent'
-import { message } from 'antd'
+import { message, notification } from 'antd'
 import logo from '../public/logo.svg'
 import BasicModal, { BasicDrawer } from '@/components/Modal'
 import { getToken } from '@/utils/auth'

+ 2 - 1
src/locales/zh-CN/menu.js

@@ -37,5 +37,6 @@ export default {
   'menu.statistic': '统计',
   'menu.statistic.product': '产品销售',
   'menu.statistic.product.software-usage': '软件锁用量',
-  'menu.statistic.product.invoice-aggregate': '发票收款入账汇总'
+  'menu.statistic.product.invoice-aggregate': '发票收款入账汇总',
+  'menu.statistic.product.invoice-trends': '发票收款入账趋势'
 }

+ 15 - 6
src/pages/Customer/Company/components/CompanyDetail/TabList.jsx

@@ -31,6 +31,7 @@ const CompanyTabList = ({
   business,
   updateKey
 }) => {
+  console.log('111')
   const dispatch = useDispatch()
   const { toggleDrawer, setDrawerProps } = useModal()
   const { TabPane } = Tabs
@@ -108,7 +109,8 @@ const CompanyTabList = ({
         if (key !== defaultSelectedKey) {
           changePriority(key, id)
         }
-      }}>
+      }}
+    >
       <Menu.Item key="1">1</Menu.Item>
       <Menu.Item key="2">2</Menu.Item>
       <Menu.Item key="3">3</Menu.Item>
@@ -130,7 +132,8 @@ const CompanyTabList = ({
         <Dropdown
           overlay={priorityMenu(text, record.id)}
           trigger="click"
-          className="hover:cursor-pointer">
+          className="hover:cursor-pointer"
+        >
           <span>
             {text} <DownOutlined />
           </span>
@@ -157,7 +160,10 @@ const CompanyTabList = ({
           value: item
         }))
       ),
-      onFilter: (value, item) => item.department === value
+      onFilter: (value, item) => item.department === value,
+      onFilterDropdownVisibleChange: visible => {
+        console.log(visible)
+      }
     },
     {
       title: '姓名',
@@ -215,14 +221,16 @@ const CompanyTabList = ({
             <Text delete type="secondary">
               <span
                 onClick={() => showLongleDrawer(record.id)}
-                className="cursor-pointer hover:text-hex-967bbd">
+                className="cursor-pointer hover:text-hex-967bbd"
+              >
                 {clientName}
               </span>
             </Text>
           ) : (
             <span
               onClick={() => showLongleDrawer(record.id)}
-              className="text-primary cursor-pointer hover:text-hex-967bbd">
+              className="text-primary cursor-pointer hover:text-hex-967bbd"
+            >
               {clientName}
             </span>
           )}
@@ -307,7 +315,8 @@ const CompanyTabList = ({
       render: (text, record) => (
         <span
           onClick={() => showDrawerBusiness(record.id)}
-          className="text-primary cursor-pointer hover:text-[#967bbd]">
+          className="text-primary cursor-pointer hover:text-[#967bbd]"
+        >
           {text}
         </span>
       )

+ 7 - 5
src/pages/Statistic/Product/InvoiceAggregate/index.jsx

@@ -53,7 +53,7 @@ const InvoiceAggregate = () => {
       const newList = [...prev]
       const keys = Object.keys(curr)
       keys.forEach(key => {
-        if (key !== 'name') {
+        if (key !== 'name' && key !== 'id') {
           newList.push({
             department: curr.name,
             type: columnGroupMap[key],
@@ -64,16 +64,18 @@ const InvoiceAggregate = () => {
       return newList
     }, []),
     isGroup: true,
-    padding: 'auto',
-    appendPadding: [12, 12, 12, 12],
+    autoFit: true,
+    // padding: 'auto',
+    // dodgePadding: 2,
+    appendPadding: [23, 32, 24, 32],
     xField: 'department',
     yField: 'value',
+    seriesField: 'type',
     legend: {
       position: 'top'
     },
     color: ['#A7D2F9', '#C4E9C1', '#E9C8C1'],
-    colorField: 'type',
-    seriesField: 'type'
+    colorField: 'type'
   }
   return (
     <div className="flex flex-col">

+ 107 - 0
src/pages/Statistic/Product/InvoiceTrends/index.jsx

@@ -0,0 +1,107 @@
+import BasicTable from '@/components/Table'
+import consts from '@/consts'
+import { querySoftInvoiceTrendsList } from '@/services/statistic'
+// import { Column } from '@ant-design/charts'
+import { DatePicker, Select } from 'antd'
+import dayjs from 'dayjs'
+import React, { useRef, useState } from 'react'
+import OptionSelect from '../../components/OptionSelect'
+
+const InvoiceTrends = () => {
+  const [state, setState] = useState({
+    params: {
+      optionType: 'staff',
+      dataIds: [],
+      priceType: 'invoice',
+      startMonth: [dayjs(new Date()), dayjs(new Date())]
+    },
+    list: []
+  })
+  const tRef = useRef(null)
+  const columns = [
+    {
+      dataIndex: 'name',
+      title: '姓名',
+      align: 'center'
+    },
+    {
+      dataIndex: 'creditPrice',
+      title: '开票金额(元)',
+      align: 'center'
+    },
+    {
+      dataIndex: 'receivablesPrice',
+      title: '收款金额(元)',
+      align: 'center'
+    },
+    {
+      dataIndex: 'invoicePrice',
+      title: '入账金额(元)',
+      align: 'center'
+    }
+  ]
+
+  const optionChange = (type, ids) =>
+    setState({ ...state, params: { ...state.params, optionType: type, dataIds: ids } })
+
+  return (
+    <div className="flex flex-col">
+      <div className="flex flex-row justify-between mb-4">
+        <div className="card-transition bg-white w-full">
+          {/* {pieData.pieLeft ? <Pie {...leftConfig} /> : null} */}
+          {/* {state.list?.length ? <Column {...columnConfig} /> : null} */}
+        </div>
+      </div>
+
+      <BasicTable
+        // manualRequest
+        search={false}
+        columns={columns}
+        actionRef={tRef}
+        rowkey="id"
+        toolbar={{
+          title: [
+            <OptionSelect onChange={optionChange} type="InvoiceTrends" key="op" multiple={false} />,
+            <div className="ml-1" key="pp">
+              <Select
+                options={[
+                  { label: '发票', value: 'invoice' },
+                  { label: '收款', value: 'receivables' },
+                  { label: '入账', value: 'credit' }
+                ]}
+                placeholder="请选择发票类型"
+                onChange={e => setState({ ...state, params: { ...state.params, priceType: e } })}
+              />
+            </div>
+          ],
+          actions: [
+            <DatePicker.RangePicker
+              picker="month"
+              key="DatePicker"
+              disabled={[false, true]}
+              value={state.startMonth}
+              onChange={val => setState({ ...state, params: { ...state.params, startMonth: val } })}
+            />
+          ]
+        }}
+        params={state.params}
+        pagination={false}
+        bordered
+        request={async (params, sort, filter) => {
+          const {
+            code = -1,
+            data: { list, sum }
+          } = await querySoftInvoiceTrendsList({ ...params, ...sort, ...filter })
+          setState({ ...state, sum, list })
+          return {
+            data: list,
+            success: code === consts.RET_CODE.SUCCESS,
+            total: list?.length || 0
+          }
+        }}
+      />
+    </div>
+  )
+}
+
+export default InvoiceTrends

+ 72 - 30
src/pages/Statistic/components/OptionSelect.tsx

@@ -4,23 +4,49 @@ import { getAuthCache, setAuthCache } from '@/utils/auth'
 import { STATISTIC_OPTION_KEY } from '@/utils/cache/cacheEnum'
 import { useDebounceFn } from 'ahooks'
 import { Select, TreeSelect } from 'antd'
-import React, { useState, useEffect } from 'react'
+import React, { useState, useEffect, useMemo } from 'react'
 
 export enum statisticOptionMap {
   STAFF = 'staff',
-  DEPARTMENT = 'department'
+  DEPARTMENT = 'department',
+  SINGLE_STAFF = 'singleStaff',
+  SINGLE_DEPARTMENT = 'singleDepartment'
 }
 
 type OptionSelectProps = {
+  /** @name 是否多选 */
+  multiple?: boolean
   type: string
   onChange: (type: 'staff' | 'department', value) => void
 }
-const OptionSelect: React.FC<OptionSelectProps> = ({ type, onChange }) => {
+
+const options = [
+  { label: '按部门', value: 'department' },
+  { label: '按员工', value: 'staff' },
+  { label: '指定部门', value: 'singleDepartment' },
+  { label: '指定员工', value: 'singleStaff' }
+]
+
+function generateTree(tree) {
+  return tree.map(item => {
+    const newItem = { ...item }
+    if (item.children?.length) {
+      newItem.children = generateTree(item.children)
+    }
+    if (!item.isLeaf) {
+      newItem.disabled = true
+    }
+    return newItem
+  })
+}
+
+const OptionSelect: React.FC<OptionSelectProps> = ({ multiple = true, type, onChange }) => {
   const authOptions = getAuthCache(STATISTIC_OPTION_KEY)
   const { optionType, dataIds } = authOptions?.[type] || {}
 
   const [state, setState] = useState({
-    optionType: optionType || statisticOptionMap.STAFF,
+    optionType:
+      optionType || (multiple ? statisticOptionMap.STAFF : statisticOptionMap.SINGLE_STAFF),
     dataIds: dataIds || [],
     department: [],
     staff: []
@@ -55,40 +81,56 @@ const OptionSelect: React.FC<OptionSelectProps> = ({ type, onChange }) => {
     },
     { wait: 500 }
   )
+
+  const treeOptions = useMemo(() => {
+    const base = {
+      className: 'min-w-180px',
+      dropdownMatchSelectWidth: true,
+      onChange: value => trySetOpIds(value)
+    }
+    if ([statisticOptionMap.STAFF, statisticOptionMap.DEPARTMENT].includes(state.optionType)) {
+      base.multiple = true
+    }
+
+    if ([statisticOptionMap.STAFF, statisticOptionMap.SINGLE_STAFF].includes(state.optionType)) {
+      base.showCheckedStrategy = 'SHOW_PARENT'
+
+      base.treeData =
+        state.optionType === statisticOptionMap.SINGLE_STAFF
+          ? generateTree(state.staff)
+          : state.staff
+    }
+
+    if (
+      [statisticOptionMap.DEPARTMENT, statisticOptionMap.SINGLE_DEPARTMENT].includes(
+        state.optionType
+      )
+    ) {
+      base.treeCheckable = state.optionType === statisticOptionMap.SINGLE_DEPARTMENT ? false : true
+      base.treeData = state.department
+    }
+
+    return base
+  }, [state.optionType, state.department, state.staff])
+
+  console.log('options', options)
+
   return (
     <div>
       <span className="mr-2">
         <Select
           defaultValue={state.optionType}
-          options={[
-            { label: '按部门', value: 'department' },
-            { label: '按员工', value: 'staff' }
-          ]}
+          options={options.filter(item => {
+            if (!multiple) {
+              return ['singleDepartment', 'singleStaff'].includes(item.value)
+            } else {
+              return ['department', 'staff'].includes(item.value)
+            }
+          })}
           onChange={val => setState({ ...state, optionType: val, dataIds: [] })}
         />
       </span>
-      {state.optionType === statisticOptionMap.STAFF ? (
-        <TreeSelect
-          value={state.dataIds}
-          className="min-w-180px"
-          // defaultOpen
-          treeCheckable
-          showCheckedStrategy="SHOW_PARENT"
-          dropdownMatchSelectWidth
-          treeData={state.staff}
-          onChange={value => trySetOpIds(value)}
-        />
-      ) : (
-        <TreeSelect
-          value={state.dataIds}
-          className="min-w-180px"
-          // defaultOpen
-          treeData={state.department}
-          dropdownMatchSelectWidth
-          multiple
-          onChange={value => trySetOpIds(value)}
-        />
-      )}
+      <TreeSelect value={state.dataIds} {...treeOptions} />
     </div>
   )
 }

+ 8 - 0
src/services/statistic.js

@@ -20,3 +20,11 @@ export async function querySoftInvoiceAggregateList(params) {
     data: params
   })
 }
+
+/** 获取发票趋势 */
+export async function querySoftInvoiceTrendsList(params) {
+  return request('/statistic/invoice/trends', {
+    method: 'POST',
+    data: params
+  })
+}

+ 2 - 3
src/utils/utils.js

@@ -1,6 +1,5 @@
 import { parse } from 'querystring'
 import dayjs from 'dayjs'
-/* eslint no-useless-escape:0 import/prefer-default-export:0 */
 
 const reg =
   /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/
@@ -64,5 +63,5 @@ export const refactorFilterField = filter => {
   return nFiter
 }
 // 可控长度的随机数拼接时间戳成产唯一id
-export const createUid = length =>
-  Number(Math.random().toString().substr(3, length) + Date.now()).toString(36)
+export const createUid = () => URL.createObjectURL(new Blob()).substr(-36)
+// Number(Math.random().toString().substr(3, length) + Date.now()).toString(36)