项目初始化

This commit is contained in:
2026-03-23 17:38:35 +08:00
parent 876b3800b4
commit 6011e29abf
12 changed files with 1554 additions and 1108 deletions

View File

@@ -0,0 +1,124 @@
package com.jeesite.modules.apps.Module;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class ChartDataItem implements Serializable {
/**
* 项目编号
*/
private String itemCode;
/**
* 序号
*/
private Long soring;
/**
* X轴名称
*/
private String axisName;
/**
* 指标01
*/
private String value01;
/**
* 指标02
*/
private String value02;
/**
* 指标03
*/
private String value03;
/**
* 指标04
*/
private String value04;
/**
* 指标05
*/
private String value05;
/**
* 指标06
*/
private String value06;
/**
* 指标07
*/
private String value07;
/**
* 指标08
*/
private String value08;
/**
* 指标09
*/
private String value09;
/**
* 指标10
*/
private String value10;
/**
* 指标11
*/
private String value11;
/**
* 指标12
*/
private String value12;
/**
* 指标13
*/
private String value13;
/**
* 指标14
*/
private String value14;
/**
* 指标15
*/
private String value15;
/**
* 业务主键编号
*/
private String bizKey;
/**
* 指标求和
*/
private BigDecimal indexSum;
/**
* 指标平均
*/
private BigDecimal indexAvg;
/**
* 指标最大
*/
private BigDecimal indexMax;
/**
* 指标最小
*/
private BigDecimal indexMin;
}

View File

