소스 검색

Merge remote-tracking branch 'origin/master'

yuhongqi 21 시간 전
부모
커밋
0721ccda1f

+ 6 - 0
package.json

@@ -157,22 +157,28 @@
     "babel-eslint": "10.1.0",
     "babel-eslint": "10.1.0",
     "babel-jest": "23.6.0",
     "babel-jest": "23.6.0",
     "babel-plugin-dynamic-import-node": "2.3.3",
     "babel-plugin-dynamic-import-node": "2.3.3",
+    "buffer": "^6.0.3",
     "chalk": "2.4.2",
     "chalk": "2.4.2",
     "chokidar": "2.1.5",
     "chokidar": "2.1.5",
     "connect": "3.6.6",
     "connect": "3.6.6",
+    "crypto-browserify": "^3.12.1",
     "eslint": "6.7.2",
     "eslint": "6.7.2",
     "eslint-plugin-vue": "6.2.2",
     "eslint-plugin-vue": "6.2.2",
     "html-webpack-plugin": "3.2.0",
     "html-webpack-plugin": "3.2.0",
     "husky": "1.3.1",
     "husky": "1.3.1",
     "lint-staged": "8.1.5",
     "lint-staged": "8.1.5",
     "mockjs": "1.0.1-beta3",
     "mockjs": "1.0.1-beta3",
+    "os-browserify": "^0.3.0",
+    "path-browserify": "^1.0.1",
     "plop": "2.3.0",
     "plop": "2.3.0",
+    "process": "^0.11.10",
     "runjs": "4.3.2",
     "runjs": "4.3.2",
     "sass": "^1.97.1",
     "sass": "^1.97.1",
     "sass-loader": "8.0.2",
     "sass-loader": "8.0.2",
     "script-ext-html-webpack-plugin": "2.1.3",
     "script-ext-html-webpack-plugin": "2.1.3",
     "script-loader": "0.7.2",
     "script-loader": "0.7.2",
     "serve-static": "1.13.2",
     "serve-static": "1.13.2",
+    "stream-browserify": "^3.0.0",
     "svg-sprite-loader": "4.1.3",
     "svg-sprite-loader": "4.1.3",
     "svgo": "1.2.0",
     "svgo": "1.2.0",
     "vue-template-compiler": "2.7.16"
     "vue-template-compiler": "2.7.16"

+ 6 - 1
src/App.vue

@@ -1,11 +1,16 @@
 <template>
 <template>
   <div id="app">
   <div id="app">
     <router-view />
     <router-view />
+    <!-- 回访提醒弹窗(仅销售端登录后生效) -->
+    <visit-reminder-popup />
   </div>
   </div>
 </template>
 </template>
 
 
 <script>
 <script>
+import VisitReminderPopup from '@/views/store/components/visitReminderPopup.vue'
+
 export default  {
 export default  {
-  name:  'App'
+  name:  'App',
+  components: { VisitReminderPopup }
 }
 }
 </script>
 </script>

