zhangwencui
7 天以前 ea142cd8de3e3cafdaf92d99b5798bd9a5e7d762
财务报表页面开发及接口对接
已修改2个文件
1406 ■■■■■ 文件已修改
src/api/financialManagement/financialStatements.js 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/financialStatements/index.vue 1374 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/financialManagement/financialStatements.js
@@ -1,31 +1,13 @@
import request from "@/utils/request";
// 根据日期查询
export const reportForms = (params) => {
  console.log(params);
/**
 * 获取财务报表月度明细
 * @param {Object} params { entryDateStart, entryDateEnd }
 */
export function accountStatementDetailsByMonth(params) {
  return request({
    url: "/account/accountExpense/report/forms",
    url: "/accounting/accountStatementDetailsByMonth",
    method: "get",
    params,
  });
};
// 查询每月数据-收入
export const reportIncome = (params) => {
  console.log(params);
  return request({
    url: "/account/accountExpense/report/income",
    method: "get",
    params,
  });
};
// 查询每月数据-支出
export const reportExpense = (params) => {
  console.log(params);
  return request({
    url: "/account/accountExpense/report/expense",
    method: "get",
    params,
  });
};
}
src/views/financialManagement/financialStatements/index.vue
@@ -1,4 +1,4 @@
 <template>
<template>
  <div style="padding: 20px;">
    <!-- 页面标题和月份筛选 -->
    <div class="w-full md:w-auto flex items-center gap-3"
