Bladeren bron

经营数据代码提交

zhangchen 2 maanden geleden
bovenliggende
commit
1130cd5284

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

@@ -0,0 +1,159 @@
+import httpApi from "@/api/indexApi";
+
+/** 经营统计数据 - 请求参数 */
+export interface GetStatisticsParams {
+  startTime: string;
+  endTime: string;
+  storeId: string | number;
+}
+
+/** 流量数据 */
+export interface TrafficData {
+  storeSearchVolume?: number;
+  pageViews?: number;
+  visitors?: number;
+  newVisitors?: number;
+  visitDuration?: number; // 秒
+  avgVisitDuration?: number; // 秒
+}
+
+/** 互动数据 */
+export interface InteractionData {
+  storeCollectionCount?: number;
+  storeShareCount?: number;
+  storeCheckInCount?: number;
+  consultMerchantCount?: number;
+  friendsCount?: number;
+  followCount?: number;
+  fansCount?: number;
+  postsPublishedCount?: number;
+  postLikesCount?: number;
+  postCommentsCount?: number;
+  postSharesCount?: number;
+  reportedCount?: number;
+  blockedCount?: number;
+}
+
+/** 优惠券/代金券数据 */
+export interface CouponVoucherData {
+  giftToFriendsCount?: number;
+  giftToFriendsAmount?: number;
+  giftToFriendsUsedCount?: number;
+  giftToFriendsUsedAmount?: number;
+  giftToFriendsUsedAmountRatio?: number;
+  friendsGiftCount?: number;
+  friendsGiftAmount?: number;
+  friendsGiftUsedCount?: number;
+  friendsGiftUsedAmount?: number;
+  friendsGiftUsedAmountRatio?: number;
+}
+
+/** 服务质量数据 */
+export interface ServiceQualityData {
+  storeRating?: number;
+  scoreOne?: number;
+  scoreTwo?: number;
+  scoreThree?: number;
+  totalReviews?: number;
+  positiveReviews?: number;
+  neutralReviews?: number;
+  negativeReviews?: number;
+  negativeReviewRatio?: number;
+  negativeReviewAppealsCount?: number;
+  negativeReviewAppealSuccessCount?: number;
+  negativeReviewAppealSuccessRatio?: number;
+}
+
+/** 价目表排名项 */
+export interface PriceListRankingItem {
+  rank: number;
+  priceId: number;
+  priceListItemName: string;
+  pageViews: number;
+  visitors: number;
+  shares: number;
+}
+
+/** 经营统计数据 - 响应 */
+export interface GetStatisticsData {
+  trafficData?: TrafficData;
+  interactionData?: InteractionData;
+  couponData?: CouponVoucherData;
+  voucherData?: CouponVoucherData;
+  serviceQualityData?: ServiceQualityData;
+  priceListRanking?: PriceListRankingItem[];
+}
+
+/**
+ * 获取经营统计数据(与 getDistrict 同源:api/alienStore)
+ */
+export const getStatistics = (params: GetStatisticsParams) => {
+  return httpApi.get<GetStatisticsData>(`/alienStore/store/operational/statistics/getStatistics`, params, {
+    loading: false
+  });
+};
+
+/** 经营统计对比数据 - 请求参数 */
+export interface GetStatisticsComparisonParams {
+  currentStartTime: string;
+  currentEndTime: string;
+  previousStartTime: string;
+  previousEndTime: string;
+  storeId: string | number;
+}
+
+/** 经营统计对比数据 - 响应(含 historyId 用于 AI 分析详情) */
+export interface GetStatisticsComparisonData {
+  historyId?: number;
+  current?: {
+    trafficData?: TrafficData;
+    interactionData?: InteractionData;
+    couponData?: CouponVoucherData;
+    voucherData?: CouponVoucherData;
+    serviceQualityData?: ServiceQualityData;
+  };
+  previous?: {
+    trafficData?: TrafficData;
+    interactionData?: InteractionData;
+    couponData?: CouponVoucherData;
+    voucherData?: CouponVoucherData;
+    serviceQualityData?: ServiceQualityData;
+  };
+  change?: {
+    trafficData?: Record<string, number>;
+    interactionData?: Record<string, number>;
+    couponData?: Record<string, number>;
+    voucherData?: Record<string, number>;
+    serviceQualityData?: Record<string, number>;
+  };
+}
+
+/**
+ * 获取经营统计对比数据
+ */
+export const getStatisticsComparison = (params: GetStatisticsComparisonParams) => {
+  return httpApi.get<GetStatisticsComparisonData>(`/alienStore/store/operational/statistics/getStatisticsComparison`, params, {
+    loading: false
+  });
+};
+
+/** AI 分析历史详情 - 请求参数 */
+export interface GetHistoryDetailParams {
+  id: number | string;
+}
+
+/** AI 分析历史详情 - 响应 */
+export interface GetHistoryDetailData {
+  summary?: string;
+  optimizationSuggestions?: string;
+  aiAnalysisCompleted?: number;
+}
+
+/**
+ * 获取经营统计历史详情(AI 分析),调用方式同 getStatisticsComparison
+ */
+export const getHistoryDetail = (params: GetHistoryDetailParams) => {
+  return httpApi.get<GetHistoryDetailData>(`/alienStore/store/operational/statistics/history/detail`, params, {
+    loading: false
+  });
+};

+ 61 - 0
src/assets/json/authMenuList.json