@@ -0,0 +1,168 @@
package com.jeesite.modules.apps.web;
import com.jeesite.modules.apps.Module.ChartDataItem;
import com.jeesite.modules.erp.entity.ErpTransactionFlow;
import com.jeesite.modules.erp.service.ErpTransactionFlowService;
import com.jeesite.modules.utils.BigDecimalUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Controller
@RequestMapping(value = "${adminPath}/erp/screen")
public class ErpScreenController {
private final ErpTransactionFlowService erpTransactionFlowService;
public ErpScreenController(ErpTransactionFlowService erpTransactionFlowService) {
this.erpTransactionFlowService = erpTransactionFlowService;
}
/**
* 账号收支
*/
@RequestMapping(value = "getErpAccountChart")
@ResponseBody
public List<ChartDataItem> getErpAccountChart(ErpTransactionFlow erpTransactionFlow) {
List<ChartDataItem> chartDataItems = new ArrayList<>();
List<ErpTransactionFlow> flowList = erpTransactionFlowService.findList(erpTransactionFlow);
Map<String, Map<String, Object>> accountMap = flowList.stream()
.collect(Collectors.groupingBy(
ErpTransactionFlow::getAccountName,
Collectors.collectingAndThen(Collectors.toList(), list -> {
String accountId = list.stream()
.findFirst()
.map(ErpTransactionFlow::getAccountId)
.orElse("");
BigDecimal sumValue01 = list.stream()
.filter(flow -> flow.getFlowType().equals("2"))
.map(ErpTransactionFlow::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal sumValue02 = list.stream()
.filter(flow -> flow.getFlowType().equals("1"))
.map(ErpTransactionFlow::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal sumValue03 = BigDecimalUtils.subtract(sumValue01, sumValue02);
return Map.of(
"accountId", accountId,
"sumValue01", sumValue01,
"sumValue02", sumValue02,
"sumValue03", sumValue03
);
})
));
for (Map.Entry<String, Map<String, Object>> entry : accountMap.entrySet()) {
String accountName = entry.getKey();
Map<String, Object> map = entry.getValue();
ChartDataItem item = new ChartDataItem();
item.setItemCode("CHART");
item.setAxisName(accountName);
item.setValue01(map.get("sumValue01").toString());
item.setValue02(map.get("sumValue02").toString());
item.setValue03(map.get("sumValue03").toString());
item.setBizKey(map.get("accountId").toString());
chartDataItems.add(item);
}
return chartDataItems;
}
/**
* 月份收支
*/
@RequestMapping(value = "getErpMonthChart")
@ResponseBody
public List<ChartDataItem> getErpMonthChart(ErpTransactionFlow erpTransactionFlow) {
List<ChartDataItem> chartDataItems = new ArrayList<>();
List<ErpTransactionFlow> flowList = erpTransactionFlowService.findList(erpTransactionFlow);
Map<String, Map<String, Object>> accountMap = flowList.stream()
.collect(Collectors.groupingBy(
ErpTransactionFlow::getMonthDate,
Collectors.collectingAndThen(Collectors.toList(), list -> {
BigDecimal sumValue01 = list.stream()
.filter(flow -> flow.getFlowType().equals("2"))
.map(ErpTransactionFlow::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal sumValue02 = list.stream()
.filter(flow -> flow.getFlowType().equals("1"))
.map(ErpTransactionFlow::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal sumValue04 = BigDecimalUtils.subtract(sumValue01, sumValue02);
BigDecimal sumValue03 = BigDecimalUtils.percent(sumValue04, sumValue01);
return Map.of(
"sumValue01", sumValue01,
"sumValue02", sumValue02,
"sumValue03", sumValue03,
"sumValue04", sumValue04
);
})
));
for (Map.Entry<String, Map<String, Object>> entry : accountMap.entrySet()) {
String monthDate = entry.getKey();
Map<String, Object> map = entry.getValue();
ChartDataItem item = new ChartDataItem();
item.setAxisName(monthDate);
item.setValue01(map.get("sumValue01").toString());
item.setValue02(map.get("sumValue02").toString());
item.setValue03(map.get("sumValue03").toString());
item.setValue04(map.get("sumValue04").toString());
chartDataItems.add(item);
}
return chartDataItems;
}
/**
* 分类收支
*/
@RequestMapping(value = "getCategoryChart")
@ResponseBody
public List<ChartDataItem> getCategoryChart(ErpTransactionFlow erpTransactionFlow) {
List<ChartDataItem> chartDataItems = new ArrayList<>();
List<ErpTransactionFlow> flowList = erpTransactionFlowService.findList(erpTransactionFlow);
BigDecimal totalAmount = flowList.stream()
.map(ErpTransactionFlow::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Map<String, Map<String, Object>> accountMap = flowList.stream()
.collect(Collectors.groupingBy(
ErpTransactionFlow::getCategoryName,
Collectors.collectingAndThen(Collectors.toList(), list -> {
BigDecimal sumValue01 = list.stream()
.map(ErpTransactionFlow::getAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal sumValue02 = BigDecimalUtils.percent(sumValue01, totalAmount);
return Map.of(
"sumValue01", sumValue01,
"sumValue02", sumValue02
);
})
));
List<Map.Entry<String, Map<String, Object>>> sortedList = new ArrayList<>(accountMap.entrySet());
sortedList.sort((e1, e2) -> {
BigDecimal v1 = (BigDecimal) e1.getValue().get("sumValue02");
BigDecimal v2 = (BigDecimal) e2.getValue().get("sumValue02");
return v2.compareTo(v1);
});
long soring = 1;
for (Map.Entry<String, Map<String, Object>> entry : sortedList) {
String categoryName = entry.getKey();
Map<String, Object> map = entry.getValue();
ChartDataItem item = new ChartDataItem();
item.setSoring(soring++);
item.setAxisName(categoryName);
item.setValue01(map.get("sumValue01").toString());
item.setValue02(map.get("sumValue02").toString());
chartDataItems.add(item);
}
return chartDataItems;
}
}

View File

@@ -42,8 +42,8 @@ import java.io.Serial;
@Column(name = "remark", attrName = "remark", label = "交易备注", isQuery = false),
@Column(name = "update_time", attrName = "updateTime", label = "更新时间", isQuery = false, isUpdateForce = true),
@Column(name = "business_id", attrName = "businessId", label = "业务标识"),
@Column(name = "year_date", attrName = "yearDate", label = "业务标识"),
@Column(name = "month_date", attrName = "monthDate", label = "业务标识"),
@Column(name = "year_date", attrName = "yearDate", label = "年份"),
@Column(name = "month_date", attrName = "monthDate", label = "月份"),
}, joinTable = {
@JoinTable(type = Type.LEFT_JOIN, entity = ErpAccount.class, alias = "b",
on = "a.account_id = b.account_id", attrName = "this",

View File

@@ -48,6 +48,22 @@ public class BigDecimalUtils {
return num1.divide(num2, SCALE, ROUND_MODE);
}
/**
* 百分比
*/
public static BigDecimal percent(BigDecimal num1, BigDecimal num2) {
num1 = defaultIfNull(num1);
num2 = defaultIfNull(num2);
if (num2.compareTo(BigDecimal.ZERO) == 0) {
return BigDecimal.ZERO;
}
// 先除 → 再 ×100 → 保留2位
return num1.divide(num2, 4, ROUND_MODE)
.multiply(new BigDecimal("100"))
.setScale(SCALE, ROUND_MODE);
}
private static BigDecimal defaultIfNull(BigDecimal num) {
return num == null ? BigDecimal.ZERO : num;
}

View File

@@ -73,8 +73,9 @@ const switchTabByRoute = (item) => {
router.push({
path: item.path,
query: {
year: queryDate.value,
...route.query
...route.query,
yearDate: queryDate.value,
}
}).catch(err => {
ElMessage.error('页面切换失败,请重试');
@@ -90,7 +91,7 @@ const handleQuery = () => {
path: route.path,
query: {
...route.query,
year: queryDate.value
yearDate: queryDate.value,
}
});
};
@@ -112,8 +113,8 @@ async function getList() {
onMounted(() => {
getList();
router.afterEach((to) => {
if (to.query.year) {
queryDate.value = to.query.year;
if (to.query.yearDate) {
queryDate.value = to.query.yearDate;
}
});
});

View File

@@ -7,197 +7,201 @@
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
import * as echarts from 'echarts'
// import { getItemInfoList } from '@/api/bizApi'
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import * as echarts from 'echarts';
import { ChartDataItem, ErpAccountChart } from '@jeesite/erp/api/erp/screen';
const props = defineProps({
formParams: {
type: Object,
default: () => ({})
}
})
const props = defineProps({
formParams: {
type: Object,
default: () => ({}),
},
});
const vList = ref([])
const chartRef = ref(null)
let chartInstance = null
const resizeHandler = () => chartInstance?.resize()
const vList = ref<ChartDataItem[]>([]);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const resizeHandler = () => chartInstance?.resize();
async function getList() {
try {
const params = {
...props.formParams,
itemCode: 'ERP_ACCOUNT_Y001',
const parseAmount = (value?: string | number) => {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) ? parsed : 0;
};
async function getList() {
try {
const params = {
...props.formParams,
};
const res = await ErpAccountChart(params);
vList.value = res || [];
} catch (error) {
console.error(error);
vList.value = [];
}
const res = await getItemInfoList(params)
vList.value = res || []
} catch (error) {
console.error(error)
vList.value = []
}
}
function initChart() {
const el = chartRef.value
if (!el) return
if (!chartInstance) {
chartInstance = echarts.init(el)
}
const baseOption = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70' },
padding: [8, 12],
borderRadius: 6
},
legend: {
top: '10',
left: 'center',
textStyle: { fontSize: 12, color: '#e0e6ff' },
data: ['收入', '支出', '净利润']
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
top: '15%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: [],
axisLabel: {
fontSize: 11,
interval: 0,
color: '#b4c7e7'
},
axisLine: { lineStyle: { color: '#1a508b' } },
boundaryGap: true
}
],
yAxis: [
{
type: 'value',
name: '金额 (万元)',
nameTextStyle: { fontSize: 12, color: '#b4c7e7' },
axisLabel: { formatter: '{value}', color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.3)' } }
},
{
type: 'value',
name: '净利润 (万元)',
nameTextStyle: { fontSize: 12, color: '#b4c7e7' },
axisLabel: { formatter: '{value}', color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.2)' } },
min: 'dataMin',
max: 'dataMax',
scale: true
}
],
series: [
{
name: '收入',
type: 'bar',
yAxisIndex: 0,
data: [],
barWidth: '12%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#85E868' },
{ offset: 1, color: '#67C23A' }
]),
borderRadius: [8, 8, 0, 0]
},
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#fff',
formatter: '{c}'
}
},
{
name: '支出',
type: 'bar',
yAxisIndex: 0,
data: [],
barWidth: '12%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FF8A8A' },
{ offset: 1, color: '#F56C6C' }
]),
borderRadius: [8, 8, 0, 0]
},
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#fff',
formatter: '{c}'
}
},
{
name: '净利润',
type: 'line',
yAxisIndex: 1,
data: [],
smooth: true,
lineStyle: { width: 1.5, color: '#409EFF' },
symbol: 'circle',
symbolSize: 5,
label: {
show: true,
position: 'outside',
fontSize: 10,
color: '#409EFF',
formatter: '{c}',
offset: [0, -5]
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.0)' }
])
},
emphasis: {
lineStyle: { width: 2 },
symbolSize: 7
}
}
]
}
function initChart() {
const el = chartRef.value;
if (!el) return;
if (vList.value.length > 0) {
const xData = vList.value.map(item => {
const xaxis = item.xaxis || ''
return xaxis;
})
const index01Yuan = vList.value.map(item => item.index01 || 0)
const index02Yuan = vList.value.map(item => item.index02 || 0)
const index03Yuan = vList.value.map(item => item.index03 || 0)
if (!chartInstance) {
chartInstance = echarts.init(el);
}
const index01Wan = index01Yuan.map(val => (val / 10000).toFixed(2))
const index02Wan = index02Yuan.map(val => (val / 10000).toFixed(2))
const index03Wan = index03Yuan.map(val => (val / 10000).toFixed(2))
const baseOption: echarts.EChartsOption & {
tooltip: echarts.TooltipComponentOption;
xAxis: Array<echarts.XAXisComponentOption & { data: string[] }>;
series: echarts.SeriesOption[];
} = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70' },
padding: [8, 12],
borderRadius: 6,
},
legend: {
top: '10',
left: 'center',
textStyle: { fontSize: 12, color: '#e0e6ff' },
data: ['收入', '支出', '净利润'],
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
top: '15%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: [],
axisLabel: {
fontSize: 11,
interval: 0,
color: '#b4c7e7',
},
axisLine: { lineStyle: { color: '#1a508b' } },
boundaryGap: true,
},
],
yAxis: [
{
type: 'value',
name: '金额 (万元)',
nameTextStyle: { fontSize: 12, color: '#b4c7e7' },
axisLabel: { formatter: '{value}', color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.3)' } },
},
{
type: 'value',
name: '净利润 (万元)',
nameTextStyle: { fontSize: 12, color: '#b4c7e7' },
axisLabel: { formatter: '{value}', color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.2)' } },
min: 'dataMin',
max: 'dataMax',
scale: true,
},
],
series: [
{
name: '收入',
type: 'bar',
yAxisIndex: 0,
data: [],
barWidth: '12%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#85E868' },
{ offset: 1, color: '#67C23A' },
]),
borderRadius: [8, 8, 0, 0],
},
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#fff',
formatter: '{c}',
},
},
{
name: '支出',
type: 'bar',
yAxisIndex: 0,
data: [],
barWidth: '12%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FF8A8A' },
{ offset: 1, color: '#F56C6C' },
]),
borderRadius: [8, 8, 0, 0],
},
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#fff',
formatter: '{c}',
},
},
{
name: '净利润',
type: 'line',
yAxisIndex: 1,
data: [],
smooth: true,
lineStyle: { width: 1.5, color: '#409EFF' },
symbol: 'circle',
symbolSize: 5,
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#409EFF',
formatter: '{c}',
offset: [0, -5],
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.0)' },
]),
},
emphasis: {
focus: 'series',
},
},
],
};
baseOption.tooltip.formatter = function (params) {
const title = params[0].axisValue
const idx = params[0].dataIndex
const incomeVal = index01Yuan[idx]
const expenseVal = index02Yuan[idx]
const netProfitVal = index03Yuan[idx]
return `
if (vList.value?.length > 0) {
const xData = vList.value.map((item) => item.axisName || '');
const index01Yuan = vList.value.map((item) => parseAmount(item.value01));
const index02Yuan = vList.value.map((item) => parseAmount(item.value02));
const index03Yuan = vList.value.map((item) => parseAmount(item.value03));
const index01Wan = index01Yuan.map((val) => Number((val / 10000).toFixed(2)));
const index02Wan = index02Yuan.map((val) => Number((val / 10000).toFixed(2)));
const index03Wan = index03Yuan.map((val) => Number((val / 10000).toFixed(2)));
baseOption.tooltip.formatter = function (params) {
const title = params[0].axisValue;
const idx = params[0].dataIndex;
const incomeVal = index01Yuan[idx];
const expenseVal = index02Yuan[idx];
const netProfitVal = index03Yuan[idx];
return `
<div style="text-align:center; font-weight:bold; margin-bottom:6px">${title}</div>
<table style="width:100%; border-collapse:collapse; text-align:center">
<tr>
@@ -211,77 +215,81 @@ function initChart() {
<td style="border:1px solid #409EFF; padding:4px">${netProfitVal}</td>
</tr>
</table>
`
`;
};
baseOption.xAxis[0].data = xData;
baseOption.series[0].data = index01Wan;
baseOption.series[1].data = index02Wan;
baseOption.series[2].data = index03Wan;
}
baseOption.xAxis[0].data = xData
baseOption.series[0].data = index01Wan
baseOption.series[1].data = index02Wan
baseOption.series[2].data = index03Wan
chartInstance.setOption(baseOption, true);
}
chartInstance.setOption(baseOption, true)
}
watch(
() => props.formParams,
() => {
getList();
},
{ deep: true, immediate: true },
);
watch(
() => props.formParams,
() => {
getList()
},
{ deep: true, immediate: true }
)
watch(
vList,
() => {
nextTick(() => initChart());
},
{ deep: true },
);
watch(vList, () => {
nextTick(() => initChart())
}, { deep: true })
onMounted(() => {
initChart();
window.addEventListener('resize', resizeHandler);
});
onMounted(() => {
initChart()
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style scoped>
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/box/16.png") no-repeat;
background-size: 100% 100%;
}
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/box/16.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/title/03.png") no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/title/03.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409EFF;
letter-spacing: 0.5px;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409eff;
letter-spacing: 0.5px;
}
.bar-line-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
}
</style>
.bar-line-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
}
</style>