@@ -24,125 +24,161 @@
    <main class="container mx-auto px-4 pb-10">
      <!-- 财务指标卡片 -->
      <div class="stats-cards">
        <!-- 总营收 -->
        <div class="stat-card stat-card-blue">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletBlue@2x.png"
                 alt="总营收" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletBlue@2x.png"
                 alt="总营收" /></div>
          <div class="stat-content">
            <div class="stat-label">总营收</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }} 元</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
        <!-- 总支出 -->
        <div class="stat-card stat-card-orange">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletOrange@2x.png"
                 alt="总支出" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletOrange@2x.png"
                 alt="总支出" /></div>
          <div class="stat-content">
            <div class="stat-label">总支出</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }} 元</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
        <!-- 总收入笔数 -->
        <div class="stat-card stat-card-green">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletGreen@2x.png"
                 alt="总收入笔数" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletGreen@2x.png"
                 alt="应收账款" /></div>
          <div class="stat-content">
            <div class="stat-label">总收入笔数</div>
            <div class="stat-value">{{ pageInfo.incomeNumber || 0 }} 笔</div>
            <div class="stat-label">应收账款</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalReceivable || 0) }}{{ Math.abs(pageInfo.totalReceivable) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
        <!-- 总支出笔数 -->
        <div class="stat-card stat-card-red">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletRed@2x.png"
                 alt="总支出笔数" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletRed@2x.png"
                 alt="应付账款" /></div>
          <div class="stat-content">
            <div class="stat-label">总支出笔数</div>
            <div class="stat-value">{{ pageInfo.expenseNumber || 0 }} 笔</div>
            <div class="stat-label">应付账款</div>
            <div class="stat-value">{{ formatMoney(pageInfo.totalPayable || 0) }}{{ Math.abs(pageInfo.totalPayable) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
        <!-- 净收入 -->
        <div class="stat-card stat-card-yellow">
          <div class="stat-icon">
            <img src="@/assets/icons/png/walletYellow@2x.png"
                 alt="净收入" />
          </div>
          <div class="stat-icon"><img src="@/assets/icons/png/walletYellow@2x.png"
                 alt="净利润" /></div>
          <div class="stat-content">
            <div class="stat-label">净收入</div>
            <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }} 元</div>
            <div class="stat-label">净利润</div>
            <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }}{{ Math.abs(pageInfo.netRevenue) < 10000 ? ' 元' : '' }}</div>
          </div>
        </div>
      </div>
      <!-- 中间图表区域 -->
      <!-- 图表区域 -->
      <div class="charts-row">
        <!-- 左侧:收入支出分析 -->
        <!-- 1. 收支构成分析 (双环形图 + 净利中心) -->
        <el-card class="chart-card">
          <h2 class="section-title">收入支出分析</h2>
          <div class="pie-chart-container">
            <Echarts :legend="pieLegendIncomeExpense"
                     :chartStyle="chartStylePie"
                     :series="pieSeriesIncomeExpense"
                     :tooltip="pieTooltipIncomeExpense"
                     style="height: 320px; width: 100%;">
            </Echarts>
            <div class="pie-stats">
              <div class="bar-stat-item">
                <span class="bar-stat-label">收入数量</span>
                <span class="bar-stat-value">{{ pageInfo.incomeNumber || 0 }}</span>
          <template #header>
            <div class="card-header">
              <span class="header-title">收支构成及净利分析</span>
              <el-tooltip content="左侧为收入构成,右侧为支出构成,中间展示盈亏净额"
                          placement="top">
                <el-icon>
                  <QuestionFilled />
                </el-icon>
              </el-tooltip>
            </div>
          </template>
          <div class="financial-overview-container">
            <!-- 收入展示 (左侧) -->
            <div style="width:60%">
              <div class="overview-item income"
                   style="margin-bottom: 20px;">
                <div class="overview-box">
                  <div class="icon-circle">
                    <el-icon>
                      <TrendCharts />
                    </el-icon>
                  </div>
                  <div class="data-content">
                    <div class="label">本期总收入</div>
                    <div class="value">{{ formatMoney(pageInfo.totalIncome) }}</div>
                    <div class="unit">RMB{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' / 元' : '' }}</div>
                  </div>
                  <div class="bg-decoration">INCOME</div>
                </div>
              </div>
              <div class="bar-stat-item">
                <span class="bar-stat-label">支出数量</span>
                <span class="bar-stat-value">{{ pageInfo.expenseNumber || 0 }}</span>
              <div class="overview-item expense">
                <div class="overview-box">
                  <div class="icon-circle">
                    <el-icon>
                      <Sell />
                    </el-icon>
                  </div>
                  <div class="data-content">
                    <div class="label">本期总支出</div>
                    <div class="value">{{ formatMoney(pageInfo.totalExpense) }}</div>
                    <div class="unit">RMB{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' / 元' : '' }}</div>
                  </div>
                  <div class="bg-decoration">EXPENSE</div>
                </div>
              </div>
            </div>
            <!-- 净利润核心指示 (中间) -->
            <div class="profit-indicator">
              <div class="profit-gauge-wrapper">
                <Echarts :chartStyle="chartStylePie"
                         :series="profitGaugeSeries"
                         :tooltip="gaugeTooltip"
                         style="height: 200px; width: 100%; max-width: 200px;">
                </Echarts>
                <div class="profit-center-text">
                  <div class="label">净利润</div>
                  <div class="value"
                       :class="pageInfo.netRevenue >= 0 ? 'plus' : 'minus'">
                    {{ pageInfo.netRevenue >= 0 ? '+' : '' }}{{ formatMoney(pageInfo.netRevenue) }}
                  </div>
                  <div class="rate">利润率: {{ pageInfo.totalIncome > 0 ? ((pageInfo.netRevenue / pageInfo.totalIncome) * 100).toFixed(1) : 0 }}%</div>
                </div>
              </div>
            </div>
            <!-- 支出展示 (右侧) -->
          </div>
        </el-card>
        <!-- 右侧:行项盈利分析 -->
        <!-- 2. 应收/应付对冲分析 (柱状图) -->
        <el-card class="chart-card">
          <h2 class="section-title">行项盈利分析</h2>
          <div class="bar-chart-header">
            <div class="bar-stat-item">
              <span class="bar-stat-label">当前总个数</span>
              <span class="bar-stat-value">{{ allBarTypes.value?.length || 0 }}</span>
          <template #header>
            <div class="card-header">
              <span class="header-title">应收/应付概览</span>
              <el-tooltip content="对比当前各月份的应收账款与应付账款"
                          placement="top">
                <el-icon>
                  <QuestionFilled />
                </el-icon>
              </el-tooltip>
            </div>
            <div class="bar-stat-item">
              <span class="bar-stat-label">支出金额</span>
              <span class="bar-stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}</span>
            </div>
            <div class="bar-stat-item">
              <span class="bar-stat-label">收入金额</span>
              <span class="bar-stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}</span>
            </div>
          </div>
          <Echarts ref="barChart"
                   :chartStyle="chartStyle"
          </template>
          <Echarts :chartStyle="chartStyle"
                   :grid="barGrid"
                   :legend="barLegend"
                   :series="barSeries"
                   :tooltip="barTooltip"
                   :xAxis="barXAxis"
                   :yAxis="barYAxis"
                   style="height: 300px; width: 100%;">
                   style="height: 270px; width: 100%;">
          </Echarts>
        </el-card>
      </div>
      <!-- 底部:营收趋势分析 -->
      <!-- 3. 财务综合趋势分析 (折线图) -->
      <el-card class="trend-chart-card">
        <h2 class="section-title">营收趋势分析</h2>
        <Echarts ref="trendChart"
                 :chartStyle="chartStyle"
                 :grid="grid"
        <template #header>
          <div class="card-header">
            <span class="header-title">财务绩效综合趋势</span>
            <el-tooltip content="展示收入、支出及净利润的月度变化趋势"
                        placement="top">
              <el-icon>
                <QuestionFilled />
              </el-icon>
            </el-tooltip>
          </div>
        </template>
        <Echarts :chartStyle="chartStyle"
                 :grid="trendGrid"
                 :legend="trendLegend"
                 :series="trendSeries"
                 :tooltip="tooltip"
                 :xAxis="xAxis0"
                 :tooltip="trendTooltip"
                 :xAxis="trendXAxis"
                 :yAxis="trendYAxis"
                 style="height: 350px; width: 100%;">
        </Echarts>
