Просмотр исходного кода

feat:app独立发课-直播添加分组、给app好友打IM备注

caoliqin 6 дней назад
Родитель
Сommit
b14badb83e

+ 14 - 3
src/api/app/live/index.js

@@ -1,8 +1,19 @@
-import request from '@/utils/request';
+import request from '@/utils/request'
 
-export function getLiveOptions() {
+/** 直播分组下拉 */
+export function getLiveGroupTypeList(query) {
+  return request({
+    url: '/live/liveGroupType/list',
+    method: 'get',
+    params: query || { pageNum: 1, pageSize: 999 }
+  })
+}
+
+/** 直播间下拉(可按分组筛选) */
+export function getLiveOptions(params) {
   return request({
     url: '/live/live/getOptions',
     method: 'get',
+    params
   })
-}
+}

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

@@ -170,3 +170,12 @@ export function exportAppCourseWatchLog(query) {
     params: query
   })
 }
+
+// 批量修改好友备注
+export function batchUpdateFriendRemark(data) {
+  return request({
+    url: '/course/appCourseWatchLog/batchUpdateFriendRemark',
+    method: 'post',
+    data: data
+  })
+}

+ 1 - 1
src/views/app/invitationCode/index.vue

@@ -157,7 +157,7 @@
           border
         >
           <el-table-column type="selection" width="55" align="center" />
-          <el-table-column label="邀请码" align="center" prop="invitationCode" min-width="150"/>
+          <el-table-column label="公司码(公司ID)" align="center" prop="invitationCode" min-width="150"/>
           <el-table-column label="公司" align="left" prop="companyName" min-width="150"/>
           <el-table-column label="二维码" align="center" prop="qrCode" width="120">
             <template slot-scope="scope">

+ 104 - 9
src/views/app/sop/template/updateSopTemp.vue

@@ -591,18 +591,39 @@
                                         <el-form-item label="内容" v-else-if="content.contentType == 21">
                                           <div>
                                             <el-card class="box-card">
+                                              <el-form-item label="直播分组"
+                                                required
+                                              >
+                                                <el-select
+                                                  v-model="content.liveGroupTypeId"
+                                                  placeholder="请选择直播分组"
+                                                  size="mini"
+                                                  filterable
+                                                  clearable
+                                                  :disabled="formType == 3"
+                                                  @change="liveGroupTypeChange(content)"
+                                                >
+                                                  <el-option
+                                                    v-for="dict in liveGroupTypeList"
+                                                    :key="dict.id"
+                                                    :label="dict.liveGroupType"
+                                                    :value="dict.id"
+                                                  />
+                                                </el-select>
+                                              </el-form-item>
                                               <el-form-item label="直播间"
                                                 required
                                               >
                                                 <el-select
                                                   v-model="content.liveId"
-                                                  placeholder="请选择直播间"
+                                                  placeholder="请先选择直播分组"
                                                   size="mini"
                                                   filterable
+                                                  :disabled="formType == 3 || !content.liveGroupTypeId"
                                                   @change="liveChange(content)"
                                                 >
                                                   <el-option
-                                                    v-for="dict in liveList"
+                                                    v-for="dict in (content.meta && content.meta.liveList ? content.meta.liveList : [])"
                                                     :key="dict.liveId"
                                                     :label="dict.liveName"
                                                     :value="parseInt(dict.liveId)"
@@ -1253,7 +1274,7 @@ import {
 } from "@/api/app/sop/template";
 import {courseList, videoList} from "@/api/app/course";
 import { getPackageOptions,} from "@/api/app/pkg";
-import { getLiveOptions,} from "@/api/app/live";
+import { getLiveOptions as fetchLiveOptionsApi, getLiveGroupTypeList as fetchLiveGroupTypeListApi } from "@/api/app/live";
 import { listRole } from '@/api/app/user/userList';
 import { getTagByIds } from "@/api/app/tag/tagGroup";
 import ImageUpload from "@/views/qw/sop/ImageUpload";
