| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- <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>
|