Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/master'

yuhongqi 2 dní pred
rodič
commit
9e5f99525f

+ 9 - 0
src/api/app/user/index.js

@@ -44,6 +44,15 @@ export function unbindUserCusByIdsV2(data) {
   })
 }
 
+// 按勾选/筛选条件查询已绑定销售 id
+export function listBoundSales(data) {
+  return request({
+    url: '/vip/userCusRole/boundSales',
+    method: 'post',
+    data: data
+  })
+}
+
 // 用户获取列表
 export function listAppUser(data) {
   return request({

+ 18 - 0
src/api/qw/companySession.js

@@ -74,3 +74,21 @@ export function qwGroupChatOptions({ corpId, keyword }) {
   });
 }
 
+// 客户维度列表(会话审计-客户维度,按客户ID/昵称/备注搜索)
+export function qwCustomerList(params) {
+  return request({
+    url: '/weChatSpace/customerList',
+    method: 'get',
+    params
+  });
+}
+
+// 某客户关联的客服列表
+export function qwCustomerStaffList({ corpId, externalUserId }) {
+  return request({
+    url: '/weChatSpace/customerStaffList',
+    method: 'get',
+    params: { corpId, externalUserId }
+  });
+}
+

+ 2 - 0
src/router/index.js

@@ -357,6 +357,7 @@ export const constantRoutes = [
   {
     path: '/qw/companySession',
     component: Layout,
+    hidden: true,
     meta: { title: '企微会话', icon: 'chat' },
     children: [
       {
@@ -369,6 +370,7 @@ export const constantRoutes = [
   },
   {
     path: '/qw/conversationNew',
+    hidden: true,
     component: () => import('@/views/qw/conversationNew/ConversationNew'),
     name: 'ConversationNew',
     meta: { title: '会话分析', icon: 'el-icon-chat-line-round' }

+ 134 - 39
src/views/app/courseFinishTemp/index.vue

@@ -179,7 +179,7 @@
                     style="margin-bottom: 15px;"
                     prop="videoId"
                 >
-                
+
                     <el-select
                         v-model="addForm.courseId"
                         placeholder="默认50条,可输入关键字检索"
@@ -257,7 +257,7 @@
                     >
                         <el-row style="padding-bottom: 20px">
                             <el-col :span="22">
-                                <el-form 
+                                <el-form
                                     :model="content"
                                     label-width="70px"
                                 >
@@ -265,12 +265,12 @@
                                         style="margin: 2%"
                                     >
                                         <div>
-                                            <el-radio-group 
+                                            <el-radio-group
                                                 v-model="content.contentType"
                                                 @change="handleContentTypeChange()"
                                             >
                                                 <el-radio
-                                                    :disabled="!addForm.canOp || !['11', '20'].includes(item.dictValue)"
+                                                    :disabled="!addForm.canOp || !['9','11', '20', '21'].includes(item.dictValue)"
                                                     :key="item.dictValue"
                                                     :label="item.dictValue"
                                                     v-for="item in sysQwSopAiContentType"
@@ -283,10 +283,10 @@
 
                                     <!-- 科普 -->
                                     <el-form-item label="内容" v-if="[9].includes(Number(content.contentType))">
-                                        
+
                                         <div>
                                             <el-card class="box-card">
-                                                <el-form-item 
+                                                <el-form-item
                                                     label="链接标题:"
                                                     label-width="100px"
                                                     required
@@ -298,7 +298,7 @@
                                                         :disabled="!addForm.canOp"
                                                     />
                                                 </el-form-item>
-                                                <el-form-item 
+                                                <el-form-item
                                                     label="链接描述:"
                                                     label-width="100px"
                                                     required
@@ -312,7 +312,7 @@
                                                         style="width: 90%;margin-top: 1%;"
                                                     />
                                                 </el-form-item>
-                                                <el-form-item 
+                                                <el-form-item
                                                     label="链接封面:"
                                                     label-width="100px"
                                                     required
@@ -328,11 +328,11 @@
                                                     />
                                                 </el-form-item>
                                                 <div>
-                                                    <el-form-item 
+                                                    <el-form-item
                                                         label="链接地址:"
                                                         label-width="100px"
                                                     >
-                                                        <el-tag 
+                                                        <el-tag
                                                             type="warning"
                                                             v-model="content.isBindUrl"
                                                         >选择的科普小节
@@ -340,11 +340,11 @@
                                                         </el-tag>
                                                     </el-form-item>
                                                 </div>
-                                                <el-form-item 
+                                                <el-form-item
                                                     label="课节过期时间"
                                                 >
                                                     <el-row>
-                                                        <el-input 
+                                                        <el-input
                                                         type="number"
                                                         v-model="content.expiresDays"
                                                         style="width: 200px"
@@ -413,7 +413,7 @@
                                     <el-form-item label="内容" v-else-if="[6].includes(Number(content.contentType))">
 
                                         <div>
-                                            <el-form-item 
+                                            <el-form-item
                                                 label="上传视频:"
                                                 prop="videoUrl"
                                                 label-width="100px"
@@ -429,7 +429,7 @@
                                             >
                                                 <i class="el-icon-plus avatar-uploader-icon"></i>
                                             </el-upload>
-                                            <video 
+                                            <video
                                                 v-if="content.videoUrl"
                                                 :src="content.videoUrl"
                                                 controls
@@ -443,9 +443,9 @@
 
                                     <!-- 疗法 -->
                                     <el-form-item label="内容" v-else-if="[20].includes(Number(content.contentType))">
-                                        
+
                                         <el-card class="box-card">
-                                            <el-form-item 
+                                            <el-form-item
                                                 label="选择疗法:"
                                                 label-width="100px"
                                                 required
@@ -471,7 +471,7 @@
                                                 />
                                                 </el-select>
                                             </el-form-item>
-                                            <el-form-item 
+                                            <el-form-item
                                                 label="疗法名称:"
                                                 label-width="100px"
                                             >
@@ -482,11 +482,11 @@
                                                     style="width: 90%;margin-top: 1%;"
                                                 />
                                             </el-form-item>
-                                            <el-form-item 
+                                            <el-form-item
                                                 label="疗法封面:"
                                                 label-width="100px"
                                             >
-                                                <ImageUpload 
+                                                <ImageUpload
                                                     :disabled="true"
                                                     v-model="content.packageImgUrl"
                                                     type="image"
@@ -501,15 +501,73 @@
 
                                     </el-form-item>
 
+                                    <!-- 直播 -->
+                                    <el-form-item label="内容" v-else-if="[21].includes(Number(content.contentType))">
+                                        <el-card class="box-card">
+                                            <el-form-item
+                                                label="直播间:"
+                                                label-width="100px"
+                                                required
+                                            >
+                                                <el-select
+                                                    :disabled="!addForm.canOp"
+                                                    v-model="content.liveId"
+                                                    placeholder="请选择直播间"
+                                                    style="margin-right: 10px; width: 230px;"
+                                                    size="mini"
+                                                    filterable
+                                                    clearable
+                                                    @change="liveChangeUpdate(content)"
+                                                >
+                                                    <el-option
+                                                        v-for="dict in liveList"
+                                                        :key="dict.liveId"
+                                                        :label="dict.liveName"
+                                                        :value="parseInt(dict.liveId)"
+                                                    />
+                                                </el-select>
+                                            </el-form-item>
+                                            <el-form-item
+                                                label="标题:"
+                                                label-width="100px"
+                                            >
+                                                <el-input
+                                                    disabled
+                                                    type="textarea"
+                                                    :rows="2"
+                                                    maxlength="64"
+                                                    v-model="content.liveTitle"
+                                                    placeholder="请输入消息标题,最长为64字节"
+                                                    style="width: 90%;margin-top: 1%;"
+                                                />
+                                            </el-form-item>
+                                            <el-form-item
+                                                label="封面:"
+                                                label-width="100px"
+                                            >
+                                                <ImageUpload
+                                                    :disabled="true"
+                                                    v-model="content.liveImgUrl"
+                                                    type="image"
+                                                    :num="1"
+                                                    :file-size="1"
+                                                    :width="150"
+                                                    :height="150"
+                                                    style="margin-top: 1%;"
+                                                />
+                                            </el-form-item>
+                                        </el-card>
+                                    </el-form-item>
+
                                 </el-form>
                             </el-col>
 
-                            <el-col 
+                            <el-col
                                 :span="1"
                                 :offset="1"
                             >
                                 <!-- 删除内容节点 -->
-                                <i 
+                                <i
                                     class="el-icon-delete"
                                     @click="delContentNode(contentIndex)"
                                     style="margin-top: 20px;"
@@ -544,7 +602,7 @@
                         只作用于此客服
                     </div>
                 </el-form-item>
-            
+
             </el-form>
             <div slot="footer" class="dialog-footer">
                 <el-button type="primary" v-if="addForm.canOp" @click="submitForm">确 定</el-button>
@@ -575,6 +633,7 @@ import {
 } from "@/api/app/course/courseFinishTemp";
 import { courseList, videoList } from "@/api/app/course";
 import { getPackageOptions,} from "@/api/app/pkg";
+import { getLiveOptions,} from "@/api/app/live";
 import ImageUpload from "@/views/qw/sop/ImageUpload.vue";
 import CusRoleList from '@/views/app/user/CusRoleList.vue';
 import {listRole} from '@/api/app/user/userList';
@@ -598,6 +657,7 @@ export default {
             courseList: [],
             videoList: [],
             pkgList: [],
+            liveList: [],
             queryCourseList: [],
             queryVideoList: [],
             // 遮罩层
@@ -683,6 +743,8 @@ export default {
         this.queryCourseList = courseList.list;
         let defaultPkg = await this.getPkgList(null, null);
         this.pkgList = defaultPkg.data;
+        let defaultLive = await this.getLiveList();
+        this.liveList = defaultLive.data || [];
     },
     methods: {
         //--------------------------------------------------------------列表 begin---------------------------------------------------------
@@ -713,7 +775,7 @@ export default {
         },
         /**
          * 多选框选中数据
-         * @param selection 
+         * @param selection
          */
         handleSelectionChange(selection) {
             this.ids = selection.map((item) => item.id);
@@ -735,7 +797,7 @@ export default {
         },
         /**
          * 修改按钮操作
-         * @param row 
+         * @param row
          */
         async handleUpdate(row, canOp) {
             this.reset();
@@ -766,7 +828,7 @@ export default {
                         });
                     }
                 }
-                
+
             })
             if (info.data.appCustomerIds) {
                 let userInfo = await listRole({ids: info.data.appCustomerIds});
@@ -782,7 +844,7 @@ export default {
         },
         /**
          * 删除按钮操作
-         * @param row 
+         * @param row
          */
         handleDelete(row) {
             const ids = row.id || this.ids;
@@ -808,7 +870,7 @@ export default {
         },
         /**
          * 选中的用户列表
-         * @param list 
+         * @param list
          */
         customerSelectionChangeCallForBind(config) {
             let selected = config.selected;
@@ -874,7 +936,7 @@ export default {
         },
         /**
          * 校验数据
-         * @param data 
+         * @param data
          */
         checkData(data) {
 
@@ -934,8 +996,12 @@ export default {
                     this.$message.error("疗法不能为空")
                     return false;
                 }
+                if (21 == setting.contentType && this.isEmpty(setting.liveId)) {
+                    this.$message.error("直播间不能为空")
+                    return false;
+                }
             }
-    
+
             return true;
         },
         /**
@@ -966,7 +1032,7 @@ export default {
         },
         /**
          * 科普小节变化
-         * @param mode 模式,0-查询列表,1-新增编辑页 
+         * @param mode 模式,0-查询列表,1-新增编辑页
          */
         videoIdChange(mode) {
             if (mode == 1) {
@@ -994,8 +1060,8 @@ export default {
         },
         /**
          * 疗法下拉搜索
-         * @param keyword 
-         * @param content 
+         * @param keyword
+         * @param content
          */
         async handlePkgRemoteSearch(keyword, content) {
             content.meta.pkgLoading = true;
@@ -1006,7 +1072,7 @@ export default {
         },
         /**
          * 删除内容节点
-         * @param index 
+         * @param index
          */
         delContentNode(index){
             this.addForm.setting.splice(index, 1)
@@ -1036,7 +1102,7 @@ export default {
                 : null;
             for (let i = 0; i < this.addForm.setting.length; i++) {
                 let contentItem = this.addForm.setting[i];
-                
+
                 // 科普
                 if (Number(contentItem.contentType) === 9) {
                     // 处理科普链接标题/封面
@@ -1064,11 +1130,18 @@ export default {
                         });
                     }
                 }
+                // 直播:无 liveId 时清空标题封面
+                else if (Number(contentItem.contentType) === 21) {
+                    if (contentItem.liveId == null || contentItem.liveId === '') {
+                        this.$set(contentItem, 'liveTitle', null);
+                        this.$set(contentItem, 'liveImgUrl', null);
+                    }
+                }
             }
         },
         /**
          * 疗法变动处理
-         * @param content 
+         * @param content
          */
         async pkgChangeUpdate(content) {
             //将当前规则已选中的视频小节清空
@@ -1081,6 +1154,22 @@ export default {
                 this.$set(content, 'packageImgUrl', selectedPkg.imgUrl);
             }
         },
+        /**
+         * 直播间切换
+         * @param content
+         */
+        liveChangeUpdate(content) {
+            this.$set(content, 'liveTitle', null);
+            this.$set(content, 'liveImgUrl', null);
+            if (content.liveId == null || content.liveId === '') {
+                return;
+            }
+            const selectedLive = this.liveList.find(live => parseInt(live.liveId) === parseInt(content.liveId));
+            if (selectedLive) {
+                this.$set(content, 'liveTitle', selectedLive.liveName);
+                this.$set(content, 'liveImgUrl', selectedLive.liveImgUrl);
+            }
+        },
         /**
          * 打开选择客服的对话框
          */
@@ -1089,7 +1178,7 @@ export default {
         },
         /**
          * 删除客服
-         * @param list 
+         * @param list
          */
         handleCloseCustomer(node) {
             const index = this.customerConfig.selected.findIndex(t => t == node);
@@ -1174,8 +1263,8 @@ export default {
         },
         /**
          * 根据科普获取视频小节
-         * @param courseId 
-         * @param title 
+         * @param courseId
+         * @param title
          */
         async getVideoList(courseId, title) {
             return await videoList({
@@ -1194,9 +1283,15 @@ export default {
             };
             return await getPackageOptions(data);
         },
+        /**
+         * 获取直播间信息
+         */
+        async getLiveList() {
+            return await getLiveOptions();
+        },
         /**
          * 空校验
-         * @param obj 
+         * @param obj
          */
         isEmpty(obj) {
             return obj === null || obj === undefined || obj.length === 0;

+ 18 - 18
src/views/app/customerMember/index.vue

@@ -54,17 +54,17 @@
           @click="handleUpdate()"
         >修改</el-button>
       </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="danger"
-          plain
-          icon="el-icon-delete"
-          size="mini"
-          :disabled="multiple"
-          v-hasPermi="['app:customerMember:remove']"
-          @click="handleDelete()"
-        >删除</el-button>
-      </el-col>
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="danger"-->
+<!--          plain-->
+<!--          icon="el-icon-delete"-->
+<!--          size="mini"-->
+<!--          :disabled="multiple"-->
+<!--          v-hasPermi="['app:customerMember:remove']"-->
+<!--          @click="handleDelete()"-->
+<!--        >删除</el-button>-->
+<!--      </el-col>-->
       <el-col :span="1.5">
         <el-button
           type="warning"
@@ -114,13 +114,13 @@
             v-hasPermi="['app:customerMember:edit']"
             @click="handleUpdate(scope.row)"
           >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            v-hasPermi="['app:customerMember:remove']"
-            @click="handleDelete(scope.row)"
-          >删除</el-button>
+<!--          <el-button-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            icon="el-icon-delete"-->
+<!--            v-hasPermi="['app:customerMember:remove']"-->
+<!--            @click="handleDelete(scope.row)"-->
+<!--          >删除</el-button>-->
         </template>
       </el-table-column>
     </el-table>

+ 43 - 1
src/views/app/sop/info/index.vue

@@ -105,6 +105,20 @@
                     批量执行自动化
                 </el-button>
             </el-col>
+            <el-col :span="1.5">
+                <el-tooltip class="item" effect="dark"
+                    content="此功能用于给选中的SOP规则内【所有的】客户发送消息" placement="top">
+                    <el-button
+                        type="primary"
+                        plain
+                        icon="el-icon-s-promotion"
+                        size="mini"
+                        :disabled="multiple"
+                        @click="openGroupSendMessagePage"
+                    >SOP一键群发
+                    </el-button>
+                </el-tooltip>
+            </el-col>
             <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
 
@@ -410,6 +424,16 @@
             style="font-weight: bolder">
             <sopLogsDetails ref="sopLogsDetails" :rowDetailFrom="sopLogsDialog.sopLogsForm"></sopLogsDetails>
         </el-drawer>
+
+        <el-dialog title="SOP一键群发"
+            :visible.sync="groupSendMessageForm.visible" width="1000px" append-to-body :close-on-click-modal="false">
+            <group-send-message
+                :visible="groupSendMessageForm.visible"
+                :ids="groupSendMessageForm.ids"
+                :sourceType="2"
+                @changeGroupSendMessageVisible="changeGroupSendMessageVisible"
+            />
+        </el-dialog>
     </div>
 </template>
 
@@ -432,11 +456,12 @@ import {
 import {listSopTemp} from "@/api/app/sop/template";
 import ImageUpload from "@/views/app/sop/info/ImageUpload";
 import sopLogsDetails from '@/views/app/sop/userLogs/sopLogsList.vue'
+import groupSendMessage from "@/views/app/sop/userLogsInfo/groupSendMessage.vue";
 import { listTagGroupForUserBindTag, } from "@/api/app/tag/tagGroup";
 
 export default {
     name: "Sop",
-    components: { ImageUpload, sopLogsDetails },
+    components: { ImageUpload, sopLogsDetails, groupSendMessage },
     data() {
         return {
             miniAppList: [],
@@ -472,6 +497,10 @@ export default {
             ids: [],
             //选中的map
             selectionMap: {},
+            groupSendMessageForm: {
+                ids: [],
+                visible: false,
+            },
             myQwCompanyList: [],
             tempUser: {
                 open: false,
@@ -1158,6 +1187,19 @@ export default {
             this.single = selection.length !== 1
             this.multiple = !selection.length
         },
+        /**
+         * SOP一键群发
+         */
+        openGroupSendMessagePage() {
+            if (!this.ids || this.ids.length === 0) {
+                return this.$message.error('请勾选具体SOP规则!');
+            }
+            this.groupSendMessageForm.ids = this.ids.map(id => String(id));
+            this.groupSendMessageForm.visible = true;
+        },
+        changeGroupSendMessageVisible(visible) {
+            this.groupSendMessageForm.visible = visible;
+        },
         // 新增按钮操作
         handleAdd() {
             this.reset();

+ 3 - 3
src/views/app/sop/userLogs/sopUserLogsSchedule.vue

@@ -61,7 +61,7 @@
             size="medium"
             :disabled="multiple"
             @click="openGroupSendMessagePage"
-          >一键群发
+          >营期一键群发
           </el-button>
         </el-tooltip>
       </el-col>
@@ -102,7 +102,7 @@
       <i class="el-icon-info"></i>
       【天数】:【列表:营期时间】对应列表中的天数是几 就代表着 插件助手 会发送【任务模板】里的第几天的消息
     </div>
-    
+
     <el-table border v-loading="loading" :data="sopUserLogsList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="营期编号" align="center" prop="id" />
@@ -150,7 +150,7 @@
         :params="sopUserLogsInfoDialog.params"
       />
     </el-drawer>
-  
+
     <el-dialog title="批量修改客户营期" :visible.sync="updateLogsForm.visible" width="500px" append-to-body>
       <el-form ref="updateLogsForm" :model="updateLogsForm" :rules="updateLogsForm.rules" label-width="120px">
         <el-form-item label="选择营期时间" prop="paramTime">

+ 4 - 2
src/views/app/sop/userLogsInfo/groupSendMessage.vue

@@ -4,7 +4,9 @@
     <el-alert type="warning" :closable="false" show-icon>
       <template #title>
         <span style="font-size: 25px; line-height: 1.5;">
-          此功能用于给 选中的 营期 内【所有的】客户发送 消息
+          {{ sourceType === 2
+            ? '此功能用于给选中的SOP规则内【所有的】客户发送消息'
+            : '此功能用于给选中的营期内【所有的】客户发送消息' }}
         </span>
       </template>
     </el-alert>
@@ -904,7 +906,7 @@ export default {
       type: Array,
     },
     sourceType: {
-      default: 0,//0-营期的一键群发,1-营期详情的一键群发
+      default: 0,//0-营期的一键群发,1-营期详情的一键群发,2-SOP的一键群发
       type: Number,
     },
   },

+ 1 - 1
src/views/app/sop/userLogsInfo/sopUserLogsInfoDetails.vue

@@ -50,7 +50,7 @@
                         size="medium"
                         :disabled="multiple"
                         @click="openGroupSendMessagePage"
-                    >一键群发
+                    >客户一键群发
                     </el-button>
                 </el-tooltip>
             </el-col>

+ 32 - 32
src/views/app/tagGroup/index.vue

@@ -18,27 +18,27 @@
     </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="['vip:tagGroup:add']"
-          @click="handleAdd"
-        >新增</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="success"
-          plain
-          icon="el-icon-edit"
-          size="mini"
-          :disabled="single"
-          v-hasPermi="['vip:tagGroup:edit']"
-          @click="handleUpdate"
-        >修改</el-button>
-      </el-col>
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="primary"-->
+<!--          plain-->
+<!--          icon="el-icon-plus"-->
+<!--          size="mini"-->
+<!--          v-hasPermi="['vip:tagGroup:add']"-->
+<!--          @click="handleAdd"-->
+<!--        >新增</el-button>-->
+<!--      </el-col>-->
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="success"-->
+<!--          plain-->
+<!--          icon="el-icon-edit"-->
+<!--          size="mini"-->
+<!--          :disabled="single"-->
+<!--          v-hasPermi="['vip:tagGroup:edit']"-->
+<!--          @click="handleUpdate"-->
+<!--        >修改</el-button>-->
+<!--      </el-col>-->
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
@@ -53,17 +53,17 @@
             <el-tag type="success" v-for="i in scope.row.tags" :key="i.id" style="margin: 2px 5px 0 0;">{{i.name}}</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-        <template slot-scope="scope">
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-edit"
-            v-hasPermi="['vip:tagGroup:edit']"
-            @click="handleUpdate(scope.row)"
-          >修改</el-button>
-        </template>
-      </el-table-column>
+<!--      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">-->
+<!--        <template slot-scope="scope">-->
+<!--          <el-button-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            icon="el-icon-edit"-->
+<!--            v-hasPermi="['vip:tagGroup:edit']"-->
+<!--            @click="handleUpdate(scope.row)"-->
+<!--          >修改</el-button>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
     </el-table>
 
     <pagination

+ 183 - 35
src/views/app/user/CusRoleList.vue

@@ -3,13 +3,53 @@
     <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px"
       @submit.prevent="handleQuery">
 
-      <el-form-item label="销售名称" prop="memberName">
-        <el-input v-model="queryParams.memberName" placeholder="请输入销售名称" clearable size="small"
-          @keydown.enter.native="handleQueryEnter" />
+      <el-form-item label="销售公司" prop="companyId">
+        <el-select
+          v-model="queryParams.companyId"
+          placeholder="请选择或搜索销售公司"
+          clearable
+          filterable
+          size="small"
+          style="width: 220px"
+          :disabled="lockedCompany"
+          @change="handleCompanyChange"
+        >
+          <el-option
+            v-for="item in companyOptions"
+            :key="item.id"
+            :label="item.roleName"
+            :value="item.id"
+          />
+        </el-select>
       </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 label="销售" prop="salesId">
+        <el-select
+          v-model="queryParams.salesId"
+          placeholder="可全局搜,或先选公司再搜"
+          clearable
+          filterable
+          remote
+          reserve-keyword
+          :remote-method="remoteSearchSales"
+          :loading="salesLoading"
+          size="small"
+          style="width: 240px"
+          @focus="handleSalesFocus"
+          @clear="handleSalesClear"
+        >
+          <el-option
+            v-for="item in salesOptions"
+            :key="item.id"
+            :label="formatSalesLabel(item)"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否删除" prop="isDelete">
+        <el-select v-model="queryParams.isDelete" placeholder="全部" clearable size="small" style="width: 140px">
+          <el-option label="未删除" :value="0" />
+          <el-option label="已删除" :value="1" />
+        </el-select>
       </el-form-item>
 
       <el-form-item>
@@ -31,6 +71,12 @@
       <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="isDelete" width="90">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.isDelete === 1" type="danger" size="mini">已删除</el-tag>
+          <el-tag v-else type="success" size="mini">未删除</el-tag>
+        </template>
+      </el-table-column>
       <el-table-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip/>
     </el-table>
 
@@ -50,6 +96,7 @@
 
 <script>
 import { listRole } from "@/api/app/user/userList";
+import { listCustomerRoleOptions } from "@/api/app/customerRole";
 export default {
   name: "SelectedCustomer",
   props: {
@@ -90,33 +137,62 @@ export default {
     return {
       // 遮罩层
       loading: true,
+      salesLoading: false,
       // 总条数
       total: 0,
       // 客服成员表格数据
       roleList: [],
+      companyOptions: [],
+      salesOptions: [],
       // 查询参数
       queryParams: {
         pageNum: 1,
         pageSize: 10,
-        memberName: null,
-        roleName: null,
         companyId: null,
+        salesId: null,
+        isDelete: 0,
       },
     };
   },
+  computed: {
+    lockedCompany() {
+      return this.companyId !== null && this.companyId !== undefined && this.companyId !== '';
+    },
+  },
   created() {
     this.queryParams.pageNum = this.defaultPageNum;
     this.queryParams.pageSize = this.defaultPageSize;
     this.queryParams.companyId = this.companyId || null;
+    // 解绑预填指定销售时,包含已删除销售
+    if (this.ids && this.ids.length) {
+      this.queryParams.isDelete = null;
+    }
+    this.loadCompanyOptions();
     this.getList();
+    this.remoteSearchSales('');
   },
   watch: {
     selected: {
       immediate: true,
-      handler(nv) {
+      handler() {
         this.setSelectionStatus();
       }
     },
+    ids: {
+      deep: true,
+      handler(n, o) {
+        const next = (n || []).map(String).join(',');
+        const prev = (o || []).map(String).join(',');
+        if (next === prev) {
+          return;
+        }
+        this.queryParams.pageNum = 1;
+        this.queryParams.salesId = null;
+        this.queryParams.isDelete = (n && n.length) ? null : 0;
+        this.getList();
+        this.remoteSearchSales('');
+      }
+    },
     companyId: {
       handler(n) {
         this.reloadByCompany(n);
@@ -124,40 +200,117 @@ export default {
     }
   },
   methods: {
+    parseId(value) {
+      if (value === null || value === undefined || value === '') {
+        return null;
+      }
+      const num = Number(value);
+      return Number.isNaN(num) ? value : num;
+    },
+    formatSalesLabel(item) {
+      if (!item) {
+        return '';
+      }
+      const name = item.memberName || '';
+      const company = item.roleName ? `(${item.roleName})` : '';
+      return `${name}${company}`;
+    },
+    loadCompanyOptions() {
+      listCustomerRoleOptions().then(response => {
+        this.companyOptions = (response.rows || []).map(item => ({
+          ...item,
+          id: this.parseId(item.id)
+        }));
+      }).catch(() => {
+        this.companyOptions = [];
+      });
+    },
     /**
      * 按公司刷新列表(搜索页切换所属公司后调用)
      */
     reloadByCompany(companyId) {
       this.queryParams.companyId = companyId || null;
+      this.queryParams.salesId = null;
+      this.queryParams.pageNum = 1;
+      this.salesOptions = [];
+      this.remoteSearchSales('');
+      this.getList();
+    },
+    handleCompanyChange() {
+      this.queryParams.salesId = null;
       this.queryParams.pageNum = 1;
+      this.salesOptions = [];
+      this.remoteSearchSales('');
       this.getList();
     },
+    handleSalesFocus() {
+      if (!this.salesOptions.length) {
+        this.remoteSearchSales('');
+      }
+    },
+    handleSalesClear() {
+      this.remoteSearchSales('');
+    },
+    /**
+     * 远程搜索销售:可选先选公司再搜,也可不选公司全局搜
+     */
+    remoteSearchSales(keyword) {
+      this.salesLoading = true;
+      const qp = {
+        pageNum: 1,
+        pageSize: 50,
+        memberName: keyword || null,
+      };
+      const companyId = this.lockedCompany ? this.companyId : this.queryParams.companyId;
+      if (companyId) {
+        qp.companyId = companyId;
+      }
+      if (this.queryParams.isDelete !== '' && this.queryParams.isDelete !== undefined && this.queryParams.isDelete !== null) {
+        qp.isDelete = this.queryParams.isDelete;
+      }
+      listRole(qp).then(response => {
+        this.salesOptions = (response.rows || []).map(item => ({
+          ...item,
+          id: this.parseId(item.id)
+        }));
+      }).catch(() => {
+        this.salesOptions = [];
+      }).finally(() => {
+        this.salesLoading = false;
+      });
+    },
     /**
      * 查询客服成员列表
      */
     getList() {
       this.loading = true;
-      let qp = { ...this.queryParams };
-      if (this.companyId) {
-        qp.companyId = this.companyId;
-      } else if (!qp.companyId) {
-        delete qp.companyId;
+      let qp = {
+        pageNum: this.queryParams.pageNum,
+        pageSize: this.queryParams.pageSize,
+      };
+      const companyId = this.lockedCompany ? this.companyId : this.queryParams.companyId;
+      if (companyId) {
+        qp.companyId = companyId;
       }
-      if (this.ignoreIds && this.ignoreIds.length) {
-        qp['ignoreIds'] = this.ignoreIds;
-      } else {
-        delete qp.ignoreIds;
+      if (this.queryParams.isDelete !== '' && this.queryParams.isDelete !== undefined && this.queryParams.isDelete !== null) {
+        qp.isDelete = this.queryParams.isDelete;
       }
-      if (this.ids && this.ids.length) {
-        qp['ids'] = this.ids;
-      } else {
-        delete qp.ids;
+      // 选了具体销售则按销售id精确查;否则按公司(或全局)查列表
+      if (this.queryParams.salesId) {
+        qp.ids = [String(this.queryParams.salesId)];
+      } else if (this.ids && this.ids.length) {
+        qp.ids = this.ids;
+      }
+      if (this.ignoreIds && this.ignoreIds.length) {
+        qp.ignoreIds = this.ignoreIds;
       }
       listRole(qp).then(response => {
         this.roleList = response.rows;
         this.total = response.total;
         this.loading = false;
         this.setSelectionStatus();
+      }).catch(() => {
+        this.loading = false;
       });
     },
     // 取消按钮
@@ -188,11 +341,12 @@ export default {
       this.queryParams = {
         pageNum: this.defaultPageNum,
         pageSize: this.defaultPageSize,
-        memberName: null,
-        roleName: null,
         companyId: this.companyId || null,
+        salesId: null,
+        isDelete: (this.ids && this.ids.length) ? null : 0,
       }
-      this.resetForm("form");
+      this.salesOptions = [];
+      this.resetForm("queryForm");
     },
     /**
      * 确定选择
@@ -211,21 +365,15 @@ export default {
       this.queryParams.pageNum = 1;
       this.getList();
     },
-    handleQueryEnter(event) {
-      // 确保事件对象存在
-      if (event && event.preventDefault) {
-        event.preventDefault(); // 阻止默认提交行为
-      }
-      this.handleQuery();
-    },
     /**
      * 重置按钮操作
      */
     resetQuery() {
-      this.resetForm("queryForm");
-      this.queryParams.companyId = this.companyId || null;
+      this.reset();
+      this.remoteSearchSales('');
       this.handleQuery();
-    },    /**
+    },
+    /**
      * 单选
      * @param selection
      * @param row

+ 145 - 56
src/views/app/user/index.vue

@@ -126,26 +126,26 @@
           @click="handleExport"
         >导出</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: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="success"
@@ -166,26 +166,26 @@
         >批量解绑客服
         </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="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"
@@ -396,9 +396,22 @@
 
       <!-- 选择app客服角色 -->
     <el-dialog :title="bindAIConfig.title" :visible.sync="bindAIConfig.visible" width="1000px" append-to-body :close-on-click-modal="false">
+      <el-alert
+        v-if="bindAIConfig.type === 0"
+        type="error"
+        :closable="false"
+        show-icon
+        class="bind-customer-tip"
+      >
+        <template slot="title">
+          <div class="bind-customer-tip-title">一个客户最多只能绑定一个客服,多绑定不生效!!如需换绑请先解绑</div>
+        </template>
+      </el-alert>
       <CusRoleList
+        v-if="bindAIConfig.visible"
         ref="customerBindRef"
         :multiple="false"
+        :ids="bindAIConfig.presetIds"
         @confirm="customerSelectedConfirmCallForBind"
         @selectionChange="customerSelectionChangeCallForBind"
         :selected="bindAIConfig.selected"
@@ -525,6 +538,7 @@ import { getUser, delUser, addUser, updateUser } from "@/api/his/user";
 import {
   bindUserCusByIdsV2,
   unbindUserCusByIdsV2,
+  listBoundSales,
   listAppUser,
   exportAppUser
 } from '@/api/app/user';
@@ -637,6 +651,8 @@ export default {
         visible: false,
         enableFilter: false,
         selected: [],
+        // 解绑时预填:仅展示这些已绑定销售
+        presetIds: [],
       },
       bindAISearch: {
         visible: false,
@@ -1326,7 +1342,7 @@ export default {
      * @param enableFilter 是否开启过滤模式
      * @param type 0-绑定,1-解绑
      */
-    openbindAIPage(enableFilter = false, type = 1) {
+    async openbindAIPage(enableFilter = false, type = 1) {
       this.bindAIConfig.enableFilter = enableFilter;
       if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
         this.$message.closeAll();
@@ -1335,8 +1351,75 @@ export default {
       this.bindAIConfig.title = type == 0 ? '绑定客服' : '解绑客服';
       this.bindAIConfig.type = type;
       this.bindAIConfig.selected = [];
+      this.bindAIConfig.presetIds = [];
+
+      // 解绑:预填已绑定销售,便于在弹窗中直接选择
+      if (type == 1) {
+        let presetIds = [];
+        try {
+          if (enableFilter) {
+            const tmpParam = this.buildBindUnbindQueryParam();
+            const res = await listBoundSales(tmpParam);
+            presetIds = (res.data || []).map(id => String(id));
+          } else {
+            const idSet = this.ids.map(String);
+            const rows = (this.userList || []).filter(u => idSet.includes(String(u.userId)));
+            const salesSet = new Set();
+            rows.forEach(row => {
+              if (!row.customerRoleIds) {
+                return;
+              }
+              String(row.customerRoleIds).split(',').forEach(id => {
+                const trimmed = (id || '').trim();
+                if (trimmed) {
+                  salesSet.add(trimmed);
+                }
+              });
+            });
+            // 列表无绑定信息时再请求后端兜底
+            if (salesSet.size === 0) {
+              const res = await listBoundSales({
+                enableFilter: false,
+                userId: this.ids,
+              });
+              (res.data || []).forEach(id => salesSet.add(String(id)));
+            }
+            presetIds = Array.from(salesSet);
+          }
+        } catch (e) {
+          this.$message.closeAll();
+          return this.$message.error('获取已绑定销售失败');
+        }
+        if (!presetIds.length) {
+          this.$message.closeAll();
+          return this.$message.warning('所选用户未绑定客服');
+        }
+        this.bindAIConfig.presetIds = presetIds;
+      }
+
       this.bindAIConfig.visible = true;
     },
+    /**
+     * 组装绑定/解绑请求参数(筛选条件)
+     */
+    buildBindUnbindQueryParam() {
+      const tmpParam = {};
+      tmpParam['enableFilter'] = this.bindAIConfig.enableFilter;
+      tmpParam['userId'] = this.ids;
+      const queryParams = JSON.parse(JSON.stringify(this.queryParams));
+      delete queryParams['startIndex'];
+      delete queryParams['pageLimit'];
+      queryParams['tagIds'] = this.tagSearchConfig.selected.map(item => item.tagId);
+      queryParams['customerRoles'] = this.bindAISearch.selected.map(item => item.id);
+      if (queryParams.inviteDate) {
+        let dateRange = queryParams.inviteDate;
+        queryParams['inviteStartDate'] = dateRange[0] + ' 00:00:00';
+        queryParams['inviteEndDate'] = dateRange[1] + ' 23:59:59';
+        delete queryParams['inviteDate']
+      }
+      tmpParam['fsUserParam'] = queryParams;
+      return tmpParam;
+    },
     /**
      * 绑定客服页面-选中客服
      */
@@ -1362,22 +1445,8 @@ export default {
         this.$message.closeAll();
         return this.$message.warning('请选择一个客服');
       }
-      let tmpParam = {};
-      tmpParam['enableFilter'] = this.bindAIConfig.enableFilter;
+      let tmpParam = this.buildBindUnbindQueryParam();
       tmpParam['roleId'] = this.bindAIConfig.selected.map(item => item.id) || [];
-      tmpParam['userId'] = this.ids;
-      const queryParams = JSON.parse(JSON.stringify(this.queryParams));
-      delete queryParams['startIndex'];
-      delete queryParams['pageLimit'];
-      queryParams['tagIds'] = this.tagSearchConfig.selected.map(item => item.tagId);
-      queryParams['customerRoles'] = this.bindAISearch.selected.map(item => item.id);
-      if (queryParams.inviteDate) {
-        let dateRange = queryParams.inviteDate;
-        queryParams['inviteStartDate'] = dateRange[0] + ' 00:00:00';
-        queryParams['inviteEndDate'] = dateRange[1] + ' 23:59:59';
-        delete queryParams['inviteDate']
-      }
-      tmpParam['fsUserParam'] = queryParams;
       let res;
       if (this.bindAIConfig.type == 0) {
         res = await bindUserCusByIdsV2(tmpParam);
@@ -1389,6 +1458,7 @@ export default {
       this.getList();
       this.bindAIConfig.visible = false;
       this.bindAIConfig.selected = [];
+      this.bindAIConfig.presetIds = [];
     },
   }
 };
@@ -1433,4 +1503,23 @@ export default {
 ::v-deep  .ai-user-info .el-form-item__label {
   text-align: right !important;
 }
+
+.bind-customer-tip {
+  margin-bottom: 16px;
+  padding: 18px 20px !important;
+  border: 2px solid #f56c6c !important;
+  border-radius: 6px;
+  background-color: #fef0f0 !important;
+}
+.bind-customer-tip-title {
+  font-size: 20px;
+  font-weight: 700;
+  line-height: 1.6;
+  color: #f56c6c;
+  letter-spacing: 1px;
+}
+::v-deep .bind-customer-tip .el-alert__icon {
+  font-size: 28px;
+  width: 28px;
+}
 </style>

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

@@ -363,57 +363,57 @@
                 </div>
             </el-form>
             <!-- 直播 -->
-<!--            <el-form v-else-if="welcomeItem.contentType == '21'" ref="liveForm" :model="liveForm" :rules="liveFormRule" label-width="110px">-->
-<!--                <div>-->
-<!--                    <el-card class="box-card">-->
-<!--                        <el-form-item label="直播间"-->
-<!--                            required-->
-<!--                            style="margin-bottom: 5px;"-->
-<!--                        >-->
-<!--                            <el-select-->
-<!--                                v-model="liveForm.liveId"-->
-<!--                                placeholder="请选择直播间"-->
-<!--                                size="mini"-->
-<!--                                filterable-->
-<!--                                @change="liveChange()"-->
-<!--                            >-->
-<!--                            <el-option-->
-<!--                                v-for="dict in liveList"-->
-<!--                                :key="dict.liveId"-->
-<!--                                :label="dict.liveName"-->
-<!--                                :value="parseInt(dict.liveId)"-->
-<!--                            />-->
-<!--                            </el-select>-->
-<!--                        </el-form-item>-->
-<!--                        <el-form-item label="标题"-->
-<!--                            prop="liveTitle"-->
-<!--                            style="margin-bottom: 5px;"-->
-<!--                        >-->
-<!--                            <el-input-->
-<!--                                v-model="liveForm.liveTitle"-->
-<!--                                placeholder="请输入消息标题,最长为64字节"-->
-<!--                                :rows="2"-->
-<!--                                maxlength="64"-->
-<!--                                type="textarea"-->
-<!--                                disabled-->
-<!--                                @input="checkByteLength(liveForm, 'liveTitle')"-->
-<!--                            />-->
-<!--                        </el-form-item>-->
-<!--                        <el-form-item label="封面"-->
-<!--                            prop="liveImgUrl"-->
-<!--                        >-->
-<!--                            <ImageUpload-->
-<!--                                v-model="liveForm.liveImgUrl"-->
-<!--                                type="image"-->
-<!--                                :num="10"-->
-<!--                                :width="150"-->
-<!--                                :height="150"-->
-<!--                                disabled-->
-<!--                            />-->
-<!--                        </el-form-item>-->
-<!--                    </el-card>-->
-<!--                </div>-->
-<!--            </el-form>-->
+            <el-form v-else-if="welcomeItem.contentType == '21'" ref="liveForm" :model="liveForm" :rules="liveFormRule" label-width="110px">
+                <div>
+                    <el-card class="box-card">
+                        <el-form-item label="直播间"
+                            required
+                            style="margin-bottom: 5px;"
+                        >
+                            <el-select
+                                v-model="liveForm.liveId"
+                                placeholder="请选择直播间"
+                                size="mini"
+                                filterable
+                                @change="liveChange()"
+                            >
+                            <el-option
+                                v-for="dict in liveList"
+                                :key="dict.liveId"
+                                :label="dict.liveName"
+                                :value="parseInt(dict.liveId)"
+                            />
+                            </el-select>
+                        </el-form-item>
+                        <el-form-item label="标题"
+                            prop="liveTitle"
+                            style="margin-bottom: 5px;"
+                        >
+                            <el-input
+                                v-model="liveForm.liveTitle"
+                                placeholder="请输入消息标题,最长为64字节"
+                                :rows="2"
+                                maxlength="64"
+                                type="textarea"
+                                disabled
+                                @input="checkByteLength(liveForm, 'liveTitle')"
+                            />
+                        </el-form-item>
+                        <el-form-item label="封面"
+                            prop="liveImgUrl"
+                        >
+                            <ImageUpload
+                                v-model="liveForm.liveImgUrl"
+                                type="image"
+                                :num="10"
+                                :width="150"
+                                :height="150"
+                                disabled
+                            />
+                        </el-form-item>
+                    </el-card>
+                </div>
+            </el-form>
             <div slot="footer" class="dialog-footer" style="text-align: center">
                 <el-button type="primary" @click="confirmUpload">确定</el-button>
                 <el-button @click="cancelUpload">取消</el-button>
@@ -429,7 +429,7 @@ import ImageUpload from "@/views/qw/sop/ImageUpload";
 import CusRoleList from '@/views/app/user/CusRoleList.vue';
 import {courseList, videoList} from "@/api/app/course";
 import { getPackageOptions,} from "@/api/app/pkg";
-// import { getLiveOptions,} from "@/api/app/live";
+import { getLiveOptions,} from "@/api/app/live";
 
 export default {
     name: "AppWelcome",
@@ -527,8 +527,8 @@ export default {
         this.getList();
         let defaultPkg = await this.getPkgList(null, null);
         this.pkgList = defaultPkg.data;
-        // let defaultLive = await this.getLiveList();
-        // this.liveList = defaultLive.data;
+        let defaultLive = await this.getLiveList();
+        this.liveList = defaultLive.data;
     },
     methods: {
         /**
@@ -557,9 +557,9 @@ export default {
         /**
          * 获取直播间信息
          */
-        // async getLiveList() {
-        //     return await getLiveOptions();
-        // },
+        async getLiveList() {
+            return await getLiveOptions();
+        },
         /**
          * 疗法下拉搜索
          * @param keyword
@@ -584,18 +584,18 @@ export default {
                 this.pkgForm.packageImgUrl = selectedPkg.imgUrl;
             }
         },
-        // /**
-        //  * 直播间切换
-        //  */
-        // liveChange() {
-        //     this.liveForm.liveTitle = null;
-        //     this.liveForm.liveImgUrl = null;
-        //     const selectedLive = this.liveList.find(live => live.liveId === this.liveForm.liveId);
-        //     if (selectedLive) {
-        //         this.liveForm.liveTitle = selectedLive.liveName; // 自动填充标题
-        //         this.liveForm.liveImgUrl = selectedLive.liveImgUrl; // 自动填充封面
-        //     }
-        // },
+        /**
+         * 直播间切换
+         */
+        liveChange() {
+            this.liveForm.liveTitle = null;
+            this.liveForm.liveImgUrl = null;
+            const selectedLive = this.liveList.find(live => live.liveId === this.liveForm.liveId);
+            if (selectedLive) {
+                this.liveForm.liveTitle = selectedLive.liveName; // 自动填充标题
+                this.liveForm.liveImgUrl = selectedLive.liveImgUrl; // 自动填充封面
+            }
+        },
         /**
          * 搜索条件:客服选中确认回调
          */

+ 6 - 16
src/views/course/courseWatchLog/indexApp.vue

@@ -32,24 +32,14 @@
           </el-option>
         </el-select>
       </el-form-item>
-      <el-form-item label="科普" prop="courseId">
-        <el-select filterable  v-model="queryParams.courseId" placeholder="请选择科普"  clearable size="small" @change="courseChange(queryParams.courseId)">
+      <el-form-item label="课程" prop="courseId">
+        <el-select filterable  v-model="queryParams.courseId" placeholder="请选择课程"  clearable size="small" @change="courseChange(queryParams.courseId)">
           <el-option
             v-for="dict in courseLists"
             :key="dict.dictValue"
-            :label="dict.remark"
+            :label="(dict.remark || dict.dictLabel) + '(' + dict.dictValue + ')'"
             :value="dict.dictValue"
           >
-          <!-- <el-tooltip
-        v-if="dict.remark"
-        :content="dict.remark"
-        placement="right"
-        effect="dark"
-        :disabled="!dict.remark"
-      >
-        <span>{{ dict.dictLabel }}</span>
-      </el-tooltip>
-      <span v-else>{{ dict.dictLabel }}</span> -->
         </el-option>
         </el-select>
       </el-form-item>
@@ -203,7 +193,7 @@
           </div>
         </template>
       </el-table-column>
-      <el-table-column label="科普名称" align="center" prop="courseName" />
+      <el-table-column label="课程名称" align="center" prop="courseName" />
       <el-table-column label="小节名称" align="center" prop="videoName" />
       <el-table-column label="记录类型" align="center" prop="logType">
         <template slot-scope="scope">
@@ -741,7 +731,7 @@ export default {
     handleAdd() {
       this.reset();
       this.open = true;
-      this.title = "添加短链科普学习记录";
+      this.title = "添加短链课程学习记录";
     },
    
     /** 提交按钮 */
@@ -769,7 +759,7 @@ export default {
     handleExport() {
       const that = this
       const queryParams = this.queryParams;
-      this.$confirm('是否确认导出所有短链科普学习记录数据项?', "警告", {
+      this.$confirm('是否确认导出所有短链课程学习记录数据项?', "警告", {
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"

+ 74 - 30
src/views/course/videoResource/index.vue

@@ -327,7 +327,7 @@
                       ></el-progress>
                       <span class="line-status-text">{{ Math.round(currentUploadProgress.line1) }}%</span>
                     </div>
-                    <div class="line-progress-item">
+                    <div v-if="!isDdgy" class="line-progress-item">
                       <span class="line-label">线路2:</span>
                       <el-progress
                         :percentage="currentUploadProgress.line2"
@@ -502,7 +502,7 @@
                   ></el-progress>
                   <span class="line-percentage">{{ Math.round(scope.row.uploadDetails.line1 || 0) }}%</span>
                 </div>
-                <div class="line-progress-row">
+                <div v-if="!isDdgy" class="line-progress-row">
                   <span class="line-label">线路2:</span>
                   <el-progress
                     :percentage="scope.row.uploadDetails.line2 || 0"
@@ -1017,7 +1017,9 @@ export default {
       isProcessingBatch: false, // 是否正在处理批次
       currentBatchIndex: 0, // 当前批次索引
       uploadCancellationTokens: new Map(), // Store cancellation functions by video ID
-      currentProject: process.env.VUE_APP_PROJECT
+      currentProject: process.env.VUE_APP_PROJECT,
+      // 叮当国医项目标识:该项目的视频不需要上传线路2
+      isDdgy: process.env.VUE_APP_PROJECT_CODE === 'ddgy' || process.env.VUE_APP_PROJECT_CODE === "'ddgy'"
     }
   },
   watch: {
@@ -1538,6 +1540,14 @@ export default {
     updateUploadProgress(key, value) {
       this.currentUploadProgress[key] = value;
 
+      // 叮当国医项目只上传线路1
+      if (this.isDdgy) {
+        this.currentUploadProgress.total = this.currentUploadProgress.line1Status === 'success'
+          ? 100
+          : Math.min(this.currentUploadProgress.line1, 99);
+        return;
+      }
+
       // 计算总进度:只有两个线路都成功才算100%
       if (this.currentUploadProgress.line1Status === 'success' && this.currentUploadProgress.line2Status === 'success') {
         this.currentUploadProgress.total = 100;
@@ -1566,20 +1576,37 @@ export default {
 
       try {
         await this.getFirstThumbnail(file, this.form);
-        const [line1Result, line2Result] = await Promise.allSettled([
-          this.uploadVideoToTxPcdn(file, this.form, options.onProgress),
-          //this.uploadVideoToHwObs(file, this.form, options.onProgress)
-          this.uploadVideoToHsy(file, this.form, options.onProgress)
-        ]);
 
-        const line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
-        const line2Success = line2Result.status === 'fulfilled' && line2Result.value.success;
+        let line1Success = false;
+        let line2Success = false;
 
-        if (line1Success && line2Success) {
+        if (this.isDdgy) {
+          // 叮当国医项目只上传线路1
+          const [line1Result] = await Promise.allSettled([
+            this.uploadVideoToTxPcdn(file, this.form, options.onProgress)
+          ]);
+          line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
+        } else {
+          const [line1Result, line2Result] = await Promise.allSettled([
+            this.uploadVideoToTxPcdn(file, this.form, options.onProgress),
+            //this.uploadVideoToHwObs(file, this.form, options.onProgress)
+            this.uploadVideoToHsy(file, this.form, options.onProgress)
+          ]);
+          line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
+          line2Success = line2Result.status === 'fulfilled' && line2Result.value.success;
+        }
+
+        if (line1Success && (this.isDdgy || line2Success)) {
           this.form.uploadStatus = 'success';
-          this.form.uploadProgress = { total: 100, line1: 100, line2: 100, line1Status: 'success', line2Status: 'success' };
+          this.form.uploadProgress = {
+            total: 100,
+            line1: 100,
+            line2: this.isDdgy ? 0 : 100,
+            line1Status: 'success',
+            line2Status: this.isDdgy ? 'pending' : 'success'
+          };
           this.currentUploadProgress.total = 100;
-          this.$message.success("视频上传完成!两个线路都上传成功");
+          this.$message.success(this.isDdgy ? "视频上传完成!" : "视频上传完成!两个线路都上传成功");
         } else {
           this.form.uploadStatus = 'failed';
           this.form.uploadProgress = {
@@ -1591,7 +1618,7 @@ export default {
           };
           const failedLines = [];
           if (!line1Success) failedLines.push('线路1');
-          if (!line2Success) failedLines.push('线路2');
+          if (!this.isDdgy && !line2Success) failedLines.push('线路2');
           this.$message.error(`视频上传失败!${failedLines.join('、')} 上传失败,请重试`);
         }
         this.form.fileName = file.name;
@@ -2002,36 +2029,44 @@ export default {
         // 获取封面
         await this.getFirstThumbnail(tempVideo.file, tempVideo);
 
-        // 并行上传到两个服务器
-        const [line1Result, line2Result] = await Promise.allSettled([
-          this.uploadVideoToTxPcdnBatch(tempVideo.file, tempVideo),
-          // this.uploadVideoToHwObsBatch(tempVideo.file, tempVideo)
-          this.uploadVideoToHSYBatch(tempVideo.file, tempVideo),
-
-        ]);
+        let line1Success = false;
+        let line2Success = false;
 
-        // 检查上传结果
-        const line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
-        const line2Success = line2Result.status === 'fulfilled' && line2Result.value.success;
+        if (this.isDdgy) {
+          // 叮当国医项目只上传线路1
+          const [line1Result] = await Promise.allSettled([
+            this.uploadVideoToTxPcdnBatch(tempVideo.file, tempVideo)
+          ]);
+          line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
+        } else {
+          // 并行上传到两个服务器
+          const [line1Result, line2Result] = await Promise.allSettled([
+            this.uploadVideoToTxPcdnBatch(tempVideo.file, tempVideo),
+            // this.uploadVideoToHwObsBatch(tempVideo.file, tempVideo)
+            this.uploadVideoToHSYBatch(tempVideo.file, tempVideo),
+          ]);
+          line1Success = line1Result.status === 'fulfilled' && line1Result.value.success;
+          line2Success = line2Result.status === 'fulfilled' && line2Result.value.success;
+        }
 
         const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
         if (index !== -1) {
-          if (line1Success && line2Success) {
+          if (line1Success && (this.isDdgy || line2Success)) {
             this.videoList[index].progress = 100;
             this.videoList[index].uploadStatus = 'success';
             this.videoList[index].uploadDetails.line1Status = 'success';
-            this.videoList[index].uploadDetails.line2Status = 'success';
+            this.videoList[index].uploadDetails.line2Status = this.isDdgy ? 'pending' : 'success';
             this.videoList[index].uploadDetails.line1 = 100;
-            this.videoList[index].uploadDetails.line2 = 100;
+            this.videoList[index].uploadDetails.line2 = this.isDdgy ? 0 : 100;
           } else {
             this.videoList[index].uploadStatus = 'failed';
             this.videoList[index].uploadDetails.line1Status = line1Success ? 'success' : 'failed';
-            this.videoList[index].uploadDetails.line2Status = line2Success ? 'success' : 'failed';
+            this.videoList[index].uploadDetails.line2Status = this.isDdgy ? 'pending' : (line2Success ? 'success' : 'failed');
             this.updateBatchProgress(index);
           }
         }
 
-        return { success: line1Success && line2Success, tempVideo };
+        return { success: line1Success && (this.isDdgy || line2Success), tempVideo };
       } catch (error) {
         const index = this.videoList.findIndex(item => item.tempId === tempVideo.tempId);
         if (index !== -1) {
@@ -2382,6 +2417,15 @@ export default {
         const item = this.videoList[index];
         const line1Progress = item.uploadDetails.line1 || 0;
         const line2Progress = item.uploadDetails.line2 || 0;
+
+        // 叮当国医项目只上传线路1
+        if (this.isDdgy) {
+          item.progress = item.uploadDetails.line1Status === 'success'
+            ? 100
+            : Math.min(line1Progress, 99);
+          return;
+        }
+
         // 只有两个线路都成功才算100%
         if (item.uploadDetails.line1Status === 'success' && item.uploadDetails.line2Status === 'success') {
           item.progress = 100;
@@ -2464,7 +2508,7 @@ export default {
 
       // 检查哪些线路需要重试
       const needRetryLine1 = uploadDetails.line1Status === 'failed' || uploadDetails.line1Status === 'pending';
-      const needRetryLine2 = uploadDetails.line2Status === 'failed' || uploadDetails.line2Status === 'pending';
+      const needRetryLine2 = this.isDdgy ? false : (uploadDetails.line2Status === 'failed' || uploadDetails.line2Status === 'pending');
 
       if (!needRetryLine1 && !needRetryLine2) {
         this.$message.info('所有线路都已上传成功,无需重试');

+ 61 - 15
src/views/live/liveWatchUser/index.vue

@@ -1,23 +1,40 @@
 <template>
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="88px">
-      <el-form-item label="直播ID" prop="liveId">
-        <el-input
-          v-model="queryParams.liveId"
-          placeholder="请输入直播ID"
+      <el-form-item label="直播组" prop="liveGroupType">
+        <el-select
+          v-model="queryParams.liveGroupType"
+          placeholder="请选择直播组"
+          filterable
           clearable
           size="small"
-          @keyup.enter.native="handleQuery"
-        />
+          style="width: 200px"
+          @change="handleLiveGroupChange"
+        >
+          <el-option
+            v-for="item in liveGroupTypeList"
+            :key="item.id"
+            :label="item.liveGroupType"
+            :value="item.id"
+          />
+        </el-select>
       </el-form-item>
-      <el-form-item label="直播名称" prop="liveName">
-        <el-input
-          v-model="queryParams.liveName"
-          placeholder="请输入直播名称"
+      <el-form-item label="直播名称" prop="liveId">
+        <el-select
+          v-model="queryParams.liveId"
+          placeholder="请选择直播"
+          filterable
           clearable
           size="small"
-          @keyup.enter.native="handleQuery"
-        />
+          style="width: 240px"
+        >
+          <el-option
+            v-for="item in liveOptions"
+            :key="item.liveId"
+            :label="(item.liveName || '') + '(' + item.liveId + ')'"
+            :value="item.liveId"
+          />
+        </el-select>
       </el-form-item>
       <el-form-item label="用户ID" prop="userId">
         <el-input
@@ -370,6 +387,7 @@ import { listCourseCouponUserListVO } from '@/api/his/courseCouponUser'
 import { listTagGroupForUserBindTag } from '@/api/app/tag/tagGroup'
 import { bindTagV2, unbindTagV2 } from '@/api/app/tag/bindTag'
 import { batchUpdateFriendRemark } from '@/api/course/courseWatchLog'
+import { getLiveGroupTypeList, getLiveOptions } from '@/api/app/live'
 
 export default {
   name: 'LiveWatchUser',
@@ -381,13 +399,15 @@ export default {
       list: [],
       ids: [],
       companyList: [],
+      liveGroupTypeList: [],
+      liveOptions: [],
       liveRewardTypeList: [],
       dateRange: [],
       queryParams: {
         pageNum: 1,
         pageSize: 10,
+        liveGroupType: null,
         liveId: null,
-        liveName: null,
         userId: null,
         userName: null,
         companyId: null,
@@ -445,6 +465,8 @@ export default {
       this.liveRewardTypeList = res.data || []
     })
     this.loadCompanyList()
+    this.loadLiveGroupTypeList()
+    this.loadLiveOptions()
     this.getList()
   },
   methods: {
@@ -708,6 +730,28 @@ export default {
         this.companyList = res.data || []
       })
     },
+    loadLiveGroupTypeList() {
+      getLiveGroupTypeList({ pageNum: 1, pageSize: 999 }).then(res => {
+        this.liveGroupTypeList = res.rows || res.data || []
+      }).catch(() => {
+        this.liveGroupTypeList = []
+      })
+    },
+    loadLiveOptions(liveGroupType) {
+      const params = {}
+      if (liveGroupType != null && liveGroupType !== '') {
+        params.liveGroupType = liveGroupType
+      }
+      getLiveOptions(params).then(res => {
+        this.liveOptions = res.data || []
+      }).catch(() => {
+        this.liveOptions = []
+      })
+    },
+    handleLiveGroupChange(val) {
+      this.queryParams.liveId = null
+      this.loadLiveOptions(val)
+    },
     buildQuery() {
       const params = { ...this.queryParams }
       if (this.dateRange && this.dateRange.length === 2) {
@@ -717,14 +761,13 @@ export default {
         params.bTime = null
         params.eTime = null
       }
-      ;['liveId', 'userId', 'companyId', 'companyUserId'].forEach(key => {
+      ;['liveId', 'userId', 'companyId', 'companyUserId', 'liveGroupType'].forEach(key => {
         if (params[key] === '' || params[key] === undefined) {
           params[key] = null
         } else if (params[key] != null) {
           params[key] = Number(params[key])
         }
       })
-      if (params.liveName === '') params.liveName = null
       if (params.userName === '') params.userName = null
       return params
     },
@@ -746,6 +789,9 @@ export default {
       this.resetForm('queryForm')
       this.queryParams.pageNum = 1
       this.queryParams.pageSize = 10
+      this.queryParams.liveGroupType = null
+      this.queryParams.liveId = null
+      this.loadLiveOptions()
       this.handleQuery()
     },
     formatDuration(seconds) {

+ 5 - 1
src/views/qw/conversationNew/ConversationNew.vue

@@ -28,6 +28,9 @@
       <el-tab-pane label="按聊天内容查询" name="content">
         <ContentSearchTab :corpId="selectedCorpId" />
       </el-tab-pane>
+      <el-tab-pane label="按客户查询" name="customer">
+        <CustomerQueryTab :corpId="selectedCorpId" />
+      </el-tab-pane>
     </el-tabs>
     <div v-else class="empty-tip">请先选择企业微信主体</div>
   </div>
@@ -37,10 +40,11 @@
 import { allCorp } from "@/api/qw/qwCompany";
 import EmployeeQueryTab from "./EmployeeQueryTab.vue";
 import ContentSearchTab from "./ContentSearchTab.vue";
+import CustomerQueryTab from "./CustomerQueryTab.vue";
 
 export default {
   name: "ConversationNew",
-  components: { EmployeeQueryTab, ContentSearchTab },
+  components: { EmployeeQueryTab, ContentSearchTab, CustomerQueryTab },
   data() {
     return {
       activeTab: "employee",

+ 83 - 7
src/views/qw/conversationNew/ConversationPanelPure.vue

@@ -35,8 +35,19 @@
       <i class="el-icon-info"></i>
       <p>暂无会话数据</p>
     </div>
-    <!-- 聊天窗口 -->
-    <div v-else id="chat-container" ref="chatContainer"></div>
+    <!-- 聊天窗口(含消息类型筛选) -->
+    <div v-else class="chat-wrapper">
+      <div class="msg-type-filter">
+        <span
+          v-for="opt in msgTypeOptions"
+          :key="String(opt.value)"
+          class="filter-item"
+          :class="{ active: msgTypeFilter === opt.value }"
+          @click="handleFilterChange(opt.value)"
+        >{{ opt.label }}</span>
+      </div>
+      <div id="chat-container" ref="chatContainer"></div>
+    </div>
   </div>
 </template>
 
@@ -49,6 +60,17 @@ window._QW_CONFIG_CACHE = window._QW_CONFIG_CACHE || new Map();
 window._QW_CONFIG_PENDING = window._QW_CONFIG_PENDING || new Map();
 const CACHE_TTL = 5 * 60 * 1000;
 
+// 消息类型筛选选项(value 为 msgtype,null 表示全部)
+const MSG_TYPE_FILTER_OPTIONS = [
+  { value: null, label: '全部' },
+  { value: 1, label: '文本' },
+  { value: 6, label: '语音' },
+  { value: 2, label: '图片' },
+  { value: 7, label: '视频' },
+  { value: 4, label: '链接' },
+  { value: 22, label: '语音通话' }
+];
+
 function getCachedConfig(corpId) {
   if (!corpId) return null;
   const cached = window._QW_CONFIG_CACHE.get(corpId);
@@ -77,6 +99,8 @@ export default {
       isLoggedIn: false,
       isReady: false,
       msgList: [],
+      msgTypeFilter: null,
+      msgTypeOptions: MSG_TYPE_FILTER_OPTIONS,
       cursor: '',
       hasMore: true,
       loadingMore: false,
@@ -93,6 +117,12 @@ export default {
       _loginPanelTimer: null
     };
   },
+  computed: {
+    filteredMsgList() {
+      if (!this.msgTypeFilter) return this.msgList;
+      return this.msgList.filter(m => m.msgtype === this.msgTypeFilter);
+    }
+  },
   watch: {
     customerId(newId, oldId) {
       if (newId && newId !== oldId && this.isLoggedIn && (this.staffUserId || this.chatId) && this.configReady && !this.configError) {
@@ -432,7 +462,7 @@ export default {
           this.msgList.push(...processed);
           this.cursor = data.next_cursor || '';
           this.hasMore = data.has_more === 1;
-          if (this.chatInstance) this.chatInstance.setData({ msgList: this.msgList });
+          if (this.chatInstance) this.chatInstance.setData({ msgList: this.filteredMsgList });
           else await this.renderOrUpdateChat();
         } else {
           this.hasMore = false;
@@ -465,12 +495,25 @@ export default {
         }
       })
     },
+
+    // 消息类型筛选
+    handleFilterChange(type) {
+      this.msgTypeFilter = type;
+      if (this.chatInstance) {
+        this.chatInstance.setData({
+          msgList: this.filteredMsgList,
+          hasMore: this.hasMore,
+          loadingMore: this.loadingMore
+        });
+      }
+    },
+
     async renderOrUpdateChat() {
       if (this.msgList.length === 0 || this.configError) return;
       const container = document.getElementById('chat-container');
       if (!container) return;
       if (this.chatInstance) {
-        this.chatInstance.setData({ msgList: this.msgList });
+        this.chatInstance.setData({ msgList: this.filteredMsgList });
         return;
       }
       const ok = await this.initSDKOnce();
@@ -563,7 +606,7 @@ export default {
           }
         `,
         data: {
-          msgList: this.msgList,
+          msgList: this.filteredMsgList,
           hasMore: this.hasMore,
           loadingMore: this.loadingMore
         },
@@ -649,6 +692,37 @@ export default {
   background: #f5f6f7;
   position: relative;
 }
+.chat-wrapper {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.msg-type-filter {
+  flex-shrink: 0;
+  display: flex;
+  gap: 4px;
+  padding: 8px 12px;
+  background: #fff;
+  border-bottom: 1px solid #e8e8e8;
+  flex-wrap: wrap;
+}
+.filter-item {
+  padding: 4px 12px;
+  font-size: 13px;
+  color: #606266;
+  cursor: pointer;
+  border-radius: 14px;
+  user-select: none;
+}
+.filter-item:hover {
+  color: #409eff;
+}
+.filter-item.active {
+  background: #409eff;
+  color: #fff;
+}
 .login-area, .empty-tip, .loading-tip {
   display: flex;
   flex-direction: column;
@@ -672,8 +746,8 @@ export default {
 }
 #chat-container {
   flex: 1;
+  min-height: 0;
   width: 100%;
-  height: 100%;
   background: #f0f2f5;
 }
 .login-container {
@@ -686,7 +760,9 @@ export default {
 </style>
 
 <style>
-#chat-container,
+#chat-container {
+  width: 100% !important;
+}
 #chat-container > div,
 #chat-container iframe {
   width: 100% !important;

+ 244 - 0
src/views/qw/conversationNew/CustomerQueryTab.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="customer-query-tab">
+    <!-- 查询表单 -->
+    <div class="query-form">
+      <el-form :model="queryParams" inline>
+        <el-form-item label="微信昵称">
+          <el-input v-model="queryParams.name" placeholder="请输入微信昵称" clearable />
+        </el-form-item>
+        <el-form-item label="微信备注">
+          <el-input v-model="queryParams.remark" placeholder="请输入微信备注" clearable />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">查询</el-button>
+          <el-button @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 客户列表(后端分页) -->
+    <el-table
+      v-loading="tableLoading"
+      :data="customerList"
+      border
+      stripe
+      style="width: 100%"
+    >
+      <el-table-column prop="externalUserId" label="客户ID" min-width="200" type="hidden" />
+      <el-table-column prop="name" label="微信昵称" min-width="120" />
+      <el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
+      <el-table-column prop="staffCount" label="关联客服数" width="100" />
+      <el-table-column label="操作" width="100">
+        <template slot-scope="scope">
+          <el-button type="text" @click="openDrawer(scope.row)">查看会话</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页组件 -->
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getCustomerList"
+    ></pagination>
+
+    <!-- 抽屉:客服列表 + 会话详情 -->
+    <el-drawer
+      :visible.sync="drawerVisible"
+      :title="(currentCustomer && currentCustomer.name ? currentCustomer.name : '') + ' 的会话'"
+      direction="rtl"
+      size="80%"
+      destroy-on-close
+    >
+      <div class="drawer-container">
+        <div class="staff-list">
+          <el-table
+            :data="staffList"
+            v-loading="staffLoading"
+            height="100%"
+            highlight-current-row
+            @row-click="handleStaffClick"
+          >
+            <el-table-column prop="qwUserName" label="客服" />
+            <el-table-column prop="remark" label="备注" show-overflow-tooltip />
+          </el-table>
+        </div>
+        <div class="conversation-panel">
+          <ConversationPanelPure
+            :corpId="corpId"
+            :customerId="currentCustomer && currentCustomer.externalUserId"
+            :staffUserId="selectedStaffUserId"
+            @logout="handleLogout"
+          />
+        </div>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { qwCustomerList, qwCustomerStaffList } from "@/api/qw/companySession";
+import ConversationPanelPure from "./ConversationPanelPure.vue";
+import Pagination from "@/components/Pagination";
+
+export default {
+  name: "CustomerQueryTab",
+  components: { ConversationPanelPure, Pagination },
+  props: {
+    corpId: { type: String, required: true }
+  },
+  watch: {
+    corpId() {
+      this.queryParams.pageNum = 1;
+      this.getCustomerList();
+    }
+  },
+  data() {
+    return {
+      queryParams: {
+        externalUserId: "",
+        name: "",
+        remark: "",
+        pageNum: 1,
+        pageSize: 10
+      },
+      customerList: [],
+      total: 0,
+      tableLoading: false,
+
+      // 抽屉相关
+      drawerVisible: false,
+      currentCustomer: null,
+      staffList: [],
+      staffLoading: false,
+      selectedStaffUserId: ''
+    };
+  },
+  mounted() {
+    this.getCustomerList();
+  },
+  methods: {
+    // 获取客户列表(后端分页)
+    async getCustomerList() {
+      this.tableLoading = true;
+      try {
+        const params = {
+          corpId: this.corpId,
+          externalUserId: this.queryParams.externalUserId || undefined,
+          name: this.queryParams.name || undefined,
+          remark: this.queryParams.remark || undefined,
+          pageNum: this.queryParams.pageNum,
+          pageSize: this.queryParams.pageSize
+        };
+        const res = await qwCustomerList(params);
+        if (res.rows) {
+          this.customerList = res.rows;
+          this.total = res.total || 0;
+        } else {
+          this.customerList = [];
+          this.total = 0;
+        }
+      } catch (e) {
+        console.error("获取客户列表失败", e);
+        this.customerList = [];
+        this.total = 0;
+      } finally {
+        this.tableLoading = false;
+      }
+    },
+
+    // 查询按钮
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getCustomerList();
+    },
+
+    // 重置查询条件
+    resetQuery() {
+      this.queryParams = {
+        externalUserId: "",
+        name: "",
+        remark: "",
+        pageNum: 1,
+        pageSize: 10
+      };
+      this.getCustomerList();
+    },
+
+    // 打开抽屉,加载该客户的客服列表
+    openDrawer(row) {
+      this.currentCustomer = row;
+      this.drawerVisible = true;
+      this.selectedStaffUserId = '';
+      this.staffList = [];
+      this.loadStaffList();
+    },
+
+    // 加载该客户关联的客服列表
+    async loadStaffList() {
+      if (!this.currentCustomer) return;
+      this.staffLoading = true;
+      try {
+        const res = await qwCustomerStaffList({
+          corpId: this.corpId,
+          externalUserId: this.currentCustomer.externalUserId
+        });
+        this.staffList = res.data || [];
+      } catch (e) {
+        console.error("获取客服列表失败", e);
+        this.staffList = [];
+      } finally {
+        this.staffLoading = false;
+      }
+    },
+
+    // 选中客服
+    handleStaffClick(row) {
+      this.selectedStaffUserId = row.userId;
+    },
+
+    // 会话登录失效处理
+    handleLogout() {
+      this.$message.warning("会话登录已失效,请刷新页面重试");
+    }
+  }
+};
+</script>
+
+<style scoped>
+.customer-query-tab {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.query-form {
+  background: #fff;
+  padding: 16px;
+  margin-bottom: 16px;
+  border-radius: 4px;
+}
+
+.drawer-container {
+  display: flex;
+  height: 100%;
+  gap: 16px;
+}
+
+.staff-list {
+  width: 320px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.conversation-panel {
+  flex: 1;
+  background: #f5f6f7;
+  border-radius: 8px;
+  overflow: hidden;
+}
+</style>

+ 62 - 15
src/views/qw/conversationNew/EmployeeQueryTab.vue

@@ -40,6 +40,13 @@
       <el-table-column prop="qwUserName" label="企微昵称" />
       <el-table-column prop="nickName" label="员工" />
       <el-table-column prop="departmentName" label="部门" />
+      <el-table-column prop="customerCount" label="客户数" width="80" />
+      <el-table-column prop="archiveCount" label="存档数" width="80" />
+      <el-table-column label="活跃时间" width="160">
+        <template slot-scope="scope">
+          {{ formatActiveTime(scope.row.lastActiveTime) }}
+        </template>
+      </el-table-column>
       <el-table-column label="操作" width="100">
         <template slot-scope="scope">
           <el-button type="text" @click="openDrawer(scope.row)">会话详情</el-button>
@@ -66,20 +73,34 @@
     >
       <div class="drawer-container">
         <div class="customer-list">
-          <el-table
-            :data="filteredCustomerList"
-            v-loading="customerLoading"
-            height="100%"
-            highlight-current-row
-            @row-click="handleCustomerClick"
-          >
-            <el-table-column label="头像" width="60">
-              <template slot-scope="scope">
-                <img :src="scope.row.avatar" style="width:40px;height:40px;border-radius:50%;" />
-              </template>
-            </el-table-column>
-            <el-table-column prop="name" label="客户名称" />
-          </el-table>
+          <div class="customer-search">
+            <el-input
+              v-model="customerRemark"
+              placeholder="按客户备注搜索"
+              clearable
+              size="small"
+              prefix-icon="el-icon-search"
+              @keyup.enter.native="handleRemarkSearch"
+              @clear="handleRemarkSearch"
+            />
+          </div>
+          <div class="customer-table-wrap">
+            <el-table
+              :data="filteredCustomerList"
+              v-loading="customerLoading"
+              height="100%"
+              highlight-current-row
+              @row-click="handleCustomerClick"
+            >
+              <el-table-column label="头像" width="60">
+                <template slot-scope="scope">
+                  <img :src="scope.row.avatar" style="width:40px;height:40px;border-radius:50%;" />
+                </template>
+              </el-table-column>
+              <el-table-column prop="name" label="客户名称" />
+              <el-table-column prop="remark" label="备注" show-overflow-tooltip />
+            </el-table>
+          </div>
           <pagination
             v-show="customerTotal > 0"
             :total="customerTotal"
@@ -154,6 +175,7 @@ export default {
       customerPageSize: 10,
       customerLoading: false,
       customerKeyword: "",
+      customerRemark: "",
       selectedCustomerId: null
     };
   },
@@ -251,6 +273,7 @@ export default {
       this.selectedCustomerId = null;
       this.currentStaffQwUserId = '';
       this.customerPageNum = 1;
+      this.customerRemark = '';
       this.loadCustomerList();
     },
 
@@ -263,7 +286,8 @@ export default {
           pageNum: this.customerPageNum,
           pageSize: this.customerPageSize,
           qwUserId: this.currentStaff.id,
-          corpId: this.corp?.corpId
+          corpId: this.corp?.corpId,
+          remark: this.customerRemark || undefined
         };
         const res = await listExternalContact(params);
         if (res.rows) {
@@ -286,6 +310,20 @@ export default {
       }
     },
 
+    // 备注搜索
+    handleRemarkSearch() {
+      this.customerPageNum = 1;
+      this.loadCustomerList();
+    },
+
+    // 活跃时间格式化(Unix秒 → yyyy-MM-dd HH:mm:ss)
+    formatActiveTime(timestamp) {
+      if (!timestamp) return '';
+      const d = new Date(timestamp * 1000);
+      const pad = n => String(n).padStart(2, '0');
+      return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+    },
+
     // 选中客户
     handleCustomerClick(row) {
       this.selectedCustomerId = row.externalUserId;
@@ -329,6 +367,15 @@ export default {
   margin-left: 20px;
 }
 
+.customer-search {
+  padding: 10px 0;
+}
+
+.customer-table-wrap {
+  flex: 1;
+  overflow: hidden;
+}
+
 .conversation-panel {
   flex: 1;
   background: #f5f6f7;

+ 38 - 2
src/views/system/config/config.vue

@@ -1853,10 +1853,18 @@
           <el-form-item label="发课发群" prop="roomLinkAllow">
             <el-switch
               v-model="form18.roomLinkAllow"
-              active-color="#ff4949"
-              inactive-color="#13ce66">
+              active-color="#13ce66"
+              inactive-color="#ff4949">
             </el-switch>
           </el-form-item>
+          <el-form-item label="群链接是好友才能看课" prop="roomLinkWatch">
+            <el-switch
+              v-model="form18.roomLinkWatch"
+              active-color="#13ce66"
+              inactive-color="#ff4949">
+            </el-switch>
+          </el-form-item>
+
           <el-form-item label="是否开启完课倒计时" prop="completionCountdown">
             <el-switch
               v-model="form18.completionCountdown"
@@ -3378,6 +3386,20 @@
         </div>
       </el-tab-pane>
 
+      <el-tab-pane label="腾讯审核配置" name="tx.config">
+        <el-form ref="form38" :model="form38" label-width="300px">
+          <el-form-item label="腾讯图片审核">
+            <el-switch v-model="form38.imgStatus"></el-switch>
+          </el-form-item>
+          <el-form-item label="腾讯文字审核">
+            <el-switch v-model="form38.txtStatus"></el-switch>
+          </el-form-item>
+        </el-form>
+        <div class="footer">
+          <el-button type="primary" @click="submitForm38">提  交</el-button>
+        </div>
+      </el-tab-pane>
+
     </el-tabs>
 
 
@@ -3452,6 +3474,7 @@ export default {
       configId: null,
       storeOPtions: [],
       roomLinkAllow:"",
+      roomLinkWatch:"",
       // 表单参数
       form1: {},
       form2: {},
@@ -3475,6 +3498,7 @@ export default {
       form18: {
         viewCommentNum: 200,
         roomLinkAllow:true,
+        roomLinkWatch:true,
         smsDomainName: '', // 初始化为空字符串或其他默认值
         smsAcquisitionName: '', // 初始化为空字符串或其他默认值
         completionCountdown: false,//是否开启点播完课倒计时
@@ -3598,6 +3622,10 @@ export default {
       form37:{
         tradeType: ''
       },
+      form38: {
+        imgStatus: false,
+        txtStatus: false
+      },
       form40: {
         enablePhoneConfig: false,
         enablePhoneLimitConfig:false,
@@ -4601,6 +4629,14 @@ export default {
         }
       });
     },
+    submitForm38(){
+      const param = { configId: this.configId, configKey: this.configKey, configValue: JSON.stringify(this.form36) }
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('修改成功')
+        }
+      })
+    },
     submitForm32(){
       const param = { configId: this.configId, configName : "直播源配置", configKey: this.configKey, configValue: JSON.stringify(this.form32) }
       updateConfigByKey(param).then(response => {