+ 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) {
 export function listAppUser(data) {
   return request({
   return request({

+ 53 - 0
src/api/store/customerVisit.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 查询客户回访记录列表
+export function listCustomerVisit(query) {
+  return request({
+    url: '/store/store/customerVisit/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询客户回访记录详情
+export function getCustomerVisit(id) {
+  return request({
+    url: '/store/store/customerVisit/' + id,
+    method: 'get'
+  })
+}
+
+// 新增客户回访记录
+export function addCustomerVisit(data) {
+  return request({
+    url: '/store/store/customerVisit',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改客户回访记录
+export function updateCustomerVisit(data) {
+  return request({
+    url: '/store/store/customerVisit',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除客户回访记录
+export function delCustomerVisit(ids) {
+  return request({
+    url: '/store/store/customerVisit/' + ids,
+    method: 'delete'
+  })
+}
+
+// 关闭弹窗提醒(批量标记为已提醒)
+export function closeRemind(ids) {
+  return request({
+    url: '/store/store/customerVisit/closeRemind',
+    method: 'put',
+    data: ids
+  })
+}

+ 2 - 0
src/layout/components/Navbar.vue

@@ -165,6 +165,8 @@ export default {
           });
           });
         }
         }
 
 
+        // 通知回访提醒组件
+        this.$root.$emit('visit-reminders-update', response.visitReminders || []);
       }).catch(error => {
       }).catch(error => {
         console.error("获取消息计数失败:", error);
         console.error("获取消息计数失败:", error);
       });
       });

+ 2 - 0
src/utils/node-polyfill.js

@@ -0,0 +1,2 @@
+// Mock for Node.js built-in modules used by server-side AWS SDK packages
+module.exports = {};

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

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

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

@@ -105,6 +105,20 @@
                     批量执行自动化
                     批量执行自动化
                 </el-button>
                 </el-button>
             </el-col>
             </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>
             <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
         </el-row>
         </el-row>
 
 
@@ -410,6 +424,16 @@
             style="font-weight: bolder">
             style="font-weight: bolder">
             <sopLogsDetails ref="sopLogsDetails" :rowDetailFrom="sopLogsDialog.sopLogsForm"></sopLogsDetails>
             <sopLogsDetails ref="sopLogsDetails" :rowDetailFrom="sopLogsDialog.sopLogsForm"></sopLogsDetails>
         </el-drawer>
         </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>
     </div>
 </template>
 </template>
 
 
@@ -432,11 +456,12 @@ import {
 import {listSopTemp} from "@/api/app/sop/template";
 import {listSopTemp} from "@/api/app/sop/template";
 import ImageUpload from "@/views/app/sop/info/ImageUpload";
 import ImageUpload from "@/views/app/sop/info/ImageUpload";
 import sopLogsDetails from '@/views/app/sop/userLogs/sopLogsList.vue'
 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";
 import { listTagGroupForUserBindTag, } from "@/api/app/tag/tagGroup";
 
 
 export default {
 export default {
     name: "Sop",
     name: "Sop",
-    components: { ImageUpload, sopLogsDetails },
+    components: { ImageUpload, sopLogsDetails, groupSendMessage },
     data() {
     data() {
         return {
         return {
             miniAppList: [],
             miniAppList: [],
@@ -472,6 +497,10 @@ export default {
             ids: [],
             ids: [],
             //选中的map
             //选中的map
             selectionMap: {},
             selectionMap: {},
+            groupSendMessageForm: {
+                ids: [],
+                visible: false,
+            },
             myQwCompanyList: [],
             myQwCompanyList: [],
             tempUser: {
             tempUser: {
                 open: false,
                 open: false,
@@ -1159,6 +1188,19 @@ export default {
             this.single = selection.length !== 1
             this.single = selection.length !== 1
             this.multiple = !selection.length
             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() {
         handleAdd() {
             this.reset();
             this.reset();

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

@@ -4,7 +4,9 @@
     <el-alert type="warning" :closable="false" show-icon>
     <el-alert type="warning" :closable="false" show-icon>
       <template #title>
       <template #title>
         <span style="font-size: 25px; line-height: 1.5;">
         <span style="font-size: 25px; line-height: 1.5;">
-          此功能用于给 选中的 营期 内【所有的】客户发送 消息
+          {{ sourceType === 2
+            ? '此功能用于给选中的SOP规则内【所有的】客户发送消息'
+            : '此功能用于给选中的营期内【所有的】客户发送消息' }}
         </span>
         </span>
       </template>
       </template>
     </el-alert>
     </el-alert>
@@ -902,7 +904,7 @@ export default {
       type: Array,
       type: Array,
     },
     },
     sourceType: {
     sourceType: {
-      default: 0,//0-营期的一键群发,1-营期详情的一键群发
+      default: 0,//0-营期的一键群发,1-营期详情的一键群发,2-SOP的一键群发
       type: Number,
       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"
     <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px"
       @submit.prevent="handleQuery">
       @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>
-      <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>
 
 
       <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="销售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="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="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-column label="备注" align="center" prop="remark" min-width="160" show-overflow-tooltip/>
     </el-table>
     </el-table>
 
 
@@ -50,6 +96,7 @@
 
 
 <script>
 <script>
 import { listRole } from "@/api/app/user/userList";
 import { listRole } from "@/api/app/user/userList";
+import { listCustomerRoleOptions } from "@/api/app/customerRole";
 export default {
 export default {
   name: "SelectedCustomer",
   name: "SelectedCustomer",
   props: {
   props: {
@@ -80,59 +127,190 @@ export default {
       type: Array,
       type: Array,
       default: () => [],
       default: () => [],
     },
     },
+    // 所属公司id:有值时仅查该公司下销售
+    companyId: {
+      type: [Number, String],
+      default: null,
+    },
   },
   },
   data() {
   data() {
     return {
     return {
       // 遮罩层
       // 遮罩层
       loading: true,
       loading: true,
+      salesLoading: false,
       // 总条数
       // 总条数
       total: 0,
       total: 0,
       // 客服成员表格数据
       // 客服成员表格数据
       roleList: [],
       roleList: [],
+      companyOptions: [],
+      salesOptions: [],
       // 查询参数
       // 查询参数
       queryParams: {
       queryParams: {
         pageNum: 1,
         pageNum: 1,
         pageSize: 10,
         pageSize: 10,
-        memberName: null,
-        roleName: null
+        companyId: null,
+        salesId: null,
+        isDelete: 0,
       },
       },
     };
     };
   },
   },
+  computed: {
+    lockedCompany() {
+      return this.companyId !== null && this.companyId !== undefined && this.companyId !== '';
+    },
+  },
   created() {
   created() {
     this.queryParams.pageNum = this.defaultPageNum;
     this.queryParams.pageNum = this.defaultPageNum;
     this.queryParams.pageSize = this.defaultPageSize;
     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.getList();
+    this.remoteSearchSales('');
   },
   },
   watch: {
   watch: {
     selected: {
     selected: {
       immediate: true,
       immediate: true,
-      handler(nv) {
+      handler() {
         this.setSelectionStatus();
         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: {
   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() {
     getList() {
       this.loading = true;
       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 => {
       listRole(qp).then(response => {
         this.roleList = response.rows;
         this.roleList = response.rows;
         this.total = response.total;
         this.total = response.total;
         this.loading = false;
         this.loading = false;
         this.setSelectionStatus();
         this.setSelectionStatus();
+      }).catch(() => {
+        this.loading = false;
       });
       });
     },
     },
     // 取消按钮
     // 取消按钮
@@ -163,10 +341,12 @@ export default {
       this.queryParams = {
       this.queryParams = {
         pageNum: this.defaultPageNum,
         pageNum: this.defaultPageNum,
         pageSize: this.defaultPageSize,
         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.queryParams.pageNum = 1;
       this.getList();
       this.getList();
     },
     },
-    handleQueryEnter(event) {
-      // 确保事件对象存在
-      if (event && event.preventDefault) {
-        event.preventDefault(); // 阻止默认提交行为
-      }
-      this.handleQuery();
-    },
     /**
     /**
      * 重置按钮操作
      * 重置按钮操作
      */
      */
     resetQuery() {
     resetQuery() {
-      this.resetForm("queryForm");
+      this.reset();
+      this.remoteSearchSales('');
       this.handleQuery();
       this.handleQuery();
     },
     },
     /**
     /**

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

@@ -136,7 +136,7 @@
           size="mini"
           size="mini"
           v-hasPermi="['app:user:bindCustomer']"
           v-hasPermi="['app:user:bindCustomer']"
           @click="openbindAIPage(false, 0)"
           @click="openbindAIPage(false, 0)"
-        >批量换绑销售
+        >批量绑定客服
         </el-button>
         </el-button>
       </el-col>
       </el-col>
       <el-col :span="1.5">
       <el-col :span="1.5">
@@ -166,7 +166,7 @@
           size="mini"
           size="mini"
           v-hasPermi="['app:user:batchBindCustomer']"
           v-hasPermi="['app:user:batchBindCustomer']"
           @click="openbindAIPage(true, 0)"
           @click="openbindAIPage(true, 0)"
-        >批量换绑销售(筛选条件)
+        >批量绑定客服(筛选条件)
         </el-button>
         </el-button>
       </el-col>
       </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
@@ -359,9 +359,22 @@
 
 
       <!-- 选择app客服角色 -->
       <!-- 选择app客服角色 -->
     <el-dialog :title="bindAIConfig.title" :visible.sync="bindAIConfig.visible" width="1000px" append-to-body :close-on-click-modal="false">
     <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
       <CusRoleList
+        v-if="bindAIConfig.visible"
         ref="customerBindRef"
         ref="customerBindRef"
         :multiple="false"
         :multiple="false"
+        :ids="bindAIConfig.presetIds"
         @confirm="customerSelectedConfirmCallForBind"
         @confirm="customerSelectedConfirmCallForBind"
         @selectionChange="customerSelectionChangeCallForBind"
         @selectionChange="customerSelectionChangeCallForBind"
         :selected="bindAIConfig.selected"
         :selected="bindAIConfig.selected"
@@ -487,6 +500,7 @@ import { getUser, delUser, addUser, updateUser } from "@/api/store/user";
 import {
 import {
   bindUserCusByIdsV2,
   bindUserCusByIdsV2,
   unbindUserCusByIdsV2,
   unbindUserCusByIdsV2,
+  listBoundSales,
   listAppUser,
   listAppUser,
   exportAppUser
   exportAppUser
 } from '@/api/app/user';
 } from '@/api/app/user';
@@ -596,6 +610,8 @@ export default {
         visible: false,
         visible: false,
         enableFilter: false,
         enableFilter: false,
         selected: [],
         selected: [],
+        // 解绑时预填:仅展示这些已绑定销售
+        presetIds: [],
       },
       },
       bindAISearch: {
       bindAISearch: {
         visible: false,
         visible: false,
@@ -1253,7 +1269,7 @@ export default {
      * @param enableFilter 是否开启过滤模式
      * @param enableFilter 是否开启过滤模式
      * @param type 0-绑定,1-解绑
      * @param type 0-绑定,1-解绑
      */
      */
-    openbindAIPage(enableFilter = false, type = 1) {
+    async openbindAIPage(enableFilter = false, type = 1) {
       this.bindAIConfig.enableFilter = enableFilter;
       this.bindAIConfig.enableFilter = enableFilter;
       if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
       if (!enableFilter && (this.ids === null || this.ids.length === 0)) {
         this.$message.closeAll();
         this.$message.closeAll();
@@ -1262,8 +1278,74 @@ export default {
       this.bindAIConfig.title = type == 0 ? '换绑销售' : '解绑客服';
       this.bindAIConfig.title = type == 0 ? '换绑销售' : '解绑客服';
       this.bindAIConfig.type = type;
       this.bindAIConfig.type = type;
       this.bindAIConfig.selected = [];
       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;
       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();
         this.$message.closeAll();
         return this.$message.warning('请选择一个客服');
         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['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;
       let res;
       if (this.bindAIConfig.type == 0) {
       if (this.bindAIConfig.type == 0) {
         res = await bindUserCusByIdsV2(tmpParam);
         res = await bindUserCusByIdsV2(tmpParam);
@@ -1316,6 +1384,7 @@ export default {
       this.getList();
       this.getList();
       this.bindAIConfig.visible = false;
       this.bindAIConfig.visible = false;
       this.bindAIConfig.selected = [];
       this.bindAIConfig.selected = [];
+      this.bindAIConfig.presetIds = [];
     },
     },
   }
   }
 };
 };
@@ -1360,4 +1429,23 @@ export default {
 ::v-deep  .ai-user-info .el-form-item__label {
 ::v-deep  .ai-user-info .el-form-item__label {
   text-align: right !important;
   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>
 </style>

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

@@ -363,57 +363,57 @@
                 </div>
                 </div>
             </el-form>
             </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">
             <div slot="footer" class="dialog-footer" style="text-align: center">
                 <el-button type="primary" @click="confirmUpload">确定</el-button>
                 <el-button type="primary" @click="confirmUpload">确定</el-button>
                 <el-button @click="cancelUpload">取消</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 CusRoleList from '@/views/app/user/CusRoleList.vue';
 import {courseList, videoList} from "@/api/app/course";
 import {courseList, videoList} from "@/api/app/course";
 import { getPackageOptions,} from "@/api/app/pkg";
 import { getPackageOptions,} from "@/api/app/pkg";
-// import { getLiveOptions,} from "@/api/app/live";
+import { getLiveOptions,} from "@/api/app/live";
 
 
 export default {
 export default {
     name: "AppWelcome",
     name: "AppWelcome",
@@ -527,8 +527,8 @@ export default {
         this.getList();
         this.getList();
         let defaultPkg = await this.getPkgList(null, null);
         let defaultPkg = await this.getPkgList(null, null);
         this.pkgList = defaultPkg.data;
         this.pkgList = defaultPkg.data;
-        // let defaultLive = await this.getLiveList();
-        // this.liveList = defaultLive.data;
+        let defaultLive = await this.getLiveList();
+        this.liveList = defaultLive.data;
     },
     },
     methods: {
     methods: {
         /**
         /**
@@ -557,9 +557,9 @@ export default {
         /**
         /**
          * 获取直播间信息
          * 获取直播间信息
          */
          */
-        // async getLiveList() {
-        //     return await getLiveOptions();
-        // },
+        async getLiveList() {
+            return await getLiveOptions();
+        },
         /**
         /**
          * 疗法下拉搜索
          * 疗法下拉搜索
          * @param keyword
          * @param keyword
@@ -584,18 +584,18 @@ export default {
                 this.pkgForm.packageImgUrl = selectedPkg.imgUrl;
                 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-option>
         </el-select>
         </el-select>
       </el-form-item>
       </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
           <el-option
             v-for="dict in courseLists"
             v-for="dict in courseLists"
             :key="dict.dictValue"
             :key="dict.dictValue"
-            :label="dict.remark"
+            :label="(dict.remark || dict.dictLabel) + '(' + dict.dictValue + ')'"
             :value="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-option>
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
@@ -203,7 +193,7 @@
           </div>
           </div>
         </template>
         </template>
       </el-table-column>
       </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="videoName" />
       <el-table-column label="记录类型" align="center" prop="logType">
       <el-table-column label="记录类型" align="center" prop="logType">
         <template slot-scope="scope">
         <template slot-scope="scope">
@@ -741,7 +731,7 @@ export default {
     handleAdd() {
     handleAdd() {
       this.reset();
       this.reset();
       this.open = true;
       this.open = true;
-      this.title = "添加短链科普学习记录";
+      this.title = "添加短链课程学习记录";
     },
     },
    
    
     /** 提交按钮 */
     /** 提交按钮 */
@@ -769,7 +759,7 @@ export default {
     handleExport() {
     handleExport() {
       const that = this
       const that = this
       const queryParams = this.queryParams;
       const queryParams = this.queryParams;
-      this.$confirm('是否确认导出所有短链科普学习记录数据项?', "警告", {
+      this.$confirm('是否确认导出所有短链课程学习记录数据项?', "警告", {
           confirmButtonText: "确定",
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           cancelButtonText: "取消",
           type: "warning"
           type: "warning"

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

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

+ 133 - 13
src/views/crm/customer/qw/myExternalContact.vue

@@ -375,7 +375,7 @@
             <div class="tag-list">
             <div class="tag-list">
               <el-tag
               <el-tag
                 v-for="name in scope.row.tagIdsName"
                 v-for="name in scope.row.tagIdsName"
-                :key="name"
+                :key="scope.row.id + '_' + name"
                 type="success"
                 type="success"
                 size="small"
                 size="small"
               >
               >
@@ -475,7 +475,7 @@
       <el-table-column label="注册时间" align="center" prop="registerTime" width="100px" />
       <el-table-column label="注册时间" align="center" prop="registerTime" width="100px" />
       <el-table-column label="备注电话号码" align="center" prop="remarkMobiles" width="150px">
       <el-table-column label="备注电话号码" align="center" prop="remarkMobiles" width="150px">
         <template slot-scope="scope">
         <template slot-scope="scope">
-          <div v-for="i in JSON.parse(scope.row.remarkMobiles)" :key="i">{{i}}</div>
+          <div v-for="(i, idx) in JSON.parse(scope.row.remarkMobiles)" :key="scope.row.id + '_' + idx">{{i}}</div>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
       <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
@@ -1275,6 +1275,7 @@
             <div class="recording-header">
             <div class="recording-header">
               <span class="recording-index">跟进记录 {{ rIndex + 1 }}</span>
               <span class="recording-index">跟进记录 {{ rIndex + 1 }}</span>
               <el-button
               <el-button
+                v-if="rIndex >= 8"
                 size="mini"
                 size="mini"
                 type="danger"
                 type="danger"
                 icon="el-icon-delete"
                 icon="el-icon-delete"
@@ -3007,6 +3008,9 @@ export default {
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
         obj.tagIds = obj.tagIds.split(",");
         obj.tagIds = obj.tagIds.split(",");
       }
       }
+      if(obj.outTagIds && typeof obj.outTagIds === 'string'){
+        obj.outTagIds = obj.outTagIds.split(",");
+      }
       batchUpdateExternalContactNotes({
       batchUpdateExternalContactNotes({
         addType: 0,
         addType: 0,
         userIds: this.ids,
         userIds: this.ids,
@@ -3043,9 +3047,20 @@ export default {
         customerInfo: {},
         customerInfo: {},
         followContent: '',
         followContent: '',
         visitRecords: [],
         visitRecords: [],
-        saving: false
+        saving: false,
+        visitSaving: false,
+        recordingSaving: false,
+        profile: { gender: null, age: null, height: null, weight: null },
+        dealLikelihood: '',
+        keyObstacle: '',
+        followStrategy: '',
+        absenceReason: '',
+        callStatus: '',
+        isDmCustomer: '',
+        customerLevel: ''
       };
       };
       this.followUpTags = [];
       this.followUpTags = [];
+      this.ensureMinVisitRecords(8);
       this.loadFollowUpData();
       this.loadFollowUpData();
       // 加载SCRM客户详情
       // 加载SCRM客户详情
       if (row.fsUserId) {
       if (row.fsUserId) {
@@ -3061,20 +3076,34 @@ export default {
     addVisitRecordSlot() {
     addVisitRecordSlot() {
       this.followUpDrawer.visitRecords.push({
       this.followUpDrawer.visitRecords.push({
         _key: 'v_' + Date.now() + '_' + Math.random(),
         _key: 'v_' + Date.now() + '_' + Math.random(),
-        registerDesc: '',
+        callContent: '',
+        customerFeedback: '',
+        nextAction: '',
         visitStatus: '',
         visitStatus: '',
         visitTime: ''
         visitTime: ''
       });
       });
     },
     },
-    /** 删除跟进记录槽位 */
+    /** 保证至少 min 条跟进记录 */
+    ensureMinVisitRecords(min) {
+      while (this.followUpDrawer.visitRecords.length < min) {
+        this.addVisitRecordSlot();
+      }
+    },
+    /** 删除跟进记录槽位(仅超过8条的才可删除) */
     deleteVisitRecord(index) {
     deleteVisitRecord(index) {
+      if (index < 8) {
+        this.$message.warning('前8条跟进记录不可删除');
+        return;
+      }
       this.followUpDrawer.visitRecords.splice(index, 1);
       this.followUpDrawer.visitRecords.splice(index, 1);
     },
     },
     /** 保存所有跟进记录(JSON方式) */
     /** 保存所有跟进记录(JSON方式) */
     saveVisitRecords() {
     saveVisitRecords() {
       this.followUpDrawer.visitSaving = true;
       this.followUpDrawer.visitSaving = true;
       const visitRecords = this.followUpDrawer.visitRecords.map(r => ({
       const visitRecords = this.followUpDrawer.visitRecords.map(r => ({
-        registerDesc: r.registerDesc || '',
+        callContent: r.callContent || '',
+        customerFeedback: r.customerFeedback || '',
+        nextAction: r.nextAction || '',
         visitStatus: r.visitStatus || '',
         visitStatus: r.visitStatus || '',
         visitTime: r.visitTime || ''
         visitTime: r.visitTime || ''
       }));
       }));
@@ -3098,9 +3127,13 @@ export default {
       getMyCustomerList({ fsUserId: fsUserId, pageNum: 1, pageSize: 1 }).then(response => {
       getMyCustomerList({ fsUserId: fsUserId, pageNum: 1, pageSize: 1 }).then(response => {
         if (response.rows && response.rows.length > 0) {
         if (response.rows && response.rows.length > 0) {
           this.followUpDrawer.customerInfo = response.rows[0];
           this.followUpDrawer.customerInfo = response.rows[0];
-          this.followUpTags = response.rows[0].tags ? response.rows[0].tags.split(',').filter(t => t) : [];
+          // 解析tags中的3个可编辑字段
+          this.parseEditableTags(response.rows[0].tags || '');
         } else {
         } else {
           this.followUpTags = [];
           this.followUpTags = [];
+          this.followUpDrawer.callStatus = '';
+          this.followUpDrawer.isDmCustomer = '';
+          this.followUpDrawer.customerLevel = '';
         }
         }
       }).catch(() => {});
       }).catch(() => {});
     },
     },
@@ -3139,20 +3172,70 @@ export default {
     syncFollowUpTags() {
     syncFollowUpTags() {
       this.followUpDrawer.customerInfo.tags = this.followUpTags.join(',');
       this.followUpDrawer.customerInfo.tags = this.followUpTags.join(',');
     },
     },
+    /** 从tags逗号分隔字符串中解析3个可编辑字段 */
+    parseEditableTags(tagsStr) {
+      this.followUpDrawer.callStatus = '';
+      this.followUpDrawer.isDmCustomer = '';
+      this.followUpDrawer.customerLevel = '';
+      if (!tagsStr) return;
+      const tags = tagsStr.split(',').map(t => t.trim()).filter(t => t);
+      // 通话状态:未接通|拒接电话|已通话
+      const callStatusValues = ['未接通', '拒接电话', '已通话'];
+      const dmValues = ['是DM', '非DM'];
+      const levelValues = ['A', 'B', 'C', 'D'];
+      const matched = new Set();
+      for (const tag of tags) {
+        if (callStatusValues.includes(tag)) {
+          this.followUpDrawer.callStatus = tag;
+          matched.add(tag);
+        } else if (dmValues.includes(tag)) {
+          this.followUpDrawer.isDmCustomer = tag;
+          matched.add(tag);
+        } else if (levelValues.includes(tag)) {
+          this.followUpDrawer.customerLevel = tag;
+          matched.add(tag);
+        }
+      }
+      // 其余标签填充到followUpTags
+      this.followUpTags = tags.filter(t => !matched.has(t));
+    },
+    /** 将followUpTags和3个可编辑字段合并为逗号分隔的tags字符串 */
+    buildMergedTags() {
+      const parts = [...this.followUpTags];
+      if (this.followUpDrawer.callStatus) parts.push(this.followUpDrawer.callStatus);
+      if (this.followUpDrawer.isDmCustomer) parts.push(this.followUpDrawer.isDmCustomer);
+      if (this.followUpDrawer.customerLevel) parts.push(this.followUpDrawer.customerLevel);
+      return parts.join(',');
+    },
     /** 保存SCRM客户信息 */
     /** 保存SCRM客户信息 */
     saveCustomerInfo() {
     saveCustomerInfo() {
       const info = this.followUpDrawer.customerInfo;
       const info = this.followUpDrawer.customerInfo;
       if (!info) return;
       if (!info) return;
       this.followUpDrawer.saving = true;
       this.followUpDrawer.saving = true;
+      const profile = this.followUpDrawer.profile || {};
+      // 合并3个可编辑字段到tags
+      const mergedTags = this.buildMergedTags();
       updateCustomer({
       updateCustomer({
         externalContactId: this.followUpDrawer.externalContactId,
         externalContactId: this.followUpDrawer.externalContactId,
         source: info.source,
         source: info.source,
         visitStatus: info.visitStatus,
         visitStatus: info.visitStatus,
         customerType: info.customerType,
         customerType: info.customerType,
-        tags: info.tags,
+        customerProfile: JSON.stringify({
+          gender: profile.gender,
+          age: profile.age,
+          height: profile.height,
+          weight: profile.weight
+        }),
+        tags: mergedTags,
         remark: info.remark,
         remark: info.remark,
         lastTime: info.lastTime,
         lastTime: info.lastTime,
-        followContent: this.followUpDrawer.followContent || ''
+        followContent: JSON.stringify({
+          content: this.followUpDrawer.followContent || '',
+          dealLikelihood: this.followUpDrawer.dealLikelihood || '',
+          keyObstacle: this.followUpDrawer.keyObstacle || '',
+          followStrategy: this.followUpDrawer.followStrategy || '',
+          absenceReason: this.followUpDrawer.absenceReason || ''
+        })
       }).then(res => {
       }).then(res => {
         if (res.code === 200) {
         if (res.code === 200) {
           this.$message.success('客户信息保存成功');
           this.$message.success('客户信息保存成功');
@@ -3187,7 +3270,9 @@ export default {
               const parsed = typeof info.visitRecords === 'string' ? JSON.parse(info.visitRecords) : info.visitRecords;
               const parsed = typeof info.visitRecords === 'string' ? JSON.parse(info.visitRecords) : info.visitRecords;
               this.followUpDrawer.visitRecords = (parsed || []).map((r, i) => ({
               this.followUpDrawer.visitRecords = (parsed || []).map((r, i) => ({
                 _key: 'v_' + Date.now() + '_' + i,
                 _key: 'v_' + Date.now() + '_' + i,
-                registerDesc: r.registerDesc || '',
+                callContent: r.callContent || '',
+                customerFeedback: r.customerFeedback || '',
+                nextAction: r.nextAction || '',
                 visitStatus: r.visitStatus || '',
                 visitStatus: r.visitStatus || '',
                 visitTime: r.visitTime || ''
                 visitTime: r.visitTime || ''
               }));
               }));
@@ -3195,9 +3280,11 @@ export default {
               this.followUpDrawer.visitRecords = [];
               this.followUpDrawer.visitRecords = [];
             }
             }
           } else {
           } else {
-            this.followUpDrawer.visitRecords = [];
-          }
-          // 解析录音记录
+          this.followUpDrawer.visitRecords = [];
+        }
+        // 保证至少8条
+        this.ensureMinVisitRecords(8);
+        // 解析录音记录
           if (info.recordings) {
           if (info.recordings) {
             try {
             try {
               const parsed = typeof info.recordings === 'string' ? JSON.parse(info.recordings) : info.recordings;
               const parsed = typeof info.recordings === 'string' ? JSON.parse(info.recordings) : info.recordings;
@@ -3215,9 +3302,42 @@ export default {
           } else {
           } else {
             this.followUpDrawer.recordings = [];
             this.followUpDrawer.recordings = [];
           }
           }
+          // 解析客户画像
+          if (info.customerProfile) {
+            try {
+              const parsed = typeof info.customerProfile === 'string' ? JSON.parse(info.customerProfile) : info.customerProfile;
+              this.followUpDrawer.profile = {
+                gender: parsed.gender != null ? parsed.gender : null,
+                age: parsed.age != null ? parsed.age : null,
+                height: parsed.height != null ? parsed.height : null,
+                weight: parsed.weight != null ? parsed.weight : null
+              };
+            } catch (e) {
+              this.followUpDrawer.profile = { gender: null, age: null, height: null, weight: null };
+            }
+          } else {
+            this.followUpDrawer.profile = { gender: null, age: null, height: null, weight: null };
+          }
+          // 解析跟进内容JSON
+          if (info.followContent) {
+            try {
+              const parsed = typeof info.followContent === 'string' ? JSON.parse(info.followContent) : info.followContent;
+              this.followUpDrawer.followContent = parsed.content || '';
+              this.followUpDrawer.dealLikelihood = parsed.dealLikelihood || '';
+              this.followUpDrawer.keyObstacle = parsed.keyObstacle || '';
+              this.followUpDrawer.followStrategy = parsed.followStrategy || '';
+              this.followUpDrawer.absenceReason = parsed.absenceReason || '';
+            } catch (e) {
+              // 兼容旧数据:非JSON格式直接作为content
+              this.followUpDrawer.followContent = info.followContent || '';
+            }
+          }
+          // 解析新增的3个可编辑字段(从tags逗号分隔中提取)
+          this.parseEditableTags(info.tags);
         } else {
         } else {
           this.followUpDrawer.visitRecords = [];
           this.followUpDrawer.visitRecords = [];
           this.followUpDrawer.recordings = [];
           this.followUpDrawer.recordings = [];
+          this.followUpDrawer.profile = { gender: null, age: null, height: null, weight: null };
         }
         }
       }).catch(() => {
       }).catch(() => {
         this.followUpDrawer.loading = false;
         this.followUpDrawer.loading = false;

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

@@ -1,23 +1,40 @@
 <template>
 <template>
   <div class="app-container">
   <div class="app-container">
     <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="88px">
     <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
           clearable
           size="small"
           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>
-      <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
           clearable
           size="small"
           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>
       <el-form-item label="用户ID" prop="userId">
       <el-form-item label="用户ID" prop="userId">
         <el-input
         <el-input
@@ -352,6 +369,7 @@ import { getCourseCouponUserListVO } from '@/api/course/courseCouponUser'
 import { listTagGroupForUserBindTag } from '@/api/app/tag/tagGroup'
 import { listTagGroupForUserBindTag } from '@/api/app/tag/tagGroup'
 import { bindTagV2, unbindTagV2 } from '@/api/app/tag/bindTag'
 import { bindTagV2, unbindTagV2 } from '@/api/app/tag/bindTag'
 import { batchUpdateFriendRemark } from '@/api/course/courseWatchLog'
 import { batchUpdateFriendRemark } from '@/api/course/courseWatchLog'
+import { getLiveGroupTypeList, getLiveOptions } from '@/api/app/live'
 
 
 export default {
 export default {
   name: 'LiveWatchUser',
   name: 'LiveWatchUser',
@@ -362,13 +380,15 @@ export default {
       total: 0,
       total: 0,
       list: [],
       list: [],
       ids: [],
       ids: [],
+      liveGroupTypeList: [],
+      liveOptions: [],
       liveRewardTypeList: [],
       liveRewardTypeList: [],
       dateRange: [],
       dateRange: [],
       queryParams: {
       queryParams: {
         pageNum: 1,
         pageNum: 1,
         pageSize: 10,
         pageSize: 10,
+        liveGroupType: null,
         liveId: null,
         liveId: null,
-        liveName: null,
         userId: null,
         userId: null,
         userName: null,
         userName: null,
         companyId: null,
         companyId: null,
@@ -431,9 +451,33 @@ export default {
     this.getDicts('live_watch_reward_type').then(res => {
     this.getDicts('live_watch_reward_type').then(res => {
       this.liveRewardTypeList = res.data || []
       this.liveRewardTypeList = res.data || []
     })
     })
+    this.loadLiveGroupTypeList()
+    this.loadLiveOptions()
     this.getList()
     this.getList()
   },
   },
   methods: {
   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) {
     handleSelectionChange(selection) {
       this.ids = [...new Set(selection.map(item => item.userId).filter(id => id != null))]
       this.ids = [...new Set(selection.map(item => item.userId).filter(id => id != null))]
     },
     },
@@ -693,14 +737,13 @@ export default {
         params.bTime = null
         params.bTime = null
         params.eTime = null
         params.eTime = null
       }
       }
-      ;['liveId', 'userId', 'companyId', 'companyUserId'].forEach(key => {
+      ;['liveId', 'userId', 'companyId', 'companyUserId', 'liveGroupType'].forEach(key => {
         if (params[key] === '' || params[key] === undefined) {
         if (params[key] === '' || params[key] === undefined) {
           params[key] = null
           params[key] = null
         } else if (params[key] != null) {
         } else if (params[key] != null) {
           params[key] = Number(params[key])
           params[key] = Number(params[key])
         }
         }
       })
       })
-      if (params.liveName === '') params.liveName = null
       if (params.userName === '') params.userName = null
       if (params.userName === '') params.userName = null
       return params
       return params
     },
     },
@@ -722,6 +765,9 @@ export default {
       this.resetForm('queryForm')
       this.resetForm('queryForm')
       this.queryParams.pageNum = 1
       this.queryParams.pageNum = 1
       this.queryParams.pageSize = 10
       this.queryParams.pageSize = 10
+      this.queryParams.liveGroupType = null
+      this.queryParams.liveId = null
+      this.loadLiveOptions()
       this.handleQuery()
       this.handleQuery()
     },
     },
     formatDuration(seconds) {
     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 !== ''){
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
         obj.tagIds = obj.tagIds.split(",");
         obj.tagIds = obj.tagIds.split(",");
       }
       }
+      if(obj.outTagIds && typeof obj.outTagIds === 'string'){
+        obj.outTagIds = obj.outTagIds.split(",");
+      }
       batchUpdateExternalContactNotes({
       batchUpdateExternalContactNotes({
         addType: 0,
         addType: 0,
         userIds: this.ids,
         userIds: this.ids,

+ 24 - 1
src/views/qw/externalContact/myExternalContact.vue

@@ -515,7 +515,7 @@
       <el-table-column label="注册时间" align="center" prop="registerTime" width="100px" />
       <el-table-column label="注册时间" align="center" prop="registerTime" width="100px" />
       <el-table-column label="备注电话号码" align="center" prop="remarkMobiles" width="150px">
       <el-table-column label="备注电话号码" align="center" prop="remarkMobiles" width="150px">
         <template slot-scope="scope">
         <template slot-scope="scope">
-          <div v-for="i in JSON.parse(scope.row.remarkMobiles)" :key="i">{{i}}</div>
+          <div v-for="i in safeParse(scope.row.remarkMobiles)" :key="i">{{i}}</div>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
       <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
@@ -1580,6 +1580,16 @@ export default {
     this.getDicts("sys_qw_external_contact_status").then(response => {
     this.getDicts("sys_qw_external_contact_status").then(response => {
       this.statusOptions = response.data;
       this.statusOptions = response.data;
     });
     });
+
+    // 支持从回访提醒跳转,自动搜索该用户
+    const queryCustomerId = this.$route.query.customerId;
+    const queryUserId = this.$route.query.userId;
+    if (queryCustomerId || queryUserId) {
+      this.$nextTick(() => {
+        this.queryParams.id = queryCustomerId || queryUserId || '';
+        this.handleQuery();
+      });
+    }
     this.getDicts("sys_qw_transfer_status").then(response => {
     this.getDicts("sys_qw_transfer_status").then(response => {
       this.transferStatusOptions = response.data;
       this.transferStatusOptions = response.data;
     });
     });
@@ -1589,6 +1599,16 @@ export default {
   },
   },
   methods: {
   methods: {
 
 
+      safeParse(str) {
+        if (!str) return []
+        try {
+          const parsed = JSON.parse(str)
+          return Array.isArray(parsed) ? parsed : [str]
+        } catch (e) {
+          return str.split(',').filter(Boolean)
+        }
+      },
+
       handleBatchUpdateLevel(){
       handleBatchUpdateLevel(){
 
 
           if (this.ids == null || this.ids == "") {
           if (this.ids == null || this.ids == "") {
@@ -2882,6 +2902,9 @@ export default {
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
       if(obj.tagIds !== null && obj.tagIds !== undefined && obj.tagIds !== ''){
         obj.tagIds = obj.tagIds.split(",");
         obj.tagIds = obj.tagIds.split(",");
       }
       }
+      if(obj.outTagIds && typeof obj.outTagIds === 'string'){
+        obj.outTagIds = obj.outTagIds.split(",");
+      }
       batchUpdateExternalContactNotes({
       batchUpdateExternalContactNotes({
         addType: 0,
         addType: 0,
         userIds: this.ids,
         userIds: this.ids,

+ 437 - 110
src/views/qw/sopTemp/deptIndex.vue

@@ -220,120 +220,218 @@
       @pagination="getList"
       @pagination="getList"
     />
     />
 
 
-    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="名称" prop="name">
-          <el-input v-model="form.name" placeholder="请输入模板标题"/>
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-radio-group v-model="form.status">
-            <el-radio
-              v-for="dict in statusOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="所属项目" prop="project" v-if="form.sendType != 5">
-          <el-select v-model="form.project" placeholder="请选择项目" filterable clearable size="small">
-            <el-option
-              v-for="dict in projectOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-            />
-          </el-select>
-        </el-form-item>
+    <el-dialog
+      :title="title"
+      :visible.sync="open"
+      width="1200px"
+      append-to-body
+      custom-class="sop-temp-dialog"
+      :close-on-click-modal="false"
+    >
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px" class="sop-temp-form">
+        <div class="form-section">
+          <div class="form-section__title">基本信息</div>
+          <el-row :gutter="28">
+            <el-col :span="12">
+              <el-form-item label="名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入模板标题" maxlength="50" show-word-limit />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="状态">
+                <el-radio-group v-model="form.status">
+                  <el-radio
+                    v-for="dict in statusOptions"
+                    :key="dict.dictValue"
+                    :label="dict.dictValue"
+                  >{{ dict.dictLabel }}</el-radio>
+                </el-radio-group>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12" v-if="form.sendType != 5">
+              <el-form-item label="所属项目" prop="project">
+                <el-select v-model="form.project" placeholder="请选择项目" filterable clearable class="w-full">
+                  <el-option
+                    v-for="dict in projectOptions"
+                    :key="dict.dictValue"
+                    :label="dict.dictLabel"
+                    :value="dict.dictValue"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="归属部门" prop="createByDept">
+                <treeselect
+                  class="dept-treeselect"
+                  :clearable="false"
+                  v-model="form.createByDept"
+                  :options="deptOptions"
+                  :show-count="true"
+                  placeholder="请选择归属部门"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12" v-if="form.sendType == 11 && !form.id">
+              <el-form-item label="课程" prop="courseId">
+                <el-select v-model="form.courseId" placeholder="请选择课程" filterable class="w-full">
+                  <el-option
+                    v-for="dict in courseList"
+                    :key="dict.dictValue"
+                    :label="dict.dictLabel"
+                    :value="parseInt(dict.dictValue)"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="间隔天数" prop="gap">
+                <el-input-number v-model="form.gap" :min="1" controls-position="right" class="num-input" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="排序" prop="sort">
+                <el-input-number v-model="form.sort" :min="0" controls-position="right" class="num-input" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
 
 
-        <el-form-item label="归属部门" prop="createByDept">
-          <treeselect
-            style="width: 220px"
-            :clearable="false"
-            v-model="form.createByDept"
-            :options="deptOptions"
-            clearable
-            :show-count="true"
-            placeholder="请选择归属部门"
-          />
-        </el-form-item>
-        <el-form-item label="课程" prop="courseId" v-if="form.sendType == 11 && !form.id">
-          <el-select v-model="form.courseId"placeholder="请选择课程" style=" margin-right: 10px;" size="mini" filterable>
-            <el-option
-              v-for="dict in courseList"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="parseInt(dict.dictValue)"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="间隔天数" prop="gap">
-          <el-input-number v-model="form.gap" :min="1" label="间隔天数"></el-input-number>
-        </el-form-item>
-        <el-form-item label="排序" prop="sort">
-          <el-input-number v-model="form.sort" :min="0" label="排序"></el-input-number>
-        </el-form-item>
-        <el-form-item label="是否开启官方群发" v-if="form.sendType == 11 && (form.id === null || form.id === undefined)">
-          <el-radio-group v-model="form.openOfficial">
-            <el-radio
-              v-for="dict in openOfficialOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="是否开启app看课" v-if="showAppCourseToggle && form.sendType == 11 && (form.id === null || form.id === undefined)">
-          <el-radio-group v-model="form.openAppCourse">
-            <el-radio
-              v-for="dict in openOfficialOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-
-        <el-form-item label="内容" prop="modeContent">
-          <el-input v-model="form.modeContent" placeholder="请输入文字内容"  type="textarea" :rows="3"/>
-        </el-form-item>
-        <el-form-item label="发课时间" prop="time" v-if="form.sendType == 11 && !form.id">
-          <el-time-picker
-            class="custom-input"
-            v-model="form.time"
-            value-format="HH:mm"
-            format="HH:mm"
-            placeholder="时间"
-            style="width: 200px;height: 20px;margin-left: 10px;margin-top: 10px">
-          </el-time-picker>
-        </el-form-item>
-        <el-form-item label="每天催课次数" prop="num" v-if="form.sendType == 11 && !form.id">
-          <el-input-number v-model="form.num" :min="0" label="每天催课次数" @change="sendNumChange"></el-input-number>
-        </el-form-item>
-        <el-form-item label="催课时间" v-if="form.sendType == 11 && !form.id && form.num > 0">
-          <div v-for="(item, index) in form.timeList" :key="index" style="margin-bottom: 10px;">
-            <el-time-picker
-              class="custom-input"
-              v-model="item.value"
-              value-format="HH:mm"
-              format="HH:mm"
-              :picker-options="{ selectableRange: startTimeRange }"
-              placeholder="时间"
-              style="width: 150px; height: 20px; margin-left: 10px; margin-top: 10px">
-            </el-time-picker>
+        <div
+          class="form-section"
+          v-if="form.sendType == 11 && (form.id === null || form.id === undefined)"
+        >
+          <div class="form-section__title">
+            发送能力
+            <span class="form-section__hint">默认小程序看课;开启后会叠加对应能力</span>
+          </div>
+          <div class="capability-grid">
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">官方群发</span>
+                <span class="capability-item__desc">首条走企微官方群发</span>
+              </div>
+              <el-switch v-model="form.openOfficial" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item" v-if="showAppCourseToggle">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">APP 看课</span>
+                <span class="capability-item__desc">与官方群发二选一</span>
+              </div>
+              <el-switch v-model="form.openAppCourse" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">飞书看课</span>
+                <span class="capability-item__desc">叠加飞书链接内容</span>
+              </div>
+              <el-switch v-model="form.openFeishu" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item" v-if="projectFrom === 'sxjz'">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">SIP 外呼</span>
+                <span class="capability-item__desc">叠加外呼催课任务</span>
+              </div>
+              <el-switch v-model="form.openSipCall" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">@所有人</span>
+                <span class="capability-item__desc">群消息是否 @所有人</span>
+              </div>
+              <el-switch v-model="form.openIsAtAll" active-value="1" inactive-value="0" />
+            </div>
+          </div>
+          <el-row :gutter="28" v-if="projectFrom === 'sxjz' && form.openSipCall == '1'" class="sip-task-row">
+            <el-col :span="12">
+              <el-form-item label="外呼任务" required>
+                <el-select
+                  v-model="form.aiCustomerRoleId"
+                  placeholder="请选择 SIP 外呼任务"
+                  filterable
+                  clearable
+                  class="w-full"
+                  @visible-change="onSipSelectVisible"
+                >
+                  <el-option
+                    v-for="task in aiSipCallTaskList"
+                    :key="task.batchId"
+                    :label="task.batchName"
+                    :value="task.batchId"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
 
 
+        <div class="form-section form-section--content">
+          <div class="form-section__title">内容与时间</div>
+          <el-form-item label="文案内容" prop="modeContent">
             <el-input
             <el-input
-              v-model="item.desc"
-              placeholder="催课内容"
-              type="textarea" :rows="2"
-              style="width: 500px; margin-left: 10px; margin-top: 10px;">
-            </el-input>
+              v-model="form.modeContent"
+              type="textarea"
+              :rows="4"
+              placeholder="发课时附带的文字内容"
+              maxlength="500"
+              show-word-limit
+            />
+          </el-form-item>
+          <el-row :gutter="28" v-if="form.sendType == 11 && !form.id">
+            <el-col :span="12">
+              <el-form-item label="发课时间" prop="time">
+                <el-time-picker
+                  v-model="form.time"
+                  value-format="HH:mm"
+                  format="HH:mm"
+                  placeholder="选择时间"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="催课次数" prop="num">
+                <el-input-number
+                  v-model="form.num"
+                  :min="0"
+                  class="num-input"
+                  @change="sendNumChange"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <div v-if="form.sendType == 11 && !form.id && form.num > 0" class="remind-list">
+            <div class="remind-list__label">催课安排</div>
+            <div class="remind-grid">
+              <div
+                v-for="(item, index) in form.timeList"
+                :key="index"
+                class="remind-row"
+              >
+                <span class="remind-row__index">第 {{ index + 1 }} 次</span>
+                <el-time-picker
+                  v-model="item.value"
+                  value-format="HH:mm"
+                  format="HH:mm"
+                  :picker-options="{ selectableRange: startTimeRange }"
+                  placeholder="催课时间"
+                  class="remind-row__time"
+                />
+                <el-input
+                  v-model="item.desc"
+                  type="textarea"
+                  :rows="2"
+                  placeholder="催课文案"
+                  class="remind-row__desc"
+                />
+              </div>
+            </div>
           </div>
           </div>
-        </el-form-item>
+        </div>
       </el-form>
       </el-form>
-      <div slot="footer" class="dialog-footer" style="display: flex;justify-content: flex-end;">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="medium" @click="cancel">取 消</el-button>
+        <el-button type="primary" size="medium" @click="submitForm">确 定</el-button>
       </div>
       </div>
     </el-dialog>
     </el-dialog>
 
 
@@ -427,7 +525,7 @@ import {
   redList,
   redList,
   shareSopTemp,
   shareSopTemp,
   updateRedPackage,
   updateRedPackage,
-  updateTemp, listSopTempDeptList
+  updateTemp, listSopTempDeptList, getConfigByKey
 } from "../../../api/qw/sopTemp";
 } from "../../../api/qw/sopTemp";
 import { getCompanyList, listCompany } from '@/api/company/company'
 import { getCompanyList, listCompany } from '@/api/company/company'
 import {courseList, getRoles} from "@/api/qw/sop";
 import {courseList, getRoles} from "@/api/qw/sop";
@@ -435,12 +533,14 @@ import {treeselect} from "../../../api/company/companyDept";
 import Treeselect from "@riophae/vue-treeselect";
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import {getCompanyUserListLikeNameByDept} from "../../../api/company/companyUser";
 import {getCompanyUserListLikeNameByDept} from "../../../api/company/companyUser";
+import { listAiSipCallTask } from "@/api/aiSipCall/aiSipCallTask";
 export default {
 export default {
   name: "SopTemp",
   name: "SopTemp",
   components: {Treeselect},
   components: {Treeselect},
   data() {
   data() {
     return {
     return {
       projectFrom:process.env.VUE_APP_PROJECT_FROM,
       projectFrom:process.env.VUE_APP_PROJECT_FROM,
+      aiSipCallTaskList: [],
       // 遮罩层
       // 遮罩层
       loading: true,
       loading: true,
       companysloading: false,
       companysloading: false,
@@ -608,9 +708,32 @@ export default {
       if (this.showAppCourseToggle && newVal === '1') {
       if (this.showAppCourseToggle && newVal === '1') {
         this.$set(this.form, 'openOfficial', '0');
         this.$set(this.form, 'openOfficial', '0');
       }
       }
+    },
+    'form.openSipCall'(newVal) {
+      if (newVal !== '1') {
+        this.$set(this.form, 'aiCustomerRoleId', null);
+      } else {
+        this.loadSipTemplates();
+      }
     }
     }
   },
   },
   methods: {
   methods: {
+    onSipSelectVisible(visible) {
+      if (visible) {
+        this.loadSipTemplates();
+      }
+    },
+    loadSipTemplates() {
+      if (this.aiSipCallTaskList.length > 0) {
+        return;
+      }
+      listAiSipCallTask({ taskType: 1, status: 0, pageNum: 1, pageSize: 100 }).then(response => {
+        this.aiSipCallTaskList = response.rows || response.data || [];
+      }).catch(() => {
+        this.$message.error('加载SIP外呼任务失败');
+      });
+    },
+
 
 
     handleCompanyUserChange(value) {
     handleCompanyUserChange(value) {
       // 当清空选择时,将空字符串转为 null
       // 当清空选择时,将空字符串转为 null
@@ -729,6 +852,10 @@ export default {
         sort: 0,
         sort: 0,
         openOfficial: "1",
         openOfficial: "1",
         openAppCourse: "0", //默认 app 看课为关
         openAppCourse: "0", //默认 app 看课为关
+        openSipCall: "0",
+        openFeishu: "0",
+        aiCustomerRoleId: null,
+        openIsAtAll: "0",
         time: "",
         time: "",
         num: 1,
         num: 1,
         timeList: [{value: "",desc:""}],
         timeList: [{value: "",desc:""}],
@@ -891,6 +1018,16 @@ export default {
               return;
               return;
             }
             }
           }
           }
+          if (this.form.sendType === 11 && !this.form.id) {
+            if (this.projectFrom === 'sxjz' && this.form.openSipCall === '1' && !this.form.aiCustomerRoleId) {
+              this.$message.error("请选择SIP外呼任务!");
+              return;
+            }
+            if (this.projectFrom !== 'sxjz') {
+              this.form.openSipCall = '0';
+              this.form.aiCustomerRoleId = null;
+            }
+          }
           if (this.command != 2 && this.form.id == null && this.form.sendType == 11){
           if (this.command != 2 && this.form.id == null && this.form.sendType == 11){
 
 
             const hasEmptyFields = this.form.timeList.some(item => {
             const hasEmptyFields = this.form.timeList.some(item => {
@@ -1021,3 +1158,193 @@ export default {
   }
   }
 };
 };
 </script>
 </script>
+<style scoped>
+.sop-temp-form {
+  max-height: 72vh;
+  overflow-y: auto;
+  padding-right: 6px;
+}
+
+.form-section {
+  margin-bottom: 24px;
+  padding: 28px 32px 16px;
+  background: #fafbfc;
+  border: 1px solid #ebeef5;
+  border-radius: 12px;
+}
+
+.form-section:last-child {
+  margin-bottom: 4px;
+}
+
+.form-section__title {
+  display: flex;
+  align-items: baseline;
+  gap: 14px;
+  margin: 0 0 24px;
+  padding-left: 12px;
+  border-left: 4px solid #409eff;
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2d3d;
+  line-height: 1.3;
+  letter-spacing: 0.02em;
+}
+
+.form-section__hint {
+  font-size: 13px;
+  font-weight: 400;
+  color: #909399;
+}
+
+.w-full {
+  width: 100%;
+}
+
+.num-input {
+  width: 100%;
+}
+
+.num-input >>> .el-input__inner {
+  text-align: left;
+}
+
+.dept-treeselect {
+  width: 100%;
+  line-height: 40px;
+}
+
+.capability-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 16px;
+  margin-bottom: 4px;
+}
+
+.capability-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  min-height: 88px;
+  padding: 20px 18px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+}
+
+.capability-item__meta {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  min-width: 0;
+}
+
+.capability-item__name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.capability-item__desc {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.45;
+}
+
+.sip-task-row {
+  margin-top: 18px;
+}
+
+.remind-list {
+  margin: 4px 0 8px 100px;
+}
+
+.remind-list__label {
+  margin-bottom: 12px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.remind-grid {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 14px;
+}
+
+.remind-row {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 14px 16px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+}
+
+.remind-row__index {
+  flex: 0 0 58px;
+  margin-top: 8px;
+  font-size: 13px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.remind-row__time {
+  flex: 0 0 130px;
+}
+
+.remind-row__desc {
+  flex: 1;
+  min-width: 0;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>
+
+<style>
+.sop-temp-dialog {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.sop-temp-dialog .el-dialog__header {
+  padding: 24px 32px 18px;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.sop-temp-dialog .el-dialog__title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1f2d3d;
+  letter-spacing: 0.02em;
+}
+
+.sop-temp-dialog .el-dialog__body {
+  padding: 28px 32px 18px;
+}
+
+.sop-temp-dialog .el-dialog__footer {
+  padding: 18px 32px 24px;
+  border-top: 1px solid #ebeef5;
+}
+
+.sop-temp-dialog .el-form-item {
+  margin-bottom: 22px;
+}
+
+.sop-temp-dialog .el-input__inner,
+.sop-temp-dialog .el-textarea__inner {
+  border-radius: 6px;
+}
+
+.sop-temp-dialog .el-button--medium {
+  min-width: 104px;
+  padding: 11px 24px;
+}
+</style>

+ 436 - 121
src/views/qw/sopTemp/index.vue

@@ -229,131 +229,218 @@
       @pagination="getList"
       @pagination="getList"
     />
     />
 
 
-    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="名称" prop="name">
-          <el-input v-model="form.name" placeholder="请输入模板标题"/>
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-radio-group v-model="form.status">
-            <el-radio
-              v-for="dict in statusOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="所属项目" prop="project" v-if="form.sendType != 5">
-          <el-select v-model="form.project" placeholder="请选择项目" filterable clearable size="small">
-            <el-option
-              v-for="dict in projectOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-            />
-          </el-select>
-        </el-form-item>
+    <el-dialog
+      :title="title"
+      :visible.sync="open"
+      width="1200px"
+      append-to-body
+      custom-class="sop-temp-dialog"
+      :close-on-click-modal="false"
+    >
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px" class="sop-temp-form">
+        <div class="form-section">
+          <div class="form-section__title">基本信息</div>
+          <el-row :gutter="28">
+            <el-col :span="12">
+              <el-form-item label="名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入模板标题" maxlength="50" show-word-limit />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="状态">
+                <el-radio-group v-model="form.status">
+                  <el-radio
+                    v-for="dict in statusOptions"
+                    :key="dict.dictValue"
+                    :label="dict.dictValue"
+                  >{{ dict.dictLabel }}</el-radio>
+                </el-radio-group>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12" v-if="form.sendType != 5">
+              <el-form-item label="所属项目" prop="project">
+                <el-select v-model="form.project" placeholder="请选择项目" filterable clearable class="w-full">
+                  <el-option
+                    v-for="dict in projectOptions"
+                    :key="dict.dictValue"
+                    :label="dict.dictLabel"
+                    :value="dict.dictValue"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="归属部门" prop="createByDept">
+                <treeselect
+                  class="dept-treeselect"
+                  :clearable="false"
+                  v-model="form.createByDept"
+                  :options="deptOptions"
+                  :show-count="true"
+                  placeholder="请选择归属部门"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12" v-if="form.sendType == 11 && !form.id">
+              <el-form-item label="课程" prop="courseId">
+                <el-select v-model="form.courseId" placeholder="请选择课程" filterable class="w-full">
+                  <el-option
+                    v-for="dict in courseList"
+                    :key="dict.dictValue"
+                    :label="dict.dictLabel"
+                    :value="parseInt(dict.dictValue)"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="间隔天数" prop="gap">
+                <el-input-number v-model="form.gap" :min="1" controls-position="right" class="num-input" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="排序" prop="sort">
+                <el-input-number v-model="form.sort" :min="0" controls-position="right" class="num-input" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
 
 
-        <el-form-item label="归属部门" prop="createByDept">
-          <treeselect
-            style="width: 220px"
-            :clearable="false"
-            v-model="form.createByDept"
-            :options="deptOptions"
-            clearable
-            :show-count="true"
-            placeholder="请选择归属部门"
-          />
-        </el-form-item>
-        <el-form-item label="课程" prop="courseId" v-if="form.sendType == 11 && !form.id">
-          <el-select v-model="form.courseId"placeholder="请选择课程" style=" margin-right: 10px;" size="mini" filterable>
-            <el-option
-              v-for="dict in courseList"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="parseInt(dict.dictValue)"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="间隔天数" prop="gap">
-          <el-input-number v-model="form.gap" :min="1" label="间隔天数"></el-input-number>
-        </el-form-item>
-        <el-form-item label="排序" prop="sort">
-          <el-input-number v-model="form.sort" :min="0" label="排序"></el-input-number>
-        </el-form-item>
-        <el-form-item label="是否开启官方群发" v-if="form.sendType == 11 && (form.id === null || form.id === undefined)">
-          <el-radio-group v-model="form.openOfficial">
-            <el-radio
-              v-for="dict in openOfficialOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="是否开启app看课" v-if="showAppCourseToggle && form.sendType == 11 && (form.id === null || form.id === undefined)">
-          <el-radio-group v-model="form.openAppCourse">
-            <el-radio
-              v-for="dict in openOfficialOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-
-        <el-form-item label="是否开启@所有人" v-if="form.sendType == 11 && (form.id === null || form.id === undefined)">
-          <el-radio-group v-model="form.openIsAtAll">
-            <el-radio
-              v-for="dict in openIsAtAllOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-
-        <el-form-item label="内容" prop="modeContent">
-          <el-input v-model="form.modeContent" placeholder="请输入文字内容"  type="textarea" :rows="3"/>
-        </el-form-item>
-        <el-form-item label="发课时间" prop="time" v-if="form.sendType == 11 && !form.id">
-          <el-time-picker
-            class="custom-input"
-            v-model="form.time"
-            value-format="HH:mm"
-            format="HH:mm"
-            placeholder="时间"
-            style="width: 200px;height: 20px;margin-left: 10px;margin-top: 10px">
-          </el-time-picker>
-        </el-form-item>
-        <el-form-item label="每天催课次数" prop="num" v-if="form.sendType == 11 && !form.id">
-          <el-input-number v-model="form.num" :min="0" label="每天催课次数" @change="sendNumChange"></el-input-number>
-        </el-form-item>
-        <el-form-item label="催课时间" v-if="form.sendType == 11 && !form.id && form.num > 0">
-          <div v-for="(item, index) in form.timeList" :key="index" style="margin-bottom: 10px;">
-            <el-time-picker
-              class="custom-input"
-              v-model="item.value"
-              value-format="HH:mm"
-              format="HH:mm"
-              :picker-options="{ selectableRange: startTimeRange }"
-              placeholder="时间"
-              style="width: 150px; height: 20px; margin-left: 10px; margin-top: 10px">
-            </el-time-picker>
+        <div
+          class="form-section"
+          v-if="form.sendType == 11 && (form.id === null || form.id === undefined)"
+        >
+          <div class="form-section__title">
+            发送能力
+            <span class="form-section__hint">默认小程序看课;开启后会叠加对应能力</span>
+          </div>
+          <div class="capability-grid">
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">官方群发</span>
+                <span class="capability-item__desc">首条走企微官方群发</span>
+              </div>
+              <el-switch v-model="form.openOfficial" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item" v-if="showAppCourseToggle">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">APP 看课</span>
+                <span class="capability-item__desc">与官方群发二选一</span>
+              </div>
+              <el-switch v-model="form.openAppCourse" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">飞书看课</span>
+                <span class="capability-item__desc">叠加飞书链接内容</span>
+              </div>
+              <el-switch v-model="form.openFeishu" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item" v-if="projectFrom === 'sxjz'">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">SIP 外呼</span>
+                <span class="capability-item__desc">叠加外呼催课任务</span>
+              </div>
+              <el-switch v-model="form.openSipCall" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">@所有人</span>
+                <span class="capability-item__desc">群消息是否 @所有人</span>
+              </div>
+              <el-switch v-model="form.openIsAtAll" active-value="1" inactive-value="0" />
+            </div>
+          </div>
+          <el-row :gutter="28" v-if="projectFrom === 'sxjz' && form.openSipCall == '1'" class="sip-task-row">
+            <el-col :span="12">
+              <el-form-item label="外呼任务" required>
+                <el-select
+                  v-model="form.aiCustomerRoleId"
+                  placeholder="请选择 SIP 外呼任务"
+                  filterable
+                  clearable
+                  class="w-full"
+                  @visible-change="onSipSelectVisible"
+                >
+                  <el-option
+                    v-for="task in aiSipCallTaskList"
+                    :key="task.batchId"
+                    :label="task.batchName"
+                    :value="task.batchId"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
 
 
+        <div class="form-section form-section--content">
+          <div class="form-section__title">内容与时间</div>
+          <el-form-item label="文案内容" prop="modeContent">
             <el-input
             <el-input
-              v-model="item.desc"
-              placeholder="催课内容"
-              type="textarea" :rows="2"
-              style="width: 500px; margin-left: 10px; margin-top: 10px;">
-            </el-input>
+              v-model="form.modeContent"
+              type="textarea"
+              :rows="4"
+              placeholder="发课时附带的文字内容"
+              maxlength="500"
+              show-word-limit
+            />
+          </el-form-item>
+          <el-row :gutter="28" v-if="form.sendType == 11 && !form.id">
+            <el-col :span="12">
+              <el-form-item label="发课时间" prop="time">
+                <el-time-picker
+                  v-model="form.time"
+                  value-format="HH:mm"
+                  format="HH:mm"
+                  placeholder="选择时间"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="催课次数" prop="num">
+                <el-input-number
+                  v-model="form.num"
+                  :min="0"
+                  class="num-input"
+                  @change="sendNumChange"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <div v-if="form.sendType == 11 && !form.id && form.num > 0" class="remind-list">
+            <div class="remind-list__label">催课安排</div>
+            <div class="remind-grid">
+              <div
+                v-for="(item, index) in form.timeList"
+                :key="index"
+                class="remind-row"
+              >
+                <span class="remind-row__index">第 {{ index + 1 }} 次</span>
+                <el-time-picker
+                  v-model="item.value"
+                  value-format="HH:mm"
+                  format="HH:mm"
+                  :picker-options="{ selectableRange: startTimeRange }"
+                  placeholder="催课时间"
+                  class="remind-row__time"
+                />
+                <el-input
+                  v-model="item.desc"
+                  type="textarea"
+                  :rows="2"
+                  placeholder="催课文案"
+                  class="remind-row__desc"
+                />
+              </div>
+            </div>
           </div>
           </div>
-        </el-form-item>
+        </div>
       </el-form>
       </el-form>
-      <div slot="footer" class="dialog-footer" style="display: flex;justify-content: flex-end;">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="medium" @click="cancel">取 消</el-button>
+        <el-button type="primary" size="medium" @click="submitForm">确 定</el-button>
       </div>
       </div>
     </el-dialog>
     </el-dialog>
 
 
@@ -470,12 +557,14 @@ import {treeselect} from "../../../api/company/companyDept";
 import Treeselect from "@riophae/vue-treeselect";
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import {getCompanyUserListLikeName} from "../../../api/company/companyUser";
 import {getCompanyUserListLikeName} from "../../../api/company/companyUser";
+import { listAiSipCallTask } from "@/api/aiSipCall/aiSipCallTask";
 export default {
 export default {
   name: "SopTemp",
   name: "SopTemp",
   components: {Treeselect},
   components: {Treeselect},
   data() {
   data() {
     return {
     return {
        projectFrom:process.env.VUE_APP_PROJECT_FROM,
        projectFrom:process.env.VUE_APP_PROJECT_FROM,
+       aiSipCallTaskList: [],
 
 
       officialForm:{
       officialForm:{
         tempId:null,
         tempId:null,
@@ -654,9 +743,31 @@ export default {
       if (this.showAppCourseToggle && newVal === '1') {
       if (this.showAppCourseToggle && newVal === '1') {
         this.$set(this.form, 'openOfficial', '0');
         this.$set(this.form, 'openOfficial', '0');
       }
       }
+    },
+    'form.openSipCall'(newVal) {
+      if (newVal !== '1') {
+        this.$set(this.form, 'aiCustomerRoleId', null);
+      } else {
+        this.loadSipTemplates();
+      }
     }
     }
   },
   },
   methods: {
   methods: {
+    onSipSelectVisible(visible) {
+      if (visible) {
+        this.loadSipTemplates();
+      }
+    },
+    loadSipTemplates() {
+      if (this.aiSipCallTaskList.length > 0) {
+        return;
+      }
+      listAiSipCallTask({ taskType: 1, status: 0, pageNum: 1, pageSize: 100 }).then(response => {
+        this.aiSipCallTaskList = response.rows || response.data || [];
+      }).catch(() => {
+        this.$message.error('加载SIP外呼任务失败');
+      });
+    },
 
 
     handleCompanyUserChange(value) {
     handleCompanyUserChange(value) {
       // 当清空选择时,将空字符串转为 null
       // 当清空选择时,将空字符串转为 null
@@ -774,7 +885,10 @@ export default {
         sort: 0,
         sort: 0,
         openOfficial: "1",
         openOfficial: "1",
         openAppCourse: "0", //默认 app 看课为关
         openAppCourse: "0", //默认 app 看课为关
-        openIsAtAll: "1",
+        openSipCall: "0",
+        openFeishu: "0",
+        aiCustomerRoleId: null,
+        openIsAtAll: "0",
         time: "",
         time: "",
         num: 1,
         num: 1,
         timeList: [{value: "",desc:""}],
         timeList: [{value: "",desc:""}],
@@ -953,6 +1067,16 @@ export default {
               return;
               return;
             }
             }
           }
           }
+          if (this.form.sendType === 11 && !this.form.id) {
+            if (this.projectFrom === 'sxjz' && this.form.openSipCall === '1' && !this.form.aiCustomerRoleId) {
+              this.$message.error("请选择SIP外呼任务!");
+              return;
+            }
+            if (this.projectFrom !== 'sxjz') {
+              this.form.openSipCall = '0';
+              this.form.aiCustomerRoleId = null;
+            }
+          }
           if (this.command != 2 && this.form.id == null && this.form.sendType == 11){
           if (this.command != 2 && this.form.id == null && this.form.sendType == 11){
 
 
             const hasEmptyFields = this.form.timeList.some(item => {
             const hasEmptyFields = this.form.timeList.some(item => {
@@ -1105,3 +1229,194 @@ export default {
   }
   }
 };
 };
 </script>
 </script>
+
+<style scoped>
+.sop-temp-form {
+  max-height: 72vh;
+  overflow-y: auto;
+  padding-right: 6px;
+}
+
+.form-section {
+  margin-bottom: 24px;
+  padding: 28px 32px 16px;
+  background: #fafbfc;
+  border: 1px solid #ebeef5;
+  border-radius: 12px;
+}
+
+.form-section:last-child {
+  margin-bottom: 4px;
+}
+
+.form-section__title {
+  display: flex;
+  align-items: baseline;
+  gap: 14px;
+  margin: 0 0 24px;
+  padding-left: 12px;
+  border-left: 4px solid #409eff;
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2d3d;
+  line-height: 1.3;
+  letter-spacing: 0.02em;
+}
+
+.form-section__hint {
+  font-size: 13px;
+  font-weight: 400;
+  color: #909399;
+}
+
+.w-full {
+  width: 100%;
+}
+
+.num-input {
+  width: 100%;
+}
+
+.num-input >>> .el-input__inner {
+  text-align: left;
+}
+
+.dept-treeselect {
+  width: 100%;
+  line-height: 40px;
+}
+
+.capability-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 16px;
+  margin-bottom: 4px;
+}
+
+.capability-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  min-height: 88px;
+  padding: 20px 18px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+}
+
+.capability-item__meta {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  min-width: 0;
+}
+
+.capability-item__name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.capability-item__desc {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.45;
+}
+
+.sip-task-row {
+  margin-top: 18px;
+}
+
+.remind-list {
+  margin: 4px 0 8px 100px;
+}
+
+.remind-list__label {
+  margin-bottom: 12px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.remind-grid {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 14px;
+}
+
+.remind-row {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 14px 16px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+}
+
+.remind-row__index {
+  flex: 0 0 58px;
+  margin-top: 8px;
+  font-size: 13px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.remind-row__time {
+  flex: 0 0 130px;
+}
+
+.remind-row__desc {
+  flex: 1;
+  min-width: 0;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>
+
+<style>
+.sop-temp-dialog {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.sop-temp-dialog .el-dialog__header {
+  padding: 24px 32px 18px;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.sop-temp-dialog .el-dialog__title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1f2d3d;
+  letter-spacing: 0.02em;
+}
+
+.sop-temp-dialog .el-dialog__body {
+  padding: 28px 32px 18px;
+}
+
+.sop-temp-dialog .el-dialog__footer {
+  padding: 18px 32px 24px;
+  border-top: 1px solid #ebeef5;
+}
+
+.sop-temp-dialog .el-form-item {
+  margin-bottom: 22px;
+}
+
+.sop-temp-dialog .el-input__inner,
+.sop-temp-dialog .el-textarea__inner {
+  border-radius: 6px;
+}
+
+.sop-temp-dialog .el-button--medium {
+  min-width: 104px;
+  padding: 11px 24px;
+}
+</style>

+ 436 - 110
src/views/qw/sopTemp/myIndex.vue

@@ -203,120 +203,218 @@
       @pagination="getMyList"
       @pagination="getMyList"
     />
     />
 
 
-    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="名称" prop="name">
-          <el-input v-model="form.name" placeholder="请输入模板标题"/>
-        </el-form-item>
-        <el-form-item label="状态">
-          <el-radio-group v-model="form.status">
-            <el-radio
-              v-for="dict in statusOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="所属项目" prop="project" v-if="form.sendType != 5">
-          <el-select v-model="form.project" placeholder="请选择项目" filterable clearable size="small">
-            <el-option
-              v-for="dict in projectOptions"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="dict.dictValue"
-            />
-          </el-select>
-        </el-form-item>
+    <el-dialog
+      :title="title"
+      :visible.sync="open"
+      width="1200px"
+      append-to-body
+      custom-class="sop-temp-dialog"
+      :close-on-click-modal="false"
+    >
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px" class="sop-temp-form">
+        <div class="form-section">
+          <div class="form-section__title">基本信息</div>
+          <el-row :gutter="28">
+            <el-col :span="12">
+              <el-form-item label="名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入模板标题" maxlength="50" show-word-limit />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="状态">
+                <el-radio-group v-model="form.status">
+                  <el-radio
+                    v-for="dict in statusOptions"
+                    :key="dict.dictValue"
+                    :label="dict.dictValue"
+                  >{{ dict.dictLabel }}</el-radio>
+                </el-radio-group>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12" v-if="form.sendType != 5">
+              <el-form-item label="所属项目" prop="project">
+                <el-select v-model="form.project" placeholder="请选择项目" filterable clearable class="w-full">
+                  <el-option
+                    v-for="dict in projectOptions"
+                    :key="dict.dictValue"
+                    :label="dict.dictLabel"
+                    :value="dict.dictValue"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="归属部门" prop="createByDept">
+                <treeselect
+                  class="dept-treeselect"
+                  :clearable="false"
+                  v-model="form.createByDept"
+                  :options="deptOptions"
+                  :show-count="true"
+                  placeholder="请选择归属部门"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12" v-if="form.sendType == 11 && !form.id">
+              <el-form-item label="课程" prop="courseId">
+                <el-select v-model="form.courseId" placeholder="请选择课程" filterable class="w-full">
+                  <el-option
+                    v-for="dict in courseList"
+                    :key="dict.dictValue"
+                    :label="dict.dictLabel"
+                    :value="parseInt(dict.dictValue)"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="间隔天数" prop="gap">
+                <el-input-number v-model="form.gap" :min="1" controls-position="right" class="num-input" />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="排序" prop="sort">
+                <el-input-number v-model="form.sort" :min="0" controls-position="right" class="num-input" />
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
 
 
-        <el-form-item label="归属部门" prop="createByDept">
-          <treeselect
-            style="width: 220px"
-            :clearable="false"
-            v-model="form.createByDept"
-            :options="deptOptions"
-            clearable
-            :show-count="true"
-            placeholder="请选择归属部门"
-          />
-        </el-form-item>
-        <el-form-item label="课程" prop="courseId" v-if="form.sendType == 11 && !form.id">
-          <el-select v-model="form.courseId"placeholder="请选择课程" style=" margin-right: 10px;" size="mini" filterable>
-            <el-option
-              v-for="dict in courseList"
-              :key="dict.dictValue"
-              :label="dict.dictLabel"
-              :value="parseInt(dict.dictValue)"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="间隔天数" prop="gap">
-          <el-input-number v-model="form.gap" :min="1" label="间隔天数"></el-input-number>
-        </el-form-item>
-        <el-form-item label="排序" prop="sort">
-          <el-input-number v-model="form.sort" :min="0" label="排序"></el-input-number>
-        </el-form-item>
-        <el-form-item label="是否开启官方群发" v-if="form.sendType == 11 && (form.id === null || form.id === undefined)">
-          <el-radio-group v-model="form.openOfficial">
-            <el-radio
-              v-for="dict in openOfficialOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-        <el-form-item label="是否开启app看课" v-if="showAppCourseToggle && form.sendType == 11 && (form.id === null || form.id === undefined)">
-          <el-radio-group v-model="form.openAppCourse">
-            <el-radio
-              v-for="dict in openOfficialOptions"
-              :key="dict.dictValue"
-              :label="dict.dictValue"
-            >{{ dict.dictLabel }}
-            </el-radio>
-          </el-radio-group>
-        </el-form-item>
-
-        <el-form-item label="内容" prop="modeContent">
-          <el-input v-model="form.modeContent" placeholder="请输入文字内容"  type="textarea" :rows="3"/>
-        </el-form-item>
-        <el-form-item label="发课时间" prop="time" v-if="form.sendType == 11 && !form.id">
-          <el-time-picker
-            class="custom-input"
-            v-model="form.time"
-            value-format="HH:mm"
-            format="HH:mm"
-            placeholder="时间"
-            style="width: 200px;height: 20px;margin-left: 10px;margin-top: 10px">
-          </el-time-picker>
-        </el-form-item>
-        <el-form-item label="每天催课次数" prop="num" v-if="form.sendType == 11 && !form.id">
-          <el-input-number v-model="form.num" :min="0" label="每天催课次数" @change="sendNumChange"></el-input-number>
-        </el-form-item>
-        <el-form-item label="催课时间" v-if="form.sendType == 11 && !form.id && form.num > 0">
-          <div v-for="(item, index) in form.timeList" :key="index" style="margin-bottom: 10px;">
-            <el-time-picker
-              class="custom-input"
-              v-model="item.value"
-              value-format="HH:mm"
-              format="HH:mm"
-              :picker-options="{ selectableRange: startTimeRange }"
-              placeholder="时间"
-              style="width: 150px; height: 20px; margin-left: 10px; margin-top: 10px">
-            </el-time-picker>
+        <div
+          class="form-section"
+          v-if="form.sendType == 11 && (form.id === null || form.id === undefined)"
+        >
+          <div class="form-section__title">
+            发送能力
+            <span class="form-section__hint">默认小程序看课;开启后会叠加对应能力</span>
+          </div>
+          <div class="capability-grid">
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">官方群发</span>
+                <span class="capability-item__desc">首条走企微官方群发</span>
+              </div>
+              <el-switch v-model="form.openOfficial" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item" v-if="showAppCourseToggle">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">APP 看课</span>
+                <span class="capability-item__desc">与官方群发二选一</span>
+              </div>
+              <el-switch v-model="form.openAppCourse" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">飞书看课</span>
+                <span class="capability-item__desc">叠加飞书链接内容</span>
+              </div>
+              <el-switch v-model="form.openFeishu" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item" v-if="projectFrom === 'sxjz'">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">SIP 外呼</span>
+                <span class="capability-item__desc">叠加外呼催课任务</span>
+              </div>
+              <el-switch v-model="form.openSipCall" active-value="1" inactive-value="0" />
+            </div>
+            <div class="capability-item">
+              <div class="capability-item__meta">
+                <span class="capability-item__name">@所有人</span>
+                <span class="capability-item__desc">群消息是否 @所有人</span>
+              </div>
+              <el-switch v-model="form.openIsAtAll" active-value="1" inactive-value="0" />
+            </div>
+          </div>
+          <el-row :gutter="28" v-if="projectFrom === 'sxjz' && form.openSipCall == '1'" class="sip-task-row">
+            <el-col :span="12">
+              <el-form-item label="外呼任务" required>
+                <el-select
+                  v-model="form.aiCustomerRoleId"
+                  placeholder="请选择 SIP 外呼任务"
+                  filterable
+                  clearable
+                  class="w-full"
+                  @visible-change="onSipSelectVisible"
+                >
+                  <el-option
+                    v-for="task in aiSipCallTaskList"
+                    :key="task.batchId"
+                    :label="task.batchName"
+                    :value="task.batchId"
+                  />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+        </div>
 
 
+        <div class="form-section form-section--content">
+          <div class="form-section__title">内容与时间</div>
+          <el-form-item label="文案内容" prop="modeContent">
             <el-input
             <el-input
-              v-model="item.desc"
-              placeholder="催课内容"
-              type="textarea" :rows="2"
-              style="width: 500px; margin-left: 10px; margin-top: 10px;">
-            </el-input>
+              v-model="form.modeContent"
+              type="textarea"
+              :rows="4"
+              placeholder="发课时附带的文字内容"
+              maxlength="500"
+              show-word-limit
+            />
+          </el-form-item>
+          <el-row :gutter="28" v-if="form.sendType == 11 && !form.id">
+            <el-col :span="12">
+              <el-form-item label="发课时间" prop="time">
+                <el-time-picker
+                  v-model="form.time"
+                  value-format="HH:mm"
+                  format="HH:mm"
+                  placeholder="选择时间"
+                  class="w-full"
+                />
+              </el-form-item>
+            </el-col>
+            <el-col :span="12">
+              <el-form-item label="催课次数" prop="num">
+                <el-input-number
+                  v-model="form.num"
+                  :min="0"
+                  class="num-input"
+                  @change="sendNumChange"
+                />
+              </el-form-item>
+            </el-col>
+          </el-row>
+          <div v-if="form.sendType == 11 && !form.id && form.num > 0" class="remind-list">
+            <div class="remind-list__label">催课安排</div>
+            <div class="remind-grid">
+              <div
+                v-for="(item, index) in form.timeList"
+                :key="index"
+                class="remind-row"
+              >
+                <span class="remind-row__index">第 {{ index + 1 }} 次</span>
+                <el-time-picker
+                  v-model="item.value"
+                  value-format="HH:mm"
+                  format="HH:mm"
+                  :picker-options="{ selectableRange: startTimeRange }"
+                  placeholder="催课时间"
+                  class="remind-row__time"
+                />
+                <el-input
+                  v-model="item.desc"
+                  type="textarea"
+                  :rows="2"
+                  placeholder="催课文案"
+                  class="remind-row__desc"
+                />
+              </div>
+            </div>
           </div>
           </div>
-        </el-form-item>
+        </div>
       </el-form>
       </el-form>
-      <div slot="footer" class="dialog-footer" style="display: flex;justify-content: flex-end;">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="medium" @click="cancel">取 消</el-button>
+        <el-button type="primary" size="medium" @click="submitForm">确 定</el-button>
       </div>
       </div>
     </el-dialog>
     </el-dialog>
 
 
@@ -417,6 +515,7 @@ import {courseList, getRoles} from "@/api/qw/sop";
 import {treeselect} from "../../../api/company/companyDept";
 import {treeselect} from "../../../api/company/companyDept";
 import Treeselect from "@riophae/vue-treeselect";
 import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+import { listAiSipCallTask } from "@/api/aiSipCall/aiSipCallTask";
 export default {
 export default {
   name: "SopTemp",
   name: "SopTemp",
   components: {Treeselect},
   components: {Treeselect},
@@ -424,6 +523,7 @@ export default {
     return {
     return {
 
 
       projectFrom:process.env.VUE_APP_PROJECT_FROM,
       projectFrom:process.env.VUE_APP_PROJECT_FROM,
+      aiSipCallTaskList: [],
       // 遮罩层
       // 遮罩层
       loading: true,
       loading: true,
       companysloading: false,
       companysloading: false,
@@ -578,9 +678,31 @@ export default {
       if (this.showAppCourseToggle && newVal === '1') {
       if (this.showAppCourseToggle && newVal === '1') {
         this.$set(this.form, 'openOfficial', '0');
         this.$set(this.form, 'openOfficial', '0');
       }
       }
+    },
+    'form.openSipCall'(newVal) {
+      if (newVal !== '1') {
+        this.$set(this.form, 'aiCustomerRoleId', null);
+      } else {
+        this.loadSipTemplates();
+      }
     }
     }
   },
   },
   methods: {
   methods: {
+    onSipSelectVisible(visible) {
+      if (visible) {
+        this.loadSipTemplates();
+      }
+    },
+    loadSipTemplates() {
+      if (this.aiSipCallTaskList.length > 0) {
+        return;
+      }
+      listAiSipCallTask({ taskType: 1, status: 0, pageNum: 1, pageSize: 100 }).then(response => {
+        this.aiSipCallTaskList = response.rows || response.data || [];
+      }).catch(() => {
+        this.$message.error('加载SIP外呼任务失败');
+      });
+    },
 
 
     handleCompanyQuery() {
     handleCompanyQuery() {
       this.queryCompanyParams.pageNum = 1;
       this.queryCompanyParams.pageNum = 1;
@@ -627,6 +749,10 @@ export default {
         sort: 0,
         sort: 0,
         openOfficial: "1",
         openOfficial: "1",
         openAppCourse: "0", //默认 app 看课为关
         openAppCourse: "0", //默认 app 看课为关
+        openSipCall: "0",
+        openFeishu: "0",
+        aiCustomerRoleId: null,
+        openIsAtAll: "0",
         time: "",
         time: "",
         num: 1,
         num: 1,
         timeList: [{value: "",desc:""}],
         timeList: [{value: "",desc:""}],
@@ -792,6 +918,16 @@ export default {
               return;
               return;
             }
             }
           }
           }
+          if (this.form.sendType === 11 && !this.form.id) {
+            if (this.projectFrom === 'sxjz' && this.form.openSipCall === '1' && !this.form.aiCustomerRoleId) {
+              this.$message.error("请选择SIP外呼任务!");
+              return;
+            }
+            if (this.projectFrom !== 'sxjz') {
+              this.form.openSipCall = '0';
+              this.form.aiCustomerRoleId = null;
+            }
+          }
 
 
           if (this.command != 2 && this.form.id == null && this.form.sendType == 11){
           if (this.command != 2 && this.form.id == null && this.form.sendType == 11){
 
 
@@ -872,7 +1008,7 @@ export default {
       }).then(function () {
       }).then(function () {
         return delSopTemp(ids);
         return delSopTemp(ids);
       }).then(() => {
       }).then(() => {
-        this.getMyListgetMyList();
+        this.getMyList();
         this.msgSuccess("删除成功");
         this.msgSuccess("删除成功");
       }).catch(() => {
       }).catch(() => {
       });
       });
@@ -924,3 +1060,193 @@ export default {
   }
   }
 };
 };
 </script>
 </script>
+<style scoped>
+.sop-temp-form {
+  max-height: 72vh;
+  overflow-y: auto;
+  padding-right: 6px;
+}
+
+.form-section {
+  margin-bottom: 24px;
+  padding: 28px 32px 16px;
+  background: #fafbfc;
+  border: 1px solid #ebeef5;
+  border-radius: 12px;
+}
+
+.form-section:last-child {
+  margin-bottom: 4px;
+}
+
+.form-section__title {
+  display: flex;
+  align-items: baseline;
+  gap: 14px;
+  margin: 0 0 24px;
+  padding-left: 12px;
+  border-left: 4px solid #409eff;
+  font-size: 16px;
+  font-weight: 600;
+  color: #1f2d3d;
+  line-height: 1.3;
+  letter-spacing: 0.02em;
+}
+
+.form-section__hint {
+  font-size: 13px;
+  font-weight: 400;
+  color: #909399;
+}
+
+.w-full {
+  width: 100%;
+}
+
+.num-input {
+  width: 100%;
+}
+
+.num-input >>> .el-input__inner {
+  text-align: left;
+}
+
+.dept-treeselect {
+  width: 100%;
+  line-height: 40px;
+}
+
+.capability-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 16px;
+  margin-bottom: 4px;
+}
+
+.capability-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  min-height: 88px;
+  padding: 20px 18px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+}
+
+.capability-item__meta {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+  min-width: 0;
+}
+
+.capability-item__name {
+  font-size: 15px;
+  font-weight: 600;
+  color: #303133;
+}
+
+.capability-item__desc {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.45;
+}
+
+.sip-task-row {
+  margin-top: 18px;
+}
+
+.remind-list {
+  margin: 4px 0 8px 100px;
+}
+
+.remind-list__label {
+  margin-bottom: 12px;
+  font-size: 14px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.remind-grid {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 14px;
+}
+
+.remind-row {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 14px 16px;
+  background: #fff;
+  border: 1px solid #e4e7ed;
+  border-radius: 10px;
+}
+
+.remind-row__index {
+  flex: 0 0 58px;
+  margin-top: 8px;
+  font-size: 13px;
+  font-weight: 500;
+  color: #606266;
+}
+
+.remind-row__time {
+  flex: 0 0 130px;
+}
+
+.remind-row__desc {
+  flex: 1;
+  min-width: 0;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+}
+</style>
+
+<style>
+.sop-temp-dialog {
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.sop-temp-dialog .el-dialog__header {
+  padding: 24px 32px 18px;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.sop-temp-dialog .el-dialog__title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1f2d3d;
+  letter-spacing: 0.02em;
+}
+
+.sop-temp-dialog .el-dialog__body {
+  padding: 28px 32px 18px;
+}
+
+.sop-temp-dialog .el-dialog__footer {
+  padding: 18px 32px 24px;
+  border-top: 1px solid #ebeef5;
+}
+
+.sop-temp-dialog .el-form-item {
+  margin-bottom: 22px;
+}
+
+.sop-temp-dialog .el-input__inner,
+.sop-temp-dialog .el-textarea__inner {
+  border-radius: 6px;
+}
+
+.sop-temp-dialog .el-button--medium {
+  min-width: 104px;
+  padding: 11px 24px;
+}
+</style>

+ 241 - 0
src/views/store/components/customerVisitSection.vue

@@ -0,0 +1,241 @@
+<template>
+  <div class="contentx">
+    <div class="desct" style="display: flex; justify-content: space-between; align-items: center;">
+      <span>回访记录</span>
+      <el-button type="primary" size="small" @click="handleAdd">新增回访记录</el-button>
+    </div>
+
+    <el-table v-loading="loading" :data="visitList" border>
+      <el-table-column label="回访时间" align="center" prop="visitTime" width="160">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.visitTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="回访方式" align="center" prop="visitType" width="100" />
+      <el-table-column label="回访结果" align="center" prop="visitResult" width="100" />
+      <el-table-column label="回访内容" align="center" prop="visitContent" min-width="200" show-overflow-tooltip />
+      <el-table-column label="下次回访时间" align="center" prop="nextVisitTime" width="160">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.nextVisitTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="提醒状态" align="center" width="90">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.reminded === 1" type="info" size="small">已提醒</el-tag>
+          <el-tag v-else type="warning" size="small">待提醒</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="120">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            :disabled="scope.row.reminded === 1"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            style="color: #F56C6C"
+            @click="handleDelete(scope.row)"
+            :disabled="scope.row.reminded === 1"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 新增/修改弹窗 -->
+    <el-dialog
+      :title="dialogTitle"
+      :visible.sync="dialogVisible"
+      width="550px"
+      append-to-body
+      @close="resetForm"
+    >
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px">
+        <el-form-item label="回访时间" prop="visitTime">
+          <el-date-picker
+            v-model="form.visitTime"
+            type="datetime"
+            placeholder="选择回访时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="回访方式" prop="visitType">
+          <el-select v-model="form.visitType" placeholder="请选择回访方式" style="width: 100%">
+            <el-option label="电话" value="电话" />
+            <el-option label="微信" value="微信" />
+            <el-option label="面访" value="面访" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="回访结果" prop="visitResult">
+          <el-select v-model="form.visitResult" placeholder="请选择回访结果" style="width: 100%">
+            <el-option label="已联系" value="已联系" />
+            <el-option label="未接通" value="未接通" />
+            <el-option label="待跟进" value="待跟进" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="回访内容" prop="visitContent">
+          <el-input
+            v-model="form.visitContent"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入回访内容/备注"
+          />
+        </el-form-item>
+        <el-form-item label="下次回访时间" prop="nextVisitTime">
+          <el-date-picker
+            v-model="form.nextVisitTime"
+            type="datetime"
+            placeholder="选择下次回访时间"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            style="width: 100%"
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listCustomerVisit, getCustomerVisit, addCustomerVisit, updateCustomerVisit, delCustomerVisit } from '@/api/store/customerVisit'
+
+export default {
+  name: 'CustomerVisitSection',
+  data() {
+    return {
+      loading: false,
+      submitLoading: false,
+      visitList: [],
+      total: 0,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null
+      },
+      dialogVisible: false,
+      dialogTitle: '',
+      form: {
+        id: null,
+        userId: null,
+        visitTime: null,
+        visitType: '',
+        visitResult: '',
+        visitContent: '',
+        nextVisitTime: null
+      },
+      rules: {
+        visitTime: [{ required: true, message: '请选择回访时间', trigger: 'change' }],
+        visitType: [{ required: true, message: '请选择回访方式', trigger: 'change' }],
+        visitResult: [{ required: true, message: '请选择回访结果', trigger: 'change' }],
+        nextVisitTime: [{ required: true, message: '请选择下次回访时间', trigger: 'change' }]
+      },
+      isEdit: false
+    }
+  },
+  methods: {
+    init(userId) {
+      this.queryParams.userId = userId
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    getList() {
+      this.loading = true
+      listCustomerVisit(this.queryParams).then(response => {
+        this.visitList = response.rows
+        this.total = response.total
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleAdd() {
+      this.isEdit = false
+      this.dialogTitle = '新增回访记录'
+      this.resetForm()
+      this.form.userId = this.queryParams.userId
+      this.dialogVisible = true
+    },
+    handleUpdate(row) {
+      this.isEdit = true
+      this.dialogTitle = '修改回访记录'
+      this.resetForm()
+      this.form = { ...row }
+      this.dialogVisible = true
+    },
+    handleDelete(row) {
+      this.$confirm('确认删除该回访记录?', '警告', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        delCustomerVisit(row.id).then(res => {
+          this.msgSuccess('删除成功')
+          this.getList()
+        })
+      })
+    },
+    submitForm() {
+      this.$refs.form.validate(valid => {
+        if (!valid) return
+        this.submitLoading = true
+        const api = this.isEdit ? updateCustomerVisit : addCustomerVisit
+        api(this.form).then(res => {
+          if (res.code === 200) {
+            this.msgSuccess(this.isEdit ? '修改成功' : '新增成功')
+            this.dialogVisible = false
+            this.getList()
+          } else {
+            this.msgError(res.msg || '操作失败')
+          }
+        }).finally(() => {
+          this.submitLoading = false
+        })
+      })
+    },
+    resetForm() {
+      this.form = {
+        id: null,
+        userId: this.queryParams.userId,
+        visitTime: null,
+        visitType: '',
+        visitResult: '',
+        visitContent: '',
+        nextVisitTime: null
+      }
+      if (this.$refs.form) {
+        this.$refs.form.clearValidate()
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.contentx {
+  background-color: #fff;
+  padding: 0px 20px 20px;
+  margin: 20px;
+}
+.desct {
+  padding-top: 20px;
+  padding-bottom: 20px;
+  color: #524b4a;
+  font-weight: bold;
+}
+</style>

+ 12 - 2
src/views/store/components/userDetailsTemp.vue

@@ -141,13 +141,16 @@
       <userInquiryOrderDetails  ref="InquiryDetails" />
       <userInquiryOrderDetails  ref="InquiryDetails" />
     </div>
     </div>
 
 
-    <div class="contentx" v-if="item != null && fsUserInfo === 'gzzdy'" >
+    <div class="contentx" v-if="item != null && fsUserInfo === 'gzzdy' || fsUserInfo === 'hcl'" >
       <div class="desct">
       <div class="desct">
         商城订单
         商城订单
       </div>
       </div>
       <userStoreOrderList  ref="userStoreOrderList" />
       <userStoreOrderList  ref="userStoreOrderList" />
     </div>
     </div>
 
 
+    <!-- 回访记录 -->
+    <customerVisitSection ref="customerVisitSection" />
+
     <!-- 积分购弹窗 -->
     <!-- 积分购弹窗 -->
     <el-dialog
     <el-dialog
       title="积分购"
       title="积分购"
@@ -197,6 +200,7 @@ import userAddDetails from "../components/userAddDetails.vue";
 import userIntegralDetails from "../components/userIntegralDetails.vue";
 import userIntegralDetails from "../components/userIntegralDetails.vue";
 import AddOrderDialog from "../../hisStore/integralOrder/addOrder.vue"; // 导入积分购组件
 import AddOrderDialog from "../../hisStore/integralOrder/addOrder.vue"; // 导入积分购组件
 import userStoreOrderList from "../components/userStoreOrderList.vue"; //商城订单
 import userStoreOrderList from "../components/userStoreOrderList.vue"; //商城订单
+import customerVisitSection from "../components/customerVisitSection.vue"; //回访记录
 
 
 export default {
 export default {
   name: "storedet",
   name: "storedet",
@@ -208,7 +212,8 @@ export default {
     userAddDetails,
     userAddDetails,
     userIntegralDetails,
     userIntegralDetails,
     AddOrderDialog, // 注册积分购组件
     AddOrderDialog, // 注册积分购组件
-    userStoreOrderList
+    userStoreOrderList,
+    customerVisitSection
   },
   },
   data() {
   data() {
     return {
     return {
@@ -378,6 +383,11 @@ export default {
             this.$refs.userStoreOrderList.getUserOrderDetails(orderId);
             this.$refs.userStoreOrderList.getUserOrderDetails(orderId);
           }
           }
         }, 1);
         }, 1);
+        setTimeout(() => {
+          if (this.$refs.customerVisitSection) {
+            this.$refs.customerVisitSection.init(orderId);
+          }
+        }, 1);
       });
       });
 
 
       this.patient=null;
       this.patient=null;

+ 107 - 0
src/views/store/components/visitReminderPopup.vue

@@ -0,0 +1,107 @@
+<template>
+  <el-dialog
+    title="回访提醒"
+    :visible.sync="visible"
+    width="850px"
+    append-to-body
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    @close="handleClose"
+  >
+    <div v-if="reminderList.length === 0" style="text-align: center; padding: 40px; color: #999;">
+      暂无待回访提醒
+    </div>
+    <el-table v-else :data="reminderList" border max-height="400" style="width: 100%">
+      <el-table-column label="客户昵称" align="center" prop="nickName" min-width="100" />
+      <el-table-column label="外部联系人ID" align="center" prop="customerId" width="130" />
+      <el-table-column label="上次回访" align="center" width="145">
+        <template slot-scope="scope">
+          <span>{{ parseTime(scope.row.visitTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="方式" align="center" prop="visitType" width="70" />
+      <el-table-column label="结果" align="center" prop="visitResult" width="80" />
+      <el-table-column label="回访内容" align="center" prop="visitContent" show-overflow-tooltip min-width="120" />
+      <el-table-column label="应回访时间" align="center" width="145">
+        <template slot-scope="scope">
+          <el-tag v-if="isOverdue(scope.row.nextVisitTime)" type="danger" size="small">超期</el-tag>
+          <span>{{ parseTime(scope.row.nextVisitTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="80">
+        <template slot-scope="scope">
+          <el-button type="primary" size="mini" @click="handleGo(scope.row)">去处理</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-dialog>
+</template>
+
+<script>
+import { getMsg } from '@/api/crm/msg'
+import { closeRemind } from '@/api/store/customerVisit'
+import { getToken } from '@/utils/auth'
+
+export default {
+  name: 'VisitReminderPopup',
+  data() {
+    return {
+      visible: false,
+      reminderList: []
+    }
+  },
+  created() {
+    this.checkFromGetMsg()
+    this.$root.$on('visit-reminders-update', this.onRemindersUpdate)
+  },
+  beforeDestroy() {
+    this.$root.$off('visit-reminders-update', this.onRemindersUpdate)
+  },
+  methods: {
+    checkFromGetMsg() {
+      if (this.visible || !getToken()) return
+      getMsg().then(res => {
+        if (res.code === 200 && res.visitReminders && res.visitReminders.length > 0) {
+          this.showReminders(res.visitReminders)
+        }
+      }).catch(() => { })
+    },
+    onRemindersUpdate(visitReminders) {
+      if (this.visible) return
+      if (visitReminders && visitReminders.length > 0) {
+        this.showReminders(visitReminders)
+      }
+    },
+    showReminders(list) {
+      this.reminderList = list
+      if (!this.visible) {
+        this.visible = true
+      }
+    },
+    handleGo(row) {
+      const resolved = this.$router.resolve({
+        path: '/qw/qwExternal/myExternalContact',
+        query: { userId: row.userId, customerId: row.customerId }
+      })
+      window.open(resolved.href, '_blank')
+      // 标记该条为已提醒并关闭弹窗
+      closeRemind([row.id]).then(() => { }).catch(() => { })
+      this.reminderList = this.reminderList.filter(item => item.id !== row.id)
+      if (this.reminderList.length === 0) {
+        this.visible = false
+      }
+    },
+    handleClose() {
+      const ids = this.reminderList.map(item => item.id)
+      if (ids.length > 0) {
+        closeRemind(ids).then(() => { }).catch(() => { })
+      }
+      this.reminderList = []
+    },
+    isOverdue(nextVisitTime) {
+      if (!nextVisitTime) return false
+      return new Date(nextVisitTime) < new Date()
+    }
+  }
+}
+</script>

+ 12 - 0
src/views/store/user/list.vue

@@ -255,6 +255,18 @@ export default {
     this.getDicts("sys_company_or").then(response => {
     this.getDicts("sys_company_or").then(response => {
           this.orOptions = response.data;
           this.orOptions = response.data;
     });
     });
+    // 支持从回访提醒跳转,自动打开客户详情抽屉
+    const queryUserId = this.$route.query.userId;
+    if (queryUserId) {
+      this.$nextTick(() => {
+        this.show.open = true;
+        setTimeout(() => {
+          if (this.$refs.userDetails) {
+            this.$refs.userDetails.getDetails(queryUserId);
+          }
+        }, 500);
+      });
+    }
   },
   },
   methods: {
   methods: {
     change(){
     change(){

+ 12 - 0
src/views/store/user/myList.vue

@@ -259,6 +259,18 @@ export default {
     this.getDicts("sys_company_or").then(response => {
     this.getDicts("sys_company_or").then(response => {
           this.orOptions = response.data;
           this.orOptions = response.data;
     });
     });
+    // 支持从回访提醒跳转,自动打开客户详情抽屉
+    const queryUserId = this.$route.query.userId;
+    if (queryUserId) {
+      this.$nextTick(() => {
+        this.show.open = true;
+        setTimeout(() => {
+          if (this.$refs.userDetails) {
+            this.$refs.userDetails.getDetails(queryUserId);
+          }
+        }, 500);
+      });
+    }
   },
   },
   methods: {
   methods: {
     change(){
     change(){