|
|
@@ -0,0 +1,441 @@
|
|
|
+<template>
|
|
|
+ <div class="order-value-view">
|
|
|
+ <!-- 头部区域 -->
|
|
|
+ <header class="page-header">
|
|
|
+ <h1 class="page-title">订单价值</h1>
|
|
|
+ <div class="header-controls">
|
|
|
+ <span class="control-label">时光回溯控制器</span>
|
|
|
+ <div class="control-item-group">
|
|
|
+ <button :class="{'time-tab': true, 'active': activeTab === '7d'}" @click="selectDateRange('7d')">最近7天</button>
|
|
|
+ <button :class="{'time-tab': true, 'active': activeTab === 'tm'}" @click="selectDateRange('tm')">本月</button>
|
|
|
+ <button :class="{'time-tab': true, 'active': activeTab === 'lm'}" @click="selectDateRange('lm')">上月</button>
|
|
|
+ <button :class="{'time-tab': true, 'active': activeTab === 'all'}" @click="selectDateRange('all')">全量数据</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <!-- ✨【修改点1:KPI卡片的值现在是动态的】✨ -->
|
|
|
+ <section class="kpi-cards-grid">
|
|
|
+ <KpiCard title="总交易额 (GMV)" :value="kpiData.gmv" :trend="kpiData.gmvTrend" :trend-color="getTrendColor(kpiData.gmvTrend)" icon="📈" />
|
|
|
+ <KpiCard title="P80 订单贡献比" :value="kpiData.p80Contribution" :trend="kpiData.p80Trend" :trend-color="getTrendColor(kpiData.p80Trend)" icon="🏆" />
|
|
|
+ <KpiCard title="Top 5 商品贡献比" :value="kpiData.top5Contribution" :trend="kpiData.top5Trend" :trend-color="getTrendColor(kpiData.top5Trend)" icon="🔥" />
|
|
|
+ <KpiCard title="平均支付响应" :value="kpiData.averagePaymentTime" :trend="kpiData.avgTimeTrend" :trend-color="getTrendColor(kpiData.avgTimeTrend)" icon="⏱" />
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- 图表区域 -->
|
|
|
+ <section class="charts-area">
|
|
|
+ <!-- 支付决策漏斗图 -->
|
|
|
+ <div class="chart-wrapper funnel-chart-wrapper">
|
|
|
+ <FunnelChart :date-range="currentDateRange" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 明星商品价值环图 (Top 5) -->
|
|
|
+ <div class="chart-wrapper">
|
|
|
+ <Top5PieChart :date-range="currentDateRange" />
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- 订单价值漏损分析 (退款) - 铺满整排 -->
|
|
|
+ <section class="leakage-section">
|
|
|
+ <div class="chart-wrapper leakage-card-wrapper full-width">
|
|
|
+ <LeakageCard :date-range="currentDateRange" />
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import axios from 'axios';
|
|
|
+import KpiCard from './KpiCard/index.vue';
|
|
|
+import FunnelChart from './FunnelChart/index.vue';
|
|
|
+import Top5PieChart from './Top5PieChart/index.vue';
|
|
|
+import LeakageCard from './LeakageCard/index.vue';
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'OrderValue',
|
|
|
+ components: {
|
|
|
+ KpiCard,
|
|
|
+ FunnelChart,
|
|
|
+ Top5PieChart,
|
|
|
+ LeakageCard
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ selectedDate: new Date().toISOString().split('T')[0],
|
|
|
+ activeTab: '7d',
|
|
|
+ maxDate: '',
|
|
|
+ currentDateRange: { start: '', end: '' },
|
|
|
+ kpiData: {
|
|
|
+ gmv: '?0',
|
|
|
+ gmvTrend: '+0%',
|
|
|
+ p80Contribution: '0%',
|
|
|
+ p80Trend: '+0%',
|
|
|
+ top5Contribution: '0%',
|
|
|
+ top5Trend: '+0%',
|
|
|
+ averagePaymentTime: '00:00',
|
|
|
+ avgTimeTrend: '+0%'
|
|
|
+ }
|
|
|
+ };
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ selectedDate(newVal) {
|
|
|
+ if (this.activeTab === '7d') {
|
|
|
+ const startDate = this.formatYmd(this.addDays(newVal, -6));
|
|
|
+ const endDate = this.formatYmd(newVal);
|
|
|
+ this.currentDateRange = { start: startDate, end: endDate };
|
|
|
+ this.fetchAllApiData({ start: startDate, end: endDate });
|
|
|
+ } else {
|
|
|
+ this.selectDateRange(this.activeTab);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.initDashboard();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ pad2(value) {
|
|
|
+ return String(value).padStart(2, '0');
|
|
|
+ },
|
|
|
+ toDate(value) {
|
|
|
+ if (value instanceof Date) return new Date(value.getTime());
|
|
|
+ if (!value) return new Date();
|
|
|
+ return new Date(`${value}T00:00:00`);
|
|
|
+ },
|
|
|
+ formatYmd(value) {
|
|
|
+ const date = this.toDate(value);
|
|
|
+ return `${date.getFullYear()}-${this.pad2(date.getMonth() + 1)}-${this.pad2(date.getDate())}`;
|
|
|
+ },
|
|
|
+ addDays(value, days) {
|
|
|
+ const date = this.toDate(value);
|
|
|
+ date.setDate(date.getDate() + days);
|
|
|
+ return date;
|
|
|
+ },
|
|
|
+ addMonths(value, months) {
|
|
|
+ const date = this.toDate(value);
|
|
|
+ date.setMonth(date.getMonth() + months);
|
|
|
+ return date;
|
|
|
+ },
|
|
|
+ startOfMonth(value) {
|
|
|
+ const date = this.toDate(value);
|
|
|
+ return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
|
+ },
|
|
|
+ endOfMonth(value) {
|
|
|
+ const date = this.toDate(value);
|
|
|
+ return new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
|
|
+ },
|
|
|
+ async fetchAllApiData(customRange = null) {
|
|
|
+ try {
|
|
|
+ let startDate;
|
|
|
+ let endDate;
|
|
|
+ if (customRange && customRange.start && customRange.end) {
|
|
|
+ startDate = customRange.start;
|
|
|
+ endDate = customRange.end;
|
|
|
+ } else {
|
|
|
+ endDate = this.selectedDate;
|
|
|
+ startDate = this.formatYmd(this.addDays(endDate, -6));
|
|
|
+ }
|
|
|
+
|
|
|
+ const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
|
|
|
+ axios.get(`/api/analysis/gmv?startDate=${startDate}&endDate=${endDate}`),
|
|
|
+ axios.get(`/api/analysis/r-big?startDate=${startDate}&endDate=${endDate}`),
|
|
|
+ axios.get(`/api/analysis/top5-percentage?startDate=${startDate}&endDate=${endDate}`),
|
|
|
+ axios.get(`/api/analysis/average-payment-time?startDate=${startDate}&endDate=${endDate}`)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const gmvValue = gmvRes.data || 0;
|
|
|
+ this.kpiData.gmv = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(gmvValue);
|
|
|
+
|
|
|
+ const p80Value = p80Res.data?.rBigRatio || 0;
|
|
|
+ this.kpiData.p80Contribution = `${Math.round(p80Value)}%`;
|
|
|
+
|
|
|
+ const top5Value = top5Res.data?.data?.top5Percentage || 0;
|
|
|
+ this.kpiData.top5Contribution = `${Math.round(top5Value)}%`;
|
|
|
+
|
|
|
+ const avgSeconds = avgTimeRes.data?.averagePaymentSeconds || 0;
|
|
|
+ const minutes = Math.floor(avgSeconds / 60);
|
|
|
+ const seconds = Math.round(avgSeconds % 60);
|
|
|
+ this.kpiData.averagePaymentTime = `${this.pad2(minutes)}:${this.pad2(seconds)} ??`;
|
|
|
+
|
|
|
+ if (this.activeTab !== 'tm') {
|
|
|
+ this.kpiData.gmvTrend = '+0%';
|
|
|
+ this.kpiData.p80Trend = '+0%';
|
|
|
+ this.kpiData.top5Trend = '+0%';
|
|
|
+ this.kpiData.avgTimeTrend = '+0%';
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('??????:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ calculateTrend(currentValue, previousValue) {
|
|
|
+ if (previousValue === 0) return '+0%';
|
|
|
+ const change = ((currentValue - previousValue) / previousValue) * 100;
|
|
|
+ const sign = change >= 0 ? '+' : '';
|
|
|
+ return `${sign}${change.toFixed(1)}%`;
|
|
|
+ },
|
|
|
+ parseTimeString(timeString) {
|
|
|
+ if (!timeString) return 0;
|
|
|
+ const timeMatch = timeString.match(/(\d{2}):(\d{2})/);
|
|
|
+ if (timeMatch) {
|
|
|
+ const minutes = parseInt(timeMatch[1], 10);
|
|
|
+ const seconds = parseInt(timeMatch[2], 10);
|
|
|
+ return minutes * 60 + seconds;
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+ },
|
|
|
+ getTrendColor(trend) {
|
|
|
+ if (!trend) return 'green';
|
|
|
+ const value = parseFloat(trend);
|
|
|
+ return value >= 0 ? 'green' : 'red';
|
|
|
+ },
|
|
|
+ async fetchPreviousMonthData(currentStart) {
|
|
|
+ try {
|
|
|
+ const currentStartDate = this.toDate(currentStart);
|
|
|
+ const previousMonthBase = this.addMonths(currentStartDate, -1);
|
|
|
+ const previousMonthStart = this.formatYmd(this.startOfMonth(previousMonthBase));
|
|
|
+ const previousMonthEnd = this.formatYmd(this.endOfMonth(previousMonthBase));
|
|
|
+
|
|
|
+ const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
|
|
|
+ axios.get(`/api/analysis/gmv?startDate=${previousMonthStart}&endDate=${previousMonthEnd}`),
|
|
|
+ axios.get(`/api/analysis/r-big?startDate=${previousMonthStart}&endDate=${previousMonthEnd}`),
|
|
|
+ axios.get(`/api/analysis/top5-percentage?startDate=${previousMonthStart}&endDate=${previousMonthEnd}`),
|
|
|
+ axios.get(`/api/analysis/average-payment-time?startDate=${previousMonthStart}&endDate=${previousMonthEnd}`)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return {
|
|
|
+ gmv: gmvRes.data || 0,
|
|
|
+ p80: p80Res.data?.rBigRatio || 0,
|
|
|
+ top5: top5Res.data?.data?.top5Percentage || 0,
|
|
|
+ avgTime: avgTimeRes.data?.averagePaymentSeconds || 0
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.error('????????:', error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async initDashboard() {
|
|
|
+ try {
|
|
|
+ const res = await axios.get('/api/analysis/max-date');
|
|
|
+ this.maxDate = res.data;
|
|
|
+ this.selectedDate = res.data;
|
|
|
+
|
|
|
+ const startDate = this.formatYmd(this.addDays(res.data, -6));
|
|
|
+ this.currentDateRange = { start: startDate, end: res.data };
|
|
|
+ await this.fetchAllApiData();
|
|
|
+ this.kpiData.gmvTrend = '+0%';
|
|
|
+ this.kpiData.p80Trend = '+0%';
|
|
|
+ this.kpiData.top5Trend = '+0%';
|
|
|
+ this.kpiData.avgTimeTrend = '+0%';
|
|
|
+ } catch (error) {
|
|
|
+ console.error('???????????????');
|
|
|
+ const today = this.formatYmd(new Date());
|
|
|
+ this.maxDate = today;
|
|
|
+ this.selectedDate = today;
|
|
|
+ const startDate = this.formatYmd(this.addDays(today, -6));
|
|
|
+ this.currentDateRange = { start: startDate, end: today };
|
|
|
+ await this.fetchAllApiData();
|
|
|
+ this.kpiData.gmvTrend = '+0%';
|
|
|
+ this.kpiData.p80Trend = '+0%';
|
|
|
+ this.kpiData.top5Trend = '+0%';
|
|
|
+ this.kpiData.avgTimeTrend = '+0%';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async selectDateRange(type) {
|
|
|
+ this.activeTab = type;
|
|
|
+ const baseDate = this.toDate(this.selectedDate);
|
|
|
+ let start;
|
|
|
+ let end;
|
|
|
+
|
|
|
+ if (type === '7d') {
|
|
|
+ end = this.formatYmd(baseDate);
|
|
|
+ start = this.formatYmd(this.addDays(baseDate, -6));
|
|
|
+ } else if (type === 'tm') {
|
|
|
+ start = this.formatYmd(this.startOfMonth(baseDate));
|
|
|
+ end = this.formatYmd(this.endOfMonth(baseDate));
|
|
|
+ } else if (type === 'lm') {
|
|
|
+ const lastMonth = this.addMonths(baseDate, -1);
|
|
|
+ start = this.formatYmd(this.startOfMonth(lastMonth));
|
|
|
+ end = this.formatYmd(this.endOfMonth(lastMonth));
|
|
|
+ } else if (type === 'all') {
|
|
|
+ start = '2022-01-01';
|
|
|
+ end = this.maxDate || this.formatYmd(baseDate);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.currentDateRange = { start, end };
|
|
|
+
|
|
|
+ if (type === 'tm') {
|
|
|
+ await this.fetchAllApiData({ start, end });
|
|
|
+ const previousMonthData = await this.fetchPreviousMonthData(start);
|
|
|
+ if (previousMonthData) {
|
|
|
+ const currentGmv = parseFloat(this.kpiData.gmv.replace(/[?,]/g, '')) || 0;
|
|
|
+ const currentP80 = parseFloat(this.kpiData.p80Contribution.replace('%', '')) || 0;
|
|
|
+ const currentTop5 = parseFloat(this.kpiData.top5Contribution.replace('%', '')) || 0;
|
|
|
+ const currentAvgTime = this.parseTimeString(this.kpiData.averagePaymentTime) || 0;
|
|
|
+
|
|
|
+ this.kpiData.gmvTrend = this.calculateTrend(currentGmv, previousMonthData.gmv);
|
|
|
+ this.kpiData.p80Trend = this.calculateTrend(currentP80, previousMonthData.p80);
|
|
|
+ this.kpiData.top5Trend = this.calculateTrend(currentTop5, previousMonthData.top5);
|
|
|
+ this.kpiData.avgTimeTrend = this.calculateTrend(currentAvgTime, previousMonthData.avgTime);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.fetchAllApiData({ start, end });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async fetchAllKpiData() {
|
|
|
+ try {
|
|
|
+ const [gmvRes, p80Res, top5Res, avgTimeRes] = await Promise.all([
|
|
|
+ axios.get('/api/analysis/gmv'),
|
|
|
+ axios.get('/api/analysis/r-big'),
|
|
|
+ axios.get('/api/analysis/top5-percentage'),
|
|
|
+ axios.get('/api/analysis/average-payment-time')
|
|
|
+ ]);
|
|
|
+
|
|
|
+ const gmvValue = gmvRes.data || 0;
|
|
|
+ this.kpiData.gmv = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(gmvValue);
|
|
|
+ const p80Value = p80Res.data?.rBigRatio || 0;
|
|
|
+ this.kpiData.p80Contribution = `${Math.round(p80Value)}%`;
|
|
|
+ const top5Value = top5Res.data?.data?.top5Percentage || 0;
|
|
|
+ this.kpiData.top5Contribution = `${Math.round(top5Value)}%`;
|
|
|
+ const avgSeconds = avgTimeRes.data?.averagePaymentSeconds || 0;
|
|
|
+ const minutes = Math.floor(avgSeconds / 60);
|
|
|
+ const seconds = Math.round(avgSeconds % 60);
|
|
|
+ this.kpiData.averagePaymentTime = `${this.pad2(minutes)}:${this.pad2(seconds)} ??`;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('????KPI????:', error);
|
|
|
+ this.kpiData.gmv = '????';
|
|
|
+ this.kpiData.p80Contribution = '????';
|
|
|
+ this.kpiData.top5Contribution = '????';
|
|
|
+ this.kpiData.averagePaymentTime = '????';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.header-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+ margin-top: 10px;
|
|
|
+}
|
|
|
+.control-label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+.control-item-group {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+.date-input {
|
|
|
+ padding: 4px 8px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.time-tab {
|
|
|
+ padding: 5px 15px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ background-color: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 14px;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+.time-tab:hover {
|
|
|
+ border-color: #188df0;
|
|
|
+ color: #188df0;
|
|
|
+}
|
|
|
+.time-tab.active {
|
|
|
+ background-color: #188df0;
|
|
|
+ color: #fff;
|
|
|
+ border-color: #188df0;
|
|
|
+}
|
|
|
+/* 样式部分和之前一样,不用修改 */
|
|
|
+.order-value-view {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+.page-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ background-color: white;
|
|
|
+ padding: 15px 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+.page-title {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+.header-controls {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.control-item {
|
|
|
+ margin-left: 20px;
|
|
|
+ color: #666;
|
|
|
+}
|
|
|
+.time-range-selector {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.date-input {
|
|
|
+ padding: 6px 10px;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-right: 10px;
|
|
|
+}
|
|
|
+.time-tab {
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #ddd;
|
|
|
+ padding: 8px 12px;
|
|
|
+ margin-left: -1px;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #555;
|
|
|
+ transition: all 0.2s;
|
|
|
+}
|
|
|
+.time-tab:first-of-type {
|
|
|
+ border-top-left-radius: 4px;
|
|
|
+ border-bottom-left-radius: 4px;
|
|
|
+}
|
|
|
+.time-tab:last-of-type {
|
|
|
+ border-top-right-radius: 4px;
|
|
|
+ border-bottom-right-radius: 4px;
|
|
|
+}
|
|
|
+.time-tab.active {
|
|
|
+ background-color: #3366CC;
|
|
|
+ color: white;
|
|
|
+ border-color: #3366CC;
|
|
|
+ z-index: 1;
|
|
|
+}
|
|
|
+.kpi-cards-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(4, 1fr);
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+.charts-area {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 20px;
|
|
|
+}
|
|
|
+.leakage-section {
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+.full-width {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.chart-wrapper {
|
|
|
+ background-color: white;
|
|
|
+ padding: 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+</style>
|