Przeglądaj źródła

1.恒春来增加回访定时弹框
2.恒春来增加商城订单展示

jzp 1 dzień temu
rodzic
commit
6f1ab7f9a7

+ 6 - 0
package.json

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

+ 6 - 1
src/App.vue

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

+ 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 => {
         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 = {};

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

@@ -375,7 +375,7 @@
             <div class="tag-list">
               <el-tag
                 v-for="name in scope.row.tagIdsName"
-                :key="name"
+                :key="scope.row.id + '_' + name"
                 type="success"
                 size="small"
               >
@@ -475,7 +475,7 @@
       <el-table-column label="注册时间" align="center" prop="registerTime" width="100px" />
       <el-table-column label="备注电话号码" align="center" prop="remarkMobiles" width="150px">
         <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>
       </el-table-column>
       <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
@@ -1275,6 +1275,7 @@
             <div class="recording-header">
               <span class="recording-index">跟进记录 {{ rIndex + 1 }}</span>
               <el-button
+                v-if="rIndex >= 8"
                 size="mini"
                 type="danger"
                 icon="el-icon-delete"
@@ -3043,9 +3044,20 @@ export default {
         customerInfo: {},
         followContent: '',
         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.ensureMinVisitRecords(8);
       this.loadFollowUpData();
       // 加载SCRM客户详情
       if (row.fsUserId) {
@@ -3061,20 +3073,34 @@ export default {
     addVisitRecordSlot() {
       this.followUpDrawer.visitRecords.push({
         _key: 'v_' + Date.now() + '_' + Math.random(),
-        registerDesc: '',
+        callContent: '',
+        customerFeedback: '',
+        nextAction: '',
         visitStatus: '',
         visitTime: ''
       });
     },
-    /** 删除跟进记录槽位 */
+    /** 保证至少 min 条跟进记录 */
+    ensureMinVisitRecords(min) {
+      while (this.followUpDrawer.visitRecords.length < min) {
+        this.addVisitRecordSlot();
+      }
+    },
+    /** 删除跟进记录槽位(仅超过8条的才可删除) */
     deleteVisitRecord(index) {
+      if (index < 8) {
+        this.$message.warning('前8条跟进记录不可删除');
+        return;
+      }
       this.followUpDrawer.visitRecords.splice(index, 1);
     },
     /** 保存所有跟进记录(JSON方式) */
     saveVisitRecords() {
       this.followUpDrawer.visitSaving = true;
       const visitRecords = this.followUpDrawer.visitRecords.map(r => ({
-        registerDesc: r.registerDesc || '',
+        callContent: r.callContent || '',
+        customerFeedback: r.customerFeedback || '',
+        nextAction: r.nextAction || '',
         visitStatus: r.visitStatus || '',
         visitTime: r.visitTime || ''
       }));
@@ -3098,9 +3124,13 @@ export default {
       getMyCustomerList({ fsUserId: fsUserId, pageNum: 1, pageSize: 1 }).then(response => {
         if (response.rows && response.rows.length > 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 {
           this.followUpTags = [];
+          this.followUpDrawer.callStatus = '';
+          this.followUpDrawer.isDmCustomer = '';
+          this.followUpDrawer.customerLevel = '';
         }
       }).catch(() => {});
     },
@@ -3139,20 +3169,70 @@ export default {
     syncFollowUpTags() {
       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客户信息 */
     saveCustomerInfo() {
       const info = this.followUpDrawer.customerInfo;
       if (!info) return;
       this.followUpDrawer.saving = true;
+      const profile = this.followUpDrawer.profile || {};
+      // 合并3个可编辑字段到tags
+      const mergedTags = this.buildMergedTags();
       updateCustomer({
         externalContactId: this.followUpDrawer.externalContactId,
         source: info.source,
         visitStatus: info.visitStatus,
         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,
         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 => {
         if (res.code === 200) {
           this.$message.success('客户信息保存成功');
@@ -3187,7 +3267,9 @@ export default {
               const parsed = typeof info.visitRecords === 'string' ? JSON.parse(info.visitRecords) : info.visitRecords;
               this.followUpDrawer.visitRecords = (parsed || []).map((r, i) => ({
                 _key: 'v_' + Date.now() + '_' + i,
-                registerDesc: r.registerDesc || '',
+                callContent: r.callContent || '',
+                customerFeedback: r.customerFeedback || '',
+                nextAction: r.nextAction || '',
                 visitStatus: r.visitStatus || '',
                 visitTime: r.visitTime || ''
               }));
@@ -3195,9 +3277,11 @@ export default {
               this.followUpDrawer.visitRecords = [];
             }
           } else {
-            this.followUpDrawer.visitRecords = [];
-          }
-          // 解析录音记录
+          this.followUpDrawer.visitRecords = [];
+        }
+        // 保证至少8条
+        this.ensureMinVisitRecords(8);
+        // 解析录音记录
           if (info.recordings) {
             try {
               const parsed = typeof info.recordings === 'string' ? JSON.parse(info.recordings) : info.recordings;
@@ -3215,9 +3299,42 @@ export default {
           } else {
             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 {
           this.followUpDrawer.visitRecords = [];
           this.followUpDrawer.recordings = [];
+          this.followUpDrawer.profile = { gender: null, age: null, height: null, weight: null };
         }
       }).catch(() => {
         this.followUpDrawer.loading = false;

+ 21 - 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="remarkMobiles" width="150px">
         <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>
       </el-table-column>
       <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
@@ -1580,6 +1580,16 @@ export default {
     this.getDicts("sys_qw_external_contact_status").then(response => {
       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.transferStatusOptions = response.data;
     });
@@ -1589,6 +1599,16 @@ export default {
   },
   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(){
 
           if (this.ids == null || this.ids == "") {

+ 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" />
     </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>
       <userStoreOrderList  ref="userStoreOrderList" />
     </div>
 
+    <!-- 回访记录 -->
+    <customerVisitSection ref="customerVisitSection" />
+
     <!-- 积分购弹窗 -->
     <el-dialog
       title="积分购"
@@ -197,6 +200,7 @@ import userAddDetails from "../components/userAddDetails.vue";
 import userIntegralDetails from "../components/userIntegralDetails.vue";
 import AddOrderDialog from "../../hisStore/integralOrder/addOrder.vue"; // 导入积分购组件
 import userStoreOrderList from "../components/userStoreOrderList.vue"; //商城订单
+import customerVisitSection from "../components/customerVisitSection.vue"; //回访记录
 
 export default {
   name: "storedet",
@@ -208,7 +212,8 @@ export default {
     userAddDetails,
     userIntegralDetails,
     AddOrderDialog, // 注册积分购组件
-    userStoreOrderList
+    userStoreOrderList,
+    customerVisitSection
   },
   data() {
     return {
@@ -378,6 +383,11 @@ export default {
             this.$refs.userStoreOrderList.getUserOrderDetails(orderId);
           }
         }, 1);
+        setTimeout(() => {
+          if (this.$refs.customerVisitSection) {
+            this.$refs.customerVisitSection.init(orderId);
+          }
+        }, 1);
       });
 
       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.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: {
     change(){

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

@@ -259,6 +259,18 @@ export default {
     this.getDicts("sys_company_or").then(response => {
           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: {
     change(){