Jelajahi Sumber

直播数据统计

yuhongqi 3 jam lalu
induk
melakukan
e8837ef52c
2 mengubah file dengan 1231 tambahan dan 0 penghapusan
  1. 56 0
      src/api/live/liveData.js
  2. 1175 0
      src/views/live/liveStatistics/index.vue

+ 56 - 0
src/api/live/liveData.js

@@ -121,3 +121,59 @@ export function exportLiveDataCompany(data) {
     data: data
     data: data
   })
   })
 }
 }
+
+
+//
+// // 直播数据统计-数据概览(12项指标)
+// export function getLiveStatisticsOverview(liveIds) {
+//     return request({
+//         url: '/liveData/liveData/getLiveStatisticsOverview',
+//         method: 'post',
+//         data: liveIds || []
+//     })
+// }
+//
+// // 直播趋势-进入人数折线图(基于 live_user_first_entry 与直播开始时间的相对时间)
+// export function getLiveEntryTrend(liveIds) {
+//     return request({
+//         url: '/liveData/liveData/getLiveEntryTrend',
+//         method: 'post',
+//         data: liveIds || []
+//     })
+// }
+//
+// // 直播间学员列表(分页)
+// export function listLiveRoomStudents(data) {
+//     return request({
+//         url: '/liveData/liveData/listLiveRoomStudents',
+//         method: 'post',
+//         data: data || {}
+//     })
+// }
+//
+// // 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
+// export function listProductCompareStats(data) {
+//     return request({
+//         url: '/liveData/liveData/listProductCompareStats',
+//         method: 'post',
+//         data: data || {}
+//     })
+// }
+//
+// // 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
+// export function listInviteSalesOptions(liveIds) {
+//     return request({
+//         url: '/liveData/liveData/listInviteSalesOptions',
+//         method: 'post',
+//         data: liveIds || []
+//     })
+// }
+//
+// // 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
+// export function listInviteCompareStats(data) {
+//     return request({
+//         url: '/liveData/liveData/listInviteCompareStats',
+//         method: 'post',
+//         data: data || {}
+//     })
+// }

+ 1175 - 0
src/views/live/liveStatistics/index.vue