View File

@@ -7,222 +7,245 @@
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
// import { getItemInfoList } from '@/api/bizApi'
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { ChartDataItem, CategoryChart } from '@jeesite/erp/api/erp/screen';
const props = defineProps({
formParams: {
type: Object,
default: () => ({})
}
})
const vList = ref([])
const chartRef = ref(null)
let chartInstance = null
const resizeHandler = () => chartInstance?.resize()
async function getList() {
try {
const params = {
...props.formParams,
itemCode: 'ERP_CATEGORY_Y001',
}
const res = await getItemInfoList(params)
vList.value = res || []
} catch (error) {
console.error('获取支出数据失败:', error)
vList.value = []
}
}
const initPieChart = () => {
const el = chartRef.value
if (!el) return
if (chartInstance) {
chartInstance.dispose()
}
chartInstance = echarts.init(el)
const pieData = vList.value.map(item => {
const originalValue = item.index01 || 0
const wanValue = (originalValue / 10000).toFixed(2)
return {
name: item.xaxis || '未知分类',
value: Number(wanValue),
originalValue: originalValue
}
}).filter(item => item.value > 0)
const colorList = [
'#409EFF', '#36CFc9', '#67C23A', '#E6A23C', '#F56C6C',
'#909399', '#722ED1', '#EB2F96', '#1890FF', '#52C41A',
'#FAAD14', '#F5222D', '#8C8C8C', '#A062D4', '#F7BA1E'
]
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70', fontSize: 12 },
padding: [10, 15],
borderRadius: 6,
formatter: function(params) {
return `分类:${params.name}<br/>支出:${params.data.originalValue}元<br/>占比:${params.percent.toFixed(1)}%`
}
const props = defineProps({
formParams: {
type: Object,
default: () => ({}),
},
legend: {
orient: 'horizontal',
top: '10%',
left: 'center',
textStyle: { fontSize: 11, color: '#e0e6ff' },
itemWidth: 12,
itemHeight: 12,
itemGap: 10,
pageIconColor: '#409EFF',
pageTextStyle: { color: '#e0e6ff', fontSize: 10 },
pageButtonItemGap: 6,
pageButtonGap: 10,
type: 'scroll'
});
const vList = ref<ChartDataItem[]>([]);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const resizeHandler = () => chartInstance?.resize();
const parseAmount = (value?: string | number) => {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) ? parsed : 0;
};
async function getList() {
try {
const params = {
...props.formParams,
flowType: '1',
};
const res = await CategoryChart(params);
vList.value = res || [];
} catch (error) {
console.error('获取支出数据失败:', error);
vList.value = [];
}
}
const initPieChart = () => {
const el = chartRef.value;
if (!el) return;
if (chartInstance) {
chartInstance.dispose();
}
chartInstance = echarts.init(el);
const pieData = vList.value
.map((item) => {
const originalValue = parseAmount(item.value01);
const wanValue = (originalValue / 10000).toFixed(2);
return {
name: item.axisName || '未知分类',
value: Number(wanValue),
originalValue: originalValue,
};
})
.filter((item) => item.value > 0);
const colorList = [
'#409EFF',
'#36CFc9',
'#67C23A',
'#E6A23C',
'#F56C6C',
'#909399',
'#722ED1',
'#EB2F96',
'#1890FF',
'#52C41A',
'#FAAD14',
'#F5222D',
'#8C8C8C',
'#A062D4',
'#F7BA1E',
];
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70', fontSize: 12 },
padding: [10, 15],
borderRadius: 6,
formatter: function (params) {
return `分类:${params.name}<br/>支出:${params.data.originalValue}元<br/>占比:${params.percent.toFixed(1)}%`;
},
},
legend: {
orient: 'horizontal',
top: '10%',
left: 'center',
textStyle: { fontSize: 11, color: '#e0e6ff' },
itemWidth: 12,
itemHeight: 12,
itemGap: 10,
pageIconColor: '#409EFF',
pageTextStyle: { color: '#e0e6ff', fontSize: 10 },
pageButtonItemGap: 6,
pageButtonGap: 10,
type: 'scroll',
},
series: [
{
name: '支出',
type: 'pie',
radius: ['30%', '55%'],
center: ['50%', '65%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 4,
borderColor: 'rgba(15, 52, 96, 0.9)',
borderWidth: 1,
},
label: {
show: true,
position: 'outside',
fontSize: 10,
color: '#e0e6ff',
formatter: '{b} {c}万元 ({d}%)',
overflow: 'truncate',
ellipsis: '...',
distance: 8,
},
labelLine: {
show: true,
length: 12,
length2: 8,
lineStyle: { color: '#e0e6ff', width: 1 },
smooth: 0.2,
minTurnAngle: 45,
},
data: pieData,
color: colorList,
},
],
};
chartInstance.setOption(option);
};
watch(
() => props.formParams,
() => {
getList();
},
series: [
{
name: '支出',
type: 'pie',
radius: ['30%', '55%'],
center: ['50%', '65%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 4,
borderColor: 'rgba(15, 52, 96, 0.9)',
borderWidth: 1
},
label: {
show: true,
position: 'outside',
fontSize: 10,
color: '#e0e6ff',
formatter: '{b} {c}万元 ({d}%)',
overflow: 'truncate',
ellipsis: '...',
distance: 8
},
labelLine: {
show: true,
length: 12,
length2: 8,
lineStyle: { color: '#e0e6ff', width: 1 },
smooth: 0.2,
minTurnAngle: 45
},
data: pieData,
color: colorList
}
]
}
{ deep: true, immediate: true },
);
chartInstance.setOption(option)
}
watch(
vList,
() => {
initPieChart();
},
{ deep: true },
);
watch(
() => props.formParams,
() => {
getList()
},
{ deep: true, immediate: true }
)
onMounted(() => {
window.addEventListener('resize', resizeHandler);
});
watch(vList, () => {
initPieChart()
}, { deep: true })
onMounted(() => {
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style scoped>
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/box/16.png") no-repeat;
background-size: 100% 100%;
}
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/box/16.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/title/23.png") no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/title/23.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409EFF;
letter-spacing: 0.5px;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409eff;
letter-spacing: 0.5px;
}
.pie-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
margin: 0;
padding: 0;
}
.pie-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
margin: 0;
padding: 0;
}
:deep(.echarts-tooltip) {
background-color: rgba(145, 200, 255, 0.9) !important;
border-color: #409EFF !important;
color: #0a3b70 !important;
border-radius: 6px !important;
}
:deep(.echarts-tooltip) {
background-color: rgba(145, 200, 255, 0.9) !important;
border-color: #409eff !important;
color: #0a3b70 !important;
border-radius: 6px !important;
}
:deep(.echarts-legend-scroll) {
background-color: transparent !important;
}
:deep(.echarts-legend-scroll-text) {
color: #e0e6ff !important;
font-size: 11px !important;
}
:deep(.echarts-legend-scroll-button) {
border-color: #1a508b !important;
}
:deep(.echarts-legend-scroll-button-icon) {
color: #409EFF !important;
}
:deep(.echarts-legend-scroll) {
background-color: transparent !important;
}
:deep(.echarts-legend-scroll-text) {
color: #e0e6ff !important;
font-size: 11px !important;
}
:deep(.echarts-legend-scroll-button) {
border-color: #1a508b !important;
}
:deep(.echarts-legend-scroll-button-icon) {
color: #409eff !important;
}
:deep(.ec-label) {
z-index: 9999 !important;
white-space: nowrap !important;
font-size: 10px !important;
color: #e0e6ff !important;
}
:deep(.ec-label) {
z-index: 9999 !important;
white-space: nowrap !important;
font-size: 10px !important;
color: #e0e6ff !important;
}
:deep(.ec-label-line) {
stroke: #e0e6ff !important;
stroke-width: 1px !important;
}
</style>
:deep(.ec-label-line) {
stroke: #e0e6ff !important;
stroke-width: 1px !important;
}
</style>

