完成统计页,待后端上线

main
wuyize 2023-01-05 12:47:16 +08:00
parent b30fbc9fb9
commit eac61bc7ba
3 changed files with 3706 additions and 133 deletions

3273
src/assets/GeoCoordMap.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,13 @@
export interface StatResponse { export interface StatResponse {
/**
* yearly=true1212
*/
departmentStats: Array<DepartmentStat[]>;
invoiceKindsStats: InvoiceKindsStat[]; invoiceKindsStats: InvoiceKindsStat[];
invoiceLaunchCount: number; invoiceLaunchCount: number;
lastAdditionalTotalAmount: number; lastAdditionalTotalAmount: number;
lastAllTotalAmount: number; lastAllTotalAmount: number;
reimbursementAdditionalTotalAmount: number; reimbursementAdditionalTotalAmount: number;
reimbursementAllTotalAmount: number; reimbursementAllTotalAmount: number;
reimbursementDepartureStats: ReimbursementDepartureStat[];
reimbursementDestinationStats: ReimbursementDestinationStat[];
reimbursementLaunchCount: number; reimbursementLaunchCount: number;
} reimbursementPlaceStats: ReimbursementPlaceStat[];
temporalDepartmentStats: TemporalDepartmentStat[];
export interface DepartmentStat {
departmentName: string;
reimbursementAmount: string;
} }
export interface InvoiceKindsStat { export interface InvoiceKindsStat {
@ -24,12 +15,21 @@ export interface InvoiceKindsStat {
invoiceLaunchCount: number; invoiceLaunchCount: number;
} }
export interface ReimbursementDepartureStat { export interface ReimbursementPlaceStat {
placeName: string; departureName: string;
destinationName: string;
reimbursementLaunchCount: number; reimbursementLaunchCount: number;
} }
export interface ReimbursementDestinationStat { export interface TemporalDepartmentStat {
placeName: string; departmentStats: DepartmentStat[];
reimbursementLaunchCount: number; /**
*
*/
value: number;
}
export interface DepartmentStat {
departmentId: number;
reimbursementAmount: number;
} }

View File

@ -1,22 +1,45 @@
import type {DatePickerProps, TimePickerProps} from 'antd'; import type {DatePickerProps, TimePickerProps} from 'antd';
import {theme, DatePicker, Select, Space, Card, Statistic, Spin} from 'antd'; import {theme, DatePicker, Select, Space, Card, Statistic, Spin, Radio} from 'antd';
import {ArrowDownOutlined, ArrowUpOutlined, createFromIconfontCN} from '@ant-design/icons' import {ArrowDownOutlined, ArrowUpOutlined, createFromIconfontCN} from '@ant-design/icons'
import {useEffect, useRef, useState} from "react"; import React, {useEffect, useRef, useState} from "react";
import ReactECharts from 'echarts-for-react'; import ReactECharts from 'echarts-for-react';
import * as Echarts from 'echarts' import * as Echarts from 'echarts'
import china from '../../assets/mapjson/china.json' import china from '../../assets/mapjson/china.json'
import dayjs, {Dayjs} from "dayjs"; import dayjs, {Dayjs} from "dayjs";
import {DepartmentStat, ReimbursementPlaceStat, StatResponse} from "../../models/Stat";
import {invoiceTypeNameMap} from "../../models/Invoice";
import GeoCoordMap from "../../assets/GeoCoordMap";
import axiosInstance from "../../utils/axiosInstance";
import {Department, Staff} from "../../models/Staff";
import {setStaff} from "../../models/store";
// @ts-ignore // @ts-ignore
Echarts.registerMap('china', china); Echarts.registerMap('china', china);
const {Option} = Select; const {Option} = Select;
type PickerType = 'month' | 'year'; type StatType = 'month' | 'year';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_3830502_eyfw75skyu.js', scriptUrl: '//at.alicdn.com/t/c/font_3830502_eyfw75skyu.js',
}); });
function TrendStatistic(props: { value: number, type: StatType }) {
const {
token: {colorSuccess, colorError},
} = theme.useToken();
return (
<Statistic
title={props.type === 'month' ? "较上个月" : '较去年'}
value={Math.abs(props.value)}
precision={2}
valueStyle={{color: props.value >= 0 ? colorSuccess : colorError}}
prefix={props.value >= 0 ? <ArrowUpOutlined/> : <ArrowDownOutlined/>}
suffix="%"
/>
)
}
function HorizontalBarChart(props: { title: string, values: number[], labels: string[] }) { function HorizontalBarChart(props: { title: string, values: number[], labels: string[] }) {
return <ReactECharts option={{ return <ReactECharts option={{
color: [ color: [
@ -73,20 +96,64 @@ function HorizontalBarChart(props: { title: string, values: number[], labels: st
}} notMerge={true} lazyUpdate={true}/> }} notMerge={true} lazyUpdate={true}/>
} }
var mMapData =
[
{from: '青海省', to: '青海省', value: 90},
{from: '青海省', to: '安徽省', value: 80},
{from: '青海省', to: '甘肃省', value: 70},
{from: '青海省', to: '宁夏省', value: 60},
{from: '青海省', to: '山西省', value: 50},
{from: '青海省', to: '陕西省', value: 40},
{from: '青海省', to: '广东省', value: 30},
{from: '青海省', to: '重庆省', value: 20},
{from: '青海省', to: '西藏省', value: 10}
];
var convertLineData = function (data: { from: string, to: string, value: number }[]) {
var res = [];
for (var i = 0; i < data.length; i++) {
var dataItem = data[i];
// @ts-ignore
var fromCoord = GeoCoordMap[dataItem.from];
// @ts-ignore
var toCoord = GeoCoordMap[dataItem.to];
if (fromCoord && toCoord) {
res.push(
{
fromName: dataItem.from,
toName: dataItem.to,
coords: [fromCoord, toCoord],
value: dataItem.value,
});
}
}
return res;
};
function StatView() { function StatView() {
const { const {
token: {colorBgContainer, colorPrimary, colorSuccess}, token: {colorBgContainer, colorPrimary, colorSuccess},
} = theme.useToken(); } = theme.useToken();
const [type, setType] = useState<PickerType>('month'); const [type, setType] = useState<StatType>('month');
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [reimbursementAmount, setReimbursementAmount] = useState(0)
const [reimbursementAmountTrend, setReimbursementAmountTrend] = useState(0)
const [reimbursementAdditionalAmount, setReimbursementAdditionalAmount] = useState(0)
const [reimbursementAdditionalAmountTrend, setReimbursementAdditionalAmountTrend] = useState(0)
const [amounts, setAmounts] = useState([0, 0]) const [amounts, setAmounts] = useState([0, 0])
const [invoiceKinds, setInvoiceKinds] = useState<any>([]) const [invoiceKinds, setInvoiceKinds] = useState<any>([])
const [departmentPieData, setDepartmentPieData] = useState<any>([]) const [departmentPieData, setDepartmentPieData] = useState<any>([])
const [mapData, setMapData] = useState<any>([])
const [departureNames, setDepartureNames] = useState<any>([]) const [departureNames, setDepartureNames] = useState<any>([])
const [departureValues, setDepartureValues] = useState<any>([]) const [departureValues, setDepartureValues] = useState<any>([])
const [destinationNames, setDestinationNames] = useState<any>([]) const [destinationNames, setDestinationNames] = useState<any>([])
const [destinationValues, setDestinationValues] = useState<any>([]) const [destinationValues, setDestinationValues] = useState<any>([])
const [dates, setDates] = useState<any>([])
const [departmentSeries, setDepartmentSeries] = useState<any>([])
useEffect(() => { useEffect(() => {
getStatData(dayjs()) getStatData(dayjs())
@ -95,24 +162,302 @@ function StatView() {
const getStatData = (time: Dayjs | null) => { const getStatData = (time: Dayjs | null) => {
console.log(time) console.log(time)
setLoading(true) setLoading(true)
axiosInstance({
url: 'common/department',
method: 'get'
}).then(response => {
const departments: Department[] = response.data
let departmentMap = new Map<number, string>()
for (const department of departments)
departmentMap.set(department.departmentId, department.departmentName)
if (time === null)
time = dayjs()
let dates: string[] = []
if (type === 'month')
for (let i = 1; i <= time.daysInMonth(); i++) {
dates.push(time.date(i).format('M月D日'))
}
else
for (let i = 1; i <= 12; i++) {
dates.push(i + '月')
}
setDates(dates)
setTimeout(() => { setTimeout(() => {
setAmounts([120, 43]) let statResponse: StatResponse = {
setInvoiceKinds([{value: 1048, name: '增值税发票'}, "temporalDepartmentStats": [
{value: 735, name: '火车票'}, {
{value: 580, name: '机票行程单'}, "value": 4,
{value: 484, name: '通用机打发票'}, "departmentStats": [
{value: 300, name: '过路过桥费发票'}]) {
setDepartmentPieData([{value: 1048, name: '钝角部'}, "departmentId": 1,
{value: 735, name: '投影立体角部'}, "reimbursementAmount": 79
{value: 580, name: '财务部'}, },
{value: 484, name: '采购部'}, {
{value: 300, name: '销售部'}]) "departmentId": 2,
setDepartureNames(['北京', '上海', '广州', '深圳', '成都']) "reimbursementAmount": 9
setDepartureValues([100, 80, 70, 60, 50]) },
setDestinationNames(['北京', '上海', '广州', '深圳', '成都']) {
setDestinationValues([100, 80, 70, 60, 50]) "departmentId": 3,
"reimbursementAmount": 29
},
{
"departmentId": 4,
"reimbursementAmount": 46
},
{
"departmentId": 5,
"reimbursementAmount": 66
}
]
},
{
"value": 3,
"departmentStats": [
{
"departmentId": 1,
"reimbursementAmount": 79
},
{
"departmentId": 2,
"reimbursementAmount": 9
},
{
"departmentId": 3,
"reimbursementAmount": 29
},
{
"departmentId": 4,
"reimbursementAmount": 46
},
{
"departmentId": 5,
"reimbursementAmount": 66
}
]
},
{
"value": 6,
"departmentStats": [
{
"departmentId": 1,
"reimbursementAmount": 79
},
{
"departmentId": 2,
"reimbursementAmount": 9
},
{
"departmentId": 3,
"reimbursementAmount": 29
},
{
"departmentId": 4,
"reimbursementAmount": 46
},
{
"departmentId": 5,
"reimbursementAmount": 66
}
]
},
{
"value": 13,
"departmentStats": [
{
"departmentId": 1,
"reimbursementAmount": 79
},
{
"departmentId": 2,
"reimbursementAmount": 9
},
{
"departmentId": 3,
"reimbursementAmount": 29
},
{
"departmentId": 4,
"reimbursementAmount": 46
},
{
"departmentId": 5,
"reimbursementAmount": 66
}
]
}
],
"reimbursementAllTotalAmount": 79,
"reimbursementAdditionalTotalAmount": 25,
"lastAllTotalAmount": 7,
"lastAdditionalTotalAmount": 19,
"reimbursementLaunchCount": 86,
"invoiceLaunchCount": 26,
"invoiceKindsStats": [
{
"invoiceKind": 1,
"invoiceLaunchCount": 39
},
{
"invoiceKind": 2,
"invoiceLaunchCount": 39
},
{
"invoiceKind": 3,
"invoiceLaunchCount": 39
},
{
"invoiceKind": 5,
"invoiceLaunchCount": 39
},
{
"invoiceKind": 6,
"invoiceLaunchCount": 39
},
{
"invoiceKind": 7,
"invoiceLaunchCount": 39
},
],
"reimbursementPlaceStats": [
{
"departureName": "青海",
"reimbursementLaunchCount": 80,
"destinationName": "安徽"
},
{
"departureName": "青海",
"reimbursementLaunchCount": 70,
"destinationName": "甘肃"
}
]
}
setReimbursementAmount(statResponse.reimbursementAllTotalAmount)
setReimbursementAmountTrend(
(statResponse.reimbursementAllTotalAmount - statResponse.lastAllTotalAmount) / statResponse.lastAllTotalAmount)
setReimbursementAdditionalAmount(statResponse.reimbursementAdditionalTotalAmount)
setReimbursementAdditionalAmountTrend(
(statResponse.reimbursementAdditionalTotalAmount - statResponse.lastAdditionalTotalAmount) / statResponse.lastAdditionalTotalAmount)
setAmounts([statResponse.invoiceLaunchCount, statResponse.reimbursementLaunchCount])
setInvoiceKinds(statResponse.invoiceKindsStats.map((item) => {
return {value: item.invoiceLaunchCount, name: invoiceTypeNameMap.get(item.invoiceKind)}
}))
let departmentAmountMap = new Map<number, Map<number, number>>()
departmentMap.forEach(function (value, key, map) {
departmentAmountMap.set(key, new Map<number, number>())
})
for (const temporalDepartmentStat of statResponse.temporalDepartmentStats) {
for (const departmentStat of temporalDepartmentStat.departmentStats) {
let valueMap = departmentAmountMap.get(departmentStat.departmentId)
if (valueMap) {
valueMap.set(temporalDepartmentStat.value, departmentStat.reimbursementAmount)
departmentAmountMap.set(departmentStat.departmentId, valueMap)
}
}
}
let data: { value: number, name: string }[] = []
let series: any = []
departmentAmountMap.forEach(function (valueMap, key, map) {
let departmentName = departmentMap.get(key)
if (!departmentName) return
let total = 0
valueMap.forEach(function (value, key) {
total += value
})
data.push({value: total, name: departmentName})
let valueMapArray = Array.from(valueMap);
valueMapArray.sort(function (a, b) {
return a[0] - b[0]
})
series.push({
name: departmentName,
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: valueMapArray.map(function ([key, value]) {
return value
})
})
})
setDepartmentPieData(data)
setDepartmentSeries(series)
const geoCoordMap = Object.entries(GeoCoordMap)
const getCoords = (placeName: string) => {
return geoCoordMap.find(([name,]) => name.includes(placeName))
}
let mapDataTemp: { fromName: string, toName: string, coords: any[], value: number }[] = []
let departures = new Map<string, number>()
let destinations = new Map<string, number>()
for (const item of statResponse.reimbursementPlaceStats) {
let departureValue = departures.get(item.departureName)
if (departureValue)
departures.set(item.departureName, departureValue + item.reimbursementLaunchCount)
else
departures.set(item.departureName, item.reimbursementLaunchCount)
let destinationValue = departures.get(item.destinationName)
if (destinationValue)
destinations.set(item.destinationName, destinationValue + item.reimbursementLaunchCount)
else
destinations.set(item.destinationName, item.reimbursementLaunchCount)
const fromCoords = getCoords(item.departureName)
const toCoords = getCoords(item.destinationName)
if (fromCoords && toCoords) {
mapDataTemp.push({
"fromName": fromCoords[0],
"toName": toCoords[0],
"coords": [fromCoords[1], toCoords[1]],
"value": item.reimbursementLaunchCount,
})
}
}
setMapData(mapDataTemp)
{
let placeNames: string[] = []
let reimbursementCounts: number[] = []
departures.forEach(function (val, key, map) {
placeNames.push(key)
reimbursementCounts.push(val)
})
setDepartureNames(placeNames)
setDepartureValues(reimbursementCounts)
}
{
let placeNames: string[] = []
let reimbursementCounts: number[] = []
destinations.forEach(function (val, key, map) {
placeNames.push(key)
reimbursementCounts.push(val)
})
setDestinationNames(placeNames)
setDestinationValues(reimbursementCounts)
}
setLoading(false) setLoading(false)
}, 1000); }, 1000);
}).catch(function (error) {
console.log(error)
})
} }
return ( return (
@ -138,53 +483,39 @@ function StatView() {
</Space> </Space>
</div> </div>
{loading ? {loading ?
<div style={{flex: 1,display: "flex", alignItems: "center", justifyContent: "center"}}> <div style={{flex: 1, display: "flex", alignItems: "center", justifyContent: "center"}}>
<Spin size="large"/> <Spin size="large"/>
</div> : </div> :
<div> <div>
<div style={{ <div style={{
width: '100%', width: '100%',
padding: '30px 30px 0px 30px', padding: '20px 20px 0px 20px',
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
flexWrap: "wrap" flexWrap: "wrap"
}}> }}>
<div style={{flex: 1, flexBasis: 400, display: "flex", flexDirection: "row", flexWrap: "wrap"}}> <div style={{flex: 1, flexBasis: 400, display: "flex", flexDirection: "row", flexWrap: "wrap"}}>
<Card style={{flex: 1, flexBasis: 200, margin: 10}}> <Card style={{flex: 1, flexBasis: 200, margin: 10}}>
<Statistic title="报销金额" value={1128.45} precision={2} <Statistic title="报销金额" value={reimbursementAmount} precision={2}
prefix={<IconFont type={'icon-renminbi'}/>}/> prefix={<IconFont type={'icon-renminbi'}/>}/>
</Card> </Card>
<Card style={{flex: 1, flexBasis: 200, margin: 10}}> <Card style={{flex: 1, flexBasis: 200, margin: 10}}>
<Statistic <TrendStatistic type={type} value={reimbursementAmountTrend}/>
title={type === 'month' ? "较上个月" : '较去年'}
value={11.28}
precision={2}
valueStyle={{color: '#3f8600'}}
prefix={<ArrowUpOutlined/>}
suffix="%"
/>
</Card> </Card>
</div> </div>
<div style={{flex: 1, flexBasis: 400, display: "flex", flexDirection: "row", flexWrap: "wrap"}}> <div style={{flex: 1, flexBasis: 400, display: "flex", flexDirection: "row", flexWrap: "wrap"}}>
<Card style={{flex: 1, flexBasis: 200, margin: 10}}> <Card style={{flex: 1, flexBasis: 200, margin: 10}}>
<Statistic title="补贴金额" value={1128.45} precision={2} <Statistic title="补贴金额" value={reimbursementAdditionalAmount} precision={2}
prefix={<IconFont type={'icon-renminbi'}/>}/> prefix={<IconFont type={'icon-renminbi'}/>}/>
</Card> </Card>
<Card style={{flex: 1, flexBasis: 200, margin: 10}}> <Card style={{flex: 1, flexBasis: 200, margin: 10}}>
<Statistic <TrendStatistic type={type} value={reimbursementAdditionalAmountTrend}/>
title={type === 'month' ? "较上个月" : '较去年'}
value={11.28}
precision={2}
valueStyle={{color: '#3f8600'}}
prefix={<ArrowUpOutlined/>}
suffix="%"
/>
</Card> </Card>
</div> </div>
</div> </div>
<div style={{ <div style={{
width: '100%', width: '100%',
padding: '0 30px 0 30px', padding: '0 20px 0 20px',
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
flexWrap: "wrap" flexWrap: "wrap"
@ -333,7 +664,7 @@ function StatView() {
<div style={{ <div style={{
width: '100%', width: '100%',
padding: '0px 30px 0px 30px', padding: '0px 20px 0px 20px',
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
flexWrap: "wrap" flexWrap: "wrap"
@ -342,23 +673,38 @@ function StatView() {
<Card style={{flex: 1, flexBasis: 600, margin: 10, minWidth: 0, minHeight: 0}}> <Card style={{flex: 1, flexBasis: 600, margin: 10, minWidth: 0, minHeight: 0}}>
<ReactECharts style={{height: 600}} option={{ <ReactECharts style={{height: 600}} option={{
title: { title: {
text: '差旅流向图', text: '差旅去向',
left: 'center', left: 'center',
}, },
series: [ geo: {
{
type: 'map',
map: 'china', map: 'china',
emphasis: { emphasis: {
disabled: true disabled: true
}, },
select: { select: {
disabled: true disabled: true
},
itemStyle: {
areaColor: '#d4e2fd',
borderColor: '#8c8c8c'
} }
},
series: [
{
type: 'lines',
zlevel: 2,
lineStyle: {
normal: {
color: '#',
width: 3,
opacity: 0.2,
curveness: .3
} }
},
data: mapData
},
] ]
}} notMerge={true} }} notMerge={true} lazyUpdate={true}/>
lazyUpdate={true}/>
</Card> </Card>
</div> </div>
<div style={{flex: 1, flexBasis: 300, display: "flex", flexDirection: "row", flexWrap: "wrap"}}> <div style={{flex: 1, flexBasis: 300, display: "flex", flexDirection: "row", flexWrap: "wrap"}}>
@ -371,8 +717,20 @@ function StatView() {
</Card> </Card>
</div> </div>
</div> </div>
<Card style={{margin: '10px 40px 40px 40px', minWidth: 0, minHeight: 0}}> <Card style={{margin: '10px 30px 30px 30px', minWidth: 0, minHeight: 0}}>
<ReactECharts option={{ <ReactECharts option={{
color: [
'#6395f9',
'#d4e2fd',
'#73deb3',
'#d4f5e8',
'#657798',
'#d0d6e0',
'#7666f9',
'#d5d0fd',
'#f6c022',
'#fcecbd',
],
title: { title: {
text: '各部门报销金额', text: '各部门报销金额',
left: 'center' left: 'center'
@ -388,7 +746,6 @@ function StatView() {
}, },
legend: { legend: {
type: 'scroll', type: 'scroll',
data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine'],
top: 40 top: 40
}, },
grid: { grid: {
@ -402,7 +759,7 @@ function StatView() {
{ {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] data: dates
} }
], ],
yAxis: [ yAxis: [
@ -410,65 +767,8 @@ function StatView() {
type: 'value' type: 'value'
} }
], ],
series: [ series: departmentSeries
{ }} notMerge={true} lazyUpdate={true}/>
name: 'Email',
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [120, 132, 101, 134, 90, 230, 210]
},
{
name: 'Union Ads',
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [220, 182, 191, 234, 290, 330, 310]
},
{
name: 'Video Ads',
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [150, 232, 201, 154, 190, 330, 410]
},
{
name: 'Direct',
type: 'line',
stack: 'Total',
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [320, 332, 301, 334, 390, 330, 320]
},
{
name: 'Search Engine',
type: 'line',
stack: 'Total',
label: {
show: true,
position: 'top'
},
areaStyle: {},
emphasis: {
focus: 'series'
},
data: [820, 932, 901, 934, 1290, 1330, 1320]
}
]
}}
notMerge={true}
lazyUpdate={true}/>
</Card> </Card>
</div> </div>
} }