Ver Fonte

feat(tj): 添加收据统计页面及图表展示功能- 实现年度、季度和月度收据金额统计功能
- 集成 ECharts 图表库展示数据可视化
- 添加年份选择器以筛选不同年份的统计数据
- 设计响应式统计卡片展示关键指标
- 实现图表自适应窗口大小变化
- 添加数据加载状态提示和错误处理机制
- 使用 Element Plus 组件库构建用户界面- 添加金额格式化显示功能- 实现季度和月度数据的饼图和折线图展示- 添加渐变背景和悬停动画效果提升用户体验

nahida há 7 meses atrás
pai
commit
709b9be691
1 ficheiros alterados com 489 adições e 0 exclusões
  1. 489 0
      src/views/tj/sjtj.vue

+ 489 - 0
src/views/tj/sjtj.vue

@@ -0,0 +1,489 @@
+<script setup lang="ts">
+import { computed, onMounted, ref, watch } from 'vue'
+import { clientGet } from '@/utils/request.ts'
+import { ElMessage } from 'element-plus'
+import * as echarts from 'echarts'
+import type { BarSeriesOption, LineSeriesOption, PieSeriesOption } from 'echarts/charts'
+import type { ComposeOption } from 'echarts/core'
+import { BarChart3, Calendar, DollarSign, TrendingUp } from 'lucide-vue-next'
+
+interface ExistYearResponse extends BaseResponse {
+  data: number[]
+}
+
+interface YearlyStatisticsResponse extends BaseResponse {
+  data: Record<string, number>
+}
+
+interface QuarterlyStatisticsResponse extends BaseResponse {
+  data: Record<string, number>
+}
+
+interface MonthlyStatisticsResponse extends BaseResponse {
+  data: Record<string, number>
+}
+
+// 状态管理
+const availableYears = ref<number[]>([])
+const selectedYear = ref<number | null>(null)
+const yearlyData = ref<Record<string, number>>({})
+const quarterlyData = ref<Record<string, number>>({})
+const monthlyData = ref<Record<string, number>>({})
+const loading = ref(false)
+
+// 图表实例
+const yearlyChartRef = ref<HTMLElement | null>(null)
+const quarterlyChartRef = ref<HTMLElement | null>(null)
+const monthlyChartRef = ref<HTMLElement | null>(null)
+let yearlyChart: echarts.ECharts | null = null
+let quarterlyChart: echarts.ECharts | null = null
+let monthlyChart: echarts.ECharts | null = null
+
+// 计算统计数据
+const totalYearlyAmount = computed(() => {
+  return Object.values(yearlyData.value).reduce((sum, val) => sum + val, 0)
+})
+
+const currentYearAmount = computed(() => {
+  if (!selectedYear.value) return 0
+  return yearlyData.value[selectedYear.value] || 0
+})
+
+const totalQuarterlyAmount = computed(() => {
+  return Object.values(quarterlyData.value).reduce((sum, val) => sum + val, 0)
+})
+
+const averageMonthlyAmount = computed(() => {
+  const values = Object.values(monthlyData.value)
+  if (values.length === 0) return 0
+  return values.reduce((sum, val) => sum + val, 0) / values.length
+})
+
+// 获取已经存在的年份
+const getExistingYears = async () => {
+  try {
+    const res = await clientGet<null, ExistYearResponse>('/AReceiptInfo/getExistingYears')
+    if (res.code !== 200) {
+      ElMessage.error(res.msg)
+      return
+    }
+    availableYears.value = res.data.sort((a, b) => b - a)
+    if (availableYears.value.length > 0 && !selectedYear.value) {
+      selectedYear.value = availableYears.value[0]
+    }
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('获取年份数据失败')
+  }
+}
+
+// 计算年份的收据金额
+const getCalculateYearlyStatistics = async () => {
+  try {
+    const res = await clientGet<null, YearlyStatisticsResponse>(
+      '/AReceiptInfo/calculateYearlyStatistics',
+    )
+    if (res.code !== 200) {
+      ElMessage.error(res.msg)
+      return
+    }
+    yearlyData.value = res.data
+    renderYearlyChart()
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('获取年度统计失败')
+  }
+}
+
+// 传入年份返回对应的季度的数据
+const getCalculateQuarterlyStatistics = async (year: number) => {
+  try {
+    const res = await clientGet<{ year: number }, QuarterlyStatisticsResponse>(
+      '/AReceiptInfo/calculateQuarterlyStatistics',
+      {
+        params: { year },
+      },
+    )
+    if (res.code !== 200) {
+      ElMessage.error(res.msg)
+      return
+    }
+    quarterlyData.value = res.data
+    renderQuarterlyChart()
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('获取季度统计失败')
+  }
+}
+
+// 传入年份返回对应的月份的数据
+const getCalculateMonthlyStatistics = async (year: number) => {
+  try {
+    const res = await clientGet<{ year: number }, MonthlyStatisticsResponse>(
+      '/AReceiptInfo/calculateMonthlyStatistics',
+      {
+        params: { year },
+      },
+    )
+    if (res.code !== 200) {
+      ElMessage.error(res.msg)
+      return
+    }
+    monthlyData.value = res.data
+    renderMonthlyChart()
+  } catch (error) {
+    console.error(error)
+    ElMessage.error('获取月度统计失败')
+  }
+}
+
+// 渲染年度统计图表
+const renderYearlyChart = () => {
+  if (!yearlyChartRef.value) return
+
+  if (!yearlyChart) {
+    yearlyChart = echarts.init(yearlyChartRef.value)
+  }
+
+  const years = Object.keys(yearlyData.value).sort()
+  const values = years.map((year) => yearlyData.value[year])
+
+  const option: ComposeOption<BarSeriesOption> = {
+    title: {
+      text: '年度收据金额统计',
+      left: 'center',
+      textStyle: {
+        fontSize: 16,
+        fontWeight: 'bold',
+      },
+    },
+    tooltip: {
+      trigger: 'axis',
+      formatter: '{b}: ¥{c}',
+    },
+    xAxis: {
+      type: 'category',
+      data: years,
+      axisLabel: {
+        rotate: 0,
+      },
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        formatter: '¥{value}',
+      },
+    },
+    series: [
+      {
+        data: values,
+        type: 'bar',
+        itemStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: '#4f46e5' },
+            { offset: 1, color: '#818cf8' },
+          ]),
+        },
+        label: {
+          show: true,
+          position: 'top',
+          formatter: '¥{c}',
+        },
+      },
+    ],
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '3%',
+      containLabel: true,
+    },
+  }
+
+  yearlyChart.setOption(option)
+}
+
+// 渲染季度统计图表
+const renderQuarterlyChart = () => {
+  if (!quarterlyChartRef.value) return
+
+  if (!quarterlyChart) {
+    quarterlyChart = echarts.init(quarterlyChartRef.value)
+  }
+
+  const quarters = Object.keys(quarterlyData.value).sort()
+  const values = quarters.map((q) => quarterlyData.value[q])
+
+  const option: ComposeOption<PieSeriesOption> = {
+    title: {
+      text: `${selectedYear.value}年季度收据金额统计`,
+      left: 'center',
+      textStyle: {
+        fontSize: 16,
+        fontWeight: 'bold',
+      },
+    },
+    tooltip: {
+      trigger: 'item',
+      formatter: '{b}: ¥{c} ({d}%)',
+    },
+    legend: {
+      orient: 'vertical',
+      left: 'left',
+      top: 'middle',
+    },
+    series: [
+      {
+        type: 'pie',
+        radius: ['40%', '70%'],
+        avoidLabelOverlap: false,
+        itemStyle: {
+          borderRadius: 10,
+          borderColor: '#fff',
+          borderWidth: 2,
+        },
+        label: {
+          show: true,
+          formatter: '{b}: ¥{c}',
+        },
+        emphasis: {
+          label: {
+            show: true,
+            fontSize: 16,
+            fontWeight: 'bold',
+          },
+        },
+        data: quarters.map((q, index) => ({
+          value: values[index],
+          name: q,
+          itemStyle: {
+            color: ['#4f46e5', '#06b6d4', '#10b981', '#f59e0b'][index % 4],
+          },
+        })),
+      },
+    ],
+  }
+
+  quarterlyChart.setOption(option)
+}
+
+// 渲染月度统计图表
+const renderMonthlyChart = () => {
+  if (!monthlyChartRef.value) return
+
+  if (!monthlyChart) {
+    monthlyChart = echarts.init(monthlyChartRef.value)
+  }
+
+  const months = Object.keys(monthlyData.value).sort()
+  const values = months.map((m) => monthlyData.value[m])
+
+  const option: ComposeOption<LineSeriesOption> = {
+    title: {
+      text: `${selectedYear.value}年月度收据金额统计`,
+      left: 'center',
+      textStyle: {
+        fontSize: 16,
+        fontWeight: 'bold',
+      },
+    },
+    tooltip: {
+      trigger: 'axis',
+      formatter: '{b}: ¥{c}',
+    },
+    xAxis: {
+      type: 'category',
+      data: months.map((m) => m.split('-')[1] + '月'),
+      axisLabel: {
+        rotate: 45,
+      },
+    },
+    yAxis: {
+      type: 'value',
+      axisLabel: {
+        formatter: '¥{value}',
+      },
+    },
+    series: [
+      {
+        data: values,
+        type: 'line',
+        smooth: true,
+        areaStyle: {
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+            { offset: 0, color: 'rgba(79, 70, 229, 0.3)' },
+            { offset: 1, color: 'rgba(79, 70, 229, 0.05)' },
+          ]),
+        },
+        itemStyle: {
+          color: '#4f46e5',
+        },
+        lineStyle: {
+          width: 3,
+        },
+      },
+    ],
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '10%',
+      containLabel: true,
+    },
+  }
+
+  monthlyChart.setOption(option)
+}
+
+// 加载数据
+const loadData = async () => {
+  loading.value = true
+  try {
+    await getCalculateYearlyStatistics()
+    if (selectedYear.value) {
+      await Promise.all([
+        getCalculateQuarterlyStatistics(selectedYear.value),
+        getCalculateMonthlyStatistics(selectedYear.value),
+      ])
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+// 监听年份变化
+watch(selectedYear, (newYear) => {
+  if (newYear) {
+    loadData()
+  }
+})
+
+// 初始化
+onMounted(async () => {
+  await getExistingYears()
+  await loadData()
+
+  // 响应式调整图表大小
+  window.addEventListener('resize', () => {
+    yearlyChart?.resize()
+    quarterlyChart?.resize()
+    monthlyChart?.resize()
+  })
+})
+
+// 格式化金额
+const formatAmount = (amount: number) => {
+  return new Intl.NumberFormat('zh-CN', {
+    style: 'currency',
+    currency: 'CNY',
+  }).format(amount)
+}
+</script>
+
+<template>
+  <div class="min-h-screen bg-gradient-to-br from-indigo-50 via-white to-purple-50 p-6">
+    <div class="max-full mx-auto">
+      <div class="mb-8">
+        <h1 class="text-4xl font-bold text-gray-900 mb-2 flex items-center gap-3">
+          <BarChart3 class="w-10 h-10 text-indigo-600" />
+        </h1>
+        <h1 class="text-gray-600">收据统计</h1>
+      </div>
+
+      <div class="mb-6 flex items-center gap-4">
+        <Calendar class="w-5 h-5 text-indigo-600" />
+        <span class="text-gray-700 font-medium">选择年份:</span>
+        <el-select v-model="selectedYear" placeholder="请选择年份" size="large" class="w-48!">
+          <el-option
+            v-for="year in availableYears"
+            :key="year"
+            :label="`${year}年`"
+            :value="year"
+          />
+        </el-select>
+      </div>
+
+      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
+        <div
+          class="bg-white rounded-2xl shadow-lg p-6 border-l-4 border-indigo-500 hover:shadow-xl transition-shadow"
+        >
+          <div class="flex items-center justify-between mb-4">
+            <div class="bg-indigo-100 p-3 rounded-xl">
+              <DollarSign class="w-6 h-6 text-indigo-600" />
+            </div>
+            <TrendingUp class="w-5 h-5 text-green-500" />
+          </div>
+          <h3 class="text-gray-600 text-sm font-medium mb-1">总收据金额</h3>
+          <p class="text-3xl font-bold text-gray-900">{{ formatAmount(totalYearlyAmount) }}</p>
+        </div>
+
+        <div
+          class="bg-white rounded-2xl shadow-lg p-6 border-l-4 border-cyan-500 hover:shadow-xl transition-shadow"
+        >
+          <div class="flex items-center justify-between mb-4">
+            <div class="bg-cyan-100 p-3 rounded-xl">
+              <Calendar class="w-6 h-6 text-cyan-600" />
+            </div>
+          </div>
+          <h3 class="text-gray-600 text-sm font-medium mb-1">{{ selectedYear }}年金额</h3>
+          <p class="text-3xl font-bold text-gray-900">{{ formatAmount(currentYearAmount) }}</p>
+        </div>
+
+        <div
+          class="bg-white rounded-2xl shadow-lg p-6 border-l-4 border-green-500 hover:shadow-xl transition-shadow"
+        >
+          <div class="flex items-center justify-between mb-4">
+            <div class="bg-green-100 p-3 rounded-xl">
+              <BarChart3 class="w-6 h-6 text-green-600" />
+            </div>
+          </div>
+          <h3 class="text-gray-600 text-sm font-medium mb-1">季度总金额</h3>
+          <p class="text-3xl font-bold text-gray-900">{{ formatAmount(totalQuarterlyAmount) }}</p>
+        </div>
+
+        <div
+          class="bg-white rounded-2xl shadow-lg p-6 border-l-4 border-amber-500 hover:shadow-xl transition-shadow"
+        >
+          <div class="flex items-center justify-between mb-4">
+            <div class="bg-amber-100 p-3 rounded-xl">
+              <TrendingUp class="w-6 h-6 text-amber-600" />
+            </div>
+          </div>
+          <h3 class="text-gray-600 text-sm font-medium mb-1">月均金额</h3>
+          <p class="text-3xl font-bold text-gray-900">{{ formatAmount(averageMonthlyAmount) }}</p>
+        </div>
+      </div>
+
+      <div v-loading="loading" class="space-y-6">
+        <div class="bg-white rounded-2xl shadow-lg p-6 hover:shadow-xl transition-shadow">
+          <div ref="yearlyChartRef" class="w-full h-96"></div>
+        </div>
+
+        <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+          <div class="bg-white rounded-2xl shadow-lg p-6 hover:shadow-xl transition-shadow">
+            <div ref="quarterlyChartRef" class="w-full h-96"></div>
+          </div>
+
+          <div class="bg-white rounded-2xl shadow-lg p-6 hover:shadow-xl transition-shadow">
+            <div ref="monthlyChartRef" class="w-full h-96"></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+/* Element Plus 样式覆盖 */
+:deep(.el-select) {
+  --el-select-border-color-hover: #4f46e5;
+  --el-select-input-focus-border-color: #4f46e5;
+}
+
+:deep(.el-select__wrapper) {
+  border-radius: 0.75rem;
+  box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
+}
+
+:deep(.el-select__wrapper:hover) {
+  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
+}
+</style>