View File

@@ -7,222 +7,245 @@
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
// import { getItemInfoList } from '@/api/bizApi'
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { ChartDataItem, CategoryChart } from '@jeesite/erp/api/erp/screen';
const props = defineProps({
formParams: {
type: Object,
default: () => ({})
}
})
const vList = ref([])
const chartRef = ref(null)
let chartInstance = null
const resizeHandler = () => chartInstance?.resize()
async function getList() {
try {
const params = {
...props.formParams,
itemCode: 'ERP_CATEGORY_Y002',
}
const res = await getItemInfoList(params)
vList.value = res || []
} catch (error) {
console.error('获取收入数据失败:', error)
vList.value = []
}
}
const initPieChart = () => {
const el = chartRef.value
if (!el) return
if (chartInstance) {
chartInstance.dispose()
}
chartInstance = echarts.init(el)
const pieData = vList.value.map(item => {
const originalValue = item.index01 || 0
const wanValue = (originalValue / 10000).toFixed(2)
return {
name: item.xaxis || '未知分类',
value: Number(wanValue),
originalValue: originalValue
}
}).filter(item => item.value > 0)
const colorList = [
'#409EFF', '#36CFc9', '#67C23A', '#E6A23C', '#F56C6C',
'#909399', '#722ED1', '#EB2F96', '#1890FF', '#52C41A',
'#FAAD14', '#F5222D', '#8C8C8C', '#A062D4', '#F7BA1E'
]
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70', fontSize: 12 },
padding: [10, 15],
borderRadius: 6,
formatter: function(params) {
return `分类:${params.name}<br/>收入:${params.data.originalValue}元<br/>占比:${params.percent.toFixed(1)}%`
}
const props = defineProps({
formParams: {
type: Object,
default: () => ({}),
},
legend: {
orient: 'horizontal',
top: '10%',
left: 'center',
textStyle: { fontSize: 11, color: '#e0e6ff' },
itemWidth: 12,
itemHeight: 12,
itemGap: 10,
pageIconColor: '#409EFF',
pageTextStyle: { color: '#e0e6ff', fontSize: 10 },
pageButtonItemGap: 6,
pageButtonGap: 10,
type: 'scroll'
});
const vList = ref<ChartDataItem[]>([]);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const resizeHandler = () => chartInstance?.resize();
const parseAmount = (value?: string | number) => {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) ? parsed : 0;
};
async function getList() {
try {
const params = {
...props.formParams,
flowType: '2',
};
const res = await CategoryChart(params);
vList.value = res || [];
} catch (error) {
console.error('获取收入数据失败:', error);
vList.value = [];
}
}
const initPieChart = () => {
const el = chartRef.value;
if (!el) return;
if (chartInstance) {
chartInstance.dispose();
}
chartInstance = echarts.init(el);
const pieData = vList.value
.map((item) => {
const originalValue = parseAmount(item.value01);
const wanValue = (originalValue / 10000).toFixed(2);
return {
name: item.axisName || '未知分类',
value: Number(wanValue),
originalValue: originalValue,
};
})
.filter((item) => item.value > 0);
const colorList = [
'#409EFF',
'#36CFc9',
'#67C23A',
'#E6A23C',
'#F56C6C',
'#909399',
'#722ED1',
'#EB2F96',
'#1890FF',
'#52C41A',
'#FAAD14',
'#F5222D',
'#8C8C8C',
'#A062D4',
'#F7BA1E',
];
const option = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70', fontSize: 12 },
padding: [10, 15],
borderRadius: 6,
formatter: function (params) {
return `分类:${params.name}<br/>收入:${params.data.originalValue}元<br/>占比:${params.percent.toFixed(1)}%`;
},
},
legend: {
orient: 'horizontal',
top: '10%',
left: 'center',
textStyle: { fontSize: 11, color: '#e0e6ff' },
itemWidth: 12,
itemHeight: 12,
itemGap: 10,
pageIconColor: '#409EFF',
pageTextStyle: { color: '#e0e6ff', fontSize: 10 },
pageButtonItemGap: 6,
pageButtonGap: 10,
type: 'scroll',
},
series: [
{
name: '收入',
type: 'pie',
radius: ['30%', '55%'],
center: ['50%', '65%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 4,
borderColor: 'rgba(15, 52, 96, 0.9)',
borderWidth: 1,
},
label: {
show: true,
position: 'outside',
fontSize: 10,
color: '#e0e6ff',
formatter: '{b} {c}万元 ({d}%)',
overflow: 'truncate',
ellipsis: '...',
distance: 8,
},
labelLine: {
show: true,
length: 12,
length2: 8,
lineStyle: { color: '#e0e6ff', width: 1 },
smooth: 0.2,
minTurnAngle: 45,
},
data: pieData,
color: colorList,
},
],
};
chartInstance.setOption(option);
};
watch(
() => props.formParams,
() => {
getList();
},
series: [
{
name: '收入',
type: 'pie',
radius: ['30%', '55%'],
center: ['50%', '65%'],
avoidLabelOverlap: true,
itemStyle: {
borderRadius: 4,
borderColor: 'rgba(15, 52, 96, 0.9)',
borderWidth: 1
},
label: {
show: true,
position: 'outside',
fontSize: 10,
color: '#e0e6ff',
formatter: '{b} {c}万元 ({d}%)',
overflow: 'truncate',
ellipsis: '...',
distance: 8
},
labelLine: {
show: true,
length: 12,
length2: 8,
lineStyle: { color: '#e0e6ff', width: 1 },
smooth: 0.2,
minTurnAngle: 45
},
data: pieData,
color: colorList
}
]
}
{ deep: true, immediate: true },
);
chartInstance.setOption(option)
}
watch(
vList,
() => {
initPieChart();
},
{ deep: true },
);
watch(
() => props.formParams,
() => {
getList()
},
{ deep: true, immediate: true }
)
onMounted(() => {
window.addEventListener('resize', resizeHandler);
});
watch(vList, () => {
initPieChart()
}, { deep: true })
onMounted(() => {
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style scoped>
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/box/16.png") no-repeat;
background-size: 100% 100%;
}
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/box/16.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/title/23.png") no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/title/23.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409EFF;
letter-spacing: 0.5px;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409eff;
letter-spacing: 0.5px;
}
.pie-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
margin: 0;
padding: 0;
}
.pie-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
margin: 0;
padding: 0;
}
:deep(.echarts-tooltip) {
background-color: rgba(145, 200, 255, 0.9) !important;
border-color: #409EFF !important;
color: #0a3b70 !important;
border-radius: 6px !important;
}
:deep(.echarts-tooltip) {
background-color: rgba(145, 200, 255, 0.9) !important;
border-color: #409eff !important;
color: #0a3b70 !important;
border-radius: 6px !important;
}
:deep(.echarts-legend-scroll) {
background-color: transparent !important;
}
:deep(.echarts-legend-scroll-text) {
color: #e0e6ff !important;
font-size: 11px !important;
}
:deep(.echarts-legend-scroll-button) {
border-color: #1a508b !important;
}
:deep(.echarts-legend-scroll-button-icon) {
color: #409EFF !important;
}
:deep(.echarts-legend-scroll) {
background-color: transparent !important;
}
:deep(.echarts-legend-scroll-text) {
color: #e0e6ff !important;
font-size: 11px !important;
}
:deep(.echarts-legend-scroll-button) {
border-color: #1a508b !important;
}
:deep(.echarts-legend-scroll-button-icon) {
color: #409eff !important;
}
:deep(.ec-label) {
z-index: 9999 !important;
white-space: nowrap !important;
font-size: 10px !important;
color: #e0e6ff !important;
}
:deep(.ec-label) {
z-index: 9999 !important;
white-space: nowrap !important;
font-size: 10px !important;
color: #e0e6ff !important;
}
:deep(.ec-label-line) {
stroke: #e0e6ff !important;
stroke-width: 1px !important;
}
</style>
:deep(.ec-label-line) {
stroke: #e0e6ff !important;
stroke-width: 1px !important;
}
</style>

