Kaynağa Gözat

Merge branch 'master' into 企微聊天

ct 19 saat önce
ebeveyn
işleme
c0fc0bd72b

+ 2 - 2
.env.prod-bjzm

@@ -20,9 +20,9 @@ VUE_APP_OBS_BUCKET = bjzm
 # 存储桶配置
 VUE_APP_COS_BUCKET = bjzmky-1323137866
 # 存储桶配置
-VUE_APP_COS_REGION = ap-guangzhou
+VUE_APP_COS_REGION = ap-chongqing
 # 线路一地址
-VUE_APP_VIDEO_LINE_1 = https://bjzmky-1323137866.cos.ap-guangzhou.myqcloud.com
+VUE_APP_VIDEO_LINE_1 = https://bjzmky-1323137866.cos.ap-chongqing.myqcloud.com
 # 线路二地址
 VUE_APP_VIDEO_LINE_2 = https://bjzmobs.ylrztop.com
 

+ 1 - 1
.env.prod-heyantang

@@ -16,7 +16,7 @@ ENV = 'production'
 VUE_APP_BASE_API = '/prod-api'
 
 #默认 1、会员 2、企微
-VUE_APP_COURSE_DEFAULT = 2
+VUE_APP_COURSE_DEFAULT = 1
 
 # 路由懒加载
 VUE_CLI_BABEL_TRANSPILE_MODULES = true

+ 3 - 3
src/views/course/courseUserStatistics/qw/index.vue

@@ -32,9 +32,9 @@
         >
           <el-option
             v-for="item in companyUserOptions"
-            :key="item.value"
-            :label="item.label"
-            :value="item.value"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="item.dictValue"
           >
           </el-option>
         </el-select>

+ 190 - 150
src/views/fastGpt/fastGptRole/fastGptRoleUpdate.vue

@@ -2,73 +2,119 @@
   <div class="app-container">
 
 
-      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
-              <el-form-item label="客服名称" prop="roleName">
-                <el-input v-model="form.roleName" placeholder="请输入角色名" />
-              </el-form-item>
-              <el-form-item label="角色类型" prop="roleType">
-                <el-select v-model="form.roleType" placeholder="请选择类型" clearable size="small" style="width: 150px" >
-                  <el-option
-                    v-for="item in typeOptions"
-                    :key="item.dictValue"
-                    :label="item.dictLabel"
-                    :value="String(item.dictValue)"
-                  />
-                </el-select>
-              </el-form-item>
-
-              <el-form-item label="客服头像" prop="avatar">
-                <ImageUpload v-model="form.avatar" type="image" :num="1" :width="150" :height="150" style="margin-top: 1%;" />
-              </el-form-item>
-              <el-form-item label="APPKey"  >
-                <el-input type="textarea" v-model="form.modeConfigJson.APPKey" placeholder="请输入FastGPT的APPKey(特定key)" />
-              </el-form-item>
-              <el-form-item  label="提示词"  >
-                <el-input type="textarea" :rows="3" v-model="form.reminderWords" placeholder="请输入FastGPT的提示词" />
-              </el-form-item>
-<!--              <el-form-item  label="标签人设"  >
-                <div v-if="tagsFormList.length > 0" style="display: flex; align-items: center; flex-wrap: wrap; width: 100%;">
-                  <div style="min-height: 40px; max-height: 200px; overflow-y: auto; width: 100%;">
-                    <div style="display: flex; flex-wrap: wrap; width: 100%;">
-                      <div v-for="(tagsForm, index) in tagsFormList" :key="tagsForm.id">
-                        <el-tag type="success"
-                                closable
-                                :disable-transitions="false"
-                                @click="handleEditRoleTag(tagsForm, index)"
-                                @close="handleCloseRoleTag(tagsForm, index)"
-                                style="margin: 3px;">
-                          {{ getTagNames(tagsForm.tagIds) }}
-                        </el-tag>
+    <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+      <el-form-item label="客服名称" prop="roleName">
+        <el-input v-model="form.roleName" placeholder="请输入角色名" />
+        <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+          <i class="el-icon-info"></i>
+          AI角色名字
+        </div>
+      </el-form-item>
+      <el-form-item label="角色类型" prop="roleType">
+        <el-select v-model="form.roleType" placeholder="请选择类型" clearable size="small" style="width: 150px" >
+          <el-option
+            v-for="item in typeOptions"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="String(item.dictValue)"
+          />
+        </el-select>
+        <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+          <i class="el-icon-info"></i>
+          选择角色类型:一般选择伴学助手,医生工作室,伴学助手无性别,无性别版本只会叫用户同学,其余的会叫用户先生女士
+        </div>
+      </el-form-item>
+
+      <el-form-item label="渠道类型" prop="channelType">
+        <el-select v-model="form.channelType" placeholder="请选择类型" clearable size="small" style="width: 150px" >
+          <el-option
+            v-for="item in channelOptions"
+            :key="item.dictValue"
+            :label="item.dictLabel"
+            :value="String(item.dictLabel)"
+          />
+        </el-select>
+        <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+          <i class="el-icon-info"></i>
+          选择进线渠道:只影响新客户进来的首次对话
+        </div>
+      </el-form-item>
+
+      <el-form-item label="物流提醒" prop="logistics">
+        <el-select v-model="form.logistics" placeholder="请选择类型" clearable size="small" style="width: 150px" >
+          <el-option
+            v-for="item in logisticsOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+        <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+          <i class="el-icon-info"></i>
+          AI是否主动发送物流信息,发货和到货的时候会提醒用户
+        </div>
+      </el-form-item>
+
+      <el-form-item label="客服头像" prop="avatar">
+        <ImageUpload v-model="form.avatar" type="image" :num="1" :width="150" :height="150" style="margin-top: 1%;" />
+      </el-form-item>
+      <el-form-item label="APPKey"  >
+        <el-input type="textarea" v-model="form.modeConfigJson.APPKey" placeholder="请输入APPKey(特定key)" />
+        <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+          <i class="el-icon-info"></i>
+          根据管理员发送的APPKey填写
+        </div>
+      </el-form-item>
+      <el-form-item  label="提示词"  >
+        <el-input type="textarea" :rows="3" v-model="form.reminderWords" placeholder="请输入提示词" />
+        <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+          <i class="el-icon-info"></i>
+          输入个人的介绍,并且在最后增加一个自我介绍(用于首次交流触发),模板可以找管理员获取
+        </div>
+      </el-form-item>
+      <!--              <el-form-item  label="标签人设"  >
+                      <div v-if="tagsFormList.length > 0" style="display: flex; align-items: center; flex-wrap: wrap; width: 100%;">
+                        <div style="min-height: 40px; max-height: 200px; overflow-y: auto; width: 100%;">
+                          <div style="display: flex; flex-wrap: wrap; width: 100%;">
+                            <div v-for="(tagsForm, index) in tagsFormList" :key="tagsForm.id">
+                              <el-tag type="success"
+                                      closable
+                                      :disable-transitions="false"
+                                      @click="handleEditRoleTag(tagsForm, index)"
+                                      @close="handleCloseRoleTag(tagsForm, index)"
+                                      style="margin: 3px;">
+                                {{ getTagNames(tagsForm.tagIds) }}
+                              </el-tag>
+                            </div>
+                          </div>
+                        </div>
                       </div>
-                    </div>
-                  </div>
-                </div>
-                  <el-button
-                    size="mini"
-                    type="primary" plain
-                    icon="el-icon-circle-plus-outline"
-                    @click="handleAddTags(form.roleId,form.bindCorpId)"
-                    v-hasPermi="['fastGptRole:fastGptRole:edit']"
-                  >添加标签人设</el-button>
-              </el-form-item> -->
-
-<!--      		<el-form-item label="修改栏目" prop="contactInfo">
-      		  <el-select v-model="contactInfo"  multiple filterable placeholder="请选择修改栏目" clearable size="small" style="width: 50%" >
-      		    <el-option
-      		      v-for="item in externalInfoOptions"
-      		      :key="item.dictValue"
-      		      :label="item.dictLabel"
-      		      :value="item.dictValue"
-      		    />
-      		  </el-select>
-      		</el-form-item> -->
-		 </el-form>
-      <div slot="footer" class="dialog-footer" style="padding-bottom: 40px;">
-        <el-button type="primary" @click="submitForm" style="float: right;margin-right: 20px;">确 定</el-button>
-        <el-button @click="cancel" style="float: right;margin-right: 20px;">取 消</el-button>
-      </div>
-
-<!--    修改-->
+                        <el-button
+                          size="mini"
+                          type="primary" plain
+                          icon="el-icon-circle-plus-outline"
+                          @click="handleAddTags(form.roleId,form.bindCorpId)"
+                          v-hasPermi="['fastGptRole:fastGptRole:edit']"
+                        >添加标签人设</el-button>
+                    </el-form-item> -->
+
+      <!--      		<el-form-item label="修改栏目" prop="contactInfo">
+                  <el-select v-model="contactInfo"  multiple filterable placeholder="请选择修改栏目" clearable size="small" style="width: 50%" >
+                    <el-option
+                      v-for="item in externalInfoOptions"
+                      :key="item.dictValue"
+                      :label="item.dictLabel"
+                      :value="item.dictValue"
+                    />
+                  </el-select>
+                </el-form-item> -->
+    </el-form>
+    <div slot="footer" class="dialog-footer" style="padding-bottom: 40px;">
+      <el-button type="primary" @click="submitForm" style="float: right;margin-right: 20px;">确 定</el-button>
+      <el-button @click="cancel" style="float: right;margin-right: 20px;">取 消</el-button>
+    </div>
+
+    <!--    修改-->
     <el-dialog :title="changeTagsOpen.title" :visible.sync="changeTagsOpen.open" width="800px" append-to-body>
 
       <el-form ref="tagsForm" :model="tagsForm"  :rules="tagsFormRules">
@@ -102,18 +148,15 @@
         <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
           <span class="name-background">{{ item.name }}</span>
         </div>
-        <!-- 添加外层滚动容器 -->
-        <div class="scroll-wrapper">
-          <div class="tag-container">
-            <a
-              v-for="tagItem in item.tag"
-              class="tag-box"
-              @click="tagSelection(tagItem)"
-              :class="{ 'tag-selected': tagItem.isSelected }"
-            >
-              {{ tagItem.name }}
-            </a>
-          </div>
+        <div class="tag-container">
+          <a
+            v-for="tagItem in item.tag"
+            class="tag-box"
+            @click="tagSelection(tagItem)"
+            :class="{ 'tag-selected': tagItem.isSelected }"
+          >
+            {{ tagItem.name }}
+          </a>
         </div>
       </div>
       <div slot="footer" class="dialog-footer">
