|
|
@@ -0,0 +1,258 @@
|
|
|
+<template>
|
|
|
+ <div class="app-page">
|
|
|
+ <section class="glass-card page-hero">
|
|
|
+ <div class="page-hero__meta">
|
|
|
+ <span class="page-hero__eyebrow">Sales Analytics</span>
|
|
|
+ <h1>销售分析报表</h1>
|
|
|
+ <p>多维度分析销售数据,洞察GMV、订单量、客单价趋势,支持按渠道/国家/商品维度下钻。</p>
|
|
|
+ </div>
|
|
|
+ <div class="chip-list">
|
|
|
+ <span class="chip">实时数据</span>
|
|
|
+ <span class="chip">支持导出</span>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="glass-card section-card">
|
|
|
+ <el-form inline class="filter-form">
|
|
|
+ <el-form-item label="时间范围">
|
|
|
+ <el-date-picker v-model="filters.dateRange" type="daterange" range-separator="至" start-placeholder="开始" end-placeholder="结束" style="width:240px" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="渠道">
|
|
|
+ <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
|
|
|
+ <el-option label="Shopify" value="Shopify" />
|
|
|
+ <el-option label="TikTok Shop" value="TikTok Shop" />
|
|
|
+ <el-option label="Amazon" value="Amazon" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="国家">
|
|
|
+ <el-select v-model="filters.country" placeholder="全部国家" clearable style="width:120px">
|
|
|
+ <el-option label="美国" value="US" />
|
|
|
+ <el-option label="英国" value="UK" />
|
|
|
+ <el-option label="日本" value="JP" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="对比周期">
|
|
|
+ <el-select v-model="filters.comparePeriod" placeholder="选择对比周期" clearable style="width:130px">
|
|
|
+ <el-option label="上周同期" value="lastWeek" />
|
|
|
+ <el-option label="上月同期" value="lastMonth" />
|
|
|
+ <el-option label="去年同期" value="lastYear" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="loadData">查询</el-button>
|
|
|
+ <el-button @click="resetFilters">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="stat-grid" style="grid-template-columns:repeat(5, 1fr)">
|
|
|
+ <article class="glass-card stat-card">
|
|
|
+ <div class="stat-card__label">GMV</div>
|
|
|
+ <div class="stat-card__value" style="font-size:22px;color:var(--cb-primary)">$328,560</div>
|
|
|
+ <div class="stat-card__trend trend-up">+12.5% ↑</div>
|
|
|
+ </article>
|
|
|
+ <article class="glass-card stat-card">
|
|
|
+ <div class="stat-card__label">订单量</div>
|
|
|
+ <div class="stat-card__value" style="font-size:22px">1,258</div>
|
|
|
+ <div class="stat-card__trend trend-up">+8.3% ↑</div>
|
|
|
+ </article>
|
|
|
+ <article class="glass-card stat-card">
|
|
|
+ <div class="stat-card__label">客单价</div>
|
|
|
+ <div class="stat-card__value" style="font-size:22px">$261.2</div>
|
|
|
+ <div class="stat-card__trend trend-up">+3.8% ↑</div>
|
|
|
+ </article>
|
|
|
+ <article class="glass-card stat-card">
|
|
|
+ <div class="stat-card__label">转化率</div>
|
|
|
+ <div class="stat-card__value" style="font-size:22px">3.8%</div>
|
|
|
+ <div class="stat-card__trend trend-down">-0.2% ↓</div>
|
|
|
+ </article>
|
|
|
+ <article class="glass-card stat-card">
|
|
|
+ <div class="stat-card__label">复购率</div>
|
|
|
+ <div class="stat-card__value" style="font-size:22px">18.5%</div>
|
|
|
+ <div class="stat-card__trend trend-up">+1.2% ↑</div>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="page-grid page-grid--two">
|
|
|
+ <article class="glass-card section-card">
|
|
|
+ <h3 style="margin:0 0 16px">GMV 趋势</h3>
|
|
|
+ <v-chart :option="gmvTrendOption" autoresize style="height:300px" />
|
|
|
+ </article>
|
|
|
+ <article class="glass-card section-card">
|
|
|
+ <h3 style="margin:0 0 16px">订单量趋势</h3>
|
|
|
+ <v-chart :option="orderTrendOption" autoresize style="height:300px" />
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="page-grid page-grid--two">
|
|
|
+ <article class="glass-card section-card">
|
|
|
+ <h3 style="margin:0 0 16px">渠道销售占比</h3>
|
|
|
+ <v-chart :option="channelPieOption" autoresize style="height:280px" />
|
|
|
+ </article>
|
|
|
+ <article class="glass-card section-card">
|
|
|
+ <h3 style="margin:0 0 16px">TOP 10 销售商品</h3>
|
|
|
+ <el-table :data="topProducts" stripe size="small">
|
|
|
+ <el-table-column prop="rank" label="排名" width="60" />
|
|
|
+ <el-table-column prop="title" label="商品名称" min-width="180" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="salesQty" label="销量" width="80" />
|
|
|
+ <el-table-column prop="salesAmount" label="销售额" width="100" />
|
|
|
+ </el-table>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="glass-card section-card">
|
|
|
+ <h3 style="margin:0 0 16px">销售明细</h3>
|
|
|
+ <el-table :data="salesDetail" stripe v-loading="loading">
|
|
|
+ <el-table-column prop="date" label="日期" width="110" />
|
|
|
+ <el-table-column prop="channel" label="渠道" width="100" />
|
|
|
+ <el-table-column prop="country" label="国家" width="80" />
|
|
|
+ <el-table-column prop="orderCount" label="订单数" width="80" />
|
|
|
+ <el-table-column prop="gmv" label="GMV" width="120">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span style="font-weight:600">${{ row.gmv.toLocaleString() }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="avgOrderValue" label="客单价" width="100" />
|
|
|
+ <el-table-column prop="conversionRate" label="转化率" width="80" />
|
|
|
+ <el-table-column prop="mom" label="环比" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="row.mom >= 0 ? 'trend-up' : 'trend-down'">{{ row.mom >= 0 ? '+' : '' }}{{ row.mom }}%</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="yoy" label="同比" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <span :class="row.yoy >= 0 ? 'trend-up' : 'trend-down'">{{ row.yoy >= 0 ? '+' : '' }}{{ row.yoy }}%</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <div style="display:flex;justify-content:flex-end;margin-top:16px">
|
|
|
+ <el-pagination v-model:current-page="page" v-model:page-size="pageSize" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" />
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { onMounted, ref, computed } from 'vue';
|
|
|
+import VChart from 'vue-echarts';
|
|
|
+import { use } from 'echarts/core';
|
|
|
+import { CanvasRenderer } from 'echarts/renderers';
|
|
|
+import { LineChart, BarChart, PieChart } from 'echarts/charts';
|
|
|
+import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
|
|
|
+
|
|
|
+use([CanvasRenderer, LineChart, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent]);
|
|
|
+
|
|
|
+const loading = ref(false);
|
|
|
+const page = ref(1);
|
|
|
+const pageSize = ref(10);
|
|
|
+const total = ref(0);
|
|
|
+
|
|
|
+const filters = ref({
|
|
|
+ dateRange: null as [Date, Date] | null,
|
|
|
+ channel: '',
|
|
|
+ country: '',
|
|
|
+ comparePeriod: 'lastWeek'
|
|
|
+});
|
|
|
+
|
|
|
+const salesDetail = ref([
|
|
|
+ { date: '2026-04-20', channel: 'Shopify', country: 'US', orderCount: 128, gmv: 35680, avgOrderValue: 278.75, conversionRate: 4.2, mom: 5.8, yoy: 12.3 },
|
|
|
+ { date: '2026-04-20', channel: 'TikTok Shop', country: 'UK', orderCount: 86, gmv: 18920, avgOrderValue: 220.00, conversionRate: 3.5, mom: 8.2, yoy: 25.6 },
|
|
|
+ { date: '2026-04-19', channel: 'Shopify', country: 'JP', orderCount: 95, gmv: 28450, avgOrderValue: 299.47, conversionRate: 3.8, mom: -2.1, yoy: 8.9 },
|
|
|
+ { date: '2026-04-19', channel: 'Amazon', country: 'US', orderCount: 156, gmv: 42180, avgOrderValue: 270.38, conversionRate: 4.5, mom: 12.4, yoy: 15.2 },
|
|
|
+ { date: '2026-04-18', channel: 'TikTok Shop', country: 'US', orderCount: 68, gmv: 15200, avgOrderValue: 223.53, conversionRate: 3.2, mom: -5.6, yoy: 32.1 }
|
|
|
+]);
|
|
|
+
|
|
|
+const topProducts = ref([
|
|
|
+ { rank: 1, title: 'Nomad 防水背包 黑色 中号', salesQty: 258, salesAmount: '¥186,240' },
|
|
|
+ { rank: 2, title: 'AeroDry 速干T恤 绿色 L码', salesQty: 425, salesAmount: '¥89,250' },
|
|
|
+ { rank: 3, title: 'UrbanTrail 徒步鞋 白色 XL', salesQty: 186, salesAmount: '¥128,340' },
|
|
|
+ { rank: 4, title: 'Nomad 防水背包 黑色 小号', salesQty: 168, salesAmount: '¥100,632' },
|
|
|
+ { rank: 5, title: 'AeroDry 运动短裤 蓝色 M码', salesQty: 312, salesAmount: '¥54,600' },
|
|
|
+ { rank: 6, title: 'UrbanTrail 登山靴 黑色 42码', salesQty: 95, salesAmount: '¥86,450' },
|
|
|
+ { rank: 7, title: 'Nomad 防水背包 灰色 大号', salesQty: 142, salesAmount: '¥109,340' },
|
|
|
+ { rank: 8, title: 'AeroDry 速干帽 黑色', salesQty: 520, salesAmount: '¥41,600' },
|
|
|
+ { rank: 9, title: 'UrbanTrail 徒步袜 专业款', salesQty: 680, salesAmount: '¥27,200' },
|
|
|
+ { rank: 10, title: 'Nomad 收纳袋 套装', salesQty: 385, salesAmount: '¥34,650' }
|
|
|
+]);
|
|
|
+
|
|
|
+const days = ['4/14', '4/15', '4/16', '4/17', '4/18', '4/19', '4/20'];
|
|
|
+
|
|
|
+const gmvTrendOption = computed(() => ({
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ grid: { left: 60, right: 20, top: 20, bottom: 30 },
|
|
|
+ xAxis: { type: 'category', data: days },
|
|
|
+ yAxis: { type: 'value', axisLabel: { formatter: '${value}' } },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '本期GMV',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: [32800, 41200, 38600, 45200, 39800, 47500, 42300],
|
|
|
+ areaStyle: { opacity: 0.15 },
|
|
|
+ itemStyle: { color: '#409EFF' }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '上期GMV',
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ data: [29000, 35500, 34200, 38800, 36500, 41200, 37600],
|
|
|
+ itemStyle: { color: '#909399' },
|
|
|
+ lineStyle: { type: 'dashed' }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}));
|
|
|
+
|
|
|
+const orderTrendOption = computed(() => ({
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ grid: { left: 50, right: 20, top: 20, bottom: 30 },
|
|
|
+ xAxis: { type: 'category', data: days },
|
|
|
+ yAxis: { type: 'value' },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '本期订单',
|
|
|
+ type: 'bar',
|
|
|
+ data: [128, 156, 142, 168, 152, 178, 156],
|
|
|
+ itemStyle: { color: '#409EFF', borderRadius: [4, 4, 0, 0] }
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '上期订单',
|
|
|
+ type: 'bar',
|
|
|
+ data: [112, 138, 128, 148, 138, 162, 142],
|
|
|
+ itemStyle: { color: '#E6A23C', borderRadius: [4, 4, 0, 0] }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}));
|
|
|
+
|
|
|
+const channelPieOption = computed(() => ({
|
|
|
+ tooltip: { trigger: 'item', formatter: '{b}: ${c} ({d}%)' },
|
|
|
+ legend: { orient: 'vertical', right: 20, top: 'center' },
|
|
|
+ series: [{
|
|
|
+ type: 'pie',
|
|
|
+ radius: ['40%', '70%'],
|
|
|
+ center: ['35%', '50%'],
|
|
|
+ data: [
|
|
|
+ { value: 158200, name: 'Shopify', itemStyle: { color: '#409EFF' } },
|
|
|
+ { value: 89200, name: 'TikTok Shop', itemStyle: { color: '#67C23A' } },
|
|
|
+ { value: 81200, name: 'Amazon', itemStyle: { color: '#E6A23C' } }
|
|
|
+ ],
|
|
|
+ label: { show: false }
|
|
|
+ }]
|
|
|
+}));
|
|
|
+
|
|
|
+const loadData = () => {
|
|
|
+ loading.value = true;
|
|
|
+ setTimeout(() => { loading.value = false; }, 300);
|
|
|
+};
|
|
|
+
|
|
|
+const resetFilters = () => {
|
|
|
+ filters.value = { dateRange: null, channel: '', country: '', comparePeriod: 'lastWeek' };
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(loadData);
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.filter-form :deep(.el-form-item) { margin-bottom: 0; }
|
|
|
+.trend-up { color: var(--el-color-success); }
|
|
|
+.trend-down { color: var(--el-color-danger); }
|
|
|
+</style>
|