View File

@@ -7,225 +7,236 @@
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
import * as echarts from 'echarts'
// import { getItemInfoList } from '@/api/bizApi'
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import * as echarts from 'echarts';
import { ChartDataItem, ErpMonthChart } from '@jeesite/erp/api/erp/screen';
const props = defineProps({
formParams: {
type: Object,
default: () => ({})
}
});
const props = defineProps({
formParams: {
type: Object,
default: () => ({}),
},
});
const vList = ref([])
const chartRef = ref(null)
let chartInstance = null
const resizeHandler = () => chartInstance?.resize()
const vList = ref<ChartDataItem[]>([]);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const resizeHandler = () => chartInstance?.resize();
async function getList() {
try {
const params = {
...props.formParams,
itemCode: 'ERP_YEARPSAV_M001',
const parseAmount = (value?: string | number) => {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) ? parsed : 0;
};
const parseRate = (value?: string | number) => {
const parsed = Number(value ?? 0);
return Number.isFinite(parsed) ? Number(parsed.toFixed(2)) : 0;
};
async function getList() {
try {
const params = {
...props.formParams,
};
const res = await ErpMonthChart(params);
vList.value = res || [];
} catch (error) {
console.error(error);
vList.value = [];
}
const res = await getItemInfoList(params)
vList.value = res || []
} catch (error) {
console.error(error)
vList.value = []
}
}
function initChart() {
const el = chartRef.value
if (!el) return
if (!chartInstance) {
chartInstance = echarts.init(el)
}
const baseOption = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70' },
padding: [8, 12],
borderRadius: 6
},
legend: {
top: '10',
left: 'center',
textStyle: { fontSize: 12, color: '#e0e6ff' },
data: ['收入', '支出', '利润率', '净利润']
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
top: '15%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: [],
axisLabel: {
fontSize: 11,
interval: 0,
color: '#b4c7e7'
},
axisLine: { lineStyle: { color: '#1a508b' } },
boundaryGap: true
}
],
yAxis: [
{
type: 'value',
name: '金额 (万元)',
nameTextStyle: { fontSize: 12, color: '#b4c7e7' },
axisLabel: { formatter: '{value}', color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.3)' } }
},
{
type: 'value',
name: '利润率 (%)',
nameTextStyle: { fontSize: 12, color: '#b4c7e7' },
axisLabel: { formatter: '{value} %', color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.2)' } },
min: 'dataMin',
max: 'dataMax',
scale: true
}
],
series: [
{
name: '收入',
type: 'bar',
yAxisIndex: 0,
data: [],
barWidth: '12%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#85E868' },
{ offset: 1, color: '#67C23A' }
]),
borderRadius: [8, 8, 0, 0]
},
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#fff',
formatter: '{c}'
}
},
{
name: '支出',
type: 'bar',
yAxisIndex: 0,
data: [],
barWidth: '12%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FF8A8A' },
{ offset: 1, color: '#F56C6C' }
]),
borderRadius: [8, 8, 0, 0]
},
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#fff',
formatter: '{c}'
}
},
{
name: '利润率',
type: 'line',
yAxisIndex: 1,
data: [],
smooth: true,
lineStyle: { width: 1.5, color: '#409EFF' },
symbol: 'circle',
symbolSize: 5,
label: {
show: true,
position: 'outside',
fontSize: 10,
color: '#409EFF',
formatter: '{c}',
offset: [0, -5]
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.0)' }
])
},
emphasis: {
lineStyle: { width: 2 },
symbolSize: 7
}
},
{
name: '净利润',
type: 'line',
yAxisIndex: 0,
data: [],
smooth: true,
lineStyle: { width: 1.5, color: '#FF9D28' },
symbol: 'circle',
symbolSize: 5,
label: {
show: true,
fontSize: 10,
color: '#FF9D28',
formatter: '{c}'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 157, 40, 0.3)' },
{ offset: 1, color: 'rgba(255, 157, 40, 0)' }
])
},
emphasis: {
lineStyle: { width: 2 },
symbolSize: 7
}
}
]
}
function initChart() {
const el = chartRef.value;
if (!el) return;
if (vList.value.length > 0) {
const xData = vList.value.map(item => {
const xaxis = item.xaxis || ''
return xaxis.includes('月') ? xaxis : `${xaxis}`;
})
const index01Yuan = vList.value.map(item => item.index01 || 0)
const index02Yuan = vList.value.map(item => item.index02 || 0)
const index03 = vList.value.map(item => item.index03 || 0)
const index04Yuan = vList.value.map(item => item.index04 || 0)
if (!chartInstance) {
chartInstance = echarts.init(el);
}
const index01Wan = index01Yuan.map(val => (val / 10000).toFixed(2))
const index02Wan = index02Yuan.map(val => (val / 10000).toFixed(2))
const index04Wan = index04Yuan.map(val => (val / 10000).toFixed(2))
const baseOption: echarts.EChartsOption & {
tooltip: echarts.TooltipComponentOption;
xAxis: Array<echarts.XAXisComponentOption & { data: string[] }>;
series: echarts.SeriesOption[];
} = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' },
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70' },
padding: [8, 12],
borderRadius: 6,
},
legend: {
top: '10',
left: 'center',
textStyle: { fontSize: 12, color: '#e0e6ff' },
data: ['收入', '支出', '利润率', '净利润'],
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
top: '15%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: [],
axisLabel: {
fontSize: 11,
interval: 0,
color: '#b4c7e7',
},
axisLine: { lineStyle: { color: '#1a508b' } },
boundaryGap: true,
},
],
yAxis: [
{
type: 'value',
name: '金额 (万元)',
nameTextStyle: { fontSize: 12, color: '#b4c7e7' },
axisLabel: { formatter: '{value}', color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.3)' } },
},
{
type: 'value',
name: '利润率 (%)',
nameTextStyle: { fontSize: 12, color: '#b4c7e7' },
axisLabel: { formatter: '{value} %', color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.2)' } },
min: 'dataMin',
max: 'dataMax',
scale: true,
},
],
series: [
{
name: '收入',
type: 'bar',
yAxisIndex: 0,
data: [],
barWidth: '12%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#85E868' },
{ offset: 1, color: '#67C23A' },
]),
borderRadius: [8, 8, 0, 0],
},
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#fff',
formatter: '{c}',
},
},
{
name: '支出',
type: 'bar',
yAxisIndex: 0,
data: [],
barWidth: '12%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#FF8A8A' },
{ offset: 1, color: '#F56C6C' },
]),
borderRadius: [8, 8, 0, 0],
},
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#fff',
formatter: '{c}',
},
},
{
name: '利润率',
type: 'line',
yAxisIndex: 1,
data: [],
smooth: true,
lineStyle: { width: 1.5, color: '#409EFF' },
symbol: 'circle',
symbolSize: 5,
label: {
show: true,
position: 'top',
fontSize: 10,
color: '#409EFF',
formatter: '{c}',
offset: [0, -5],
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.0)' },
]),
},
emphasis: {
focus: 'series',
},
},
{
name: '净利润',
type: 'line',
yAxisIndex: 0,
data: [],
smooth: true,
lineStyle: { width: 1.5, color: '#FF9D28' },
symbol: 'circle',
symbolSize: 5,
label: {
show: true,
fontSize: 10,
color: '#FF9D28',
formatter: '{c}',
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 157, 40, 0.3)' },
{ offset: 1, color: 'rgba(255, 157, 40, 0)' },
]),
},
emphasis: {
focus: 'series',
},
},
],
};
baseOption.tooltip.formatter = function (params) {
const title = params[0].axisValue
const idx = params[0].dataIndex
const incomeVal = index01Yuan[idx]
const expenseVal = index02Yuan[idx]
const rateVal = index03[idx]
const profitVal = index04Yuan[idx]
return `
if (vList.value?.length > 0) {
const xData = vList.value.map((item) => {
const axisName = item.axisName || '';
return axisName.includes('月') ? axisName : `${axisName}`;
});
const index01Yuan = vList.value.map((item) => parseAmount(item.value01));
const index02Yuan = vList.value.map((item) => parseAmount(item.value02));
const index03 = vList.value.map((item) => parseRate(item.value03));
const index04Yuan = vList.value.map((item) => parseAmount(item.value04));
const index01Wan = index01Yuan.map((val) => Number((val / 10000).toFixed(2)));
const index02Wan = index02Yuan.map((val) => Number((val / 10000).toFixed(2)));
const index04Wan = index04Yuan.map((val) => Number((val / 10000).toFixed(2)));
baseOption.tooltip.formatter = function (params) {
const title = params[0].axisValue;
const idx = params[0].dataIndex;
const incomeVal = index01Yuan[idx];
const expenseVal = index02Yuan[idx];
const rateVal = index03[idx];
const profitVal = index04Yuan[idx];
return `
<div style="text-align:center; font-weight:bold; margin-bottom:6px">${title}</div>
<table style="width:100%; border-collapse:collapse; text-align:center">
<tr>
@@ -241,78 +252,82 @@ function initChart() {
<td style="border:1px solid #409EFF; padding:4px">${profitVal}</td>
</tr>
</table>
`
`;
};
baseOption.xAxis[0].data = xData;
baseOption.series[0].data = index01Wan;
baseOption.series[1].data = index02Wan;
baseOption.series[2].data = index03;
baseOption.series[3].data = index04Wan;
}
baseOption.xAxis[0].data = xData
baseOption.series[0].data = index01Wan
baseOption.series[1].data = index02Wan
baseOption.series[2].data = index03
baseOption.series[3].data = index04Wan
chartInstance.setOption(baseOption, true);
}
chartInstance.setOption(baseOption, true)
}
watch(
() => props.formParams,
() => {
getList();
},
{ deep: true, immediate: true },
);
watch(
() => props.formParams,
() => {
getList()
},
{ deep: true, immediate: true }
)
watch(
vList,
() => {
nextTick(() => initChart());
},
{ deep: true },
);
watch(vList, () => {
nextTick(() => initChart())
}, { deep: true })
onMounted(() => {
initChart();
window.addEventListener('resize', resizeHandler);
});
onMounted(() => {
initChart()
window.addEventListener('resize', resizeHandler)
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style scoped>
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/box/16.png") no-repeat;
background-size: 100% 100%;
}
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/box/16.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/title/03.png") no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/title/03.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409EFF;
letter-spacing: 0.5px;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409eff;
letter-spacing: 0.5px;
}
.bar-line-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
}
</style>
.bar-line-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
}
</style>

