瀏覽代碼

feat: 合同管理-概括页

lanjianrong 4 年之前
父節點
當前提交
4a3a97510d

+ 76 - 0
src/assets/css/common.scss

@@ -535,3 +535,79 @@
     background-color: #dc3545;
     background-color: #dc3545;
   }
   }
 }
 }
+
+// grid布局
+.pi-grid {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+@mixin multi-border($n) {
+  &.pi-grid-border {
+    & > div {
+      @include pi-border;
+      &:not(:nth-last-child(-n + #{$n})) {
+        &::after {
+          border-bottom: 1px solid $pi-line-color;
+        }
+      }
+      &:not(:nth-child(#{$n}n)) {
+        &::after {
+          border-right: 1px solid $pi-line-color;
+        }
+      }
+    }
+  }
+}
+
+.pi-grid.pi-col-2 {
+  & > div {
+    width: 50%;
+  }
+
+  @include multi-border(2);
+}
+
+.pi-grid.pi-col-3 {
+  & > div {
+    width: calc(100% / 3);
+  }
+
+  @include multi-border(3);
+}
+
+@mixin space-calc($i, $n) {
+  & > div {
+    width: calc((100% - #{($n - 1) * $i}rem) / #{$n});
+    &:not(:nth-last-child(-n + #{$n})) {
+      margin-bottom: #{$i}rem;
+    }
+    &:not(:nth-child(#{$n}n)) {
+      margin-right: #{$i}rem;
+    }
+  }
+}
+
+// 处理列之间需要有空隙情况
+@for $i from 1 through 50 {
+  .pi-grid.pi-col-1.pi-col-space-#{$i} {
+    & > div {
+      margin-bottom: #{$i}rem;
+    }
+  }
+  .pi-grid.pi-col-2.pi-col-space-#{$i} {
+    @include space-calc($i, 2);
+  }
+  .pi-grid.pi-col-3.pi-col-space-#{$i} {
+    @include space-calc($i, 3);
+  }
+  .pi-grid.pi-col-4.pi-col-space-#{$i} {
+    @include space-calc($i, 4);
+  }
+  .pi-grid.pi-col-5.pi-col-space-#{$i} {
+    @include space-calc($i, 5);
+  }
+  .pi-grid.pi-col-6.pi-col-space-#{$i} {
+    @include space-calc($i, 6);
+  }
+}

+ 21 - 0
src/assets/css/variable.scss

@@ -5,3 +5,24 @@ $pi-light-gray: #bbbbbb !default;
 $pi-blue: #007bff !default;
 $pi-blue: #007bff !default;
 $pi-red: #dc3545 !default;
 $pi-red: #dc3545 !default;
 $pi-yellow: #ffc170 !default;
 $pi-yellow: #ffc170 !default;
+
+$pi-line-color: rgba(0,0,0,.125) !default;
+
+// 实现0.5px的效果
+@mixin pi-border {
+  position: relative;
+  &::after {
+    content: ' ';
+    width: 200%;
+    height: 200%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    border-radius: inherit;
+    transform: scale(0.5);
+    transform-origin: 0 0;
+    pointer-events: none;
+    box-sizing: border-box;
+    z-index: 2;
+  }
+}

+ 3 - 3
src/pages/Contract/Content/Spending/api.ts

@@ -73,11 +73,11 @@ export async function apiResfulContract(type: string, payload: object) {
   let url: string = '', method: string = ''
   let url: string = '', method: string = ''
   switch (type) {
   switch (type) {
     case 'create':
     case 'create':
-      url = '/api/contract/paid/create'
+      url = '/api/contract/expenditure/create'
       method = 'post'
       method = 'post'
       break
       break
     case 'update':
     case 'update':
-      url = '/api/contract/paid/update'
+      url = '/api/contract/expenditure/update'
       method = 'post'
       method = 'post'
       break
       break
     case 'close':
     case 'close':
@@ -99,6 +99,6 @@ export async function apiResfulContract(type: string, payload: object) {
     default:
     default:
       break
       break
   }
   }
-  const { data } = await request[method](url, payload)
+  const { data } = await request[method](url, { ...payload, treeType: consts.CONTRACT_TREE.PAID })
   return data
   return data
 }
 }

+ 10 - 0
src/pages/Contract/Content/Summary/api.ts

@@ -0,0 +1,10 @@
+import request from "@/utils/common/request"
+
+/**
+ * 获取合同概述
+ * @param bid 标段id
+ */
+export async function apiGetContractSurvey(bid: string) {
+  const { data } = await request.get('/api/contract/survey', { bidsectionId: bid })
+  return data
+}

+ 34 - 0
src/pages/Contract/Content/Summary/components/Content/index.scss

@@ -0,0 +1,34 @@
+.card-body {
+  padding: 1.25rem;
+  background-color: #ffffff;
+  .card-title {
+    margin-bottom: 0.75rem;
+    font-size: 1.25rem;
+    font-weight: 500;
+    line-height: 1.2;
+    text-align: center;
+  }
+  .progress-content {
+    display: flex;
+    height: 1rem;
+    overflow: hidden;
+    font-size: 0.75rem;
+    background-color: #e9ecef;
+    border-radius: 0.25rem;
+    .progress-bar {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      color: #ffffff;
+      text-align: center;
+      white-space: nowrap;
+      transition: width 0.6s ease;
+    }
+  }
+}
+
+.card-border {
+  background-clip: border-box;
+  border: 1px solid rgba(0, 0, 0, 0.125);
+  border-radius: 0.25rem;
+}

+ 116 - 0
src/pages/Contract/Content/Summary/components/Content/index.tsx

@@ -0,0 +1,116 @@
+import React, { memo } from 'react'
+import './index.scss'
+import { Pie, G2, DualAxes  } from '@ant-design/charts'
+import { PieConfig } from '@ant-design/charts/es/pie'
+import './index.scss'
+import { dayjsFormat, formatMoney } from '@/utils/util'
+import { Tooltip } from 'antd'
+import { DualAxesConfig } from '@ant-design/charts/es/dualAxes'
+G2.registerTheme('custom-pie', {
+  colors10: [ '#C7B8A1', '#9CA8B8', '#7B8B6F' ],
+  colors29: [ '#DACAB1', '#ABB8CA', '#87987A' ]
+})
+
+interface iContentPieState {
+  type: string
+  value: number
+}
+
+interface iContentData {
+  progress: number
+  pieData: iContentPieState[]
+  returnDate: iDualAxes
+  totalContractPrice: number
+  totalContractPriceShow: number
+  totalReturnPriceShow?: number
+  totalPaidPriceShow?: number
+}
+
+type iDualAxes = {
+  month: string
+  value: number
+}[]
+interface iContentProps {
+  data: iContentData
+  type: 'expenditure' | 'income'
+}
+const Content:React.FC<iContentProps> = ({ data, type }) => {
+  console.log(data.progress)
+
+  const pieConfig:PieConfig = {
+    data: data.pieData,
+    autoFit: true,
+    padding: 40,
+    angleField: 'value',
+    colorField: 'type',
+    radius: 1,
+    innerRadius: 0.7,
+    label: {
+      type: 'inner',
+      offset: '-50%',
+      style: { textAlign: 'center' },
+      autoRotate: false,
+      content: '{value}'
+    },
+
+    interactions: [ { type: 'element-selected' }, { type: 'element-active' } ],
+    legend: {
+      layout: 'horizontal',
+      position: 'top'
+    },
+    theme: 'custom-pie'
+  }
+
+  const dualAxesConfig: DualAxesConfig = {
+
+  }
+
+  return (
+    <div className="pi-flex-column">
+      <div className="card-body card-border">
+        <h5 className="card-title">{type === 'expenditure' ? '收入' : '支出'}合同结算进度</h5>
+        <div className="mb-0">
+          <div className="progress-content">
+            <Tooltip title={type === 'expenditure' ? '已回款: ¥' : '已支付: ¥' + data.totalContractPriceShow}>
+              <div className={[ "progress-bar", type === 'expenditure' ? 'pi-bg-success' : 'pi-bg-red' ].join(' ')} style={{ width: (data.progress * 100) + '%' }}>
+                {data.progress * 100}%
+              </div>
+            </Tooltip>
+            <Tooltip title={type === 'expenditure' ? '未回款:¥' + data.totalPaidPriceShow : '未支付:¥' + data.totalReturnPriceShow}>
+              <div className="progress-bar pi-bg-gray" style={{ width: ((1 - data.progress) * 100) + '%'  }} >
+                {((1 - data.progress) * 100)}%
+              </div>
+            </Tooltip>
+          </div>
+        </div>
+      </div>
+      <div className="pi-justify-between">
+        <div className="pi-flex-sub pi-mg-right-15 card-border pi-mg-right-15">
+          <Pie {...pieConfig}></Pie>
+        </div>
+        <div className="pi-flex-sub pi-mg-left-15 pi-mg-left-15">
+          <div className="pi-flex-column pi-justify-between">
+            <div className="card-body card-border pi-flex-column">
+              <h5 className="card-title">{formatMoney(data.totalContractPrice)}</h5>
+              <span className="pi-text-center">{type === 'expenditure' ? '收入' : '支出'}合同金额</span>
+            </div>
+            <div className="card-body card-border pi-flex-column pi-mg-tb-15">
+              <h5 className="card-title">{formatMoney(data.totalContractPriceShow)}</h5>
+              <span className="pi-text-center">{type === 'expenditure' ? '未回款' : '未支付'}</span>
+            </div>
+            <div className="card-body card-border pi-flex-column">
+              <h5 className="card-title">{formatMoney(type === 'expenditure' ? data.totalPaidPriceShow: data.totalReturnPriceShow)}</h5>
+              <span className="pi-text-center">{type === 'expenditure' ? '已回款' : '已支付'}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className="card-body card-border">
+        <h5 className="">{type === 'expenditure' ? '收入' : '支出'}合同结算趋势</h5>
+        {/* <DualAxes></DualAxes> */}
+      </div>
+    </div>
+  )
+}
+
+export default memo(Content)

+ 0 - 0
src/pages/Contract/Content/Summary/index.scss


+ 131 - 3
src/pages/Contract/Content/Summary/index.tsx

@@ -1,9 +1,137 @@
-import React from 'react'
+import Header from '@/components/Header'
+import { tenderStore } from '@/store/mobx'
+import consts from '@/utils/consts'
+import React, { useEffect, useState, useMemo } from 'react'
+import { useActivate } from 'react-activation'
+import { apiGetContractSurvey } from './api'
+import Content from './components/Content'
+import './index.scss'
+
+type ireturnDate = {
+  [key: string]: number
+}
+interface iBaseState {
+  closeNumber: number
+  performNumber: number
+  returnDate: ireturnDate
+  totalContractPrice: number
+  totalContractPriceShow: number
+  uncloseNumber: number
+}
+interface iExpenditureState extends iBaseState {
+  totalPaidPriceShow: number
+}
+
+interface iIncomeState extends iBaseState {
+  totalReturnPriceShow: number
+}
+
+interface iContractSummaryState {
+  expenditureData: iExpenditureState
+  incomeData: iIncomeState
+}
 
 
 export default function Summary() {
 export default function Summary() {
+  useEffect(() => {
+    initData()
+  }, [ tenderStore.bid ])
+  useActivate(() => initData())
+  const [ state, setState ] = useState<iContractSummaryState>({
+    expenditureData: {
+      closeNumber: 0,
+      performNumber: 0,
+      returnDate: {},
+      totalContractPrice: 0,
+      totalContractPriceShow: 0,
+      totalPaidPriceShow: 0,
+      uncloseNumber: 0
+    },
+    incomeData: {
+      closeNumber: 0,
+      performNumber: 0,
+      returnDate: {},
+      totalContractPrice: 0,
+      totalContractPriceShow: 0,
+      totalReturnPriceShow: 0,
+      uncloseNumber: 0
+    }
+  })
+  const initData = async () => {
+    const { code = -1, data } = await apiGetContractSurvey(tenderStore.bid)
+    if (code === consts.RET_CODE.SUCCESS) {
+      setState({ ...state, ...data })
+    }
+  }
+
+  const expenditurePieData = useMemo(() => {
+    const { closeNumber, performNumber, uncloseNumber,  totalContractPrice, totalContractPriceShow, totalPaidPriceShow, returnDate  } = state.expenditureData
+    const pieData = [
+      {
+        type: '正常关闭',
+        value: closeNumber
+      },
+      {
+        type: '履行中',
+        value: performNumber
+      },
+      {
+        type: '待关闭',
+        value: uncloseNumber
+      }
+    ]
+    const dualAxes = []
+    for (const key in returnDate) {
+      if (Object.prototype.hasOwnProperty.call(returnDate, key)) {
+        const value = returnDate[key]
+        dualAxes.push({ month: key, value })
+      }
+    }
+
+    const newDualAxes = dualAxes.map(item => {
+      return { ...item, count: item.value / totalContractPrice * 100 }
+    })
+    const progress = totalPaidPriceShow ? (totalPaidPriceShow / totalContractPrice) : 0
+    return { pieData, returnDate: newDualAxes, totalContractPrice, totalContractPriceShow, totalPaidPriceShow, progress }
+  }, [ state.expenditureData ])
+
+  const incomePieData = useMemo(() => {
+    const { closeNumber, performNumber, uncloseNumber, returnDate, totalContractPrice, totalContractPriceShow, totalReturnPriceShow } = state.incomeData
+
+    const pieData = [
+      {
+        type: '正常关闭',
+        value: closeNumber
+      },
+      {
+        type: '履行中',
+        value: performNumber
+      },
+      {
+        type: '待关闭',
+        value: uncloseNumber
+      }
+    ]
+
+    const dualAxes = []
+    for (const key in returnDate) {
+      if (Object.prototype.hasOwnProperty.call(returnDate, key)) {
+        const value = returnDate[key] as number
+        dualAxes.push({ month: key, value })
+      }
+    }
+    const newDualAxes = dualAxes.map(item => {
+      return { ...item, count: item.value / totalContractPrice * 100 }
+    })
+    const progress = totalReturnPriceShow ? (totalReturnPriceShow / totalContractPrice) : 0
+    return { pieData, returnDate: newDualAxes, totalContractPrice, totalContractPriceShow, totalReturnPriceShow, progress }
+  }, [ state.incomeData ])
   return (
   return (
-    <div>
-<h5>111</h5>
+    <div className="wrap-contaniner">
+      <Header title="合同概况"></Header>
+      <div className="wrap-content m-3 pi-grid pi-col-2 pi-col-space-3">
+        <Content data={expenditurePieData} type="expenditure"></Content>
+        <Content data={incomePieData} type="income"></Content>
+      </div>
     </div>
     </div>
   )
   )
 }
 }

+ 2 - 1
src/utils/util.ts

@@ -199,7 +199,8 @@ const formatDate = (d: string) => {
 }
 }
 
 
 // 数字千分位
 // 数字千分位
-const formatMoney = (num: number) => {
+const formatMoney = (num: number | undefined) => {
+  if (!num) return '0.00'
   return (Math.round(num) + '').replace(/\d{1,3}(?=(\d{3})+(\.\d*)?$)/g, '$&,') + '.00'
   return (Math.round(num) + '').replace(/\d{1,3}(?=(\d{3})+(\.\d*)?$)/g, '$&,') + '.00'
 }
 }
 export { getCookie, storage, throttle, debounce, combinationPath, dayjsFormat, generatePsw, formatDate, formatMoney }
 export { getCookie, storage, throttle, debounce, combinationPath, dayjsFormat, generatePsw, formatDate, formatMoney }