xw 2 недель назад
Родитель
Сommit
82ae143f2e

+ 22 - 1
src/api/qw/externalContact.js

@@ -1,6 +1,13 @@
 import request from '@/utils/request'
 
-// 查询企业微信客户列表
+/**
+ * 列表行中与后端 QwExternalContactVO 对齐的扩展字段(详见 `src/types/qwExternalContact.ts`)。
+ * @typedef {Object} QwExternalContactListRowExtras
+ * @property {string|null} [firstLoginAppTime] 首次登录 App 时间
+ * @property {string|null} [firstLoginRewardAddress] 首次登录奖励收货/联系地址
+ */
+
+// 查询企业微信客户列表 — GET /qw/externalContact/list
 export function listExternalContact(query) {
   return request({
     url: '/qw/externalContact/list',
@@ -43,6 +50,7 @@ export function companyExtList(query) {
   })
 }
 
+// 查询我的企微客户列表 — GET /qw/externalContact/myList
 export function myList(query) {
   return request({
     url: '/qw/externalContact/myList',
@@ -159,6 +167,19 @@ export function updateExternalContact(data) {
   })
 }
 
+/**
+ * 修改首次登录奖励地址 — PUT /qw/externalContact/firstLoginRewardAddress
+ * 请求体类型见 `src/types/qwExternalContact.ts` 中 UpdateFirstLoginRewardAddressBody
+ * @param {{ id: number, firstLoginRewardAddress: string }} data
+ */
+export function updateFirstLoginRewardAddress(data) {
+  return request({
+    url: '/qw/externalContact/firstLoginRewardAddress',
+    method: 'put',
+    data
+  })
+}
+
 // 修改企业微信客户称呼
 export function updateExternalContactCall(data) {
   return request({

+ 21 - 0
src/types/qwExternalContact.ts

@@ -0,0 +1,21 @@
+/**
+ * 企微外部联系人列表行数据中与后端 QwExternalContactVO 对齐的字段。
+ *
+ * 列表接口:
+ * - GET /qw/externalContact/list
+ * - GET /qw/externalContact/myList
+ */
+export interface QwExternalContactVO {
+  /** 关联小程序用户「首次登录 App」时间,未绑定或未登录时为 null */
+  firstLoginAppTime?: string | null
+  /** 首次登录可领取奖励的收货/联系地址,无则为 null */
+  firstLoginRewardAddress?: string | null
+}
+
+/** PUT /qw/externalContact/firstLoginRewardAddress 请求体 */
+export interface UpdateFirstLoginRewardAddressBody {
+  /** 企微外部联系人主键 id */
+  id: number
+  /** 奖励地址;传空字符串表示清空 */
+  firstLoginRewardAddress: string
+}

+ 138 - 2
src/views/qw/externalContact/index.vue

@@ -464,6 +464,17 @@
       <el-table-column label="流失时间" align="center" prop="lossTime" width="100px" />
       <el-table-column label="删除时间" align="center" prop="delTime" width="100px" />
       <el-table-column label="注册时间" align="center" prop="registerTime" width="100px" />
+      <el-table-column label="首次登录App时间" align="center" prop="firstLoginAppTime" width="165px">
+        <template slot-scope="scope">
+          <span v-if="scope.row.firstLoginAppTime">{{ parseTime(scope.row.firstLoginAppTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          <span v-else>未绑定</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="首次登录奖励地址" align="center" prop="firstLoginRewardAddress" width="180px" show-overflow-tooltip>
+        <template slot-scope="scope">
+          <span>{{ scope.row.firstLoginRewardAddress || '-' }}</span>
+        </template>
+      </el-table-column>
       <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>
@@ -503,7 +514,7 @@
           <el-tag v-else type="info"> 未绑定</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+      <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="200px" fixed="right">
         <template slot-scope="scope">
           <el-button
             v-if="scope.row.status==0||scope.row.status==2"
@@ -519,6 +530,18 @@
             icon="el-icon-user-solid"
             @click="handleAppellation(scope.row)"
           >修改客户称呼</el-button>
+          <el-tooltip :disabled="!!scope.row.fsUserId" content="未绑定小程序会员,无法修改奖励地址" placement="top">
+            <span class="table-action-btn-wrap">
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-location-outline"
+                :disabled="!scope.row.fsUserId"
+                @click="handleFirstLoginRewardAddress(scope.row)"
+                v-hasPermi="['qw:externalContact:edit']"
+              >修改奖励地址</el-button>
+            </span>
+          </el-tooltip>
         </template>
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
@@ -948,6 +971,31 @@
       <el-button type="primary" @click="submitStatusForm">提 交</el-button>
     </div>
   </el-dialog>
+
+  <el-dialog
+    title="修改首次登录奖励地址"
+    :visible.sync="firstLoginRewardDialog.open"
+    width="600px"
+    append-to-body
+    @closed="resetFirstLoginRewardDialog"
+  >
+    <el-form ref="firstLoginRewardFormRef" :model="firstLoginRewardForm" :rules="firstLoginRewardRules" label-width="130px">
+      <el-form-item label="奖励地址" prop="firstLoginRewardAddress">
+        <el-input
+          v-model="firstLoginRewardForm.firstLoginRewardAddress"
+          type="textarea"
+          :rows="6"
+          placeholder="请输入收货/联系地址;留空并确定可清空。trim 后不超过 2000 字。"
+          maxlength="2000"
+          show-word-limit
+        />
+      </el-form-item>
+    </el-form>
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="firstLoginRewardDialog.open = false">取 消</el-button>
+      <el-button type="primary" :loading="firstLoginRewardDialog.loading" @click="submitFirstLoginRewardAddress">确 定</el-button>
+    </div>
+  </el-dialog>
   </div>
 </template>
 
@@ -967,7 +1015,8 @@ import {
   setCustomerCourseSop,
   getCustomerCourseSop,
   setCustomerCourseSopList,
-  unBindUserId, updateExternalContactCall,updateExternalContactStatus,getWatchLogList
+  unBindUserId, updateExternalContactCall,updateExternalContactStatus,getWatchLogList,
+  updateFirstLoginRewardAddress
 } from '@/api/qw/externalContact'
 import {getMyQwUserList, getMyQwCompanyList, updateUser,getQwUserListLikeName} from "@/api/qw/user";
 import {listTag, getTag, searchTags} from "@/api/qw/tag";
@@ -1020,6 +1069,30 @@ export default {
           {required: true, message: '状态不能为空', trigger: 'change'}
         ]
       },
+      firstLoginRewardDialog: {
+        open: false,
+        loading: false,
+        original: ''
+      },
+      firstLoginRewardForm: {
+        id: null,
+        firstLoginRewardAddress: ''
+      },
+      firstLoginRewardRules: {
+        firstLoginRewardAddress: [
+          {
+            validator(rule, value, callback) {
+              const t = (value || '').trim()
+              if (t.length > 2000) {
+                callback(new Error('地址去掉首尾空格后长度不能超过 2000 个字符'))
+              } else {
+                callback()
+              }
+            },
+            trigger: 'blur'
+          }
+        ]
+      },
       notesOpen: {
         type: 1,
         nameType: 3,
@@ -2055,6 +2128,65 @@ export default {
       });
     },
 
+    handleFirstLoginRewardAddress(row) {
+      if (!row.fsUserId) {
+        this.msgWarning('未绑定小程序会员')
+        return
+      }
+      this.firstLoginRewardForm.id = row.id
+      this.firstLoginRewardForm.firstLoginRewardAddress =
+        row.firstLoginRewardAddress == null ? '' : String(row.firstLoginRewardAddress)
+      this.firstLoginRewardDialog.original = this.firstLoginRewardForm.firstLoginRewardAddress
+      this.firstLoginRewardDialog.open = true
+      this.$nextTick(() => {
+        if (this.$refs.firstLoginRewardFormRef) {
+          this.$refs.firstLoginRewardFormRef.clearValidate()
+        }
+      })
+    },
+    resetFirstLoginRewardDialog() {
+      this.firstLoginRewardForm.id = null
+      this.firstLoginRewardForm.firstLoginRewardAddress = ''
+      this.firstLoginRewardDialog.original = ''
+      this.$nextTick(() => {
+        if (this.$refs.firstLoginRewardFormRef) {
+          this.$refs.firstLoginRewardFormRef.clearValidate()
+        }
+      })
+    },
+    submitFirstLoginRewardAddress() {
+      this.$refs.firstLoginRewardFormRef.validate(valid => {
+        if (!valid) return
+        const nextVal = (this.firstLoginRewardForm.firstLoginRewardAddress || '').trim()
+        const hadVal = (this.firstLoginRewardDialog.original || '').trim()
+        const doSubmit = () => {
+          this.firstLoginRewardDialog.loading = true
+          updateFirstLoginRewardAddress({
+            id: this.firstLoginRewardForm.id,
+            firstLoginRewardAddress: nextVal
+          }).then(() => {
+            this.msgSuccess('保存成功')
+            const row = this.externalContactList.find(r => r.id === this.firstLoginRewardForm.id)
+            if (row) {
+              row.firstLoginRewardAddress = nextVal || null
+            }
+            this.firstLoginRewardDialog.open = false
+          }).finally(() => {
+            this.firstLoginRewardDialog.loading = false
+          })
+        }
+        if (!nextVal && hadVal) {
+          this.$confirm('确定清空奖励地址?', '提示', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          }).then(() => doSubmit()).catch(() => {})
+        } else {
+          doSubmit()
+        }
+      })
+    },
+
     handleAppellation(val){
       this.callOpen.open=true;
       this.callOpenFrom.stageStatus=val.stageStatus;
@@ -2374,6 +2506,10 @@ export default {
 
 </script>
 <style scoped>
+/* 禁用按钮时 el-tooltip 需要包一层 */
+.table-action-btn-wrap {
+  display: inline-block;
+}
 /* CSS 样式 */
 .tag-container {
   display: flex;

+ 137 - 2
src/views/qw/externalContact/myExternalContact.vue

@@ -416,6 +416,17 @@
       <el-table-column label="流失时间" align="center" prop="lossTime" width="100px" />
       <el-table-column label="删除时间" align="center" prop="delTime" width="100px" />
       <el-table-column label="注册时间" align="center" prop="registerTime" width="100px" />
+      <el-table-column label="首次登录App时间" align="center" prop="firstLoginAppTime" width="165px">
+        <template slot-scope="scope">
+          <span v-if="scope.row.firstLoginAppTime">{{ parseTime(scope.row.firstLoginAppTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+          <span v-else>未绑定</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="首次登录奖励地址" align="center" prop="firstLoginRewardAddress" width="180px" show-overflow-tooltip>
+        <template slot-scope="scope">
+          <span>{{ scope.row.firstLoginRewardAddress || '-' }}</span>
+        </template>
+      </el-table-column>
       <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>
@@ -454,7 +465,7 @@
           <el-tag v-else type="info"> 未绑定</el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+      <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="200px" fixed="right">
         <template slot-scope="scope">
           <el-button
             v-show="scope.row.status==0||scope.row.status==2"
@@ -470,6 +481,18 @@
             icon="el-icon-user-solid"
             @click="handleAppellation(scope.row)"
           >修改客户称呼</el-button>
+          <el-tooltip :disabled="!!scope.row.fsUserId" content="未绑定小程序会员,无法修改奖励地址" placement="top">
+            <span class="table-action-btn-wrap">
+              <el-button
+                size="mini"
+                type="text"
+                icon="el-icon-location-outline"
+                :disabled="!scope.row.fsUserId"
+                @click="handleFirstLoginRewardAddress(scope.row)"
+                v-hasPermi="['qw:externalContact:edit', 'qw:externalContact:myEdit']"
+              >修改奖励地址</el-button>
+            </span>
+          </el-tooltip>
         </template>
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
@@ -997,6 +1020,31 @@
       <el-button type="primary" @click="submitStatusForm">提 交</el-button>
     </div>
   </el-dialog>
+
+  <el-dialog
+    title="修改首次登录奖励地址"
+    :visible.sync="firstLoginRewardDialog.open"
+    width="600px"
+    append-to-body
+    @closed="resetFirstLoginRewardDialog"
+  >
+    <el-form ref="firstLoginRewardFormRef" :model="firstLoginRewardForm" :rules="firstLoginRewardRules" label-width="130px">
+      <el-form-item label="奖励地址" prop="firstLoginRewardAddress">
+        <el-input
+          v-model="firstLoginRewardForm.firstLoginRewardAddress"
+          type="textarea"
+          :rows="6"
+          placeholder="请输入收货/联系地址;留空并确定可清空。trim 后不超过 2000 字。"
+          maxlength="2000"
+          show-word-limit
+        />
+      </el-form-item>
+    </el-form>
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="firstLoginRewardDialog.open = false">取 消</el-button>
+      <el-button type="primary" :loading="firstLoginRewardDialog.loading" @click="submitFirstLoginRewardAddress">确 定</el-button>
+    </div>
+  </el-dialog>
   </div>
 </template>
 
@@ -1018,7 +1066,8 @@ import {
   setCustomerCourseSop,
   getCustomerCourseSop,
   setCustomerCourseSopList,
-  syncMyExternalContact, unBindUserId, updateExternalContactCall,exportMyExternalContact,updateExternalContactStatus,getWatchLogList
+  syncMyExternalContact, unBindUserId, updateExternalContactCall,exportMyExternalContact,updateExternalContactStatus,getWatchLogList,
+  updateFirstLoginRewardAddress
 } from '@/api/qw/externalContact'
 import info from "@/views/qw/externalContact/info.vue";
 import {getMyQwUserList, getMyQwCompanyList, handleInputAuthAppKey, updateUser} from "@/api/qw/user";
@@ -1077,6 +1126,30 @@ export default {
           {required: true, message: '状态不能为空', trigger: 'change'}
         ]
       },
+      firstLoginRewardDialog: {
+        open: false,
+        loading: false,
+        original: ''
+      },
+      firstLoginRewardForm: {
+        id: null,
+        firstLoginRewardAddress: ''
+      },
+      firstLoginRewardRules: {
+        firstLoginRewardAddress: [
+          {
+            validator(rule, value, callback) {
+              const t = (value || '').trim()
+              if (t.length > 2000) {
+                callback(new Error('地址去掉首尾空格后长度不能超过 2000 个字符'))
+              } else {
+                callback()
+              }
+            },
+            trigger: 'blur'
+          }
+        ]
+      },
       collection:{
         titile:"信息采集",
         open:false,
@@ -2066,6 +2139,65 @@ export default {
       });
     },
 
+    handleFirstLoginRewardAddress(row) {
+      if (!row.fsUserId) {
+        this.msgWarning('未绑定小程序会员')
+        return
+      }
+      this.firstLoginRewardForm.id = row.id
+      this.firstLoginRewardForm.firstLoginRewardAddress =
+        row.firstLoginRewardAddress == null ? '' : String(row.firstLoginRewardAddress)
+      this.firstLoginRewardDialog.original = this.firstLoginRewardForm.firstLoginRewardAddress
+      this.firstLoginRewardDialog.open = true
+      this.$nextTick(() => {
+        if (this.$refs.firstLoginRewardFormRef) {
+          this.$refs.firstLoginRewardFormRef.clearValidate()
+        }
+      })
+    },
+    resetFirstLoginRewardDialog() {
+      this.firstLoginRewardForm.id = null
+      this.firstLoginRewardForm.firstLoginRewardAddress = ''
+      this.firstLoginRewardDialog.original = ''
+      this.$nextTick(() => {
+        if (this.$refs.firstLoginRewardFormRef) {
+          this.$refs.firstLoginRewardFormRef.clearValidate()
+        }
+      })
+    },
+    submitFirstLoginRewardAddress() {
+      this.$refs.firstLoginRewardFormRef.validate(valid => {
+        if (!valid) return
+        const nextVal = (this.firstLoginRewardForm.firstLoginRewardAddress || '').trim()
+        const hadVal = (this.firstLoginRewardDialog.original || '').trim()
+        const doSubmit = () => {
+          this.firstLoginRewardDialog.loading = true
+          updateFirstLoginRewardAddress({
+            id: this.firstLoginRewardForm.id,
+            firstLoginRewardAddress: nextVal
+          }).then(() => {
+            this.msgSuccess('保存成功')
+            const row = this.externalContactList.find(r => r.id === this.firstLoginRewardForm.id)
+            if (row) {
+              row.firstLoginRewardAddress = nextVal || null
+            }
+            this.firstLoginRewardDialog.open = false
+          }).finally(() => {
+            this.firstLoginRewardDialog.loading = false
+          })
+        }
+        if (!nextVal && hadVal) {
+          this.$confirm('确定清空奖励地址?', '提示', {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning'
+          }).then(() => doSubmit()).catch(() => {})
+        } else {
+          doSubmit()
+        }
+      })
+    },
+
     handleAppellation(val){
       this.callOpen.open=true;
       this.callOpenFrom.stageStatus=val.stageStatus;
@@ -2442,6 +2574,9 @@ export default {
 };
 </script>
 <style scoped>
+.table-action-btn-wrap {
+  display: inline-block;
+}
 /* CSS 样式 */
 .tag-container {
   display: flex;

+ 18 - 5
src/views/qw/sopTemp/updateSopTemp.vue

@@ -512,8 +512,9 @@
                                                 <i class="el-icon-plus avatar-uploader-icon"></i>
                                               </el-upload>
                                               <el-link v-if="setList.fileUrl" type="primary"
-                                                       :href="downloadUrl(setList.fileUrl)" download>
-                                                {{ setList.fileUrl }}
+                                                       :href="downloadUrl(setList.fileUrl)"
+                                                       :download="setList.fileName || undefined">
+                                                {{ sopFileDisplayName(setList) }}
                                               </el-link>
                                             </el-form-item>
 
@@ -2133,15 +2134,27 @@ export default {
       if (res.code === 200) {
         // 使用 $set 确保响应式更新
         this.$set(content, 'fileUrl', res.url);
-        // 保存文件名
-        if (res.fileName) {
-          this.$set(content, 'fileName', res.fileName);
+        const originName = (file && file.name) || (res && (res.originalFilename || res.originalFileName || res.fileName));
+        if (originName) {
+          this.$set(content, 'fileName', originName);
         }
       } else {
         this.msgError(res.msg);
       }
 
     },
+    /** 文件消息:链接文案优先展示用户上传时的源文件名 */
+    sopFileDisplayName(setList) {
+      if (!setList || !setList.fileUrl) {
+        return '';
+      }
+      if (setList.fileName) {
+        return setList.fileName;
+      }
+      const path = String(setList.fileUrl).split('?')[0];
+      const seg = path.split('/').filter(Boolean).pop();
+      return seg || setList.fileUrl;
+    },
     beforeAvatarUploadFile(file) {
       const isLt1M = file.size / 1024 / 1024 < 10;
       if (!isLt1M) {