@@ -1296,6 +1317,7 @@ export default {
       msgTypeOptions: [],//消息类别选项
       courseList: [],//默认科普
       pkgList: [],//默认疗法
+      liveGroupTypeList: [],//直播分组
       liveList: [],//默认直播间
       productList: [],//默认民品列表
       medicinesList: [],//默认药品列表
@@ -1409,19 +1431,19 @@ export default {
       const [
         courseInfo,
         defaultPkg,
-        defaultLive,
+        defaultLiveGroup,
         defaultArticle,
         defaultOpenClassVideo,
       ] = await Promise.all([
         this.getCourseList(null, null),
         this.getPkgList(null, null),
-        this.getLiveList(),
+        this.fetchLiveGroupTypeList(),
         this.getArticleList(null, null),
         this.getOpenClassVideoList(null, null),
       ]);
       this.courseList = courseInfo.list;
       this.pkgList = defaultPkg.data;
-      this.liveList = defaultLive.data || defaultLive.rows || [];
+      this.liveGroupTypeList = defaultLiveGroup.rows || defaultLiveGroup.data || [];
       // 民品/药品/短视频接口暂不放开,避免控制台报错
       // this.productList = ...
       // this.medicinesList = ...
@@ -1452,11 +1474,21 @@ export default {
       };
       return await getPackageOptions(data);
     },
+    /**
+     * 获取直播分组下拉
+     */
+    async fetchLiveGroupTypeList() {
+      return await fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 });
+    },
     /**
      * 获取直播间下拉
+     * @param liveGroupType 直播分组id
      */
-    async getLiveList() {
-      return await getLiveOptions();
+    async fetchLiveOptionsByGroup(liveGroupType) {
+      if (!liveGroupType) {
+        return { data: [] };
+      }
+      return await fetchLiveOptionsApi({ liveGroupType });
     },
     // /**
     //  * 获取民品列表
@@ -1703,6 +1735,21 @@ export default {
                     })
                   }
                 }