@@ -0,0 +1,1175 @@
+<template>
+  <div class="app-container">
+    <!-- 左上角:选择直播间(多选,最多展示8个,多余折叠)+ 查询按钮 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <div
+          class="live-select-input"
+          @click="openLiveSelectDialog">
+          <div class="live-select-content">
+            <template v-if="selectedLiveList.length === 0">
+              <span class="placeholder">请选择直播间</span>
+            </template>
+            <template v-else>
+              <template v-for="(item, idx) in displayedLives">
+                <span :key="item.liveId" class="live-tag">{{ item.liveName }}({{ item.liveId }})</span>
+                <span v-if="idx < displayedLives.length - 1 || selectedLiveList.length > 7" class="tag-sep">、</span>
+              </template>
+              <span v-if="selectedLiveList.length > 7" class="fold-text">等{{ selectedLiveList.length }}个</span>
+            </template>
+          </div>
+          <div class="live-select-suffix">
+            <i v-if="selectedLiveList.length > 0" class="el-icon-circle-close" @click.stop="handleClearLive"></i>
+            <i class="el-icon-arrow-down"></i>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="1">
+        <el-button
+          type="primary"
+          icon="el-icon-search"
+          size="small"
+          :loading="overviewLoading"
+          :disabled="selectedLiveList.length === 0"
+          @click="fetchOverview">
+          查询
+        </el-button>
+      </el-col>
+    </el-row>
+
+    <el-card ref="statsCard" class="box-card" shadow="never">
+      <div class="stats-main">
+        <!-- 固定导航栏:滚动时固定在顶部 -->
+        <div class="stats-nav-wrapper">
+          <div v-if="navFixed" ref="statsNavSpacer" class="stats-nav-spacer" :style="{ height: navHeight + 'px' }"/>
+          <div
+            ref="statsNavBar"
+            :class="['stats-nav-bar', { 'is-fixed': navFixed }]"
+            :style="navFixed ? navFixedStyle : {}">
+            <span
+              v-for="item in navItems"
+              :key="item.key"
+              :class="['nav-item', { active: activeNav === item.key }]"
+              @click="scrollToSection(item.key)">
+              {{ item.label }}
+            </span>
+          </div>
+        </div>
+        <!-- 四个板块竖排展示,每个板块独立卡片样式 -->
+        <el-card ref="sectionOverview" class="stats-section-card" shadow="never" data-section="overview">
+          <div slot="header" class="section-title">数据概览</div>
+          <div v-loading="overviewLoading" class="overview-content">
+            <template >
+              <!-- 直播数据 -->
+              <div class="overview-block">
+                <div class="overview-block-title">直播数据</div>
+                <el-row :gutter="16" class="overview-grid">
+                  <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="item in overviewItems" :key="item.key">
+                    <div class="overview-item">
+                      <div class="overview-label">{{ item.label }}</div>
+                      <div class="overview-value">{{ formatOverviewValue(overviewData[item.key]) }}</div>
+                    </div>
+                  </el-col>
+                </el-row>
+              </div>
+              <!-- 互动带货数据 -->
+              <div class="overview-block">
+                <div class="overview-block-title">互动带货数据</div>
+                <el-row :gutter="16" class="overview-grid">
+                  <el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="item in interactionItems" :key="item.key">
+                    <div class="overview-item">
+                      <div class="overview-label">{{ item.label }}</div>
+                      <div class="overview-value">{{ formatOverviewValue(overviewData[item.key]) }}</div>
+                    </div>
+                  </el-col>
+                </el-row>
+              </div>
+            </template>
+          </div>
+        </el-card>
+        <el-card ref="sectionTrend" class="stats-section-card" shadow="never" data-section="trend">
+          <div slot="header" class="section-title">直播趋势</div>
+          <div v-loading="trendLoading" class="trend-chart-wrap">
+            <div  ref="trendChartRef" class="trend-chart"></div>
+          </div>
+        </el-card>
+        <el-card ref="sectionStudent" class="stats-section-card" shadow="never" data-section="student">
+          <div slot="header" class="section-title">直播间学员</div>
+          <div class="student-section">
+            <el-form :model="studentQueryParams" :inline="true" class="filter-form student-filter">
+              <el-form-item label="直播名称">
+                <el-select
+                  v-model="studentQueryParams.liveIds"
+                  multiple
+                  collapse-tags
+                  placeholder="不选则展示全部已选直播间"
+                  size="small"
+                  clearable
+                  style="width: 280px;">
+                  <el-option
+                    v-for="item in selectedLiveList"
+                    :key="item.liveId"
+                    :label="item.liveName + '(' + item.liveId + ')'"
+                    :value="item.liveId"/>
+                </el-select>
+              </el-form-item>
+              <el-form-item label="首次访问时间">
+                <el-date-picker
+                  v-model="studentFirstEntryRange"
+                  type="datetimerange"
+                  range-separator="至"
+                  start-placeholder="开始时间"
+                  end-placeholder="结束时间"
+                  value-format="yyyy-MM-dd HH:mm:ss"
+                  size="small"
+                  style="width: 340px;">
+                </el-date-picker>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="small" :loading="studentLoading" @click="fetchStudentList">查询</el-button>
+                <el-button icon="el-icon-refresh" size="small" @click="resetStudentQuery">重置</el-button>
+              </el-form-item>
+            </el-form>
+            <el-table
+              v-loading="studentLoading"
+              :data="studentList"
+              border
+              style="width: 100%">
+              <el-table-column label="用户名" align="center" min-width="160">
+                <template slot-scope="scope">
+                  <div class="user-name-cell">
+                    <el-avatar :size="28" :src="scope.row.avatar" icon="el-icon-user-solid"/>
+                    <el-tooltip :content="scope.row.userName" placement="top">
+                      <span class="user-name-text">{{ scope.row.userName && scope.row.userName.length > 8 ? scope.row.userName.substring(0, 8) + '...' : scope.row.userName }}</span>
+                    </el-tooltip>
+                  </div>
+                </template>
+              </el-table-column>
+              <el-table-column label="直播间名称" align="center" prop="liveName" min-width="150" show-overflow-tooltip/>
+              <el-table-column label="销售名称" align="center" prop="salesName" min-width="100" show-overflow-tooltip/>
+              <el-table-column label="用户创建时间" align="center" prop="userCreateTime" width="170"/>
+              <el-table-column label="联系方式" align="center" prop="contact" width="140"/>
+            </el-table>
+            <pagination
+              v-show="studentTotal > 0"
+              :total="studentTotal"
+              :page.sync="studentQueryParams.pageNum"
+              :limit.sync="studentQueryParams.pageSize"
+              :auto-scroll="false"
+              @pagination="fetchStudentList"
+            />
+          </div>
+        </el-card>
+        <el-card ref="sectionCompare" class="stats-section-card" shadow="never" data-section="compare">
+          <div slot="header" class="section-title">数据对比</div>
+          <div class="compare-tabs-wrap">
+            <el-radio-group v-model="compareTab" size="small" class="compare-tabs">
+              <el-radio-button label="invite">邀课对比</el-radio-button>
+              <el-radio-button label="product">商品对比</el-radio-button>
+            </el-radio-group>
+            <!-- 邀课对比 -->
+            <div v-show="compareTab === 'invite'" class="compare-panel">
+              <el-form :model="inviteCompareQueryParams" :inline="true" class="filter-form">
+                <el-form-item label="分享人">
+                  <el-select
+                    v-model="inviteCompareQueryParams.companyUserIds"
+                    multiple
+                    collapse-tags
+                    placeholder="请选择分享人(需先选择直播间并查询)"
+                    size="small"
+                    clearable
+                    style="width: 400px;">
+                    <el-option
+                      v-for="item in inviteSalesOptions"
+                      :key="(item.companyId || '') + '_' + (item.companyUserId || '')"
+                      :label="item.salesName + (item.companyName ? '(' + item.companyName + ')' : '')"
+                      :value="(item.companyId || '') + '_' + (item.companyUserId || '')"/>
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" icon="el-icon-search" size="small" :loading="inviteCompareLoading" @click="fetchInviteCompareList">查询</el-button>
+                  <el-button icon="el-icon-refresh" size="small" @click="resetInviteCompareQuery">重置</el-button>
+                </el-form-item>
+              </el-form>
+              <el-table
+                v-loading="inviteCompareLoading"
+                :data="inviteCompareList"
+                border
+                style="width: 100%">
+                <el-table-column label="归属公司" align="center" prop="companyName" min-width="140" show-overflow-tooltip/>
+                <el-table-column label="销售名称" align="center" prop="salesName" min-width="120" show-overflow-tooltip/>
+                <el-table-column label="邀请人数" align="center" prop="inviteCount" width="120"/>
+                <el-table-column label="已支付订单数" align="center" prop="paidOrderCount" width="140"/>
+                <el-table-column label="订单总金额" align="center" prop="totalGmv" width="140">
+                  <template slot-scope="scope">
+                    {{ formatProductMoney(scope.row.totalGmv) }}
+                  </template>
+                </el-table-column>
+              </el-table>
+              <pagination
+                v-show="inviteCompareTotal > 0"
+                :total="inviteCompareTotal"
+                :page.sync="inviteCompareQueryParams.pageNum"
+                :limit.sync="inviteCompareQueryParams.pageSize"
+                :auto-scroll="false"
+                @pagination="fetchInviteCompareList"
+              />
+            </div>
+            <!-- 商品对比 -->
+            <div v-show="compareTab === 'product'" class="compare-panel">
+              <el-form :model="productCompareQueryParams" :inline="true" class="filter-form">
+                <el-form-item label="直播间带货的产品">
+                  <el-select
+                    v-model="productCompareQueryParams.productIds"
+                    multiple
+                    collapse-tags
+                    placeholder="请选择商品(需先选择直播间并查询)"
+                    size="small"
+                    clearable
+                    style="width: 400px;">
+                    <el-option
+                      v-for="item in liveProductOptions"
+                      :key="item.productId"
+                      :label="item.productName"
+                      :value="item.productId"/>
+                  </el-select>
+                </el-form-item>
+                <el-form-item>
+                  <el-button type="primary" icon="el-icon-search" size="small" :loading="productCompareLoading" @click="fetchProductCompareList">查询</el-button>
+                  <el-button icon="el-icon-refresh" size="small" @click="resetProductCompareQuery">重置</el-button>
+                </el-form-item>
+              </el-form>
+              <el-table
+                v-loading="productCompareLoading"
+                :data="productCompareList"
+                border
+                style="width: 100%">
+                <el-table-column label="商品名称" align="center" prop="productName" min-width="180" show-overflow-tooltip/>
+                <el-table-column label="下单未支付人数" align="center" prop="unpaidUserCount" width="140"/>
+                <el-table-column label="成交人数" align="center" prop="paidUserCount" width="120"/>
+                <el-table-column label="成交金额" align="center" prop="totalGmv" width="140">
+                  <template slot-scope="scope">
+                    {{ formatProductMoney(scope.row.totalGmv) }}
+                  </template>
+                </el-table-column>
+              </el-table>
+              <pagination
+                v-show="productCompareTotal > 0"
+                :total="productCompareTotal"
+                :page.sync="productCompareQueryParams.pageNum"
+                :limit.sync="productCompareQueryParams.pageSize"
+                :auto-scroll="false"
+                @pagination="fetchProductCompareList"
+              />
+            </div>
+          </div>
+        </el-card>
+      </div>
+    </el-card>
+
+    <!-- 选择直播间弹窗 -->
+    <el-dialog
+      title="选择直播间"
+      :visible.sync="liveSelectDialogVisible"
+      width="900px"
+      append-to-body
+      :close-on-click-modal="false"
+      @open="handleDialogOpen"
+      @close="handleDialogClose">
+      <!-- 上面:筛选条件 -->
+      <el-form :model="liveQueryParams" ref="liveQueryForm" :inline="true" label-width="100px" class="filter-form">
+        <el-form-item label="直播名称" prop="liveName">
+          <el-input
+            v-model="liveQueryParams.liveName"
+            placeholder="请输入直播名称"
+            clearable
+            size="small"
+            style="width: 160px;"
+            @keyup.enter.native="handleLiveSearch"/>
+        </el-form-item>
+        <el-form-item label="直播时间" prop="createTimeRange">
+          <el-date-picker
+            v-model="createTimeRange"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            size="small"
+            style="width: 340px;">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="公司名称" prop="companyName">
+          <el-input
+            v-model="liveQueryParams.companyName"
+            placeholder="请输入公司名称"
+            clearable
+            size="small"
+            style="width: 160px;"
+            @keyup.enter.native="handleLiveSearch"/>
+        </el-form-item>
+        <el-form-item label="直播状态" prop="status">
+          <el-select v-model="liveQueryParams.status" placeholder="请选择" clearable size="small" style="width: 120px;">
+            <el-option label="待直播" :value="1"></el-option>
+            <el-option label="直播中" :value="2"></el-option>
+            <el-option label="已结束" :value="3"></el-option>
+            <el-option label="直播回放中" :value="4"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="small" @click="handleLiveSearch">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="small" @click="resetLiveQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 下面:展示列表(多选,支持翻页继续选择) -->
+      <el-table
+        ref="liveTable"
+        v-loading="liveListLoading"
+        :data="liveList"
+        row-key="liveId"
+        @selection-change="handleSelectionChange"
+        border
+        max-height="400">
+        <el-table-column type="selection" width="55" align="center" reserve-selection />
+        <el-table-column label="直播ID" align="center" prop="liveId" width="100"/>
+        <el-table-column label="直播名称" align="center" prop="liveName" min-width="150" show-overflow-tooltip/>
+        <el-table-column label="直播状态" align="center" prop="status" width="120">
+          <template slot-scope="scope">
+            <el-tag v-if="scope.row.status === 1" size="small">待直播</el-tag>
+            <el-tag v-else-if="scope.row.status === 2" type="success" size="small">直播中</el-tag>
+            <el-tag v-else-if="scope.row.status === 3" type="info" size="small">已结束</el-tag>
+            <el-tag v-else-if="scope.row.status === 4" type="warning" size="small">直播回放中</el-tag>
+            <span v-else>---</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="直播公司" align="center" prop="companyName" min-width="120" show-overflow-tooltip>
+          <template slot-scope="scope">
+            {{ scope.row.companyName || '总台' }}
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="liveTotal > 0"
+        :total="liveTotal"
+        :page.sync="liveQueryParams.pageNum"
+        :limit.sync="liveQueryParams.pageSize"
+        @pagination="getLiveList"
+      />
+
+      <div slot="footer" class="dialog-footer">
+        <span class="selected-count">已选 {{ selectedLiveList.length }}/15 个</span>
+        <el-button @click="liveSelectDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="confirmSelectLive">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import { listLive } from '@/api/live/live'
+import { getLiveStatisticsOverview, getLiveEntryTrend, listLiveRoomStudents, listProductCompareStats, listInviteSalesOptions, listInviteCompareStats } from '@/api/live/liveData'
+import { listLiveGoods } from '@/api/live/liveGoods'
+
+export default {
+  name: 'LiveStatistics',
+  data() {
+    return {
+      // 已选择的直播间(多选)
+      selectedLiveList: [],
+      // 选择弹窗
+      liveSelectDialogVisible: false,
+      liveListLoading: false,
+      liveList: [],
+      liveTotal: 0,
+      // 直播间查询参数
+      liveQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        liveName: null,
+        companyName: null,
+        status: null,
+        beginTime: null,
+        endTime: null
+      },
+      // 创建时间范围(用于日期选择器双向绑定)
+      createTimeRange: null,
+      // 当前激活的导航
+      activeNav: 'overview',
+      // 导航配置
+      navItems: [
+        { key: 'overview', label: '数据概览' },
+        { key: 'trend', label: '直播趋势' },
+        { key: 'student', label: '直播间学员' },
+        { key: 'compare', label: '数据对比' }
+      ],
+      // 导航栏固定
+      navFixed: false,
+      navHeight: 48,
+      navFixedStyle: {},
+      // 数据概览
+      overviewData: {},
+      overviewLoading: false,
+      // 直播趋势折线图
+      trendLoading: false,
+      trendChartData: { xAxis: [], series: [] },
+      trendChart: null,
+      // 直播间学员
+      studentList: [],
+      studentTotal: 0,
+      studentLoading: false,
+      studentQueryParams: {
+        liveIds: [],
+        pageNum: 1,
+        pageSize: 10
+      },
+      studentFirstEntryRange: null,
+      // 数据对比切换 tab:invite=邀课对比,product=商品对比
+      compareTab: 'invite',
+      // 商品对比(嵌入数据对比)
+      liveProductOptions: [],
+      productCompareList: [],
+      productCompareTotal: 0,
+      productCompareLoading: false,
+      productCompareQueryParams: {
+        productIds: [],
+        liveIds: [],
+        pageNum: 1,
+        pageSize: 10
+      },
+      // 邀课对比(嵌入数据对比)
+      inviteSalesOptions: [],
+      inviteCompareList: [],
+      inviteCompareTotal: 0,
+      inviteCompareLoading: false,
+      inviteCompareQueryParams: {
+        companyUserIds: [],
+        pageNum: 1,
+        pageSize: 10
+      }
+    }
+  },
+  computed: {
+    overviewItems() {
+      return [
+        { key: 'beforeLiveUv', label: '开播前访问人数(uv)' },
+        { key: 'totalWatchUv', label: '累计观看人数(uv)' },
+        { key: 'over10MinCount', label: '停留时长超过10分钟人数' },
+        { key: 'totalWatchMinutes', label: '总观看时长(分钟)' },
+        { key: 'avgWatchMinutes', label: '平均观看时长(分钟)' },
+        { key: 'replayWatchUv', label: '累计回放观看人数(uv)' },
+        { key: 'replayVisitPv', label: '累计回放访问人数(pv)' },
+        { key: 'replayOnlyCount', label: '仅看过回放的人数' },
+        { key: 'replayTotalMinutes', label: '回放总观看时长(分钟)' },
+        { key: 'replayAvgMinutes', label: '回放平均观看时长(分钟)' },
+        { key: 'completeCount', label: '完课人数' },
+        { key: 'subscribeCount', label: '预约人数' }
+      ]
+    },
+    interactionItems() {
+      return [
+        { key: 'lotteryCount', label: '抽奖次数' },
+        { key: 'lotteryJoinCount', label: '参与抽奖人数' },
+        { key: 'lotteryWinCount', label: '中奖人数' },
+        { key: 'paidUserCount', label: '支付点击人数' },
+        { key: 'unpaidUserCount', label: '未支付人数' },
+        { key: 'totalGmv', label: '总成交金额' },
+        { key: 'totalProductQty', label: '商品总销量' }
+      ]
+    },
+    headerOffset() {
+      const fixed = this.$store.state.settings.fixedHeader
+      const tagsView = this.$store.state.settings.tagsView
+      if (fixed && tagsView) return 84
+      if (fixed) return 50
+      return 0
+    },
+    displayedLives() {
+      return this.selectedLiveList.slice(0, 7)
+    },
+    hasTrendData() {
+      return this.trendChartData.xAxis && this.trendChartData.xAxis.length > 0
+    }
+  },
+  mounted() {
+    this.$nextTick(() => this.measureNavHeight())
+    window.addEventListener('scroll', this.handleNavScroll, { passive: true })
+    window.addEventListener('resize', this.handleNavScroll)
+    window.addEventListener('resize', this.handleTrendChartResize)
+    this.trendChartData = this.getDefaultTrendData()
+    this.$nextTick(() => setTimeout(() => this.renderTrendChart(), 100))
+  },
+  beforeDestroy() {
+    window.removeEventListener('scroll', this.handleNavScroll)
+    window.removeEventListener('resize', this.handleNavScroll)
+    window.removeEventListener('resize', this.handleTrendChartResize)
+    if (this.trendChart) {
+      this.trendChart.dispose()
+      this.trendChart = null
+    }
+  },
+  methods: {
+    openLiveSelectDialog() {
+      this.liveSelectDialogVisible = true
+    },
+    handleClearLive() {
+      this.selectedLiveList = []
+      this.studentList = []
+      this.studentTotal = 0
+      this.studentQueryParams.liveIds = []
+      this.studentFirstEntryRange = null
+      this.liveProductOptions = []
+      this.productCompareList = []
+      this.productCompareTotal = 0
+      this.productCompareQueryParams.productIds = []
+      if (this.trendChart) {
+        this.trendChart.dispose()
+        this.trendChart = null
+      }
+      this.trendChartData = { xAxis: [], series: [] }
+      this.overviewData = {}
+      this.$nextTick(() => {
+        const table = this.$refs.liveTable
+        if (table && typeof table.clearSelection === 'function') {
+          table.clearSelection()
+        }
+        this.handleTrendChartResize()
+      })
+    },
+    handleDialogOpen() {
+      this.getLiveList()
+    },
+    handleDialogClose() {
+      this.resetLiveQuery()
+    },
+    handleLiveSearch() {
+      this.liveQueryParams.pageNum = 1
+      this.getLiveList()
+    },
+    resetLiveQuery() {
+      this.liveQueryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        liveName: null,
+        companyName: null,
+        status: null,
+        beginTime: null,
+        endTime: null
+      }
+      this.createTimeRange = null
+    },
+    getLiveList() {
+      const params = { ...this.liveQueryParams }
+      if (this.createTimeRange && this.createTimeRange.length === 2) {
+        params.beginTime = this.createTimeRange[0]
+        params.endTime = this.createTimeRange[1]
+      }
+      this.liveListLoading = true
+      listLive(params).then(res => {
+        this.liveList = res.rows || []
+        this.liveTotal = res.total || 0
+        this.liveListLoading = false
+        this.$nextTick(() => this.setTableSelection())
+      }).catch(() => {
+        this.liveListLoading = false
+      })
+    },
+    handleSelectionChange(selection) {
+      if (selection && selection.length > 15) {
+        this.$message.warning('最多只能选择15个直播间')
+        this.$nextTick(() => this.setTableSelection())
+      } else {
+        this.selectedLiveList = selection || []
+      }
+    },
+    setTableSelection() {
+      this.$nextTick(() => {
+        const table = this.$refs.liveTable
+        if (!table || !this.liveList.length) return
+        const selectedIds = new Set(this.selectedLiveList.map(l => l.liveId))
+        this.liveList.forEach(row => {
+          table.toggleRowSelection(row, selectedIds.has(row.liveId))
+        })
+      })
+    },
+    confirmSelectLive() {
+      this.liveSelectDialogVisible = false
+      // 默认不选直播名称=展示全部已选直播间的人数
+    },
+    formatProductMoney(val) {
+      if (val === undefined || val === null) return '¥0.00'
+      const num = typeof val === 'number' ? val : parseFloat(val)
+      return '¥' + (isNaN(num) ? '0.00' : num.toFixed(2))
+    },
+    fetchLiveProductsForSelectedLives() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.liveProductOptions = []
+        return
+      }
+      const promises = liveIds.map(liveId => listLiveGoods({ liveId }).then(res => (res.rows || res.data?.rows || [])))
+      Promise.all(promises).then(results => {
+        const productMap = new Map()
+        results.flat().forEach(item => {
+          const pid = item.productId
+          if (pid && !productMap.has(pid)) {
+            productMap.set(pid, { productId: pid, productName: item.productName || item.name || '未知商品' })
+          }
+        })
+        this.liveProductOptions = Array.from(productMap.values())
+      }).catch(() => {
+        this.liveProductOptions = []
+      })
+    },
+    fetchProductCompareList() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.$message.warning('请先选择直播间')
+        return
+      }
+      const params = {
+        liveIds,
+        productIds: this.productCompareQueryParams.productIds,
+        pageNum: this.productCompareQueryParams.pageNum,
+        pageSize: this.productCompareQueryParams.pageSize
+      }
+      this.productCompareLoading = true
+      listProductCompareStats(params).then(res => {
+        this.productCompareList = res.rows || res.data?.rows || []
+        this.productCompareTotal = res.total || res.data?.total || 0
+        this.productCompareLoading = false
+      }).catch(() => {
+        this.productCompareLoading = false
+      })
+    },
+    resetProductCompareQuery() {
+      this.productCompareQueryParams.productIds = []
+      this.productCompareQueryParams.pageNum = 1
+      this.productCompareQueryParams.pageSize = 10
+      this.fetchProductCompareList()
+    },
+    fetchInviteSalesOptions() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.inviteSalesOptions = []
+        return
+      }
+      listInviteSalesOptions(liveIds).then(res => {
+        const data = res.data || res || []
+        this.inviteSalesOptions = Array.isArray(data) ? data : []
+      }).catch(() => {
+        this.inviteSalesOptions = []
+      })
+    },
+    fetchInviteCompareList() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.$message.warning('请先选择直播间')
+        return
+      }
+      const rawIds = this.inviteCompareQueryParams.companyUserIds || []
+      const companyUserIds = rawIds.map(v => {
+        if (typeof v === 'string' && v.includes('_')) {
+          return parseInt(v.split('_')[1], 10)
+        }
+        return typeof v === 'number' ? v : parseInt(v, 10)
+      }).filter(id => !isNaN(id))
+      const params = {
+        liveIds,
+        companyUserIds: companyUserIds.length ? companyUserIds : undefined,
+        pageNum: this.inviteCompareQueryParams.pageNum,
+        pageSize: this.inviteCompareQueryParams.pageSize
+      }
+      this.inviteCompareLoading = true
+      listInviteCompareStats(params).then(res => {
+        this.inviteCompareList = res.rows || res.data?.rows || []
+        this.inviteCompareTotal = res.total || res.data?.total || 0
+        this.inviteCompareLoading = false
+      }).catch(() => {
+        this.inviteCompareLoading = false
+      })
+    },
+    resetInviteCompareQuery() {
+      this.inviteCompareQueryParams.companyUserIds = []
+      this.inviteCompareQueryParams.pageNum = 1
+      this.inviteCompareQueryParams.pageSize = 10
+      this.fetchInviteCompareList()
+    },
+    fetchOverview() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.overviewData = {}
+        this.trendChartData = { xAxis: [], series: [] }
+        if (this.trendChart) {
+          this.trendChart.dispose()
+          this.trendChart = null
+        }
+        return
+      }
+      this.overviewLoading = true
+      getLiveStatisticsOverview(liveIds).then(res => {
+        this.overviewData = res.data || res || {}
+        this.overviewLoading = false
+        this.fetchTrend()
+        // 直播间学员:默认不选直播名称,自动查询全部已选直播间的用户
+        this.studentQueryParams.liveIds = []
+        this.studentFirstEntryRange = null
+        this.studentQueryParams.pageNum = 1
+        this.fetchStudentList()
+        // 商品对比:加载已选直播间的带货产品
+        this.fetchLiveProductsForSelectedLives()
+        // 邀课对比:加载已选直播间的分享人选项
+        this.fetchInviteSalesOptions()
+      }).catch(() => {
+        this.overviewLoading = false
+      })
+    },
+    fetchTrend() {
+      const liveIds = (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.trendChartData = this.getDefaultTrendData()
+        this.$nextTick(() => {
+          setTimeout(() => this.renderTrendChart(), 150)
+        })
+        return
+      }
+      this.trendLoading = true
+      getLiveEntryTrend(liveIds).then(res => {
+        // 兼容多种返回格式: { code, msg, data: { xAxis, series } } 或 { data: [series] } 或 { xAxis, series }
+        const body = res && typeof res === 'object' && res.data !== undefined ? res.data : res
+        let xAxis = []
+        let series = []
+        const dataObj = body && typeof body === 'object' ? body : null
+        if (Array.isArray(body) && body.length > 0) {
+          series = body
+        } else if (dataObj && Array.isArray(dataObj.series) && dataObj.series.length > 0) {
+          xAxis = Array.isArray(dataObj.xAxis) ? dataObj.xAxis : []
+          series = dataObj.series
+        } else if (dataObj && Array.isArray(dataObj.data) && dataObj.data.length > 0) {
+          series = dataObj.data
+        } else if (dataObj && Array.isArray(dataObj.rows) && dataObj.rows.length > 0) {
+          series = dataObj.rows
+        }
+        series = Array.isArray(series) ? series : []
+        if (series.length && (!Array.isArray(xAxis) || !xAxis.length) && series[0] && Array.isArray(series[0].data)) {
+          const len = series[0].data.length
+          xAxis = ['开播前']
+          for (let m = 0; m < len - 1; m++) xAxis.push((m * 5) + 'min')
+        }
+        if (!series.length) {
+          this.trendChartData = this.getDefaultTrendData()
+        } else {
+          if (!xAxis.length) xAxis = this.getDefaultTrendData().xAxis
+          this.trendChartData = { xAxis, series }
+        }
+        this.trendLoading = false
+        this.$nextTick(() => {
+          setTimeout(() => {
+            this.renderTrendChart()
+            this.$nextTick(() => { if (this.trendChart) this.trendChart.resize() })
+          }, 80)
+        })
+      }).catch(() => {
+        this.trendChartData = this.getDefaultTrendData()
+        this.trendLoading = false
+        this.$nextTick(() => {
+          setTimeout(() => {
+            this.renderTrendChart()
+            this.$nextTick(() => {
+              if (this.trendChart) this.trendChart.resize()
+            })
+          }, 150)
+        })
+      })
+    },
+    getDefaultTrendData() {
+      const xAxis = ['开播前']
+      for (let m = 0; m <= 240; m += 5) {
+        xAxis.push(m + 'min')
+      }
+      const zeros = xAxis.map(() => 0)
+      return {
+        xAxis,
+        series: [{ name: '进入人数', data: zeros }]
+      }
+    },
+    renderTrendChart() {
+      const el = this.$refs.trendChartRef
+      if (!el) return
+      let xAxis = this.trendChartData.xAxis && Array.isArray(this.trendChartData.xAxis) ? this.trendChartData.xAxis : []
+      const series = this.trendChartData.series && Array.isArray(this.trendChartData.series) ? this.trendChartData.series : []
+      if (!series.length) return
+      if (!xAxis.length && series[0] && series[0].data) {
+        const len = series[0].data.length
+        xAxis = ['开播前']
+        for (let m = 0; m < len - 1; m++) xAxis.push((m * 5) + 'min')
+      }
+      if (!xAxis.length) xAxis = this.getDefaultTrendData().xAxis
+      if (this.trendChart) this.trendChart.dispose()
+      this.trendChart = echarts.init(el)
+      const option = {
+        title: { text: '相对直播开始时间的累计进入人数', left: 'center' },
+        tooltip: { trigger: 'axis' },
+        legend: { bottom: 0, type: 'scroll' },
+        grid: { left: '3%', right: '4%', bottom: '15%', top: 40, containLabel: true },
+        xAxis: { type: 'category', boundaryGap: false, data: xAxis },
+        yAxis: { type: 'value', name: '累计进入人数' },
+        series: series.map((s, i) => ({
+          name: s.name,
+          type: 'line',
+          smooth: true,
+          data: s.data || []
+        }))
+      }
+      this.trendChart.setOption(option)
+      this.$nextTick(() => {
+        if (this.trendChart) this.trendChart.resize()
+      })
+    },
+    handleTrendChartResize() {
+      if (this.trendChart) this.trendChart.resize()
+    },
+    fetchStudentList() {
+      const liveIds = this.studentQueryParams.liveIds && this.studentQueryParams.liveIds.length > 0
+        ? this.studentQueryParams.liveIds
+        : (this.selectedLiveList || []).map(l => l.liveId)
+      if (!liveIds.length) {
+        this.studentList = []
+        this.studentTotal = 0
+        return
+      }
+      const params = {
+        liveIds,
+        pageNum: this.studentQueryParams.pageNum || 1,
+        pageSize: this.studentQueryParams.pageSize || 10
+      }
+      if (this.studentFirstEntryRange && this.studentFirstEntryRange.length === 2) {
+        params.firstEntryTimeBegin = this.studentFirstEntryRange[0]
+        params.firstEntryTimeEnd = this.studentFirstEntryRange[1]
+      }
+      this.studentLoading = true
+      listLiveRoomStudents(params).then(res => {
+        this.studentList = res.rows || []
+        this.studentTotal = res.total || 0
+        this.studentLoading = false
+      }).catch(() => {
+        this.studentLoading = false
+      })
+    },
+    resetStudentQuery() {
+      this.studentQueryParams.liveIds = []
+      this.studentQueryParams.pageNum = 1
+      this.studentQueryParams.pageSize = 10
+      this.studentFirstEntryRange = null
+      this.fetchStudentList()
+    },
+    formatOverviewValue(val) {
+      if (val === undefined || val === null) return '0'
+      if (typeof val === 'number' && !Number.isInteger(val)) return Number(val).toFixed(2)
+      return String(val)
+    },
+    measureNavHeight() {
+      const nav = this.$refs.statsNavBar
+      if (nav) {
+        this.navHeight = nav.offsetHeight || 48
+      }
+    },
+    handleNavScroll() {
+      const nav = this.$refs.statsNavBar
+      const card = this.$refs.statsCard
+      if (!nav || !card) return
+
+      if (this.navFixed) {
+        const spacer = this.$refs.statsNavSpacer
+        if (spacer) {
+          const rect = spacer.getBoundingClientRect()
+          if (rect.top >= this.headerOffset) {
+            this.navFixed = false
+            this.navFixedStyle = {}
+          }
+        }
+      } else {
+        const rect = nav.getBoundingClientRect()
+        if (rect.top <= this.headerOffset) {
+          const cardRect = card.$el ? card.$el.getBoundingClientRect() : card.getBoundingClientRect()
+          this.navFixedStyle = {
+            left: cardRect.left + 'px',
+            top: this.headerOffset + 'px',
+            width: cardRect.width + 'px'
+          }
+          this.navFixed = true
+        }
+      }
+    },
+    scrollToSection(key) {
+      this.activeNav = key
+      const refMap = {
+        overview: 'sectionOverview',
+        trend: 'sectionTrend',
+        student: 'sectionStudent',
+        compare: 'sectionCompare'
+      }
+      const el = this.$refs[refMap[key]]
+      if (el && el.$el) {
+        el.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
+      } else if (el) {
+        el.scrollIntoView({ behavior: 'smooth', block: 'start' })
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.live-select-input {
+  display: flex;
+  align-items: flex-start;
+  width: 100%;
+  min-width: 200px;
+  min-height: 32px;
+  padding: 6px 12px;
+  border: 1px solid #DCDFE6;
+  border-radius: 4px;
+  cursor: pointer;
+  background: #fff;
+  overflow: hidden;
+}
+.live-select-input:hover {
+  border-color: #C0C4CC;
+}
+.live-select-content {
+  flex: 1;
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  align-items: center;
+  gap: 6px 8px;
+  min-height: 22px;
+  min-width: 0;
+  overflow: hidden;
+}
+.live-select-content .placeholder {
+  color: #C0C4CC;
+  font-size: 14px;
+}
+.live-tag {
+  display: inline-block;
+  padding: 0 8px;
+  height: 22px;
+  line-height: 22px;
+  font-size: 12px;
+  color: #606266;
+  background: #f4f4f5;
+  border-radius: 4px;
+}
+.tag-sep {
+  color: #909399;
+  margin: 0 2px;
+}
+.fold-text {
+  font-size: 12px;
+  color: #909399;
+}
+.live-select-suffix {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding-left: 10px;
+}
+.live-select-suffix i {
+  color: #C0C4CC;
+  font-size: 14px;
+  cursor: pointer;
+}
+.live-select-suffix i:hover {
+  color: #909399;
+}
+.stats-main {
+  padding-top: 0;
+}
+.stats-nav-wrapper {
+  position: relative;
+}
+.stats-nav-spacer {
+  flex-shrink: 0;
+}
+.stats-nav-bar {
+  position: relative;
+  z-index: 100;
+  display: flex;
+  width: 100%;
+  flex: 1 1 auto;
+  background: #fff;
+  border-bottom: 1px solid #ebeef5;
+  margin: 0 -20px 20px -20px;
+  padding: 0;
+  box-shadow: 0 1px 2px rgba(0,0,0,0.03);
+}
+.stats-nav-bar.is-fixed {
+  position: fixed;
+  margin: 0;
+  left: 0;
+  right: 0;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+}
+.stats-nav-bar .nav-item {
+  flex: 1;
+  text-align: center;
+  padding: 14px 12px;
+  font-size: 14px;
+  color: #606266;
+  cursor: pointer;
+  border-bottom: 2px solid transparent;
+  margin-bottom: -1px;
+  transition: all 0.2s;
+}
+.stats-nav-bar .nav-item:hover {
+  color: #409EFF;
+}
+.stats-nav-bar .nav-item.active {
+  color: #409EFF;
+  font-weight: 500;
+  border-bottom-color: #409EFF;
+}
+.overview-content {
+  min-height: 120px;
+}
+.overview-grid {
+  margin-top: 8px;
+}
+.overview-item {
+  padding: 12px 16px;
+  margin-bottom: 12px;
+  background: #fafafa;
+  border-radius: 4px;
+  border-left: 3px solid #409EFF;
+  border: 1px solid #ebeef5;
+}
+.overview-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 6px;
+  line-height: 1.4;
+}
+.overview-value {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+.overview-block {
+  margin-bottom: 24px;
+}
+.overview-block:last-child {
+  margin-bottom: 0;
+}
+.overview-block-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin-bottom: 12px;
+  padding-bottom: 8px;
+  border-bottom: 1px solid #ebeef5;
+}
+.stats-section-card {
+  margin-bottom: 30px;
+  /* 固定导航时:顶部 84px + 导航栏 ~48px ≈ 132,留足空间避免遮挡 */
+  scroll-margin-top: 140px;
+}
+.stats-section-card:last-child {
+  margin-bottom: 0;
+}
+.stats-section {
+  min-height: 300px;
+  padding: 20px 0;
+}
+.stats-section .section-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+  margin-bottom: 16px;
+  padding-bottom: 12px;
+  padding-left: 10px;
+  border-bottom: 1px solid #ebeef5;
+  border-left: 3px solid #67C23A;
+}
+.box-card {
+  margin-bottom: 20px;
+}
+.filter-form {
+  margin-bottom: 16px;
+  padding-bottom: 16px;
+  border-bottom: 1px solid #ebeef5;
+}
+.dialog-footer {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12px;
+}
+.dialog-footer .selected-count {
+  margin-right: auto;
+  font-size: 13px;
+  color: #606266;
+}
+.trend-chart-wrap {
+  min-height: 320px;
+}
+.trend-chart {
+  width: 100%;
+  height: 360px;
+}
+.student-section {
+  min-height: 200px;
+}
+.student-filter {
+  margin-bottom: 16px;
+}
+.user-name-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+}
+.user-name-cell .user-name-text {
+  flex: 1;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.overview-content {
+  min-height: 120px;
+}
+.overview-grid {
+  margin-top: 0;
+}
+.overview-item {
+  padding: 12px 16px;
+  margin-bottom: 12px;
+  background: #fafafa;
+  border-radius: 4px;
+  border: 1px solid #ebeef5;
+}
+.overview-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 8px;
+  line-height: 1.4;
+}
+.overview-value {
+  font-size: 18px;
+  font-weight: 600;
+  color: #303133;
+}
+.compare-tabs-wrap {
+  padding-top: 4px;
+}
+.compare-tabs {
+  margin-bottom: 16px;
+}
+.compare-panel {
+  min-height: 200px;
+}
+</style>