| | |
| | | </el-form-item> |
| | | |
| | | <el-form-item> |
| | | <el-button type="primary" icon="el-icon-search" @click="handleSearch">查询</el-button> |
| | | <el-button icon="el-icon-refresh" @click="handleReset">重置</el-button> |
| | | <el-button type="success" icon="el-icon-download" @click="handleExport">导出</el-button> |
| | | <el-button type="primary" icon="el-icon-search" @click="handleSearch" |
| | | >查询</el-button |
| | | > |
| | | <el-button icon="el-icon-refresh" @click="handleReset" |
| | | >重置</el-button |
| | | > |
| | | <el-button |
| | | type="success" |
| | | icon="el-icon-download" |
| | | @click="handleExport" |
| | | >导出</el-button |
| | | > |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- 数据概览 --> |
| | | <el-row :gutter="20" class="overview-row"> |
| | | <el-col :span="6" v-for="item in overviewData" :key="item.title"> |
| | | <el-col :span="8" v-for="item in overviewData" :key="item.title"> |
| | | <el-card class="overview-card"> |
| | | <div class="overview-content"> |
| | | <div class="overview-icon" :style="{ backgroundColor: item.color }"> |
| | |
| | | type="text" |
| | | @click="toggleChartType('trend')" |
| | | > |
| | | {{ chartTypes.trend === 'line' ? '柱状图' : '折线图' }} |
| | | {{ chartTypes.trend === "line" ? "柱状图" : "折线图" }} |
| | | </el-button> |
| | | </div> |
| | | <EChartsWrapper |
| | |
| | | border |
| | | stripe |
| | | > |
| | | <el-table-column label="地区" prop="region" align="center" /> |
| | | <el-table-column label="月份" prop="month" align="center" > |
| | | <el-table-column label="地区" prop="region" align="center" /> |
| | | <el-table-column label="月份" prop="month" align="center"> |
| | | <template slot-scope="{ row }"> |
| | | {{ formatMonth(row.month) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="潜在捐献案例数" prop="potentialCount" align="center" /> |
| | | <el-table-column label="完成捐献数量" prop="completedCount" align="center" /> |
| | | <el-table-column label="完成率" prop="completionRate" align="center" > |
| | | <el-table-column |
| | | label="潜在捐献案例数" |
| | | prop="potentialCount" |
| | | align="center" |
| | | /> |
| | | <el-table-column |
| | | label="完成捐献数量" |
| | | prop="completedCount" |
| | | align="center" |
| | | /> |
| | | <el-table-column label="完成率" prop="completionRate" align="center"> |
| | | <template slot-scope="{ row }"> |
| | | <el-progress |
| | | :percentage="row.completionRate" |
| | |
| | | <span>{{ row.completionRate }}%</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="案例明细" align="center" > |
| | | <el-table-column label="案例明细" align="center"> |
| | | <template slot-scope="{ row }"> |
| | | <el-button |
| | | type="text" |
| | | size="mini" |
| | | @click="viewCaseDetail(row)" |
| | | > |
| | | <el-button type="text" size="mini" @click="viewCaseDetail(row)"> |
| | | 查看({{ row.caseCount || 0 }}) |
| | | </el-button> |
| | | </template> |
| | |
| | | </el-card> |
| | | |
| | | <!-- 案例明细弹窗 --> |
| | | <el-dialog |
| | | title="案例明细" |
| | | :visible.sync="showDetail" |
| | | |
| | | append-to-body |
| | | > |
| | | <case-detail |
| | | :query="detailQuery" |
| | | @close="showDetail = false" |
| | | /> |
| | | <el-dialog title="案例明细" :visible.sync="showDetail" append-to-body> |
| | | <case-detail :query="detailQuery" @close="showDetail = false" /> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import EChartsWrapper from '@/components/charts/EChartsWrapper.vue'; |
| | | import CaseDetail from './components/CaseDetail.vue'; |
| | | |
| | | import EChartsWrapper from "@/components/charts/EChartsWrapper.vue"; |
| | | import CaseDetail from "./components/CaseDetail.vue"; |
| | | import { caseTrendArea, caseTrend, caseTrendMoon, totals,trendList } from "@/api/businessApi"; |
| | | import { toTotalsList, toMonth, toNum } from "@/utils/totals"; |
| | | import statisticsMode from "@/utils/statisticsMode"; |
| | | import { COLORS, axisTooltip, grid, xAxis, yAxis, legend, barSeries, lineSeries } from "@/utils/chartStyle"; |
| | | export default { |
| | | name: 'CaseStatistics', |
| | | name: "CaseStatistics", |
| | | components: { |
| | | EChartsWrapper, |
| | | CaseDetail |
| | |
| | | limit: 10 |
| | | }, |
| | | |
| | | // 地区选项 |
| | | // 地区选项(value 直接传地区名称给接口 area 字段) |
| | | regionOptions: [ |
| | | { label: '青岛地区', value: 'qingdao' }, |
| | | { label: '日照地区', value: 'rizhao' }, |
| | | { label: '济南地区', value: 'jinan' }, |
| | | { label: '烟台地区', value: 'yantai' }, |
| | | { label: '威海地区', value: 'weihai' }, |
| | | { label: '潍坊地区', value: 'weifang' }, |
| | | { label: '临沂地区', value: 'linyi' } |
| | | { label: "青岛地区", value: "青岛地区" }, |
| | | { label: "日照地区", value: "日照地区" }, |
| | | { label: "济南地区", value: "济南地区" }, |
| | | { label: "烟台地区", value: "烟台地区" }, |
| | | { label: "威海地区", value: "威海地区" }, |
| | | { label: "潍坊地区", value: "潍坊地区" }, |
| | | { label: "临沂地区", value: "临沂地区" } |
| | | ], |
| | | |
| | | // 加载状态 |
| | |
| | | |
| | | // 概览数据 |
| | | overviewData: [ |
| | | { title: '总潜在捐献案例', value: 0, desc: '统计周期内', icon: 'el-icon-s-flag', color: '#409EFF' }, |
| | | { title: '总完成捐献数量', value: 0, desc: '统计周期内', icon: 'el-icon-circle-check', color: '#67C23A' }, |
| | | { title: '平均完成率', value: '0%', desc: '统计周期内', icon: 'el-icon-s-data', color: '#E6A23C' }, |
| | | { title: '涉及案例数', value: 0, desc: '统计周期内', icon: 'el-icon-document', color: '#F56C6C' } |
| | | { |
| | | title: "总潜在捐献案例", |
| | | value: 0, |
| | | desc: "统计周期内", |
| | | icon: "el-icon-s-flag", |
| | | color: "#5B8FF9" |
| | | }, |
| | | { |
| | | title: "总完成捐献数量", |
| | | value: 0, |
| | | desc: "统计周期内", |
| | | icon: "el-icon-circle-check", |
| | | color: "#5AD8A6" |
| | | }, |
| | | { |
| | | title: "平均完成率", |
| | | value: "0%", |
| | | desc: "统计周期内", |
| | | icon: "el-icon-s-data", |
| | | color: "#F6BD16" |
| | | } |
| | | ], |
| | | |
| | | // 图表类型 |
| | | chartTypes: { |
| | | trend: 'line' |
| | | trend: "line" |
| | | }, |
| | | |
| | | // 图表数据 |
| | |
| | | |
| | | // 弹窗控制 |
| | | showDetail: false, |
| | | detailQuery: {}, |
| | | |
| | | // 模拟数据 |
| | | mockData: { |
| | | table: [ |
| | | { id: 1, region: '青岛地区', month: '2024-01', potentialCount: 15, completedCount: 8, completionRate: 53.3, caseCount: 5 }, |
| | | { id: 2, region: '青岛地区', month: '2024-02', potentialCount: 18, completedCount: 10, completionRate: 55.6, caseCount: 6 }, |
| | | { id: 3, region: '日照地区', month: '2024-01', potentialCount: 8, completedCount: 4, completionRate: 50.0, caseCount: 3 }, |
| | | { id: 4, region: '日照地区', month: '2024-02', potentialCount: 10, completedCount: 6, completionRate: 60.0, caseCount: 4 }, |
| | | { id: 5, region: '济南地区', month: '2024-01', potentialCount: 20, completedCount: 12, completionRate: 60.0, caseCount: 7 }, |
| | | { id: 6, region: '济南地区', month: '2024-02', potentialCount: 22, completedCount: 14, completionRate: 63.6, caseCount: 8 } |
| | | ], |
| | | trend: [ |
| | | { month: '2024-01', potential: 55, completed: 31 }, |
| | | { month: '2024-02', potential: 64, completed: 39 }, |
| | | { month: '2024-03', potential: 58, completed: 35 }, |
| | | { month: '2024-04', potential: 62, completed: 40 }, |
| | | { month: '2024-05', potential: 68, completed: 45 }, |
| | | { month: '2024-06', potential: 72, completed: 50 } |
| | | ], |
| | | region: [ |
| | | { region: '青岛地区', potential: 150, completed: 85 }, |
| | | { region: '日照地区', potential: 80, completed: 45 }, |
| | | { region: '济南地区', potential: 200, completed: 120 }, |
| | | { region: '烟台地区', potential: 100, completed: 60 } |
| | | ] |
| | | } |
| | | detailQuery: {} |
| | | }; |
| | | }, |
| | | created() { |
| | | this.loadData(); |
| | | // 数据源(mock/真实接口)切换时重新加载 |
| | | this.$watch(() => statisticsMode.useMock, () => this.loadData()); |
| | | }, |
| | | methods: { |
| | | // 组装接口查询条件 |
| | | buildQuery() { |
| | | const q = {}; |
| | | if (this.query.region && this.query.region.length) { |
| | | q.area = this.query.region.join(","); |
| | | } |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | q.startTime = this.query.monthRange[0]; |
| | | q.endTime = this.query.monthRange[1]; |
| | | } |
| | | return q; |
| | | }, |
| | | |
| | | // 加载数据 |
| | | async loadData() { |
| | | this.loading = true; |
| | | try { |
| | | // 模拟接口调用 |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | const q = this.buildQuery(); |
| | | const [areaRes, trendRes, moonRes, totalsRes] = await Promise.all([ |
| | | caseTrendArea(q), |
| | | caseTrend(q), |
| | | caseTrendMoon(q), |
| | | totals(q) |
| | | ]); |
| | | |
| | | // 处理表格数据 |
| | | let filteredData = [...this.mockData.table]; |
| | | // 地区分布对比 |
| | | this.chartData.region = toTotalsList(areaRes).map(item => ({ |
| | | region: item.area || item.region || "", |
| | | potential: toNum(item.potentialCount), |
| | | completed: toNum(item.completeCount) |
| | | })); |
| | | |
| | | if (this.query.region.length > 0) { |
| | | filteredData = filteredData.filter(item => |
| | | this.query.region.includes(item.region.replace('地区', '').toLowerCase()) |
| | | ); |
| | | } |
| | | // 案例趋势 |
| | | this.chartData.trend = toTotalsList(trendRes).map(item => ({ |
| | | month: toMonth(item.donateTime) || toMonth(item.month), |
| | | potential: toNum(item.potentialCount), |
| | | completed: toNum(item.completeCount) |
| | | })); |
| | | |
| | | if (this.query.monthRange && this.query.monthRange.length === 2) { |
| | | const [start, end] = this.query.monthRange; |
| | | filteredData = filteredData.filter(item => |
| | | item.month >= start && item.month <= end |
| | | ); |
| | | } |
| | | // 明细表(地区 x 月份) |
| | | const all = toTotalsList(moonRes).map(item => { |
| | | const potential = toNum(item.potentialCount); |
| | | const completed = toNum(item.completeCount); |
| | | const rate = |
| | | potential > 0 |
| | | ? Number(((completed / potential) * 100).toFixed(1)) |
| | | : 0; |
| | | return { |
| | | region: item.area || item.region || "", |
| | | month: toMonth(item.donateTime) || toMonth(item.month), |
| | | potentialCount: potential, |
| | | completedCount: completed, |
| | | completionRate: rate, |
| | | caseCount: toNum(item.totalCount) || potential |
| | | }; |
| | | }); |
| | | this.total = all.length; |
| | | const start = (this.query.page - 1) * this.query.limit; |
| | | this.tableData = all.slice(start, start + this.query.limit); |
| | | |
| | | this.tableData = filteredData; |
| | | this.total = filteredData.length; |
| | | |
| | | // 更新概览数据 |
| | | this.updateOverviewData(filteredData); |
| | | |
| | | // 更新图表数据 |
| | | this.chartData.trend = [...this.mockData.trend]; |
| | | this.chartData.region = [...this.mockData.region]; |
| | | |
| | | this.updateOverviewData(toTotalsList(totalsRes)); |
| | | } catch (error) { |
| | | console.error('加载数据失败:', error); |
| | | this.$message.error('数据加载失败'); |
| | | console.error("加载数据失败:", error); |
| | | this.$message.error("数据加载失败"); |
| | | } finally { |
| | | this.loading = false; |
| | | } |
| | | }, |
| | | |
| | | // 更新概览数据 |
| | | updateOverviewData(data) { |
| | | if (data.length === 0) return; |
| | | |
| | | const totalPotential = data.reduce((sum, item) => sum + item.potentialCount, 0); |
| | | const totalCompleted = data.reduce((sum, item) => sum + item.completedCount, 0); |
| | | const avgRate = data.length > 0 |
| | | ? (data.reduce((sum, item) => sum + item.completionRate, 0) / data.length).toFixed(1) |
| | | : 0; |
| | | const totalCases = data.reduce((sum, item) => sum + (item.caseCount || 0), 0); |
| | | // 更新概览数据(总潜在/总完成捐献数来自 /opo/totals/totals 接口) |
| | | updateOverviewData(totalsList) { |
| | | const totalPotential = totalsList.reduce( |
| | | (sum, item) => sum + toNum(item.totalPotential), |
| | | 0 |
| | | ); |
| | | const totalCompleted = totalsList.reduce( |
| | | (sum, item) => sum + toNum(item.totalCompletion), |
| | | 0 |
| | | ); |
| | | const avgRate = |
| | | totalPotential > 0 |
| | | ? ((totalCompleted / totalPotential) * 100).toFixed(1) |
| | | : 0; |
| | | |
| | | this.overviewData[0].value = totalPotential; |
| | | this.overviewData[1].value = totalCompleted; |
| | | this.overviewData[2].value = `${avgRate}%`; |
| | | this.overviewData[3].value = totalCases; |
| | | }, |
| | | |
| | | // 获取趋势图表配置 |
| | | getTrendChartOptions() { |
| | | const isLine = this.chartTypes.trend === 'line'; |
| | | const isLine = this.chartTypes.trend === "line"; |
| | | const xData = this.chartData.trend.map(item => item.month); |
| | | const potential = this.chartData.trend.map(item => item.potential); |
| | | const completed = this.chartData.trend.map(item => item.completed); |
| | | |
| | | const series = isLine |
| | | ? [ |
| | | lineSeries("潜在捐献数", potential, COLORS.blue, { area: true }), |
| | | lineSeries("完成捐献数", completed, COLORS.green, { area: true }) |
| | | ] |
| | | : [ |
| | | barSeries("潜在捐献数", potential, COLORS.blue), |
| | | barSeries("完成捐献数", completed, COLORS.green) |
| | | ]; |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['潜在捐献数', '完成捐献数'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: this.chartData.trend.map(item => this.formatMonth(item.month)) |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '数量(个)' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '潜在捐献数', |
| | | type: isLine ? 'line' : 'bar', |
| | | data: this.chartData.trend.map(item => item.potential), |
| | | itemStyle: { color: '#409EFF' }, |
| | | smooth: isLine, |
| | | lineStyle: isLine ? { width: 3 } : {} |
| | | }, |
| | | { |
| | | name: '完成捐献数', |
| | | type: isLine ? 'line' : 'bar', |
| | | data: this.chartData.trend.map(item => item.completed), |
| | | itemStyle: { color: '#67C23A' }, |
| | | smooth: isLine, |
| | | lineStyle: isLine ? { width: 3 } : {} |
| | | } |
| | | ] |
| | | tooltip: axisTooltip(), |
| | | legend: legend(["潜在捐献数", "完成捐献数"]), |
| | | grid: grid(), |
| | | xAxis: xAxis(xData), |
| | | yAxis: yAxis(), |
| | | series |
| | | }; |
| | | }, |
| | | |
| | | // 获取地区图表配置 |
| | | getRegionChartOptions() { |
| | | return { |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['潜在捐献数', '完成捐献数'] |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: this.chartData.region.map(item => item.region) |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: '数量(个)' |
| | | }, |
| | | tooltip: axisTooltip(), |
| | | legend: legend(["潜在捐献数", "完成捐献数"]), |
| | | grid: grid(), |
| | | xAxis: xAxis(this.chartData.region.map(item => item.region)), |
| | | yAxis: yAxis(), |
| | | series: [ |
| | | { |
| | | name: '潜在捐献数', |
| | | type: 'bar', |
| | | data: this.chartData.region.map(item => item.potential), |
| | | itemStyle: { color: '#409EFF' } |
| | | }, |
| | | { |
| | | name: '完成捐献数', |
| | | type: 'bar', |
| | | data: this.chartData.region.map(item => item.completed), |
| | | itemStyle: { color: '#67C23A' } |
| | | } |
| | | barSeries("潜在捐献数", this.chartData.region.map(item => item.potential), COLORS.blue), |
| | | barSeries("完成捐献数", this.chartData.region.map(item => item.completed), COLORS.green) |
| | | ] |
| | | }; |
| | | }, |
| | | |
| | | // 获取进度条颜色 |
| | | getProgressColor(percentage) { |
| | | if (percentage >= 60) return '#67C23A'; |
| | | if (percentage >= 50) return '#E6A23C'; |
| | | return '#F56C6C'; |
| | | if (percentage >= 60) return COLORS.green; |
| | | if (percentage >= 50) return COLORS.amber; |
| | | return COLORS.coral; |
| | | }, |
| | | |
| | | // 格式化月份显示 |
| | | formatMonth(month) { |
| | | if (!month) return ''; |
| | | return month.replace('-', '年') + '月'; |
| | | if (!month) return ""; |
| | | return month.replace("-", "年") + "月"; |
| | | }, |
| | | |
| | | // 处理查询 |
| | |
| | | |
| | | // 处理导出 |
| | | handleExport() { |
| | | this.$message.info('导出功能开发中...'); |
| | | // 这里可以调用导出接口 |
| | | this.$message.info("导出功能开发中..."); |
| | | }, |
| | | |
| | | // 切换图表类型 |
| | | toggleChartType(type) { |
| | | this.chartTypes[type] = this.chartTypes[type] === 'line' ? 'bar' : 'line'; |
| | | this.chartTypes[type] = this.chartTypes[type] === "line" ? "bar" : "line"; |
| | | }, |
| | | |
| | | // 查看案例详情 |
| | |
| | | |
| | | .overview-desc { |
| | | font-size: 12px; |
| | | color: #C0C4CC; |
| | | color: #c0c4cc; |
| | | } |
| | | |
| | | .chart-row { |