财务门户设计
1
web-vue/packages/assets/icons/erp.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg t="1771406164187" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5408" width="200" height="200"><path d="M845.1 944.2h-600c-16.6 0-30-13.4-30-30v-720c0-16.6 13.4-30 30-30h600c16.6 0 30 13.4 30 30v720c0 16.6-13.5 30-30 30z" fill="#B1CBFA" p-id="5409"></path><path d="M845.1 956.7h-600c-23.4 0-42.5-19.1-42.5-42.5v-720c0-23.4 19.1-42.5 42.5-42.5h600c23.4 0 42.5 19.1 42.5 42.5v720c0 23.4-19.1 42.5-42.5 42.5z m-600-780c-9.6 0-17.5 7.9-17.5 17.5v720c0 9.6 7.9 17.5 17.5 17.5h600c9.6 0 17.5-7.9 17.5-17.5v-720c0-9.6-7.9-17.5-17.5-17.5h-600z" fill="#40220F" p-id="5410"></path><path d="M260.9 367.7h-94c-1.7 0-3-1.3-3-3s1.3-3 3-3h94c1.7 0 3 1.3 3 3s-1.3 3-3 3z" fill="#40220F" p-id="5411"></path><path d="M260.9 380.2h-94c-8.5 0-15.5-7-15.5-15.5s7-15.5 15.5-15.5h94c8.5 0 15.5 7 15.5 15.5s-7 15.5-15.5 15.5zM260.9 572h-94c-1.7 0-3-1.3-3-3s1.3-3 3-3h94c1.7 0 3 1.3 3 3 0 1.6-1.3 3-3 3z" fill="#40220F" p-id="5412"></path><path d="M260.9 584.5h-94c-8.5 0-15.5-7-15.5-15.5s7-15.5 15.5-15.5h94c8.5 0 15.5 7 15.5 15.5s-7 15.5-15.5 15.5z" fill="#40220F" p-id="5413"></path><path d="M260.9 766h-94c-1.7 0-3-1.3-3-3s1.3-3 3-3h94c1.7 0 3 1.3 3 3 0 1.6-1.3 3-3 3z" fill="#40220F" p-id="5414"></path><path d="M260.9 778.5h-94c-8.5 0-15.5-7-15.5-15.5s7-15.5 15.5-15.5h94c8.5 0 15.5 7 15.5 15.5s-7 15.5-15.5 15.5z" fill="#40220F" p-id="5415"></path><path d="M844 927.6H547.4V178.8H844c8.3 0 15 6.7 15 15v718.8c0 8.3-6.7 15-15 15z" fill="#DFE2FE" p-id="5416"></path><path d="M676.6 517.8l-258.4-1c-0.8 0-1.5-0.7-1.5-1.5v-2.2c0-0.8 0.7-1.5 1.5-1.5l258.4 1c0.8 0 1.5 0.7 1.5 1.5v2.2c0 0.8-0.7 1.5-1.5 1.5z" fill="#FFFFFF" p-id="5417"></path><path d="M676.6 530.3s-0.1 0 0 0l-258.4-1c-3.7 0-7.2-1.5-9.9-4.1s-4.1-6.2-4.1-9.9v-2.2c0-3.7 1.5-7.2 4.1-9.9 2.7-2.6 6.2-4.1 9.9-4.1l258.4 1c7.7 0 14 6.3 13.9 14v2.2c0 3.7-1.5 7.3-4.1 9.9-2.6 2.6-6.1 4.1-9.8 4.1z m0-25z" fill="#40220F" p-id="5418"></path><path d="M550.1 514.7l-1 258.4c0 0.8-0.7 1.5-1.5 1.5h-2.2c-0.8 0-1.5-0.7-1.5-1.5l1-258.4c0-0.8 0.7-1.5 1.5-1.5h2.2c0.8 0 1.5 0.7 1.5 1.5z" fill="#FFFFFF" p-id="5419"></path><path d="M547.6 787.1h-2.3c-7.7 0-14-6.3-14-14l1-258.4c0-3.7 1.5-7.3 4.1-9.9 2.7-2.6 6.2-4 9.9-4.1h2.2c7.7 0 14 6.3 14 14l-1 258.4c0.1 7.7-6.2 14-13.9 14z" fill="#40220F" p-id="5420"></path><path d="M676.2 622.3l-258.4-1c-0.8 0-1.5-0.7-1.5-1.5v-2.2c0-0.8 0.7-1.5 1.5-1.5l258.4 1c0.8 0 1.5 0.7 1.5 1.5v2.2c0 0.8-0.7 1.5-1.5 1.5z" fill="#FFFFFF" p-id="5421"></path><path d="M676.2 634.8l-258.4-1c-3.7 0-7.3-1.5-9.9-4.1-2.6-2.7-4.1-6.2-4.1-9.9v-2.2c0-7.7 6.3-13.9 14-13.9l258.4 1c3.7 0 7.3 1.5 9.9 4.1s4.1 6.2 4.1 9.9v2.2c0 3.7-1.5 7.3-4.1 9.9-2.7 2.6-6.2 4-9.9 4z" fill="#40220F" p-id="5422"></path><path d="M676.2 398.1L554.7 518.8c-0.6 0.6-1.5 0.6-2.1 0l-1.6-1.6c-0.6-0.6-0.6-1.5 0-2.1l121.5-120.6c0.6-0.6 1.5-0.6 2.1 0l1.6 1.6c0.6 0.5 0.6 1.4 0 2z" fill="#FFFFFF" p-id="5423"></path><path d="M553.7 531.7c-3.6 0-7.2-1.4-9.9-4.1l-1.6-1.6c-5.4-5.5-5.4-14.4 0.1-19.8l121.5-120.6c5.5-5.4 14.4-5.4 19.8 0.1l1.6 1.6c2.6 2.6 4.1 6.2 4.1 9.9s-1.5 7.2-4.1 9.9L563.5 527.6c-2.7 2.7-6.3 4.1-9.8 4.1z m113.7-142.4z" fill="#40220F" p-id="5424"></path><path d="M419.5 397.2l120.6 121.5c0.6 0.6 1.5 0.6 2.1 0l1.6-1.6c0.6-0.6 0.6-1.5 0-2.1L423.2 393.5c-0.6-0.6-1.5-0.6-2.1 0l-1.6 1.6c-0.5 0.5-0.5 1.5 0 2.1z" fill="#FFFFFF" p-id="5425"></path><path d="M541.2 531.6c-3.8 0-7.3-1.5-9.9-4.1L410.7 406c-5.4-5.5-5.4-14.4 0.1-19.8l1.6-1.6c2.6-2.6 6.1-4.1 9.9-4.1h0.1c3.7 0 7.2 1.5 9.9 4.1l120.6 121.5c5.4 5.5 5.4 14.4-0.1 19.8l-1.6 1.6c-2.7 2.7-6.2 4.1-10 4.1z" fill="#40220F" p-id="5426"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
72
web-vue/packages/biz/api/biz/itemInfo.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2013-Now http://jeesite.com All rights reserved.
|
||||||
|
* No deletion without permission, or be held responsible to law.
|
||||||
|
* @author gaoxq
|
||||||
|
*/
|
||||||
|
import { defHttp } from '@jeesite/core/utils/http/axios';
|
||||||
|
import { useGlobSetting } from '@jeesite/core/hooks/setting';
|
||||||
|
import { BasicModel, Page } from '@jeesite/core/api/model/baseModel';
|
||||||
|
import { UploadApiResult } from '@jeesite/core/api/sys/upload';
|
||||||
|
import { UploadFileParams } from '@jeesite/types/axios';
|
||||||
|
import { AxiosProgressEvent } from 'axios';
|
||||||
|
|
||||||
|
const { ctxPath, adminPath } = useGlobSetting();
|
||||||
|
|
||||||
|
export interface BizItemInfo extends BasicModel<BizItemInfo> {
|
||||||
|
createTime?: string; // 创建时间
|
||||||
|
itemName: string; // 项目名称
|
||||||
|
itemCode: string; // 项目编号
|
||||||
|
title?: string; // 备注描述
|
||||||
|
seq?: number; // 序号
|
||||||
|
ym: string; // 请求日期
|
||||||
|
cycle?: string; // 指标周期
|
||||||
|
reqParam?: string; // 请求参数
|
||||||
|
xAxis?: string; // x轴名称
|
||||||
|
index01?: string; // 指标01
|
||||||
|
index02?: string; // 指标02
|
||||||
|
index03?: string; // 指标03
|
||||||
|
index04?: string; // 指标04
|
||||||
|
index05?: string; // 指标05
|
||||||
|
index06?: string; // 指标06
|
||||||
|
index07?: string; // 指标07
|
||||||
|
index08?: string; // 指标08
|
||||||
|
index09?: string; // 指标09
|
||||||
|
index10?: string; // 指标10
|
||||||
|
index11?: string; // 指标11
|
||||||
|
index12?: string; // 指标12
|
||||||
|
index13?: string; // 指标13
|
||||||
|
index14?: string; // 指标14
|
||||||
|
index15?: string; // 指标15
|
||||||
|
updateTime?: string; // 更新时间
|
||||||
|
isDeleted?: string; // 删除标识(1-未删,0-已删)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bizItemInfoList = (params?: BizItemInfo | any) =>
|
||||||
|
defHttp.get<BizItemInfo>({ url: adminPath + '/biz/itemInfo/list', params });
|
||||||
|
|
||||||
|
export const bizItemInfoListAll = (params?: BizItemInfo | any) =>
|
||||||
|
defHttp.get<BizItemInfo>({ url: adminPath + '/biz/itemInfo/listAll', params });
|
||||||
|
|
||||||
|
export const bizItemInfoListData = (params?: BizItemInfo | any) =>
|
||||||
|
defHttp.post<Page<BizItemInfo>>({ url: adminPath + '/biz/itemInfo/listData', params });
|
||||||
|
|
||||||
|
export const bizItemInfoForm = (params?: BizItemInfo | any) =>
|
||||||
|
defHttp.get<BizItemInfo>({ url: adminPath + '/biz/itemInfo/form', params });
|
||||||
|
|
||||||
|
export const bizItemInfoSave = (params?: any, data?: BizItemInfo | any) =>
|
||||||
|
defHttp.postJson<BizItemInfo>({ url: adminPath + '/biz/itemInfo/save', params, data });
|
||||||
|
|
||||||
|
export const bizItemInfoImportData = (
|
||||||
|
params: UploadFileParams,
|
||||||
|
onUploadProgress: (progressEvent: AxiosProgressEvent) => void,
|
||||||
|
) =>
|
||||||
|
defHttp.uploadFile<UploadApiResult>(
|
||||||
|
{
|
||||||
|
url: ctxPath + adminPath + '/biz/itemInfo/importData',
|
||||||
|
onUploadProgress,
|
||||||
|
},
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const bizItemInfoDelete = (params?: BizItemInfo | any) =>
|
||||||
|
defHttp.get<BizItemInfo>({ url: adminPath + '/biz/itemInfo/delete', params });
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度分类支出结构" style="width: 100%; height: 100%;">
|
||||||
|
<div ref="chartDom" style="width: 100%; height: 100%;"></div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, watchEffect } from 'vue';
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
import { BizItemInfo, bizItemInfoListAll } from '@jeesite/biz/api/biz/itemInfo';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formParams: Record<string, any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rawData = ref<BizItemInfo[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let chartInstance: echarts.ECharts | null = null;
|
||||||
|
let resizeTimer: number | null = null;
|
||||||
|
|
||||||
|
const formatNumber = (num: string | undefined, decimal = 2): number => {
|
||||||
|
if (!num) return 0;
|
||||||
|
const parsed = Number(num);
|
||||||
|
return isNaN(parsed) ? 0 : Number(parsed.toFixed(decimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTenThousandYuan = (num: string | undefined): number => {
|
||||||
|
const rawNum = formatNumber(num);
|
||||||
|
return formatNumber((rawNum / 10000).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortByMonth = (data: BizItemInfo[]): BizItemInfo[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const monthA = a.xaxis ? parseInt(a.xaxis, 10) : 0;
|
||||||
|
const monthB = b.xaxis ? parseInt(b.xaxis, 10) : 0;
|
||||||
|
return monthA - monthB;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataList = async (params?: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
const reqParams = {
|
||||||
|
...(params || props.formParams),
|
||||||
|
itemCode: "ERP_CATEGORY_Y001",
|
||||||
|
};
|
||||||
|
const result = await bizItemInfoListAll(reqParams);
|
||||||
|
const validData = (result || []).filter(item => item.xaxis && item.index01);
|
||||||
|
rawData.value = sortByMonth(validData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据列表失败:', error);
|
||||||
|
rawData.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!chartDom.value) return;
|
||||||
|
|
||||||
|
if (!chartInstance) {
|
||||||
|
chartInstance = echarts.init(chartDom.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawData.value.length === 0) {
|
||||||
|
chartInstance.setOption({
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
top: '50%',
|
||||||
|
text: '暂无支出数据',
|
||||||
|
textStyle: { fontSize: 18, color: '#333' }
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'item' },
|
||||||
|
padding: [0, 0, 0, 0]
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pieData = rawData.value.map(item => ({
|
||||||
|
name: item.xaxis || '',
|
||||||
|
value: toTenThousandYuan(item.index01)
|
||||||
|
})).filter(item => item.value > 0);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
padding: [0, 0, 0, 0],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
textStyle: { fontSize: 12 },
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderColor: '#e8e8e8',
|
||||||
|
borderWidth: 1,
|
||||||
|
formatter: (params: any) => `
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">${params.name}</div>
|
||||||
|
<div style="color: #666;">支出:${params.value.toFixed(2)} 万元</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
type: 'scroll',
|
||||||
|
orient: 'horizontal',
|
||||||
|
top: 5,
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#333',
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
|
itemGap: 15,
|
||||||
|
itemWidth: 12,
|
||||||
|
itemHeight: 12,
|
||||||
|
selectedMode: true,
|
||||||
|
scrollDataIndex: 0,
|
||||||
|
pageButtonPosition: 'end',
|
||||||
|
pageIconColor: '#1890ff',
|
||||||
|
pageIconInactiveColor: '#ccc',
|
||||||
|
pageTextStyle: { fontSize: 9 },
|
||||||
|
height: 30,
|
||||||
|
formatter: (name: string) => {
|
||||||
|
return name.length > 6 ? `${name.substring(0, 6)}...` : name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '支出',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['30%', '60%'],
|
||||||
|
center: ['50%', '45%'],
|
||||||
|
data: pieData,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'outside',
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const shortName = params.name.length > 4 ? `${params.name.substring(0, 4)}...` : params.name;
|
||||||
|
return `${shortName}: ${params.value}万元`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
chartInstance.setOption(option, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeChart = () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.resize({
|
||||||
|
animation: {
|
||||||
|
duration: 300,
|
||||||
|
easing: 'quadraticInOut'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounceResize = () => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.formParams,
|
||||||
|
(newParams) => {
|
||||||
|
if (Object.keys(newParams).length) {
|
||||||
|
fetchDataList(newParams);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (rawData.value || chartDom.value) {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDataList(props.formParams);
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
chartInstance = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:not(.active):hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend) {
|
||||||
|
display: block !important;
|
||||||
|
z-index: 999 !important;
|
||||||
|
padding: 0 0 0 5px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-scroll) {
|
||||||
|
overflow: hidden !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
height: 30px !important;
|
||||||
|
line-height: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-item) {
|
||||||
|
display: inline-block !important;
|
||||||
|
margin-right: 15px !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip) {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #e8e8e8 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度分类收入分布" style="width: 100%; height: 100%;">
|
||||||
|
<div ref="chartDom" style="width: 100%; height: 100%;"></div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, watchEffect } from 'vue';
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
import { BizItemInfo, bizItemInfoListAll } from '@jeesite/biz/api/biz/itemInfo';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formParams: Record<string, any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rawData = ref<BizItemInfo[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let chartInstance: echarts.ECharts | null = null;
|
||||||
|
let resizeTimer: number | null = null;
|
||||||
|
|
||||||
|
const formatNumber = (num: string | undefined, decimal = 2): number => {
|
||||||
|
if (!num) return 0;
|
||||||
|
const parsed = Number(num);
|
||||||
|
return isNaN(parsed) ? 0 : Number(parsed.toFixed(decimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTenThousandYuan = (num: string | undefined): number => {
|
||||||
|
const rawNum = formatNumber(num);
|
||||||
|
return formatNumber((rawNum / 10000).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortByMonth = (data: BizItemInfo[]): BizItemInfo[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const monthA = a.xaxis ? parseInt(a.xaxis, 10) : 0;
|
||||||
|
const monthB = b.xaxis ? parseInt(b.xaxis, 10) : 0;
|
||||||
|
return monthA - monthB;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataList = async (params?: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
const reqParams = {
|
||||||
|
...(params || props.formParams),
|
||||||
|
itemCode: "ERP_CATEGORY_Y002",
|
||||||
|
};
|
||||||
|
const result = await bizItemInfoListAll(reqParams);
|
||||||
|
const validData = (result || []).filter(item => item.xaxis && item.index01);
|
||||||
|
rawData.value = sortByMonth(validData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据列表失败:', error);
|
||||||
|
rawData.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!chartDom.value) return;
|
||||||
|
|
||||||
|
if (!chartInstance) {
|
||||||
|
chartInstance = echarts.init(chartDom.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawData.value.length === 0) {
|
||||||
|
chartInstance.setOption({
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
top: '50%',
|
||||||
|
text: '暂无收入数据',
|
||||||
|
textStyle: { fontSize: 18, color: '#333' }
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'item' },
|
||||||
|
padding: [0, 0, 0, 0]
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pieData = rawData.value.map(item => ({
|
||||||
|
name: item.xaxis || '',
|
||||||
|
value: toTenThousandYuan(item.index01)
|
||||||
|
})).filter(item => item.value > 0);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
padding: [0, 0, 0, 0],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
textStyle: { fontSize: 12 },
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderColor: '#e8e8e8',
|
||||||
|
borderWidth: 1,
|
||||||
|
formatter: (params: any) => `
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 4px;">${params.name}</div>
|
||||||
|
<div style="color: #666;">收入:${params.value.toFixed(2)} 万元</div>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
type: 'scroll',
|
||||||
|
orient: 'horizontal',
|
||||||
|
top: 5,
|
||||||
|
left: 'center',
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#333',
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
|
itemGap: 15,
|
||||||
|
itemWidth: 12,
|
||||||
|
itemHeight: 12,
|
||||||
|
selectedMode: true,
|
||||||
|
scrollDataIndex: 0,
|
||||||
|
pageButtonPosition: 'end',
|
||||||
|
pageIconColor: '#1890ff',
|
||||||
|
pageIconInactiveColor: '#ccc',
|
||||||
|
pageTextStyle: { fontSize: 9 },
|
||||||
|
height: 30,
|
||||||
|
formatter: (name: string) => {
|
||||||
|
return name.length > 6 ? `${name.substring(0, 6)}...` : name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '收入',
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['30%', '60%'],
|
||||||
|
center: ['50%', '45%'],
|
||||||
|
data: pieData,
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'outside',
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const shortName = params.name.length > 4 ? `${params.name.substring(0, 4)}...` : params.name;
|
||||||
|
return `${shortName}: ${params.value}万元`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 2
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
shadowBlur: 10,
|
||||||
|
shadowOffsetX: 0,
|
||||||
|
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
chartInstance.setOption(option, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeChart = () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.resize({
|
||||||
|
animation: {
|
||||||
|
duration: 300,
|
||||||
|
easing: 'quadraticInOut'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounceResize = () => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.formParams,
|
||||||
|
(newParams) => {
|
||||||
|
if (Object.keys(newParams).length) {
|
||||||
|
fetchDataList(newParams);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (rawData.value || chartDom.value) {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDataList(props.formParams);
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
chartInstance = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:not(.active):hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend) {
|
||||||
|
display: block !important;
|
||||||
|
z-index: 999 !important;
|
||||||
|
padding: 0 0 0 5px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-scroll) {
|
||||||
|
overflow: hidden !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
height: 30px !important;
|
||||||
|
line-height: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-item) {
|
||||||
|
display: inline-block !important;
|
||||||
|
margin-right: 15px !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip) {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #e8e8e8 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
459
web-vue/packages/erp/views/erp/green/components/ChartMom.vue
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度各月环比情况" style="width: 100%; height: 100%;">
|
||||||
|
<div ref="chartDom" style="width: 100%; height: 100%;"></div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, watchEffect } from 'vue';
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
|
||||||
|
import { BizItemInfo, bizItemInfoListAll } from '@jeesite/biz/api/biz/itemInfo';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formParams: Record<string, any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rawData = ref<BizItemInfo[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let myChart: echarts.ECharts | null = null;
|
||||||
|
let resizeTimer: number | null = null;
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
const formatNumber = (num: string | undefined, decimal = 2): number => {
|
||||||
|
if (!num) return 0;
|
||||||
|
const parsed = Number(num);
|
||||||
|
return isNaN(parsed) ? 0 : Number(parsed.toFixed(decimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTenThousandYuan = (num: string | number | undefined): number => {
|
||||||
|
const rawNum = formatNumber(num.toString());
|
||||||
|
return formatNumber((rawNum / 10000).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatWithThousandsSeparator = (num: string | number | undefined): string => {
|
||||||
|
const parsed = Number(num);
|
||||||
|
if (isNaN(parsed)) return '0.00';
|
||||||
|
return parsed.toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMonth = (xaxis: string | undefined): string => {
|
||||||
|
if (!xaxis) return '';
|
||||||
|
const monthNum = parseInt(xaxis, 10);
|
||||||
|
return isNaN(monthNum) ? xaxis : `${monthNum}月`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateMonthOnMonth = (current: string | undefined, last: string | undefined): number => {
|
||||||
|
const currentNum = formatNumber(current);
|
||||||
|
const lastNum = formatNumber(last);
|
||||||
|
if (lastNum === 0) return 0;
|
||||||
|
const mom = ((currentNum - lastNum) / lastNum) * 100;
|
||||||
|
return formatNumber(mom.toString(), 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateNetProfitForChart = (income: string | undefined, expense: string | undefined): number => {
|
||||||
|
const profitInYuan = formatNumber((formatNumber(income) - formatNumber(expense)).toString());
|
||||||
|
return toTenThousandYuan(profitInYuan);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortByMonth = (data: BizItemInfo[]): BizItemInfo[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const monthA = a.xaxis ? parseInt(a.xaxis, 10) : 0;
|
||||||
|
const monthB = b.xaxis ? parseInt(b.xaxis, 10) : 0;
|
||||||
|
return monthA - monthB;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateYAxisExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [0, 10];
|
||||||
|
const min = Math.min(...data);
|
||||||
|
const max = Math.max(...data);
|
||||||
|
const padding = Math.max((max - min) * 0.1, 1);
|
||||||
|
let minExtent = Math.min(min - padding, 0);
|
||||||
|
let maxExtent = max + padding;
|
||||||
|
return [minExtent, maxExtent];
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculatePercentExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [-20, 100];
|
||||||
|
const min = Math.min(...data);
|
||||||
|
const max = Math.max(...data);
|
||||||
|
const padding = Math.max((max - min) * 0.15, 10);
|
||||||
|
return [min - padding, max + padding];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDataList = async (params?: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
const reqParams = {
|
||||||
|
...(params || props.formParams),
|
||||||
|
itemCode: "ERP_YEARDATA_M001",
|
||||||
|
};
|
||||||
|
const result = await bizItemInfoListAll(reqParams);
|
||||||
|
const validData = (result || []).filter(item => item.xaxis);
|
||||||
|
rawData.value = sortByMonth(validData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据失败:', error);
|
||||||
|
createMessage.error('获取月度环比数据失败,请稍后重试');
|
||||||
|
rawData.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!chartDom.value) return;
|
||||||
|
|
||||||
|
if (!myChart) {
|
||||||
|
myChart = echarts.init(chartDom.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawData.value.length === 0) {
|
||||||
|
myChart.setOption({
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
legend: { data: ['本月收入','上月收入','本月支出','上月支出','收入环比','支出环比','净收益'], top: 10 },
|
||||||
|
grid: { left:10, right:40, bottom:60, top:40, containLabel:true },
|
||||||
|
xAxis: { type: 'category', data: [] },
|
||||||
|
yAxis: [
|
||||||
|
{ type: 'value', name: '万元' },
|
||||||
|
{ type: 'value', name: '%', position: 'right' }
|
||||||
|
],
|
||||||
|
series: [],
|
||||||
|
noDataLoadingOption: {
|
||||||
|
text: '暂无月度环比数据',
|
||||||
|
textStyle: { fontSize: 16, color: '#666', fontWeight: '500' },
|
||||||
|
position: 'center'
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xAxisData = rawData.value.map(item => formatMonth(item.xaxis));
|
||||||
|
const incomeData = rawData.value.map(item => toTenThousandYuan(item.index01));
|
||||||
|
const lastIncomeData = rawData.value.map(item => toTenThousandYuan(item.index04));
|
||||||
|
const expenseData = rawData.value.map(item => toTenThousandYuan(item.index02));
|
||||||
|
const lastExpenseData = rawData.value.map(item => toTenThousandYuan(item.index05));
|
||||||
|
const incomeMomData = rawData.value.map(item => calculateMonthOnMonth(item.index01, item.index04));
|
||||||
|
const expenseMomData = rawData.value.map(item => calculateMonthOnMonth(item.index02, item.index05));
|
||||||
|
const netProfitData = rawData.value.map(item => calculateNetProfitForChart(item.index01, item.index02));
|
||||||
|
|
||||||
|
const amountData = [...incomeData, ...lastIncomeData, ...expenseData, ...lastExpenseData, ...netProfitData];
|
||||||
|
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||||
|
const [pMin, pMax] = calculatePercentExtent([...incomeMomData, ...expenseMomData]);
|
||||||
|
|
||||||
|
const profitBarLabelConfig = {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
textStyle: { fontSize: 9, fontWeight: 'bold' },
|
||||||
|
formatter: (v: any) => formatWithThousandsSeparator(v.value)
|
||||||
|
};
|
||||||
|
|
||||||
|
const momLineStyle = {
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 5,
|
||||||
|
lineStyle: { width: 1.8 },
|
||||||
|
emphasis: {
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: { width: 2.2 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderColor: '#eee',
|
||||||
|
borderWidth: 1,
|
||||||
|
textStyle: { fontSize: 12 },
|
||||||
|
formatter: (params: any[]) => {
|
||||||
|
const month = params[0]?.axisValue;
|
||||||
|
const item = rawData.value.find(i => formatMonth(i.xaxis) === month);
|
||||||
|
if (!item) return '无数据';
|
||||||
|
|
||||||
|
const incomeMom = calculateMonthOnMonth(item.index01, item.index04);
|
||||||
|
const expenseMom = calculateMonthOnMonth(item.index02, item.index05);
|
||||||
|
const netProfitInYuan = formatNumber(item.index01) - formatNumber(item.index02);
|
||||||
|
|
||||||
|
let netProfitText = '';
|
||||||
|
let netProfitColor = '';
|
||||||
|
if (netProfitInYuan > 0) {
|
||||||
|
netProfitText = `<span style="font-weight:bold;">↑ ${formatWithThousandsSeparator(netProfitInYuan)}</span>`;
|
||||||
|
netProfitColor = '#52c41a';
|
||||||
|
} else if (netProfitInYuan < 0) {
|
||||||
|
netProfitText = `<span style="font-weight:bold;">↓ ${formatWithThousandsSeparator(Math.abs(netProfitInYuan))}</span>`;
|
||||||
|
netProfitColor = '#f5222d';
|
||||||
|
} else {
|
||||||
|
netProfitText = '<span style="font-weight:bold;">— 收支平衡</span>';
|
||||||
|
netProfitColor = '#666';
|
||||||
|
}
|
||||||
|
|
||||||
|
const incomeMomText = incomeMom >= 0
|
||||||
|
? `<span style="color:#f5222d; font-weight:bold;">↑ ${incomeMom.toFixed(2)}</span>`
|
||||||
|
: `<span style="color:#1890ff; font-weight:bold;">↓ ${Math.abs(incomeMom).toFixed(2)}</span>`;
|
||||||
|
|
||||||
|
const expenseMomText = expenseMom >= 0
|
||||||
|
? `<span style="color:#f5222d; font-weight:bold;">↑ ${expenseMom.toFixed(2)}</span>`
|
||||||
|
: `<span style="color:#1890ff; font-weight:bold;">↓ ${Math.abs(expenseMom).toFixed(2)}</span>`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="text-align:center; font-weight:600; margin-bottom:8px;">${month}</div>
|
||||||
|
<table style="width:100%; border-collapse:collapse; font-size:12px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#f7f8fa;">
|
||||||
|
<th style="border:1px solid #eee; padding:6px; text-align:center;">本月收入(元)</th>
|
||||||
|
<th style="border:1px solid #eee; padding:6px; text-align:center;">上月收入(元)</th>
|
||||||
|
<th style="border:1px solid #eee; padding:6px; text-align:center;">收入环比(%)</th>
|
||||||
|
<th style="border:1px solid #eee; padding:6px; text-align:center;">本月支出(元)</th>
|
||||||
|
<th style="border:1px solid #eee; padding:6px; text-align:center;">上月支出(元)</th>
|
||||||
|
<th style="border:1px solid #eee; padding:6px; text-align:center;">支出环比(%)</th>
|
||||||
|
<th style="border:1px solid #eee; padding:6px; text-align:center;">净收益(元)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border:1px solid #eee; padding:6px; color:#1890ff; text-align:right;">${formatWithThousandsSeparator(item.index01)}</td>
|
||||||
|
<td style="border:1px solid #eee; padding:6px; color:#ff7875; text-align:right;">${formatWithThousandsSeparator(item.index04)}</td>
|
||||||
|
<td style="border:1px solid #eee; padding:6px; text-align:center;">${incomeMomText}</td>
|
||||||
|
<td style="border:1px solid #eee; padding:6px; color:#f5222d; text-align:right;">${formatWithThousandsSeparator(item.index02)}</td>
|
||||||
|
<td style="border:1px solid #eee; padding:6px; color:#ffc53d; text-align:right;">${formatWithThousandsSeparator(item.index05)}</td>
|
||||||
|
<td style="border:1px solid #eee; padding:6px; text-align:center;">${expenseMomText}</td>
|
||||||
|
<td style="border:1px solid #eee; padding:6px; color:${netProfitColor}; text-align:center;">${netProfitText}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['本月收入', '上月收入', '本月支出', '上月支出', '收入环比', '支出环比', '净收益'],
|
||||||
|
top: 10, left: 'center', textStyle: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
grid: { left: 10, right: 40, bottom: 60, top: 40, containLabel: true },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xAxisData,
|
||||||
|
axisLabel: { fontSize: 10, rotate: 60 }
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '万元',
|
||||||
|
min: amountMin, max: amountMax,
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (v: any) => formatWithThousandsSeparator(v),
|
||||||
|
fontSize: 10
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { type: 'dashed', opacity: 0.3 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value', name: '%', position: 'right',
|
||||||
|
min: pMin, max: pMax,
|
||||||
|
axisLabel: { formatter: (v: any) => v.toFixed(1) + '%' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '上月收入',
|
||||||
|
type: 'line',
|
||||||
|
data: lastIncomeData,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 4,
|
||||||
|
lineStyle: { width: 1.5, color: '#ff7875' },
|
||||||
|
itemStyle: { color: '#ff7875' },
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: 'rgba(255,120,117,0.25)' },
|
||||||
|
{ offset: 1, color: 'rgba(255,120,117,0.02)' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
label: { show: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '本月收入',
|
||||||
|
type: 'line',
|
||||||
|
data: incomeData,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1.5,
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#1890ff' },
|
||||||
|
{ offset: 1, color: '#096dd9' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
itemStyle: { color: '#1890ff' },
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: 'rgba(24,144,255,0.25)' },
|
||||||
|
{ offset: 1, color: 'rgba(24,144,255,0.02)' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
label: { show: false },
|
||||||
|
emphasis: { symbolSize: 8, lineStyle: { width: 2 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '上月支出',
|
||||||
|
type: 'line',
|
||||||
|
data: lastExpenseData,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 4,
|
||||||
|
lineStyle: { width: 1.5, color: '#ffc53d' },
|
||||||
|
itemStyle: { color: '#ffc53d' },
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: 'rgba(255,197,61,0.2)' },
|
||||||
|
{ offset: 1, color: 'rgba(255,197,61,0.02)' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
label: { show: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '本月支出',
|
||||||
|
type: 'line',
|
||||||
|
data: expenseData,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
smooth: true,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1.5,
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#f5222d' },
|
||||||
|
{ offset: 1, color: '#cf1322' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
itemStyle: { color: '#f5222d' },
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: 'rgba(245,34,45,0.25)' },
|
||||||
|
{ offset: 1, color: 'rgba(245,34,45,0.02)' }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
label: { show: false },
|
||||||
|
emphasis: { symbolSize: 8, lineStyle: { width: 2 } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '收入环比',
|
||||||
|
type: 'line',
|
||||||
|
data: incomeMomData,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
...momLineStyle,
|
||||||
|
lineStyle: { ...momLineStyle.lineStyle, color: '#13c2c2' },
|
||||||
|
itemStyle: { color: '#13c2c2' },
|
||||||
|
label: { show: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '支出环比',
|
||||||
|
type: 'line',
|
||||||
|
data: expenseMomData,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
...momLineStyle,
|
||||||
|
lineStyle: { ...momLineStyle.lineStyle, color: '#722ed1' },
|
||||||
|
itemStyle: { color: '#722ed1' },
|
||||||
|
label: { show: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '净收益',
|
||||||
|
type: 'bar',
|
||||||
|
data: netProfitData,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
barWidth: 10,
|
||||||
|
barGap: '10%',
|
||||||
|
barCategoryGap: '20%',
|
||||||
|
itemStyle: {
|
||||||
|
color: (params: any) => params.data >= 0 ? '#52c41a' : '#f5222d',
|
||||||
|
borderRadius: [4, 4, 0, 0],
|
||||||
|
opacity: 0.85
|
||||||
|
},
|
||||||
|
label: profitBarLabelConfig,
|
||||||
|
emphasis: { itemStyle: { opacity: 1 } }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
animationDuration: 500,
|
||||||
|
animationEasingUpdate: 'quinticInOut'
|
||||||
|
};
|
||||||
|
|
||||||
|
myChart.setOption(option, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeChart = () => {
|
||||||
|
if (myChart) {
|
||||||
|
myChart.resize({
|
||||||
|
animation: { duration: 300, easing: 'quadraticInOut' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounceResize = () => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.formParams,
|
||||||
|
(newParams) => {
|
||||||
|
if (Object.keys(newParams).length) {
|
||||||
|
fetchDataList(newParams);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (rawData.value || chartDom.value) {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDataList(props.formParams);
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
if (myChart) {
|
||||||
|
myChart.dispose();
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
:deep(.ant-card:hover) {
|
||||||
|
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
:deep(.echarts-tooltip) {
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0,0,0,0.15);
|
||||||
|
border: 1px solid #e8e8e8 !important;
|
||||||
|
}
|
||||||
|
:deep(.echarts-tooltip table) {
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
:deep(.echarts-tooltip small) {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
611
web-vue/packages/erp/views/erp/green/components/ChartQuarter.vue
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度各季收支趋势" style="width: 100%; height: 100%;">
|
||||||
|
<div ref="chartDom" style="width: 100%; height: 100%;"></div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, watchEffect } from 'vue';
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
|
||||||
|
import { BizItemInfo, bizItemInfoListAll } from '@jeesite/biz/api/biz/itemInfo';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formParams: Record<string, any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rawData = ref<BizItemInfo[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let myChart: echarts.ECharts | null = null;
|
||||||
|
let resizeTimer: number | null = null; // 修复定时器类型
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
// 格式化数字(保留指定位数小数)
|
||||||
|
const formatNumber = (num: string | undefined, decimal = 2): number => {
|
||||||
|
if (!num) return 0;
|
||||||
|
const parsed = Number(num);
|
||||||
|
if (isNaN(parsed)) return 0;
|
||||||
|
return Number(parsed.toFixed(decimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 转换为万元单位
|
||||||
|
const toTenThousandYuan = (num: string | undefined): number => {
|
||||||
|
const rawNum = formatNumber(num);
|
||||||
|
return formatNumber((rawNum / 10000).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 千分位格式化数字
|
||||||
|
const formatWithThousandsSeparator = (num: string | number | undefined): string => {
|
||||||
|
const parsed = Number(num);
|
||||||
|
if (isNaN(parsed)) return '0.00';
|
||||||
|
return parsed.toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化百分比
|
||||||
|
const formatPercentage = (num: string | undefined, decimal = 2): string => {
|
||||||
|
const parsed = formatNumber(num, decimal);
|
||||||
|
return `${parsed.toFixed(decimal)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算净收益
|
||||||
|
const calculateNetProfit = (income: string | undefined, expense: string | undefined): number => {
|
||||||
|
const incomeNum = formatNumber(income);
|
||||||
|
const expenseNum = formatNumber(expense);
|
||||||
|
return formatNumber((incomeNum - expenseNum).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按月份排序
|
||||||
|
const sortByMonth = (data: BizItemInfo[]): BizItemInfo[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const monthA = a.xaxis ? parseInt(a.xaxis, 10) : 0;
|
||||||
|
const monthB = b.xaxis ? parseInt(b.xaxis, 10) : 0;
|
||||||
|
return monthA - monthB;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算金额轴的范围
|
||||||
|
const calculateYAxisExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [0, 10];
|
||||||
|
|
||||||
|
const min = Math.min(...data);
|
||||||
|
const max = Math.max(...data);
|
||||||
|
|
||||||
|
const padding = Math.max((max - min) * 0.1, 1);
|
||||||
|
|
||||||
|
let minExtent = min - padding;
|
||||||
|
let maxExtent = max + padding;
|
||||||
|
|
||||||
|
minExtent = Math.max(minExtent, 0);
|
||||||
|
maxExtent = Math.max(maxExtent, 0);
|
||||||
|
|
||||||
|
return [minExtent, maxExtent];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算百分比轴的范围
|
||||||
|
const calculatePercentYAxisExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [0, 100];
|
||||||
|
|
||||||
|
const min = Math.min(...data);
|
||||||
|
const max = Math.max(...data);
|
||||||
|
|
||||||
|
const actualMin = Math.min(Math.max(min, 0), 100);
|
||||||
|
const actualMax = Math.max(max, 0);
|
||||||
|
|
||||||
|
const padding = Math.max((actualMax - actualMin) * 0.1, 5);
|
||||||
|
const maxExtent = Math.ceil(actualMax + padding);
|
||||||
|
|
||||||
|
return [0, maxExtent > 0 ? maxExtent : 10];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取数据列表
|
||||||
|
const fetchDataList = async (params?: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
const reqParams = {
|
||||||
|
...(params || props.formParams),
|
||||||
|
itemCode: "ERP_YEARQUAR_Y001",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await bizItemInfoListAll(reqParams);
|
||||||
|
const validData = (result || [])
|
||||||
|
.filter(item => item.xaxis)
|
||||||
|
.filter(item => item.index01 || item.index02);
|
||||||
|
rawData.value = sortByMonth(validData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据列表失败:', error);
|
||||||
|
createMessage.error('获取收支趋势数据失败,请稍后重试'); // 增加友好提示
|
||||||
|
rawData.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化/更新图表(核心优化)
|
||||||
|
const initChart = () => {
|
||||||
|
// 确保DOM元素存在
|
||||||
|
if (!chartDom.value) return;
|
||||||
|
|
||||||
|
// 懒初始化图表实例,避免重复销毁创建
|
||||||
|
if (!myChart) {
|
||||||
|
myChart = echarts.init(chartDom.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无数据时的配置
|
||||||
|
if (rawData.value.length === 0) {
|
||||||
|
myChart.setOption({
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 18, color: '#333' }
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
|
legend: { data: ['收入', '支出', '支出占比'], top: 10, textStyle: { fontSize: 12 } },
|
||||||
|
grid: {
|
||||||
|
left: 10,
|
||||||
|
right: 40,
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: [],
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => value.toFixed(2)
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '百分比(%)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => `${value.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
splitLine: { show: false },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: false,
|
||||||
|
position: 'right',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [],
|
||||||
|
noDataLoadingOption: {
|
||||||
|
text: '暂无收入支出数据',
|
||||||
|
textStyle: { fontSize: 16, color: '#666', fontWeight: '500' },
|
||||||
|
position: 'center',
|
||||||
|
effect: 'none'
|
||||||
|
}
|
||||||
|
}, true); // 强制更新配置
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图表数据
|
||||||
|
const xAxisData = rawData.value.map(item => item.xaxis);
|
||||||
|
const incomeData = rawData.value.map(item => toTenThousandYuan(item.index01));
|
||||||
|
const expenseData = rawData.value.map(item => toTenThousandYuan(item.index02));
|
||||||
|
const proportionData = rawData.value.map(item => formatNumber(item.index03));
|
||||||
|
|
||||||
|
const amountData = [...incomeData, ...expenseData];
|
||||||
|
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||||
|
const [percentMin, percentMax] = calculatePercentYAxisExtent(proportionData);
|
||||||
|
|
||||||
|
// 标签配置
|
||||||
|
const barLabelConfig = {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
distance: 3,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: '500'
|
||||||
|
},
|
||||||
|
formatter: (params: any) => `${params.value.toFixed(2)}`
|
||||||
|
};
|
||||||
|
|
||||||
|
const lineLabelConfig = {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
distance: 5,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: '500',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||||
|
padding: [2, 4],
|
||||||
|
borderRadius: 2
|
||||||
|
},
|
||||||
|
formatter: (params: any) => `${params.value.toFixed(1)}%`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 图表主配置
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 18, color: '#333' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontSize: 12 },
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderColor: '#e8e8e8',
|
||||||
|
borderWidth: 1,
|
||||||
|
formatter: (params: any[]) => {
|
||||||
|
if (!params || params.length === 0) return '<div style="padding: 8px;">暂无数据</div>';
|
||||||
|
|
||||||
|
const currentQuarter = params[0]?.axisValue || '';
|
||||||
|
const item = rawData.value.find(i => i.xaxis === currentQuarter);
|
||||||
|
|
||||||
|
if (!item) return `<div style="padding: 8px;">${currentQuarter}:暂无明细</div>`;
|
||||||
|
|
||||||
|
const netProfit = calculateNetProfit(item.index01, item.index02);
|
||||||
|
const netProfitColor = netProfit > 0 ? '#52c41a' : netProfit < 0 ? '#f5222d' : '#666';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 8px; text-align: center;">${currentQuarter}</div>
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 11px; min-width: 400px; margin-bottom: 8px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f8f8f8;">
|
||||||
|
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">总收入</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">总支出</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">净收益</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">支出占比</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: #1890ff;">${formatWithThousandsSeparator(item.index01)} 元</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: #f5222d;">${formatWithThousandsSeparator(item.index02)} 元</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: ${netProfitColor};">${formatWithThousandsSeparator(netProfit)} 元</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: #722ed1;">${formatPercentage(item.index03)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: ['收入', '支出', '支出占比'],
|
||||||
|
top: 10,
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 10,
|
||||||
|
right: 40,
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xAxisData,
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
margin: 8
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
},
|
||||||
|
boundaryGap: true
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => value.toFixed(2)
|
||||||
|
},
|
||||||
|
min: amountMin,
|
||||||
|
max: amountMax,
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '百分比(%)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => `${value.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
min: percentMin,
|
||||||
|
max: percentMax,
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
splitLine: { show: false },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: false,
|
||||||
|
position: 'right',
|
||||||
|
boundaryGap: [0, '10%']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
noDataLoadingOption: {
|
||||||
|
text: '暂无数据',
|
||||||
|
textStyle: { fontSize: 14, color: '#999' },
|
||||||
|
effect: 'bubble',
|
||||||
|
effectOption: { effect: { n: 0 } }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '收入',
|
||||||
|
type: 'bar',
|
||||||
|
data: incomeData,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#1890ff' },
|
||||||
|
{ offset: 1, color: '#096dd9' }
|
||||||
|
]),
|
||||||
|
borderRadius: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
barGap: '30%',
|
||||||
|
barCategoryGap: '20%',
|
||||||
|
label: barLabelConfig,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#40a9ff' },
|
||||||
|
{ offset: 1, color: '#1890ff' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '支出',
|
||||||
|
type: 'bar',
|
||||||
|
data: expenseData,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#f5222d' },
|
||||||
|
{ offset: 1, color: '#cf1322' }
|
||||||
|
]),
|
||||||
|
borderRadius: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
label: barLabelConfig,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#ff4d4f' },
|
||||||
|
{ offset: 1, color: '#f5222d' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '支出占比',
|
||||||
|
type: 'line',
|
||||||
|
data: proportionData,
|
||||||
|
yAxisIndex: 1,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: {
|
||||||
|
width: 1,
|
||||||
|
color: '#722ed1',
|
||||||
|
type: 'solid'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#722ed1',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
label: lineLabelConfig,
|
||||||
|
emphasis: {
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
animationDuration: 500,
|
||||||
|
animationEasingUpdate: 'quinticInOut'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关键:第二个参数设为true,强制更新配置
|
||||||
|
myChart.setOption(option, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 调整图表大小(防抖)
|
||||||
|
const resizeChart = () => {
|
||||||
|
if (myChart) {
|
||||||
|
myChart.resize({
|
||||||
|
animation: {
|
||||||
|
duration: 300,
|
||||||
|
easing: 'quadraticInOut'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounceResize = () => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听formParams变化,自动重新获取数据
|
||||||
|
watch(
|
||||||
|
() => props.formParams,
|
||||||
|
(newParams) => {
|
||||||
|
if (Object.keys(newParams).length) {
|
||||||
|
fetchDataList(newParams);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听数据/DOM变化,自动更新图表(更高效)
|
||||||
|
watchEffect(() => {
|
||||||
|
if (rawData.value || chartDom.value) {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生命周期管理
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始加载数据
|
||||||
|
fetchDataList(props.formParams);
|
||||||
|
// 监听窗口大小变化
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理定时器,防止内存泄漏
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
// 移除事件监听
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
// 销毁图表实例
|
||||||
|
if (myChart) {
|
||||||
|
myChart.dispose();
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:not(.active):hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card:hover) {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip) {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #e8e8e8 !important;
|
||||||
|
max-width: 450px;
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip table) {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-xaxis-label) {
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-yaxis-name) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-item) {
|
||||||
|
margin-right: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-label) {
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip::-webkit-scrollbar) {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip::-webkit-scrollbar-thumb) {
|
||||||
|
background: #d9d9d9;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip::-webkit-scrollbar-track) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
380
web-vue/packages/erp/views/erp/green/components/ChartRank.vue
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度支出排名及占比" style="width: 100%; height: 100%;">
|
||||||
|
<div ref="chartDom" style="width: 100%; height: calc(100% - 40px);"></div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, watchEffect } from 'vue';
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
import { BizItemInfo, bizItemInfoListAll } from '@jeesite/biz/api/biz/itemInfo';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formParams: Record<string, any>;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const rawData = ref<BizItemInfo[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let chartInstance: echarts.ECharts | null = null;
|
||||||
|
let resizeTimer: number | null = null;
|
||||||
|
|
||||||
|
// 格式化数字
|
||||||
|
const formatNumber = (num: string | undefined): number => {
|
||||||
|
if (!num) return 0;
|
||||||
|
const parsed = Number(num);
|
||||||
|
return isNaN(parsed) ? 0 : parsed;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化金额显示
|
||||||
|
const formatMoney = (num: number): string => {
|
||||||
|
return num.toLocaleString('zh-CN');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 转换为万元单位
|
||||||
|
const toTenThousandYuan = (num: number): number => {
|
||||||
|
return Number((num / 10000).toFixed(2));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 按序号排序
|
||||||
|
const sortBySeq = (data: BizItemInfo[]): BizItemInfo[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const seqA = a.seq ? parseInt(a.seq, 10) : 999;
|
||||||
|
const seqB = b.seq ? parseInt(b.seq, 10) : 999;
|
||||||
|
return seqA - seqB;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取数据列表
|
||||||
|
const fetchDataList = async (params?: Record<string, any>) => {
|
||||||
|
try {
|
||||||
|
const reqParams = {
|
||||||
|
...(params || props.formParams),
|
||||||
|
itemCode: "ERP_YEARRANK_Y001"
|
||||||
|
};
|
||||||
|
const result = await bizItemInfoListAll(reqParams);
|
||||||
|
const validData = (result || []).filter(item => item.xaxis && item.seq && item.index01);
|
||||||
|
const sortedData = sortBySeq(validData);
|
||||||
|
rawData.value = sortedData.slice(0, 10);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据列表失败:', error);
|
||||||
|
rawData.value = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取排名对应的渐变颜色
|
||||||
|
const getRankColorGradient = (rank: number) => {
|
||||||
|
if (rank === 1) {
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#f5222d' },
|
||||||
|
{ offset: 0.5, color: '#ff4d4f' },
|
||||||
|
{ offset: 1, color: '#ff7875' }
|
||||||
|
]);
|
||||||
|
} else if (rank === 2) {
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#fa8c16' },
|
||||||
|
{ offset: 0.5, color: '#ffa940' },
|
||||||
|
{ offset: 1, color: '#ffc069' }
|
||||||
|
]);
|
||||||
|
} else if (rank === 3) {
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#fadb14' },
|
||||||
|
{ offset: 0.5, color: '#ffec3d' },
|
||||||
|
{ offset: 1, color: '#fff566' }
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#1890ff' },
|
||||||
|
{ offset: 0.5, color: '#40a9ff' },
|
||||||
|
{ offset: 1, color: '#69c0ff' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取hover时的强调色
|
||||||
|
const getRankEmphasisColor = (rank: number) => {
|
||||||
|
if (rank === 1) {
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#cf1322' },
|
||||||
|
{ offset: 1, color: '#f5222d' }
|
||||||
|
]);
|
||||||
|
} else if (rank === 2) {
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#d46b08' },
|
||||||
|
{ offset: 1, color: '#fa8c16' }
|
||||||
|
]);
|
||||||
|
} else if (rank === 3) {
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#d4b106' },
|
||||||
|
{ offset: 1, color: '#fadb14' }
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||||||
|
{ offset: 0, color: '#096dd9' },
|
||||||
|
{ offset: 1, color: '#1890ff' }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化/更新图表
|
||||||
|
const initChart = () => {
|
||||||
|
// 确保DOM元素存在
|
||||||
|
if (!chartDom.value) return;
|
||||||
|
|
||||||
|
// 初始化图表实例(避免重复创建)
|
||||||
|
if (!chartInstance) {
|
||||||
|
chartInstance = echarts.init(chartDom.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无数据时显示提示
|
||||||
|
if (rawData.value.length === 0) {
|
||||||
|
chartInstance.setOption({
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
top: '50%',
|
||||||
|
text: '暂无排名数据',
|
||||||
|
textStyle: { fontSize: 18, color: '#333' }
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
padding: [2, 2, 2, 2]
|
||||||
|
}, true); // 第二个参数设为true,强制更新
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理图表数据
|
||||||
|
const categories = rawData.value.map(item => item.xaxis || '').reverse();
|
||||||
|
const rawAmountData = rawData.value.map(item => formatNumber(item.index01)).reverse();
|
||||||
|
const amountData = rawAmountData.map(num => toTenThousandYuan(num));
|
||||||
|
const ratioData = rawData.value.map(item => {
|
||||||
|
const ratio = formatNumber(item.index02);
|
||||||
|
return Number(ratio.toFixed(2));
|
||||||
|
}).reverse();
|
||||||
|
const rankData = rawData.value.map(item => parseInt(item.seq || '0', 10)).reverse();
|
||||||
|
|
||||||
|
// 图表配置项
|
||||||
|
const option = {
|
||||||
|
padding: [2, 2, 2, 2],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontSize: 12 },
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderColor: '#e8e8e8',
|
||||||
|
borderWidth: 1,
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const index = params[0].dataIndex;
|
||||||
|
return `
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 8px;">${categories[index]}</div>
|
||||||
|
<div style="color: #666; margin-bottom: 4px;">排名:${rankData[index]}名</div>
|
||||||
|
<div style="color: #666; margin-bottom: 4px;">支出:${formatMoney(rawAmountData[index])} 元</div>
|
||||||
|
<div style="color: #666;">占比:${ratioData[index]}%</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '2%',
|
||||||
|
right: '18%',
|
||||||
|
top: '2%',
|
||||||
|
bottom: '2%',
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '支出金额 (万元)',
|
||||||
|
nameTextStyle: { fontSize: 11, color: '#666' },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#666',
|
||||||
|
formatter: (value: number) => value.toFixed(1)
|
||||||
|
},
|
||||||
|
axisLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
|
splitLine: { lineStyle: { color: '#f5f5f5' } }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
data: categories,
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#333',
|
||||||
|
formatter: (name: string, index: number) => {
|
||||||
|
const shortName = name.length > 8 ? `${name.substring(0, 8)}...` : name;
|
||||||
|
return `${rankData[index]} ${shortName}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLine: { show: false },
|
||||||
|
axisTick: { show: false },
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: ['#fafafa', '#fff'],
|
||||||
|
type: 'solid'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '支出金额',
|
||||||
|
type: 'bar',
|
||||||
|
data: amountData,
|
||||||
|
barWidth: 12,
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: [0, 4, 4, 0],
|
||||||
|
color: (params: any) => getRankColorGradient(rankData[params.dataIndex])
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
position: 'right',
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#666',
|
||||||
|
formatter: (params: any) => `${params.data}万元 (${ratioData[params.dataIndex]}%)`
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: (params: any) => getRankEmphasisColor(rankData[params.dataIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置配置项,强制更新(关键:第二个参数为true)
|
||||||
|
chartInstance.setOption(option, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 防抖调整图表大小
|
||||||
|
const resizeChart = () => {
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.resize({
|
||||||
|
animation: {
|
||||||
|
duration: 300,
|
||||||
|
easing: 'quadraticInOut'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const debounceResize = () => {
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听formParams变化,重新获取数据
|
||||||
|
watch(
|
||||||
|
() => props.formParams,
|
||||||
|
(newParams) => {
|
||||||
|
if (Object.keys(newParams).length) {
|
||||||
|
fetchDataList(newParams);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听数据变化,更新图表(使用watchEffect更高效)
|
||||||
|
watchEffect(() => {
|
||||||
|
// 当rawData或chartDom变化时,重新初始化图表
|
||||||
|
if (rawData.value || chartDom.value) {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始加载数据
|
||||||
|
fetchDataList(props.formParams);
|
||||||
|
// 监听窗口大小变化
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理定时器
|
||||||
|
if (resizeTimer) clearTimeout(resizeTimer);
|
||||||
|
// 移除事件监听
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
// 销毁图表实例
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.dispose();
|
||||||
|
chartInstance = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:not(.active):hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 16px !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip) {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #e8e8e8 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-bar) {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-axis-line) {
|
||||||
|
stroke: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-text) {
|
||||||
|
fill: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
587
web-vue/packages/erp/views/erp/green/components/ChartYear.vue
Normal file
@@ -0,0 +1,587 @@
|
|||||||
|
<template>
|
||||||
|
<Card title="年度收支趋势及占比" style="width: 100%; height: 100%;">
|
||||||
|
<div ref="chartDom" style="width: 100%; height: 100%;"></div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||||
|
import { Card } from 'ant-design-vue';
|
||||||
|
import { useMessage } from '@jeesite/core/hooks/web/useMessage';
|
||||||
|
import { BizItemInfo, bizItemInfoListAll } from '@jeesite/biz/api/biz/itemInfo';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
const rawData = ref<BizItemInfo[]>([]);
|
||||||
|
const chartDom = ref<HTMLDivElement | null>(null);
|
||||||
|
let myChart: echarts.ECharts | null = null;
|
||||||
|
const { createMessage } = useMessage();
|
||||||
|
|
||||||
|
const formatNumber = (num: string | undefined, decimal = 2): number => {
|
||||||
|
if (!num) return 0;
|
||||||
|
const parsed = Number(num);
|
||||||
|
if (isNaN(parsed)) return 0;
|
||||||
|
return Number(parsed.toFixed(decimal));
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTenThousandYuan = (num: string | undefined): number => {
|
||||||
|
const rawNum = formatNumber(num);
|
||||||
|
return formatNumber((rawNum / 10000).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatWithThousandsSeparator = (num: string | number | undefined): string => {
|
||||||
|
const parsed = Number(num);
|
||||||
|
if (isNaN(parsed)) return '0.00';
|
||||||
|
return parsed.toLocaleString('zh-CN', {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:格式化百分比数据
|
||||||
|
const formatPercentage = (num: string | undefined, decimal = 2): string => {
|
||||||
|
const parsed = formatNumber(num, decimal);
|
||||||
|
return `${parsed.toFixed(decimal)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateNetProfit = (income: string | undefined, expense: string | undefined): number => {
|
||||||
|
const incomeNum = formatNumber(income);
|
||||||
|
const expenseNum = formatNumber(expense);
|
||||||
|
return formatNumber((incomeNum - expenseNum).toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatYear = (xaxis: string | undefined): string => {
|
||||||
|
if (!xaxis) return '';
|
||||||
|
const yearNum = parseInt(xaxis, 10);
|
||||||
|
return isNaN(yearNum) ? xaxis : `${yearNum}年`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortByMonth = (data: BizItemInfo[]): BizItemInfo[] => {
|
||||||
|
return data.sort((a, b) => {
|
||||||
|
const monthA = a.xaxis ? parseInt(a.xaxis, 10) : 0;
|
||||||
|
const monthB = b.xaxis ? parseInt(b.xaxis, 10) : 0;
|
||||||
|
return monthA - monthB;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateYAxisExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [0, 10];
|
||||||
|
|
||||||
|
const min = Math.min(...data);
|
||||||
|
const max = Math.max(...data);
|
||||||
|
|
||||||
|
const padding = Math.max((max - min) * 0.1, 1);
|
||||||
|
|
||||||
|
let minExtent = min - padding;
|
||||||
|
let maxExtent = max + padding;
|
||||||
|
|
||||||
|
if (minExtent > 0) minExtent = 0;
|
||||||
|
if (maxExtent < 0) maxExtent = 0;
|
||||||
|
|
||||||
|
return [minExtent, maxExtent];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:计算百分比Y轴范围
|
||||||
|
const calculatePercentYAxisExtent = (data: number[]) => {
|
||||||
|
if (data.length === 0) return [0, 100];
|
||||||
|
|
||||||
|
const min = Math.min(...data);
|
||||||
|
const max = Math.max(...data);
|
||||||
|
|
||||||
|
// 百分比数据的padding调整为5%
|
||||||
|
const padding = Math.max(Math.abs(max - min) * 0.1, 5);
|
||||||
|
|
||||||
|
let minExtent = Math.floor(min - padding);
|
||||||
|
let maxExtent = Math.ceil(max + padding);
|
||||||
|
|
||||||
|
// 确保最小值不小于0(百分比通常非负)
|
||||||
|
if (minExtent < 0) minExtent = 0;
|
||||||
|
|
||||||
|
return [minExtent, maxExtent];
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
itemCode: "ERP_YEARPMOM_M001"
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await bizItemInfoListAll(params);
|
||||||
|
const validData = (result || [])
|
||||||
|
.filter(item => item.xaxis)
|
||||||
|
.filter(item => item.index01 || item.index02);
|
||||||
|
rawData.value = sortByMonth(validData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据列表失败:', error);
|
||||||
|
rawData.value = [];
|
||||||
|
} finally {
|
||||||
|
initChart();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initChart = () => {
|
||||||
|
if (!chartDom.value) return;
|
||||||
|
|
||||||
|
if (myChart) {
|
||||||
|
myChart.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
myChart = echarts.init(chartDom.value);
|
||||||
|
|
||||||
|
if (rawData.value.length === 0) {
|
||||||
|
myChart.setOption({
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 18, color: '#333' }
|
||||||
|
},
|
||||||
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
|
// 移除环比,只保留收入、支出、占比
|
||||||
|
legend: { data: ['收入', '支出', '占比'], top: 10, textStyle: { fontSize: 12 } },
|
||||||
|
grid: {
|
||||||
|
left: 10,
|
||||||
|
right: 40, // 增加右侧边距给百分比Y轴
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: [],
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => value.toFixed(2)
|
||||||
|
},
|
||||||
|
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '百分比(%)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => `${value.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
splitLine: { show: false }, // 不显示百分比轴的分割线
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true,
|
||||||
|
position: 'right', // 百分比轴显示在右侧
|
||||||
|
max: 100 // 百分比最大100
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [],
|
||||||
|
noDataLoadingOption: {
|
||||||
|
text: '暂无收入支出数据',
|
||||||
|
textStyle: { fontSize: 16, color: '#666', fontWeight: '500' },
|
||||||
|
position: 'center',
|
||||||
|
effect: 'none'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const xAxisData = rawData.value.map(item => formatYear(item.xaxis));
|
||||||
|
const incomeData = rawData.value.map(item => toTenThousandYuan(item.index01));
|
||||||
|
const expenseData = rawData.value.map(item => toTenThousandYuan(item.index02));
|
||||||
|
// 移除环比数据,只保留占比数据
|
||||||
|
const proportionData = rawData.value.map(item => formatNumber(item.index04));
|
||||||
|
|
||||||
|
const amountData = [...incomeData, ...expenseData];
|
||||||
|
const [amountMin, amountMax] = calculateYAxisExtent(amountData);
|
||||||
|
// 只计算占比的百分比轴范围
|
||||||
|
const [percentMin, percentMax] = calculatePercentYAxisExtent(proportionData);
|
||||||
|
|
||||||
|
const barLabelConfig = {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
distance: 3,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#333',
|
||||||
|
fontWeight: '500'
|
||||||
|
},
|
||||||
|
formatter: (params: any) => `${params.value.toFixed(2)}`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增:折线图标签配置
|
||||||
|
const lineLabelConfig = {
|
||||||
|
show: true,
|
||||||
|
position: 'top',
|
||||||
|
distance: 5,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: '500',
|
||||||
|
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||||
|
padding: [2, 4],
|
||||||
|
borderRadius: 2
|
||||||
|
},
|
||||||
|
formatter: (params: any) => `${params.value.toFixed(1)}%`
|
||||||
|
};
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 18, color: '#333' }
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
textStyle: { fontSize: 12 },
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderColor: '#e8e8e8',
|
||||||
|
borderWidth: 1,
|
||||||
|
formatter: (params: any[]) => {
|
||||||
|
if (!params || params.length === 0) return '<div style="padding: 8px;">暂无数据</div>';
|
||||||
|
|
||||||
|
const currentMonth = params[0]?.axisValue || '';
|
||||||
|
const item = rawData.value.find(i => formatYear(i.xaxis) === currentMonth);
|
||||||
|
|
||||||
|
if (!item) return `<div style="padding: 8px;">${currentMonth}:暂无明细</div>`;
|
||||||
|
|
||||||
|
const netProfit = calculateNetProfit(item.index01, item.index02);
|
||||||
|
const netProfitColor = netProfit > 0 ? '#52c41a' : netProfit < 0 ? '#f5222d' : '#666';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="font-weight: 600; color: #333; margin-bottom: 8px; text-align: center;">${currentMonth}</div>
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 11px; min-width: 400px; margin-bottom: 8px;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background-color: #f8f8f8;">
|
||||||
|
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">总收入</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">总支出</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">净收益</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; font-weight: 600;">占比</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: #1890ff;">${formatWithThousandsSeparator(item.index01)} 元</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: #f5222d;">${formatWithThousandsSeparator(item.index02)} 元</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: ${netProfitColor};">${formatWithThousandsSeparator(netProfit)} 元</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #e8e8e8; text-align: center; color: #722ed1;">${formatPercentage(item.index04)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
// 移除环比,只保留收入、支出、占比
|
||||||
|
data: ['收入', '支出', '占比'],
|
||||||
|
top: 10,
|
||||||
|
left: 'center',
|
||||||
|
textStyle: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: 10,
|
||||||
|
right: 40, // 增加右侧边距给百分比Y轴
|
||||||
|
bottom: 60,
|
||||||
|
top: 40,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: xAxisData,
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
rotate: 60,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 60,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
margin: 8
|
||||||
|
},
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
axisTick: {
|
||||||
|
alignWithLabel: true,
|
||||||
|
length: 3
|
||||||
|
},
|
||||||
|
boundaryGap: true
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '金额(万元)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => value.toFixed(2)
|
||||||
|
},
|
||||||
|
min: amountMin,
|
||||||
|
max: amountMax,
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
splitLine: { lineStyle: { color: '#e8e8e8' } },
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '百分比(%)',
|
||||||
|
nameTextStyle: { fontSize: 11 },
|
||||||
|
axisLabel: {
|
||||||
|
fontSize: 10,
|
||||||
|
formatter: (value: number) => `${value.toFixed(1)}%`
|
||||||
|
},
|
||||||
|
min: percentMin,
|
||||||
|
max: percentMax,
|
||||||
|
axisLine: { onZero: true },
|
||||||
|
splitLine: { show: false }, // 不显示百分比轴的分割线
|
||||||
|
axisTick: { alignWithLabel: true },
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true,
|
||||||
|
position: 'right', // 百分比轴显示在右侧
|
||||||
|
}
|
||||||
|
],
|
||||||
|
noDataLoadingOption: {
|
||||||
|
text: '暂无数据',
|
||||||
|
textStyle: { fontSize: 14, color: '#999' },
|
||||||
|
effect: 'bubble',
|
||||||
|
effectOption: { effect: { n: 0 } }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '收入',
|
||||||
|
type: 'bar',
|
||||||
|
data: incomeData,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#1890ff' },
|
||||||
|
{ offset: 1, color: '#096dd9' }
|
||||||
|
]),
|
||||||
|
borderRadius: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
barGap: '30%',
|
||||||
|
barCategoryGap: '20%',
|
||||||
|
label: barLabelConfig,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#40a9ff' },
|
||||||
|
{ offset: 1, color: '#1890ff' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '支出',
|
||||||
|
type: 'bar',
|
||||||
|
data: expenseData,
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#f5222d' },
|
||||||
|
{ offset: 1, color: '#cf1322' }
|
||||||
|
]),
|
||||||
|
borderRadius: [6, 6, 0, 0]
|
||||||
|
},
|
||||||
|
barWidth: 12,
|
||||||
|
label: barLabelConfig,
|
||||||
|
yAxisIndex: 0,
|
||||||
|
emphasis: {
|
||||||
|
itemStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: '#ff4d4f' },
|
||||||
|
{ offset: 1, color: '#f5222d' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 只保留占比曲线,移除环比曲线
|
||||||
|
{
|
||||||
|
name: '占比',
|
||||||
|
type: 'line',
|
||||||
|
data: proportionData,
|
||||||
|
yAxisIndex: 1, // 使用右侧的百分比Y轴
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: 6,
|
||||||
|
smooth: true, // 平滑曲线
|
||||||
|
lineStyle: {
|
||||||
|
width: 1,
|
||||||
|
color: '#722ed1', // 紫色
|
||||||
|
type: 'solid'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#722ed1',
|
||||||
|
borderColor: '#fff',
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
label: lineLabelConfig,
|
||||||
|
emphasis: {
|
||||||
|
symbolSize: 8,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
animationDuration: 500,
|
||||||
|
animationEasingUpdate: 'quinticInOut'
|
||||||
|
};
|
||||||
|
|
||||||
|
myChart.setOption(option);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeChart = () => {
|
||||||
|
if (myChart) {
|
||||||
|
myChart.resize({
|
||||||
|
animation: {
|
||||||
|
duration: 300,
|
||||||
|
easing: 'quadraticInOut'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let resizeTimer: number;
|
||||||
|
const debounceResize = () => {
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = window.setTimeout(resizeChart, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchList();
|
||||||
|
window.addEventListener('resize', debounceResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', debounceResize);
|
||||||
|
if (myChart) {
|
||||||
|
myChart.dispose();
|
||||||
|
myChart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(rawData, () => {
|
||||||
|
initChart();
|
||||||
|
}, { deep: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.status-filter-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active {
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #1890ff;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item:not(.active):hover {
|
||||||
|
color: #40a9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card:hover) {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
padding: 0 !important;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip) {
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #e8e8e8 !important;
|
||||||
|
max-width: 500px; /* 加宽tooltip以容纳新增列 */
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip table) {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-xaxis-label) {
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-yaxis-name) {
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-legend-item) {
|
||||||
|
margin-right: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-label) {
|
||||||
|
font-family: 'Microsoft YaHei', sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip::-webkit-scrollbar) {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip::-webkit-scrollbar-thumb) {
|
||||||
|
background: #d9d9d9;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.echarts-tooltip::-webkit-scrollbar-track) {
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
web-vue/web/public/resource/img/icons/erp-expense.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1771407056700" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11634" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#FAAD14" p-id="11635"></path><path d="M564.098612 667.021061h0.543347a16.509388 16.509388 0 0 1 0 33.039674h-41.252571v37.156571a20.58449 20.58449 0 1 1-41.148082 0v-37.156571h-41.795918a16.509388 16.509388 0 0 1 0-32.997878h41.795918v-24.764081h-41.795918a16.509388 16.509388 0 0 1 0-32.997878h20.082939l-17.303511-53.331592-0.229877-0.731428a16.48849 16.48849 0 0 1 31.346939-10.177307l0.167183 0.501551v0.376164l20.48 63.320816h15.318204l20.500898-63.237224v-0.41796l0.397061-0.480653a16.48849 16.48849 0 0 1 31.346939 10.114613l-0.229877 0.689632-17.30351 53.331592h19.623183a16.509388 16.509388 0 0 1 0 33.039674h-41.252571v24.722285h40.709224z" fill="#FAAD14" p-id="11636"></path><path d="M623.929469 302.247184l-116.610612 116.610612-116.610612-116.610612a32.997878 32.997878 0 0 0-46.665143 46.665143l105.158531 105.200326h-55.985633a33.018776 33.018776 0 0 0 0 66.016653h82.254367V572.604082h-82.902204a35.651918 35.651918 0 0 0 0 71.282938h82.902204v71.471021a33.018776 33.018776 0 0 0 66.016653 0v-71.471021h82.12898a35.651918 35.651918 0 1 0 0-71.282938h-82.12898v-52.474776h82.776817a33.018776 33.018776 0 1 0 0-66.016653h-58.827755l105.15853-105.200326a32.997878 32.997878 0 0 0-46.665143-46.665143z" fill="#FFFFFF" p-id="11637"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
1
web-vue/web/public/resource/img/icons/erp-fencheng.svg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
web-vue/web/public/resource/img/icons/erp-home.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
1
web-vue/web/public/resource/img/icons/erp-income.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1771407178032" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13001" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M64 512a448 448 0 1 0 896 0 448 448 0 1 0-896 0z" fill="#FFBD27" p-id="13002"></path><path d="M413.776 279.52l32.256 41.92h112.864l32.256-41.92 3.232-6.448v-3.232c0-6.448-3.232-9.664-9.68-12.896 0 0-45.152-9.664-83.84-9.664-38.72 0-83.856 9.664-83.856 9.664-16.128 6.448-3.232 22.576-3.232 22.576z m154.8 70.96H452.48C362.176 382.72 288 482.704 288 576.24c0 116.08 64.496 170.912 222.528 170.912 158.032 0 222.528-54.832 222.528-170.928 0-93.52-74.176-193.504-164.48-225.76z m22.576 238.64c6.448 0 12.896 6.448 12.896 12.912 0 6.448-6.448 12.896-12.896 12.896h-64.496v45.152c0 6.448-6.448 12.896-12.896 12.896-6.464 0-12.912-6.448-12.912-12.896v-41.92h-64.496c-6.448 0-12.896-6.464-12.896-12.912s6.448-12.896 12.896-12.896h64.496V556.88h-64.496c-6.448 0-12.896-6.448-12.896-12.912 0-6.448 6.448-12.896 12.896-12.896h67.728v-3.216s-3.232 0-3.232-3.232l-51.6-61.28c-6.448-3.216-6.448-12.896 0-19.344s16.128-3.232 19.36 3.232l45.152 51.6 45.136-51.6c6.464-6.464 12.912-6.464 19.36-3.232 6.448 6.448 6.448 12.896 3.216 19.36l-51.6 61.28c0 3.2-3.216 3.2-6.448 3.2h67.728c6.448 0 12.896 6.464 12.896 12.912s-6.448 12.896-12.896 12.896h-64.496v35.472h64.496z" fill="#FFFFFF" p-id="13003"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M366.157576 633.018182c17.066667-44.993939 29.478788-89.987879 63.612121-124.121212C209.454545 501.139394 35.684848 439.078788 35.684848 341.333333v107.054546c0 85.333333 147.393939 161.357576 330.472728 184.630303z" fill="#2E9CFE" /><path d="M380.121212 817.648485c-7.757576-27.927273-13.963636-55.854545-13.963636-85.333333 0-13.963636 3.10303-26.375758 4.654545-40.339394C195.490909 671.806061 37.236364 614.4 37.236364 530.618182v107.054545C34.133333 732.315152 162.909091 791.272727 380.121212 817.648485zM480.969697 448.387879c9.309091 0 18.618182-1.551515 27.927273-1.551515 65.163636-38.787879 145.842424-62.060606 231.175757-62.060606 20.169697 0 40.339394 3.10303 60.509091 4.654545 77.575758-34.133333 125.672727-80.678788 125.672727-131.878788v-63.612121C926.254545 88.436364 727.660606 4.654545 480.969697 4.654545 235.830303 4.654545 35.684848 89.987879 35.684848 193.939394v63.612121c0 105.50303 200.145455 190.836364 445.284849 190.836364z" fill="#2E9CFE" /><path d="M398.739394 878.157576C203.248485 862.642424 35.684848 809.890909 35.684848 721.454545v107.054546c0 105.50303 198.593939 189.284848 445.284849 189.284848 7.757576 0 17.066667-1.551515 24.824242-1.551515-55.854545-32.581818-77.575758-85.333333-107.054545-138.084848zM712.145455 423.563636c-155.151515 0-279.272727 133.430303-279.272728 297.890909s125.672727 297.890909 279.272728 297.89091c155.151515 0 279.272727-133.430303 279.272727-297.89091s-125.672727-297.890909-279.272727-297.890909z m94.642424 350.642425H505.793939v-89.987879H926.254545v89.987879h-119.466666z" fill="#2E9CFE" /></svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
1
web-vue/web/public/resource/img/icons/erp-jinglirun.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="192.84px" viewBox="0 0 1062 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M735.744 701.44m-311.296 0a311.296 311.296 0 1 0 622.592 0 311.296 311.296 0 1 0-622.592 0Z" fill="#FFCAD8" /><path d="M804.864 659.456c5.12 1.024 10.24 1.536 14.336 2.048h47.104c4.608-0.512 8.704 1.024 11.776 5.12 3.072 3.584 4.096 11.264 4.096 11.264v33.792s-3.584 10.24-13.312 11.264h-81.92c-3.584-1.536-6.656-0.512-10.752 1.536-3.072 1.536-4.608 4.608-4.608 8.704 0 4.096 0 8.192 0.512 12.288 0.512 4.096 0.512 8.192 0.512 10.752 0 5.632 4.608 7.68 13.824 8.704h80.896c9.216-2.56 18.944 10.24 18.944 10.24v31.232s-12.288 13.824-25.088 10.24h-17.92c-7.68 3.584-14.848 4.096-23.04 4.608-8.192 0.512-14.848 1.024-21.504 0.512h-12.8c-8.192 0.512-12.8 14.336-12.8 14.336v55.808c0 4.608-3.584 6.656-10.752 6.656h-41.984c-11.264 0-16.896-4.608-16.896-14.336 0-3.584 0.512-11.264 0.512-23.04 0.512-11.776 0-19.456-0.512-24.064 0-4.096-0.512-7.68-1.536-8.704-0.512-1.024-3.072-2.048-8.192-2.048h-87.552c-12.288 0-18.432-6.144-18.432-18.944 0-2.56-0.512-6.144-1.536-11.264-1.024-4.096-1.024-8.704 0-13.824 1.024-4.608 2.56-9.216 4.096-13.312 2.048-3.584 5.632-5.12 11.264-4.096h96.256c2.048 0 2.56-2.048 2.56-7.68v-26.624c0-4.608-5.632-6.656-16.896-6.656h-81.92c-3.584 0-6.656 0-10.24-0.512-3.072-0.512-4.096-3.072-4.096-8.192v-18.432s-1.536-13.312-2.048-16.896c-2.048-5.632-0.512-10.752 4.096-13.824 4.608-4.096 10.752-5.632 17.92-5.632 7.68 1.024 15.36 1.024 24.576 0.512 8.704-0.512 15.36-0.512 19.968-3.584h16.384c5.632 3.072 7.68 1.024 5.632-2.048-1.536-2.048-7.68-9.216-16.384-20.992-9.728-12.8-18.944-25.088-29.184-38.4-12.288-15.872-26.112-33.28-41.984-52.736-6.144-10.752-5.12-18.944 4.096-25.6 4.608-3.072 9.728-6.144 14.336-10.752 5.12-4.096 11.264-8.192 17.408-11.776 9.216-5.632 17.92-0.512 26.624 15.36 3.072 4.096 8.192 10.752 15.872 20.48 8.192 10.24 15.872 19.968 24.576 31.744 8.192 11.776 15.872 21.504 24.064 30.208 7.68 9.216 11.776 14.848 12.8 16.384 1.536 3.584 5.12 6.144 10.24 6.656 5.12 1.024 9.216-0.512 10.752-4.096 1.024-2.048 4.608-8.192 11.776-17.92 6.656-9.728 14.336-20.48 23.04-32.768s15.872-24.064 23.552-34.816c7.68-10.752 12.288-17.408 13.824-19.968 4.608-6.656 8.704-10.752 11.776-12.8 3.072-2.048 8.704-1.536 15.872 1.536 4.608 2.048 9.728 4.608 14.848 8.704 5.632 3.584 9.728 6.656 11.264 8.704 10.24 10.752 13.312 18.944 8.704 27.136-1.536 3.072-6.656 10.24-14.848 20.992-8.704 11.264-17.408 23.552-27.136 35.84-10.24 12.8-18.432 24.576-26.624 35.84s-12.288 17.92-13.312 19.456c-0.512 3.584 0.512 6.656 5.12 7.68z" fill="#FFFFFF" /><path d="M26.624 189.952a382.976 189.952 0 1 0 765.952 0 382.976 189.952 0 1 0-765.952 0Z" fill="#FB7895" /><path d="M530.944 428.544C492.544 435.2 452.096 438.272 409.6 438.272c-182.272 0-334.848-62.976-373.76-147.968-6.144 13.824-9.728 27.648-9.728 42.496 0 104.96 171.52 189.952 382.976 189.952 10.24 0 19.968-0.512 30.208-1.024 24.576-36.352 55.296-67.584 91.648-93.184zM782.848 376.832c6.656-13.824 10.752-28.672 10.752-43.52 0-14.336-3.584-28.672-9.728-42.496-13.824 30.208-42.496 57.344-80.896 79.872 5.12-0.512 10.752-1.024 16.384-1.024 20.992 0.512 42.496 2.56 63.488 7.168z" fill="#FB7895" /><path d="M409.6 564.224c-182.272 0-334.848-62.976-373.76-147.968-6.144 13.824-9.728 27.648-9.728 42.496 0 101.888 161.792 185.344 365.568 189.952 4.608-29.696 13.312-57.856 25.6-84.48H409.6z" fill="#FB7895" /><path d="M388.096 688.64c-172.544-4.608-314.88-66.56-351.744-147.456-6.144 13.824-9.728 27.648-9.728 42.496 0 102.4 163.84 186.368 368.64 189.952-5.12-23.552-8.192-47.104-8.192-71.68 0.512-4.608 1.024-9.216 1.024-13.312z" fill="#FB7895" /><path d="M36.352 666.624c-6.144 13.824-9.728 27.648-9.728 42.496 0 104.96 171.52 189.952 382.976 189.952 14.336 0 28.16-0.512 41.984-1.024-18.432-25.6-33.28-53.248-44.032-83.456-181.248-1.024-332.8-64.512-371.2-147.968z" fill="#FB7895" /><path d="M484.864 935.424c-24.576 2.56-49.152 3.584-75.264 3.584-182.272 0-334.848-62.976-373.76-147.968-6.144 13.824-9.728 27.648-9.728 42.496 0 104.96 171.52 189.952 382.976 189.952 62.464 0 120.832-7.68 173.056-20.48-35.84-16.896-69.12-39.424-97.28-67.584z" fill="#FB7895" /></svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
1
web-vue/web/public/resource/img/icons/erp-lirunfenxi.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M119.467 494.933c-5.12 0-8.534-1.706-11.947-5.12-6.827-6.826-6.827-17.066 0-23.893l238.933-238.933c6.827-6.827 17.067-6.827 23.894 0L512 368.64l226.987-226.987c6.826-6.826 17.066-6.826 23.893 0s6.827 17.067 0 23.894L523.947 404.48c-6.827 6.827-17.067 6.827-23.894 0L358.4 262.827 131.413 489.813c-3.413 3.414-6.826 5.12-11.946 5.12zM682.667 409.6h153.6v477.867h-153.6V409.6z" fill="#CDCDCD" /><path d="M836.267 904.533h-153.6c-10.24 0-17.067-6.826-17.067-17.066V409.6c0-10.24 6.827-17.067 17.067-17.067h153.6c10.24 0 17.066 6.827 17.066 17.067v477.867c0 10.24-6.826 17.066-17.066 17.066zM699.733 870.4H819.2V426.667H699.733V870.4zM426.667 563.2h153.6v324.267h-153.6V563.2z" fill="#CDCDCD" /><path d="M580.267 904.533h-153.6c-10.24 0-17.067-6.826-17.067-17.066V563.2c0-10.24 6.827-17.067 17.067-17.067h153.6c10.24 0 17.066 6.827 17.066 17.067v324.267c0 10.24-6.826 17.066-17.066 17.066zM443.733 870.4H563.2V580.267H443.733V870.4z m-119.466 34.133H187.733c-10.24 0-17.066-6.826-17.066-17.066V648.533c0-10.24 6.826-17.066 17.066-17.066h136.534c10.24 0 17.066 6.826 17.066 17.066v238.934c0 10.24-6.826 17.066-17.066 17.066zM204.8 870.4h102.4V665.6H204.8v204.8zM119.467 431.787c25.6 0 46.08 20.48 46.08 46.08 0 25.6-20.48 46.08-46.08 46.08s-46.08-20.48-46.08-46.08c0-25.6 20.48-46.08 46.08-46.08z" fill="#CDCDCD" /><path d="M119.467 541.013c-34.134 0-63.147-27.306-63.147-63.146s29.013-63.147 63.147-63.147 63.146 27.307 63.146 63.147-29.013 63.146-63.146 63.146z m0-92.16c-15.36 0-29.014 11.947-29.014 29.014s11.947 29.013 29.014 29.013 29.013-11.947 29.013-29.013-13.653-29.014-29.013-29.014zM358.4 192.853c25.6 0 46.08 20.48 46.08 46.08 0 25.6-20.48 46.08-46.08 46.08-25.6 0-46.08-20.48-46.08-46.08 0-25.6 20.48-46.08 46.08-46.08z" fill="#CDCDCD" /><path d="M358.4 302.08c-34.133 0-63.147-27.307-63.147-63.147s27.307-63.146 63.147-63.146 63.147 27.306 63.147 63.146-29.014 63.147-63.147 63.147z m0-92.16c-15.36 0-29.013 11.947-29.013 29.013s11.946 29.014 29.013 29.014 29.013-11.947 29.013-29.014S373.76 209.92 358.4 209.92zM512 346.453c25.6 0 46.08 20.48 46.08 46.08 0 25.6-20.48 46.08-46.08 46.08s-46.08-20.48-46.08-46.08c0-25.6 20.48-46.08 46.08-46.08z" fill="#CDCDCD" /><path d="M512 455.68c-34.133 0-63.147-27.307-63.147-63.147S476.16 329.387 512 329.387c34.133 0 63.147 27.306 63.147 63.146S546.133 455.68 512 455.68z m0-92.16c-15.36 0-29.013 11.947-29.013 29.013s11.946 29.014 29.013 29.014 29.013-11.947 29.013-29.014S527.36 363.52 512 363.52zM768 90.453c25.6 0 46.08 20.48 46.08 46.08s-20.48 46.08-46.08 46.08-46.08-20.48-46.08-46.08 20.48-46.08 46.08-46.08z" fill="#CDCDCD" /><path d="M768 199.68c-34.133 0-63.147-27.307-63.147-63.147S733.867 73.387 768 73.387s63.147 29.013 63.147 63.146S802.133 199.68 768 199.68z m0-92.16c-15.36 0-29.013 11.947-29.013 29.013s11.946 29.014 29.013 29.014 29.013-11.947 29.013-29.014S783.36 107.52 768 107.52z" fill="#CDCDCD" /></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
1
web-vue/web/public/resource/img/icons/erp-shouru.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M820.30848 447.99744 820.30848 311.91168c0-44.14848-36.28544-80.06528-80.88576-80.06528l-1.85728 0-40.46464-82.05056c-9.53344-19.32672-30.96576-32.31232-53.3312-32.31232-5.1392 0-10.19904 0.68608-15.04 2.03776L250.20032 225.3824c-4.21504 1.17888-7.98592 3.39712-11.01824 6.464l-3.2896 0c-44.60032 0-80.88576 35.9168-80.88576 80.06528l0 515.26656c0 44.14848 36.28544 80.06656 80.88576 80.06656l503.53152 0c44.60032 0 80.88576-35.91808 80.88576-80.06656L820.30976 691.0912c62.944-6.6496 111.65824-59.26656 111.65824-121.54624C931.96672 507.26272 883.25376 454.64576 820.30848 447.99744zM411.04 231.8464l231.22688-64.6656c0.42624-0.11904 0.91904-0.1792 1.46688-0.1792 3.53408 0 7.4816 2.44736 8.51968 4.5504l29.7344 60.29312L411.04 231.84512zM204.93184 311.91168c0-16.84608 13.88928-30.55104 30.96064-30.55104l503.53152 0c17.07136 0 30.96064 13.70496 30.96064 30.55104l0 135.3664-103.47136 0c-69.0304 0-125.1904 54.848-125.1904 122.2656 0 67.4176 56.16 122.2656 125.1904 122.2656l103.47136 0 0 135.36896c0 16.84608-13.88928 30.55232-30.96064 30.55232L235.89248 857.73056c-17.07136 0-30.96064-13.70624-30.96064-30.55232L204.93184 311.91168zM882.04288 569.54368c0 40.1152-33.76384 72.75136-75.26528 72.75136L666.91328 642.29504c-41.50144 0-75.26528-32.63616-75.26528-72.75136 0-40.1152 33.76384-72.75136 75.26528-72.75136l139.86432 0C848.27904 496.79232 882.04288 529.42848 882.04288 569.54368zM678.5792 513.09056c-31.7568 0-57.5936 25.57696-57.5936 57.01632 0 31.43808 25.8368 57.01504 57.5936 57.01504s57.5936-25.57696 57.5936-57.01504C736.1728 538.66752 710.336 513.09056 678.5792 513.09056zM686.24768 570.10688c0 3.9168-3.44064 7.10272-7.66848 7.10272s-7.66848-3.18592-7.66848-7.10272c0-3.9168 3.44064-7.104 7.66848-7.104S686.24768 566.19008 686.24768 570.10688z" fill="#272636" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
1
web-vue/web/public/resource/img/icons/erp-zhichu.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M835.98 932.97H185.92c-40.42 0-73.31-32.88-73.31-73.31V457.22c0-40.42 32.88-73.31 73.31-73.31h650.05c40.42 0 73.31 32.88 73.31 73.31v402.43c0.01 40.43-32.88 73.32-73.3 73.32zM185.92 432.79c-13.48 0-24.44 10.96-24.44 24.44v402.43c0 13.48 10.96 24.44 24.44 24.44h650.05c13.47 0 24.44-10.96 24.44-24.44V457.22c0-13.48-10.97-24.44-24.44-24.44H185.92zM276.19 528.58h469.52v48.87H276.19zM276.19 648.06h469.52v48.87H276.19zM282.09 435.64l-42.32-24.43 146.47-253.69 392.85 226.82-26.55 46.01-42.33-24.44 2.13-3.68-308.22-177.95zM761.7 429.11L570.36 157.62l-102.19 72.02-28.16-39.95L582.15 89.51l219.5 311.45z" /></svg>
|
||||||
|
After Width: | Height: | Size: 877 B |
1
web-vue/web/public/resource/img/icons/erp-zongshouru.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M880.514012 124.087882c10.998926 0 19.998047 8.999121 19.998047 19.998047v735.928132c0 10.998926-8.999121 19.998047-19.998047 19.998047h-735.928132c-10.998926 0-19.998047-8.999121-19.998047-19.998047v-735.928132c0-10.998926 8.999121-19.998047 19.998047-19.998047h735.928132m0-59.994141h-735.928132c-44.195684 0-79.992188 35.796504-79.992188 79.992188v735.928132c0 44.195684 35.796504 79.992188 79.992188 79.992188h735.928132c44.195684 0 79.992188-35.796504 79.992189-79.992188v-735.928132c0-44.195684-35.796504-79.992188-79.992189-79.992188z" fill="#FF6600" opacity=".502" /><path d="M64.593692 244.076164h119.988282v59.994142h-119.988282z" fill="#FF6600" /><path d="M64.593692 720.029685h119.988282v59.994141h-119.988282z" fill="#FF6600" /><path d="M688.53276 276.17303h-48.795234L512.949907 503.150864l-126.687628-226.977834h-48.795235l139.186408 241.576409H370.463822v26.997363h121.388146v69.293233H370.463822v27.097354h121.388146v106.18963h42.295869V641.137389h121.388146v-27.097354H534.147837v-69.293233h121.388146v-26.997363H549.246363z" fill="#FF6600" /></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |