| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- <template>
- <div class="app-page">
- <!-- 页面头部 -->
- <section class="glass-card page-hero">
- <div class="page-hero__meta">
- <span class="page-hero__eyebrow">BI Center</span>
- <h1>报表中心</h1>
- <p>当前把报表目录、筛选条件和导出动作先放进统一框架,适合后续接真实图表和异步导出能力。</p>
- </div>
- <div class="chip-list">
- <span class="chip">支持异步导出</span>
- <span class="chip">可保存常用视图</span>
- </div>
- </section>
- <!-- 报表类型与筛选 -->
- <section class="glass-card section-card">
- <div class="table-toolbar">
- <el-form inline class="filter-form">
- <el-form-item label="报表类型">
- <el-select v-model="reportType" placeholder="请选择报表类型" style="width:170px" @change="onReportTypeChange">
- <el-option label="渠道报表" value="channel" />
- <el-option label="SKU 报表" value="sku" />
- <el-option label="履约报表" value="fulfillment" />
- <el-option label="自定义报表" value="custom" />
- </el-select>
- </el-form-item>
- <!-- 时间范围:所有类型都有 -->
- <el-form-item label="时间范围">
- <el-date-picker
- v-model="filters.timeRange"
- type="daterange"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- style="width:240px"
- />
- </el-form-item>
- <!-- 渠道报表筛选 -->
- <template v-if="reportType === 'channel'">
- <el-form-item label="渠道">
- <el-select v-model="filters.channel" placeholder="全部渠道" clearable style="width:140px">
- <el-option label="Shopify US" value="Shopify US" />
- <el-option label="Shopify JP" value="Shopify JP" />
- <el-option label="TikTok UK" value="TikTok UK" />
- </el-select>
- </el-form-item>
- <el-form-item label="店铺">
- <el-select v-model="filters.shop" placeholder="全部店铺" clearable style="width:140px">
- <el-option label="旗舰店" value="旗舰店" />
- <el-option label="折扣店" value="折扣店" />
- </el-select>
- </el-form-item>
- </template>
- <!-- SKU 报表筛选 -->
- <template v-if="reportType === 'sku'">
- <el-form-item label="SKU">
- <el-input v-model="filters.sku" placeholder="请输入 SKU" clearable style="width:160px" />
- </el-form-item>
- <el-form-item label="供应商">
- <el-select v-model="filters.supplier" placeholder="全部供应商" clearable style="width:140px">
- <el-option label="供应商 A" value="供应商 A" />
- <el-option label="供应商 B" value="供应商 B" />
- </el-select>
- </el-form-item>
- </template>
- <!-- 履约报表筛选 -->
- <template v-if="reportType === 'fulfillment'">
- <el-form-item label="仓库">
- <el-select v-model="filters.warehouse" placeholder="全部仓库" clearable style="width:140px">
- <el-option label="华东仓" value="华东仓" />
- <el-option label="华南仓" value="华南仓" />
- <el-option label="海外仓-US" value="海外仓-US" />
- </el-select>
- </el-form-item>
- <el-form-item label="国家">
- <el-select v-model="filters.country" placeholder="全部国家" clearable style="width:140px">
- <el-option label="美国" value="US" />
- <el-option label="日本" value="JP" />
- <el-option label="英国" value="UK" />
- </el-select>
- </el-form-item>
- </template>
- </el-form>
- <div class="chip-list">
- <el-button type="primary" @click="generateReport">生成报表</el-button>
- <el-button @click="doExport('excel')">导出 Excel</el-button>
- <el-button @click="doExport('csv')">导出 CSV</el-button>
- <el-button @click="savePersonalView">保存视图</el-button>
- </div>
- </div>
- </section>
- <!-- 已保存视图 -->
- <section v-if="savedViews.length" class="glass-card section-card" style="padding:12px 24px">
- <div class="chip-list" style="margin-bottom:0">
- <span style="color:var(--cb-text-soft);font-size:13px;margin-right:8px">我的视图:</span>
- <el-tag
- v-for="(view, idx) in savedViews"
- :key="idx"
- closable
- size="large"
- @close="removeView(idx)"
- @click="applyView(view)"
- style="cursor:pointer"
- >
- {{ view.name }}
- </el-tag>
- </div>
- </section>
- <!-- 报表数据 -->
- <section class="glass-card section-card" v-if="reportData.length">
- <div class="section-card__title" style="margin-bottom:16px">
- <div>
- <h3>{{ reportTypeLabel }} — 报表数据</h3>
- <p>共 {{ reportData.length }} 条指标</p>
- </div>
- </div>
- <el-table :data="reportData" stripe style="width:100%" v-loading="loading">
- <el-table-column prop="metric" label="指标名称" min-width="200" />
- <el-table-column prop="value" label="数值" width="160" />
- <el-table-column prop="mom" label="环比" width="120">
- <template #default="{ row }">
- <span :class="trendClass(row.mom)">{{ row.mom }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="yoy" label="同比" width="120">
- <template #default="{ row }">
- <span :class="trendClass(row.yoy)">{{ row.yoy }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="dimension" label="维度分组" min-width="200" />
- <template #empty>
- <el-empty description="暂无数据" />
- </template>
- </el-table>
- <div style="display:flex;justify-content:flex-end;margin-top:16px" v-if="reportDataTotal > 0">
- <el-pagination v-model:current-page="reportDataPage" v-model:page-size="reportDataPageSize" :total="reportDataTotal" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @size-change="generateReport" @current-change="generateReport" />
- </div>
- </section>
- <!-- 图表区 -->
- <section v-if="reportData.length" class="page-grid page-grid--two">
- <article class="glass-card section-card">
- <h3 style="margin:0 0 16px">{{ reportTypeLabel }} — 指标对比</h3>
- <v-chart :option="barOption" autoresize style="height:300px" />
- </article>
- <article class="glass-card section-card">
- <h3 style="margin:0 0 16px">{{ reportTypeLabel }} — 环比趋势</h3>
- <v-chart :option="lineOption" autoresize style="height:300px" />
- </article>
- </section>
- <!-- 报表目录 -->
- <section class="glass-card section-card">
- <div class="section-card__title" style="margin-bottom:16px">
- <div>
- <h3>报表目录</h3>
- <p>已保存的报表模板列表。</p>
- </div>
- </div>
- <el-table :data="reportList" stripe style="width:100%" v-loading="loading">
- <el-table-column prop="reportType" label="报表类型" width="160" />
- <el-table-column prop="dimensions" label="维度" min-width="250" />
- <el-table-column prop="owner" label="使用角色" width="140" />
- <el-table-column prop="updatedAt" label="最近更新时间" width="170" />
- <el-table-column label="操作" width="120">
- <template #default="{ row }">
- <el-button link type="primary" @click="loadSavedReport(row)">查看</el-button>
- </template>
- </el-table-column>
- <template #empty>
- <el-empty description="暂无数据" />
- </template>
- </el-table>
- <div style="display:flex;justify-content:flex-end;margin-top:16px" v-if="reportListTotal > 0">
- <el-pagination v-model:current-page="reportListPage" v-model:page-size="reportListPageSize" :total="reportListTotal" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next" @size-change="loadData" @current-change="loadData" />
- </div>
- </section>
- <!-- 保存视图对话框 -->
- <el-dialog v-model="saveViewVisible" title="保存个人视图" width="400px" destroy-on-close>
- <el-form :model="viewForm" label-width="80px">
- <el-form-item label="视图名称">
- <el-input v-model="viewForm.name" placeholder="请输入视图名称" />
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="saveViewVisible = false">取消</el-button>
- <el-button type="primary" @click="confirmSaveView">保存</el-button>
- </template>
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, onMounted, ref, reactive } from 'vue';
- import { ElMessage } from 'element-plus';
- import VChart from 'vue-echarts';
- import { use } from 'echarts/core';
- import { CanvasRenderer } from 'echarts/renderers';
- import { LineChart, BarChart } from 'echarts/charts';
- import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components';
- import { api } from '@/api/services';
- import type { ReportItem, ReportDataItem } from '@/types/page';
- use([CanvasRenderer, LineChart, BarChart, GridComponent, TooltipComponent, LegendComponent]);
- const reportType = ref('channel');
- const reportList = ref<ReportItem[]>([]);
- const reportData = ref<ReportDataItem[]>([]);
- const loading = ref(false);
- const reportDataPage = ref(1);
- const reportDataPageSize = ref(10);
- const reportDataTotal = computed(() => reportData.value.length);
- const reportListPage = ref(1);
- const reportListPageSize = ref(10);
- const reportListTotal = computed(() => reportList.value.length);
- const filters = ref({
- timeRange: null as [Date, Date] | null,
- channel: '',
- shop: '',
- sku: '',
- supplier: '',
- warehouse: '',
- country: ''
- });
- interface SavedView {
- name: string;
- reportType: string;
- filters: typeof filters.value;
- }
- const savedViews = ref<SavedView[]>([]);
- const saveViewVisible = ref(false);
- const viewForm = reactive({ name: '' });
- const reportTypeLabel = computed(() => {
- const map: Record<string, string> = {
- channel: '渠道报表',
- sku: 'SKU 报表',
- fulfillment: '履约报表',
- custom: '自定义报表'
- };
- return map[reportType.value] || '报表';
- });
- const trendClass = (val: string) => {
- if (!val) return '';
- if (val.startsWith('+') || val.startsWith('↑')) return 'trend-up';
- if (val.startsWith('-') || val.startsWith('↓')) return 'trend-down';
- return '';
- };
- const barOption = computed(() => {
- const metrics = reportData.value.map(r => r.metric);
- const values = reportData.value.map(r => parseFloat(r.value.replace(/[^0-9.]/g, '')) || 0);
- return {
- tooltip: { trigger: 'axis' as const },
- grid: { left: 80, right: 20, top: 20, bottom: 60 },
- xAxis: { type: 'category' as const, data: metrics, axisLabel: { rotate: 30 } },
- yAxis: { type: 'value' as const },
- series: [{ type: 'bar', data: values, itemStyle: { color: '#409EFF', borderRadius: [4, 4, 0, 0] } }]
- };
- });
- const lineOption = computed(() => {
- const metrics = reportData.value.map(r => r.metric);
- const momValues = reportData.value.map(r => {
- const m = r.mom.match(/[\d.]+/);
- const sign = r.mom.startsWith('-') || r.mom.startsWith('↓') ? -1 : 1;
- return m ? sign * parseFloat(m[0]) : 0;
- });
- return {
- tooltip: { trigger: 'axis' as const },
- grid: { left: 60, right: 20, top: 20, bottom: 60 },
- xAxis: { type: 'category' as const, data: metrics, axisLabel: { rotate: 30 } },
- yAxis: { type: 'value' as const, axisLabel: { formatter: '{value}%' } },
- series: [{
- type: 'line', smooth: true, data: momValues,
- areaStyle: { opacity: 0.15 },
- itemStyle: { color: '#67C23A' }
- }]
- };
- });
- const loadData = async () => {
- loading.value = true;
- try {
- const res = await api.getReports();
- reportList.value = res.items;
- } finally {
- loading.value = false;
- }
- };
- const onReportTypeChange = () => {
- reportData.value = [];
- };
- const generateReport = async () => {
- loading.value = true;
- try {
- const res = await api.getReportData();
- reportData.value = res.items;
- ElMessage.success('报表数据已加载');
- } finally {
- loading.value = false;
- }
- };
- const loadSavedReport = async (item: ReportItem) => {
- reportType.value = item.reportType === '渠道销售日报' ? 'channel' : 'channel';
- loading.value = true;
- try {
- const res = await api.getReportData();
- reportData.value = res.items;
- ElMessage.success(`已加载「${item.reportType}」`);
- } finally {
- loading.value = false;
- }
- };
- const doExport = (format: string) => {
- if (reportData.value.length === 0) {
- ElMessage.warning('请先生成报表数据');
- return;
- }
- ElMessage.info(`正在导出 ${format.toUpperCase()} 文件,完成后自动下载`);
- };
- const savePersonalView = () => {
- viewForm.name = '';
- saveViewVisible.value = true;
- };
- const confirmSaveView = () => {
- if (!viewForm.name.trim()) {
- ElMessage.warning('请输入视图名称');
- return;
- }
- savedViews.value.push({
- name: viewForm.name,
- reportType: reportType.value,
- filters: { ...filters.value }
- });
- saveViewVisible.value = false;
- ElMessage.success('视图已保存');
- };
- const applyView = (view: SavedView) => {
- reportType.value = view.reportType;
- Object.assign(filters.value, view.filters);
- generateReport();
- };
- const removeView = (idx: number) => {
- savedViews.value.splice(idx, 1);
- };
- onMounted(loadData);
- </script>
- <style scoped>
- .filter-form :deep(.el-form-item) { margin-bottom: 0; }
- .trend-up { color: var(--el-color-success); font-weight: 500; }
- .trend-down { color: var(--el-color-danger); font-weight: 500; }
- </style>
|