@@ -160,883 +196,453 @@
    nextTick,
    getCurrentInstance,
  } from "vue";
  import "element-plus/dist/index.css";
  import { QuestionFilled, TrendCharts, Sell } from "@element-plus/icons-vue";
  import Echarts from "@/components/Echarts/echarts.vue";
  import {
    reportForms,
    reportIncome,
    reportExpense,
  } from "@/api/financialManagement/financialStatements";
  import { accountStatementDetailsByMonth } from "@/api/financialManagement/financialStatements";
  import dayjs from "dayjs";
  // 日期范围
  const dateRange = ref(null);
  const { proxy } = getCurrentInstance();
  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 lineSeries0 = ref([]);
  const lineSeries1 = ref([]);
  // 根据月份范围生成 x 轴数据
  const generateMonthLabels = (startMonth, endMonth) => {
    const labels = [];
    let current = dayjs(startMonth);
    const end = dayjs(endMonth);
    while (current.isBefore(end) || current.isSame(end, "month")) {
      labels.push(`${current.month() + 1}月`);
      current = current.add(1, "month");
    }
    return labels;
  };
  const xAxis0 = ref([
    {
      type: "category",
      axisTick: { show: true, alignWithLabel: true },
      data: [],
    },
  ]);
  const xAxis1 = ref([
    {
      type: "category",
      axisTick: { show: true, alignWithLabel: true },
      data: [],
    },
  ]);
  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 || [])
      .filter(item => item && item.name)
      .map(item => item.name),
    formatter: function (name) {
      if (!name) return "";
      const item = pieData0.value.find(i => 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 || [])
      .filter(item => item && item.name)
      .map(item => item.name),
    formatter: function (name) {
      if (!name) return "";
      const item = pieData1.value.find(i => 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 || []).filter(item => item && item.name),
      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 || []).filter(item => item && item.name),
      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 dateRange = ref(null);
  const pageInfo = reactive({
    totalIncome: 0,
    totalExpense: 0,
    totalReceivable: 0,
    totalPayable: 0,
    netRevenue: 0,
  });
  const pageInfo = ref({});
  const chartStyle = { width: "100%", height: "100%", position: "relative" };
  const chartStylePie = { width: "100%", height: "100%" };
  // 格式化金额
  const formatMoney = value => {
    if (!value && value !== 0) return "0";
    return Number(value).toLocaleString("zh-CN", {
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    });
  };
  const monthlyTrendList = ref([]);
  const receivablePayableList = ref([]);
  // 收入支出分析饼图
  const pieDataIncomeExpense = computed(() => {
    const totalIncome = Number(pageInfo.value.totalIncome) || 0;
    const totalExpense = Number(pageInfo.value.totalExpense) || 0;
    const total = totalIncome + totalExpense;
    if (total === 0) {
      return [
        { name: "收入", value: 0, percent: "0%" },
        { name: "支出", value: 0, percent: "0%" },
      ];
    }
    const incomePercent = ((totalIncome / total) * 100).toFixed(0);
    const expensePercent = ((totalExpense / total) * 100).toFixed(0);
  // --- 1. 收支构成分析 (简化版逻辑) ---
  const gaugeTooltip = { show: false };
  const profitGaugeSeries = computed(() => {
    const rate =
      pageInfo.totalIncome > 0
        ? (pageInfo.netRevenue / pageInfo.totalIncome) * 100
        : 0;
    return [
      { name: "收入", value: totalIncome, percent: `${incomePercent}%` },
      { name: "支出", value: totalExpense, percent: `${expensePercent}%` },
    ];
  });
  const pieLegendIncomeExpense = computed(() => ({
    show: false,
  }));
  const pieTooltipIncomeExpense = reactive({
    trigger: "item",
    formatter: function (params) {
      if (!params.data) return params.name;
      return `${params.name}占比 ${params.percent}%`;
    },
  });
  const pieSeriesIncomeExpense = computed(() => [
    {
      type: "pie",
      radius: ["0%", "70%"],
      center: ["50%", "50%"],
      avoidLabelOverlap: true,
      itemStyle: {
        borderColor: "#fff",
        borderWidth: 2,
      },
      label: {
        show: true,
        position: "outside",
        formatter: function (params) {
          return `${params.name}占比 ${params.percent}%`;
        },
        fontSize: 14,
        color: "#333",
      },
      labelLine: {
        show: true,
        length: 15,
        length2: 10,
        lineStyle: {
          color: "#333",
        },
      },
      emphasis: {
        label: {
      {
        type: "gauge",
        startAngle: 210,
        endAngle: -30,
        min: 0,
        max: 100,
        splitNumber: 10,
        radius: "100%",
        progress: {
          show: true,
          fontSize: 16,
          fontWeight: "bold",
          width: 14,
          itemStyle: { color: pageInfo.netRevenue >= 0 ? "#10b981" : "#f43f5e" },
        },
        pointer: { show: false },
        axisLine: { lineStyle: { width: 14, color: [[1, "#f1f5f9"]] } },
        axisTick: { show: false },
        splitLine: { show: false },
        axisLabel: { show: false },
        anchor: { show: false },
        title: { show: false },
        detail: { show: false },
        data: [{ value: Math.max(0, Math.min(100, rate)) }],
      },
      data: pieDataIncomeExpense.value,
      color: ["#1890FF", "#FACC14"],
    ];
  });
  // --- 2. 应收/应付概览 (柱状图) ---
  const barGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true };
  const barLegend = { top: "0", right: "center" };
  const barXAxis = computed(() => [
    {
      type: "category",
      data: receivablePayableList.value.map(item => item.month || ""),
      axisTick: { alignWithLabel: true },
    },
  ]);
  const barYAxis = [{ type: "value", name: "金额 (元)" }];
  const barTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
  const barSeries = computed(() => [
    {
      name: "应收账款",
      type: "bar",
      barWidth: "30%",
      data: receivablePayableList.value.map(item => item.receivable || 0),
      itemStyle: { color: "#10b981" },
    },
    {
      name: "应付账款",
      type: "bar",
      barWidth: "30%",
      data: receivablePayableList.value.map(item => item.payable || 0),
      itemStyle: { color: "#ef4444" },
    },
  ]);
  // 行项盈利分析柱状图
  const barXAxis = computed(() => {
    return [
      {
        type: "category",
        data:
          allBarTypes.value && allBarTypes.value.length > 0
            ? allBarTypes.value
            : ["项目1", "项目2", "项目3", "项目4", "项目5", "项目6", "项目7"],
        axisTick: { show: true, alignWithLabel: true },
      },
    ];
  });
  const barYAxis = [
  // --- 3. 财务综合趋势分析 (折线图) ---
  const trendGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true };
  const trendLegend = { top: "0", right: "center" };
  const trendXAxis = computed(() => [
    {
      type: "value",
      name: "单位: 元",
      position: "left",
      min: 0,
      nameTextStyle: {
        color: "#000",
        fontSize: 14,
      },
      type: "category",
      boundaryGap: false,
      data: monthlyTrendList.value.map(item => item.month || ""),
    },
  ];
  const barGrid = {
    left: "3%",
    right: "4%",
    bottom: "3%",
    containLabel: true,
  };
  const barLegend = {
    show: true,
    top: 10,
    right: 10,
  };
  // 获取所有类型名称
  const allBarTypes = computed(() => {
    const incomeTypes = (lineSeries0.value || [])
      .map(item => item.name || item.typeName)
      .filter(Boolean);
    const expenseTypes = (lineSeries1.value || [])
      .map(item => item.name || item.typeName)
      .filter(Boolean);
    return [...new Set([...incomeTypes, ...expenseTypes])];
  });
  const barSeries = computed(() => {
    if (allBarTypes.value.length === 0) {
      return [
        {
          name: "支出",
          type: "bar",
          data: [],
          itemStyle: { color: "#1890FF" },
        },
        {
          name: "收入",
          type: "bar",
          data: [],
          itemStyle: { color: "#13C2C2" },
        },
      ];
    }
    // 计算每个项目的总收入(汇总所有月份)
    const incomeData = allBarTypes.value.map(typeName => {
      const incomeItem = (lineSeries0.value || []).find(
        item => (item.name || item.typeName) === typeName
      );
      if (incomeItem && incomeItem.data && Array.isArray(incomeItem.data)) {
        return incomeItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0);
      }
      return 0;
    });
    // 计算每个项目的总支出(汇总所有月份)
    const expenseData = allBarTypes.value.map(typeName => {
      const expenseItem = (lineSeries1.value || []).find(
        item => (item.name || item.typeName) === typeName
      );
      if (expenseItem && expenseItem.data && Array.isArray(expenseItem.data)) {
        return expenseItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0);
      }
      return 0;
    });
    return [
      {
        name: "支出",
        type: "bar",
        data: expenseData,
        itemStyle: { color: "#1890FF" },
      },
      {
        name: "收入",
        type: "bar",
        data: incomeData,
        itemStyle: { color: "#13C2C2" },
      },
    ];
  });
  const barTooltip = reactive({
    trigger: "axis",
    axisPointer: {
      type: "shadow",
    },
    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>`;
          const value =
            typeof p.value === "number" ? p.value.toFixed(2) : p.value;
          return `${colorDot}${p.seriesName} ${value}`;
        })
        .join("<br/>");
      return `<div>${axisLabel}</div><div>${rows}</div>`;
    },
  });
  // 营收趋势分析
  const trendLegend = {
    show: true,
    top: 10,
    right: 10,
  };
  const trendYAxis = [
  ]);
  const trendYAxis = [{ type: "value", name: "金额 (元)" }];
  const trendTooltip = { trigger: "axis" };
  const trendSeries = computed(() => [
    {
      type: "value",
      name: "单位: 元",
      position: "left",
      min: 0,
      nameTextStyle: {
        color: "#000",
        fontSize: 14,
      },
      name: "总营收",
      type: "line",
      smooth: true,
      data: monthlyTrendList.value.map(item => item.income || 0),
      itemStyle: { color: "#4f46e5" },
      areaStyle: { opacity: 0.1 },
    },
  ];
    {
      name: "总支出",
      type: "line",
      smooth: true,
      data: monthlyTrendList.value.map(item => item.expense || 0),
      itemStyle: { color: "#f97316" },
    },
    {
      name: "净利润",
      type: "line",
      smooth: true,
      data: monthlyTrendList.value.map(item => item.profit || 0),
      lineStyle: { width: 4, type: "dashed" },
      itemStyle: { color: "#10b981" },
    },
  ]);
  const trendSeries = computed(() => {
    // 汇总所有支出类型的数据
    let expenseTrend = [];
    if (lineSeries1.value.length > 0) {
      const monthCount = Math.max(
        ...lineSeries1.value.map(item => item.data?.length || 0)
      );
      expenseTrend = Array(monthCount).fill(0);
      lineSeries1.value.forEach(item => {
        if (item.data && Array.isArray(item.data)) {
          item.data.forEach((val, index) => {
            if (index < monthCount) {
              expenseTrend[index] += Number(val) || 0;
            }
          });
        }
      });
    }
    // 汇总所有收入类型的数据
    let incomeTrend = [];
    if (lineSeries0.value.length > 0) {
      const monthCount = Math.max(
        ...lineSeries0.value.map(item => item.data?.length || 0)
      );
      incomeTrend = Array(monthCount).fill(0);
      lineSeries0.value.forEach(item => {
        if (item.data && Array.isArray(item.data)) {
          item.data.forEach((val, index) => {
            if (index < monthCount) {
              incomeTrend[index] += Number(val) || 0;
            }
          });
        }
      });
    }
    return [
      {
        name: "支出",
        type: "line",
        data: expenseTrend,
        itemStyle: { color: "#1890FF" },
        smooth: true,
      },
      {
        name: "收入",
        type: "line",
        data: incomeTrend,
        itemStyle: { color: "#13C2C2" },
        smooth: true,
      },
    ];
  });
  // 获取最近六个月的范围
  const getLastSixMonths = () => {
    const endMonth = dayjs().format("YYYY-MM");
    const startMonth = dayjs().subtract(5, "month").format("YYYY-MM");
    return [startMonth, endMonth];
  // --- 公用逻辑 ---
  const formatMoney = val => {
    return val;
  };
  const handleDateChange = val => {
    if (val) getData();
  };
  const resetDateRange = () => {
    dateRange.value = [
      dayjs().subtract(5, "month").format("YYYY-MM"),
      dayjs().format("YYYY-MM"),
    ];
    getData();
  };
  const disabledDate = time => dayjs(time).isAfter(dayjs(), "month");
  const getData = async () => {
    if (
      !dateRange.value ||
      !Array.isArray(dateRange.value) ||
      dateRange.value.length !== 2
    ) {
      return;
    }
    const startDateStr = dateRange.value[0];
    const endDateStr = dateRange.value[1];
    if (!startDateStr || !endDateStr) {
      return;
    }
    if (!dateRange.value || dateRange.value.length !== 2) return;
    // 验证日期格式并转换为完整日期
    const startDate = dayjs(startDateStr);
    const endDate = dayjs(endDateStr);
    if (!startDate.isValid() || !endDate.isValid()) {
      console.error("无效的日期格式");
      return;
    }
    // 更新 x 轴数据
    const monthLabels = generateMonthLabels(startDateStr, endDateStr);
    xAxis0.value[0].data = monthLabels;
    xAxis1.value[0].data = monthLabels;
    // 开始月份拼接第一天,结束月份拼接最后一天
    const entryDateStart = startDate.startOf("month").format("YYYY-MM-DD");
    const entryDateEnd = endDate.endOf("month").format("YYYY-MM-DD");
    const params = {
      entryDateStart: dayjs(dateRange.value[0])
        .startOf("month")
        .format("YYYY-MM-DD"),
      entryDateEnd: dayjs(dateRange.value[1]).endOf("month").format("YYYY-MM-DD"),
    };
    try {
      // const {code,data} = await reportForms({entryDateStart, entryDateEnd});
      // if(code === 200 && data) {
      //   pageInfo.value = data || {};
      //   // 安全处理数据,过滤掉 null 或 undefined
      //   pieData0.value = (data.incomeType || []).filter(item => item && item.typeName).map(item=>({
      //     name:item.typeName || '',
      //     value:item.account || 0,
      //     percent:`${((item.proportion || 0) * 100).toFixed(2)}%`,
      //     amount:`¥${(item.account || 0).toFixed(2)}`
      //   }))
      //   pieData1.value = (data.expenseType || []).filter(item => item && item.typeName).map(item=>({
      //     name:item.typeName || '',
      //     value:item.account || 0,
      //     percent:`${((item.proportion || 0) * 100).toFixed(2)}%`,
      //     amount:`¥${(item.account || 0).toFixed(2)}`
      //   }))
      // }
      const res = await accountStatementDetailsByMonth(params);
      if (res.code === 200 && res.data) {
        const data = res.data;
        // 更新顶部汇总卡片数据
        pageInfo.totalIncome = data.totalIncome || 0;
        pageInfo.totalExpense = data.totalExpense || 0;
        pageInfo.totalReceivable = data.accountsReceivable || 0;
        pageInfo.totalPayable = data.accountsPayable || 0;
        pageInfo.netRevenue = data.netRevenue || 0;
        // 更新图表数据
        monthlyTrendList.value = data.monthlyTrendList || [];
        receivablePayableList.value = data.receivablePayableList || [];
      }
    } catch (error) {
      console.error("获取财务指标数据失败:", error);
    }
    try {
      // const { code, data } = await reportIncome({ entryDateStart, entryDateEnd });
      // if (code == 200 && data && Array.isArray(data)) {
      //   lineSeries0.value = data
      //     .filter(item => item && item.typeName)
      //     .map(item => ({
      //       name: item.typeName || "",
      //       type: "line",
      //       data: (item.account || []).map(val => Number(val) || 0),
      //     }));
      // }
    } catch (error) {
      console.error("获取财务指标数据失败:", error);
    }
    try {
      // const { code, data } = await reportExpense({
      //   entryDateStart,
      //   entryDateEnd,
      // });
      // if (code == 200 && data && Array.isArray(data)) {
      //   lineSeries1.value = data
      //     .filter(item => item && item.typeName)
      //     .map(item => ({
      //       name: item.typeName || "",
      //       type: "line",
      //       data: (item.account || []).map(val => Number(val) || 0),
      //     }));
      // }
    } catch (error) {
      console.error("获取财务指标数据失败:", error);
      console.error("获取财务报表数据失败:", error);
    }
  };
  // 初始化
  onMounted(() => {
    // 设置默认值为最近六个月
    const defaultRange = getLastSixMonths();
    dateRange.value = defaultRange;
    // 使用 nextTick 确保组件完全渲染后再调用
    nextTick(() => {
      getData();
    });
    resetDateRange();
  });
  // 限制月份选择范围(最多12个月)
  const disabledDate = time => {
    // 如果没有选择开始月份,不禁用任何日期
    if (
      !dateRange.value ||
      !Array.isArray(dateRange.value) ||
      !dateRange.value[0]
    ) {
      return false;
    }
    const startMonth = dayjs(dateRange.value[0]);
    const currentMonth = dayjs(time);
    // 如果当前月份在开始月份之前,禁用
    if (currentMonth.isBefore(startMonth, "month")) {
      return true;
    }
    // 计算最大允许的月份(开始月份 + 11个月 = 12个月)
    const maxMonth = startMonth.add(11, "month");
    // 禁用超过12个月的月份
    return currentMonth.isAfter(maxMonth, "month");
  };
  // 处理月份范围变化
  const handleDateChange = newRange => {
    if (!newRange || !Array.isArray(newRange) || newRange.length !== 2) {
      return;
    }
    // 验证月份范围不超过12个月
    const startDate = dayjs(newRange[0]);
    const endDate = dayjs(newRange[1]);
    const monthDiff = endDate.diff(startDate, "month");
    if (monthDiff > 11) {
      proxy.$modal.msgWarning("最多只能选择12个月份");
      // 自动调整为12个月
      const adjustedEnd = startDate.add(11, "month").format("YYYY-MM");
      dateRange.value = [newRange[0], adjustedEnd];
      getData();
      return;
    }
    dateRange.value = newRange;
    getData();
  };
  // 重置月份范围
  const resetDateRange = () => {
    // 重置为最近六个月
    dateRange.value = getLastSixMonths();
    getData();
  };
</script>
<style scoped lang="scss">
  /* 基础样式补充 */
  :root {
    --el-color-primary: #4f46e5;
  }
  /* 统计卡片样式 */
  .stats-cards {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 20px;
    margin-bottom: 20px;
    margin-bottom: 24px;
  }
  .stat-card {
    background: #fff;
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    padding: 20px;
    border: 1px solid #edf2f7;
    border-radius: 12px;
    padding: 24px;
    display: flex;
    align-items: center;
    gap: 15px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    transition: all 0.3s;
    gap: 16px;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    &:hover {
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
      transform: translateY(-2px);
      box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
      transform: translateY(-4px);
    }
    .stat-icon {
      width: 48px;
      height: 48px;
      flex-shrink: 0;
      width: 56px;
      height: 56px;
      background: #f7fafc;
      border-radius: 12px;
      display: flex;
      align-items: center;
      justify-content: center;
      img {
        width: 100%;
        height: 100%;
        object-fit: contain;
        width: 32px;
        height: 32px;
      }
    }
    .stat-content {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .stat-label {
      font-size: 14px;
      color: #666;
      line-height: 1.2;
    }
    .stat-value {
      font-size: 24px;
      font-weight: 600;
      color: #333;
      line-height: 1.2;
    }
    .stat-trend {
      font-size: 12px;
      line-height: 1.2;
      &.trend-up {
        color: #f56c6c;
      .stat-label {
        font-size: 14px;
        color: #718096;
        margin-bottom: 4px;
      }
      &.trend-down {
        color: #67c23a;
      .stat-value {
        font-size: 20px;
        font-weight: 700;
        color: #2d3748;
      }
    }
  }
  /* 图表行布局 */
  .charts-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    margin-bottom: 20px;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
    gap: 24px;
    margin-bottom: 24px;
  }
  .chart-card {
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    :deep(.el-card__body) {
      padding: 20px !important;
    }
  }
  .trend-chart-card {
    border: 1px solid #e4e7ed;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
    :deep(.el-card__body) {
      padding: 20px !important;
    }
  }
  /* 饼图容器 */
  .pie-chart-container {
    position: relative;
    .pie-stats {
      display: flex;
      justify-content: space-between;
      gap: 20px;
      margin-top: 20px;
      .bar-stat-item {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
        padding: 15px;
        background: #f5f7fa;
        border-radius: 6px;
        flex: 1;
        .bar-stat-label {
          font-size: 14px;
          color: #666;
        }
        .bar-stat-value {
          font-size: 18px;
          font-weight: 600;
          color: #333;
        }
      }
    }
  }
  /* 柱状图头部统计 */
  .bar-chart-header {
    display: flex;
    justify-content: space-between;
    gap: 20px;
    margin-bottom: 20px;
    .bar-stat-item {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 8px;
      padding: 15px;
      background: #f5f7fa;
      border-radius: 6px;
      flex: 1;
      .bar-stat-label {
        font-size: 14px;
        color: #666;
      }
      .bar-stat-value {
        font-size: 18px;
        font-weight: 600;
        color: #333;
      }
    }
  }
  /* 标题样式 */
  .section-title {
    position: relative;
    font-size: 18px;
    color: #333;
    padding-left: 12px;
    margin-bottom: 20px;
    font-weight: 700;
    &::before {
      position: absolute;
      left: 0;
      top: 2px;
      content: "";
      width: 4px;
      height: 18px;
      background-color: #002fa7;
      border-radius: 2px;
    }
  }
  /* 响应式设计 */
  @media (max-width: 1400px) {
    .stats-cards {
      grid-template-columns: repeat(3, 1fr);
    }
  }
  @media (max-width: 1024px) {
    .stats-cards {
  @media (min-width: 1200px) {
    .charts-row {
      grid-template-columns: repeat(2, 1fr);
    }
  }
    .charts-row {
      grid-template-columns: 1fr;
  .chart-card,
  .trend-chart-card {
    border-radius: 16px;
    border: none;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
    .card-header {
      display: flex;
      align-items: center;
      gap: 8px;
      .header-title {
        font-size: 16px;
        font-weight: 600;
        color: #1a202c;
      }
      .el-icon {
        color: #a0aec0;
        cursor: help;
      }
    }
  }
  @media (max-width: 640px) {
    .stats-cards {
      grid-template-columns: 1fr;
  .financial-overview-container {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: nowrap;
    gap: 10px;
    padding: 20px 0;
    width: 100%;
    overflow: hidden;
    .overview-item {
      flex: 1;
      min-width: 0; // 允许在 flex 容器中缩写,防止内容撑开
      display: flex;
      justify-content: center;
      .overview-box {
        position: relative;
        width: 100%;
        max-width: 320px;
        height: 110px;
        background: #f8fafc;
        border-radius: 12px;
        padding: 12px 16px;
        display: flex;
        align-items: center;
        gap: 12px;
        overflow: hidden;
        transition: all 0.3s ease;
        &:hover {
          transform: translateY(-5px);
          box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
        }
        .icon-circle {
          flex-shrink: 0;
          width: 42px;
          height: 42px;
          border-radius: 10px;
          display: flex;
          align-items: center;
          justify-content: center;
          font-size: 20px;
          z-index: 2;
        }
        .data-content {
          z-index: 2;
          min-width: 0;
          .label {
            font-size: 13px;
            color: #718096;
            margin-bottom: 2px;
            font-weight: 500;
            white-space: nowrap;
          }
          .value {
            font-size: 18px;
            font-weight: 800;
            color: #1a202c;
            line-height: 1.2;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
          }
          .unit {
            font-size: 11px;
            color: #a0aec0;
          }
        }
        .bg-decoration {
          position: absolute;
          right: -5px;
          bottom: -5px;
          font-size: 32px;
          font-weight: 950;
          color: rgba(0, 0, 0, 0.03);
          font-style: italic;
          user-select: none;
          z-index: 1;
        }
      }
      &.income {
        .icon-circle {
          background: #eef2ff;
          color: #4f46e5;
        }
        .overview-box {
          border-left: 5px solid #4f46e5;
        }
      }
      &.expense {
        .icon-circle {
          background: #fff7ed;
          color: #f97316;
        }
        .overview-box {
          border-left: 5px solid #f97316;
        }
      }
    }
    .profit-indicator {
      flex: 0 40%; // 固定宽度,不参与弹性缩放以保证仪表盘完整
      display: flex;
      justify-content: center;
      align-items: center;
      .profit-gauge-wrapper {
        position: relative;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        // max-width: 180px;
        .profit-center-text {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          text-align: center;
          width: 100%;
          .label {
            font-size: 12px;
            color: #718096;
            font-weight: 500;
          }
          .value {
            font-size: 20px;
            font-weight: 800;
            margin: 2px 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            &.plus {
              color: #10b981;
            }
            &.minus {
              color: #f43f5e;
            }
          }
          .rate {
            font-size: 11px;
            color: #a0aec0;
            font-weight: 500;
          }
        }
      }
    }
    // 针对非常窄的屏幕进行整体缩放
    @media (max-width: 1400px) {
      transform-origin: center;
      // 如果容器太窄,通过缩小内部元素来适应
      // 这里不使用 transform: scale 因为会影响布局流,改用内部尺寸微调
      .overview-item .overview-box {
        padding: 10px;
        gap: 8px;
        .value {
          font-size: 16px;
        }
        .icon-circle {
          width: 36px;
          height: 36px;
          font-size: 18px;
        }
      }
      .profit-indicator {
        flex: 0 40%;
        .profit-gauge-wrapper .value {
          font-size: 18px;
        }
      }
    }
  }
</style>