李妹妹 il y a 6 jours
Parent
commit
1f4a4bf613
3 fichiers modifiés avec 642 ajouts et 120 suppressions
  1. 38 4
      src/api/qw/im.js
  2. 171 23
      src/views/qw/qwChat/qq.vue
  3. 433 93
      src/views/qw/qwChat/userDetail/index.vue

+ 38 - 4
src/api/qw/im.js

@@ -49,17 +49,19 @@ export function getQwUserList() {
   })
 }
 //联系人列表
-export function getContactList(userId) {
+export function getContactList(userId,query) {
   return request({
     url: '/qw/qwMsg/contactList/'+userId,
-    method: 'get'
+    method: 'get',
+    params: query
   })
 }
 //群聊
-export function getGroupList(userId) {
+export function getGroupList(userId,query) {
   return request({
     url: '/qw/qwMsg/groupList/'+userId,
-    method: 'get'
+    method: 'get',
+    params: query
   })
 }
 //会话记录生成
@@ -71,6 +73,38 @@ export function getConversationId(query) {
   })
 }
 
+//企业标签列表
+export function getCorpTagList(query) {
+  return request({
+    url: '/qw/qwMsg/getCorpTagList',
+  method: 'get',
+  params: query
+  })
+}
+//联系人标签列表
+export function getQwExternalContactTagList(query) {
+  return request({
+    url: '/qw/qwMsg/getQwExternalContactTagList',
+  method: 'get',
+  params: query
+  })
+}
+//添加联系人标签
+export function addQwExternalContactTag(data) {
+  return request({
+    url: '/qw/qwMsg/addQwExternalContactTag',
+ method: 'post',
+ data: data
+  })
+}
+//删除联系人标签
+export function delQwExternalContactTag(data) {
+  return request({
+    url: '/qw/qwMsg/delQwExternalContactTag',
+ method: 'post',
+ data: data
+  })
+}
 // 发送文本消息
 export function sendMsg(data) {
   return request({

+ 171 - 23
src/views/qw/qwChat/qq.vue

@@ -108,9 +108,47 @@
         </template>
         <template #sidebar-contact-top>
           <!-- 联系人顶部 -->
+          <div style="padding: 12px; display: flex; align-items: center;position: sticky;z-index: 9;top:0;background-color: #fff;">
+            <el-input
+              v-model="pageGroup.name"
+              prefix-icon="el-icon-search"
+              placeholder="搜索"
+              size="small"
+              clearable
+              @input="handleSearchName"
+              style="flex: 1;height: 32px;"
+              class="top-input"
+            />
+            <!-- <el-popover
+              placement="bottom-end"
+              width="200"
+              v-model="filterPopoverVisible"
+              trigger="click"
+            > -->
+              <!-- 这里放你的过滤选项内容 -->
+             <!-- <div>
+                <el-checkbox-group v-model="selectedFilters">
+                  <el-checkbox label="1">去掉重粉</el-checkbox>
+                  <el-checkbox label="2">去掉黑粉</el-checkbox>
+                  <el-checkbox label="3">按催课排序</el-checkbox>
+                </el-checkbox-group>
+                <div style="margin-top: 10px; text-align: right;">
+                  <el-button size="mini" type="primary" @click="applyFilter">确定</el-button>
+                </div>
+              </div>
+              <el-button
+                slot="reference"
+                icon="el-icon-setting"
+                style="padding: 8px;height: 32px;border: none;background: #F1F4F8;width:32px;"
+                size="medium"
+                :type="selectedFilters.length > 0 ? 'primary' : 'default'"
+              ></el-button>
+            </el-popover> -->
+          </div>
+
         </template>
        <template #group-top>
-           <div class="group-item" @click="toggleGroup" style="padding-top: 14px;">
+           <div class="group-item" @click="toggleGroup">
              <div class="left">
                <i class="el-icon-arrow-right" :class="{ 'rotate': openGroup }"></i>
                <div class="name">群聊</div>
@@ -269,6 +307,18 @@ export default {
         pageNum: 1,
         pageSize: 500
       },
+      pageGroup: {
+        pageNum: 1,
+        pageSize: 500,
+        userId:null,
+        name:''
+      },
+      pageContact: {
+        pageNum: 1,
+        pageSize: 500,
+        userId:null,
+         name:''
+      },
       detail: {
         title: '',
         open: false
@@ -293,7 +343,12 @@ export default {
       qwGroupList:[],
       qwGroupNum:0,
       //课程列表
-      showCourseManage:false
+      showCourseManage:false,
+       cache: {
+            contacts: {}, // 缓存联系人:{ appKey1: { list: [], total: 0, time: 时间戳 }, ... }
+            groups: {}    // 缓存群聊:{ appKey1: { list: [], total: 0, time: 时间戳 }, ... }
+          },
+          cacheExpire: 300000 // 缓存过期时间(5分钟,可根据需求调整)
     };
   },
   created(){
@@ -546,35 +601,117 @@ export default {
 
     // 切换企微账号
     qwUserChange(tab){
+      this.pageGroup.name='';
+      this.pageContact.name='';
       this.appKey = tab.appKey;
       this.appId = tab.id;
       let index= this.qwUserList.findIndex(item => item.appKey === tab.appKey);
       this.qwUser=this.qwUserList[index];
       // 清除当前账号的未读消息数
       this.clearUnreadCount(this.appKey);
+      //清除过期缓存
+      this.clearContact();
       // 不需要重新创建WebSocket连接,只需更新当前账号信息
       this.setQwUserInfo();
       this.getConversation();   //获取会话信息
       this.initGroup(this.qwUser.id)
     },
+    //清除过期缓存
+    clearContact(){
+       // 清理所有账号的过期缓存
+        Object.keys(this.cache.contacts).forEach(appKey => {
+          const cache = this.cache.contacts[appKey];
+          if (Date.now() - cache.time >= this.cacheExpire) {
+            delete this.cache.contacts[appKey];
+          }
+        });
+        // 同理清理群聊过期缓存
+        Object.keys(this.cache.groups).forEach(appKey => {
+          const cache = this.cache.groups[appKey];
+          if (Date.now() - cache.time >= this.cacheExpire) {
+            delete this.cache.groups[appKey];
+          }
+        });
+    },
     //联系人列表
     initContact(id){
-      getContactList(id).then(response => {
-        this.qwContactList=response.rows;
-        this.qwContactNum=response.total
-        const mergedList = [...this.qwGroupList, ...this.qwContactList];
-        this.$refs.IMUI.initContacts(mergedList);
-        // this.getConversationId(id)
-      });
+      // getContactList(id,this.pageContact).then(response => {
+      //   this.qwContactList=response.rows;
+      //   this.qwContactNum=response.total
+      //   const mergedList = [...this.qwGroupList, ...this.qwContactList];
+      //   this.$refs.IMUI.initContacts(mergedList);
+      // });
+      const currentAppKey = this.appKey;
+        const contactCache = this.cache.contacts[currentAppKey];
+
+        // 检查联系人缓存是否有效
+        if (contactCache && (Date.now() - contactCache.time) < this.cacheExpire) {
+          // 使用缓存数据
+          this.qwContactList = contactCache.list;
+          this.qwContactNum = contactCache.total;
+          // 合并群聊和联系人并初始化IM组件
+          this.mergeAndInitContacts();
+          return;
+        }
+
+        // 缓存无效,请求接口
+        getContactList(id, this.pageContact).then(response => {
+          const contactData = {
+            list: response.rows,
+            total: response.total,
+            time: Date.now()
+          };
+          // 更新当前账号的联系人缓存
+          this.cache.contacts[currentAppKey] = contactData;
+          // 更新页面数据
+          this.qwContactList = contactData.list;
+          this.qwContactNum = contactData.total;
+          // 合并并初始化
+          this.mergeAndInitContacts();
+        }).catch(error => {
+          console.error('加载联系人失败:', error);
+        });
     },
     //群聊列表
     initGroup(id){
-      getGroupList(id).then(response => {
-        this.qwGroupList=response.rows
-        this.qwGroupNum = response.total
-        this.initContact(id)
-        //this.$refs.IMUI.initContacts(this.qwContactList);
-      });
+      // getGroupList(id,this.pageGroup).then(response => {
+      //   this.qwGroupList=response.rows
+      //   this.qwGroupNum = response.total
+      //   this.initContact(id)
+      // });
+       const currentAppKey = this.appKey; // 当前企微账号的appKey
+        const groupCache = this.cache.groups[currentAppKey]; // 获取该账号的群聊缓存
+
+        // 检查缓存是否有效(存在且未过期)
+        if (groupCache && (Date.now() - groupCache.time) < this.cacheExpire) {
+          // 使用缓存数据
+          this.qwGroupList = groupCache.list;
+          this.qwGroupNum = groupCache.total;
+          this.initContact(id); // 继续加载联系人
+          return; // 终止后续接口请求
+        }
+
+        // 缓存无效,请求接口
+        getGroupList(id, this.pageGroup).then(response => {
+          const groupData = {
+            list: response.rows,
+            total: response.total,
+            time: Date.now() // 记录缓存时间
+          };
+          // 更新当前账号的群聊缓存
+          this.cache.groups[currentAppKey] = groupData;
+          // 更新页面数据
+          this.qwGroupList = groupData.list;
+          this.qwGroupNum = groupData.total;
+          // 继续加载联系人
+          this.initContact(id);
+        }).catch(error => {
+          console.error('加载群聊失败:', error);
+        });
+    },
+    mergeAndInitContacts() {
+      const mergedList = [...this.qwGroupList, ...this.qwContactList];
+      this.$refs.IMUI.initContacts(mergedList);
     },
     // 切换分组展开/收起状态
         toggleGroup() {
@@ -599,7 +736,7 @@ export default {
     },
     getConversation(){
       const IMUI = this.$refs.IMUI;
-
+      const currentAppKey = this.appKey; // 记录当前 appKey
       // 检查是否已有该企微账号的会话记录缓存
       if (this.qwUserSessions[this.appKey]) {
         // 如果有,直接使用缓存的会话记录
@@ -621,9 +758,12 @@ export default {
         // 如果没有缓存,则从服务器获取会话记录
         getConversations(this.qwUser.id,this.pageParams).then(response => {
           this.conversationData = response.data.list;
-
+            if (this.appKey !== currentAppKey) {
+        console.warn('企微已切换,忽略旧会话数据');
+        return;
+           }
           // 缓存会话记录
-          this.qwUserSessions[this.appKey] = response.data.list;
+          this.qwUserSessions[currentAppKey] = response.data.list;
 
           IMUI.initConversations(response.data.list);
           const fstConversation = this.conversationData[0];
@@ -668,7 +808,7 @@ export default {
       });
     },
     handleChangeConversation(conversation, instance) {
-      console.log(conversation,'conversation')
+      //console.log(conversation,'conversation')
         this.showCourseManage=false
       if(conversation.isGroup){
         // this.contact.extId=conversation.extId
@@ -716,7 +856,7 @@ export default {
           qwUserId:this.qwUser.id,
         }
         getConversationId(data).then(response => {
-          IMUI.appendConversation(response.data)
+          IMUI.appendConversation(response.data);
           IMUI.changeContact(contact.conversationId);
           IMUI.currentNewContactId=contact.id;
         });
@@ -1079,6 +1219,14 @@ export default {
       });
       this.$refs.IMUI.initConversations(filtered);
     },
+    //联系人搜索
+    handleSearchName(value) {
+      if(value){
+        this.pageGroup.name=value
+        this.pageContact.name=value
+        this.initGroup(this.qwUser.id)
+        }
+    },
     applyFilter() {
       this.filterPopoverVisible = false;
       this.handleSearch();
@@ -1216,15 +1364,15 @@ border-radius: 0 6px 6px 0;
       padding-left: 12px;
       padding-right: 12px;
       width: 100%;
-      height: 45px;
-      line-height: 45px;
+      height: 32px;
+      line-height: 32px;
       // padding-bottom: 12px;
       display: flex;
       justify-content: space-between;
       align-items: center;
       position: sticky;
       position: -webkit-sticky;
-    top: 0px;
+    top: 56px;
         z-index: 9;
         background-color: #fff;
       /* 箭头旋转动画 */

+ 433 - 93
src/views/qw/qwChat/userDetail/index.vue

@@ -1,11 +1,10 @@
 <template>
-  <div
-    :style="{background: 'linear-gradient(#E9F1FE, #F1F4F8)', minHeight: '100%'}">
+  <div :style="{background: 'linear-gradient(#E9F1FE, #F1F4F8)', minHeight: '100%'}">
     <!-- 客户详情 -->
     <div v-if="showDetail" style="padding: 12px;">
       <div class="top-box">
         <div class="left">
-          <img class="avatar-box" :src="qwUserInfo.avatar"/>
+          <img class="avatar-box" :src="qwUserInfo.avatar" />
           <div class="left-box">
             <div class="title2">{{qwUserDetail.name || '未知'}}</div>
             <div class="age-box">
@@ -24,13 +23,15 @@
       <div style="background-color: #fff;padding: 12px;border-radius: 6px 6px 6px 6px;">
         <div class="title-top">
           <div class="title">客户信息</div>
-          <div class="down-text" >
-           <el-button type="text" @click="handleDown">{{isExpanded?'收起':'展开'}}</el-button>
-            <img src="@/assets/image/sq_arrow_down.png" width="12" height="12" :class="{ 'rotate-90': !isExpanded }"/></div>
+          <div class="down-text">
+            <el-button type="text" @click="handleDown">{{isExpanded?'收起':'展开'}}</el-button>
+            <img src="@/assets/image/sq_arrow_down.png" width="12" height="12" :class="{ 'rotate-90': !isExpanded }" />
+          </div>
         </div>
         <!--  基本信息 -->
-        <el-descriptions :column="2" :class="extend ? 'detail-description-extend' : 'detail-description'" :style="{display:isExpanded?'':'none'}" size="medium">
-         <!-- <template slot="extra">
+        <el-descriptions :column="2" :class="extend ? 'detail-description-extend' : 'detail-description'"
+          :style="{display:isExpanded?'':'none'}" size="medium">
+          <!-- <template slot="extra">
 
           </template> -->
           <!-- <el-descriptions-item label="姓名">{{qwUserInfo.name || '未知'}}</el-descriptions-item>
@@ -46,8 +47,69 @@
           <el-descriptions-item label="咨询产品">{{qwUserDetail.consultProduct || '-'}}</el-descriptions-item>
           <el-descriptions-item label="是否已购产品">{{qwUserDetail.isBuy || '无'}}</el-descriptions-item>
         </el-descriptions>
-      </div>
+        <div class="addTag">
+          <div class="title">他的标签</div>
+          <div class="tags-wrapper">
+            <el-tag v-for="tag in showAllTags ? userTags : userTags.slice(0,5)" closable :disable-transitions="false" :key="tag.tagId"
+              @close="handleClose(tag)">
+              {{tag.name}}
+            </el-tag>
+             <!-- 超出5个时显示“更多”按钮 -->
+                <el-tag
+                 class="other"
+                  v-if="userTags.length > 5 && !showAllTags"
+
+                  @click="showAllTags = true"
+                >
+                  + 更多
+                </el-tag>
+
+                <!-- 显示全部时,添加“收起”按钮 -->
+                <el-tag
+                class="other"
+                  v-if="userTags.length > 5 && showAllTags"
+
+                  @click="showAllTags = false"
+                >
+                  收起
+                </el-tag>
+
+          </div>
+        <el-button type="primary" size="small" @click="handleChangeTags()" icon="el-icon-circle-plus" plain>添加标签</el-button>
 
+        </div>
+      </div>
+      <el-dialog title="添加标签" :visible.sync="tagOpen" width="800px" append-to-body>
+        <div>搜索标签:
+          <el-input v-model="tagChange.tagName" placeholder="请输入标签名称" clearable size="small"
+            style="width: 200px;margin-right: 10px" />
+          <el-button type="primary" icon="el-icon-search" size="mini"
+            @click="handleSearchTags(tagChange.tagName)">搜索</el-button>
+          <el-button type="primary" icon="el-icon-plus" size="mini" @click="cancelSearchTags">重置</el-button>
+        </div>
+        <el-form ref="form" :model="addTagForm" label-width="80px">
+          <div v-for="item in tagList" :key="item.id">
+            <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+              <span class="name-background">{{ item.name }}</span>
+            </div>
+            <!-- 添加外层滚动容器 -->
+            <div class="scroll-wrapper">
+              <div class="tag-container">
+                <a v-for="tagItem in item.tag" class="tag-box" @click="tagSelection(tagItem)"
+                  :class="{ 'tag-selected': tagItem.isSelected }">
+                  {{ tagItem.name }}
+                </a>
+              </div>
+            </div>
+          </div>
+        </el-form>
+        <pagination v-show="tagTotal>0" :total="tagTotal" :page.sync="queryTagParams.pageNum"
+          :limit.sync="queryTagParams.pageSize" @pagination="getCorpTagList" />
+        <div slot="footer" class="dialog-footer">
+          <el-button type="primary" @click="addTagSubmitForm()">确 定</el-button>
+          <el-button @click="addTagCancel">取 消</el-button>
+        </div>
+      </el-dialog>
       <div :style="{backgroundColor: '#FFF',margin: !extend ? '12px 0' : '12px 0'}">
         <div class="tab-box">
           <div :class="activeName==index?'tabs active':'tabs'" v-for="(item,index) in tabs" :key="index"
@@ -56,8 +118,9 @@
           </div>
         </div>
         <div v-if="activeName==0" class="tab-content">
-           <!-- 近七天看课记录 -->
-          <div style="display: flex;justify-content: space-between;align-items: center;flex-direction: row;background: #FFF;padding: 10px">
+          <!-- 近七天看课记录 -->
+          <div
+            style="display: flex;justify-content: space-between;align-items: center;flex-direction: row;background: #FFF;padding: 10px">
             <div class="section-title">
               近七天看课记录
             </div>
@@ -75,28 +138,28 @@
 
               <div class="course-status-block" :style="{ backgroundColor: getLogTypeColor(item.logType) }"
                 :title="getLogTypeText(item.logType)">
-                <img v-if="item.logType!==2" src="@/assets/image/view_class_records.png" width="20" height="20"/>
-                <img v-if="item.logType==2" src="@/assets/image/end_of_class.png" width="20" height="20"/>
-                </div>
-                <span class="course-date">{{ item.date.split('-')[2] }}日</span>
+                <img v-if="item.logType!==2" src="@/assets/image/view_class_records.png" width="20" height="20" />
+                <img v-if="item.logType==2" src="@/assets/image/end_of_class.png" width="20" height="20" />
+              </div>
+              <span class="course-date">{{ item.date.split('-')[2] }}日</span>
             </div>
             <div v-if="courseWatch7day.length === 0" class="empty-record">
               暂无看课记录
             </div>
           </div>
- <!-- 已看过课程 -->
-      <div class="record-box" v-if="localExtId">
-        <div class="top">
-          <div class="title">
-            用户看课记录
-          </div>
-          <!-- <div class="more" @click="courseManage">
+          <!-- 已看过课程 -->
+          <div class="record-box" v-if="localExtId">
+            <div class="top">
+              <div class="title">
+                用户看课记录
+              </div>
+              <!-- <div class="more" @click="courseManage">
             全部看课记录
             <i class="el-icon-arrow-right" style="font-size: 14px; color: #979B9E;"></i>
           </div> -->
-        </div>
-        <userCourseWatchLog ref="userWatchLog" />
-      </div>
+            </div>
+            <userCourseWatchLog ref="userWatchLog" />
+          </div>
 
         </div>
         <div v-if="activeName==1" class="tab-content">
@@ -104,14 +167,14 @@
             <!-- <div style="padding: 10px;font-weight: bold">
               用户订单
             </div> -->
-            <userStorerDetails ref="userDetails"/>
+            <userStorerDetails ref="userDetails" />
           </div>
 
         </div>
         <!-- 访问记录 -->
         <div v-if="activeName==2" class="tab-content">
           <div v-if="localExtId">
-            <userBehavior ref="userBehaviorLog"/>
+            <userBehavior ref="userBehaviorLog" />
           </div>
 
         </div>
@@ -120,19 +183,19 @@
 
 
       <!-- 看课记录 -->
-     <!-- <div :style="{backgroundColor: '#FFF',margin: !extend ? '10px' : '5px 0'}"> -->
-       <!-- <div style="padding: 10px;font-weight: bold">
+      <!-- <div :style="{backgroundColor: '#FFF',margin: !extend ? '10px' : '5px 0'}"> -->
+      <!-- <div style="padding: 10px;font-weight: bold">
           看课记录
         </div> -->
-        <!-- 状态 -->
-       <!-- <div class="status-container">
+      <!-- 状态 -->
+      <!-- <div class="status-container">
           <div v-for="(item, index) in logTypeList" :key="index" class="status-item">
             <div class="status-color-block" :style="{ backgroundColor: item.color }"></div>
             <span class="status-text">{{ item.text }}</span>
           </div>
         </div> -->
-        <!-- 近七天看课记录 -->
-        <!-- <div>
+      <!-- 近七天看课记录 -->
+      <!-- <div>
           <div style="display: flex;justify-content: space-between;align-items: center;flex-direction: row;background: #FFF;padding: 10px 10px 20px">
             <div class="section-title">
               近七天看课记录
@@ -153,7 +216,7 @@
       <!-- </div> -->
 
       <!-- 购买记录 -->
-    <!--  <div :style="{backgroundColor: '#FFF',margin: !extend ? '10px' : '5px 0'}" v-if="qwUserInfo.fsUserId">
+      <!--  <div :style="{backgroundColor: '#FFF',margin: !extend ? '10px' : '5px 0'}" v-if="qwUserInfo.fsUserId">
         <div style="padding: 10px;font-weight: bold">
           用户订单
         </div>
@@ -174,7 +237,8 @@
     <user-info-edit v-if="showUpdate" :userDetail="qwUserDetail" @back="handleBack" @save-success="handleSaveSuccess" />
 
     <!-- 课程管理 -->
-    <course-manage v-if="showCourseManage" :conversationId="conversationId"  :extend="extend" :userId="qwUserInfo.id" @back="handleBack" />
+    <course-manage v-if="showCourseManage" :conversationId="conversationId" :extend="extend" :userId="qwUserInfo.id"
+      @back="handleBack" />
   </div>
 </template>
 
@@ -187,7 +251,11 @@
   import {
     getQwExternalContactDetails,
     getQwUserInfo,
-    queryCourseWatchStatistics
+    queryCourseWatchStatistics,
+    getQwExternalContactTagList,
+    getCorpTagList,
+    addQwExternalContactTag,
+    delQwExternalContactTag
   } from "@/api/qw/im";
 
   export default {
@@ -197,7 +265,7 @@
         type: String,
         default: null
       },
-      conversationId:{
+      conversationId: {
         type: String,
         default: null
       },
@@ -230,9 +298,31 @@
         showDetail: true,
         showUpdate: false,
         courseWatch7day: [],
+        tagList: [],
+        userTags: [],
         logTypeColors,
         activeName: 0,
         isExpanded: true,
+        showAllTags: false, // 控制是否显示全部标签
+        //标签弹窗选择
+        addTagForm: {
+          qwExternalContactId: null,
+          tagIds: []
+        },
+        tagChange: {
+          open: false,
+          index: null,
+        },
+        tagOpen: false,
+        selectedTagIds:[],
+        queryTagParams: {
+          pageNum: 1,
+          pageSize: 5,
+          total: 0,
+          name: null,
+          qwExternalContactId: null,
+        },
+        tagTotal: 0,
         tabs: [{
             label: '看课记录'
           },
@@ -288,9 +378,101 @@
     methods: {
       getDetail(extId) {
         this.localExtId = extId
+        this.queryTagParams.qwExternalContactId = extId
         this.getQwExternalContactDetails()
         this.getQwUserInfo()
         this.queryCourseWatchStatistics()
+        this.getQwExternalContactTagList()
+
+      },
+      tagSelection(row) {
+
+        row.isSelected = !row.isSelected;
+        this.$forceUpdate();
+      },
+      handleChangeTags() {
+        this.tagOpen = true
+        this.getCorpTagList()
+      },
+      handleSearchTags(name) {
+
+        if (!name) {
+          return this.$message.error("请输入要搜索的标签")
+        }
+        this.queryTagParams.name = name;
+        this.getCorpTagList()
+      },
+      cancelSearchTags() {
+        this.queryTagParams = {
+            pageNum: 1,
+            pageSize: 5,
+            total: 0,
+            name: null,
+          },
+          this.getCorpTagList()
+      },
+      addTagCancel() {
+
+        this.tagOpen = false;
+
+        this.addTagForm = {
+          qwExternalContactId: null,
+          tagIds: []
+        };
+      },
+      addTagSubmitForm() {
+
+        for (let i = 0; i < this.tagList.length; i++) {
+          for (let x = 0; x < this.tagList[i].tag.length; x++) {
+            if(this.tagList[i].tag[x].isSelected==true){
+              this.addTagForm.tagIds.push(this.tagList[i].tag[x].id)
+            }
+
+          }
+        }
+        if (this.addTagForm.tagIds == [] || this.addTagForm.tagIds == null || this.addTagForm.tagIds == "") {
+          return this.$message('请选择标签');
+        }
+
+        this.addTagForm.qwExternalContactId = this.queryTagParams.qwExternalContactId
+        let loadingRock = this.$loading({
+          lock: true,
+          text: '正在执行中请稍后~~请不要刷新页面!!',
+          spinner: 'el-icon-loading',
+          background: 'rgba(0, 0, 0, 0.7)'
+        });
+        addQwExternalContactTag(this.addTagForm).then(response => {
+          this.msgSuccess(response.msg);
+          this.tagOpen = false;
+          loadingRock.close();
+          this.addTagForm = {
+            qwExternalContactId: null,
+            tagIds: []
+          };
+          this.getQwExternalContactTagList()
+        }).finally(res => {
+          loadingRock.close();
+        });
+
+      },
+      //删除一些选择的标签
+      handleClose(tag) {
+        const list=[]
+        list.push(tag.id)
+        const data = {
+          tagIds: list,
+          qwExternalContactId: this.localExtId
+        }
+        this.$confirm('确认删除他的标签吗?', "警告", {
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            type: "warning"
+        }).then(function() {
+          return delQwExternalContactTag(data);
+        }).then(() => {
+          this.getQwExternalContactTagList();
+          this.msgSuccess("删除成功");
+        }).catch(function() {});
       },
       queryCourseWatchStatistics() {
         const data = {
@@ -308,19 +490,19 @@
         getQwExternalContactDetails(query).then(response => {
           this.qwUserInfo = response.data
           if (this.localExtId) {
-             this.$nextTick(() => {
-                    // 调用 userWatchLog 前先判断
-                    if (this.$refs.userWatchLog && this.activeName==0) {
-                      this.$refs.userWatchLog.getUserWatchLog(this.localExtId)
-                    }
-                    // 调用 userDetails 前先判断
-                    if (this.$refs.userDetails && this.activeName==1) {
-                      this.$refs.userDetails.getUserDetails(this.localExtId)
-                    }
-                    if (this.$refs.userBehaviorLog && this.activeName==2) {
-                       this.$refs.userBehaviorLog.getDetails(this.localExtId);
-                    }
-                  })
+            this.$nextTick(() => {
+              // 调用 userWatchLog 前先判断
+              if (this.$refs.userWatchLog && this.activeName == 0) {
+                this.$refs.userWatchLog.getUserWatchLog(this.localExtId)
+              }
+              // 调用 userDetails 前先判断
+              if (this.$refs.userDetails && this.activeName == 1) {
+                this.$refs.userDetails.getUserDetails(this.localExtId)
+              }
+              if (this.$refs.userBehaviorLog && this.activeName == 2) {
+                this.$refs.userBehaviorLog.getDetails(this.localExtId);
+              }
+            })
           }
         })
       },
@@ -333,11 +515,39 @@
 
         })
       },
+      getQwExternalContactTagList() {
+        const query = {
+          pageNum: 1,
+          pageSize: 100,
+          qwExternalContactId: this.localExtId
+        }
+        getQwExternalContactTagList(query).then(response => {
+          this.userTags = response.rows
+
+        })
+      },
+      getCorpTagList() {
+        getCorpTagList(this.queryTagParams).then(response => {
+          this.tagList = response.rows;
+          this.tagTotal = response.total;
+         if(this.userTags.length>0){
+           const selectedTagIds = new Set(
+             (this.userTags || []).map(tagItem => tagItem?.id)
+           );
+           const filteredTagList = this.tagList.map(group => ({
+                ...group, // 保留分组信息(如分组名等)
+                tag: group.tag.filter(it => !selectedTagIds.has(it.id)) // 过滤分组内的标签
+              })).filter(group => group.tag.length > 0); // 移除空分组
+              this.tagList=filteredTagList
+              //console.log(filteredTagList,'filteredTagList')
+         }
+        })
+      },
       updateQwDetail() {
         this.showDetail = false
         this.showUpdate = true
       },
-      handleDown(){
+      handleDown() {
         this.isExpanded = !this.isExpanded;
       },
       handleBack() {
@@ -389,6 +599,19 @@
     transform: rotate(-90deg);
     transition: transform 0.3s ease;
   }
+.tags-wrapper {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-bottom: 10px;
+  .el-tag + .el-tag{
+    margin-left: 0 !important;
+  }
+  .other{
+    cursor: pointer;
+  }
+}
+
   .top-box {
     display: flex;
     align-items: center;
@@ -399,16 +622,18 @@
     .left {
       display: flex;
       align-items: center;
-       .avatar-box{
-         width: 44px;
-         height: 44px;
-         border-radius: 4px 4px 4px 4px;
-         object-fit:cover;
-       }
+
+      .avatar-box {
+        width: 44px;
+        height: 44px;
+        border-radius: 4px 4px 4px 4px;
+        object-fit: cover;
+      }
+
       .left-box {
         margin-left: 12px;
         display: flex;
-        align-items: center;
+        align-items: flex-start;
         flex-direction: column;
 
         .title2 {
@@ -449,34 +674,138 @@
       }
     }
   }
-   .title-top{
+
+  .title-top {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+
+    .title {
+      font-weight: 500;
+      font-size: 16px;
+      color: #141F36;
+    }
+
+    .down-text {
       display: flex;
       align-items: center;
-      justify-content: space-between;
-      .title{
-        font-weight: 500;
-        font-size: 16px;
-        color: #141F36;
+      // font-weight: 400;
+      // font-size: 12px;
+
+      cursor: pointer;
+
+      .el-button--text {
+        color: #84868C !important;
+        font-size: 12px !important;
       }
-      .down-text {
-        display: flex;
-        align-items: center;
-        // font-weight: 400;
-        // font-size: 12px;
 
-        cursor: pointer;
-        .el-button--text{
-          color: #84868C !important;
-          font-size: 12px !important;
-        }
-        img {
-          margin-left: 4px;
-        }
+      img {
+        margin-left: 4px;
       }
     }
-  .el-descriptions__extra {
+  }
+.addTag{
+  border-top: 1px solid #F1F4F8;
+  padding-top: 20px;
+  .title{
+    margin-bottom: 14px;
+    font-family: PingFang SC, PingFang SC;
+    font-weight: 500;
+    font-size: 16px;
+    color: #141F36;
+    text-align: left;
+  }
+
+}
+  /* CSS 样式 */
+  .tag-container {
+    display: flex;
+    flex-wrap: wrap;
+    /* 超出宽度时自动换行 */
+    gap: 8px;
+    /* 设置标签之间的间距 */
+  }
+
+  .name-background {
+    display: inline-block;
+    background-color: #abece6;
+    /* 背景颜色 */
+    padding: 4px 8px;
+    /* 调整内边距,让背景包裹文字 */
+    border-radius: 4px;
+    /* 可选:设置圆角 */
+  }
 
+  /* CSS 样式 */
+  .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;
+  }
+
+  .el-tag+.el-tag {
+    margin-left: 10px;
+  }
+
+  /* 新增的滚动容器样式(不影响原有样式) */
+  .scroll-wrapper {
+    max-height: 130px;
+    /* 大约三行的高度 */
+    overflow-y: auto;
+    /* 垂直滚动 */
+    padding-right: 5px;
+    /* 为滚动条留出空间 */
+  }
+
+  /* 美化滚动条(可选) */
+  .scroll-wrapper::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  .scroll-wrapper::-webkit-scrollbar-thumb {
+    background: rgba(0, 0, 0, 0.2);
+    border-radius: 3px;
+  }
+
+  .tag-container {
+    max-height: 200px;
+    overflow-y: auto;
+    padding: 1px;
+    border: 1px solid #ebeef5;
+    border-radius: 1px;
+    background-color: #fafafa;
+  }
+
+  .tag-list {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
   }
 
   .tab-box {
@@ -488,18 +817,20 @@
     border-bottom: 1px solid #F1F4F8;
 
     .tabs {
-      flex:1;
+      flex: 1;
       font-weight: 400;
       font-size: 16px;
       color: #5C5F66;
       text-align: center;
-height: 48px;
-line-height: 48px;
- position: relative;
- cursor: pointer;
+      height: 48px;
+      line-height: 48px;
+      position: relative;
+      cursor: pointer;
+
       &.active {
         font-weight: 600;
         color: #202124;
+
         &:after {
           content: '';
           position: absolute;
@@ -508,27 +839,34 @@ line-height: 48px;
           width: 30px;
           height: 4px;
           background: #409EFF;
-          transition: all 0.25s ease; /* 快速过渡更显灵敏 */
-          left: 50%; /* 水平居中 */
-              transform: translateX(-50%); /* 配合 left:50% 实现精确居中 */
+          transition: all 0.25s ease;
+          /* 快速过渡更显灵敏 */
+          left: 50%;
+          /* 水平居中 */
+          transform: translateX(-50%);
+          /* 配合 left:50% 实现精确居中 */
         }
       }
     }
   }
-  .record-box{
+
+  .record-box {
     width: 100%;
     padding: 10px;
-    .top{
+
+    .top {
       display: flex;
       align-items: center;
       justify-content: space-between;
-      .title{
+
+      .title {
         margin: 0 !important;
         font-weight: 500;
         font-size: 16px;
         color: #141F36;
       }
-      .more{
+
+      .more {
         font-weight: 400;
         font-size: 14px;
         color: #84868C;
@@ -537,6 +875,7 @@ line-height: 48px;
     }
 
   }
+
   .status-container {
     display: flex;
     justify-content: space-between;
@@ -563,7 +902,7 @@ line-height: 48px;
   }
 
   .section-title {
-   line-height: 24px;
+    line-height: 24px;
     font-weight: 500;
     font-size: 16px;
     color: #141F36;
@@ -572,7 +911,7 @@ line-height: 48px;
   .course-record-container {
     display: flex;
     justify-content: space-between;
-    margin:10px;
+    margin: 10px;
     gap: 10px;
     overflow-x: auto;
     padding-bottom: 20px;
@@ -620,7 +959,8 @@ line-height: 48px;
   .detail-description-extend {
     // padding: 12px;
     background-color: #FFF;
-margin-top: 20px;
+    margin-top: 20px;
+    padding-bottom: 10px;
   }
 
   ::v-deep .detail-description-extend .el-descriptions__title {