Selaa lähdekoodia

app单独sop板块的优化

三七 14 tuntia sitten
vanhempi
commit
75f6db5937

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

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

+ 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,
@@ -1159,6 +1188,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();

+ 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>
@@ -902,7 +904,7 @@ export default {
       type: Array,
     },
     sourceType: {
-      default: 0,//0-营期的一键群发,1-营期详情的一键群发
+      default: 0,//0-营期的一键群发,1-营期详情的一键群发,2-SOP的一键群发
       type: Number,
     },
   },

+ 203 - 29
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: {
@@ -80,59 +127,190 @@ export default {
       type: Array,
       default: () => [],
     },
+    // 所属公司id:有值时仅查该公司下销售
+    companyId: {
+      type: [Number, String],
+      default: null,
+    },
   },
   data() {
     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);
+      }
     }
   },
   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.ignoreIds && this.ignoreIds.length) {
-        qp['ignoreIds'] = this.ignoreIds;
-      } else {
-        delete qp.ignoreIds;
+      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.ids && this.ids.length) {
-        qp['ids'] = this.ids;
-      } else {
-        delete qp.ids;
+      if (this.queryParams.isDelete !== '' && this.queryParams.isDelete !== undefined && this.queryParams.isDelete !== null) {
+        qp.isDelete = this.queryParams.isDelete;
+      }
+      // 选了具体销售则按销售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;
       });
     },
     // 取消按钮
@@ -163,10 +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");
     },
     /**
      * 确定选择
@@ -185,18 +365,12 @@ export default {
       this.queryParams.pageNum = 1;
       this.getList();
     },
-    handleQueryEnter(event) {
-      // 确保事件对象存在
-      if (event && event.preventDefault) {
-        event.preventDefault(); // 阻止默认提交行为
-      }
-      this.handleQuery();
-    },
     /**
      * 重置按钮操作
      */
     resetQuery() {
-      this.resetForm("queryForm");
+      this.reset();
+      this.remoteSearchSales('');
       this.handleQuery();
     },
     /**

+ 106 - 18
src/views/app/user/index.vue

@@ -136,7 +136,7 @@
           size="mini"
           v-hasPermi="['app:user:bindCustomer']"
           @click="openbindAIPage(false, 0)"
-        >批量换绑销售
+        >批量绑定客服
         </el-button>
       </el-col>
       <el-col :span="1.5">
@@ -166,7 +166,7 @@
           size="mini"
           v-hasPermi="['app:user:batchBindCustomer']"
           @click="openbindAIPage(true, 0)"
-        >批量换绑销售(筛选条件)
+        >批量绑定客服(筛选条件)
         </el-button>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
@@ -359,9 +359,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"
@@ -487,6 +500,7 @@ import { getUser, delUser, addUser, updateUser } from "@/api/store/user";
 import {
   bindUserCusByIdsV2,
   unbindUserCusByIdsV2,
+  listBoundSales,
   listAppUser,
   exportAppUser
 } from '@/api/app/user';
@@ -596,6 +610,8 @@ export default {
         visible: false,
         enableFilter: false,
         selected: [],
+        // 解绑时预填:仅展示这些已绑定销售
+        presetIds: [],
       },
       bindAISearch: {
         visible: false,
@@ -1253,7 +1269,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();
@@ -1262,8 +1278,74 @@ 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;
+    },
     /**
      * 绑定客服页面-选中客服
      */
@@ -1289,22 +1371,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);
@@ -1316,6 +1384,7 @@ export default {
       this.getList();
       this.bindAIConfig.visible = false;
       this.bindAIConfig.selected = [];
+      this.bindAIConfig.presetIds = [];
     },
   }
 };
@@ -1360,4 +1429,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; // 自动填充封面
+            }
+        },
         /**
          * 搜索条件:客服选中确认回调
          */

+ 3 - 0
src/views/crm/customer/qw/externalContact.vue

@@ -2557,6 +2557,9 @@ export default {
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
         obj.tagIds = obj.tagIds.split(",");
       }
+      if(obj.outTagIds && typeof obj.outTagIds === 'string'){
+        obj.outTagIds = obj.outTagIds.split(",");
+      }
       batchUpdateExternalContactNotes({
         addType: 0,
         userIds: this.ids,

+ 3 - 0
src/views/crm/customer/qw/myExternalContact.vue

@@ -3008,6 +3008,9 @@ export default {
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
         obj.tagIds = obj.tagIds.split(",");
       }
+      if(obj.outTagIds && typeof obj.outTagIds === 'string'){
+        obj.outTagIds = obj.outTagIds.split(",");
+      }
       batchUpdateExternalContactNotes({
         addType: 0,
         userIds: this.ids,

+ 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
@@ -352,6 +369,7 @@ import { getCourseCouponUserListVO } from '@/api/course/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',
@@ -362,13 +380,15 @@ export default {
       total: 0,
       list: [],
       ids: [],
+      liveGroupTypeList: [],
+      liveOptions: [],
       liveRewardTypeList: [],
       dateRange: [],
       queryParams: {
         pageNum: 1,
         pageSize: 10,
+        liveGroupType: null,
         liveId: null,
-        liveName: null,
         userId: null,
         userName: null,
         companyId: null,
@@ -431,9 +451,33 @@ export default {
     this.getDicts('live_watch_reward_type').then(res => {
       this.liveRewardTypeList = res.data || []
     })
+    this.loadLiveGroupTypeList()
+    this.loadLiveOptions()
     this.getList()
   },
   methods: {
+    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)
+    },
     handleSelectionChange(selection) {
       this.ids = [...new Set(selection.map(item => item.userId).filter(id => id != null))]
     },
@@ -693,14 +737,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
     },
@@ -722,6 +765,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) {

+ 3 - 0
src/views/qw/externalContact/index.vue

@@ -2775,6 +2775,9 @@ export default {
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
         obj.tagIds = obj.tagIds.split(",");
       }
+      if(obj.outTagIds && typeof obj.outTagIds === 'string'){
+        obj.outTagIds = obj.outTagIds.split(",");
+      }
       batchUpdateExternalContactNotes({
         addType: 0,
         userIds: this.ids,

+ 3 - 0
src/views/qw/externalContact/myExternalContact.vue

@@ -2902,6 +2902,9 @@ export default {
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
         obj.tagIds = obj.tagIds.split(",");
       }
+      if(obj.outTagIds && typeof obj.outTagIds === 'string'){
+        obj.outTagIds = obj.outTagIds.split(",");
+      }
       batchUpdateExternalContactNotes({
         addType: 0,
         userIds: this.ids,