+                //直播
+                else if (contentType === 21) {
+                  if (setting.liveGroupTypeId) {
+                    let liveRes = await this.fetchLiveOptionsByGroup(setting.liveGroupTypeId);
+                    this.$set(setting, 'meta', {
+                      liveLoading: false,
+                      liveList: liveRes.data || liveRes.rows || [],
+                    });
+                  } else {
+                    this.$set(setting, 'meta', {
+                      liveLoading: false,
+                      liveList: [],
+                    });
+                  }
+                }
                 //民品
                 else if(contentType === 22) {
                   if (!setting.productId) {
@@ -1933,6 +1980,10 @@ export default {
               this.$message.error("疗法不能为空")
               return false;
             }
+            if ([21].includes(Number(dcs.contentType)) && this.isEmpty(dcs.liveGroupTypeId)) {
+              this.$message.error("直播分组不能为空")
+              return false;
+            }
             if ([21].includes(Number(dcs.contentType)) && this.isEmpty(dcs.liveId)) {
               this.$message.error("直播间不能为空")
               return false;
@@ -2270,6 +2321,34 @@ export default {
         this.$set(content, 'packageImgUrl', selectedPkg.imgUrl);
       }
     },
+    /**
+     * 直播分组切换
+     * @param content
+     */
+    async liveGroupTypeChange(content) {
+      this.$set(content, 'liveId', null);
+      this.$set(content, 'liveTitle', null);
+      this.$set(content, 'liveImgUrl', null);
+      if (!content.liveGroupTypeId) {
+        this.$set(content, 'meta', {
+          ...(content.meta || {}),
+          liveLoading: false,
+          liveList: [],
+        });
+        return;
+      }
+      this.$set(content, 'meta', {
+        ...(content.meta || {}),
+        liveLoading: true,
+        liveList: [],
+      });
+      const liveRes = await this.fetchLiveOptionsByGroup(content.liveGroupTypeId);
+      this.$set(content, 'meta', {
+        ...(content.meta || {}),
+        liveLoading: false,
+        liveList: liveRes.data || liveRes.rows || [],
+      });
+    },
     /**
      * 直播间切换
      * @param content
@@ -2277,7 +2356,8 @@ export default {
     liveChange(content) {
       content.liveTitle = null;
       content.liveImgUrl = null;
-      const selectedLive = this.liveList.find(live => live.liveId === content.liveId);
+      const list = (content.meta && content.meta.liveList) ? content.meta.liveList : [];
+      const selectedLive = list.find(live => parseInt(live.liveId) === parseInt(content.liveId));
       if (selectedLive) {
         content.liveTitle = selectedLive.liveName; // 自动填充标题
         content.liveImgUrl = selectedLive.liveImgUrl; // 自动填充封面
@@ -2421,6 +2501,21 @@ export default {
               });
             }
           }
+          //直播
+          else if (contentType === 21) {
+            if (setting.liveGroupTypeId) {
+              let liveRes = await this.fetchLiveOptionsByGroup(setting.liveGroupTypeId);
+              this.$set(setting, 'meta', {
+                liveLoading: false,
+                liveList: liveRes.data || liveRes.rows || [],
+              });
+            } else {
+              this.$set(setting, 'meta', {
+                liveLoading: false,
+                liveList: [],
+              });
+            }
+          }
           //民品
           else if(contentType === 22) {
             if (!setting.productId) {

+ 88 - 10
src/views/app/sop/userLogsInfo/groupSendMessage.vue

@@ -366,18 +366,38 @@
                 <el-form-item label="内容" v-else-if="21 == content.contentType">
                   <div>
                     <el-card class="box-card">
+                      <el-form-item label="直播分组"
+                        required
+                      >
+                        <el-select
+                          v-model="content.liveGroupTypeId"
+                          placeholder="请选择直播分组"
+                          size="mini"
+                          filterable
+                          clearable
+                          @change="liveGroupTypeChange(content)"
+                        >
+                          <el-option
+                            v-for="dict in liveGroupTypeList"
+                            :key="dict.id"
+                            :label="dict.liveGroupType"
+                            :value="dict.id"
+                          />
+                        </el-select>
+                      </el-form-item>
                       <el-form-item label="直播间"
                         required
                       >
                         <el-select
                           v-model="content.liveId"
-                          placeholder="请选择直播间"
+                          placeholder="请先选择直播分组"
                           size="mini"
                           filterable
+                          :disabled="!content.liveGroupTypeId"
                           @change="liveChange(content)"
                         >
                           <el-option
-                            v-for="dict in liveList"
+                            v-for="dict in (content.meta && content.meta.liveList ? content.meta.liveList : [])"
                             :key="dict.liveId"
                             :label="dict.liveName"
                             :value="parseInt(dict.liveId)"
@@ -813,7 +833,7 @@ import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
 import {courseList, videoList} from "@/api/app/course";
 import { getPackageOptions,} from "@/api/app/pkg";
 import { groupSendMessage, } from '@/api/app/userLogs'
-import { getLiveOptions,} from "@/api/app/live";
+import { getLiveOptions as fetchLiveOptionsApi, getLiveGroupTypeList as fetchLiveGroupTypeListApi } from "@/api/app/live";
 // import { getMedicinesOptions, } from "@/api/app/medicines";
 // import { getProductOptions, } from "@/api/app/product";
 // import { getShortVideoOptions, } from "@/api/app/shortVideo";
@@ -840,6 +860,7 @@ export default {
       courseList:[],
       videoList:[],
       pkgList: [],//默认疗法列表
+      liveGroupTypeList: [],//直播分组
       liveList: [],//默认直播列表
       productList: [],//默认民品列表
       medicinesList: [],//默认药品列表
@@ -899,19 +920,19 @@ export default {
     const [
       courseInfo,
       defaultPkg,
-      defaultLive,
+      defaultLiveGroup,
       defaultArticle,
       defaultOpenClassVideo,
     ] = await Promise.all([
       courseList(),
       this.getPkgList(null, null),
-      this.getLiveList(),
+      this.fetchLiveGroupTypeList(),
       this.getArticleList(null, null),
       this.getOpenClassVideoList(null, null),
     ]);
     this.courseList = courseInfo.list;
     this.pkgList = defaultPkg.data;
-    this.liveList = defaultLive.data || defaultLive.rows || [];
+    this.liveGroupTypeList = defaultLiveGroup.rows || defaultLiveGroup.data || [];
     this.articleList = defaultArticle.data;
     this.openClassVideoList = defaultOpenClassVideo.data;
     // const [
@@ -926,7 +947,7 @@ export default {
     // ] = await Promise.all([
     //   courseList(),
     //   this.getPkgList(null, null),
-    //   this.getLiveList(),
+    //   this.fetchLiveOptionsByGroup(),
     //   this.getProductList(null, null),
     //   this.getMedicinesList(null, null),
     //   this.getShortVideoList(null, null),
@@ -992,11 +1013,21 @@ export default {
       };
       return await getPackageOptions(data);
     },
+    /**
+     * 获取直播分组下拉
+     */
+    async fetchLiveGroupTypeList() {
+      return await fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 });
+    },
     /**
      * 获取直播间信息
+     * @param liveGroupType 直播分组id
      */