@@ -122,7 +165,7 @@
       </div>
     </el-dialog>
 
-<!--    新增-->
+    <!--    新增-->
     <el-dialog :title="changeTagsOpenAdd.title" :visible.sync="changeTagsOpenAdd.open" width="800px" append-to-body>
 
       <el-form ref="tagsFormAdd" :model="tagsFormAdd"  :rules="tagsFormAddRules">
@@ -156,18 +199,15 @@
         <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
           <span class="name-background">{{ item.name }}</span>
         </div>
-        <!-- 添加外层滚动容器 -->
-        <div class="scroll-wrapper">
-          <div class="tag-container">
-            <a
-              v-for="tagItem in item.tag"
-              class="tag-box"
-              @click="tagSelection(tagItem)"
-              :class="{ 'tag-selected': tagItem.isSelected }"
-            >
-              {{ tagItem.name }}
-            </a>
-          </div>
+        <div class="tag-container">
+          <a
+            v-for="tagItem in item.tag"
+            class="tag-box"
+            @click="tagSelection(tagItem)"
+            :class="{ 'tag-selected': tagItem.isSelected }"
+          >
+            {{ tagItem.name }}
+          </a>
         </div>
       </div>
       <div slot="footer" class="dialog-footer">
@@ -197,6 +237,10 @@ export default {
   components: {ImageUpload},
   data() {
     return {
+      logisticsOptions: [
+        { label: '是', value: 1 },
+        { label: '否', value: 0 }
+      ],
       // 遮罩层
       loading: true,
       // 导出遮罩层
@@ -205,6 +249,8 @@ export default {
       typeOptions: [],
       //AI模型
       modeOptions: [],
+      //渠道类型
+      channelOptions: [],
       changeTagsOpen: {
         title : "",
         open :false,
@@ -234,7 +280,7 @@ export default {
 
       //所有的标签
       tagList:[],
-		contactInfo:[],
+      contactInfo:[],
       //已经选择的标签
       tagListFormIndex:[],
 
@@ -244,7 +290,7 @@ export default {
       uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
       // 选中数组
       ids: [],
-	  externalInfoOptions : [],
+      externalInfoOptions : [],
       // 非单个禁用
       single: true,
       // 非多个禁用
@@ -270,7 +316,9 @@ export default {
         kfId: null,
         kfUrl: null,
         avatar: null,
-        kfMediaId: null
+        kfMediaId: null,
+        channelType: null,
+        logistics: null
       },
       // 表单参数
       form: {
@@ -289,6 +337,9 @@ export default {
         roleType: [
           { required: true, message: "角色类型不能为空", trigger: "change" }
         ],
+        logistics: [
+          { required: true, message: "物流提醒不能为空", trigger: "change" }
+        ],
       },
       tagsFormRules:{
         tagIds:[
@@ -364,7 +415,7 @@ export default {
   },
   created() {
 
-	 this.handleUpdate();
+    this.handleUpdate();
     //客服类型
     // this.getDicts("chat_role_type").then((response) => {
     //   this.typeOptions = response.data;
@@ -374,12 +425,16 @@ export default {
       this.modeOptions = response.data;
     });
     this.getDicts("sys_fastgpt_role_external_info").then((response) => {
-    	  this.externalInfoOptions = response.data;
+      this.externalInfoOptions = response.data;
+    });
+    //渠道类型
+    this.getDicts("sys_fastgpt_channel_type").then((response) => {
+      this.channelOptions = response.data;
     });
 
-	getAllRoleType().then(response => {
-        this.typeOptions = response.data;
-      });
+    getAllRoleType().then(response => {
+      this.typeOptions = response.data;
+    });
   },
   methods: {
     /** 查询应用列表 */
@@ -395,7 +450,7 @@ export default {
     cancel() {
 
       this.reset();
-	  window.location.replace('/fastGpt/fastGptRole')
+      window.location.replace('/fastGpt/fastGptRole')
     },
     cancelTags(){
       this.changeTagsOpen.open=false;
@@ -482,7 +537,7 @@ export default {
 
     /** 修改按钮操作 */
     handleUpdate() {
-		var id=this.$route.params.command
+      var id=this.$route.params.command
       this.reset();
       getFastGptRole(id).then(response => {
         this.form = response.role;
@@ -490,11 +545,11 @@ export default {
         if(this.form.modeConfigJson!=null&&this.form.modeConfigJson!=""){
           this.form.modeConfigJson=JSON.parse(this.form.modeConfigJson)
         }else{
-			 this.form.modeConfigJson={APPKey:null}
-		}
-		if(this.form.contactInfo!=null){
-			  this.contactInfo = (this.form.contactInfo).split(",");
-			}
+          this.form.modeConfigJson={APPKey:null}
+        }
+        if(this.form.contactInfo!=null){
+          this.contactInfo = (this.form.contactInfo).split(",");
+        }
         //含标签吗
         getListByRoleId(id).then(res => {
           this.tagsFormList=res.rows;
@@ -668,9 +723,9 @@ export default {
     },
     /** 提交按钮 */
     submitForm() {
-	if(this.contactInfo!=null){
-		   this.form.contactInfo= (this.contactInfo).toString()
-		}
+      if(this.contactInfo!=null){
+        this.form.contactInfo= (this.contactInfo).toString()
+      }
       this.$refs["form"].validate(valid => {
         if (valid) {
           this.form.modeConfigJson=JSON.stringify(this.form.modeConfigJson)
@@ -701,7 +756,7 @@ export default {
               this.changeTagsOpen = false;
               this.getList();
             });
-            }
+          }
         }
       });
     },
@@ -711,12 +766,12 @@ export default {
         if (valid) {
           this.tagsFormAdd.roleId=roleId
           this.tagsFormAdd.tagIds=this.tagsFormAdd.tagIds.join(",")
-            addFastGptRoleTag(this.tagsFormAdd).then(response => {
-              this.msgSuccess("新增成功");
-              this.changeTagsOpenAdd = false;
-              this.open=false;
-              this.handleUpdate();
-            });
+          addFastGptRoleTag(this.tagsFormAdd).then(response => {
+            this.msgSuccess("新增成功");
+            this.changeTagsOpenAdd = false;
+            this.open=false;
+            this.handleUpdate();
+          });
 
         }
       });
@@ -725,30 +780,30 @@ export default {
     handleDelete(row) {
       const roleIds = row.roleId || this.ids;
       this.$confirm('是否确认删除应用编号为"' + roleIds + '"的数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
-          return delFastGptRole(roleIds);
-        }).then(() => {
-          this.getList();
-          this.msgSuccess("删除成功");
-        }).catch(() => {});
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return delFastGptRole(roleIds);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {});
     },
     /** 导出按钮操作 */
     handleExport() {
       const queryParams = this.queryParams;
       this.$confirm('是否确认导出所有应用数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.exportLoading = true;
-          return exportFastGptRole(queryParams);
-        }).then(response => {
-          this.download(response.msg);
-          this.exportLoading = false;
-        }).catch(() => {});
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportFastGptRole(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {});
     }
   }
 };
@@ -802,19 +857,4 @@ export default {
   overflow-y: auto; /* 内容超出时显示滚动条 */
   line-height: 1.5em; /* 行高设置,确保每行高度一致 */
 }
-/* 新增的滚动容器样式(不影响原有样式) */
-.scroll-wrapper {
-  max-height: 130px; /* 大约三行的高度 */
-  overflow-y: auto;  /* 垂直滚动 */
-  padding-right: 5px; /* 为滚动条留出空间 */
-}
-
-/* 美化滚动条(可选) */
-.scroll-wrapper::-webkit-scrollbar {
-  width: 6px;
-}
-.scroll-wrapper::-webkit-scrollbar-thumb {
-  background: rgba(0, 0, 0, 0.2);
-  border-radius: 3px;
-}
 </style>

+ 17 - 0
src/views/live/liveConfig/index.vue

@@ -98,7 +98,9 @@ export default {
       activeTab: 'watchReward',
       liveId: null,
       liveInfo: {},
+      socket:null,
       currentComponent: WatchReward,
+      liveWsUrl: process.env.VUE_APP_LIVE_WS_URL + '/app/webSocket',
       menuList:[
         { name: '观看奖励', label: '观看奖励', index: 'watchReward'},
         { name: '直播预告', label: '直播预告', index: 'preview'},
@@ -118,8 +120,23 @@ export default {
   created() {
     this.liveId = this.$route.params.liveId
     this.getLiving()
+    this.connectWebSocket()
+  },
+  computed: {
+    userId() {
+      return this.$store.state.user.user.userId
+    },
   },
   methods: {
+    connectWebSocket() {
+      this.$store.dispatch('initLiveWs', {
+        liveWsUrl: this.liveWsUrl,
+        liveId: this.liveId,
+        userId: this.userId
+      })
+      this.socket = this.$store.state.liveWs[this.liveId]
+      this.socket.onmessage = (event) => this.handleWsMessage(event)
+    },
     handleSelect(index){
       this.currentComponent = index; // 切换当前显示的组件
     },

+ 44 - 0
src/views/live/liveConfig/task.vue

@@ -210,6 +210,26 @@
             </div>
           </el-select>
         </el-form-item>
+        <el-form-item label="商品选择" prop="goodsId" v-if="form.taskType == 6">
+          <el-select v-model="form.goodsId" placeholder="请选择商品" ref="selectGoods" >
+            <el-option v-for="i in productOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
+            <!-- 加载载中状态 -->
+            <div v-if="isLoading" class="loading-indicator">
+              <i class="el-icon-loading"></i>
+              <span>加载中...</span>
+            </div>
+
+            <!-- 没有更多数据 -->
+            <div v-if="!hasMore && !isLoading" class="no-more">
+              没有更多数据了
+            </div>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="上下架状态" prop="goodsStatus" v-if="form.taskType == 6">
+          <el-select v-model="form.goodsStatus" placeholder="请选择上下架状态">
+            <el-option v-for="i in goodsStatusOptions" :key="i.value" :label="i.label" :value="i.value"></el-option>
+          </el-select>
+        </el-form-item>
         <el-form-item label="触发时间" prop="content">
           <el-time-picker
             default-value="2025-01-01 00:00:00"
@@ -281,6 +301,10 @@ export default {
         {
           value: 5,
           label: "定时优惠券"
+        },
+        {
+          value: 6,
+          label: "定时商品上下架"
         }
       ],
       statusOptions:[{
@@ -292,6 +316,15 @@ export default {
           label: "启用"
         }
       ],
+      goodsStatusOptions:[{
+        value: 0,
+        label: "下架"
+      },
+        {
+          value: 1,
+          label: "上架"
+        }
+      ],
       productOptions:[],
       haveData:{
         goods:false,
@@ -435,6 +468,8 @@ export default {
           return "定时抽奖";
         case 5:
           return "定时优惠券";
+        case 6:
+          return "定时商品上下架";
         default:
           return "--";
       }
@@ -507,6 +542,8 @@ export default {
           await this.addLotteryList();
         }else if(this.form.taskType == 5){
           await this.addCouponList();
+        }else if(this.form.taskType == 6){
+          await this.addGoodsList();
         }
       }catch ( err){
         console.error('加载数据失败:', err);
@@ -645,6 +682,8 @@ export default {
         triggerType: null,
         triggerValue: null,
         content: null,
+        goodsId: null,
+        goodsStatus: null,
         status: 1,
         createdTime: null,
         updatedTime: null
@@ -690,6 +729,8 @@ export default {
         await this.addLotteryList();
       }else if(row.taskType == 5) {
         await this.addCouponList();
+      }else if(row.taskType == 6) {
+        await this.addGoodsList();
       }
       const id = row.id || this.ids
       getTask(id).then(response => {
@@ -703,6 +744,9 @@ export default {
           this.form.content = content.desc;
         }else if(this.form.taskType == 5){
           this.form.content = content.couponId;
+        }else if(this.form.taskType == 6){
+          this.form.goodsId = content.goodsId;
+          this.form.goodsStatus = content.goodsStatus;
         }
         this.open = true;
         this.title = "修改直播间自动化任务配置";

+ 127 - 59
src/views/live/liveConsole/LiveConsole.vue

@@ -112,7 +112,7 @@
           </label>
         </div>
         <el-scrollbar class="custom-scrollbar" style="height: 500px; width: 100%;" ref="manageRightRef">
-          <el-row v-for="m in msgList" >
+          <el-row v-for="m in msgList" :key="m.msgId">
             <el-row v-if="m.userId !== userId" style="margin-top: 5px" type="flex" align="top" >
               <el-col :span="3" style="margin-left: 10px"><el-avatar :src="m.avatar"/></el-col>
               <el-col :span="15">
@@ -145,23 +145,23 @@
           <!-- 底部留白 -->
           <div style="height: 20px;"></div>
         </el-scrollbar>
-<!--        <div class="message-list">-->
-<!--          <div class="message-item" v-for="msg in msgList" :key="msg.id">-->
-<!--            <div class="message-avatar">-->
-<!--              <img :src="msg.avatar" alt="用户头像">-->
-<!--            </div>-->
-<!--            <div class="message-content">-->
-<!--              <div class="message-user">{{ msg.user }}</div>-->
-<!--              <div class="message-text">{{ msg.text }}</div>-->
-<!--            </div>-->
-<!--            <div class="message-actions">-->
-<!--&lt;!&ndash;              <button @click="toggleVisible(msg)">&ndash;&gt;-->
-<!--&lt;!&ndash;                {{ msg.isVisible ? '仅用户自见' : '全局可见' }}&ndash;&gt;-->
-<!--&lt;!&ndash;              </button>&ndash;&gt;-->
-<!--              <button @click="deleteMessage(msg)">删除</button>-->
-<!--            </div>-->
-<!--          </div>-->
-<!--        </div>-->
+        <!--        <div class="message-list">-->
+        <!--          <div class="message-item" v-for="msg in msgList" :key="msg.id">-->
+        <!--            <div class="message-avatar">-->
+        <!--              <img :src="msg.avatar" alt="用户头像">-->
+        <!--            </div>-->
+        <!--            <div class="message-content">-->
+        <!--              <div class="message-user">{{ msg.user }}</div>-->
+        <!--              <div class="message-text">{{ msg.text }}</div>-->
+        <!--            </div>-->
+        <!--            <div class="message-actions">-->
+        <!--&lt;!&ndash;              <button @click="toggleVisible(msg)">&ndash;&gt;-->
+        <!--&lt;!&ndash;                {{ msg.isVisible ? '仅用户自见' : '全局可见' }}&ndash;&gt;-->
+        <!--&lt;!&ndash;              </button>&ndash;&gt;-->
+        <!--              <button @click="deleteMessage(msg)">删除</button>-->
+        <!--            </div>-->
+        <!--          </div>-->
+        <!--        </div>-->
       </div>
       <div class="system-messages">
         <h3>系统消息</h3>
@@ -169,6 +169,7 @@
         <div class="message-actions">
           <button @click="sendMessage">发送消息</button>
           <button @click="sendPopMessage">弹窗消息</button>
+          <button @click="showTopMsgDialog">顶部消息</button>
         </div>
       </div>
     </div>
@@ -191,7 +192,7 @@
               <button class="delete" @click="removeTimelineItem(item)">删除</button>
             </div>
           </div>
-<!--          <button class="add" @click="addTimelineItem">添加时间节点</button>-->
+          <!--          <button class="add" @click="addTimelineItem">添加时间节点</button>-->
         </div>
       </div>
 
@@ -214,6 +215,33 @@
       </div>
     </div>
 
+    <!-- 顶部消息对话框 -->
+    <el-dialog title="发送顶部消息" :visible.sync="topMsgDialogVisible" width="500px">
+      <el-form :model="topMsgForm" label-width="100px">
+        <el-form-item label="消息内容">
+          <el-input
+            type="textarea"
+            v-model="topMsgForm.msg"
+            placeholder="请输入消息内容"
+            :rows="3"
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="显示时长">
+          <el-input-number
+            v-model="topMsgForm.duration"
+            :min="1"
+            :max="60"
+            placeholder="请输入显示时长(分钟)"
+          ></el-input-number>
+          <span style="margin-left: 10px;">分钟</span>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="topMsgDialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="sendTopMessage">确 定</el-button>
+      </div>
+    </el-dialog>
+
   </div>
 </template>
 
@@ -357,6 +385,11 @@ export default {
       newMsg:'',
       autoMsgTimer: null,
       checkInterval: 2000, // 检查间隔(1分钟,可根据需求调整)
+      topMsgDialogVisible: false,
+      topMsgForm: {
+        msg: '',
+        duration: 5
+      }
     };
   },
   computed: {
@@ -436,9 +469,9 @@ export default {
       }
       this.disabled = this.autoWatermark
       if(this.autoWatermark){
-          this.autoMsgTimer = setInterval(() => {
-            this.sendNormalMessage()
-          }, 1000 * this.interval)
+        this.autoMsgTimer = setInterval(() => {
+          this.sendNormalMessage()
+        }, 1000 * this.interval)
       } else {
         clearInterval(this.autoMsgTimer)
         this.watermarkIndex = 0;
@@ -494,6 +527,41 @@ export default {
 
       this.newMsg = '';
     },
+    // 显示顶部消息对话框
+    showTopMsgDialog() {
+      this.topMsgForm.msg = '';
+      this.topMsgForm.duration = 5;
+      this.topMsgDialogVisible = true;
+    },
+    // 发送顶部消息
+    sendTopMessage() {
+      // 发送前简单校验
+      if (this.topMsgForm.msg.trim() === '') {
+        this.$message.warning('请输入消息内容');
+        return;
+      }
+
+      if (!this.topMsgForm.duration || this.topMsgForm.duration < 1) {
+        this.$message.warning('请输入有效的显示时长');
+        return;
+      }
+
+      let msg = {
+        msg: this.topMsgForm.msg,
+        duration: this.topMsgForm.duration,
+        liveId: this.liveId,
+        userId: this.userId,
+        userType: 1,
+        cmd: 'sendTopMsg',
+        avatar: this.$store.state.user.user.avatar,
+        nickName: this.$store.state.user.user.nickName,
+      }
+
+      this.socket.send(JSON.stringify(msg))
+
+      this.topMsgDialogVisible = false;
+      this.$message.success('顶部消息发送成功');
+    },
     sendMessage() {
       // 发送前简单校验
       if (this.newMsg.trim() === '') {
@@ -1045,47 +1113,47 @@ export default {
       this.pageParams=  {
         al: {
           currentPage: 1,
-            pageSize: 20,
-            prevPage: 0,
-            totalLoaded: 0,
-            total: 0,
-            hasMore: true,
-            hasPrev: false
+          pageSize: 20,
+          prevPage: 0,
+          totalLoaded: 0,
+          total: 0,
+          hasMore: true,
+          hasPrev: false
         },
         online: {
           currentPage: 1,       // 当前页(下一页加载用)
-            pageSize: 20,       // 当前页(下一页加载用)
-            prevPage: 0,          // 上一页页码(上一页加载用)
-            totalLoaded: 0,       // 已加载总条数
-            total: 0,             // 总数据量
-            hasMore: true,        // 是否有下一页
-            hasPrev: false        // 是否有上一页
+          pageSize: 20,       // 当前页(下一页加载用)
+          prevPage: 0,          // 上一页页码(上一页加载用)
+          totalLoaded: 0,       // 已加载总条数
+          total: 0,             // 总数据量
+          hasMore: true,        // 是否有下一页
+          hasPrev: false        // 是否有上一页
         },
         offline: {
           currentPage: 1,
-            pageSize: 20,
-            prevPage: 0,
-            totalLoaded: 0,
-            total: 0,
-            hasMore: true,
-            hasPrev: false
+          pageSize: 20,
+          prevPage: 0,
+          totalLoaded: 0,
+          total: 0,
+          hasMore: true,
+          hasPrev: false
         },
         silenced: {
           currentPage: 1,
-            pageSize: 20,
-            prevPage: 0,
-            totalLoaded: 0,
-            total: 0,
-            hasMore: true,
-            hasPrev: false
+          pageSize: 20,
+          prevPage: 0,
+          totalLoaded: 0,
+          total: 0,
+          hasMore: true,
+          hasPrev: false
         }
       },
-      this.scrLoading = {
-        al: { next: false, prev: false },
-        online: { next: false, prev: false },
-        offline: { next: false, prev: false },
-        silenced: { next: false, prev: false }
-      }
+        this.scrLoading = {
+          al: { next: false, prev: false },
+          online: { next: false, prev: false },
+          offline: { next: false, prev: false },
+          silenced: { next: false, prev: false }
+        }
     },
     resetMsgParams() {
       // 消息参数保留
@@ -1096,13 +1164,13 @@ export default {
         liveId: this.liveId
       };
       this.taskParams = {
-          currentPage: 1,
-          pageSize: 20,
-          prevPage: 0,
-          totalLoaded: 0,
-          total: 0,
-          hasMore: true,
-          hasPrev: false
+        currentPage: 1,
+        pageSize: 20,
+        prevPage: 0,
+        totalLoaded: 0,
+        total: 0,
+        hasMore: true,
+        hasPrev: false
       }
     },
     getScrollElement(tabName) {

+ 136 - 44
src/views/live/liveConsole/LiveDashboard.vue

@@ -7,6 +7,10 @@
         <div class="card" v-for="item in dataCards" :key="item.title">
           <h3>{{ item.title }}</h3>
           <p class="value">{{ item.value }}</p>
+          <div class="sub-values" v-if="item.subValues">
+            <span class="sub-item">直播: {{ item.subValues.live }}</span>
+            <span class="sub-item">回放: {{ item.subValues.replay }}</span>
+          </div>
         </div>
       </div>
 
@@ -15,12 +19,12 @@
         <!-- 左侧用户分析区(占50%宽度) -->
         <div class="user-analysis">
           <div class="new-old">
-            <h3>新老用户占比</h3>
-            <EChartsComponent chartId="newOldChart" :option="newOldOption" />
+            <h3>直播新老用户占比</h3>
+            <EChartsComponent chartId="liveNewOldChart" :option="liveNewOldOption" />
           </div>
           <div class="region">
-            <h3>地域访客</h3>
-            <EChartsComponent chartId="regionChart" :option="regionOption" />
+            <h3>回放新老用户占比</h3>
+            <EChartsComponent chartId="replayNewOldChart" :option="replayNewOldOption" />
           </div>
         </div>
 
@@ -45,10 +49,10 @@
       </div>
 
       <!-- 底部趋势图(占20%高度) -->
-<!--      <div class="trend">-->
-<!--        <h3>实时在线人数趋势</h3>-->
-<!--        <EChartsComponent chartId="trendChart" :option="trendOption" />-->
-<!--      </div>-->
+      <!--      <div class="trend">-->
+      <!--        <h3>实时在线人数趋势</h3>-->
+      <!--        <EChartsComponent chartId="trendChart" :option="trendOption" />-->
+      <!--      </div>-->
     </div>
   </div>
 </template>
@@ -58,7 +62,7 @@ import EChartsComponent from './EchartsComponent.vue';
 import {dashboardData} from '@/api/live/liveData'
 
 export default {
-  components: { EChartsComponent },
+  components: {EChartsComponent},
   props: {
     liveId: {
       type: String,
@@ -69,44 +73,67 @@ export default {
     return {
       // 数据保持不变(与原代码一致)
       dataCards: [
-        { title: '在线人数', value: '2.5k' },
-        { title: '观看人次', value: '15.2k' },
-        { title: '点赞数', value: '12.8k' },
-        { title: '评论数', value: '3.2k' }
+        {title: '在线人数', value: 0},
+        {
+          title: '观看人次',
+          value: 0,
+          subValues: {live: 0, replay: 0}
+        },
+        {
+          title: '点赞数',
+          value: 0,
+          subValues: {live: 0, replay: 0}
+        },
+        {
+          title: '评论数',
+          value: 0,
+          subValues: {live: 0, replay: 0}
+        }
       ],
-      newOldOption: {
-        tooltip: { trigger: 'item' },
+      liveNewOldOption: {
+        tooltip: {trigger: 'item'},
+        series: [{
+          type: 'pie', radius: '70%',
+          data: [{value: 0, name: '新用户'}, {value: 0, name: '老用户'}],
+          itemStyle: {colors: ['#36cfc9', '#722ed1']}
+        }]
+      },
+      replayNewOldOption: {
+        tooltip: {trigger: 'item'},
         series: [{
           type: 'pie', radius: '70%',
-          data: [{ value: 65, name: '新用户' }, { value: 35, name: '老用户' }],
-          itemStyle: { colors: ['#36cfc9', '#722ed1'] }
+          data: [{value: 0, name: '新用户'}, {value: 0, name: '老用户'}],
+          itemStyle: {colors: ['#36cfc9', '#722ed1']}
         }]
       },
       regionOption: {
-        tooltip: { trigger: 'item' },
+        tooltip: {trigger: 'item'},
         series: [{
           type: 'map', map: 'china', roam: true,
           itemStyle: {
-            normal: { areaColor: '#36cfc9', borderColor: '#fff' },
-            emphasis: { areaColor: '#722ed1' }
+            normal: {areaColor: '#36cfc9', borderColor: '#fff'},
+            emphasis: {areaColor: '#722ed1'}
           },
-          data: [{ name: '北京', value: 120 }, { name: '上海', value: 150 }, { name: '广州', value: 90 }, { name: '深圳', value: 80 }, { name: '杭州', value: 70 }]
+          data: [{name: '北京', value: 120}, {name: '上海', value: 150}, {name: '广州', value: 90}, {
+            name: '深圳',
+            value: 80
+          }, {name: '杭州', value: 70}]
         }]
       },
       sourceOption: {
-        tooltip: { trigger: 'item' },
+        tooltip: {trigger: 'item'},
         series: [{
           type: 'pie', radius: '70%',
-          data: [{ value: 45, name: '分享链接' }, { value: 30, name: '直接访问' }, { value: 25, name: '其他' }],
-          itemStyle: { colors: ['#3b82f6', '#10b981', '#f59e0b'] }
+          data: [{value: 45, name: '分享链接'}, {value: 30, name: '直接访问'}, {value: 25, name: '其他'}],
+          itemStyle: {colors: ['#3b82f6', '#10b981', '#f59e0b']}
         }]
       },
       rankList: [
-        { userName: '达人1', inviteNum: 258 },
-        { userName: '达人2', inviteNum: 186 },
-        { userName: '达人3', inviteNum: 152 },
-        { userName: '达人4', inviteNum: 121 },
-        { userName: '达人5', inviteNum: 98 }
+        {userName: '达人1', inviteNum: 258},
+        {userName: '达人2', inviteNum: 186},
+        {userName: '达人3', inviteNum: 152},
+        {userName: '达人4', inviteNum: 121},
+        {userName: '达人5', inviteNum: 98}
       ],
       trendOption: {
         // 网格:控制图表与容器的边距,避免内容被裁剪
@@ -180,7 +207,7 @@ export default {
           }
         }]
       },
-      socket:null,
+      socket: null,
       timer: null
     };
   },
@@ -196,35 +223,86 @@ export default {
   methods: {
     async getInitData() {
       dashboardData(this.liveId).then(res => {
-        if(res.code == 200){
+        if (res.code == 200) {
           this.dealData(res.data)
         }
       });
     },
-    dealData(data){
+    dealData(data) {
+      // 计算总计
+      const totalViewNum = (data.liveViewNum || 0) + (data.replayViewNum || 0);
+      const totalLikeNum = (data.liveLikeNum || 0) + (data.replayLikeNum || 0);
+      const totalCommentNum = (data.liveCommentNum || 0) + (data.replayCommentNum || 0);
+
       this.dataCards = [
-        { title: '在线人数', value: data.onlineNum },
-        { title: '观看人次', value: data.viewNum },
-        { title: '点赞数', value: data.likeNum },
-        { title: '评论数', value: data.commentNum }
+        {title: '在线人数', value: data.onlineNum || 0},
+        {
+          title: '观看人次',
+          value: totalViewNum,
+          subValues: {
+            live: data.liveViewNum || 0,
+            replay: data.replayViewNum || 0
+          }
+        },
+        {
+          title: '点赞数',
+          value: totalLikeNum,
+          subValues: {
+            live: data.liveLikeNum || 0,
+            replay: data.replayLikeNum || 0
+          }
+        },
+        {
+          title: '评论数',
+          value: totalCommentNum,
+          subValues: {
+            live: data.liveCommentNum || 0,
+            replay: data.replayCommentNum || 0
+          }
+        }
       ]
-      this.newOldOption= {
-        tooltip: { trigger: 'item' },
+
+      // 直播新老用户占比
+      this.liveNewOldOption = {
+        tooltip: {trigger: 'item'},
         series: [{
           type: 'pie', radius: '70%',
-          data: [{ value: data.newUserNum, name: '新用户' }, { value: data.oldUserNum, name: '老用户' }],
-          itemStyle: { colors: ['#36cfc9', '#722ed1'] }
+          data: [
+            {value: data.liveNewUserNum || 0, name: '新用户'},
+            {value: data.liveOldUserNum || 0, name: '老用户'}
+          ],
+          itemStyle: {colors: ['#36cfc9', '#722ed1']}
         }]
       }
+
+      // 回放新老用户占比
+      this.replayNewOldOption = {
+        tooltip: {trigger: 'item'},
+        series: [{
+          type: 'pie', radius: '70%',
+          data: [
+            {value: data.replayNewUserNum || 0, name: '新用户'},
+            {value: data.replayOldUserNum || 0, name: '老用户'}
+          ],
+          itemStyle: {colors: ['#36cfc9', '#722ed1']}
+        }]
+      }
+
+      // 观众来源
       this.sourceOption = {
-        tooltip: { trigger: 'item' },
+        tooltip: {trigger: 'item'},
         series: [{
           type: 'pie', radius: '70%',
-          data: [{ value: data.shareUrlNum, name: '分享链接' }, { value: data.directAccessNum, name: '直接访问' }, { value: 0, name: '其他' }],
-          itemStyle: { colors: ['#3b82f6', '#10b981', '#f59e0b'] }
+          data: [
+            {value: data.shareUrlNum || 0, name: '分享链接'},
+            {value: data.directAccessNum || 0, name: '直接访问'}
+          ],
+          itemStyle: {colors: ['#3b82f6', '#10b981']}
         }]
       }
-      this.rankList = data.inviteUserList;
+
+      // 邀请用户列表
+      this.rankList = data.inviteUserList || [];
     },
   }
 };
@@ -282,6 +360,20 @@ export default {
   margin: 0;
 }
 
+.card .sub-values {
+  display: flex;
+  justify-content: space-around;
+  margin-top: 10px;
+  font-size: 0.8vw;
+  color: #94a3b8;
+}
+
+.card .sub-item {
+  padding: 3px 8px;
+  background: rgba(54, 207, 201, 0.1);
+  border-radius: 4px;
+}
+
 /* 中间内容区:高度40%,左右分栏 */
 .middle-content {
   width: 100%;

+ 1 - 0
src/views/live/liveVideo/index.vue

@@ -249,6 +249,7 @@ export default {
             this.msgError("请上传视频");
             return;
           }
+          this.form.videoUrl = this.form.videoUrl.replace('.mp4', '.m3u8');
           addLiveVideo(this.form).then(response => {
             this.msgSuccess("新增成功");
             this.open = false;