Ver código fonte

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_adminUI

hulin 1 dia atrás
pai
commit
093929302f

+ 4 - 4
.env.prod-jsbk

@@ -1,15 +1,15 @@
 # 页面标题
-VUE_APP_TITLE =济世百康管理系统
+VUE_APP_TITLE =康好健康管理系统
 # 首页菜单标题
-VUE_APP_TITLE_INDEX =济世百
+VUE_APP_TITLE_INDEX =康好健
 # 公司名称
-VUE_APP_COMPANY_NAME =辽宁济世百康大药房有限公司
+VUE_APP_COMPANY_NAME =杭州康好互联网科技有限公司
 # ICP备案号
 VUE_APP_ICP_RECORD =辽ICP备2025055445号-16
 # ICP网站访问地址
 VUE_APP_ICP_URL =https://beian.miit.gov.cn
 # 网站LOG
-VUE_APP_LOG_URL =@/assets/logo/jsbk.jpg
+VUE_APP_LOG_URL =@/assets/logo/jsbk.png
 # 存储桶配置
 VUE_APP_OBS_ACCESS_KEY_ID = K2UTJGIN7UTZJR2XMXYG
 # 存储桶配置

+ 30 - 0
src/api/course/courseWatchLog.js

@@ -115,3 +115,33 @@ export function watchLogStatistics(query) {
     params: query
   })
 }
+
+// 查询课程小结详情总体数据
+export function getCourseStatisticsDetail(videoId, periodId) {
+  return request({
+    url: '/course/courseWatchLog/courseStatisticsDetail',
+    method: 'get',
+    params: {
+      videoId: videoId,
+      periodId: periodId
+    }
+  })
+}
+
+// 查询课程小结用户详情列表(分页)
+export function getCourseStatisticsUserDetailList(params) {
+  return request({
+    url: '/course/courseWatchLog/courseStatisticsUserDetail',
+    method: 'get',
+    params: params
+  })
+}
+
+// 导出课程小结用户详情(按创建时间倒序,最多50000条)
+export function exportCourseStatisticsUserDetail(videoId, periodId) {
+  return request({
+    url: '/course/courseWatchLog/courseStatisticsUserDetailExport',
+    method: 'get',
+    params: { videoId, periodId }
+  })
+}

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

@@ -73,4 +73,59 @@ export function exportLiveDataCompany(data) {
     method: 'post',
     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 || {}
+  })
+}

BIN
src/assets/logo/jsbk.png


+ 1 - 1
src/utils/cos.js