@@ -183,6 +183,67 @@
       ]
     },
     {
+      "path": "/businessData",
+      "name": "businessData",
+      "redirect": "/businessData/overview",
+      "meta": {
+        "icon": "DataAnalysis",
+        "title": "经营数据",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      },
+      "children": [
+        {
+          "path": "/businessData/overview",
+          "name": "businessDataOverview",
+          "component": "/businessData/overview",
+          "meta": {
+            "icon": "DataLine",
+            "title": "数据概况",
+            "activeMenu": "/businessData",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/businessData/history",
+          "name": "businessDataHistory",
+          "component": "/businessData/history",
+          "meta": {
+            "icon": "TrendCharts",
+            "title": "历史分析",
+            "activeMenu": "/businessData",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/businessData/compare",
+          "name": "businessDataCompare",
+          "component": "/businessData/compare",
+          "meta": {
+            "icon": "Menu",
+            "title": "对比分析",
+            "activeMenu": "/businessData",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        }
+      ]
+    },
+    {
       "path": "/storeDecoration",
       "name": "storeDecoration",
       "redirect": "/storeDecoration/basicStoreInformation",

+ 1024 - 0
src/views/businessData/compare.vue

@@ -0,0 +1,1024 @@
+<template>
+  <div class="table-box compare-page">
+    <DetailHeader title="对比分析" @back="goBack">
+      <template #right>
+        <a href="javascript:void(0)" class="ai-link" @click="openAiAnalysis">AI分析</a>
+      </template>
+    </DetailHeader>
+
+    <!-- AI 分析 - 右侧抽屉 -->
+    <el-drawer
+      v-model="aiDialogVisible"
+      title="AI分析"
+      direction="rtl"
+      size="420px"
+      class="ai-analysis-drawer"
+      :close-on-click-modal="true"
+      @close="onAiDialogClose"
+    >
+      <div class="ai-drawer-body">
+        <template v-if="aiDetailLoading || !aiAnalysisCompleted">
+          <div class="ai-analyzing">AI分析中</div>
+        </template>
+        <template v-else>
+          <div class="ai-section">
+            <div class="ai-section-title">运营分析</div>
+            <div class="ai-section-content">
+              {{ aiSummary || "--" }}
+            </div>
+          </div>
+          <div class="ai-section">
+            <div class="ai-section-title">建议运营方案</div>
+            <div class="ai-section-content">
+              {{ aiOptimizationSuggestions || "--" }}
+            </div>
+          </div>
+        </template>
+      </div>
+    </el-drawer>
+
+    <div class="content">
+      <!-- 日期 PK 展示 -->
+      <div class="date-pk-bar">
+        {{ dateRangeText }}
+      </div>
+
+      <!-- 标签页(与数据概况等页一致的 el-card + el-tabs) -->
+      <el-card class="content-card" shadow="hover" v-loading="loading">
+        <el-tabs v-model="activeTab" class="data-tabs">
+          <el-tab-pane label="流量数据" name="traffic">
+            <div class="compare-cards">
+              <div v-for="item in trafficCompareList" :key="item.key" class="compare-card">
+                <div class="card-title">
+                  {{ item.title }}
+                </div>
+                <div class="card-values">
+                  <span class="current-value">{{ item.current }}</span>
+                  <span class="compare-value">{{ item.compare }}</span>
+                </div>
+                <div class="card-change" :class="item.trend">
+                  <el-icon v-if="item.trend === 'up'">
+                    <Top />
+                  </el-icon>
+                  <el-icon v-else>
+                    <Bottom />
+                  </el-icon>
+                  <span>{{ item.changeText }} 较{{ compareLabel }}</span>
+                </div>
+                <div class="card-chart" :class="item.trend">
+                  <svg viewBox="0 0 60 24" class="mini-chart">
+                    <polyline
+                      v-if="item.trend === 'up'"
+                      fill="none"
+                      stroke="currentColor"
+                      stroke-width="1.5"
+                      :points="item.chartPoints"
+                    />
+                    <polyline v-else fill="none" stroke="currentColor" stroke-width="1.5" :points="item.chartPoints" />
+                  </svg>
+                </div>
+              </div>
+            </div>
+          </el-tab-pane>
+          <el-tab-pane label="互动数据" name="interaction">
+            <div class="compare-cards">
+              <div v-for="item in interactionCompareList" :key="item.key" class="compare-card">
+                <div class="card-title">
+                  {{ item.title }}
+                </div>
+                <div class="card-values">
+                  <span class="current-value">{{ item.current }}</span>
+                  <span class="compare-value">{{ item.compare }}</span>
+                </div>
+                <div class="card-change" :class="item.trend">
+                  <el-icon v-if="item.trend === 'up'">
+                    <Top />
+                  </el-icon>
+                  <el-icon v-else>
+                    <Bottom />
+                  </el-icon>
+                  <span>{{ item.changeText }} 较{{ compareLabel }}</span>
+                </div>
+                <div class="card-chart" :class="item.trend">
+                  <svg viewBox="0 0 60 24" class="mini-chart">
+                    <polyline
+                      v-if="item.trend === 'up'"
+                      fill="none"
+                      stroke="currentColor"
+                      stroke-width="1.5"
+                      :points="item.chartPoints"
+                    />
+                    <polyline v-else fill="none" stroke="currentColor" stroke-width="1.5" :points="item.chartPoints" />
+                  </svg>
+                </div>
+              </div>
+            </div>
+          </el-tab-pane>
+          <el-tab-pane label="优惠券" name="coupon">
+            <div class="compare-cards">
+              <div v-for="item in couponCompareList" :key="item.key" class="compare-card">
+                <div class="card-title">
+                  {{ item.title }}
+                </div>
+                <div class="card-values">
+                  <span class="current-value">{{ item.current }}</span>
+                  <span class="compare-value">{{ item.compare }}</span>
+                </div>
+                <div class="card-change" :class="item.trend">
+                  <el-icon v-if="item.trend === 'up'">
+                    <Top />
+                  </el-icon>
+                  <el-icon v-else>
+                    <Bottom />
+                  </el-icon>
+                  <span>{{ item.changeText }} 较{{ compareLabel }}</span>
+                </div>
+                <div class="card-chart" :class="item.trend">
+                  <svg viewBox="0 0 60 24" class="mini-chart">
+                    <polyline
+                      v-if="item.trend === 'up'"
+                      fill="none"
+                      stroke="currentColor"
+                      stroke-width="1.5"
+                      :points="item.chartPoints"
+                    />
+                    <polyline v-else fill="none" stroke="currentColor" stroke-width="1.5" :points="item.chartPoints" />
+                  </svg>
+                </div>
+              </div>
+            </div>
+          </el-tab-pane>
+          <el-tab-pane label="代金券" name="voucher">
+            <div class="compare-cards">
+              <div v-for="item in voucherCompareList" :key="item.key" class="compare-card">
+                <div class="card-title">
+                  {{ item.title }}
+                </div>
+                <div class="card-values">
+                  <span class="current-value">{{ item.current }}</span>
+                  <span class="compare-value">{{ item.compare }}</span>
+                </div>
+                <div class="card-change" :class="item.trend">
+                  <el-icon v-if="item.trend === 'up'">
+                    <Top />
+                  </el-icon>
+                  <el-icon v-else>
+                    <Bottom />
+                  </el-icon>
+                  <span>{{ item.changeText }} 较{{ compareLabel }}</span>
+                </div>
+                <div class="card-chart" :class="item.trend">
+                  <svg viewBox="0 0 60 24" class="mini-chart">
+                    <polyline
+                      v-if="item.trend === 'up'"
+                      fill="none"
+                      stroke="currentColor"
+                      stroke-width="1.5"
+                      :points="item.chartPoints"
+                    />
+                    <polyline v-else fill="none" stroke="currentColor" stroke-width="1.5" :points="item.chartPoints" />
+                  </svg>
+                </div>
+              </div>
+            </div>
+          </el-tab-pane>
+          <el-tab-pane label="服务质量" name="service">
+            <div class="compare-cards">
+              <div v-for="item in serviceCompareList" :key="item.key" class="compare-card">
+                <div class="card-title">
+                  {{ item.title }}
+                </div>
+                <div class="card-values">
+                  <span class="current-value">{{ item.current }}</span>
+                  <span class="compare-value">{{ item.compare }}</span>
+                </div>
+                <div class="card-change" :class="item.trend">
+                  <el-icon v-if="item.trend === 'up'" size="12">
+                    <Top />
+                  </el-icon>
+                  <el-icon v-else-if="item.trend === 'down'" size="12">
+                    <Bottom />
+                  </el-icon>
+                  <el-icon v-else size="12">
+                    <Right />
+                  </el-icon>
+                  <span>{{ item.changeText }} 较{{ compareLabel }}</span>
+                </div>
+                <div class="card-chart" :class="item.trend">
+                  <svg viewBox="0 0 60 24" class="mini-chart">
+                    <polyline
+                      v-if="item.trend === 'up'"
+                      fill="none"
+                      stroke="currentColor"
+                      stroke-width="1.5"
+                      :points="item.chartPoints"
+                    />
+                    <polyline v-else fill="none" stroke="currentColor" stroke-width="1.5" :points="item.chartPoints" />
+                  </svg>
+                </div>
+              </div>
+            </div>
+          </el-tab-pane>
+          <el-tab-pane label="价目表排名" name="ranking">
+            <div class="compare-cards">
+              <table class="price-list-table">
+                <thead>
+                  <tr>
+                    <th>排名</th>
+                    <th>价目表名称</th>
+                    <th>浏览量</th>
+                    <th>访客</th>
+                    <th>分享数</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  <tr v-for="(item, index) in priceListRankingData" :key="item.priceId">
+                    <td>{{ index + 1 }}</td>
+                    <td>{{ item.priceListItemName }}</td>
+                    <td>
+                      <div class="value-group">
+                        <span class="current-value">{{ item.pageViews.current }}</span>
+                        <span class="compare-value">{{ item.pageViews.previous }}</span>
+                        <span class="change-value" :class="item.pageViews.changeRate >= 0 ? 'up' : 'down'">
+                          {{ item.pageViews.changeRate >= 0 ? "↑" : "↓" }}{{ Math.abs(item.pageViews.changeRate) }}
+                        </span>
+                      </div>
+                    </td>
+                    <td>
+                      <div class="value-group">
+                        <span class="current-value">{{ item.visitors.current }}</span>
+                        <span class="compare-value">{{ item.visitors.previous }}</span>
+                        <span class="change-value" :class="item.visitors.changeRate >= 0 ? 'up' : 'down'">
+                          {{ item.visitors.changeRate >= 0 ? "↑" : "↓" }}{{ Math.abs(item.visitors.changeRate) }}
+                        </span>
+                      </div>
+                    </td>
+                    <td>
+                      <div class="value-group">
+                        <span class="current-value">{{ item.shares.current }}</span>
+                        <span class="compare-value">{{ item.shares.previous }}</span>
+                        <span class="change-value" :class="item.shares.changeRate >= 0 ? 'up' : 'down'">
+                          {{ item.shares.changeRate >= 0 ? "↑" : "↓" }}{{ Math.abs(item.shares.changeRate) }}
+                        </span>
+                      </div>
+                    </td>
+                  </tr>
+                </tbody>
+              </table>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </el-card>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="businessDataCompare">
+import { ref, computed, onMounted } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { Top, Bottom } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+import { localGet } from "@/utils";
+import DetailHeader from "./components/DetailHeader.vue";
+import { getStatisticsComparison, getHistoryDetail } from "@/api/modules/businessData";
+import type { TrafficData } from "@/api/modules/businessData";
+
+const router = useRouter();
+const route = useRoute();
+const activeTab = ref("traffic");
+const loading = ref(false);
+// 对比接口返回的 historyId,用于 AI 分析详情
+const comparisonHistoryId = ref<number | undefined>(undefined);
+// AI 分析弹窗
+const aiDialogVisible = ref(false);
+const aiDetailLoading = ref(false);
+const aiSummary = ref("");
+const aiOptimizationSuggestions = ref("");
+const aiAnalysisCompleted = ref(0);
+
+// 从路由 query 取日期与对比类型
+const queryStart = (route.query.start as string) || "";
+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 compareLabel = computed(() => (compareType === "samePeriod" ? "同期" : "上期"));
+
+const dateRangeText = computed(() => {
+  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, "/")}`;
+
+  // 优先使用概况页传入的 compareStart/compareEnd
+  if (queryCompareStart && queryCompareEnd) {
+    const otherStr = `${queryCompareStart.replace(/-/g, "/")}-${queryCompareEnd.replace(/-/g, "/")}`;
+    return `${currentStr} PK ${otherStr}`;
+  }
+
+  // 兜底:根据开始日期自动算上一年同月同日
+  const year = parseInt(queryStart.slice(0, 4), 10);
+  const otherStart = `${year - 1}${queryStart.slice(4)}`.replace(/-/g, "/");
+  const otherEnd = `${year - 1}${queryEnd.slice(4)}`.replace(/-/g, "/");
+  return `${currentStr} PK ${otherStart}-${otherEnd}`;
+});
+
+function goBack() {
+  router.back();
+}
+
+// 打开 AI 分析弹窗并拉取详情
+async function openAiAnalysis() {
+  aiDialogVisible.value = true;
+  aiDetailLoading.value = true;
+  aiSummary.value = "";
+  aiOptimizationSuggestions.value = "";
+  aiAnalysisCompleted.value = 0;
+
+  const historyId = comparisonHistoryId.value;
+  if (historyId == null || historyId === undefined) {
+    aiDetailLoading.value = false;
+    return;
+  }
+
+  try {
+    const res: any = await getHistoryDetail({ id: historyId });
+    const data = res?.data ?? res;
+    const completed = data?.aiAnalysisCompleted;
+    const hasCompleted = completed === 1;
+    aiAnalysisCompleted.value = hasCompleted ? 1 : 0;
+    if (hasCompleted) {
+      aiSummary.value = data?.summary ?? "";
+      aiOptimizationSuggestions.value = data?.optimizationSuggestions ?? "";
+    }
+  } catch (e) {
+    aiAnalysisCompleted.value = 0;
+  } finally {
+    aiDetailLoading.value = false;
+  }
+}
+
+function onAiDialogClose() {
+  aiSummary.value = "";
+  aiOptimizationSuggestions.value = "";
+  aiAnalysisCompleted.value = 0;
+}
+
+// 秒 -> XhXmXs 或 XmXs
+function formatDuration(seconds: number | undefined): string {
+  if (seconds == null || isNaN(Number(seconds))) return "--";
+  const n = Math.floor(Number(seconds));
+  if (n < 60) return `${n}s`;
+  const m = Math.floor(n / 60);
+  const s = n % 60;
+  if (m < 60) return `${m}m${s}s`;
+  const h = Math.floor(m / 60);
+  const mm = m % 60;
+  return `${h}h${mm}m${s}s`;
+}
+
+// 数字或占比展示
+function formatNum(val: number | undefined): string {
+  if (val == null || (typeof val === "number" && isNaN(val))) return "--";
+  return String(val);
+}
+
+// 格式化百分比
+function formatPercent(percent: number | undefined): string {
+  if (percent == null || (typeof percent === "number" && isNaN(percent))) return "--";
+  return `${Number(percent).toFixed(2)}%`;
+}
+
+// 数据相同时为直线,否则为趋势折线(viewBox 0 0 60 24)
+function getChartPoints(changeVal: number): string {
+  if (changeVal === 0) return "2,12 58,12";
+  return changeVal > 0 ? "2,20 15,14 30,8 45,4 58,2" : "2,4 18,8 35,14 50,18 58,20";
+}
+
+// 对比数据项类型
+interface CompareItem {
+  key: string;
+  title: string;
+  current: string;
+  compare: string;
+  trend: "up" | "down";
+  changeText: string;
+  chartPoints: string;
+}
+
+const trafficCompareList = ref<CompareItem[]>([]);
+// 互动数据对比
+const interactionCompareList = ref<CompareItem[]>([]);
+// 优惠券数据对比
+const couponCompareList = ref<CompareItem[]>([]);
+// 代金券数据对比
+const voucherCompareList = ref<CompareItem[]>([]);
+// 服务质量数据对比
+const serviceCompareList = ref<CompareItem[]>([]);
+// 价目表排名数据
+const priceListRankingData = ref<any[]>([]);
+
+// 设置流量数据对比
+function setTrafficCompare(trafficData: any | undefined) {
+  const trafficItems = [
+    { key: "storeSearchVolume", title: "店铺搜索量" },
+    { key: "pageViews", title: "浏览量" },
+    { key: "visitors", title: "访客数" },
+    { key: "newVisitors", title: "新增访客数" },
+    { key: "visitDuration", title: "访问时长", formatter: formatDuration },
+    { key: "avgVisitDuration", title: "平均访问时长", formatter: formatDuration }
+  ];
+
+  trafficCompareList.value = trafficItems.map(item => {
+    const itemData = trafficData ? (trafficData as any)[item.key] : undefined;
+    const currentVal = itemData?.current;
+    const previousVal = itemData?.previous;
+    const changeVal = itemData?.changeRate || 0;
+    const formatter = item.formatter || formatNum;
+
+    return {
+      key: item.key,
+      title: item.title,
+      current: formatter(currentVal),
+      compare: formatter(previousVal),
+      trend: changeVal >= 0 ? "up" : "down",
+      changeText: changeVal >= 0 ? `↑${Math.abs(changeVal)}` : `↓${Math.abs(changeVal)}`,
+      chartPoints: getChartPoints(changeVal)
+    };
+  });
+}
+
+// 设置互动数据对比
+function setInteractionCompare(interactionData: any | undefined) {
+  const interactionItems = [
+    { key: "storeCollectionCount", title: "店铺收藏次数" },
+    { key: "storeShareCount", title: "店铺分享次数" },
+    { key: "storeCheckInCount", title: "店铺打卡次数" },
+    { key: "consultMerchantCount", title: "咨询商家次数" },
+    { key: "friendsCount", title: "好友数量" },
+    { key: "followCount", title: "关注数量" },
+    { key: "fansCount", title: "粉丝数量" },
+    { key: "postsPublishedCount", title: "发布动态数量" },
+    { key: "postLikesCount", title: "动态点赞数量" },
+    { key: "postCommentsCount", title: "动态评论数量" },
+    { key: "postSharesCount", title: "动态转发数量" },
+    { key: "reportedCount", title: "被举报次数" },
+    { key: "blockedCount", title: "被拉黑次数" }
+  ];
+
+  interactionCompareList.value = interactionItems.map(item => {
+    const itemData = interactionData ? (interactionData as any)[item.key] : undefined;
+    const currentVal = itemData?.current;
+    const previousVal = itemData?.previous;
+    const changeVal = itemData?.changeRate || 0;
+    const formatter = formatNum;
+
+    return {
+      key: item.key,
+      title: item.title,
+      current: formatter(currentVal),
+      compare: formatter(previousVal),
+      trend: changeVal >= 0 ? "up" : "down",
+      changeText: changeVal >= 0 ? `↑${Math.abs(changeVal)}` : `↓${Math.abs(changeVal)}`,
+      chartPoints: getChartPoints(changeVal)
+    };
+  });
+}
+
+// 设置优惠券数据对比
+function setCouponCompare(couponData: any | undefined) {
+  const couponItems = [
+    { key: "giftToFriendsCount", title: "赠送好友数量" },
+    { key: "giftToFriendsAmount", title: "赠送好友金额合计" },
+    { key: "giftToFriendsUsedCount", title: "赠送好友使用数量" },
+    { key: "giftToFriendsUsedAmount", title: "赠送好友使用金额合计" },
+    { key: "giftToFriendsUsedAmountRatio", title: "赠送好友使用金额占比", formatter: formatPercent },
+    { key: "friendsGiftCount", title: "好友赠送数量" },
+    { key: "friendsGiftAmount", title: "好友赠送金额合计" },
+    { key: "friendsGiftUsedCount", title: "好友赠送使用数量" },
+    { key: "friendsGiftUsedAmount", title: "好友赠送使用金额合计" },
+    { key: "friendsGiftUsedAmountRatio", title: "好友赠送使用金额占比", formatter: formatPercent }
+  ];
+
+  couponCompareList.value = couponItems.map(item => {
+    const itemData = couponData ? (couponData as any)[item.key] : undefined;
+    const currentVal = itemData?.current;
+    const previousVal = itemData?.previous;
+    const changeVal = itemData?.changeRate || 0;
+    const formatter = item.formatter || formatNum;
+
+    return {
+      key: item.key,
+      title: item.title,
+      current: formatter(currentVal),
+      compare: formatter(previousVal),
+      trend: changeVal >= 0 ? "up" : "down",
+      changeText: changeVal >= 0 ? `↑${Math.abs(changeVal)}` : `↓${Math.abs(changeVal)}`,
+      chartPoints: getChartPoints(changeVal)
+    };
+  });
+}
+
+// 设置代金券数据对比
+function setVoucherCompare(voucherData: any | undefined) {
+  const voucherItems = [
+    { key: "giftToFriendsCount", title: "赠送好友数量" },
+    { key: "giftToFriendsAmount", title: "赠送好友金额合计" },
+    { key: "giftToFriendsUsedCount", title: "赠送好友使用数量" },
+    { key: "giftToFriendsUsedAmount", title: "赠送好友使用金额合计" },
+    { key: "giftToFriendsUsedAmountRatio", title: "赠送好友使用金额占比", formatter: formatPercent },
+    { key: "friendsGiftCount", title: "好友赠送数量" },
+    { key: "friendsGiftAmount", title: "好友赠送金额合计" },
+    { key: "friendsGiftUsedCount", title: "好友赠送使用数量" },
+    { key: "friendsGiftUsedAmount", title: "好友赠送使用金额合计" },
+    { key: "friendsGiftUsedAmountRatio", title: "好友赠送使用金额占比", formatter: formatPercent }
+  ];
+
+  voucherCompareList.value = voucherItems.map(item => {
+    const itemData = voucherData ? (voucherData as any)[item.key] : undefined;
+    const currentVal = itemData?.current;
+    const previousVal = itemData?.previous;
+    const changeVal = itemData?.changeRate || 0;
+    const formatter = item.formatter || formatNum;
+
+    return {
+      key: item.key,
+      title: item.title,
+      current: formatter(currentVal),
+      compare: formatter(previousVal),
+      trend: changeVal >= 0 ? "up" : "down",
+      changeText: changeVal >= 0 ? `↑${Math.abs(changeVal)}` : `↓${Math.abs(changeVal)}`,
+      chartPoints: getChartPoints(changeVal)
+    };
+  });
+}
+
+// 设置服务质量数据对比
+function setServiceCompare(serviceData: any | undefined) {
+  const serviceItems = [
+    { key: "storeRating", title: "店铺评分" },
+    { key: "scoreOne", title: "口味评分" },
+    { key: "scoreTwo", title: "环境评分" },
+    { key: "totalReviews", title: "评价数量" },
+    { key: "positiveReviews", title: "好评数量" },
+    { key: "neutralReviews", title: "中评数量" },
+    { key: "negativeReviews", title: "差评数量" },
+    { key: "negativeReviewRatio", title: "差评占比", formatter: formatPercent },
+    { key: "negativeReviewAppealsCount", title: "差评申诉次数" },
+    { key: "negativeReviewAppealsSuccessCount", title: "差评申诉成功次数" },
+    { key: "negativeReviewAppealsSuccessRatio", title: "差评申诉成功占比", formatter: formatPercent }
+  ];
+
+  serviceCompareList.value = serviceItems.map(item => {
+    const itemData = serviceData ? (serviceData as any)[item.key] : undefined;
+    const currentVal = itemData?.current;
+    const previousVal = itemData?.previous;
+    const changeVal = itemData?.changeRate || 0;
+    const formatter = item.formatter || formatNum;
+
+    return {
+      key: item.key,
+      title: item.title,
+      current: formatter(currentVal),
+      compare: formatter(previousVal),
+      trend: changeVal >= 0 ? "up" : "down",
+      changeText: changeVal >= 0 ? `↑${Math.abs(changeVal)}` : `↓${Math.abs(changeVal)}`,
+      chartPoints: getChartPoints(changeVal)
+    };
+  });
+}
+
+// 获取对比数据
+async function fetchComparisonData() {
+  const storeId = localGet("createdId");
+  if (!storeId) {
+    ElMessage.warning("请先选择门店");
+    return;
+  }
+
+  if (!queryStart || !queryEnd || !queryCompareStart || !queryCompareEnd) {
+    ElMessage.warning("日期参数不完整");
+    return;
+  }
+
+  loading.value = true;
+  try {
+    console.log("开始获取对比数据", {
+      currentStartTime: queryStart,
+      currentEndTime: queryEnd,
+      previousStartTime: queryCompareStart,
+      previousEndTime: queryCompareEnd,
+      storeId
+    });
+
+    const res: any = await getStatisticsComparison({
+      currentStartTime: queryStart,
+      currentEndTime: queryEnd,
+      previousStartTime: queryCompareStart,
+      previousEndTime: queryCompareEnd,
+      storeId
+    });
+
+    console.log("获取对比数据成功", res);
+
+    const data = res?.data ?? res;
+    comparisonHistoryId.value = data?.historyId;
+
+    console.log("接口返回的流量数据", data?.trafficData);
+    console.log("接口返回的互动数据", data?.interactionData);
+    console.log("接口返回的优惠券数据", data?.couponData);
+    console.log("接口返回的代金券数据", data?.voucherData);
+    console.log("接口返回的服务质量数据", data?.serviceQualityData);
+    console.log("接口返回的价目表排名数据", data?.priceListRanking);
+
+    // 设置流量数据对比
+    setTrafficCompare(data?.trafficData);
+    // 设置互动数据对比
+    setInteractionCompare(data?.interactionData);
+    // 设置优惠券数据对比
+    setCouponCompare(data?.couponData);
+    // 设置代金券数据对比
+    setVoucherCompare(data?.voucherData);
+    // 设置服务质量数据对比
+    setServiceCompare(data?.serviceQualityData);
+    // 设置价目表排名数据
+    priceListRankingData.value = data?.priceListRanking || [];
+
+    console.log("设置流量数据对比完成", trafficCompareList.value);
+    console.log("设置互动数据对比完成", interactionCompareList.value);
+    console.log("设置优惠券数据对比完成", couponCompareList.value);
+    console.log("设置代金券数据对比完成", voucherCompareList.value);
+    console.log("设置服务质量数据对比完成", serviceCompareList.value);
+    console.log("设置价目表排名数据完成", priceListRankingData.value);
+  } catch (e) {
+    console.error("获取对比数据失败", e);
+    ElMessage.error("获取对比数据失败");
+
+    // 模拟数据,用于测试显示效果
+    setTrafficCompare({
+      storeSearchVolume: { current: 1024, previous: 1111, changeRate: 112 },
+      pageViews: { current: 1024, previous: 1030, changeRate: -6 },
+      visitors: { current: 1024, previous: 1030, changeRate: -6 },
+      newVisitors: { current: 1024, previous: 1030, changeRate: 112 },
+      visitDuration: { current: 37413, previous: 37413, changeRate: 0 },
+      avgVisitDuration: { current: 93, previous: 33, changeRate: -6 }
+    });
+
+    // 模拟互动数据,用于测试显示效果
+    setInteractionCompare({
+      storeCollectionCount: { current: 1024, previous: 1111, changeRate: 112 },
+      storeShareCount: { current: 1024, previous: 1030, changeRate: -6 },
+      storeCheckInCount: { current: 1024, previous: 1030, changeRate: -6 },
+      consultMerchantCount: { current: 1024, previous: 1030, changeRate: 112 },
+      friendsCount: { current: 1024, previous: 1111, changeRate: 112 },
+      followCount: { current: 1024, previous: 1030, changeRate: -6 },
+      fansCount: { current: 1024, previous: 1030, changeRate: -6 },
+      postsPublishedCount: { current: 1024, previous: 1030, changeRate: 112 },
+      postLikesCount: { current: 1024, previous: 1111, changeRate: 112 },
+      postCommentsCount: { current: 1024, previous: 1030, changeRate: -6 },
+      postSharesCount: { current: 1024, previous: 1030, changeRate: -6 },
+      reportedCount: { current: 1024, previous: 1030, changeRate: 112 },
+      blockedCount: { current: 1024, previous: 1111, changeRate: 112 }
+    });
+
+    // 模拟优惠券数据,用于测试显示效果
+    setCouponCompare({
+      giftToFriendsCount: { current: 1024, previous: 1111, changeRate: 112 },
+      giftToFriendsAmount: { current: 1024, previous: 1030, changeRate: -6 },
+      giftToFriendsUsedCount: { current: 1024, previous: 1030, changeRate: -6 },
+      giftToFriendsUsedAmount: { current: 1024, previous: 1030, changeRate: 112 },
+      giftToFriendsUsedAmountRatio: { current: 15.21, previous: 13.21, changeRate: 2 },
+      friendsGiftCount: { current: 1024, previous: 1111, changeRate: 112 },
+      friendsGiftAmount: { current: 1024, previous: 1030, changeRate: -6 },
+      friendsGiftUsedCount: { current: 1024, previous: 1030, changeRate: -6 },
+      friendsGiftUsedAmount: { current: 1024, previous: 1030, changeRate: 112 },
+      friendsGiftUsedAmountRatio: { current: 15.21, previous: 13.21, changeRate: 2 }
+    });
+
+    // 模拟代金券数据,用于测试显示效果
+    setVoucherCompare({
+      giftToFriendsCount: { current: 512, previous: 600, changeRate: -88 },
+      giftToFriendsAmount: { current: 512, previous: 550, changeRate: -38 },
+      giftToFriendsUsedCount: { current: 512, previous: 550, changeRate: -38 },
+      giftToFriendsUsedAmount: { current: 512, previous: 550, changeRate: -38 },
+      giftToFriendsUsedAmountRatio: { current: 10.5, previous: 8.5, changeRate: 2 },
+      friendsGiftCount: { current: 512, previous: 600, changeRate: -88 },
+      friendsGiftAmount: { current: 512, previous: 550, changeRate: -38 },
+      friendsGiftUsedCount: { current: 512, previous: 550, changeRate: -38 },
+      friendsGiftUsedAmount: { current: 512, previous: 550, changeRate: -38 },
+      friendsGiftUsedAmountRatio: { current: 10.5, previous: 8.5, changeRate: 2 }
+    });
+
+    // 模拟服务质量数据,用于测试显示效果
+    setServiceCompare({
+      storeRating: { current: 4.5, previous: 4.2, changeRate: 0.3 },
+      scoreOne: { current: 4.6, previous: 4.3, changeRate: 0.3 },
+      scoreTwo: { current: 4.4, previous: 4.1, changeRate: 0.3 },
+      totalReviews: { current: 1000, previous: 900, changeRate: 100 },
+      positiveReviews: { current: 800, previous: 700, changeRate: 100 },
+      neutralReviews: { current: 150, previous: 160, changeRate: -10 },
+      negativeReviews: { current: 50, previous: 40, changeRate: 10 },
+      negativeReviewRatio: { current: 5.0, previous: 4.44, changeRate: 0.56 },
+      negativeReviewAppealsCount: { current: 10, previous: 8, changeRate: 2 },
+      negativeReviewAppealsSuccessCount: { current: 8, previous: 6, changeRate: 2 },
+      negativeReviewAppealsSuccessRatio: { current: 80.0, previous: 75.0, changeRate: 5.0 }
+    });
+
+    // 模拟价目表排名数据,用于测试显示效果
+    priceListRankingData.value = [
+      {
+        priceId: 1,
+        priceListItemName: "锅包肉",
+        pageViews: {
+          current: 2345,
+          previous: 2345,
+          changeRate: 0.5
+        },
+        visitors: {
+          current: 2789,
+          previous: 2789,
+          changeRate: 0.5
+        },
+        shares: {
+          current: 234,
+          previous: 234,
+          changeRate: 0.5
+        }
+      },
+      {
+        priceId: 2,
+        priceListItemName: "锅包肉",
+        pageViews: {
+          current: 2345,
+          previous: 2345,
+          changeRate: 0.5
+        },
+        visitors: {
+          current: 2789,
+          previous: 2789,
+          changeRate: 0.5
+        },
+        shares: {
+          current: 234,
+          previous: 234,
+          changeRate: 0.5
+        }
+      },
+      {
+        priceId: 3,
+        priceListItemName: "锅包肉",
+        pageViews: {
+          current: 2345,
+          previous: 2345,
+          changeRate: 0.5
+        },
+        visitors: {
+          current: 2789,
+          previous: 2789,
+          changeRate: 0.5
+        },
+        shares: {
+          current: 234,
+          previous: 234,
+          changeRate: 0.5
+        }
+      },
+      {
+        priceId: 4,
+        priceListItemName: "锅包肉",
+        pageViews: {
+          current: 2345,
+          previous: 2345,
+          changeRate: -6
+        },
+        visitors: {
+          current: 2789,
+          previous: 2789,
+          changeRate: -6
+        },
+        shares: {
+          current: 234,
+          previous: 234,
+          changeRate: -6
+        }
+      }
+    ];
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 页面加载时获取数据
+onMounted(() => {
+  fetchComparisonData();
+});
+</script>
+
+<style lang="scss" scoped>
+/* 与订单详情等页一致的 table-box 容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  height: auto !important;
+  min-height: 100%;
+  background-color: #ffffff;
+}
+.compare-page .content {
+  flex: 1;
+  padding: 16px 24px 24px;
+}
+.ai-link {
+  font-size: 14px;
+  color: var(--el-color-primary);
+  text-decoration: none;
+  cursor: pointer;
+}
+.ai-drawer-body {
+  padding: 0 4px;
+}
+.ai-analyzing {
+  padding: 60px 24px;
+  font-size: 16px;
+  color: var(--el-text-color-secondary);
+  text-align: center;
+}
+.ai-section {
+  padding: 16px;
+  margin-bottom: 20px;
+  background: var(--el-fill-color-light);
+  border: 1px solid var(--el-border-color-lighter);
+  border-radius: 8px;
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+.ai-section-title {
+  padding-bottom: 8px;
+  margin-bottom: 12px;
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+}
+.ai-section-content {
+  font-size: 14px;
+  line-height: 1.7;
+  color: var(--el-text-color-regular);
+  word-break: break-word;
+  white-space: pre-wrap;
+}
+:deep(.ai-analysis-drawer.el-drawer) {
+  .el-drawer__header {
+    margin-bottom: 16px;
+    font-size: 18px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+  }
+  .el-drawer__body {
+    padding: 0 20px 20px;
+    overflow-y: auto;
+  }
+}
+.date-pk-bar {
+  padding: 12px 16px;
+  margin-bottom: 16px;
+  font-size: 14px;
+  color: #ffffff;
+  text-align: center;
+  background: #5a5e66;
+  border-radius: 8px;
+}
+.content-card {
+  border-radius: 8px;
+  :deep(.el-card__body) {
+    padding: 0;
+  }
+}
+.data-tabs {
+  :deep(.el-tabs__header) {
+    margin: 0 16px;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+  :deep(.el-tabs__content) {
+    padding: 20px 16px;
+  }
+  :deep(.el-tabs__item.is-active) {
+    font-weight: 600;
+  }
+}
+.compare-cards {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 16px;
+  &.placeholder-tip {
+    padding: 40px;
+    font-size: 14px;
+    color: var(--el-text-color-secondary);
+    text-align: center;
+  }
+}
+.compare-card {
+  padding: 16px;
+  background: var(--el-fill-color-light);
+  border-radius: 8px;
+  .card-title {
+    margin-bottom: 8px;
+    font-size: 14px;
+    color: var(--el-text-color-regular);
+  }
+  .card-values {
+    display: flex;
+    gap: 8px;
+    align-items: baseline;
+    margin-bottom: 8px;
+    .current-value {
+      font-size: 22px;
+      font-weight: 600;
+      color: var(--el-text-color-primary);
+    }
+    .compare-value {
+      font-size: 13px;
+      color: var(--el-text-color-secondary);
+    }
+  }
+  .card-change {
+    display: inline-flex;
+    gap: 4px;
+    align-items: center;
+    margin-bottom: 8px;
+    font-size: 13px;
+    &.up {
+      color: var(--el-color-success);
+    }
+    &.down {
+      color: var(--el-color-danger);
+    }
+  }
+  .card-chart {
+    height: 28px;
+    color: var(--el-color-success);
+    .mini-chart {
+      display: block;
+      width: 100%;
+      height: 100%;
+    }
+    &.down {
+      color: var(--el-color-danger);
+    }
+  }
+}
+
+@media (width <= 1024px) {
+  .compare-cards {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+
+@media (width <= 600px) {
+  .compare-cards {
+    grid-template-columns: 1fr;
+  }
+}
+
+/* 价目表排名表格样式 */
+.price-list-table {
+  width: 100%;
+  font-size: 14px;
+  border-collapse: collapse;
+  th {
+    padding: 12px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+    text-align: left;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+  td {
+    padding: 12px;
+    color: var(--el-text-color-regular);
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+  .value-group {
+    display: flex;
+    flex-flow: row wrap;
+    gap: 8px 12px;
+    align-items: center;
+  }
+  .current-value {
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+  }
+  .compare-value {
+    font-size: 12px;
+    color: var(--el-text-color-secondary);
+  }
+  .change-value {
+    font-size: 12px;
+    &.up {
+      color: var(--el-color-success);
+    }
+    &.down {
+      color: var(--el-color-danger);
+    }
+  }
+}
+</style>

+ 44 - 0
src/views/businessData/components/DetailHeader.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="header">
+    <el-button @click="$emit('back')"> 返回 </el-button>
+    <h2 class="title">
+      {{ title }}
+    </h2>
+    <div v-if="$slots.right" class="header-right">
+      <slot name="right" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="DetailHeader">
+defineProps<{
+  title: string;
+}>();
+defineEmits<{
+  (e: "back"): void;
+}>();
+</script>
+
+<style scoped lang="scss">
+/* 与订单详情、代金券详情等页面一致的头部结构,仅用于经营数据模块 */
+.header {
+  display: flex;
+  align-items: center;
+  padding: 20px 24px;
+  background-color: #ffffff;
+  border-bottom: 1px solid #e4e7ed;
+  box-shadow: 0 2px 4px rgb(0 0 0 / 2%);
+}
+.title {
+  flex: 1;
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+  text-align: center;
+}
+.header-right {
+  flex-shrink: 0;
+  min-width: 60px;
+}
+</style>

+ 59 - 0
src/views/businessData/components/StatCardList.vue

@@ -0,0 +1,59 @@
+<template>
+  <div class="stat-cards">
+    <div v-for="item in items" :key="item.key" class="stat-item">
+      <div class="stat-title">
+        {{ item.title }}
+      </div>
+      <div class="stat-value">
+        {{ item.value }}
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="StatCardList">
+defineProps<{
+  items: Array<{ key: string; title: string; value: string }>;
+}>();
+</script>
+
+<style scoped lang="scss">
+.stat-cards {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 16px;
+}
+.stat-item {
+  padding: 16px;
+  background: var(--el-fill-color-light);
+  border-radius: 8px;
+  .stat-title {
+    margin-bottom: 8px;
+    font-size: 14px;
+    color: var(--el-text-color-regular);
+  }
+  .stat-value {
+    font-size: 20px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+  }
+}
+
+@media (width <= 1200px) {
+  .stat-cards {
+    grid-template-columns: repeat(3, 1fr);
+  }
+}
+
+@media (width <= 768px) {
+  .stat-cards {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+
+@media (width <= 600px) {
+  .stat-cards {
+    grid-template-columns: 1fr;
+  }
+}
+</style>

+ 61 - 0
src/views/businessData/history.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="table-box history-page">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <template #tableHeader>
+        <div class="table-header-btn">
+          <span class="page-title">历史分析</span>
+        </div>
+      </template>
+    </ProTable>
+  </div>
+</template>
+
+<script setup lang="ts" name="businessDataHistory">
+import { reactive, ref } from "vue";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+
+const DEFAULT_PAGE_SIZE = 10;
+
+const proTable = ref<ProTableInstance>();
+
+const columns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "index",
+    label: "序号",
+    width: 80,
+    render: (scope: any) => {
+      const { pageable } = proTable.value || {};
+      const pageNum = pageable?.pageNum ?? 1;
+      const pageSize = pageable?.pageSize ?? DEFAULT_PAGE_SIZE;
+      return scope.$index + (pageNum - 1) * pageSize + 1;
+    }
+  },
+  { prop: "placeholder", label: "占位列", minWidth: 120 }
+]);
+
+const initParam = reactive<Record<string, any>>({});
+
+const dataCallback = (data: any) => ({
+  list: data?.records ?? data?.list ?? [],
+  total: data?.total ?? 0
+});
+
+const getTableList = async (_params: any) => {
+  return Promise.resolve({ data: { list: [], total: 0 } });
+};
+</script>
+
+<style lang="scss" scoped>
+.table-box {
+  display: flex;
+  flex-direction: column;
+  min-height: 100%;
+}
+.table-header-btn {
+  .page-title {
+    font-size: 16px;
+    font-weight: 500;
+  }
+}
+</style>

+ 403 - 0
src/views/businessData/overview.vue

@@ -0,0 +1,403 @@
+<template>
+  <!-- 与订单管理、优惠券管理等页一致的 table-box 容器 + el-card/el-tabs 展示 -->
+  <div class="table-box data-overview-page">
+    <!-- 顶部筛选/操作区 -->
+    <div class="filter-bar">
+      <div class="filter-left">
+        <span class="filter-label">日期区间</span>
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="YYYY-MM-DD"
+          class="date-range-picker"
+        />
+        <span class="filter-label compare-label">对比区间</span>
+        <el-select v-model="compareRange" placeholder="请选择" class="compare-select" clearable>
+          <el-option label="较上期" value="lastPeriod" />
+          <el-option label="较同期" value="samePeriod" />
+        </el-select>
+      </div>
+      <div class="filter-actions">
+        <el-button @click="handleReset"> 重置 </el-button>
+        <el-button type="primary" plain @click="handleCompare"> 比较 </el-button>
+        <el-button type="primary" :loading="loading" @click="handleQuery"> 查询 </el-button>
+      </div>
+    </div>
+
+    <!-- 数据展示卡片区 -->
+    <el-card class="data-card" shadow="hover" v-loading="loading">
+      <el-tabs v-model="activeTab" class="data-tabs">
+        <el-tab-pane label="流量数据" name="traffic">
+          <StatCardList :items="trafficStats" />
+        </el-tab-pane>
+        <el-tab-pane label="互动数据" name="interaction">
+          <StatCardList :items="interactionStats" />
+        </el-tab-pane>
+        <el-tab-pane label="优惠券" name="coupon">
+          <StatCardList :items="couponStats" />
+        </el-tab-pane>
+        <el-tab-pane label="代金券" name="voucher">
+          <StatCardList :items="voucherStats" />
+        </el-tab-pane>
+        <el-tab-pane label="服务质量" name="service">
+          <StatCardList :items="serviceStats" />
+        </el-tab-pane>
+        <el-tab-pane label="价目表排名" name="ranking">
+          <div class="ranking-wrap">
+            <el-table :data="priceListRanking" border stripe>
+              <el-table-column prop="rank" label="排名" width="80" align="center" />
+              <el-table-column prop="priceListItemName" label="价目表名称" min-width="120" show-overflow-tooltip />
+              <el-table-column prop="pageViews" label="浏览量" width="100" align="center" />
+              <el-table-column prop="visitors" label="访客" width="100" align="center" />
+              <el-table-column prop="shares" label="分享数" width="100" align="center" />
+            </el-table>
+          </div>
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="businessDataOverview">
+import { ref, reactive, onMounted } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { localGet } from "@/utils";
+import { getStatistics } from "@/api/modules/businessData";
+import type {
+  TrafficData,
+  InteractionData,
+  CouponVoucherData,
+  ServiceQualityData,
+  PriceListRankingItem
+} from "@/api/modules/businessData";
+import StatCardList from "./components/StatCardList.vue";
+
+/** 统计项:key、标题、展示值 */
+interface StatItem {
+  key: string;
+  title: string;
+  value: string;
+}
+
+const router = useRouter();
+const loading = ref(false);
+const dateRange = ref<[string, string] | null>(null);
+const compareRange = ref("lastPeriod");
+const activeTab = ref("traffic");
+
+// 价目表排名(接口返回的 priceListRanking 全量展示,不分页)
+const priceListRanking = ref<PriceListRankingItem[]>([]);
+
+// 秒 -> XhXmXs 或 XmXs
+function formatDuration(seconds: number | undefined): string {
+  if (seconds == null || isNaN(Number(seconds))) return "--";
+  const n = Math.floor(Number(seconds));
+  if (n < 60) return `${n}s`;
+  const m = Math.floor(n / 60);
+  const s = n % 60;
+  if (m < 60) return `${m}m${s}s`;
+  const h = Math.floor(m / 60);
+  const mm = m % 60;
+  return `${h}h${mm}m${s}s`;
+}
+
+// 数字或占比展示
+function formatNum(val: number | undefined): string {
+  if (val == null || (typeof val === "number" && isNaN(val))) return "--";
+  return String(val);
+}
+function formatPercent(val: number | undefined): string {
+  if (val == null || (typeof val === "number" && isNaN(val))) return "--";
+  return `${val}%`;
+}
+
+// 流量数据
+const trafficStats = reactive([
+  { key: "storeSearch", title: "店铺搜索量", value: "--" },
+  { key: "pageViews", title: "浏览量", value: "--" },
+  { key: "visitors", title: "访客数", value: "--" },
+  { key: "newVisitors", title: "新增访客数", value: "--" },
+  { key: "visitDuration", title: "访问时长", value: "--" },
+  { key: "avgVisitDuration", title: "平均访问时长", value: "--" }
+]);
+
+function setTraffic(d: TrafficData | undefined) {
+  if (!d) return;
+  const map: Record<string, () => string> = {
+    storeSearch: () => formatNum(d.storeSearchVolume),
+    pageViews: () => formatNum(d.pageViews),
+    visitors: () => formatNum(d.visitors),
+    newVisitors: () => formatNum(d.newVisitors),
+    visitDuration: () => formatDuration(d.visitDuration),
+    avgVisitDuration: () => formatDuration(d.avgVisitDuration)
+  };
+  trafficStats.forEach(item => {
+    const fn = map[item.key];
+    if (fn) item.value = fn();
+  });
+}
+
+// 互动数据
+const interactionStats = reactive([
+  { key: "storeCollectionCount", title: "店铺收藏次数", value: "--" },
+  { key: "storeShareCount", title: "店铺分享次数", value: "--" },
+  { key: "storeCheckInCount", title: "店铺打卡次数", value: "--" },
+  { key: "consultMerchantCount", title: "咨询商家次数", value: "--" },
+  { key: "friendsCount", title: "好友数量", value: "--" },
+  { key: "followCount", title: "关注数量", value: "--" },
+  { key: "fansCount", title: "粉丝数量", value: "--" },
+  { key: "postsPublishedCount", title: "发布动态数量", value: "--" },
+  { key: "postLikesCount", title: "动态点赞数量", value: "--" },
+  { key: "postCommentsCount", title: "动态评论数量", value: "--" },
+  { key: "postSharesCount", title: "动态转发数量", value: "--" },
+  { key: "reportedCount", title: "被举报次数", value: "--" },
+  { key: "blockedCount", title: "被拉黑次数", value: "--" }
+]);
+
+function setInteraction(d: InteractionData | undefined) {
+  if (!d) return;
+  interactionStats.forEach(item => {
+    const v = (d as Record<string, number | undefined>)[item.key];
+    item.value = formatNum(v);
+  });
+}
+
+// 优惠券
+const couponStats = reactive([
+  { key: "giftToFriendsCount", title: "赠送好友数量", value: "--" },
+  { key: "giftToFriendsAmount", title: "赠送好友金额合计", value: "--" },
+  { key: "giftToFriendsUsedCount", title: "赠送好友使用数量", value: "--" },
+  { key: "giftToFriendsUsedAmount", title: "赠送好友使用金额合计", value: "--" },
+  { key: "giftToFriendsUsedAmountRatio", title: "赠送好友使用金额占比", value: "--" },
+  { key: "friendsGiftCount", title: "好友赠送数量", value: "--" },
+  { key: "friendsGiftAmount", title: "好友赠送金额合计", value: "--" },
+  { key: "friendsGiftUsedCount", title: "好友赠送使用数量", value: "--" },
+  { key: "friendsGiftUsedAmount", title: "好友赠送使用金额合计", value: "--" },
+  { key: "friendsGiftUsedAmountRatio", title: "好友赠送使用金额占比", value: "--" }
+]);
+
+function setCouponVoucher(list: StatItem[], d: CouponVoucherData | undefined) {
+  if (!d) return;
+  list.forEach(item => {
+    const v = (d as Record<string, number | undefined>)[item.key];
+    item.value = item.key.includes("Ratio") ? formatPercent(v) : formatNum(v);
+  });
+}
+
+// 代金券
+const voucherStats = reactive([
+  { key: "giftToFriendsCount", title: "赠送好友数量", value: "--" },
+  { key: "giftToFriendsAmount", title: "赠送好友金额合计", value: "--" },
+  { key: "giftToFriendsUsedCount", title: "赠送好友使用数量", value: "--" },
+  { key: "giftToFriendsUsedAmount", title: "赠送好友使用金额合计", value: "--" },
+  { key: "giftToFriendsUsedAmountRatio", title: "赠送好友使用金额占比", value: "--" },
+  { key: "friendsGiftCount", title: "好友赠送数量", value: "--" },
+  { key: "friendsGiftAmount", title: "好友赠送金额合计", value: "--" },
+  { key: "friendsGiftUsedCount", title: "好友赠送使用数量", value: "--" },
+  { key: "friendsGiftUsedAmount", title: "好友赠送使用金额合计", value: "--" },
+  { key: "friendsGiftUsedAmountRatio", title: "好友赠送使用金额占比", value: "--" }
+]);
+
+// 服务质量
+const serviceStats = reactive([
+  { key: "storeRating", title: "店铺评分", value: "--" },
+  { key: "scoreOne", title: "口味评分", value: "--" },
+  { key: "scoreTwo", title: "环境评分", value: "--" },
+  { key: "scoreThree", title: "服务评分", value: "--" },
+  { key: "totalReviews", title: "评价数量", value: "--" },
+  { key: "positiveReviews", title: "好评数量", value: "--" },
+  { key: "neutralReviews", title: "中评数量", value: "--" },
+  { key: "negativeReviews", title: "差评数量", value: "--" },
+  { key: "negativeReviewRatio", title: "差评占比", value: "--" },
+  { key: "negativeReviewAppealsCount", title: "差评申诉次数", value: "--" },
+  { key: "negativeReviewAppealSuccessCount", title: "差评申诉成功次数", value: "--" },
+  { key: "negativeReviewAppealSuccessRatio", title: "差评申诉成功占比", value: "--" }
+]);
+
+function setService(d: ServiceQualityData | undefined) {
+  if (!d) return;
+  serviceStats.forEach(item => {
+    const v = (d as Record<string, number | undefined>)[item.key];
+    item.value = item.key.includes("Ratio") ? formatPercent(v) : formatNum(v);
+  });
+}
+
+const toDateString = (d: Date) => d.toISOString().slice(0, 10);
+
+/** 默认日期区间:本周一到当天 */
+function initDateRange() {
+  const end = new Date();
+  const start = new Date(end);
+  const day = start.getDay();
+  const daysToMonday = day === 0 ? 6 : day - 1;
+  start.setDate(start.getDate() - daysToMonday);
+  start.setHours(0, 0, 0, 0);
+  dateRange.value = [toDateString(start), toDateString(end)];
+}
+
+async function fetchData() {
+  const storeId = localGet("createdId");
+  if (!storeId) {
+    ElMessage.warning("请先选择门店");
+    return;
+  }
+  if (!dateRange.value || dateRange.value.length !== 2) {
+    ElMessage.warning("请选择日期区间");
+    return;
+  }
+  const [startTime, endTime] = dateRange.value;
+  loading.value = true;
+  try {
+    const res: any = await getStatistics({ startTime, endTime, storeId });
+    const data = res?.data ?? res;
+    setTraffic(data?.trafficData);
+    setInteraction(data?.interactionData);
+    setCouponVoucher(couponStats, data?.couponData);
+    setCouponVoucher(voucherStats, data?.voucherData);
+    setService(data?.serviceQualityData);
+    priceListRanking.value = data?.priceListRanking ?? [];
+  } catch (e) {
+    ElMessage.error("获取数据失败");
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleQuery() {
+  fetchData();
+}
+
+function handleReset() {
+  initDateRange();
+  fetchData();
+}
+
+function handleCompare() {
+  if (!dateRange.value || dateRange.value.length !== 2) {
+    ElMessage.warning("请先选择日期区间");
+    return;
+  }
+
+  const [start, end] = dateRange.value;
+  const startDate = new Date(start);
+  const endDate = new Date(end);
+  let compareStart = "";
+  let compareEnd = "";
+
+  if (compareRange.value === "samePeriod") {
+    const compareStartDate = new Date(startDate);
+    const compareEndDate = new Date(endDate);
+    compareStartDate.setFullYear(compareStartDate.getFullYear() - 1);
+    compareEndDate.setFullYear(compareEndDate.getFullYear() - 1);
+    compareStart = toDateString(compareStartDate);
+    compareEnd = toDateString(compareEndDate);
+  } else if (compareRange.value === "lastPeriod") {
+    const duration = endDate.getTime() - startDate.getTime();
+    const msPerDay = 86400000;
+    compareStart = toDateString(new Date(startDate.getTime() - duration - msPerDay));
+    compareEnd = toDateString(new Date(startDate.getTime() - msPerDay));
+  }
+
+  router.push({
+    path: "/businessData/compare",
+    query: {
+      start,
+      end,
+      compareType: compareRange.value,
+      compareStart,
+      compareEnd
+    }
+  });
+}
+
+onMounted(() => {
+  initDateRange();
+  fetchData();
+});
+</script>
+
+<style lang="scss" scoped>
+/* 与订单详情等页一致的 table-box 容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  height: auto !important;
+  min-height: 100%;
+}
+.data-overview-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;
+    &.compare-label {
+      margin-left: 8px;
+    }
+  }
+  .date-range-picker {
+    width: 240px;
+  }
+  .compare-select {
+    width: 120px;
+  }
+  .filter-actions {
+    display: flex;
+    gap: 8px;
+  }
+}
+.data-card {
+  flex: 1;
+  border-radius: 8px;
+  :deep(.el-card__body) {
+    padding: 0;
+  }
+}
+.data-tabs {
+  :deep(.el-tabs__header) {
+    margin: 0 16px;
+    border-bottom: 1px solid var(--el-border-color-lighter);
+  }
+  :deep(.el-tabs__content) {
+    padding: 20px 16px;
+  }
+  :deep(.el-tabs__item.is-active) {
+    font-weight: 600;
+  }
+}
+.ranking-wrap {
+  min-height: 120px;
+}
+
+@media (width <= 768px) {
+  .filter-bar .filter-left {
+    width: 100%;
+  }
+  .filter-bar .filter-actions {
+    justify-content: flex-end;
+    width: 100%;
+  }
+}
+</style>