瀏覽代碼

Merge remote-tracking branch 'origin/master'

yuhongqi 1 天之前
父節點
當前提交
e78b5c4afe

+ 16 - 0
src/api/app/customerMember/index.js

@@ -34,3 +34,19 @@ export function delCustomerMember(ids) {
     method: 'delete'
   })
 }
+
+// 同步销售(异步)
+export function syncSalesCustomerMember() {
+  return request({
+    url: '/app/cusrole/member/syncSales',
+    method: 'post'
+  })
+}
+
+// 查询同步销售任务状态
+export function getSyncSalesCustomerMemberStatus() {
+  return request({
+    url: '/app/cusrole/member/syncSales/status',
+    method: 'get'
+  })
+}

+ 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/courseFinishTemp/index.vue

@@ -530,7 +530,7 @@
                     <div>
                         <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in customerConfig.selected" closable
                             :disable-transitions="false" @close="handleCloseCustomer(s)">
-                            <span>{{ s.roleName }}</span>
+                            <span>{{ s.memberName }}</span>
                         </el-tag>
                     </div>
                     <div style="

+ 97 - 11
src/views/app/customerMember/index.vue

@@ -33,16 +33,16 @@
     </el-form>
 
     <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          icon="el-icon-plus"
-          size="mini"
-          v-hasPermi="['app:customerMember:add']"
-          @click="handleAdd"
-        >新增</el-button>
-      </el-col>
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="primary"-->
+<!--          plain-->
+<!--          icon="el-icon-plus"-->
+<!--          size="mini"-->
+<!--          v-hasPermi="['app:customerMember:add']"-->
+<!--          @click="handleAdd"-->
+<!--        >新增</el-button>-->
+<!--      </el-col>-->
       <el-col :span="1.5">
         <el-button
           type="success"
@@ -65,6 +65,24 @@
           @click="handleDelete()"
         >删除</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-refresh"
+          size="mini"
+          :loading="syncLoading"
+          v-hasPermi="['app:customerMember:add']"
+          @click="handleSyncSales"
+        >同步销售</el-button>
+        <el-tooltip
+          effect="dark"
+          content="该按钮初次同步可能较慢,后续再次同步只会新增新的销售数据"
+          placement="top"
+        >
+          <i class="el-icon-question sync-sales-tip"></i>
+        </el-tooltip>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
@@ -212,7 +230,9 @@ import {
   listCustomerMember,
   addCustomerMember,
   updateCustomerMember,
-  delCustomerMember
+  delCustomerMember,
+  syncSalesCustomerMember,
+  getSyncSalesCustomerMemberStatus
 } from '@/api/app/customerMember'
 import { listCustomerRoleOptions } from '@/api/app/customerRole'
 import { getCompanyList, getCompanyUserList } from '@/api/company/companyUser'