@@ -48,7 +48,7 @@ export const uploadObject = async (file, onProgress, type, callBackUp) => {
     const strDate = date.getDate()
     const uploadDay = `${year}${month}${strDate}`
     const videoKey = `/userVideo/${uploadDay}/${upload_file_name}`
-    const courseKey = `/live/${uploadDay}/${upload_file_name}`
+    const courseKey = `/course/${uploadDay}/${upload_file_name}`
     let liveKey;
     console.log("LIVE_PATH value:", process.env.VUE_APP_LIVE_PATH);  // 添加这行查看实际值
     console.log("Type of LIVE_PATH:", typeof process.env.VUE_APP_LIVE_PATH);  // 查看类型

+ 632 - 0
src/views/course/userCoursePeriod/courseStatistics.vue

@@ -0,0 +1,632 @@
+<template>
+  <div class="course-statistics-container">
+    <!-- 筛选条件 -->
+    <el-form :inline="true" :model="queryParams" class="demo-form-inline" style="margin-bottom: 20px;">
+      <el-form-item label="小节名称">
+        <el-input
+          v-model="queryParams.videoName"
+          placeholder="请输入小节名称关键字"
+          clearable
+          size="small"
+          style="width: 300px;"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" size="small" @click="handleQuery">查询</el-button>
+        <el-button size="small" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 数据表格 -->
+    <el-table
+      v-loading="loading"
+      :data="list"
+      border
+      style="width: 100%"
+    >
+      <el-table-column label="课程名称" align="center" prop="courseName" width="200" />
+      <el-table-column label="小节" align="center" prop="videoName" width="210" />
+<!--      <el-table-column label="小节状态" align="center" prop="videoStatus" width="120">-->
+<!--        <template slot-scope="scope">-->
+<!--          <el-tag :type="scope.row.videoStatus === '已开课' ? 'success' : scope.row.videoStatus === '已结束' ? 'info' : 'warning'">-->
+<!--            {{ scope.row.videoStatus }}-->
+<!--          </el-tag>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+      <el-table-column label="开课状态" align="center" prop="openStatus" width="120">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.openStatus === '已开课' ? 'success' : scope.row.openStatus === '已结束' ? 'info' : 'warning'">
+            {{ scope.row.openStatus }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="营期时间" align="center" prop="periodDate" width="120" />
+      <el-table-column label="开始时间" align="center" prop="startTime" width="180" />
+      <el-table-column label="结束时间" align="center" prop="endTime" width="180" />
+      <el-table-column label="操作" align="center"  fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            type="text"
+            size="small"
+            @click="handleViewDetail(scope.row)"
+          >
+            查看详情
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 课程详情抽屉 -->
+    <el-drawer
+      title="课程小结详情"
+      :visible.sync="detailDialog.visible"
+      direction="rtl"
+      size="60%"
+      :close-on-click-modal="false"
+      append-to-body
+      class="course-detail-drawer"
+    >
+      <div v-loading="detailDialog.loading" style="padding: 20px;">
+        <!-- 第一块:总体数据 -->
+        <el-card class="detail-card" shadow="never">
+          <div slot="header" class="card-header">
+            <span>总体数据</span>
+            <el-button type="primary" size="small" @click="handleViewUserDetail">查看用户详情</el-button>
+          </div>
+          <el-row :gutter="20">
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">视频时长</div>
+                <div class="stat-value">{{ formatDuration(detailDialog.data.videoDuration) }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">累计观看人数</div>
+                <div class="stat-value">{{ detailDialog.data.totalWatchCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">累计完课人数</div>
+                <div class="stat-value">{{ detailDialog.data.totalCompleteCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">到课完课率</div>
+                <div class="stat-value">{{ detailDialog.data.completeRate || '0%' }}</div>
+              </div>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 第二块:首次点播数据 -->
+        <el-card class="detail-card" shadow="never">
+          <div slot="header" class="card-header">
+            <span>首次点播数据</span>
+          </div>
+          <el-row :gutter="20">
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">观看人数</div>
+                <div class="stat-value">{{ detailDialog.data.firstWatchCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">>=20分钟人数(首次)</div>
+                <div class="stat-value">{{ detailDialog.data.firstWatch20MinCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">>=30分钟人数(首次)</div>
+                <div class="stat-value">{{ detailDialog.data.firstWatch30MinCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">到课完课率首次(>=20分钟)</div>
+                <div class="stat-value">{{ detailDialog.data.firstCompleteRate20Min || '0%' }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">到课完课率首次(>=30分钟)</div>
+                <div class="stat-value">{{ detailDialog.data.firstCompleteRate30Min || '0%' }}</div>
+              </div>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 第三块:第2-n次观看数据 -->
+        <el-card class="detail-card" shadow="never">
+          <div slot="header" class="card-header">
+            <span>第2-n次观看数据</span>
+          </div>
+          <el-row :gutter="20">
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">观看人数</div>
+                <div class="stat-value">{{ detailDialog.data.repeatWatchCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">>=20分钟人数</div>
+                <div class="stat-value">{{ detailDialog.data.repeatWatch20MinCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">>=30分钟人数</div>
+                <div class="stat-value">{{ detailDialog.data.repeatWatch30MinCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">到课完课率2-n次(>=20分钟)</div>
+                <div class="stat-value">{{ detailDialog.data.repeatCompleteRate20Min || '0%' }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">到课完课率2-n次(>=30分钟)</div>
+                <div class="stat-value">{{ detailDialog.data.repeatCompleteRate30Min || '0%' }}</div>
+              </div>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 第四块:订单数据 -->
+        <el-card class="detail-card" shadow="never">
+          <div slot="header" class="card-header">
+            <span>订单数据</span>
+          </div>
+          <el-row :gutter="20">
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">GMV</div>
+                <div class="stat-value">¥{{ detailDialog.data.gmv || '0.00' }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">付费人数</div>
+                <div class="stat-value">{{ detailDialog.data.paidUserCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">付费单数</div>
+                <div class="stat-value">{{ detailDialog.data.paidOrderCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">总付费转换率</div>
+                <div class="stat-value">{{ detailDialog.data.totalPaidConversionRate || '0%' }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">20min付费转化率</div>
+                <div class="stat-value">{{ detailDialog.data.paidConversionRate20Min || '0%' }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">完课R值</div>
+                <div class="stat-value">{{ detailDialog.data.completeRValue || '0.00' }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">领红包人数</div>
+                <div class="stat-value">{{ detailDialog.data.redPacketUserCount || 0 }}</div>
+              </div>
+            </el-col>
+            <el-col :span="6">
+              <div class="stat-item">
+                <div class="stat-label">答题人数</div>
+                <div class="stat-value">{{ detailDialog.data.answerUserCount || 0 }}</div>
+              </div>
+            </el-col>
+          </el-row>
+        </el-card>
+
+        <!-- 第五块:单品销量统计 -->
+        <el-card class="detail-card" shadow="never">
+          <div slot="header" class="card-header">
+            <span>单品销量统计</span>
+          </div>
+          <el-table
+            :data="detailDialog.data.productList || []"
+            border
+            style="width: 100%"
+          >
+            <el-table-column label="商品名称" align="center" prop="productName" />
+            <el-table-column label="销量" align="center" prop="salesCount" />
+            <el-table-column label="销售额" align="center" prop="salesAmount">
+              <template slot-scope="scope">
+                ¥{{ scope.row.salesAmount || '0.00' }}
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </div>
+    </el-drawer>
+
+    <!-- 用户详情抽屉 -->
+    <el-drawer
+      title="用户看课数据"
+      :visible.sync="userDetailDialog.visible"
+      direction="rtl"
+      size="60%"
+      :close-on-click-modal="false"
+      append-to-body
+    >
+      <div v-loading="userDetailDialog.loading" style="padding: 20px;">
+        <div style="margin-bottom: 20px; text-align: right;">
+          <el-button type="primary" size="small" @click="handleExportUserDetail">导出用户详情</el-button>
+        </div>
+        <el-table
+          :data="userDetailDialog.list"
+          border
+          style="width: 100%"
+        >
+          <el-table-column label="用户名称" align="center" prop="userName" width="150" />
+          <el-table-column label="观看时长" align="center" prop="watchDuration" width="120">
+            <template slot-scope="scope">
+              {{ formatDuration(scope.row.watchDuration) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="第2-n次观看时长" align="center" prop="repeatWatchDuration" width="150">
+            <template slot-scope="scope">
+              {{ formatDuration(scope.row.repeatWatchDuration) }}
+            </template>
+          </el-table-column>
+          <el-table-column label="订单数" align="center" prop="orderCount" width="100" />
+          <el-table-column label="订单金额" align="center" prop="orderAmount" width="120">
+            <template slot-scope="scope">
+              ¥{{ scope.row.orderAmount || '0.00' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="分公司名称" align="center" prop="companyName" width="150" />
+          <el-table-column label="销售名称" align="center" prop="salesName" />
+        </el-table>
+
+        <!-- 分页 -->
+        <pagination
+          v-show="userDetailDialog.total > 0"
+          :total="userDetailDialog.total"
+          :page.sync="userDetailDialog.queryParams.pageNum"
+          :limit.sync="userDetailDialog.queryParams.pageSize"
+          @pagination="getUserDetailList"
+        />
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { getDays } from "@/api/course/userCoursePeriod";
+import { getCourseStatisticsDetail, getCourseStatisticsUserDetailList, exportCourseStatisticsUserDetail } from "@/api/course/courseWatchLog";
+import { download } from "@/utils/common";
+
+export default {
+  name: "CourseStatistics",
+  props: {
+    periodId: {
+      type: [String, Number],
+      default: null
+    },
+    active: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      list: [],
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        videoName: null,
+        periodId: null
+      },
+      // 详情弹窗
+      detailDialog: {
+        visible: false,
+        loading: false,
+        data: {}
+      },
+      // 用户详情弹窗
+      userDetailDialog: {
+        visible: false,
+        loading: false,
+        list: [],
+        total: 0,
+        queryParams: {
+          pageNum: 1,
+          pageSize: 10,
+          videoId: null,
+          periodId: null
+        }
+      }
+    };
+  },
+  watch: {
+    active(newVal) {
+      if (newVal && this.periodId) {
+        this.queryParams.periodId = this.periodId;
+        this.getList();
+      }
+    },
+    periodId(newVal) {
+      if (newVal && this.active) {
+        this.queryParams.periodId = newVal;
+        this.getList();
+      }
+    }
+  },
+  mounted() {
+    if (this.active && this.periodId) {
+      this.queryParams.periodId = this.periodId;
+      this.getList();
+    }
+  },
+  methods: {
+    /** 查询列表 */
+    getList() {
+      if (!this.queryParams.periodId) {
+        this.loading = false;
+        return;
+      }
+      this.loading = true;
+      getDays(this.queryParams).then(response => {
+        if (response.code === 200) {
+          // 映射字段并格式化数据
+          this.list = (response.rows || []).map(item => {
+            return {
+              ...item,
+              // 映射字段
+              periodDate: item.dayDate,
+              startTime: item.startDateTime,
+              endTime: item.endDateTime,
+              // 格式化开课状态
+              openStatus: this.formatCourseStatus(item.status),
+              // 小节状态(可以根据需要设置,这里先使用开课状态)
+              videoStatus: this.formatCourseStatus(item.status)
+            };
+          });
+          this.total = response.total || 0;
+        } else {
+          this.$message.error(response.msg || '查询失败');
+          this.list = [];
+          this.total = 0;
+        }
+        this.loading = false;
+      }).catch(error => {
+        this.$message.error('查询失败:' + (error.message || '未知错误'));
+        this.loading = false;
+        this.list = [];
+        this.total = 0;
+      });
+    },
+    /** 格式化课程状态 */
+    formatCourseStatus(status) {
+      const statusMap = {
+        0: '未开始',
+        1: '已开课',
+        2: '已结束'
+      };
+      return statusMap[status] || '未知状态';
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.queryParams.videoName = null;
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 查看详情 */
+    handleViewDetail(row) {
+      this.detailDialog.visible = true;
+      this.detailDialog.loading = true;
+
+      // 调用API获取总体数据
+      const videoId = row.videoId || row.id;
+      const periodId = this.queryParams.periodId;
+
+      if (!videoId || !periodId) {
+        this.$message.error('视频ID或营期ID不能为空');
+        this.detailDialog.loading = false;
+        return;
+      }
+
+      getCourseStatisticsDetail(videoId, periodId).then(response => {
+        if (response.code === 200 && response.data) {
+          const data = response.data;
+          // 设置总体数据
+          this.detailDialog.data = {
+            videoDuration: data.videoDuration || 0,
+            totalWatchCount: data.totalWatchCount || 0,
+            totalCompleteCount: data.totalCompleteCount || 0,
+            completeRate: data.completeRate != null ? Number(data.completeRate).toFixed(2) + '%' : '0%',
+            // 首次点播数据(接口返回)
+            firstWatchCount: data.firstWatchCount ?? 0,
+            firstWatch20MinCount: data.firstWatch20MinCount ?? 0,
+            firstWatch30MinCount: data.firstWatch30MinCount ?? 0,
+            firstCompleteRate20Min: data.firstCompleteRate20Min != null ? Number(data.firstCompleteRate20Min).toFixed(2) + '%' : '0%',
+            firstCompleteRate30Min: data.firstCompleteRate30Min != null ? Number(data.firstCompleteRate30Min).toFixed(2) + '%' : '0%',
+            // 第2-n次观看数据(接口返回)
+            repeatWatchCount: data.repeatWatchCount ?? 0,
+            repeatWatch20MinCount: data.repeatWatch20MinCount ?? 0,
+            repeatWatch30MinCount: data.repeatWatch30MinCount ?? 0,
+            repeatCompleteRate20Min: data.repeatCompleteRate20Min != null ? Number(data.repeatCompleteRate20Min).toFixed(2) + '%' : '0%',
+            repeatCompleteRate30Min: data.repeatCompleteRate30Min != null ? Number(data.repeatCompleteRate30Min).toFixed(2) + '%' : '0%',
+            // 订单数据(接口返回)
+            gmv: data.gmv != null ? Number(data.gmv).toFixed(2) : '0.00',
+            paidUserCount: data.paidUserCount ?? 0,
+            paidOrderCount: data.paidOrderCount ?? 0,
+            totalPaidConversionRate: data.totalPaidConversionRate != null ? Number(data.totalPaidConversionRate).toFixed(2) + '%' : '0%',
+            paidConversionRate20Min: data.paidConversionRate20Min != null ? Number(data.paidConversionRate20Min).toFixed(2) + '%' : '0%',
+            completeRValue: data.completeRValue != null ? Number(data.completeRValue).toFixed(2) : '0.00',
+            redPacketUserCount: data.redPacketUserCount ?? 0,
+            answerUserCount: data.answerUserCount ?? 0,
+            productList: (data.productList || []).map(p => ({
+              productName: p.productName || '未知商品',
+              salesCount: p.salesCount ?? 0,
+              salesAmount: p.salesAmount != null ? Number(p.salesAmount).toFixed(2) : '0.00'
+            })),
+            videoId: videoId,
+            id: row.id
+          };
+        } else {
+          this.$message.error(response.msg || '获取数据失败');
+        }
+        this.detailDialog.loading = false;
+      }).catch(error => {
+        this.$message.error('获取数据失败:' + (error.message || '未知错误'));
+        this.detailDialog.loading = false;
+      });
+    },
+    /** 查看用户详情 */
+    handleViewUserDetail() {
+      this.userDetailDialog.visible = true;
+      this.userDetailDialog.queryParams.videoId = this.detailDialog.data.videoId || this.detailDialog.data.id;
+      this.userDetailDialog.queryParams.periodId = this.queryParams.periodId;
+      this.userDetailDialog.queryParams.pageNum = 1;
+      this.getUserDetailList();
+    },
+    /** 获取用户详情列表 */
+    getUserDetailList() {
+      const videoId = this.userDetailDialog.queryParams.videoId;
+      const periodId = this.userDetailDialog.queryParams.periodId;
+      if (!videoId || !periodId) {
+        this.$message.error('视频ID或营期ID不能为空');
+        this.userDetailDialog.loading = false;
+        return;
+      }
+      this.userDetailDialog.loading = true;
+      getCourseStatisticsUserDetailList(this.userDetailDialog.queryParams).then(response => {
+        if (response.code === 200 && response.data) {
+          const d = response.data;
+          this.userDetailDialog.list = d.list || d.rows || [];
+          this.userDetailDialog.total = d.total ?? 0;
+        } else {
+          this.userDetailDialog.list = [];
+          this.userDetailDialog.total = 0;
+        }
+        this.userDetailDialog.loading = false;
+      }).catch(() => {
+        this.userDetailDialog.list = [];
+        this.userDetailDialog.total = 0;
+        this.userDetailDialog.loading = false;
+      });
+    },
+    /** 导出用户详情(按创建时间倒序,最多50000条) */
+    handleExportUserDetail() {
+      const videoId = this.userDetailDialog.queryParams.videoId;
+      const periodId = this.userDetailDialog.queryParams.periodId;
+      if (!videoId || !periodId) {
+        this.$message.error('请先查看用户详情');
+        return;
+      }
+      this.$confirm('是否确认导出用户看课数据?(按创建时间倒序,最多50000条)', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        exportCourseStatisticsUserDetail(videoId, periodId).then(response => {
+          if (response.code === 200 && response.msg) {
+            download(response.msg);
+            this.$message.success('导出成功');
+          } else {
+            this.$message.error(response.msg || '导出失败');
+          }
+        }).catch(() => {
+          this.$message.error('导出失败');
+        });
+      }).catch(() => {});
+    },
+    /** 格式化时长 */
+    formatDuration(seconds) {
+      if (!seconds) return '0秒';
+      const hours = Math.floor(seconds / 3600);
+      const minutes = Math.floor((seconds % 3600) / 60);
+      const secs = seconds % 60;
+      if (hours > 0) {
+        return `${hours}小时${minutes}分钟${secs}秒`;
+      } else if (minutes > 0) {
+        return `${minutes}分钟${secs}秒`;
+      } else {
+        return `${secs}秒`;
+      }
+    }
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.course-statistics-container {
+  padding: 20px;
+}
+
+.detail-card {
+  margin-bottom: 20px;
+
+  .card-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-weight: bold;
+    font-size: 16px;
+  }
+
+  .stat-item {
+    text-align: center;
+    padding: 20px;
+    border: 1px solid #EBEEF5;
+    border-radius: 4px;
+    background-color: #F5F7FA;
+    margin-top: 10px;
+
+    .stat-label {
+      font-size: 14px;
+      color: #606266;
+      margin-bottom: 10px;
+    }
+
+    .stat-value {
+      font-size: 24px;
+      font-weight: bold;
+      color: #303133;
+    }
+  }
+}
+
+.course-detail-drawer {
+  ::v-deep .el-drawer__body {
+    overflow-y: auto;
+  }
+}
+</style>

+ 88 - 24
src/views/course/userCoursePeriod/index.vue

@@ -478,15 +478,23 @@
       </div>
     </el-dialog>
 
-    <el-dialog title="修改营期时间" :visible.sync="updatePeriodDate.open" width="500px" append-to-body>
-      <el-form ref="courseUpdateForm" :model="updatePeriodDate.form" label-width="100px">
-        <el-form-item label="营期时间" prop="dayDate">
+    <el-dialog title="修改营期时间" :visible.sync="updatePeriodDate.open" width="550px" append-to-body>
+      <el-form ref="courseUpdateForm" :model="updatePeriodDate.form" label-width="120px">
+        <el-form-item v-if="updatePeriodDate.ids.length > 1" label="已选课程数">
+          <span>{{ updatePeriodDate.ids.length }} 个课程将统一修改</span>
+        </el-form-item>
+        <el-form-item label="营期时间" prop="timeRange">
           <el-date-picker
-            v-model="updatePeriodDate.form.dayDate"
-            :selectableRange="updatePeriodDate.form.dayDate"
-            value-format="yyyy-MM-dd"
-            type="date"
-            placeholder="选择日期">
+            v-model="updatePeriodDate.form.timeRange"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            format="yyyy-MM-dd HH:mm:ss"
+            :default-time="['00:00:00', '23:59:59']"
+            placeholder="选择时间范围"
+            style="width: 100%">
           </el-date-picker>
         </el-form-item>
       </el-form>
@@ -527,14 +535,14 @@
                 v-hasPermi="['course:period:addCourse']"
               >添加课程</el-button>
             </el-col>
-<!--            <el-col :span="1.5">-->
-<!--              <el-button-->
-<!--                type="primary"-->
-<!--                size="mini"-->
-<!--                :disabled="updateCourse.ids.length <= 0"-->
-<!--                @click="handleUpdatePeriodDate"-->
-<!--              >修改营期时间</el-button>-->
-<!--            </el-col>-->
+            <el-col :span="1.5">
+              <el-button
+                type="primary"
+                size="mini"
+                :disabled="updateCourse.ids.length <= 0"
+                @click="handleUpdatePeriodDate"
+              >修改营期时间</el-button>
+            </el-col>
             <el-col :span="1.5">
               <el-button
                 type="primary"
@@ -632,6 +640,13 @@
             :active="activeTab === 'statistics'"
           />
         </el-tab-pane>
+
+        <el-tab-pane label="课程数据" name="courseStatistics">
+          <course-statistics-data
+            :periodId="periodSettingsData.periodId"
+            :active="activeTab === 'courseStatistics'"
+          />
+        </el-tab-pane>
       </el-tabs>
     </div>
   </el-drawer>
@@ -653,6 +668,7 @@ import { courseList,videoList } from '@/api/course/courseRedPacketLog'
 import RedPacket from './redPacket.vue'
 import BatchRedPacket from './batchRedPacket.vue'
 import CourseStatistics from './statistics.vue'
+import CourseStatisticsData from './courseStatistics.vue'
 import Da from "element-ui/src/locale/lang/da";
 import { getConfigByKey } from '@/api/system/config'
 export default {
@@ -660,7 +676,8 @@ export default {
   components: {
     RedPacket,
     BatchRedPacket,
-    CourseStatistics
+    CourseStatistics,
+    CourseStatisticsData
   },
   data() {
     return {
@@ -727,9 +744,11 @@ export default {
       //修改营期时间参数
       updatePeriodDate: {
         open: false,
-        loading: true,
+        loading: false,
         ids: [],
-        form: {},
+        form: {
+          timeRange: null
+        },
       },
       joinTimeSwitch:true,
       updateCourse: {
@@ -1609,12 +1628,37 @@ export default {
       });
     },
     updatePeriodDateSubmit(){
-      updateCourseDate(this.updatePeriodDate.form).then(response => {
+      const form = this.updatePeriodDate.form;
+      if (!form.timeRange || !Array.isArray(form.timeRange) || form.timeRange.length !== 2) {
+        this.$message.warning('请选择完整的营期开始时间和结束时间');
+        return;
+      }
+      const [startDateTime, endDateTime] = form.timeRange;
+      if (startDateTime >= endDateTime) {
+        this.$message.warning('开始时间必须早于结束时间');
+        return;
+      }
+      const ids = this.updatePeriodDate.ids;
+      if (!ids || ids.length === 0) {
+        this.$message.warning('请选择要修改的课程');
+        return;
+      }
+      updateCourseDate({
+        ids: ids,
+        startDateTime: startDateTime,
+        endDateTime: endDateTime
+      }).then(response => {
+        if (response.code === 200) {
           this.$message.success('修改成功');
           this.updatePeriodDate.open = false;
-          // 重新加载课程列表
+          this.updatePeriodDate.form.timeRange = null;
           this.getCourseList();
-        });
+        } else {
+          this.$message.error(response.msg || '修改失败');
+        }
+      }).catch(() => {
+        this.$message.error('修改失败');
+      });
     },
     // saveCourseData(){
     //   updateListCourseData(this.course.list).then(response => {
@@ -1743,9 +1787,29 @@ export default {
       };
       return statusMap[row.status] || '未知状态';
     },
-    /** 营期状态格式化 */
+    /** 单行修改营期时间 */
     handleUpdateDate(row) {
-      this.updatePeriodDate.form = {id: row.id, dayDate: row.dayDate};
+      this.updatePeriodDate.ids = [row.id];
+      this.updatePeriodDate.form = {
+        timeRange: row.startDateTime && row.endDateTime ? [row.startDateTime, row.endDateTime] : null
+      };
+      this.updatePeriodDate.open = true;
+    },
+    /** 批量修改营期时间 */
+    handleUpdatePeriodDate() {
+      const ids = this.updateCourse.ids;
+      if (!ids || ids.length === 0) {
+        this.$message.warning('请先选择要修改的课程');
+        return;
+      }
+      this.updatePeriodDate.ids = [...ids];
+      // 取第一个选中行的开始和结束时间作为默认值
+      const firstRow = this.course.list.find(item => item.id === ids[0]);
+      this.updatePeriodDate.form = {
+        timeRange: firstRow && firstRow.startDateTime && firstRow.endDateTime
+          ? [firstRow.startDateTime, firstRow.endDateTime]
+          : null
+      };
       this.updatePeriodDate.open = true;
     },
     // disabledDate(time) {

+ 38 - 3
src/views/course/videoResource/index.vue

@@ -168,6 +168,11 @@
           <div style="padding: 4px 12px;background: linear-gradient(to right, rgb(196 219 255), #409EFF)">{{ formatDuration(scope.row.duration) }}</div>
         </template>
       </el-table-column>
+      <el-table-column label="视频展示类型" align="center" prop="displayType" width="110">
+        <template slot-scope="scope">
+          {{ scope.row.displayType === 'portrait' ? '竖屏' : '横屏' }}
+        </template>
+      </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template slot-scope="scope">
           <el-button
@@ -235,6 +240,13 @@
           <el-input-number v-model="form.sort"  :min="0" label="请输入排序"></el-input-number>
         </el-form-item>
 
+        <el-form-item label="视频展示类型" prop="displayType">
+          <el-radio-group v-model="form.displayType">
+            <el-radio label="landscape">横屏</el-radio>
+            <el-radio label="portrait">竖屏</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
         <el-form-item label="关联题目" prop="projectIds">
           <el-select
             ref="customSelect"
@@ -594,6 +606,13 @@
             </el-select>
           </el-form-item>
 
+          <el-form-item label="视频展示类型" prop="displayType">
+            <el-radio-group v-model="batchUploadForm.displayType">
+              <el-radio label="landscape">横屏</el-radio>
+              <el-radio label="portrait">竖屏</el-radio>
+            </el-radio-group>
+          </el-form-item>
+
           <el-form-item label="关联题目" prop="projectIds" v-show="currentProject === 'myhk'">
             <el-select
               ref="customSelect"
@@ -635,6 +654,13 @@
             <el-input v-model="batchEditDialog.form.resourceName" placeholder="请输入" />
           </el-form-item>
 
+          <el-form-item label="视频展示类型" prop="displayType">
+            <el-radio-group v-model="batchEditDialog.form.displayType">
+              <el-radio label="landscape">横屏</el-radio>
+              <el-radio label="portrait">竖屏</el-radio>
+            </el-radio-group>
+          </el-form-item>
+
           <el-form-item label="文件名称" prop="fileName" style="margin-top: 20px;display: none">
             <el-input v-model="batchEditDialog.form.fileName" placeholder="请输入" />
           </el-form-item>
@@ -865,6 +891,7 @@ export default {
         typeSubId: null,
         projectIds: [],
         sort: null,
+        displayType: 'landscape',
         hsyVid:null,//火山云上传视频返回vid
         hsyVodUrl:null,//火山云url
         // 新增上传状态字段
@@ -930,7 +957,8 @@ export default {
         typeId: null,
         typeSubId: null,
         projectIds: [],
-        files: []
+        files: [],
+        displayType: 'landscape'
       },
       batchFileList: [],
 
@@ -1049,7 +1077,8 @@ export default {
         videoUrl: null,
         typeId: null,
         typeSubId: null,
-        projectIds: []
+        projectIds: [],
+        displayType: 'landscape'
       };
       // 重置表单验证状态
       this.resetForm("form");
@@ -1105,6 +1134,10 @@ export default {
       // 获取数据并设置表单
       getVideoResource(id).then(async response => {
         this.form = response.data;
+        // 视频展示类型:旧数据可能为空,默认横屏
+        if (!this.form.displayType) {
+          this.form.displayType = 'landscape';
+        }
         await this.changeCateType(this.form.typeId)
 
         // 处理projectIds,确保是数组格式
@@ -1845,7 +1878,8 @@ export default {
           typeId: null,
           typeSubId: null,
           projectIds: [],
-          files: []
+          files: [],
+          displayType: 'landscape'
         };
         this.subTypeList = []
       }
@@ -1878,6 +1912,7 @@ export default {
         typeId: this.batchUploadForm.typeId,
         typeSubId: this.batchUploadForm.typeSubId,
         projectIds: this.batchUploadForm.projectIds,
+        displayType: this.batchUploadForm.displayType || 'landscape',
         progress: 0,
         uploadStatus: 'queued', // Set initial status to queued
         uploadDetails: {

+ 20 - 3
src/views/his/company/index.vue

@@ -756,6 +756,9 @@ export default {
   name: 'Company',
   data() {
     return {
+
+      projectFrom:process.env.VUE_APP_HSY_SPACE,
+
       signProjectName:"",
       redSubmit: false,
       //分账参数
@@ -934,9 +937,22 @@ export default {
         open: false,
         title: '批量修改小程序'
       },
+
     }
   },
   created() {
+
+    cateList().then((response) => {
+      if (this.projectFrom =='mengniu-2114522511'){
+
+        this.getDicts('store_product_package_cate').then(response => {
+          this.cateList = response.data
+        })
+      }else {
+        this.cateList = response.rows
+      }
+    })
+
     getSignProjectName()
     .then(res=>{
       this.signProjectName = res.signProjectName;
@@ -955,9 +971,10 @@ export default {
     getFollowDoctorList().then((response) => {
       this.followDoctorList = response.rows
     })
-    cateList().then((response) => {
-      this.cateList = response.rows
-    })
+    // cateList().then((response) => {
+    //   this.cateList = response.rows
+    // })
+
     listDept().then(response => {
       this.deptOptions = response.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>

+ 783 - 104
src/views/system/config/config.vue

@@ -1,8 +1,42 @@
 <template>
   <div class="app-container">
     <el-tabs v-model="activeName" @tab-click="handleClick">
+      <el-tab-pane label="CID配置" name="cId.config">
+        <el-form :model="form40" label-width="200px">
+          <el-form-item label="是否开启手机号配置" prop="enablePhoneConfig">
+            <el-switch v-model="form40.enablePhoneConfig"></el-switch>
+          </el-form-item>
+
+          <template v-if="form40.enablePhoneConfig">
+            <el-form-item label="生成条数" prop="generateCount">
+              <el-input-number v-model="form40.generateCount" :min="1" :step="1" :precision="0" placeholder="请输入生成条数"></el-input-number>
+            </el-form-item>
+
+            <el-form-item label="开始位置" prop="startIndex">
+              <el-input-number v-model="form40.startIndex" :min="1" :step="1" :precision="0" placeholder="例如: 1"></el-input-number>
+              <span class="tip-text">(从第几位开始生成)</span>
+            </el-form-item>
 
+            <el-form-item label="结束位置" prop="endIndex">
+              <el-input-number v-model="form40.endIndex" :min="1" :step="1" :precision="0" placeholder="例如: 11"></el-input-number>
+              <span class="tip-text">(到第几位结束)</span>
+            </el-form-item>
+          </template>
 
+          <el-form-item label="是否开手机号拨打次数限制" prop="enablePhoneLimitConfig">
+            <el-switch v-model="form40.enablePhoneLimitConfig"></el-switch>
+          </el-form-item>
+          <template v-if="form40.enablePhoneLimitConfig">
+            <el-form-item label="拨打次数限制" prop="startIndex">
+              <el-input-number v-model="form40.numberCalls" :min="1" :step="1" :precision="0" placeholder="例如: 1"></el-input-number>
+            </el-form-item>
+          </template>
+
+          <div class="footer">
+            <el-button type="primary" @click="submitForm40">提 交</el-button>
+          </div>
+        </el-form>
+      </el-tab-pane>
 
       <el-tab-pane label="个微配置" name="wx.config">
         <el-form :model="form30" label-width="200px">
@@ -461,13 +495,13 @@
             </el-switch>
           </el-form-item>
           <el-form-item label="API_URL" v-if="form7.isIdVerification == 1" prop="API_URL">
-              <el-input   v-model="form7.API_URL"  label="请输入API_URL"></el-input>
+            <el-input   v-model="form7.API_URL"  label="请输入API_URL"></el-input>
           </el-form-item>
           <el-form-item label="HASHCODE" v-if="form7.isIdVerification == 1" prop="HASHCODE">
-              <el-input   v-model="form7.HASHCODE"  label="请输入HASHCODE"></el-input>
+            <el-input   v-model="form7.HASHCODE"  label="请输入HASHCODE"></el-input>
           </el-form-item>
           <el-form-item label="SECRET_KEY" v-if="form7.isIdVerification == 1" prop="SECRET_KEY">
-              <el-input   v-model="form7.SECRET_KEY"  label="请输入SECRET_KEY"></el-input>
+            <el-input   v-model="form7.SECRET_KEY"  label="请输入SECRET_KEY"></el-input>
           </el-form-item>
           <el-form-item label="检查库存" prop="checkStock">
             <el-radio-group v-model="form7.checkStock">
@@ -916,14 +950,14 @@
           <el-form-item label="快递鸟地址url" prop="kdnAddressUrl">
             <el-input v-model="form13.kdnAddressUrl" label="请输入kdnAddressUrl"></el-input>
           </el-form-item>
-      <el-form-item label="最低定金金额" prop="shares">
-              <el-input-number v-model="form13.retainer" :min="100"   label="最低定金金额"></el-input-number>
-      </el-form-item>
-      <el-form-item   label="比率" prop="rate">
-          <el-input   v-model="form13.rate"  label="请输入比率">
-            <template slot="append">%</template>
-          </el-input>
-      </el-form-item>
+          <el-form-item label="最低定金金额" prop="shares">
+            <el-input-number v-model="form13.retainer" :min="100"   label="最低定金金额"></el-input-number>
+          </el-form-item>
+          <el-form-item   label="比率" prop="rate">
+            <el-input   v-model="form13.rate"  label="请输入比率">
+              <template slot="append">%</template>
+            </el-input>
+          </el-form-item>
           <el-form-item label="是否开启erp">
             <el-switch
               v-model="form13.erpOpen"
@@ -1518,13 +1552,13 @@
             </el-switch>
           </el-form-item>
 
-           <el-form-item label="是否开启IM" prop="isOpenIM">
-             <el-switch
-               v-model="form18.isOpenIM"
-               active-color="#13ce66"
-               inactive-color="#ff4949">
-             </el-switch>
-           </el-form-item>
+          <el-form-item label="是否开启IM" prop="isOpenIM">
+            <el-switch
+              v-model="form18.isOpenIM"
+              active-color="#13ce66"
+              inactive-color="#ff4949">
+            </el-switch>
+          </el-form-item>
           <el-form-item label="是否开启企微二维码" prop="showQwCode">
             <el-switch
               v-model="form18.showQwCode"
@@ -1532,7 +1566,7 @@
               inactive-color="#ff4949">
             </el-switch>
           </el-form-item>
-           <el-form-item label="侧边栏是否仅展示当天课程" prop="showQwCode">
+          <el-form-item label="侧边栏是否仅展示当天课程" prop="showQwCode">
             <el-switch
               v-model="form18.sidebarOnlyShowTodayCourse"
               active-color="#13ce66"
@@ -1720,10 +1754,10 @@
           <el-form-item label="apiV3密钥" prop="apiV3Key">
             <el-input v-model="form19.apiV3Key" label="请输入apiV3Key"></el-input>
           </el-form-item>
-           <el-form-item   label="公钥ID" prop="publicKeyId">
+          <el-form-item   label="公钥ID" prop="publicKeyId">
             <el-input v-model="form19.publicKeyId" label="请输入公钥ID"></el-input>
           </el-form-item>
-           <el-form-item   label="公钥证书" prop="publicKeyPath">
+          <el-form-item   label="公钥证书" prop="publicKeyPath">
             <el-input v-model="form19.publicKeyPath" label="请输入publicKeyPath"></el-input>
           </el-form-item>
           <el-form-item label="key路径" prop="privateKeyPath">
@@ -1894,6 +1928,192 @@
               <video :src="form25.videoUrl" controls style="max-width: 400px; max-height: 400px;"></video>
             </div>
           </el-form-item>
+          <!-- 游戏配置卡片 - 增强版 -->
+          <el-card class="config-card" shadow="hover">
+            <div class="section-title">
+              <div class="title-icon icon-game">
+                <i class="el-icon-s-flag"></i>
+              </div>
+              <div class="title-text">
+                <h3>游戏配置</h3>
+                <p class="subtitle">配置APP内小游戏列表及相关参数</p>
+              </div>
+            </div>
+
+            <el-divider></el-divider>
+
+            <!-- 游戏获得积分 -->
+            <el-form-item label="游戏获得积分" prop="addIntegral">
+              <el-input-number
+                v-model="form25.addIntegral"
+                :min="-999999"
+                :max="999999"
+                :step="1"
+                :precision="0"
+                controls-position="right"
+                style="width: 320px;">
+              </el-input-number>
+              <el-tooltip content="玩一局游戏获得的积分(负数为扣减),默认100" placement="right">
+                <i class="el-icon-question help-icon"></i>
+              </el-tooltip>
+            </el-form-item>
+
+            <!-- 视频获得积分 -->
+            <el-form-item label="视频获得积分" prop="defaultRewardGold">
+              <el-input-number
+                v-model="form25.defaultRewardGold"
+                :min="-999999"
+                :max="999999"
+                :step="1"
+                :precision="0"
+                controls-position="right"
+                style="width: 320px;">
+              </el-input-number>
+              <el-tooltip content="观看视频获得的金币(负数为扣减),默认100" placement="right">
+                <i class="el-icon-question help-icon"></i>
+              </el-tooltip>
+            </el-form-item>
+
+            <!-- 游戏列表区域 -->
+            <el-form-item label="游戏列表" class="game-list-item">
+              <div class="game-list-header">
+                <el-button type="primary" size="small" @click="addGameConfig" icon="el-icon-plus">
+                  新增游戏
+                </el-button>
+                <span class="game-list-tip">最多可添加10个游戏</span>
+              </div>
+
+              <!-- 游戏列表表格 -->
+              <el-table
+                :data="form25.gameList"
+                border
+                style="width: 100%; margin-top: 15px;"
+                v-loading="gameListLoading"
+              >
+                <el-table-column label="序号" width="60" align="center">
+                  <template slot-scope="scope">
+                    <span>{{ scope.$index + 1 }}</span>
+                  </template>
+                </el-table-column>
+
+                <el-table-column label="游戏图片" width="200" align="center">
+                  <template slot-scope="scope">
+                    <div v-if="scope.row.editing">
+                      <ImageUpload
+                        v-model="scope.row.image"
+                        :limit="1"
+                        :file-type='["png", "jpg", "jpeg"]'
+                        :width="30"
+                        :height="30"
+                      />
+                    </div>
+                    <div v-else class="game-image-preview">
+                      <el-image
+                        v-if="scope.row.image"
+                        :src="scope.row.image"
+                        :preview-src-list="[scope.row.image]"
+                        style="width: 50px; height: 50px; border-radius: 4px;"
+                      >
+                        <div slot="error" class="image-slot">
+                          <i class="el-icon-picture-outline"></i>
+                        </div>
+                      </el-image>
+                      <span v-else class="no-image">暂无图片</span>
+                    </div>
+                  </template>
+                </el-table-column>
+
+                <el-table-column label="游戏名称" min-width="150">
+                  <template slot-scope="scope">
+                    <el-input
+                      v-if="scope.row.editing"
+                      v-model="scope.row.name"
+                      placeholder="请输入游戏名称"
+                      size="small"
+                    ></el-input>
+                    <span v-else>{{ scope.row.name || '-' }}</span>
+                  </template>
+                </el-table-column>
+
+                <el-table-column label="游戏链接" min-width="200">
+                  <template slot-scope="scope">
+                    <el-input
+                      v-if="scope.row.editing"
+                      v-model="scope.row.url"
+                      placeholder="请输入游戏链接"
+                      size="small"
+                    ></el-input>
+                    <span v-else class="game-link">{{ scope.row.url || '-' }}</span>
+                  </template>
+                </el-table-column>
+
+                <el-table-column label="排序" width="100" align="center">
+                  <template slot-scope="scope">
+                    <el-input-number
+                      v-if="scope.row.editing"
+                      v-model="scope.row.sort"
+                      :min="0"
+                      :max="999"
+                      size="small"
+                      controls-position="right"
+                      style="width: 80px;"
+                    ></el-input-number>
+                    <span v-else>{{ scope.row.sort || 0 }}</span>
+                  </template>
+                </el-table-column>
+
+                <el-table-column label="状态" width="80" align="center">
+                  <template slot-scope="scope">
+                    <el-switch
+                      v-if="scope.row.editing"
+                      v-model="scope.row.status"
+                      :active-value="1"
+                      :inactive-value="0"
+                      active-color="#13ce66"
+                      inactive-color="#ff4949"
+                    ></el-switch>
+                    <el-tag v-else :type="scope.row.status === 1 ? 'success' : 'info'" size="small">
+                      {{ scope.row.status === 1 ? '启用' : '禁用' }}
+                    </el-tag>
+                  </template>
+                </el-table-column>
+
+                <el-table-column label="操作" width="150" fixed="right" align="center">
+                  <template slot-scope="scope">
+                    <el-button
+                      v-if="!scope.row.editing"
+                      type="text"
+                      size="small"
+                      icon="el-icon-edit"
+                      @click="editGame(scope.$index)"
+                    >编辑</el-button>
+                    <el-button
+                      v-if="scope.row.editing"
+                      type="text"
+                      size="small"
+                      icon="el-icon-check"
+                      @click="saveGame(scope.$index)"
+                    >保存</el-button>
+                    <el-button
+                      v-if="!scope.row.editing"
+                      type="text"
+                      size="small"
+                      icon="el-icon-delete"
+                      class="delete-btn"
+                      @click="deleteGame(scope.$index)"
+                    >删除</el-button>
+                    <el-button
+                      v-if="scope.row.editing"
+                      type="text"
+                      size="small"
+                      icon="el-icon-close"
+                      @click="cancelEdit(scope.$index)"
+                    >取消</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </el-form-item>
+          </el-card>
           <div class="footer">
             <el-button type="primary" @click="submitForm25">提 交</el-button>
           </div>
@@ -2265,21 +2485,21 @@
             </el-select>
           </el-form-item>
           <el-form-item label="店铺修改不重新审核字段" prop="storeColumns" v-if="form27.isAudit">
-          <el-select v-model="form27.storeColumns"
-                     filterable
-                     multiple
-                     clearable
-                     placeholder="请选择字段"
-                     size="small"
-                     style="width: 500px">
-            <el-option
-              v-for="column in storeScrmColumns"
-              :key="column.colName"
-              :label="column.colComment"
-              :value="column.colName"
-            >
-            </el-option>
-          </el-select>
+            <el-select v-model="form27.storeColumns"
+                       filterable
+                       multiple
+                       clearable
+                       placeholder="请选择字段"
+                       size="small"
+                       style="width: 500px">
+              <el-option
+                v-for="column in storeScrmColumns"
+                :key="column.colName"
+                :label="column.colComment"
+                :value="column.colName"
+              >
+              </el-option>
+            </el-select>
           </el-form-item>
           <el-form-item label="是否药品商城" prop="isMedicalMall">
             <el-switch
@@ -2356,46 +2576,46 @@
               inactive-color="#ff4949">
             </el-switch>
           </el-form-item>
-        <div v-if="!!form29.enableRandomRedpacket" style=" display: flex;
+          <div v-if="!!form29.enableRandomRedpacket" style=" display: flex;
                   flex-direction: column;
                   margin-bottom: 50px;">
 
-             <div v-for="(rule, index) in form29.rules" :key="index" class="form-row">
+            <div v-for="(rule, index) in form29.rules" :key="index" class="form-row">
 
-           <el-form-item
-            label="金额区间"
-            :prop="`rules.${index}.minAmount`"
-            :rules="[
+              <el-form-item
+                label="金额区间"
+                :prop="`rules.${index}.minAmount`"
+                :rules="[
               { required: true, message: '请输入最小金额', trigger: 'blur' },
               { validator: validateMinAmount, trigger: 'blur', index: index }
             ]"
-            class="form-item-amount"
-          >
-            <el-input
-              v-model.number="rule.minAmount"
-              type="number"
-              :min="0.01"
-              :precision="2"
-              :step="0.01"
-              placeholder="最小金额"
-              size="small"
-              class="amount-input"
-              @input="handleAmountInput(rule, 'minAmount')"
-            ></el-input>
-            <span class="separator">-</span>
-            <el-input
-              v-model.number="rule.maxAmount"
-              type="number"
-              :min="rule.minAmount || 0.01"
-              :precision="2"
-              :step="0.01"
-              placeholder="最大金额"
-              size="small"
-              class="amount-input"
-              @input="handleAmountInput(rule, 'maxAmount')"
-            ></el-input>
-            <span class="suffix">元</span>
-          </el-form-item>
+                class="form-item-amount"
+              >
+                <el-input
+                  v-model.number="rule.minAmount"
+                  type="number"
+                  :min="0.01"
+                  :precision="2"
+                  :step="0.01"
+                  placeholder="最小金额"
+                  size="small"
+                  class="amount-input"
+                  @input="handleAmountInput(rule, 'minAmount')"
+                ></el-input>
+                <span class="separator">-</span>
+                <el-input
+                  v-model.number="rule.maxAmount"
+                  type="number"
+                  :min="rule.minAmount || 0.01"
+                  :precision="2"
+                  :step="0.01"
+                  placeholder="最大金额"
+                  size="small"
+                  class="amount-input"
+                  @input="handleAmountInput(rule, 'maxAmount')"
+                ></el-input>
+                <span class="suffix">元</span>
+              </el-form-item>
 
               <el-form-item
                 label="随机权重"
@@ -2439,7 +2659,7 @@
                 </el-button>
               </div>
             </div>
-        </div>
+          </div>
 
           <div class="footer" style="margin-top:20px">
             <el-button type="primary" @click="submitForm29">提 交</el-button>
@@ -2644,6 +2864,7 @@ export default {
       form23: {},
       form24: {},
       form25: {},
+      gameListLoading: false,
       form26: {
         bloodGlucose: {
           fasting: { normal: '' },
@@ -2719,7 +2940,7 @@ export default {
       form29:{
         //是否开启拼手气红包
         enableRandomRedpacket:false,
-         rules: [
+        rules: [
           {
             minAmount: 0.01,
             maxAmount: 0.01,
@@ -2733,6 +2954,14 @@ export default {
       form33:{},
       form34:{},
       form35:{},
+      form40: {
+        enablePhoneConfig: false,
+        enablePhoneLimitConfig:false,
+        generateCount: 1,
+        startIndex: 1,
+        endIndex: 11,
+        numberCalls:1,
+      },
       storeProductScrmColumns:[],
       storeScrmColumns: [],
       photoArr: [],
@@ -2741,6 +2970,20 @@ export default {
       // 表单校验
       rules1: {},
       rules3: {},
+      rules40: {
+        generateCount: [
+          { required: true, message: '生成条数不能为空', trigger: 'blur' },
+          { type: 'number', min: 1, message: '生成条数不能小于1', trigger: 'blur' }
+        ],
+        startIndex: [
+          { required: true, message: '开始位置不能为空', trigger: 'blur' },
+          { type: 'number', min: 1, max: 11, message: '开始位置必须在1到11之间', trigger: 'blur' }
+        ],
+        endIndex: [
+          { required: true, message: '结束位置不能为空', trigger: 'blur' },
+          { type: 'number', min: 1, max: 11, message: '结束位置必须在1到11之间', trigger: 'blur' }
+        ],
+      },
       rules4: {
         doctorRegister: [
           { required: true, message: '请输入医生注册协议', trigger: 'blur' }
@@ -2826,7 +3069,7 @@ export default {
     appImages: function(val) {
       this.form25.images = val.join(',')
     },
-     // 深度监听 rules 数组的变化,以更新总权重
+    // 深度监听 rules 数组的变化,以更新总权重
     "form29.rules": {
       handler(val) {
         this.calculateTotalWeight();
@@ -2848,6 +3091,119 @@ export default {
         this.form18.smsDomain = selectedDict.dictLabel;
       }
     },
+    /**
+     * 新增游戏配置
+     */
+    addGameConfig() {
+      if (!this.form25.gameList) {
+        this.$set(this.form25, 'gameList', [])
+      }
+
+      // 限制最多10个游戏
+      if (this.form25.gameList.length >= 10) {
+        this.$message.warning('最多只能添加10个游戏')
+        return
+      }
+
+      // 添加新游戏配置
+      this.form25.gameList.push({
+        id: Date.now() + Math.random(), // 临时ID
+        image: '',
+        name: '',
+        url: '',
+        sort: this.form25.gameList.length,
+        status: 1, // 默认启用
+        editing: true // 新增时直接进入编辑状态
+      })
+
+      this.$forceUpdate()
+    },
+
+    /**
+     * 编辑游戏
+     */
+    editGame(index) {
+      // 如果已经有其他行在编辑,先保存或取消
+      const editingIndex = this.form25.gameList.findIndex(item => item.editing)
+      if (editingIndex !== -1 && editingIndex !== index) {
+        this.$confirm('当前有其他游戏正在编辑,是否继续?', '提示', {
+          confirmButtonText: '继续',
+          cancelButtonText: '取消',
+          type: 'warning'
+        }).then(() => {
+          // 取消其他行的编辑状态
+          this.form25.gameList[editingIndex].editing = false
+          // 设置当前行为编辑状态
+          this.$set(this.form25.gameList[index], 'editing', true)
+        }).catch(() => {})
+      } else {
+        this.$set(this.form25.gameList[index], 'editing', true)
+      }
+    },
+
+    /**
+     * 保存游戏
+     */
+    saveGame(index) {
+      const game = this.form25.gameList[index]
+
+      // 验证必填项
+      if (!game.name) {
+        this.$message.error('请输入游戏名称')
+        return
+      }
+      if (!game.url) {
+        this.$message.error('请输入游戏链接')
+        return
+      }
+      if (!game.image) {
+        this.$message.error('请上传游戏图片')
+        return
+      }
+
+      // 验证URL格式
+      const urlPattern = /^(http|https):\/\/[^\s]+$/
+      if (!urlPattern.test(game.url)) {
+        this.$message.error('请输入正确的游戏链接(以http://或https://开头)')
+        return
+      }
+
+      // 保存成功,退出编辑状态
+      this.$set(this.form25.gameList[index], 'editing', false)
+      this.$message.success('保存成功')
+    },
+
+    /**
+     * 取消编辑
+     */
+    cancelEdit(index) {
+      const game = this.form25.gameList[index]
+
+      // 如果是新增但未保存(没有id),直接删除
+      if (!game.id || game.id.toString().includes('.')) {
+        this.form25.gameList.splice(index, 1)
+      } else {
+        this.$set(this.form25.gameList[index], 'editing', false)
+      }
+    },
+
+    /**
+     * 删除游戏
+     */
+    deleteGame(index) {
+      this.$confirm('确定要删除这个游戏吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.form25.gameList.splice(index, 1)
+        // 重新排序
+        this.form25.gameList.forEach((item, idx) => {
+          item.sort = idx
+        })
+        this.$message.success('删除成功')
+      }).catch(() => {})
+    },
 
 
     // 处理开关配置
@@ -2999,12 +3355,18 @@ export default {
     getConfigByKey(key) {
       getConfigByKey(key).then(response => {
         if(!!response.data){
-        this.configId = response.data.configId
-        this.configKey = response.data.configKey
+          this.configId = response.data.configId
+          this.configKey = response.data.configKey
+          if (key == 'cId.config') {
+            this.form40 = JSON.parse(response.data.configValue);
+          }
         }else{
-            this.configId = null;
-            this.configKey = key;
+          this.configId = null;
+          this.configKey = key;
+          if (key == 'cId.config') {
+            this.form40 = { enablePhoneConfig: false, generateCount: 1 };
           }
+        }
         if (key == 'sys.oss.cloudStorage') {
           this.form1 = JSON.parse(response.data.configValue)
         }
@@ -3141,16 +3503,16 @@ export default {
         }
         if (key == 'randomRedpacket:config') {
           if(!!response.data){
-          this.configId = response.data.configId
-          this.configKey = response.data.configKey
-          this.form29 = {...this.form29, ...JSON.parse(response.data.configValue)}
+            this.configId = response.data.configId
+            this.configKey = response.data.configKey
+            this.form29 = {...this.form29, ...JSON.parse(response.data.configValue)}
           }
         }
         if (key == 'wx.config') {
           if(!!response.data){
-          this.configId = response.data.configId
-          this.configKey = response.data.configKey
-          this.form30 = {...this.form30, ...JSON.parse(response.data.configValue)}
+            this.configId = response.data.configId
+            this.configKey = response.data.configKey
+            this.form30 = {...this.form30, ...JSON.parse(response.data.configValue)}
           }
         }
 
@@ -3165,10 +3527,10 @@ export default {
           this.form35 =JSON.parse(response.data.configValue);
         }
         if(key == 'vc.config'){
-           if(!!response.data){
-          this.configId = response.data.configId
-          this.configKey = response.data.configKey
-          this.form31 = {...this.form31, ...JSON.parse(response.data.configValue)}
+          if(!!response.data){
+            this.configId = response.data.configId
+            this.configKey = response.data.configKey
+            this.form31 = {...this.form31, ...JSON.parse(response.data.configValue)}
           }
         }
         if(key == 'living.config'){
@@ -3237,21 +3599,21 @@ export default {
       if (this.form7.payPostage  == null){
         this.form7.payPostage = 0;
       }
-    if(this.form7.isIdVerification != 1){
-      this.form7.API_URL = null;
-      this.form7.HASHCODE = null;
-      this.form7.SECRET_KEY = null;
-    } else{
-      // 校验这三个参数是否为空
-      if (
-        !this.form7.API_URL ||
-        !this.form7.HASHCODE ||
-        !this.form7.SECRET_KEY
-      ) {
-        this.msgError("API_URL、HASHCODE 和 SECRET_KEY 为必填项");
-        return; // 阻止提交
+      if(this.form7.isIdVerification != 1){
+        this.form7.API_URL = null;
+        this.form7.HASHCODE = null;
+        this.form7.SECRET_KEY = null;
+      } else{
+        // 校验这三个参数是否为空
+        if (
+          !this.form7.API_URL ||
+          !this.form7.HASHCODE ||
+          !this.form7.SECRET_KEY
+        ) {
+          this.msgError("API_URL、HASHCODE 和 SECRET_KEY 为必填项");
+          return; // 阻止提交
+        }
       }
-    }
       var param = { configId: this.configId, configValue: JSON.stringify(this.form7) }
       updateConfigByKey(param).then(response => {
         if (response.code === 200) {
@@ -3505,6 +3867,75 @@ export default {
         }
       })
     },
+    submitForm40() {
+      if (this.form40.enablePhoneConfig) {
+        if (!this.form40.generateCount || this.form40.generateCount < 1) {
+          this.msgError('生成条数不能为空且不能小于1');
+          return false;
+        }
+
+        // if (!this.form40.generateCount || this.form40.generateCount > 10) {
+        //   this.msgError('单个手机号生成条数不能大于10条');
+        //   return false;
+        // }
+
+        if (!this.form40.startIndex || this.form40.startIndex < 1 || this.form40.startIndex > 11) {
+          this.msgError('开始位置不能为空,且必须在1到11之间');
+          return false;
+        }
+
+        if (!this.form40.endIndex || this.form40.endIndex < 1 || this.form40.endIndex > 11) {
+          this.msgError('结束位置不能为空,且必须在1到11之间');
+          return false;
+        }
+
+        if (this.form40.startIndex > this.form40.endIndex) {
+          this.msgError('开始位置不能大于结束位置');
+          return false;
+        }
+
+        if (this.form40.endIndex < this.form40.startIndex) {
+          this.msgError('结束位置不能小于开始位置');
+          return false;
+        }
+        if (this.form40.startIndex === this.form40.endIndex) {
+          this.msgError('开始位置不能等于结束位置');
+          return false;
+        }
+
+        let num = this.form40.endIndex - this.form40.startIndex
+        if(num < 4){
+          this.msgError('开始和结束位置不能小于4位');
+          return false;
+        }
+      }
+
+      if(this.form40.enablePhoneLimitConfig){
+        if(this.form40.numberCalls == null || this.form40.numberCalls === 0){
+          this.msgError('限制次数不能为空或者大于0!');
+          return false;
+        }
+        if(this.form40.numberCalls > 100000){
+          this.msgError('限制次数不能超过10万次!');
+          return false;
+        }
+      }
+
+      this.saveConfig40();
+    },
+
+    saveConfig40() {
+      const param = {
+        configId: this.configId,
+        configKey: this.configKey,
+        configValue: JSON.stringify(this.form40)
+      };
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('修改成功');
+        }
+      });
+    },
     formatColumns(){
       console.log(this.form27.pass_columns)
 
@@ -3590,7 +4021,7 @@ export default {
     },
     submitForm29(){
       console.log("开始校验")
-       this.$refs['form29'].validate(valid => {
+      this.$refs['form29'].validate(valid => {
         if (valid) {
           var param = { configId: this.configId, configKey: this.configKey, configValue: JSON.stringify(this.form29) }
           console.log(param)
@@ -3603,7 +4034,7 @@ export default {
         }
       })
     },
-     calculateTotalWeight() {
+    calculateTotalWeight() {
       this.totalWeight = this.form29.rules.reduce((sum, rule) => {
         return sum + (rule.weight || 0);
       }, 0);
@@ -3660,7 +4091,7 @@ export default {
         });
       });
     },
-     // 实时过滤金额输入,只允许两位小数
+    // 实时过滤金额输入,只允许两位小数
     handleAmountInput(rule, field) {
       let value = rule[field];
       if (value === null || value === undefined) return;
@@ -3720,7 +4151,7 @@ export default {
   /* align-items: center;
   padding: 10px;
   border-bottom: 1px solid #ebeef5; */
-   display: flex;
+  display: flex;
   /* 关键改动:添加以下两行 */
   align-items: center;    /* 垂直居中对齐 */
   justify-content: flex-start; /* 水平方向从左到右排列(默认值,可显式写出) */
@@ -3793,4 +4224,252 @@ export default {
   display: flex;
   justify-content: flex-end;
 }
+
+.app-config-form {
+  padding: 10px;
+}
+
+.config-card {
+  margin-bottom: 24px;
+  border-radius: 12px;
+  border: none;
+  box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05);
+}
+
+.config-card ::v-deep .el-card__body {
+  padding: 24px;
+}
+
+/* 章节标题样式 - 图标和文字在一起 */
+.section-title {
+  display: flex;
+  align-items: center;
+  margin-bottom: 8px;
+}
+
+.title-icon {
+  width: 40px;
+  height: 40px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  border-radius: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 12px;
+  flex-shrink: 0;
+}
+
+.title-icon i {
+  color: #fff;
+  font-size: 20px;
+}
+
+.icon-promotion {
+  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.title-text h3 {
+  margin: 0;
+  font-size: 18px;
+  color: #303133;
+  font-weight: 600;
+  line-height: 1.4;
+}
+
+.subtitle {
+  margin: 4px 0 0 0;
+  font-size: 13px;
+  color: #909399;
+  line-height: 1.4;
+}
+
+/* 分割线 */
+.el-divider {
+  margin: 20px 0;
+  background-color: #ebeef5;
+}
+
+/* 表单项 */
+.app-config-form ::v-deep .el-form-item {
+  margin-bottom: 24px;
+}
+
+.app-config-form ::v-deep .el-form-item:last-child {
+  margin-bottom: 0;
+}
+
+.help-icon {
+  margin-left: 8px;
+  color: #c0c4cc;
+  cursor: help;
+  font-size: 16px;
+  transition: color 0.3s;
+}
+
+.help-icon:hover {
+  color: #409EFF;
+}
+
+/* 上传区域 */
+.upload-wrapper {
+  display: flex;
+  flex-direction: column;
+}
+
+.upload-tip {
+  margin-top: 8px;
+  color: #909399;
+  font-size: 12px;
+}
+
+/* 视频上传 */
+.video-upload-wrapper {
+  width: 100%;
+  max-width: 400px;
+}
+
+.video-uploader ::v-deep .el-upload {
+  width: 100%;
+}
+
+.video-uploader ::v-deep .el-upload-dragger {
+  width: 100%;
+  height: 160px;
+  background: #fafafa;
+  border: 2px dashed #dcdfe6;
+  border-radius: 8px;
+}
+
+.video-uploader ::v-deep .el-upload-dragger:hover {
+  border-color: #409EFF;
+}
+
+.preview-video {
+  width: 100%;
+  max-width: 400px;
+  max-height: 250px;
+  border-radius: 8px;
+  background: #000;
+}
+
+.video-preview {
+  position: relative;
+  display: inline-block;
+}
+
+.video-actions {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  opacity: 0;
+  transition: opacity 0.3s;
+}
+
+.video-preview:hover .video-actions {
+  opacity: 1;
+}
+
+/* 底部按钮 */
+.form-footer {
+  margin-top: 32px;
+  padding-left: 160px;
+}
+
+.form-footer .el-button {
+  min-width: 100px;
+  padding: 12px 24px;
+  border-radius: 6px;
+}
+
+/* 响应式 */
+@media screen and (max-width: 768px) {
+  .section-title {
+    margin-bottom: 16px;
+  }
+
+  .form-footer {
+    padding-left: 0;
+    text-align: center;
+  }
+
+  .config-card ::v-deep .el-card__body {
+    padding: 16px;
+  }
+}
+
+
+/* 游戏列表样式 */
+.game-list-item {
+  margin-top: 20px;
+}
+
+.game-list-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 10px;
+}
+
+.game-list-tip {
+  font-size: 12px;
+  color: #909399;
+}
+
+.game-image-preview {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-height: 50px;
+}
+
+.no-image {
+  font-size: 12px;
+  color: #909399;
+}
+
+.game-link {
+  display: inline-block;
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  color: #409EFF;
+  cursor: pointer;
+}
+
+.delete-btn {
+  color: #f56c6c !important;
+}
+
+.delete-btn:hover {
+  color: #f78989 !important;
+}
+
+/* 表格样式优化 */
+::v-deep .el-table .cell {
+  padding-left: 8px;
+  padding-right: 8px;
+}
+
+::v-deep .el-table th {
+  background-color: #f5f7fa;
+  color: #606266;
+}
+
+::v-deep .el-table--border {
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+/* 图片上传组件样式调整 */
+::v-deep .image-upload {
+  width: 60px;
+  height: 60px;
+}
+
+::v-deep .image-upload .el-upload--picture-card {
+  width: 60px;
+  height: 60px;
+  line-height: 64px;
+}
 </style>