新增待办信息

This commit is contained in:
2026-01-01 18:10:00 +08:00
parent 7a44cf1eea
commit 971e3d86f1
11 changed files with 361 additions and 83 deletions

View File

@@ -134,13 +134,13 @@ public class vDate {
return switch (cycleType) {
case "D" ->
// 日最近30天格式 yyyy-MM-dd
baseDate.minusDays(7).format(DAY_FORMATTER);
baseDate.minusDays(14).format(DAY_FORMATTER);
case "M" ->
// 月最近6个月格式 yyyy-MM
baseDate.minusMonths(6).format(MONTH_FORMATTER);
baseDate.minusMonths(12).format(MONTH_FORMATTER);
case "Q" ->
// 季度最近3年格式 yyyy-Qx
getQuarterCycleCode(baseDate.minusYears(2));
getQuarterCycleCode(baseDate.minusYears(3));
case "Y" ->
// 年最近6年格式 yyyy
String.valueOf(baseDate.minusYears(6).getYear());

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

View File

@@ -5,34 +5,64 @@
总金额<span class="amount-value">{{ totalAmountText }}</span>
</div>
</template>
<div ref="chartDom" style="width: 100%; height: 300px;"></div>
<div class="layout-container">
<div class="left-panel">
<div class="stat-card">
<span class="stat-title">本月收入</span>
<div class="stat-content">
<Icon icon="icons/erp-income.png" size="36"/>
<span class="stat-value">{{ thisIncome }} </span>
</div>
</div>
<div class="stat-card">
<span class="stat-title">本月支出</span>
<div class="stat-content">
<Icon icon="icons/erp-expense.png" size="36"/>
<span class="stat-value">{{ thisExpense }} </span>
</div>
</div>
<div class="stat-card">
<span class="stat-title">消费占比</span>
<div class="stat-content">
<Icon icon="icons/erp-share.png" size="36"/>
<span class="stat-value">{{ thisShare }} %</span>
</div>
</div>
</div>
<div class="right-panel">
<div ref="chartDom" class="chart-container"></div>
</div>
</div>
</Card>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { Card } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import { ErpAccount, erpAccountListAll } from '@jeesite/erp/api/erp/account';
import { ErpIncExpRatio, erpIncExpRatioListAll } from '@jeesite/erp/api/erp/incExpRatio';
import * as echarts from 'echarts';
import type { ECharts, EChartsOption, LegendSelectedChangedParams } from 'echarts';
import type { ECharts, EChartsOption } from 'echarts';
const listAccount = ref<ErpAccount[]>([]);
const chartDom = ref<HTMLDivElement | null>(null);
let myChart: ECharts | null = null;
// 核心修复1新增「是否全取消」的标记 + 维护所有图例名称的集合
const allLegendNames = ref<Set<string>>(new Set()); // 存储所有账户名称(图例名)
const selectedLegends = ref<Set<string>>(new Set()); // 选中的图例
const isAllUnselected = ref(false); // 标记是否所有图例都被取消选中
const thisIncome = ref(0);
const thisExpense = ref(0);
const thisShare = ref(0);
// 修复后的总金额计算逻辑
// 核心标记:是否全取消 + 维护所有图例名称的集合
const allLegendNames = ref<Set<string>>(new Set());
const selectedLegends = ref<Set<string>>(new Set());
const isAllUnselected = ref(false);
// 总金额计算逻辑
const totalAmountText = computed(() => {
// 1. 所有图例都取消选中时,显示 0 元
if (isAllUnselected.value) {
return '0.00';
}
// 2. 初始状态(全选):显示全部总金额
if (selectedLegends.value.size === 0 && !isAllUnselected.value) {
const total = listAccount.value.reduce(
(sum, item) => sum + (Number(item.currentBalance) || 0),
@@ -41,7 +71,6 @@ const totalAmountText = computed(() => {
return total.toFixed(2);
}
// 3. 部分选中:显示选中账户的合计
const total = listAccount.value
.filter(item => selectedLegends.value.has(item.accountName || '未知账户'))
.reduce((sum, item) => sum + (Number(item.currentBalance) || 0), 0);
@@ -53,6 +82,7 @@ const formatPercent = (value: number) => {
return `${value.toFixed(1)}%`;
};
// 优化颜色生成逻辑,增加颜色复用规则
const generateRandomColors = (count: number): string[] => {
const colorLibrary = [
'#1890ff', '#52c41a', '#f5a623', '#fa8c16', '#722ed1', '#eb2f96',
@@ -61,20 +91,37 @@ const generateRandomColors = (count: number): string[] => {
];
if (count <= colorLibrary.length) {
const shuffled = [...colorLibrary].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
return colorLibrary.slice(0, count);
}
const colors: string[] = [];
for (let i = 0; i < count; i++) {
const colors: string[] = [...colorLibrary];
while (colors.length < count) {
const hue = Math.floor(Math.random() * 360);
const saturation = 70 + Math.floor(Math.random() * 20);
const lightness = 45 + Math.floor(Math.random() * 15);
colors.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
colors.push(`hsl(${hue}, 75%, 55%)`);
}
return colors;
};
const getThisData = async() => {
try {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const formattedMonth = month.toString().padStart(2, '0');
const currentMonthStr = `${year}-${formattedMonth}`;
const params = {
cycleType: 'M',
statDate: currentMonthStr ,
}
const result = await erpIncExpRatioListAll(params);
thisIncome.value = result[0]?.incomeAmount || 0;
thisExpense.value = result[0]?.expenseAmount || 0;
thisShare.value = result[0]?.expenseRatio || 0;
} catch (error) {
console.error('获取数据失败:', error);
}
}
const fetchList = async () => {
try {
const result = await erpAccountListAll();
@@ -85,21 +132,18 @@ const fetchList = async () => {
}
};
// 核心修复2重构图例选中事件处理逻辑
const handleLegendSelect = (params: LegendSelectedChangedParams) => {
// 图例选中事件处理
const handleLegendSelect = (params: any) => {
const { name, selected } = params;
// 1. 更新选中集合
if (selected[name]) {
selectedLegends.value.add(name);
} else {
selectedLegends.value.delete(name);
}
// 2. 判断是否所有图例都被取消选中
isAllUnselected.value = selectedLegends.value.size === 0 && allLegendNames.value.size > 0;
// 3. 强制更新图表(确保饼图和总金额同步)
// 同步更新图表数据
myChart?.setOption({
series: [
{
@@ -113,6 +157,7 @@ const handleLegendSelect = (params: LegendSelectedChangedParams) => {
});
};
// 初始化图表(适配新布局)
const initChart = () => {
if (!chartDom.value || listAccount.value.length === 0) return;
@@ -130,9 +175,7 @@ const initChart = () => {
const validData = pieData.filter(item => item.value > 0);
const colors = generateRandomColors(validData.length);
// 核心修复3初始化所有图例名称集合
allLegendNames.value = new Set(validData.map(item => item.name));
const total = validData.reduce((sum, item) => sum + item.value, 0);
const option: EChartsOption = {
@@ -206,10 +249,9 @@ const initChart = () => {
};
myChart.setOption(option);
myChart.on('legendselectchanged', handleLegendSelect);
// 初始化选中状态:全选 + 标记非全取消
// 初始化选中状态
selectedLegends.value = new Set(validData.map(item => item.name));
isAllUnselected.value = false;
};
@@ -220,7 +262,11 @@ const resizeChart = () => {
onMounted(async () => {
await fetchList();
initChart();
await getThisData();
// 增加微延迟确保DOM渲染完成
setTimeout(() => {
initChart();
}, 100);
window.addEventListener('resize', resizeChart);
});
@@ -254,6 +300,106 @@ onUnmounted(() => {
font-weight: 600;
}
/* 核心布局样式 */
.layout-container {
display: flex;
width: 100%;
height: 300px;
gap: 16px;
padding: 8px 0;
}
/* 左侧20%面板 - 精简版 */
.left-panel {
width: 15%;
height: 100%;
border-radius: 8px;
padding: 8px;
overflow-y: auto;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 8px; /* 缩小卡片间距 */
}
/* 统计卡片样式 - 更小尺寸 */
.stat-card {
background-color: #f0f7ff; /* 淡蓝色背景 */
border-radius: 6px;
padding: 10px 8px; /* 缩小内边距 */
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
border: 1px solid #e6f4ff;
}
/* 统计标题 - 简化为span */
.stat-title {
font-size: 13px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px; /* 缩小间距 */
text-align: left;
line-height: 1.2;
}
/* 统计内容(图标+金额)- 关键优化 */
.stat-content {
display: flex;
align-items: center;
justify-content: space-between; /* 改为两端对齐 */
width: 100%; /* 确保占满容器宽度 */
padding: 0 4px; /* 增加左右内边距 */
}
/* 图标容器 - 确保图标靠左对齐 */
:deep(.stat-content .icon) {
display: flex;
align-items: center;
justify-content: flex-start;
flex-shrink: 0; /* 防止图标被压缩 */
}
/* 统计数值 - 确保数字靠右对齐 */
.stat-value {
font-size: 12px; /* 缩小字体 */
font-weight: 600;
color: #1890ff;
white-space: nowrap;
text-align: right; /* 文字右对齐 */
flex-shrink: 0; /* 防止数字被压缩 */
}
/* 右侧80%图表面板 */
.right-panel {
width: 85%;
height: 100%;
position: relative;
}
.chart-container {
width: 100%;
height: 100%;
border-radius: 8px;
}
/* 滚动条样式优化 */
:deep(.left-panel::-webkit-scrollbar) {
width: 4px; /* 缩小滚动条 */
}
:deep(.left-panel::-webkit-scrollbar-thumb) {
background: #d9d9d9;
border-radius: 2px;
}
:deep(.left-panel::-webkit-scrollbar-track) {
background: #f5f5f5;
}
/* 图表样式优化 */
:deep(.ant-card .echarts-legend-item) {
margin: 0 8px !important;
padding: 0 5px;
@@ -278,4 +424,40 @@ onUnmounted(() => {
font-size: 14px;
color: #999;
}
/* 响应式适配 */
@media (max-width: 768px) {
.layout-container {
flex-direction: column;
height: auto;
}
.left-panel, .right-panel {
width: 100%;
}
.left-panel {
flex-direction: row;
height: 140px; /* 缩小高度 */
overflow-x: auto;
padding: 6px;
}
.stat-card {
min-width: 120px; /* 缩小最小宽度 */
flex: none;
padding: 8px 6px;
}
.right-panel {
height: 300px;
margin-top: 12px;
}
/* 移动端保持图标左、数字右对齐 */
.stat-content {
justify-content: space-between;
padding: 0 2px;
}
}
</style>

View File

@@ -1,69 +1,137 @@
<template>
<PageWrapper class="dashboard-container">
<template #extra>
<BasicForm
:labelWidth="100"
:schemas="schemaForm.schemas"
style="width: 245px;"
/>
</template>
<div class="top-header-section">
<div class="header-icon-wrapper">
<img :src="headerImg" class="header-img" />
</div>
<BasicForm
:labelWidth="100"
:schemas="schemaForm.schemas"
class="search-form"
/>
</div>
<div class="two-column-layout">
<ChartPie />
<ChartLineRatio :formParams="FormValues" />
<ChartLineRatio :formParams="FormValues" />
</div>
<div class="two-column-layout">
<ChartBarCycle :formParams="FormValues" />
<ChartBarAccount :formParams="FormValues" />
</div>
<ChartLineType :formParams="FormValues" />
<div class="two-column-layout">
<ChartLineExp :formParams="FormValues" />
<ChartLineInc :formParams="FormValues" />
</div>
<ChartLineType :formParams="FormValues" />
<div class="two-column-layout">
<ChartLineExp :formParams="FormValues" />
<ChartLineInc :formParams="FormValues" />
</div>
</PageWrapper>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { Card } from 'ant-design-vue';
import { BasicForm, FormSchema, FormProps } from '@jeesite/core/components/Form';
import { PageWrapper } from '@jeesite/core/components/Page';
import ChartPie from './components/ChartPie.vue';
import ChartLineExp from './components/ChartLineExp.vue';
import ChartLineInc from './components/ChartLineInc.vue';
import ChartBarCycle from './components/ChartBarCycle.vue';
import ChartBarAccount from './components/ChartBarAccount.vue';
import ChartLineType from './components/ChartLineType.vue';
import ChartLineRatio from './components/ChartLineRatio.vue';
const FormValues = ref<Record<string, any>>({
cycleType: 'M'
});
import { ref, onMounted, onUnmounted } from 'vue';
import { Card } from 'ant-design-vue';
import { Icon } from '@jeesite/core/components/Icon';
import { BasicForm, FormSchema, FormProps } from '@jeesite/core/components/Form';
import { PageWrapper } from '@jeesite/core/components/Page';
import ChartPie from './components/ChartPie.vue';
import ChartLineExp from './components/ChartLineExp.vue';
import ChartLineInc from './components/ChartLineInc.vue';
import ChartBarCycle from './components/ChartBarCycle.vue';
import ChartBarAccount from './components/ChartBarAccount.vue';
import ChartLineType from './components/ChartLineType.vue';
import ChartLineRatio from './components/ChartLineRatio.vue';
const schemaForm: FormProps = {
baseColProps: { md: 8, lg: 6 },
labelWidth: 90,
schemas: [
{
label: '周期',
field: 'cycleType',
defaultValue: 'M',
component: 'Select',
componentProps: {
dictType: 'report_cycle',
allowClear: true,
onChange: (value: string) => {
FormValues.value.cycleType = value || '';
}
},
colProps: { md: 24, lg: 24 },
},
],
};
import headerImg from '@jeesite/assets/images/bigview.png';
const FormValues = ref<Record<string, any>>({
cycleType: 'M'
});
const schemaForm: FormProps = {
baseColProps: { md: 8, lg: 6 },
labelWidth: 90,
schemas: [
{
label: '周期',
field: 'cycleType',
defaultValue: 'M',
component: 'Select',
componentProps: {
dictType: 'report_cycle',
allowClear: true,
onChange: (value: string) => {
FormValues.value.cycleType = value || '';
}
},
colProps: { md: 24, lg: 24 },
},
],
};
</script>
<style scoped>
.dashboard-container {
padding: 0 8px;
min-height: 100vh;
}
.top-header-section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
width: 100%;
background-color: #e6f7ff;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03);
border: 1px solid #bde5ff;
}
/* 优化:图标容器样式 */
.header-icon-wrapper {
display: flex;
align-items: center;
gap: 8px; /* 图标与文本间距 */
font-size: 16px;
font-weight: 500;
transition: all 0.2s ease;
}
/* 核心优化:限制图片尺寸 */
.header-img {
width: 60px; /* 固定宽度 */
height: 30px; /* 固定高度 */
object-fit: contain; /* 保持图片比例,不拉伸 */
flex-shrink: 0; /* 防止被压缩 */
border-radius: 4px; /* 轻微圆角,更美观 */
}
/* 优化:图标核心样式 */
.dashboard-icon {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
transition: transform 0.2s ease, filter 0.2s ease;
background: transparent;
}
/* hover效果 */
.header-icon-wrapper:hover .header-img {
transform: scale(1.05);
filter: brightness(1.1);
}
/* 图标文本样式 */
.icon-text {
color: #1890ff;
margin-left: 4px;
white-space: nowrap; /* 防止文本换行 */
}
/* 右侧表单样式 */
.search-form {
width: 245px;
}
.two-column-layout {
@@ -71,6 +139,7 @@
gap: 12px;
width: 100%;
padding: 0;
margin-bottom: 12px; /* 增加图表区域间距 */
}
.chart-line-wrapper {
@@ -79,12 +148,39 @@
padding: 0;
}
/* 响应式适配(可选) */
/* 响应式适配优化 */
@media (max-width: 768px) {
.two-column-layout {
flex-direction: column;
gap: 8px;
margin-bottom: 4px; /* 移动端也保持紧凑 */
margin-bottom: 8px;
}
.top-header-section {
flex-direction: column;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
}
.search-form {
width: 100%;
}
/* 移动端图片适配 */
.header-img {
width: 32px;
height: 32px;
}
.header-icon-wrapper {
font-size: 14px;
}
}
</style>
/* 额外优化:防止图片溢出 */
:deep(.header-img) {
max-width: 100%;
height: auto;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB