zhangchen 2 месяцев назад
Родитель
Сommit
b22b29aa85

+ 64 - 0
src/api/modules/businessData.ts

@@ -147,6 +147,8 @@ export interface GetHistoryDetailData {
   summary?: string;
   optimizationSuggestions?: string;
   aiAnalysisCompleted?: number;
+  /** 对比数据 JSON 字符串,解析后结构同 StoreOperationalStatisticsComparisonVo */
+  statisticsData?: string;
 }
 
 /**
@@ -157,3 +159,65 @@ export const getHistoryDetail = (params: GetHistoryDetailParams) => {
     loading: false
   });
 };
+
+/** 历史分析报告列表 - 请求参数 */
+export interface GetHistoryReportListParams {
+  storeId: string | number;
+  page: number;
+  size: number;
+  created_time?: string; // 生成时间筛选 YYYY-MM-DD
+}
+
+/** 接口返回的历史记录项(含 queryTime) */
+export interface HistoryReportRecord {
+  id?: number | string;
+  queryTime?: string; // 完整年月日时分秒,如 "2026-02-05 13:48:35"
+  createdTime?: string;
+  [key: string]: any;
+}
+
+/** 历史分析报告列表 - 接口响应 */
+export interface GetHistoryReportListData {
+  records?: HistoryReportRecord[];
+  list?: HistoryReportRecord[];
+  total?: number;
+}
+
+/**
+ * 获取历史分析报告列表 /store/operational/statistics/history/list
+ */
+export const getHistoryReportList = (params: GetHistoryReportListParams) => {
+  return httpApi.get<GetHistoryReportListData>(`/alienStore/store/operational/statistics/history/list`, params, {
+    loading: false
+  });
+};
+
+/** 批量删除历史分析报告 - 请求参数 */
+export interface BatchDeleteHistoryParams {
+  ids: string; // 列表中 id 用英文逗号分隔,如 "235,234,233"
+}
+
+/**
+ * 批量删除历史分析报告 /store/operational/statistics/history/batchDelete(DELETE)
+ */
+export const batchDeleteHistoryReport = (params: BatchDeleteHistoryParams) => {
+  return httpApi.delete<unknown>(`/alienStore/store/operational/statistics/history/batchDelete`, params, {
+    loading: true
+  });
+};
+
+/** 根据历史记录生成对比报告 PDF - 请求参数 */
+export interface GenerateStatisticsComparisonPdfByHistoryIdParams {
+  historyId: number | string;
+  storeId: string | number;
+}
+
+/**
+ * 根据历史记录 id 生成对比报告 PDF /store/operational/statistics/generateStatisticsComparisonPdfByHistoryId
+ * 返回 data 为 PDF 文件地址(字符串)
+ */
+export const generateStatisticsComparisonPdfByHistoryId = (params: GenerateStatisticsComparisonPdfByHistoryIdParams) => {
+  return httpApi.get<string>(`/alienStore/store/operational/statistics/generateStatisticsComparisonPdfByHistoryId`, params, {
+    loading: true
+  });
+};

+ 3 - 3
src/assets/json/authMenuList.json

@@ -212,9 +212,9 @@
           }
         },
         {
-          "path": "/businessData/history",
-          "name": "businessDataHistory",
-          "component": "/businessData/history",
+          "path": "/businessData/historicalAnalysis",
+          "name": "businessDataHistoricalAnalysis",
+          "component": "/businessData/historicalAnalysis",
           "meta": {
             "icon": "TrendCharts",
             "title": "历史分析",

+ 50 - 1
src/views/businessData/compare.vue

@@ -302,10 +302,15 @@ const queryEnd = (route.query.end as string) || "";
 const queryCompareStart = (route.query.compareStart as string) || "";
 const queryCompareEnd = (route.query.compareEnd as string) || "";
 const compareType = (route.query.compareType as string) || "lastPeriod";
+const historyIdFromRoute = route.query.historyId as string | undefined;
 
 const compareLabel = computed(() => (compareType === "samePeriod" ? "同期" : "上期"));
 
+// 历史详情模式下的日期 PK 文案(由 history/detail 返回的 statisticsData 解析得到)
+const historyDateRangeText = ref("");
+
 const dateRangeText = computed(() => {
+  if (historyIdFromRoute && historyDateRangeText.value) return historyDateRangeText.value;
   if (!queryStart || !queryEnd) return "2026/01/07-2026/01/08 PK 2025/01/07-2025/01/08";
   const currentStr = `${queryStart.replace(/-/g, "/")}-${queryEnd.replace(/-/g, "/")}`;
 
@@ -807,9 +812,53 @@ async function fetchComparisonData() {
   }
 }
 
+// 根据历史 id 拉取详情并展示(statisticsData 为 JSON 字符串,结构同 StoreOperationalStatisticsComparisonVo)
+async function fetchHistoryDetail(id: string | number) {
+  loading.value = true;
+  try {
+    const res: any = await getHistoryDetail({ id });
+    const data = res?.data ?? res;
+    const rawStats = data?.statisticsData;
+    if (!rawStats || typeof rawStats !== "string") {
+      ElMessage.warning("暂无对比数据");
+      return;
+    }
+    let stats: any;
+    try {
+      stats = JSON.parse(rawStats);
+    } catch (e) {
+      ElMessage.warning("对比数据解析失败");
+      return;
+    }
+    comparisonHistoryId.value = Number(id) || stats?.historyId;
+    const curStart = stats?.currentStartTime ?? "";
+    const curEnd = stats?.currentEndTime ?? "";
+    const prevStart = stats?.previousStartTime ?? "";
+    const prevEnd = stats?.previousEndTime ?? "";
+    historyDateRangeText.value =
+      curStart && curEnd && prevStart && prevEnd
+        ? `${String(curStart).replace(/-/g, "/")}-${String(curEnd).replace(/-/g, "/")} PK ${String(prevStart).replace(/-/g, "/")}-${String(prevEnd).replace(/-/g, "/")}`
+        : "";
+    setTrafficCompare(stats?.trafficData);
+    setInteractionCompare(stats?.interactionData);
+    setCouponCompare(stats?.couponData);
+    setVoucherCompare(stats?.voucherData);
+    setServiceCompare(stats?.serviceQualityData);
+    priceListRankingData.value = stats?.priceListRanking ?? [];
+  } catch (e) {
+    ElMessage.error("获取历史详情失败");
+  } finally {
+    loading.value = false;
+  }
+}
+
 // 页面加载时获取数据
 onMounted(() => {
-  fetchComparisonData();
+  if (historyIdFromRoute) {
+    fetchHistoryDetail(historyIdFromRoute);
+  } else {
+    fetchComparisonData();
+  }
 });
 </script>
 

+ 373 - 0
src/views/businessData/historicalAnalysis.vue

@@ -0,0 +1,373 @@
+<template>
+  <div class="table-box historical-analysis-page">
+    <!-- 筛选区:生成时间、重置、查询 -->
+    <div class="filter-bar">
+      <div class="filter-left">
+        <span class="filter-label">生成时间</span>
+        <el-date-picker
+          v-model="filterDate"
+          type="date"
+          placeholder="选择日期"
+          value-format="YYYY-MM-DD"
+          format="YYYY/MM/DD"
+          class="filter-date-picker"
+          clearable
+        />
+      </div>
+      <div class="filter-actions">
+        <el-button @click="handleReset"> 重置 </el-button>
+        <el-button type="primary" :loading="loading" @click="handleQuery"> 查询 </el-button>
+      </div>
+    </div>
+
+    <!-- 历史分析报告列表 -->
+    <el-card class="list-card" shadow="hover" v-loading="loading">
+      <template #header>
+        <div class="card-header">
+          <span class="card-title">历史分析</span>
+          <div class="card-actions">
+            <el-button link type="primary" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
+              <el-icon><Delete /></el-icon>
+              删除
+            </el-button>
+          </div>
+        </div>
+      </template>
+
+      <!-- 报告列表 -->
+      <div v-if="list.length > 0" class="report-list">
+        <div v-for="item in list" :key="item.id" class="report-item" :class="{ selected: selectedIds.includes(item.id) }">
+          <el-checkbox
+            :model-value="selectedIds.includes(item.id)"
+            @update:model-value="(val: boolean) => toggleSelect(item.id, val)"
+          />
+          <div class="report-content">
+            <div class="report-title">
+              {{ item.title }}
+            </div>
+            <div class="report-meta">生成时间: {{ item.createTime }}</div>
+          </div>
+          <div class="report-item-actions">
+            <el-button type="primary" size="small" @click="handleView(item)"> 查看 </el-button>
+            <el-button type="primary" link size="small" @click="handleExport(item)">
+              <el-icon><Download /></el-icon>
+              导出
+            </el-button>
+          </div>
+        </div>
+      </div>
+
+      <el-empty v-else description="暂无历史分析报告" />
+
+      <!-- 分页 -->
+      <div v-if="total > 0" class="pagination-wrap">
+        <el-pagination
+          v-model:current-page="pagination.page"
+          v-model:page-size="pagination.pageSize"
+          :page-sizes="[10, 20, 50]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="fetchList"
+          @current-change="fetchList"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="businessDataHistoricalAnalysis">
+import { ref, reactive, onMounted } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Delete, Download } from "@element-plus/icons-vue";
+import { localGet } from "@/utils";
+import {
+  getHistoryReportList,
+  batchDeleteHistoryReport,
+  generateStatisticsComparisonPdfByHistoryId
+} from "@/api/modules/businessData";
+import type { HistoryReportRecord } from "@/api/modules/businessData";
+
+/** 页面展示用的历史报告项 */
+interface HistoryReportItem {
+  id: number | string;
+  title: string;
+  createTime: string;
+  pdfUrl?: string | null;
+}
+
+function getTodayString() {
+  const d = new Date();
+  const y = d.getFullYear();
+  const m = String(d.getMonth() + 1).padStart(2, "0");
+  const day = String(d.getDate()).padStart(2, "0");
+  return `${y}-${m}-${day}`;
+}
+
+/** 将 queryTime 转为 yyyy-MM-dd(列表名称用) */
+function formatQueryTimeDate(qt: string | undefined): string {
+  if (qt == null || qt === "") return "--";
+  const part = String(qt).trim().split(/\s/)[0];
+  return part && /^\d{4}-\d{2}-\d{2}$/.test(part) ? part : "--";
+}
+
+/** 列表项:名称 = queryTime 的 yyyy-MM-dd + 对比报告,生成时间 = queryTime 完整年月日时分秒 */
+function recordToItem(record: HistoryReportRecord): HistoryReportItem {
+  const qt = record.queryTime ?? record.createdTime ?? "";
+  const dateStr = formatQueryTimeDate(qt);
+  const fullTime = qt && dateStr !== "--" ? qt : "--";
+  return {
+    id: record.id ?? "",
+    title: `${dateStr} 对比报告`,
+    createTime: fullTime,
+    pdfUrl: record.pdfUrl ?? null
+  };
+}
+
+const router = useRouter();
+const loading = ref(false);
+const filterDate = ref<string | null>(getTodayString());
+const list = ref<HistoryReportItem[]>([]);
+const total = ref(0);
+const selectedIds = ref<(number | string)[]>([]);
+
+const pagination = reactive({
+  page: 1,
+  pageSize: 10
+});
+
+/** 获取列表 */
+async function fetchList() {
+  const storeId = localGet("createdId");
+  if (!storeId) {
+    ElMessage.warning("请先选择店铺");
+    return;
+  }
+  loading.value = true;
+  try {
+    const res: any = await getHistoryReportList({
+      storeId,
+      page: pagination.page,
+      size: pagination.pageSize,
+      created_time: filterDate.value ?? undefined
+    });
+    const data = res?.data ?? res;
+    const rawList = data?.records ?? data?.list ?? [];
+    const totalNum = data?.total ?? 0;
+    list.value = rawList.map((r: HistoryReportRecord) => recordToItem(r));
+    total.value = totalNum;
+  } catch (e) {
+    list.value = [];
+    total.value = 0;
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleReset() {
+  filterDate.value = null;
+  pagination.page = 1;
+  fetchList();
+}
+
+function handleQuery() {
+  pagination.page = 1;
+  fetchList();
+}
+
+function toggleSelect(id: number | string, checked: boolean) {
+  if (checked) {
+    if (!selectedIds.value.includes(id)) selectedIds.value.push(id);
+  } else {
+    selectedIds.value = selectedIds.value.filter(i => i !== id);
+  }
+}
+
+function handleBatchDelete() {
+  if (selectedIds.value.length === 0) return;
+  ElMessageBox.confirm(`确定删除选中的 ${selectedIds.value.length} 条报告?`, "提示", {
+    type: "warning"
+  })
+    .then(async () => {
+      const ids = selectedIds.value.join(",");
+      await batchDeleteHistoryReport({ ids });
+      ElMessage.success("删除成功");
+      selectedIds.value = [];
+      fetchList();
+    })
+    .catch(() => {});
+}
+
+function downloadPdfUrl(url: string, filename: string) {
+  const link = document.createElement("a");
+  link.href = url;
+  link.setAttribute("download", filename);
+  link.setAttribute("target", "_blank");
+  link.setAttribute("rel", "noopener noreferrer");
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  ElMessage.success("开始下载");
+}
+
+async function handleExport(item?: HistoryReportItem) {
+  if (!item) return;
+  let url = item.pdfUrl?.trim() || "";
+  if (!url) {
+    const storeId = localGet("createdId");
+    if (!storeId) {
+      ElMessage.warning("请先选择店铺");
+      return;
+    }
+    try {
+      const res: any = await generateStatisticsComparisonPdfByHistoryId({ historyId: item.id, storeId });
+      const data = res?.data ?? res;
+      url = typeof data === "string" ? data.trim() : "";
+      if (!url) {
+        ElMessage.warning("生成 PDF 失败,暂无下载地址");
+        return;
+      }
+    } catch (e) {
+      ElMessage.error("生成 PDF 失败");
+      return;
+    }
+  }
+  downloadPdfUrl(url, `${item.title || "对比报告"}.pdf`);
+}
+
+function handleView(item: HistoryReportItem) {
+  router.push({
+    path: "/businessData/compare",
+    query: { historyId: item.id }
+  });
+}
+
+onMounted(() => {
+  fetchList();
+});
+</script>
+
+<style lang="scss" scoped>
+/* 与数据概况等页一致的 table-box 容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  height: auto !important;
+  min-height: 100%;
+}
+.historical-analysis-page {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  min-height: 100%;
+}
+.filter-bar {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  .filter-left {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+    align-items: center;
+  }
+  .filter-label {
+    font-size: 14px;
+    color: var(--el-text-color-regular);
+    white-space: nowrap;
+  }
+  .filter-date-picker {
+    width: 240px;
+  }
+  .filter-actions {
+    display: flex;
+    gap: 8px;
+  }
+}
+.list-card {
+  flex: 1;
+  min-height: 0;
+  border-radius: 8px;
+  :deep(.el-card__header) {
+    padding: 14px 20px;
+  }
+  :deep(.el-card__body) {
+    padding: 0 20px 20px;
+  }
+}
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  .card-title {
+    font-size: 16px;
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+  }
+  .card-actions {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+  }
+}
+.report-list {
+  display: flex;
+  flex-direction: column;
+  gap: 0;
+}
+.report-item {
+  display: flex;
+  gap: 16px;
+  align-items: center;
+  padding: 14px 0;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  &:last-child {
+    border-bottom: none;
+  }
+  &.selected {
+    padding-right: 20px;
+    padding-left: 20px;
+    margin: 0 -20px;
+    background: var(--el-fill-color-light);
+  }
+  .report-content {
+    flex: 1;
+    min-width: 0;
+  }
+  .report-title {
+    margin-bottom: 4px;
+    font-size: 14px;
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+  }
+  .report-meta {
+    font-size: 12px;
+    color: var(--el-text-color-secondary);
+  }
+  .report-item-actions {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+  }
+}
+.pagination-wrap {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 16px;
+}
+
+@media (width <= 768px) {
+  .filter-bar .filter-left {
+    width: 100%;
+  }
+  .filter-bar .filter-actions {
+    justify-content: flex-end;
+    width: 100%;
+  }
+}
+</style>