huminmin
10 小时以前 cdf8190c92a536dabdbd3dfd6758cf67320ff6df
src/views/financialManagement/financialStatements/index.vue
@@ -0,0 +1,597 @@
 <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="monthrange"
        format="YYYY-MM"
        value-format="YYYY-MM"
        range-separator="至"
        start-placeholder="开始月份"
        end-placeholder="结束月份"
        :disabled-date="disabledDate"
        @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>
import { ref, computed, onMounted, reactive, nextTick, getCurrentInstance } 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(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 pageInfo = ref({
})
// 获取最近六个月的范围
const getLastSixMonths = () => {
  const endMonth = dayjs().format('YYYY-MM');
  const startMonth = dayjs().subtract(5, 'month').format('YYYY-MM');
  return [startMonth, endMonth];
};
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;
  }
  // 验证日期格式并转换为完整日期
  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');
  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)}`
      }))
    }
  } 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);
  }
};
// 初始化
onMounted(() => {
  // 设置默认值为最近六个月
  const defaultRange = getLastSixMonths();
  dateRange.value = defaultRange;
  // 使用 nextTick 确保组件完全渲染后再调用
  nextTick(() => {
    getData();
  });
});
// 限制月份选择范围(最多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;
}
.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>