@@ -224,6 +244,8 @@ export default {
   data() {
     return {
       loading: true,
+      syncLoading: false,
+      syncTimer: null,
       showSearch: true,
       total: 0,
       ids: [],
@@ -272,7 +294,64 @@ export default {
     this.loadRoleOptions()
     this.getList()
   },
+  beforeDestroy() {
+    this.clearSyncTimer()
+  },
   methods: {
+    clearSyncTimer() {
+      if (this.syncTimer) {
+        clearInterval(this.syncTimer)
+        this.syncTimer = null
+      }
+    },
+    handleSyncSales() {
+      this.$confirm('是否确认同步销售数据', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        this.syncLoading = true
+        return syncSalesCustomerMember()
+      }).then(response => {
+        if (response.code !== 200) {
+          this.syncLoading = false
+          this.msgError(response.msg || '提交同步任务失败')
+          return
+        }
+        this.msgSuccess('同步任务已提交,同步中。。。')
+        this.clearSyncTimer()
+        this.syncTimer = setInterval(() => {
+          this.pollSyncSalesStatus()
+        }, 2000)
+        this.pollSyncSalesStatus()
+      }).catch(() => {
+        this.syncLoading = false
+      })
+    },
+    pollSyncSalesStatus() {
+      getSyncSalesCustomerMemberStatus().then(response => {
+        if (response.code !== 200) {
+          return
+        }
+        const data = response.data || {}
+        if (data.status === 'running') {
+          return
+        }
+        this.clearSyncTimer()
+        this.syncLoading = false
+        if (data.status === 'success') {
+          this.msgSuccess(data.message || '同步完成')
+          this.getList()
+          this.loadRoleOptions()
+        } else if (data.status === 'fail') {
+          this.msgError(data.message || '同步失败')
+        }
+      }).catch(() => {
+        this.clearSyncTimer()
+        this.syncLoading = false
+        this.msgError('查询同步状态失败')
+      })
+    },
     parseRoleId(value) {
       if (value === null || value === undefined || value === '') {
         return null
@@ -472,4 +551,11 @@ export default {
 .el-input__icon {
   cursor: pointer;
 }
+.sync-sales-tip {
+  margin-left: 6px;
+  color: #909399;
+  cursor: pointer;
+  font-size: 14px;
+  vertical-align: middle;
+}
 </style>

+ 13 - 13
src/views/app/customerRole/index.vue

@@ -16,19 +16,19 @@
       </el-form-item>
     </el-form>
 
-    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          icon="el-icon-plus"
-          size="mini"
-          v-hasPermi="['app:customerRole:add']"
-          @click="handleAdd"
-        >新增</el-button>
-      </el-col>
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>
+<!--    <el-row :gutter="10" class="mb8">-->
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="primary"-->
+<!--          plain-->
+<!--          icon="el-icon-plus"-->
+<!--          size="mini"-->
+<!--          v-hasPermi="['app:customerRole:add']"-->
+<!--          @click="handleAdd"-->
+<!--        >新增</el-button>-->
+<!--      </el-col>-->
+<!--      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>-->
+<!--    </el-row>-->
 
     <el-table v-loading="loading" :data="roleList" border>
       <el-table-column label="客服组名称" align="center" prop="roleName" min-width="140" />

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

@@ -58,7 +58,7 @@
               @close="handleCloseCusForQuery(list)"
               style="margin: 3px;"
             >
-              {{ list.roleName }}
+              {{ list.memberName }}
             </el-tag>
           </div>
         </div>
@@ -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">
@@ -229,7 +229,7 @@
     />
 
     <!-- 新增/修改邀请码弹窗 -->
-    <el-dialog :title="dialog.title" :visible.sync="dialog.open" v-loading="dialog.loading" width="500px" append-to-body>
+    <el-dialog :title="dialog.title" :visible.sync="dialog.open" v-loading="dialog.loading" width="600px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="110px">
         <el-form-item label="所属部门" prop="deptId" v-if="activeName === 'person'">
           <treeselect
@@ -265,7 +265,7 @@
           <div>
             <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in dialog.selectCustomerForEdit.selected" closable
                    :disable-transitions="false" @close="handleCloseCusForEdit(s)">
-              <span>{{ s.roleName }}</span>
+              <span>{{ s.memberName }}</span>
             </el-tag>
           </div>
         </el-form-item>

+ 1 - 1
src/views/app/sop/info/addSop.vue

@@ -72,7 +72,7 @@
             <div>
               <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in selected" closable
                 :disable-transitions="false" @close="handleCloseCustomer(s)">
-                <span>{{ s.roleName }}</span>
+                <span>{{ s.memberName }}</span>
               </el-tag>
             </div>
           </el-form-item>

+ 124 - 32
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)"
@@ -953,7 +974,7 @@
                                   </el-row>
                                 </div>
 
-                                <!-- 改状态 -->
+                                <!-- 改状态(已停用,暂注释)
                                 <div v-if="ruleItem.type == 5"
                                   style="
                                     background-color: #fdfdfd;
@@ -1017,6 +1038,7 @@
                                   </el-form-item>
 
                                 </div>
+                                -->
 
                                 <!-- 添加内容节点 -->
                                 <el-link
@@ -1253,7 +1275,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 +1318,7 @@ export default {
       msgTypeOptions: [],//消息类别选项
       courseList: [],//默认科普
       pkgList: [],//默认疗法
+      liveGroupTypeList: [],//直播分组
       liveList: [],//默认直播间
       productList: [],//默认民品列表
       medicinesList: [],//默认药品列表
@@ -1401,7 +1424,8 @@ export default {
         this.sysFsSopWatchStatus = response.data;
       });
       this.getDicts("app_sop_msg_type").then(response => {
-        this.msgTypeOptions = response.data;
+        // 改状态(dictValue=5)已停用,页面过滤掉该选项
+        this.msgTypeOptions = (response.data || []).filter(d => Number(d.dictValue) !== 5);
       });
       this.getDicts("app_normal_disabled_status").then(response => {
         this.statusOptions = response.data;
@@ -1409,28 +1433,23 @@ export default {
       const [
         courseInfo,
         defaultPkg,
-        defaultLive,
-        defaultProduct,
-        defaultMedicines,
-        defaultShortVideo,
+        defaultLiveGroup,
         defaultArticle,
         defaultOpenClassVideo,
       ] = await Promise.all([
         this.getCourseList(null, null),
         this.getPkgList(null, null),
-        this.getLiveList(),
-        this.getProductList(null, null),
-        this.getMedicinesList(null, null),
-        this.getShortVideoList(null, null),
+        this.fetchLiveGroupTypeList(),
         this.getArticleList(null, null),
         this.getOpenClassVideoList(null, null),
       ]);
       this.courseList = courseInfo.list;
       this.pkgList = defaultPkg.data;
-      this.liveList = defaultLive.data;
-      this.productList = defaultProduct.data;
-      this.medicinesList = defaultMedicines.data;
-      this.shortVideoList = defaultShortVideo.data;
+      this.liveGroupTypeList = defaultLiveGroup.rows || defaultLiveGroup.data || [];
+      // 民品/药品/短视频接口暂不放开,避免控制台报错
+      // this.productList = ...
+      // this.medicinesList = ...
+      // this.shortVideoList = ...
       this.articleList = defaultArticle.data;
       this.openClassVideoList = defaultOpenClassVideo.data;
     },
@@ -1457,12 +1476,22 @@ export default {
       };
       return await getPackageOptions(data);
     },
-    // /**
-    //  * 获取直播间下拉
-    //  */
-    // async getLiveList() {
-    //   return await getLiveOptions();
-    // },
+    /**
+     * 获取直播分组下拉
+     */
+    async fetchLiveGroupTypeList() {
+      return await fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 });
+    },
+    /**
+     * 获取直播间下拉
+     * @param liveGroupType 直播分组id
+     */
+    async fetchLiveOptionsByGroup(liveGroupType) {
+      if (!liveGroupType) {
+        return { data: [] };
+      }
+      return await fetchLiveOptionsApi({ liveGroupType });
+    },
     // /**
     //  * 获取民品列表
     //  * @param keyword
@@ -1708,6 +1737,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) {
@@ -1938,20 +1982,24 @@ 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;
             }
           }
         }
-        //规则消息类别-改状态(绑定/解绑-标签/客服)
-        else if (dc.type == 5) {
-          if (this.isEmpty(dc.addTag) && this.isEmpty(dc.delTag)
-            && this.isEmpty(dc?.addCustomer) && this.isEmpty(dc?.delCustomer)) {
-            this.$message.error(`第${j + 1}条规则配置错误,未改动任何状态`);
-            return false;
-          }
-        }
+        //规则消息类别-改状态(绑定/解绑-标签/客服)(已停用,暂注释)
+        // else if (dc.type == 5) {
+        //   if (this.isEmpty(dc.addTag) && this.isEmpty(dc.delTag)
+        //     && this.isEmpty(dc?.addCustomer) && this.isEmpty(dc?.delCustomer)) {
+        //     this.$message.error(`第${j + 1}条规则配置错误,未改动任何状态`);
+        //     return false;
+        //   }
+        // }
       }
       return true;
     },
@@ -2275,6 +2323,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
@@ -2282,7 +2358,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; // 自动填充封面
@@ -2426,6 +2503,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) {

+ 97 - 16
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: [],//默认药品列表
@@ -886,12 +907,12 @@ export default {
     },
   },
   async created() {
-    const disabledContentTypes = ['21', '22', '23', '24'];
+    // 仅屏蔽民品/药品/短视频,直播(21)需要展示
+    const disabledContentTypes = ['22', '23', '24'];
     this.getDicts("app_sop_plugin_settingType").then(response => {
       this.sysQwSopAiContentType = response.data.filter(
         item => !disabledContentTypes.includes(String(item.dictValue))
       );
-      // this.sysQwSopAiContentType = response.data;
     });
     this.getDicts("app_sop_watch_status").then(response => {
       this.sysFsSopWatchStatus = response.data;
@@ -899,16 +920,19 @@ export default {
     const [
       courseInfo,
       defaultPkg,
+      defaultLiveGroup,
       defaultArticle,
       defaultOpenClassVideo,
     ] = await Promise.all([
       courseList(),
       this.getPkgList(null, null),
+      this.fetchLiveGroupTypeList(),
       this.getArticleList(null, null),
       this.getOpenClassVideoList(null, null),
     ]);
     this.courseList = courseInfo.list;
     this.pkgList = defaultPkg.data;
+    this.liveGroupTypeList = defaultLiveGroup.rows || defaultLiveGroup.data || [];
     this.articleList = defaultArticle.data;
     this.openClassVideoList = defaultOpenClassVideo.data;
     // const [
@@ -923,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),
@@ -989,12 +1013,22 @@ export default {
       };
       return await getPackageOptions(data);
     },
-    // /**
-    //  * 获取直播间信息
-    //  */
-    // async getLiveList() {
-    //   return await getLiveOptions();
-    // },
+    /**
+     * 获取直播分组下拉
+     */
+    async fetchLiveGroupTypeList() {
+      return await fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 });
+    },
+    /**
+     * 获取直播间信息
+     * @param liveGroupType 直播分组id
+     */
+    async fetchLiveOptionsByGroup(liveGroupType) {
+      if (!liveGroupType) {
+        return { data: [] };
+      }
+      return await fetchLiveOptionsApi({ liveGroupType });
+    },
     // /**
     //  * 获取民品列表
     //  * @param keyword
@@ -1256,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
@@ -1263,7 +1325,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; // 自动填充封面
@@ -1505,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) {
@@ -1630,9 +1708,12 @@ 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.liveId)) {
-        //   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("直播间不能为空");
+        }
         // if ([22].includes(Number(dcs.contentType)) && this.isEmpty(dcs.productId)) {
         //   return this.$message.error("民品不能为空");
         // }

+ 71 - 12
src/views/app/sop/userLogsInfo/sendMsgOpenTool.vue

@@ -257,15 +257,37 @@
                       </div>
 
                     </el-form-item>
-                    <div v-if="item.contentType == 10">
+                    <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,6 +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 as fetchLiveOptionsApi, getLiveGroupTypeList as fetchLiveGroupTypeListApi } from "@/api/app/live";
 
 
 export default {
@@ -472,6 +495,8 @@ export default {
 
       courseList:[],
       videoList:[],
+      liveGroupTypeList: [],//直播分组
+      liveList: [],//直播间下拉
       //插件版
       sysQwSopAiContentType:[],
 
@@ -518,6 +543,8 @@ export default {
   },
 
   created() {
+    // 本页内容类别字典:sys_qwSopAi_contentType(直播对应 dictValue=20)
+    // 注意:与群发页 app_sop_plugin_settingType(直播=21)不是同一套字典
     this.getDicts("sys_qwSopAi_contentType").then(response => {
       this.sysQwSopAiContentType = response.data;
     });
@@ -529,6 +556,10 @@ export default {
       this.courseList = response.list;
     });
 
+    fetchLiveGroupTypeListApi({ pageNum: 1, pageSize: 999 }).then(response => {
+      this.liveGroupTypeList = response.rows || response.data || [];
+    });
+
     this.loadSmsTemplates();
   },
   methods: {
@@ -544,12 +575,36 @@ 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 => 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) {
         // 从选中的直播间对象中提取标题和封面,赋值给当前内容的对应字段
-        // 假设直播间对象中标题字段为 liveTitle,封面字段为 coverImg(根据实际接口字段调整)
         content.miniprogramTitle = selectedLive.liveName || ''; // 自动填充标题
         content.miniprogramPicUrl = selectedLive.liveImgUrl || ''; // 自动填充封面
       } else {
@@ -1028,7 +1083,7 @@ export default {
             result.push(additionalMessage);
           }
         }
-        if (item.contentType == 10) {
+        if (item.contentType == 20) {
           item.miniprogramAppid = 'wx776d6bd6848eec49'
         }
         result.push(item);
@@ -1106,15 +1161,19 @@ export default {
                 return this.$message.error("语音不能为空")
               }
               // 直播间内容验证
-              if (item.contentType == 10 && (item.liveId == null || item.liveId == "")) {
+              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;
               }
-              if (item.contentType == 10 && (item.miniprogramTitle == null || item.miniprogramTitle == "")) {
+              if (item.contentType == 20 && (item.miniprogramTitle == null || item.miniprogramTitle == "")) {
                 this.$message.error("标题不能为空")
                 return false;
               }
-              if (item.contentType == 10 && (item.miniprogramPicUrl == null || item.miniprogramPicUrl == "")) {
+              if (item.contentType == 20 && (item.miniprogramPicUrl == null || item.miniprogramPicUrl == "")) {
                 this.$message.error("封面不能为空")
                 return false;
               }

+ 67 - 16
src/views/app/user/CusRoleList.vue

@@ -3,8 +3,12 @@
     <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px"
       @submit.prevent="handleQuery">
 
-      <el-form-item label="角色名称" prop="roleName">
-        <el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable size="small"
+      <el-form-item label="销售名称" prop="memberName">
+        <el-input v-model="queryParams.memberName" placeholder="请输入销售名称" clearable size="small"
+          @keydown.enter.native="handleQueryEnter" />
+      </el-form-item>
+      <el-form-item label="所属公司" prop="roleName">
+        <el-input v-model="queryParams.roleName" placeholder="请输入所属公司" clearable size="small"
           @keydown.enter.native="handleQueryEnter" />
       </el-form-item>
 
@@ -14,25 +18,27 @@
       </el-form-item>
     </el-form>
 
-    <el-table 
-      v-loading="loading" 
-      :data="roleList" 
+    <el-table
+      v-loading="loading"
+      :data="roleList"
       ref="roleList"
       :row-key="row => String(row.id)"
+      :class="{ 'cus-role-single': !multiple }"
       @select="singleSelectHandler"
       @select-all="allSelectHandler"
      >
       <el-table-column type="selection" width="55" align="center"/>
-      <el-table-column label="角色id" align="center" prop="id"/>
-      <el-table-column label="角色名称" align="center" prop="roleName"/>
-      <el-table-column label="角色备注" align="center" prop="remark"/>
+      <el-table-column label="销售id" align="center" prop="id" min-width="100"/>
+      <el-table-column label="销售名称" align="center" prop="memberName" min-width="140" show-overflow-tooltip/>
+      <el-table-column label="所属公司" align="center" prop="roleName" min-width="140" show-overflow-tooltip/>
+      <el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip/>
     </el-table>
 
     <div style="margin-top: 30px;display: flex;justify-content: center">
       <el-button type="warning" icon="el-icon-search" @click="confirmSelect">确定选择</el-button>
     </div>
 
-    <pagination 
+    <pagination
       v-show="total > 0"
       :total="total"
       :page.sync="queryParams.pageNum"
@@ -59,10 +65,17 @@ export default {
       type: Array,
       default: () => [],
     },
+    // 是否允许多选,false 时只能勾选一个
+    multiple: {
+      type: Boolean,
+      default: true,
+    },
+    // 排除的销售id
     ignoreIds: {
       type: Array,
       default: () => [],
     },
+    // 仅查询指定销售id
     ids: {
       type: Array,
       default: () => [],
@@ -74,12 +87,13 @@ export default {
       loading: true,
       // 总条数
       total: 0,
-      // 客服角色表格数据
+      // 客服成员表格数据
       roleList: [],
       // 查询参数
       queryParams: {
         pageNum: 1,
         pageSize: 10,
+        memberName: null,
         roleName: null
       },
     };
@@ -99,16 +113,20 @@ export default {
   },
   methods: {
     /**
-     * 查询用户列表
+     * 查询客服成员列表
      */
     getList() {
       this.loading = true;
       let qp = this.queryParams;
-      if (this.ignoreIds) {
+      if (this.ignoreIds && this.ignoreIds.length) {
         qp['ignoreIds'] = this.ignoreIds;
+      } else {
+        delete qp.ignoreIds;
       }
-       if (this.ids) {
+      if (this.ids && this.ids.length) {
         qp['ids'] = this.ids;
+      } else {
+        delete qp.ids;
       }
       listRole(qp).then(response => {
         this.roleList = response.rows;
@@ -145,6 +163,7 @@ export default {
       this.queryParams = {
         pageNum: this.defaultPageNum,
         pageSize: this.defaultPageSize,
+        memberName: null,
         roleName: null,
       }
       this.resetForm("form");
@@ -182,10 +201,30 @@ export default {
     },
     /**
      * 单选
-     * @param selection 
-     * @param row 
+     * @param selection
+     * @param row
      */
     singleSelectHandler(selection, row) {
+      // 单选模式:只保留当前行
+      if (!this.multiple) {
+        const isSelected = selection.some(l => l.id == row.id)
+        this.$refs.roleList.clearSelection()
+        if (isSelected) {
+          this.$nextTick(() => {
+            this.$refs.roleList.toggleRowSelection(row, true)
+          })
+        }
+        try {
+          this.$emit('selectionChange', {
+            mode: 0,
+            row,
+            selected: isSelected
+          })
+        } catch (e) {
+          console.warn('当前组件暂未指定selection回调')
+        }
+        return
+      }
       let pm = {
         mode: 0,//0-单行选,1-全选
         row,//操作的行
@@ -204,9 +243,14 @@ export default {
     },
     /**
      * 全选
-     * @param selection 
+     * @param selection
      */
     allSelectHandler(selection) {
+      // 单选模式不允许全选
+      if (!this.multiple) {
+        this.$refs.roleList.clearSelection()
+        return
+      }
       let pm = {
         mode: 1,//0-单行选,1-全选
         rows: this.roleList,//操作的所有行
@@ -221,3 +265,10 @@ export default {
   }
 };
 </script>
+
+<style scoped>
+/* 单选模式隐藏表头全选勾选框 */
+.cus-role-single ::v-deep .el-table__header-wrapper .el-checkbox {
+  display: none;
+}
+</style>

+ 10 - 23
src/views/app/user/index.vue

@@ -395,6 +395,7 @@
     <el-dialog :title="bindAIConfig.title" :visible.sync="bindAIConfig.visible" width="1000px" append-to-body :close-on-click-modal="false">
       <CusRoleList
         ref="customerBindRef"
+        :multiple="false"
         @confirm="customerSelectedConfirmCallForBind"
         @selectionChange="customerSelectionChangeCallForBind"
         :selected="bindAIConfig.selected"
@@ -1321,6 +1322,7 @@ export default {
       }
       this.bindAIConfig.title = type == 0 ? '绑定客服' : '解绑客服';
       this.bindAIConfig.type = type;
+      this.bindAIConfig.selected = [];
       this.bindAIConfig.visible = true;
     },
     /**
@@ -1328,41 +1330,26 @@ export default {
      */
     customerSelectionChangeCallForBind(config) {
       let selected = config.selected;
-      //单行操作
+      // 单选模式:最多只保留一个客服
       if (config.mode === 0) {
         let row = config.row;
         if (selected) {
-          if (!this.bindAIConfig.selected.some(s => s.id == row.id)) {
-            this.bindAIConfig.selected.push(row);
-          }
+          this.bindAIConfig.selected = [row];
         } else {
           this.bindAIConfig.selected = this.bindAIConfig.selected.filter(m => m.id != row.id);
         }
+        return;
       }
-      //操作当前页所有行
-      else {
-        let list = config.rows;
-        if (selected) {
-          list.map(l => {
-            if (!this.bindAIConfig.selected.some(s => s.id == l.id)) {
-              this.bindAIConfig.selected.push(l);
-            }
-          });
-        } else {
-          list.map(l => {
-            for (let i = 0; i < this.bindAIConfig.selected.length; i++) {
-              if (l.id == this.bindAIConfig.selected[i].id) {
-                this.bindAIConfig.selected.splice(i, 1);
-              }
-            }
-          })
-        }
-      }
+      // 全选(单选模式下 CusRoleList 已拦截,此处兜底忽略)
     },
     /**
      * 保存绑定的客服
      */
     async customerSelectedConfirmCallForBind() {
+      if (!this.bindAIConfig.selected || this.bindAIConfig.selected.length !== 1) {
+        this.$message.closeAll();
+        return this.$message.warning('请选择一个客服');
+      }
       let tmpParam = {};
       tmpParam['enableFilter'] = this.bindAIConfig.enableFilter;
       tmpParam['roleId'] = this.bindAIConfig.selected.map(item => item.id) || [];

+ 2 - 2
src/views/app/welcome/index.vue

@@ -12,7 +12,7 @@
                             :key="list.id"
                             @close="handleCloseCustomer(list, 0)"
                             style="margin: 3px;"
-                        >{{ list.roleName }}
+                        >{{ list.memberName }}
                         </el-tag>
                     </div>
                 </div>
@@ -126,7 +126,7 @@
                         <div>
                             <el-tag style="margin-left: 5px" size="medium" :key="s.id" v-for="s in appCustomerBindConfig.selected" closable
                                 :disable-transitions="false" @close="handleCloseCustomer(s, 1)">
-                                <span>{{ s.roleName }}</span>
+                                <span>{{ s.memberName }}</span>
                             </el-tag>
                         </div>
                     </el-form-item>

+ 8 - 3
src/views/components/index/statisticsDashboard.vue

@@ -619,7 +619,6 @@ const viewCharOption = {
     bottom: '3%',
     containLabel: true
   },
-  projectFrom:process.env.VUE_APP_PROJECT_FROM,
   xAxis: {
     type: 'category',
     data: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']
@@ -1014,6 +1013,10 @@ export default {
       userTypeText: process.env.VUE_APP_COURSE_DEFAULT==1?"会员":"企微",
       userType: process.env.VUE_APP_COURSE_DEFAULT,
       dealerChart: null,
+      courseWatchChart: null,
+      thisMonthOrderChart: null,
+      thisMonthRecvChart: null,
+      projectFrom: process.env.VUE_APP_PROJECT_FROM,
       // 分公司数量
       dealderCount: 0,
       // 销售数量
@@ -1106,6 +1109,7 @@ export default {
       this.initThisMonthOrderChart();
       this.initThisMonthRecvChart();
 
+      this.refresh();
 
       // 监听窗口大小变化,重新渲染图表
       window.addEventListener('resize', () => {
@@ -1116,7 +1120,6 @@ export default {
     })
   },
   created() {
-    this.refresh();
     listDept().then(res => {
       this.deptInitOptions = res.data;
       listCompany().then(res => {
@@ -1604,7 +1607,9 @@ export default {
           courseWatchOption.series[1].data = completedUserCountList;
           courseWatchOption.series[2].data = answerUserCountList;
           courseWatchOption.series[3].data = correctUserCountList;
-          this.courseWatchChart.setOption(courseWatchOption)
+          if (this.courseWatchChart) {
+            this.courseWatchChart.setOption(courseWatchOption)
+          }
         }
       })
     },

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

+ 1 - 0
src/views/his/user/indexProject.vue

@@ -288,6 +288,7 @@
       :total="total"
       :page.sync="queryParams.pageNum"
       :limit.sync="queryParams.pageSize"
+      :page-sizes="[10, 20, 30, 50, 100, 200, 300, 500]"
       @pagination="getList"
     />