maven
2025-08-08 f139849fb65e7264d173bc46f45d70409e559504
src/views/financialManagement/financialStatements/index.vue
@@ -1,4 +1,513 @@
<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>