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