-    async getLiveList() {
-      return await getLiveOptions();
+    async fetchLiveOptionsByGroup(liveGroupType) {
+      if (!liveGroupType) {
+        return { data: [] };
+      }
+      return await fetchLiveOptionsApi({ liveGroupType });
     },
     // /**
     //  * 获取民品列表
@@ -1259,6 +1290,34 @@ export default {
         this.$set(content, 'openClassVideoImgUrl', selected.thumbnail);
       }
     },
+    /**
+     * 直播分组切换
+     * @param content
+     */
+    async liveGroupTypeChange(content) {
+      this.$set(content, 'liveId', null);
+      this.$set(content, 'liveTitle', null);
+      this.$set(content, 'liveImgUrl', null);
+      if (!content.liveGroupTypeId) {
+        this.$set(content, 'meta', {
+          ...(content.meta || {}),
+          liveLoading: false,
+          liveList: [],
+        });
+        return;
+      }
+      this.$set(content, 'meta', {
+        ...(content.meta || {}),
+        liveLoading: true,
+        liveList: [],
+      });
+      const liveRes = await this.fetchLiveOptionsByGroup(content.liveGroupTypeId);
+      this.$set(content, 'meta', {
+        ...(content.meta || {}),
+        liveLoading: false,
+        liveList: liveRes.data || liveRes.rows || [],
+      });
+    },
     /**
      * 直播间切换
      * @param content
@@ -1266,7 +1325,8 @@ export default {
     liveChange(content) {
       content.liveTitle = null;
       content.liveImgUrl = null;
-      const selectedLive = this.liveList.find(live => parseInt(live.liveId) === parseInt(content.liveId));
+      const list = (content.meta && content.meta.liveList) ? content.meta.liveList : [];
+      const selectedLive = list.find(live => parseInt(live.liveId) === parseInt(content.liveId));
       if (selectedLive) {
         content.liveTitle = selectedLive.liveName; // 自动填充标题
         content.liveImgUrl = selectedLive.liveImgUrl; // 自动填充封面
@@ -1508,6 +1568,21 @@ export default {
             });
           }
         }
+        //直播
+        else if(item.contentType == 21) {
+          if (item.liveGroupTypeId) {
+            let liveRes = await this.fetchLiveOptionsByGroup(item.liveGroupTypeId);
+            this.$set(item, 'meta', {
+              liveLoading: false,
+              liveList: liveRes.data || liveRes.rows || [],
+            });
+          } else {
+            this.$set(item, 'meta', {
+              liveLoading: false,
+              liveList: [],
+            });
+          }
+        }
         // //民品
         // else if(item.contentType == 22) {
         //   if (!item.productId) {
@@ -1633,6 +1708,9 @@ export default {
         if ([20].includes(Number(dcs.contentType)) && this.isEmpty(dcs.packageId)) {
           return this.$message.error("疗法不能为空");
         }
+        if ([21].includes(Number(dcs.contentType)) && this.isEmpty(dcs.liveGroupTypeId)) {
+          return this.$message.error("直播分组不能为空");
+        }
         if ([21].includes(Number(dcs.contentType)) && this.isEmpty(dcs.liveId)) {
           return this.$message.error("直播间不能为空");
         }

+ 61 - 9
src/views/app/sop/userLogsInfo/sendMsgOpenTool.vue

@@ -260,12 +260,34 @@
                     <div v-if="item.contentType == 20">
                       <!--                                           <div >-->
                       <el-card class="box-card">
-                        <el-form-item label="直播间" >
-                          <el-select  v-model="item.liveId"
-                                      placeholder="请选择直播间" size="mini"
-                                      @change="liveChange(item)" >
+                        <el-form-item label="直播分组" required>
+                          <el-select
+                            v-model="item.liveGroupTypeId"
+                            placeholder="请选择直播分组"
+                            size="mini"
+                            filterable
+                            clearable
+                            @change="liveGroupTypeChange(item)"
+                          >
                             <el-option
-                              v-for="dict in liveList"
+                              v-for="dict in liveGroupTypeList"
+                              :key="dict.id"
+                              :label="dict.liveGroupType"
+                              :value="dict.id"
+                            />
+                          </el-select>
+                        </el-form-item>
+                        <el-form-item label="直播间" required>
+                          <el-select
+                            v-model="item.liveId"
+                            placeholder="请先选择直播分组"
+                            size="mini"
+                            filterable
+                            :disabled="!item.liveGroupTypeId"
+                            @change="liveChange(item)"
+                          >
+                            <el-option
+                              v-for="dict in (item.meta && item.meta.liveList ? item.meta.liveList : [])"
                               :key="dict.liveId"
                               :label="dict.liveName"
                               :value="dict.liveId"
@@ -398,7 +420,7 @@ import { sendMsgSopType,} from "@/api/app/userLogsInfo";
 import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
 import {courseList, videoList} from "@/api/qw/sop";
 import {updateTimeSopUserLogs} from "@/api/qw/sopUserLogs";
-import { getLiveOptions } from "@/api/app/live";
+import { getLiveOptions as fetchLiveOptionsApi, getLiveGroupTypeList as fetchLiveGroupTypeListApi } from "@/api/app/live";
 
 
 export default {
@@ -473,6 +495,7 @@ export default {
 
       courseList:[],
       videoList:[],
+      liveGroupTypeList: [],//直播分组
       liveList: [],//直播间下拉
       //插件版
       sysQwSopAiContentType:[],
@@ -533,8 +556,8 @@ export default {
       this.courseList = response.list;
     });
 
-    getLiveOptions().then(response => {
-      this.liveList = response.data || response.rows || [];
+    fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 }).then(response => {
+      this.liveGroupTypeList = response.rows || response.data || [];
     });
 
     this.loadSmsTemplates();
@@ -552,9 +575,34 @@ export default {
     checkLiveMiniprogramTitle(item) {
       this.$forceUpdate();
     },
+    /**
+     * 直播分组切换
+     */
+    async liveGroupTypeChange(content) {
+      this.$set(content, 'liveId', null);
+      this.$set(content, 'miniprogramTitle', '');
+      this.$set(content, 'miniprogramPicUrl', '');
+      if (!content.liveGroupTypeId) {
+        this.$set(content, 'meta', Object.assign({}, content.meta || {}, {
+          liveLoading: false,
+          liveList: [],
+        }));
+        return;
+      }
+      this.$set(content, 'meta', Object.assign({}, content.meta || {}, {
+        liveLoading: true,
+        liveList: [],
+      }));
+      const liveRes = await fetchLiveOptionsApi({ liveGroupType: content.liveGroupTypeId });
+      this.$set(content, 'meta', Object.assign({}, content.meta || {}, {
+        liveLoading: false,
+        liveList: liveRes.data || liveRes.rows || [],
+      }));
+    },
     liveChange(content) {
       // content.liveId 是选中的直播间 ID(liveId)
-      const selectedLive = this.liveList.find(live => parseInt(live.liveId) === parseInt(content.liveId));
+      const list = (content.meta && content.meta.liveList) ? content.meta.liveList : [];
+      const selectedLive = list.find(live => parseInt(live.liveId) === parseInt(content.liveId));
       if (selectedLive) {
         // 从选中的直播间对象中提取标题和封面,赋值给当前内容的对应字段
         content.miniprogramTitle = selectedLive.liveName || ''; // 自动填充标题
@@ -1113,6 +1161,10 @@ export default {
                 return this.$message.error("语音不能为空")
               }
               // 直播间内容验证
+              if (item.contentType == 20 && (item.liveGroupTypeId == null || item.liveGroupTypeId === "")) {
+                this.$message.error("直播分组不能为空")
+                return false;
+              }
               if (item.contentType == 20 && (item.liveId == null || item.liveId == "")) {
                 this.$message.error("直播间不能为空")
                 return false;

+ 402 - 4
src/views/course/courseWatchLog/indexApp.vue

@@ -117,6 +117,64 @@
           v-hasPermi="['course:courseWatchLog:export']"
         >导出</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:bindTag']"
+          @click="openBindTagPage(false, 0)"
+        >批量添加标签
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:unbindTag']"
+          @click="openUnbindTagPage(false, 1)"
+        >批量移除标签
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchBindTag']"
+          @click="openBindTagPage(true, 0)"
+        >批量添加标签(筛选条件)
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="info"
+          plain
+          size="mini"
+          v-hasPermi="['app:user:batchUnbindTag']"
+          @click="openUnbindTagPage(true, 1)"
+        >批量移除标签(筛选条件)
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          size="mini"
+          @click="openRemarkPage(false)"
+        >批量修改备注
+        </el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          size="mini"
+          @click="openRemarkPage(true)"
+        >批量修改备注(筛选条件)
+        </el-button>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
@@ -169,13 +227,82 @@
       @pagination="getList"
     />
 
+    <el-dialog :title="tagConfig.title" :visible.sync="tagConfig.visible" width="800px" append-to-body>
+      <div>搜索标签:
+        <el-input v-model="tagConfig.queryParams.groupName" placeholder="请输入标签名称" clearable size="small"
+          style="width: 200px;margin-right: 10px"/>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags">搜索</el-button>
+        <el-button type="primary" icon="el-icon-refresh" size="mini" @click="cancelSearchTags">重置</el-button>
+      </div>
+      <div class="dialog-body">
+        <div v-for="groupItem in tagConfig.tagGroupList" :key="groupItem.id">
+          <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+            <span class="name-background">{{ groupItem.name }}</span>
+          </div>
+          <div class="tag-container">
+            <a
+              v-for="tagItem in groupItem.tags"
+              :key="tagItem.id"
+              class="tag-box"
+              @click="tagSelection(tagItem)"
+              :class="{ 'tag-selected': tagItem.isSelected }"
+            >
+              {{ tagItem.name }}
+            </a>
+          </div>
+        </div>
+      </div>
+      <pagination
+        v-show="tagConfig.total > 0"
+        :total="tagConfig.total"
+        :page.sync="tagConfig.queryParams.pageNum"
+        :limit.sync="tagConfig.queryParams.pageSize"
+        @pagination="getTagGroupList"
+      />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="bindOrUnbindTagSubmit">确 定</el-button>
+        <el-button @click="bindTagCancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="remarkConfig.title" :visible.sync="remarkConfig.visible" width="600px" append-to-body>
+      <el-form label-width="90px">
+        <el-form-item label="备注类型">
+          <el-radio-group v-model="remarkConfig.remarkType">
+            <el-radio :label="1">用户名称添加在新备注前</el-radio>
+            <el-radio :label="2">用户名称添加在新备注后</el-radio>
+            <el-radio :label="3">不添加用户名称</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="备注内容">
+          <el-input
+            v-model="remarkConfig.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入备注信息"
+            maxlength="30"
+            show-word-limit
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" :loading="remarkConfig.loading" @click="submitRemark">确 定</el-button>
+        <el-button @click="cancelRemark">取 消</el-button>
+      </div>
+    </el-dialog>
+
   </div>
 </template>
 
 <script>
-import { listAppCourseWatchLog, exportAppCourseWatchLog,getCustomerListLikeName } from "@/api/course/courseWatchLog";
+import { listAppCourseWatchLog, exportAppCourseWatchLog, getCustomerListLikeName, batchUpdateFriendRemark } from "@/api/course/courseWatchLog";
 import { courseList,videoList } from '../../../api/course/courseRedPacketLog'
 import {getTask} from "@/api/common";
+import { listTagGroupForUserBindTag } from "@/api/app/tag/tagGroup";
+import {
+  bindTagV2,
+  unbindTagV2,
+} from "@/api/app/tag/bindTag";
 export default {
   name: "CourseWatchLog",
   data() {
@@ -190,7 +317,7 @@ export default {
       loading: true,
       // 导出遮罩层
       exportLoading: false,
-      // 选中数组
+      // 选中数组(会员userId)
       ids: [],
       // 非单个禁用
       single: true,
@@ -240,6 +367,7 @@ export default {
         scheduleEndTime: null,
         isVip: null,
         watchType:null,
+        appCustomerId: null,
       },
       // 表单参数
       form: {},
@@ -256,6 +384,28 @@ export default {
       },
       companyUserOptionsLoading: false,
       companyUserOptions: [],
+      tagConfig: {
+        visible: false,
+        enableFilter: false,
+        title: null,
+        opType: 0,//0-添加,1-移除
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      },
+      remarkConfig: {
+        visible: false,
+        enableFilter: false,
+        title: '批量修改备注',
+        remarkType: 1,
+        remark: '',
+        loading: false,
+      },
     };
   },
   created() {
@@ -366,12 +516,227 @@ export default {
       this.updateTime=null;
       this.handleQuery();
     },
-    // 多选框选中数据
+    // 多选框选中数据(按会员userId去重,用于批量打标签)
     handleSelectionChange(selection) {
-      this.ids = selection.map(item => item.logId)
+      this.ids = [...new Set(selection.map(item => item.userId).filter(id => id != null))]
       this.single = selection.length!==1
       this.multiple = !selection.length
     },
+    /**
+     * 用户绑定标签页面
+     */
+    openBindTagPage(enableFilter = false, opType = 0) {
+      this.tagConfig.enableFilter = enableFilter;
+      this.tagConfig.opType = opType;
+      if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要添加标签的用户');
+      }
+      this.getTagGroupList();
+      this.tagConfig.title = '批量添加标签' + (enableFilter ? '(筛选条件)' : '');
+      this.tagConfig.visible = true;
+    },
+    /**
+     * 用户解绑标签页面
+     */
+    openUnbindTagPage(enableFilter = false, opType = 1) {
+      this.tagConfig.enableFilter = enableFilter;
+      this.tagConfig.opType = opType;
+      if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要移除标签的用户');
+      }
+      this.getTagGroupList();
+      this.tagConfig.title = '批量移除标签' + (enableFilter ? '(筛选条件)' : '');
+      this.tagConfig.visible = true;
+    },
+    /**
+     * 绑定/解绑 标签页面搜索
+     */
+    handleSearchTags() {
+      this.tagConfig.queryParams.pageNum = 1;
+      this.getTagGroupList();
+    },
+    /**
+     * 绑定/解绑 标签页面重置
+     */
+    cancelSearchTags() {
+      this.tagConfig.queryParams = {
+        groupName: null,
+        pageNum: 1,
+        pageSize: 10,
+      }
+      this.getTagGroupList();
+    },
+    /**
+     * 切换选中状态
+     * @param row
+     */
+    tagSelection(row) {
+      row.isSelected = !row.isSelected;
+      if (row.isSelected) {
+        this.tagConfig.selected.push({ tagId: row.id, tagName: row.name });
+      } else {
+        this.tagConfig.selected = this.tagConfig.selected.filter(item => item.tagId !== row.id);
+      }
+      this.$forceUpdate();
+    },
+    /**
+     * 重置 绑定/解绑 标签参数
+     */
+    resetBindTag() {
+      this.tagConfig = {
+        visible: false,
+        enableFilter: false,
+        title: null,
+        opType: 0,
+        queryParams: {
+          groupName: null,
+          pageNum: 1,
+          pageSize: 10,
+        },
+        total: 0,
+        tagGroupList: [],
+        selected: [],
+      }
+    },
+    /**
+     * 绑定/解绑 标签-提交
+     */
+    bindOrUnbindTagSubmit() {
+      let opType = this.tagConfig.opType;
+      let tmpParam = {};
+      if (!this.tagConfig.selected || this.tagConfig.selected.length === 0) {
+        return this.$message('请选择标签');
+      }
+      tmpParam['enableFilter'] = this.tagConfig.enableFilter;
+      tmpParam['tagId'] = this.tagConfig.selected.map(item => item.tagId);
+      tmpParam['userId'] = this.ids;
+      const queryParams = JSON.parse(JSON.stringify(this.queryParams));
+      delete queryParams['startIndex'];
+      delete queryParams['pageLimit'];
+      tmpParam['fsUserParam'] = queryParams;
+      //绑定标签
+      if (opType === 0) {
+        bindTagV2(tmpParam)
+          .then(res => {
+            this.tagConfig.visible = false;
+            this.resetBindTag();
+            this.msgSuccess("正在添加中!");
+            setTimeout(() => {
+              this.getList();
+            }, 500);
+          })
+          .catch(err => {
+            this.msgError("添加标签失败!");
+          })
+      }
+      //解绑标签
+      else {
+        unbindTagV2(tmpParam)
+          .then(res => {
+            this.tagConfig.visible = false;
+            this.msgSuccess("正在移除中!");
+            this.resetBindTag();
+            setTimeout(() => {
+              this.getList();
+            }, 500);
+          })
+          .catch(err => {
+            this.msgError("移除标签失败!");
+          })
+      }
+    },
+    /**
+     * 绑定/解绑 标签-取消
+     */
+    bindTagCancel() {
+      this.resetBindTag();
+    },
+    /**
+     * 查询标签列表
+     */
+    getTagGroupList() {
+      listTagGroupForUserBindTag(this.tagConfig.queryParams).then(response => {
+        this.tagConfig.tagGroupList = response.rows;
+        this.tagConfig.total = response.total;
+      });
+    },
+    /**
+     * 打开批量修改备注弹窗
+     * @param enableFilter 是否按筛选条件
+     */
+    openRemarkPage(enableFilter = false) {
+      if (!enableFilter && (!this.ids || this.ids.length === 0)) {
+        this.$message.closeAll();
+        return this.$message('请选择需要修改备注的用户');
+      }
+      if (enableFilter && (!this.total || this.total <= 0)) {
+        this.$message.closeAll();
+        return this.$message('当前筛选条件下没有数据');
+      }
+      this.remarkConfig.enableFilter = enableFilter;
+      this.remarkConfig.remarkType = 1;
+      this.remarkConfig.remark = '';
+      this.remarkConfig.loading = false;
+      this.remarkConfig.title = '批量修改备注' + (enableFilter ? '(筛选条件)' : '');
+      this.remarkConfig.visible = true;
+    },
+    cancelRemark() {
+      this.remarkConfig.visible = false;
+      this.remarkConfig.enableFilter = false;
+      this.remarkConfig.remarkType = 1;
+      this.remarkConfig.remark = '';
+      this.remarkConfig.loading = false;
+    },
+    /**
+     * 提交批量修改备注
+     */
+    async submitRemark() {
+      const remark = (this.remarkConfig.remark || '').trim();
+      if (!remark) {
+        return this.$message.error('请输入备注内容');
+      }
+      if (remark.length > 30) {
+        return this.$message.error('备注内容不能超过30个字符');
+      }
+      this.remarkConfig.loading = true;
+      try {
+        let userIds = [];
+        if (this.remarkConfig.enableFilter) {
+          // 按筛选条件拉取全部匹配记录的 userId
+          const queryParams = JSON.parse(JSON.stringify(this.queryParams));
+          queryParams.pageNum = 1;
+          queryParams.pageSize = this.total > 0 ? this.total : 10000;
+          if (queryParams.logType == "10") {
+            queryParams.logType = null;
+          }
+          const response = await listAppCourseWatchLog(queryParams);
+          const rows = response.rows || [];
+          userIds = [...new Set(rows.map(item => item.userId).filter(id => id != null))];
+        } else {
+          userIds = this.ids;
+        }
+        if (!userIds || userIds.length === 0) {
+          this.remarkConfig.loading = false;
+          return this.$message.error('没有可修改备注的用户');
+        }
+        await batchUpdateFriendRemark({
+          remarkType: this.remarkConfig.remarkType,
+          remark,
+          userId: userIds,
+        });
+        this.msgSuccess('正在修改备注中!');
+        this.cancelRemark();
+        setTimeout(() => {
+          this.getList();
+        }, 500);
+      } catch (e) {
+        this.msgError('修改备注失败!');
+      } finally {
+        this.remarkConfig.loading = false;
+      }
+    },
     /** 新增按钮操作 */
     handleAdd() {
       this.reset();
@@ -464,3 +829,36 @@ export default {
   }
 };
 </script>
+<style lang="scss" scoped>
+.dialog-body {
+  height: 400px;
+  overflow: auto;
+}
+
+.tag-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+
+.name-background {
+  display: inline-block;
+  background-color: #abece6;
+  padding: 4px 8px;
+  border-radius: 4px;
+}
+
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+</style>