| | |
| | | <template> |
| | | <div style="padding: 20px;"> |
| | | <!-- 页面标题和日期筛选 --> |
| | | <div class="w-full md:w-auto flex items-center gap-3" style="margin-bottom: 20px;"> |
| | | <el-date-picker |
| | | v-model="dateRange" |
| | | type="daterange" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | range-separator="至" |
| | | start-placeholder="开始日期" |
| | | end-placeholder="结束日期" |
| | | :default-value="[new Date(firstDayOfMonth), new Date()]" |
| | | @change="handleDateChange" |
| | | class="w-full md:w-auto" |
| | | style="margin-right: 30px;" |
| | | /> |
| | | |
| | | <el-button |
| | | type="primary" |
| | | icon="Refresh" |
| | | @click="resetDateRange" |
| | | size="default" |
| | | > |
| | | 重置 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <main class="container mx-auto px-4 pb-10"> |
| | | <!-- 财务指标卡片 --> |
| | | <div class="grid-container"> |
| | | <!-- 总收入 --> |
| | | <el-card class="bg1"> |
| | | <p>总收入</p> |
| | | <h3> |
| | | ¥{{ pageInfo.totalIncome }} |
| | | </h3> |
| | | </el-card> |
| | | |
| | | <!-- 收入笔数 --> |
| | | <el-card class="bg2"> |
| | | <p>收入笔数</p> |
| | | <h3> |
| | | {{ pageInfo.incomeNumber }} |
| | | </h3> |
| | | </el-card> |
| | | |
| | | <!-- 总支出 --> |
| | | <el-card class="bg3"> |
| | | <p>总支出</p> |
| | | <h3> |
| | | ¥{{ pageInfo.totalExpense }} |
| | | </h3> |
| | | </el-card> |
| | | |
| | | <!-- 支出笔数 --> |
| | | <el-card class="bg4"> |
| | | <p>支出笔数</p> |
| | | <h3> |
| | | {{ pageInfo.expenseNumber }} |
| | | </h3> |
| | | </el-card> |
| | | |
| | | <!-- 净收入 --> |
| | | <el-card class="bg5"> |
| | | <p>净收入</p> |
| | | <h3> |
| | | ¥{{ pageInfo.netRevenue }} |
| | | </h3> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <!-- 收入统计图表 --> |
| | | <div class="grid-layout"> |
| | | <el-card style="margin-bottom: 20px;"> |
| | | <h2 class="section-title">收入统计(元)</h2> |
| | | <div class="echarts"> |
| | | <Echarts :legend="pieLegend0" :chartStyle="chartStylePie" |
| | | :series="materialPieSeries0" |
| | | :tooltip="pieTooltip" style="height: 260px;width: 35%;"> |
| | | <div class="chart-num"> |
| | | <span style="font-size: 22px;">收入</span> |
| | | <span style="font-size: 36px; |
| | | font-weight: 500; |
| | | font-family: 'MyCustomFont', sans-serif;">{{ pageInfo.totalIncome }}</span> |
| | | </div> |
| | | </Echarts> |
| | | <Echarts ref="chart" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="lineLegend" |
| | | :series="lineSeries0" |
| | | :tooltip="tooltip" |
| | | :xAxis="xAxis0" |
| | | :yAxis="yAxis0" |
| | | style="height: 260px;width: 64%;"></Echarts> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 支出统计图表 --> |
| | | <el-card> |
| | | <h2 class="section-title">支出统计(元)</h2> |
| | | <div class="echarts"> |
| | | <Echarts ref="chart" |
| | | :legend="pieLegend1" |
| | | :chartStyle="chartStylePie" |
| | | :series="materialPieSeries1" |
| | | :tooltip="pieTooltip" |
| | | style="height: 260px;width: 35%;"> |
| | | <div class="chart-num"> |
| | | <span style="font-size: 22px;">支出</span> |
| | | <span style="font-size: 36px; |
| | | font-weight: 500; |
| | | font-family: 'MyCustomFont', sans-serif;">{{ pageInfo.totalExpense }}</span> |
| | | </div></Echarts> |
| | | <Echarts ref="chart" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :legend="lineLegend" |
| | | :series="lineSeries1" |
| | | :tooltip="tooltip" |
| | | :xAxis="xAxis1" |
| | | :yAxis="yAxis1" |
| | | style="height: 260px;width: 64%;"></Echarts> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | </main> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | </script> |
| | | import { ref, computed, onMounted, reactive } from 'vue'; |
| | | import 'element-plus/dist/index.css'; |
| | | import Echarts from "@/components/Echarts/echarts.vue"; |
| | | import { reportForms,reportIncome,reportExpense } from "@/api/financialManagement/financialStatements"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | // 日期范围 |
| | | const dateRange = ref([]); |
| | | const firstDayOfMonth = ref(null); |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '100%', // 设置图表容器的高度 |
| | | position:'relative', |
| | | } |
| | | const grid = { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | } |
| | | const lineLegend = { |
| | | show: false, |
| | | } |
| | | // 折线图提示框 |
| | | const tooltip = reactive({ |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'line', |
| | | lineStyle: { color: '#aaa' } |
| | | }, |
| | | // 自定义内容 |
| | | formatter: function (params) { |
| | | if (!params || !params.length) return '' |
| | | const axisLabel = params[0].axisValueLabel || params[0].axisValue || '' |
| | | const rows = params |
| | | .map(p => { |
| | | const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>` |
| | | return `${colorDot}${p.seriesName}: ${p.value}` |
| | | }) |
| | | .join('<br/>') |
| | | return `<div>${axisLabel}</div><div>${rows}</div>` |
| | | } |
| | | }) |
| | | const months = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; |
| | | const lineSeries0 = ref([]) |
| | | const lineSeries1 = ref([]) |
| | | |
| | | const xAxis0 = ref([ |
| | | { |
| | | type: 'category', |
| | | axisTick: { show: true, alignWithLabel: true }, |
| | | data: months, |
| | | }, |
| | | ]); |
| | | const xAxis1 = ref([ |
| | | { |
| | | type: 'category', |
| | | axisTick: { show: true, alignWithLabel: true }, |
| | | data: months, |
| | | }, |
| | | ]); |
| | | const yAxis0 = [ |
| | | { |
| | | type: 'value', |
| | | name: '收入统计', // 左侧y轴 |
| | | position: 'left', |
| | | min: 0, |
| | | // 坐标轴名称样式 |
| | | nameTextStyle: { |
| | | color: '#000', |
| | | fontSize: 14, |
| | | }, |
| | | } |
| | | ] |
| | | |
| | | const yAxis1 = [ |
| | | { |
| | | type: 'value', |
| | | name: '支出统计', // 左侧y轴 |
| | | position: 'left', |
| | | min: 0, |
| | | // 坐标轴名称样式 |
| | | nameTextStyle: { |
| | | color: '#000', |
| | | fontSize: 14, |
| | | }, |
| | | } |
| | | ] |
| | | |
| | | const chartStylePie = { |
| | | width: '100%', |
| | | height: '100%' // 设置图表容器的高度 |
| | | } |
| | | const pieColors = ['#F04864','#FACC14', '#8543E0', '#1890FF', '#13C2C2','#2FC25B']; // 可根据实际调整 |
| | | const pieData0 = ref([]); |
| | | const pieData1 = ref([]); |
| | | |
| | | const pieLegend0 = computed(() => ({ |
| | | show: true, |
| | | top: 'center', |
| | | left: '60%', |
| | | orient: 'vertical', |
| | | icon: 'circle', |
| | | data: pieData0.value.map(item => item.name), |
| | | formatter: function(name) { |
| | | const item = pieData0.value.find(i => i.name === name); |
| | | if (!item) return name; |
| | | return `${name} | ${item.percent} ${item.amount}`; |
| | | }, |
| | | textStyle: { |
| | | color: '#333', |
| | | fontSize: 14, |
| | | lineHeight: 26, |
| | | } |
| | | })); |
| | | const pieLegend1 = computed(() => ({ |
| | | show: true, |
| | | top: 'center', |
| | | left: '60%', |
| | | orient: 'vertical', |
| | | icon: 'circle', |
| | | data: pieData1.value.map(item => item.name), |
| | | formatter: function(name) { |
| | | const item = pieData1.value.find(i => i.name === name); |
| | | if (!item) return name; |
| | | return `${name} | ${item.percent} ${item.amount}`; |
| | | }, |
| | | textStyle: { |
| | | color: '#333', |
| | | fontSize: 14, |
| | | lineHeight: 26, |
| | | } |
| | | })); |
| | | |
| | | const materialPieSeries0 = computed(() => [ |
| | | { |
| | | type: 'pie', |
| | | radius: ['50%', '65%'], |
| | | center: ['25%', '50%'], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { |
| | | borderColor: '#fff', |
| | | borderWidth: 2 |
| | | }, |
| | | label: { |
| | | show: false |
| | | }, |
| | | data: pieData0.value, |
| | | color: pieColors |
| | | } |
| | | ]); |
| | | const materialPieSeries1 = computed(() => [ |
| | | { |
| | | type: 'pie', |
| | | radius: ['50%', '65%'], |
| | | center: ['25%', '50%'], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { |
| | | borderColor: '#fff', |
| | | borderWidth: 2 |
| | | }, |
| | | label: { |
| | | show: false |
| | | }, |
| | | data: pieData1.value, |
| | | color: pieColors |
| | | } |
| | | ]); |
| | | const pieTooltip = reactive({ |
| | | trigger: 'item', |
| | | formatter: function(params) { |
| | | // 检查数据是否存在 |
| | | if (!params.data) return params.name; |
| | | // 拼接完整内容 |
| | | return ` |
| | | <div> |
| | | <div style="color:${params.color};font-size:16px;">●</div> |
| | | <div>${params.name}</div> |
| | | <div>占比:${params.data.percent}</div> |
| | | <div>金额:${params.data.amount}</div> |
| | | </div> |
| | | `; |
| | | } |
| | | }) |
| | | |
| | | |
| | | const pageInfo = ref({ |
| | | }) |
| | | |
| | | const getData = async () => { |
| | | try { |
| | | const {code,data} = await reportForms({entryDateStart:dateRange.value[0], entryDateEnd:dateRange.value[1]}); |
| | | if(code === 200) { |
| | | pageInfo.value = data |
| | | pieData0.value = data.incomeType.map(item=>({ |
| | | name:item.typeName, |
| | | value:item.account, |
| | | percent:`${item.proportion*100}%`, |
| | | amount:`¥${item.account}` |
| | | })) |
| | | pieData1.value = data.expenseType.map(item=>({ |
| | | name:item.typeName, |
| | | value:item.account, |
| | | percent:`${item.proportion*100}%`, |
| | | amount:`¥${item.account}` |
| | | })) |
| | | |
| | | } |
| | | } catch (error) { |
| | | console.error('获取财务指标数据失败:', error); |
| | | } |
| | | try{ |
| | | const {code,data} = await reportIncome(); |
| | | if(code==200){ |
| | | lineSeries0.value = data.map(item=>({ |
| | | name:item.typeName, |
| | | type: 'line', |
| | | data:item.account.map(item=>Number(item)) |
| | | })) |
| | | |
| | | } |
| | | }catch (error) { |
| | | console.error('获取财务指标数据失败:', error); |
| | | } |
| | | try{ |
| | | const {code,data} = await reportExpense(); |
| | | if(code==200){ |
| | | lineSeries1.value = data.map(item=>({ |
| | | name:item.typeName, |
| | | type: 'line', |
| | | data:item.account.map(item=>Number(item)) |
| | | })) |
| | | |
| | | } |
| | | }catch (error) { |
| | | console.error('获取财务指标数据失败:', error); |
| | | } |
| | | }; |
| | | |
| | | |
| | | // 初始化日期范围(默认当月) |
| | | onMounted(() => { |
| | | const today = new Date(); |
| | | const firstDay = new Date(today.getFullYear(), today.getMonth(), 1); |
| | | firstDayOfMonth.value = firstDay; |
| | | dateRange.value = [dayjs(firstDay).format("YYYY-MM-DD"), dayjs(today).format("YYYY-MM-DD")]; |
| | | getData() |
| | | |
| | | }); |
| | | |
| | | // 处理日期范围变化 |
| | | const handleDateChange = (newRange) => { |
| | | if (newRange && newRange.length === 2) { |
| | | dateRange.value = newRange; |
| | | getData() |
| | | } |
| | | }; |
| | | |
| | | // 重置日期范围 |
| | | const resetDateRange = () => { |
| | | const today = new Date(); |
| | | const firstDay = new Date(today.getFullYear(), today.getMonth(), 1); |
| | | dateRange.value = [dayjs(firstDay).format("YYYY-MM-DD"), dayjs(today).format("YYYY-MM-DD")]; |
| | | getData() |
| | | }; |
| | | |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | /* 基础样式补充 */ |
| | | :root { |
| | | --el-color-primary: #4f46e5; |
| | | } |
| | | .el-card{ |
| | | position: relative; |
| | | border-radius: 12px; |
| | | padding: 14px 10px 10px 10px; |
| | | box-shadow: 0 2px 8px #eee; |
| | | :deep(.el-card__body){ |
| | | padding: 10px 20px !important; |
| | | } |
| | | &.bg1{ |
| | | background: url(@/assets/icons/png/1.png) no-repeat 100% 100% !important; |
| | | } |
| | | &.bg2{ |
| | | background: url(@/assets/icons/png/2.png) no-repeat 100% 100% !important; |
| | | } |
| | | &.bg3{ |
| | | background: url(@/assets/icons/png/3.png) no-repeat 100% 100% !important; |
| | | } |
| | | &.bg4{ |
| | | background: url(@/assets/icons/png/4.png) no-repeat 100% 100% !important; |
| | | } |
| | | &.bg5{ |
| | | background: url(@/assets/icons/png/5.png) no-repeat 100% 100% !important; |
| | | } |
| | | } |
| | | |
| | | .grid-container { |
| | | /* grid 容器基础样式 */ |
| | | display: grid; |
| | | gap: 1rem; /* gap-4 对应 1rem (16px) */ |
| | | margin-bottom: 2rem; /* mb-8 对应 2rem (32px) */ |
| | | |
| | | p{ |
| | | font-size: 22px; |
| | | margin-top: 0px; |
| | | color: #fff; |
| | | } |
| | | h3{ |
| | | font-size: 36px; |
| | | font-weight: 500; |
| | | font-family: 'MyCustomFont', sans-serif; |
| | | margin: 10px 0; |
| | | color: #fff; |
| | | } |
| | | |
| | | } |
| | | |
| | | /* 移动端默认样式 (grid-cols-1) */ |
| | | .grid-container { |
| | | grid-template-columns: repeat(1, minmax(0, 1fr)); |
| | | } |
| | | |
| | | /* 小屏幕及以上 (sm:grid-cols-2) */ |
| | | @media (min-width: 640px) { |
| | | .grid-container { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | /* 大屏幕及以上 (lg:grid-cols-5) */ |
| | | @media (min-width: 1024px) { |
| | | .grid-container { |
| | | grid-template-columns: repeat(5, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | /* 卡片悬停效果增强 */ |
| | | .el-card:hover { |
| | | transform: translateY(-2px); |
| | | } |
| | | .echarts{ |
| | | display: flex; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | /* 图表容器样式 */ |
| | | .el-chart { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .section-title { |
| | | position: relative; |
| | | font-size: 18px; |
| | | color: #333; |
| | | padding-left: 10px; |
| | | margin-bottom: 10px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .section-title::before { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0px; |
| | | content: ''; |
| | | width: 4px; |
| | | height: 18px; |
| | | background-color: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | .chart-num{ |
| | | position: absolute; |
| | | z-index: 3; |
| | | top: 92px; |
| | | left: 92px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | } |
| | | </style> |