View File

@@ -7,202 +7,208 @@
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as echarts from 'echarts'
// import { getItemInfoList } from '@/api/bizApi'
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { ChartDataItem, CategoryChart } from '@jeesite/erp/api/erp/screen';
const props = defineProps({
formParams: {
type: Object,
default: () => ({})
}
})
const props = defineProps({
formParams: {
type: Object,
default: () => ({}),
},
});
const vList = ref([])
const chartRef = ref(null)
let chartInstance = null
const resizeHandler = () => chartInstance?.resize()
const vList = ref<ChartDataItem[]>([]);
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
const resizeHandler = () => chartInstance?.resize();
async function getList() {
try {
const params = {
...props.formParams,
itemCode: 'ERP_YEARRANK_Y001',
async function getList() {
try {
const params = {
...props.formParams,
flowType: '1',
};
const res = await CategoryChart(params);
vList.value = res || [];
} catch (error) {
console.error(error);
vList.value = [];
}
const res = await getItemInfoList(params)
vList.value = res || []
} catch (error) {
console.error(error)
vList.value = []
}
}
const initRankChart = () => {
const el = chartRef.value
if (!el) return
if (chartInstance) {
chartInstance.dispose()
}
chartInstance = echarts.init(el)
const initRankChart = () => {
const el = chartRef.value;
if (!el) return;
const sortedList = [...vList.value]
.sort((a, b) => Number(b.index01 || 0) - Number(a.index01 || 0))
.slice(0, 10)
if (chartInstance) {
chartInstance.dispose();
}
const yData = sortedList.map((item, index) => `${index + 1}. ${item.xaxis || '未知'}`)
const valueData = sortedList.map(item => Number((Number(item.index01 || 0) / 10000).toFixed(2)) || 0)
const percentData = sortedList.map(item => Number(Number(item.index02 || 0).toFixed(2)) || 0)
const originalData = sortedList.map(item => Number(item.index01 || 0))
chartInstance = echarts.init(el);
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70' },
padding: [8, 12],
borderRadius: 6,
formatter: params => {
const idx = params[0].dataIndex
const name = params[0].name
const amount = originalData[idx]
const rate = percentData[idx]
return `
<div style="text-align:center; font-weight:bold; margin-bottom:6px">${name}</div>
<table style="width:100%; border-collapse:collapse; text-align:center">
<tr>
<td style="border:1px solid #409EFF; padding:4px; font-weight:bold">支出(元)</td>
<td style="border:1px solid #409EFF; padding:4px; font-weight:bold">占比(%)</td>
</tr>
<tr>
<td style="border:1px solid #409EFF; padding:4px">${amount}</td>
<td style="border:1px solid #409EFF; padding:4px">${rate}</td>
</tr>
</table>
`
}
},
legend: {
show: false
},
grid: {
left: '5%',
right: '15%',
bottom: '10%',
top: '15%',
containLabel: true
},
xAxis: [
{
type: 'value',
axisLabel: { fontSize: 11, color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.3)' } }
}
],
yAxis: [
{
type: 'category',
data: yData,
axisLabel: { fontSize: 11, color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
inverse: true
}
],
series: [
{
name: '支出',
type: 'bar',
barWidth: '12%',
data: valueData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#409EFF' },
{ offset: 0, color: '#409EFF' },
{ offset: 1, color: '#1890FF' }
])
const sortedList = [...vList.value].sort((a, b) => Number(b.value01 || 0) - Number(a.value01 || 0)).slice(0, 10);
const yData = sortedList.map((item, index) => `${index + 1}. ${item.axisName || '未知'}`);
const valueData = sortedList.map((item) => Number((Number(item.value01 || 0) / 10000).toFixed(2)) || 0);
const percentData = sortedList.map((item) => Number(Number(item.value02 || 0).toFixed(2)) || 0);
const originalData = sortedList.map((item) => Number(item.value01 || 0));
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(145, 200, 255, 0.9)',
borderColor: '#409EFF',
borderWidth: 1,
textStyle: { color: '#0a3b70' },
padding: [8, 12],
borderRadius: 6,
formatter: (params) => {
const idx = params[0].dataIndex;
const name = params[0].name;
const amount = originalData[idx];
const rate = percentData[idx];
return `
<div style="text-align:center; font-weight:bold; margin-bottom:6px">${name}</div>
<table style="width:100%; border-collapse:collapse; text-align:center">
<tr>
<td style="border:1px solid #409EFF; padding:4px; font-weight:bold">支出(元)</td>
<td style="border:1px solid #409EFF; padding:4px; font-weight:bold">占比(%)</td>
</tr>
<tr>
<td style="border:1px solid #409EFF; padding:4px">${amount}</td>
<td style="border:1px solid #409EFF; padding:4px">${rate}</td>
</tr>
</table>
`;
},
label: {
show: true,
position: 'right',
fontSize: 10,
color: '#e0e6ff',
formatter: params => `${params.value} 万元 (${percentData[params.dataIndex]}%)`
}
}
]
}
},
legend: {
show: false,
},
grid: {
left: '5%',
right: '15%',
bottom: '10%',
top: '15%',
containLabel: true,
},
xAxis: [
{
type: 'value',
axisLabel: { fontSize: 11, color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
splitLine: { lineStyle: { color: 'rgba(26, 80, 139, 0.3)' } },
},
],
yAxis: [
{
type: 'category',
data: yData,
axisLabel: { fontSize: 11, color: '#b4c7e7' },
axisLine: { lineStyle: { color: '#1a508b' } },
inverse: true,
},
],
series: [
{
name: '支出',
type: 'bar',
barWidth: '12%',
data: valueData,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#409EFF' },
{ offset: 0, color: '#409EFF' },
{ offset: 1, color: '#1890FF' },
]),
},
label: {
show: true,
position: 'right',
fontSize: 10,
color: '#e0e6ff',
formatter: (params) => `${params.value} 万元 (${percentData[params.dataIndex]}%)`,
},
},
],
};
chartInstance.setOption(option)
}
chartInstance.setOption(option);
};
watch(() => props.formParams, () => {
getList()
}, { deep: true, immediate: true })
watch(
() => props.formParams,
() => {
getList();
},
{ deep: true, immediate: true },
);
watch(vList, () => {
initRankChart()
}, { deep: true })
watch(
vList,
() => {
initRankChart();
},
{ deep: true },
);
onMounted(() => {
initRankChart()
window.addEventListener('resize', resizeHandler)
})
onMounted(() => {
initRankChart();
window.addEventListener('resize', resizeHandler);
});
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler)
if (chartInstance) {
chartInstance.dispose()
chartInstance = null
}
})
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style scoped>
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/box/16.png") no-repeat;
background-size: 100% 100%;
}
.chart-card {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/box/16.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url("@/assets/chart/title/23.png") no-repeat;
background-size: 100% 100%;
}
.chart-card-header {
height: 40px;
line-height: 40px;
padding: 0 16px;
background-color: rgba(26, 80, 139, 0.5);
border-bottom: 1px solid #1a508b;
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.1) url('@jeesite/assets/chart/title/23.png') no-repeat;
background-size: 100% 100%;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409EFF;
letter-spacing: 0.5px;
}
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: #409eff;
letter-spacing: 0.5px;
}
.rank-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
}
.rank-chart-container {
flex: 1;
width: 100%;
height: calc(100% - 40px);
}
:deep(.echarts-tooltip) {
background-color: rgba(145, 200, 255, 0.9) !important;
border-color: #409EFF !important;
color: #0a3b70 !important;
border-radius: 6px !important;
}
</style>
:deep(.echarts-tooltip) {
background-color: rgba(145, 200, 255, 0.9) !important;
border-color: #409eff !important;
color: #0a3b70 !important;
border-radius: 6px !important;
}
</style>

View File

@@ -93,7 +93,7 @@ const route = useRoute();
// 1. 完善类型定义,避免 any 类型
const FormValues = ref({
reqParam: route.query.year || ''
yearDate: route.query.yearDate || ''
});
// 2. 定义组件映射的类型
@@ -143,9 +143,9 @@ async function getChartList() {
// 7. 监听路由参数变化
watch(
() => route.query.year,
() => route.query.yearDate,
(newVal) => {
FormValues.value.reqParam = newVal || '';
FormValues.value.yearDate = newVal || '';
},
{
deep: true,

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2013-Now https://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 } from '@jeesite/core/api/model/baseModel';
const { adminPath } = useGlobSetting();
export interface ErpTransactionFlow extends BasicModel<ErpTransactionFlow> {
createTime?: string; // 记录时间
flowId?: string; // 流水
flowName: string; // 交易名称
flowType: string; // 交易类型
amount: number; // 交易金额
tradeTime: string; // 交易时间
accountId: string; // 交易账户
categoryId: string; // 交易分类
remark?: string; // 交易备注
updateTime?: string; // 更新时间
businessId: string; // 业务标识
yearDate: string; //年份
monthDate: string; //月份
}
export interface ChartDataItem extends BasicModel<ChartDataItem> {
itemCode: string; // 项目编号
soring: number; // 序号
axisName: string; // X轴名称
value01: string; // 指标01
value02: string; // 指标02
value03: string; // 指标03
value04: string; // 指标04
value05: string; // 指标05
value06: string; // 指标06
value07: string; // 指标07
value08: string; // 指标08
value09: string; // 指标09
value10: string; // 指标10
value11: string; // 指标11
value12: string; // 指标12
value13: string; // 指标13
value14: string; // 指标14
value15: string; // 指标15
bizKey: string; // 业务主键编号
indexSum: number; // 指标求和
indexAvg: number; // 指标平均
indexMax: number; // 指标最大
indexMin: number; // 指标最小
}
export const ErpMonthChart = (params?: ErpTransactionFlow | any) =>
defHttp.get<ChartDataItem[]>({ url: adminPath + '/erp/screen/getErpMonthChart', params });
export const CategoryChart = (params?: ErpTransactionFlow | any) =>
defHttp.get<ChartDataItem[]>({ url: adminPath + '/erp/screen/getCategoryChart', params });
export const ErpAccountChart = (params?: ErpTransactionFlow | any) =>
defHttp.get<ChartDataItem[]>({ url: adminPath + '/erp/screen/getErpAccountChart', params });