Преглед изворни кода

Merge remote-tracking branch 'origin/development' into uat

LuTong пре 1 месец
родитељ
комит
020a837899

+ 9 - 0
src/api/modules/contractManagement.ts

@@ -83,3 +83,12 @@ export const signContract = (storeId: string | number, contractId: string | numb
 export const getContractSignUrl = (data: any = {}) => {
   return contractAxios.post(`/api/store/esign/signurl`, data);
 };
+
+/**
+ * 获取合同详情(新接口)
+ * @param {string} id - 合同ID(sign_flow_id)
+ * @returns {Promise} 返回 { contract_url, sign_url, sign_flow_id, status }
+ */
+export const getContractDetailById = (id: string | number) => {
+  return contractAxios.get(`/api/store/contracts/detail/${id}`);
+};

+ 1 - 1
src/api/modules/dynamicManagement.ts

@@ -233,7 +233,7 @@ export const blockUser = (params: {
   blockerId: number | string; // 拉黑者ID
   blockedId: number | string; // 被拉黑者ID
 }) => {
-  return httpStore.post(PORT_NONE + `/life-blacklist/blackList`, params);
+  return httpApi.post(`alienStore/life-blacklist/blackList`, params, { loading: false });
 };
 
 // 根据手机号获取用户ID

+ 64 - 4
src/components/Upload/Imgs.vue

@@ -13,6 +13,7 @@
       :on-exceed="handleExceed"
       :on-success="showSuccessNotification ? uploadSuccess : undefined"
       :on-error="uploadError"
+      :on-change="handleFileListChange"
       :drag="drag"
       :accept="fileType.join(',')"
     >
@@ -55,7 +56,7 @@
 </template>
 
 <script setup lang="ts" name="UploadImgs">
-import { ref, computed, inject, watch } from "vue";
+import { ref, computed, inject, watch, nextTick } from "vue";
 import { Plus, Picture } from "@element-plus/icons-vue";
 import { uploadImg } from "@/api/modules/upload";
 import type { UploadProps, UploadFile, UploadUserFile, UploadRequestOptions } from "element-plus";
@@ -106,14 +107,38 @@ const self_disabled = computed(() => {
 
 const _fileList = ref<UploadUserFile[]>(props.fileList);
 
+// 标记是否正在从 props 同步,避免循环更新
+let isSyncingFromProps = false;
+
 // 监听 props.fileList 列表默认值改变
 watch(
   () => props.fileList,
   (n: UploadUserFile[]) => {
+    isSyncingFromProps = true;
     _fileList.value = n;
+    nextTick(() => {
+      isSyncingFromProps = false;
+    });
   }
 );
 
+// 监听 _fileList 的变化,自动同步到父组件(排除从 props 同步的情况)
+// 注意:on-change 事件已经处理了大部分情况,这个 watch 作为备用
+watch(
+  () => _fileList.value,
+  (newList, oldList) => {
+    if (!isSyncingFromProps && newList !== oldList) {
+      // 延迟执行,避免与 on-change 重复
+      nextTick(() => {
+        if (!isSyncingFromProps) {
+          emit("update:fileList", [...newList]);
+        }
+      });
+    }
+  },
+  { deep: true, immediate: false }
+);
+
 // 记录正在上传的文件数量(使用 Set 跟踪文件 UID,更可靠)
 const uploadingFiles = new Set<string | number>();
 // 标记是否已经显示过成功提示,防止重复提示
@@ -170,11 +195,34 @@ const handleHttpUpload = async (options: UploadRequestOptions) => {
   try {
     const api = props.api ?? uploadImg;
     const { data } = await api(formData);
-    // 调用成功回调(如果提供了的话)
-    if (props.onSuccess) {
-      props.onSuccess(data.fileUrl ? data.fileUrl : data[0]);
+    // 无论是否显示成功提示,都先把当前文件的 url 设为服务器地址,否则父组件校验会认为仍是 blob 而报「请上传」
+    const fileUrl =
+      typeof data === "string"
+        ? data
+        : Array.isArray(data) && data.length > 0
+          ? data[0]
+          : (data?.fileUrl ?? (data && (Object.values(data)[0] as string)) ?? "");
+    if (fileUrl) {
+      (options.file as UploadFile).url = fileUrl;
+      (options.file as UploadFile).status = "success";
     }
     options.onSuccess(data);
+    emit("update:fileList", _fileList.value);
+    if (props.onSuccess) {
+      try {
+        const result = props.onSuccess(data?.fileUrl ? data.fileUrl : (data?.[0] ?? fileUrl));
+        // 如果回调返回 Promise,等待它完成(但不影响上传成功状态)
+        if (result && typeof result.then === "function") {
+          result.catch((callbackError: any) => {
+            // 回调失败不影响上传成功状态,只记录错误
+            console.error("onSuccess callback error:", callbackError);
+          });
+        }
+      } catch (callbackError) {
+        // 回调失败不影响上传成功状态,只记录错误
+        console.error("onSuccess callback error:", callbackError);
+      }
+    }
   } catch (error) {
     // 上传失败,移除文件 UID
     uploadingFiles.delete(fileUid);
@@ -224,6 +272,18 @@ const uploadSuccess = (response: { fileUrl: string } | string | string[] | undef
 };
 
 /**
+ * @description 文件列表变化处理
+ * @param uploadFile 变化的文件
+ * @param uploadFiles 当前文件列表
+ */
+const handleFileListChange: UploadProps["onChange"] = (uploadFile, uploadFiles) => {
+  // 当文件列表变化时,同步到父组件
+  if (!isSyncingFromProps) {
+    emit("update:fileList", uploadFiles as UploadUserFile[]);
+  }
+};
+
+/**
  * @description 删除图片
  * @param file 删除的文件
  * */

+ 6 - 5
src/layouts/components/Header/components/NotificationDrawerContent.vue

@@ -82,14 +82,15 @@
                 :key="item.id + '_' + index"
                 class="message-card"
                 :class="{ unread: item.unread }"
+                @click.stop="handleAvatarClick(item)"
               >
-                <div class="message-avatar avatar-clickable" @click.stop="handleAvatarClick(item)">
+                <div class="message-avatar avatar-clickable">
                   <el-avatar :size="40" :src="item.userImage">
                     <el-icon><UserFilled /></el-icon>
                   </el-avatar>
                   <span v-if="item.unread" class="unread-dot message-unread-dot" />
                 </div>
-                <div class="message-body" @click="handleViewDetail(item)">
+                <div class="message-body">
                   <div class="message-row">
                     <span class="message-sender">{{ item.userName || item.title || "未知" }}</span>
                     <span class="message-date">{{ item.date }}</span>
@@ -686,8 +687,8 @@ async function handleViewDetail(item: NoticeItem) {
     }
   }
 
-  // 系统通知 - 意见反馈回复通知:跳转反馈详情页(与商家端 s-informList 一致)
-  if (activeCategory.value === "system" && item.feedbackId != null) {
+  // 意见反馈回复通知:跳转反馈详情页
+  if (item.title === "意见反馈回复通知" && item.feedbackId != null) {
     emit("close");
     router.push({ path: "/feedback/detail", query: { id: String(item.feedbackId) } });
     return;
@@ -806,7 +807,7 @@ watch(activeTab, val => {
   flex-shrink: 0;
   flex-direction: column;
   gap: 6px;
-  width: 160px;
+  width: 170px;
   padding: 12px 0;
   background: var(--el-fill-color-lighter);
   border-radius: 8px;

+ 25 - 23
src/views/contractManagement/detail.vue

@@ -11,9 +11,14 @@
       <div v-if="loading" class="loading-container">
         <el-loading :loading="loading" text="加载中..." />
       </div>
-      <div v-else-if="contractUrl" class="contract-content">
+      <!-- 已签署:显示合同PDF -->
+      <div v-else-if="isSigned && contractUrl" class="contract-content">
         <iframe :src="contractUrl" class="contract-iframe" frameborder="0" />
       </div>
+      <!-- 未签署:显示签署页面 -->
+      <div v-else-if="!isSigned && signUrl" class="contract-content">
+        <iframe :src="signUrl" class="contract-iframe" frameborder="0" />
+      </div>
       <div v-else class="error-container">
         <el-empty description="合同内容加载失败" />
       </div>
@@ -25,13 +30,15 @@
 import { ref, onMounted } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
-import { getContractSignUrl } from "@/api/modules/contractManagement";
+import { getContractDetailById } from "@/api/modules/contractManagement";
 
 const route = useRoute();
 const router = useRouter();
 
-// 合同URL
+// 合同PDF URL(用于查看合同)
 const contractUrl = ref<string>("");
+// 签署页面URL(用于签署合同)
+const signUrl = ref<string>("");
 // 加载状态
 const loading = ref<boolean>(true);
 // 是否已签署
@@ -41,33 +48,28 @@ const isSigned = ref<boolean>(false);
 const loadContractDetail = async () => {
   try {
     loading.value = true;
-    const signFlowId = route.query.signFlowId as string;
-    const contactPhone = route.query.contactPhone as string;
-    const signed = route.query.signed as string;
-
-    // 判断是否已签署
-    isSigned.value = signed === "true" || signed === "1";
+    // 支持多种参数格式:contractId、signFlowId、id
+    const contractId = (route.query.contractId || route.query.signFlowId || route.query.id) as string;
 
-    if (!signFlowId) {
+    if (!contractId) {
       ElMessage.error("缺少合同ID参数");
       return;
     }
 
-    // 调用获取合同签署链接的接口
-    const res: any = await getContractSignUrl({
-      sign_flow_id: signFlowId,
-      contact_phone: contactPhone || ""
-    });
+    // 调用新的合同详情接口
+    const res: any = await getContractDetailById(contractId);
+
+    if (res && res.status !== undefined) {
+      // status: 0-未签署,需要显示签署页面;其他状态表示已签署,显示合同PDF
+      const contractStatus = res.status || 0;
+      isSigned.value = contractStatus !== 0;
 
-    if (res) {
-      // 如果返回的是URL字符串,直接使用
-      if (typeof res.data === "string") {
-        contractUrl.value = res.data;
-      } else if (res.data.url) {
-        // 如果返回的是对象,取url字段
-        contractUrl.value = res.data.url;
+      if (isSigned.value) {
+        // 已签署:显示合同PDF
+        contractUrl.value = res.contract_url || "";
       } else {
-        ElMessage.error("合同链接格式错误");
+        // 未签署:显示签署页面
+        signUrl.value = res.sign_url || "";
       }
     } else {
       ElMessage.error(res?.msg || "获取合同详情失败");

+ 5 - 3
src/views/dynamicManagement/friendRelation.vue

@@ -278,11 +278,13 @@ const initParam = reactive({
   storeId: localGet("createdId") || ""
 });
 
-// 数据回调处理
+// 数据回调处理:兼容接口返回数组或 { records/list, total } 格式,确保有数据时分页显示正确总数
 const dataCallback = (data: any) => {
+  const list = Array.isArray(data) ? data : (data?.records ?? data?.list ?? []);
+  const total = typeof data?.total === "number" ? data.total : list.length;
   return {
-    list: data || [],
-    total: data?.total || 0
+    list,
+    total
   };
 };
 

+ 9 - 5
src/views/dynamicManagement/index.vue

@@ -162,7 +162,7 @@
                 {{ currentDetail.context }}
               </p>
               <span
-                v-if="currentDetail.context && currentDetail.context.length > 100"
+                v-if="currentDetail.context && currentDetail.context.length > 50"
                 class="expand-btn"
                 @click="toggleDescription"
               >
@@ -497,8 +497,8 @@
 </template>
 
 <script setup lang="ts" name="dynamicManagementIndex">
-import { ref, reactive, computed, onMounted, watch } from "vue";
-import { useRouter } from "vue-router";
+import { ref, reactive, computed, onMounted, onActivated, watch } from "vue";
+import { useRouter, useRoute } from "vue-router";
 import { ElMessage, ElMessageBox } from "element-plus";
 import {
   Picture,
@@ -1297,12 +1297,11 @@ const handleCloseShareDialog = () => {
 // 查看用户主页
 const handleViewUserProfile = () => {
   if (!currentDetail.value) return;
-
   // 跳转到他人动态主页,传递用户信息
   router.push({
     path: "/dynamicManagement/userDynamic",
     query: {
-      userId: currentDetail.value.storeUserId || currentDetail.value.userId || "",
+      userId: currentDetail.value.storeOrUserId || "",
       phoneId: currentDetail.value.phoneId || "",
       userName: currentDetail.value.userName || "",
       userAvatar: currentDetail.value.userAvatar || "",
@@ -1624,6 +1623,11 @@ const handleBlockUser = async (skipConfirm: boolean = false) => {
 onMounted(() => {
   loadDynamicList();
 });
+
+// 页面激活时刷新列表(从发布页面返回时触发)
+onActivated(() => {
+  loadDynamicList();
+});
 </script>
 
 <style scoped lang="scss">

+ 1 - 1
src/views/dynamicManagement/myDynamic.vue

@@ -227,7 +227,7 @@
                 {{ currentDetail.description }}
               </p>
               <span
-                v-if="currentDetail.description && currentDetail.description.length > 100"
+                v-if="currentDetail.description && currentDetail.description.length > 50"
                 class="expand-btn"
                 @click="toggleDescription"
               >

+ 51 - 6
src/views/dynamicManagement/publishDynamic.vue

@@ -14,7 +14,7 @@
             <el-upload
               v-model:file-list="fileList"
               list-type="picture-card"
-              :limit="20"
+              :limit="hasVideoInList() ? 1 : 20"
               :on-preview="handlePicturePreview"
               :on-remove="handleRemoveImage"
               :on-change="handleFileChange"
@@ -45,7 +45,7 @@
                 <el-icon :size="32" color="#999">
                   <Plus />
                 </el-icon>
-                <div class="upload-count">({{ fileList.length }}/20)</div>
+                <div class="upload-count">({{ fileList.length }}/{{ hasVideoInList() ? 1 : 20 }})</div>
               </div>
             </el-upload>
           </div>
@@ -278,6 +278,23 @@ const handleFileChange = (uploadFile: UploadFile, uploadFiles: UploadFile[]) =>
       return;
     }
 
+    // 权限:已上传视频则不能再传;已上传图片则只能继续传图片(检查时排除当前文件,用已存在的列表)
+    const otherFiles = fileList.value.filter(f => f.uid !== uploadFile.uid);
+    const alreadyHasVideo = otherFiles.some(f => isVideoFile(f));
+    const alreadyHasImage = otherFiles.some(f => !isVideoFile(f));
+    if (alreadyHasVideo) {
+      const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
+      if (index > -1) uploadFiles.splice(index, 1);
+      ElMessage.warning("已上传视频,只能上传一个视频,不可再上传");
+      return;
+    }
+    if (alreadyHasImage && isVideo) {
+      const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
+      if (index > -1) uploadFiles.splice(index, 1);
+      // ElMessage.warning("已上传图片,后续只能上传图片,不能上传视频");
+      return;
+    }
+
     // 根据文件类型设置不同的大小限制
     const maxSize = isVideo ? 100 : 20;
     const isLtMaxSize = uploadFile.raw.size / 1024 / 1024 < maxSize;
@@ -402,14 +419,22 @@ const beforeImageUpload = (file: File) => {
   const isVideo = file.type.startsWith("video/");
   const isValidType = isImage || isVideo;
 
-  // 图片和视频使用不同的大小限制
-  const maxSize = isVideo ? 100 : 20; // 视频100MB,图片20MB
-  const isLtMaxSize = file.size / 1024 / 1024 < maxSize;
-
   if (!isValidType) {
     ElMessage.error("只能上传图片或视频文件!");
     return false;
   }
+
+  // 权限:已上传视频则不能再传;已上传图片则只能继续传图片(排除当前文件,避免清除图片后选视频被误判)
+  const permission = checkUploadPermission(file, file);
+  if (!permission.allowed) {
+    ElMessage.warning(permission.message);
+    return false;
+  }
+
+  // 图片和视频使用不同的大小限制
+  const maxSize = isVideo ? 100 : 20; // 视频100MB,图片20MB
+  const isLtMaxSize = file.size / 1024 / 1024 < maxSize;
+
   if (!isLtMaxSize) {
     ElMessage.error(`${isVideo ? "视频" : "图片"}大小不能超过 ${maxSize}MB!`);
     return false;
@@ -429,6 +454,26 @@ const isVideoFile = (file: any) => {
   return fileName.toLowerCase().endsWith(".mp4") || file.raw?.type?.startsWith("video/") || false;
 };
 
+// 上传类型权限:第一个是图片则只能继续传图片;第一个是视频则只能传一个视频
+const hasVideoInList = () => fileList.value.some(f => isVideoFile(f));
+const hasImageInList = () => fileList.value.some(f => !isVideoFile(f));
+
+/** 校验上传权限,excludeRawFile 为当前正在校验的文件,校验时排除它(避免刚选中的文件已被加入列表导致误判) */
+const checkUploadPermission = (file: File, excludeRawFile?: File): { allowed: boolean; message?: string } => {
+  const isVideo = file.type.startsWith("video/");
+  const existingFiles = excludeRawFile ? fileList.value.filter(f => f.raw !== excludeRawFile) : fileList.value;
+  const existingHasVideo = existingFiles.some(f => isVideoFile(f));
+  const existingHasImage = existingFiles.some(f => !isVideoFile(f));
+
+  if (existingHasVideo) {
+    return { allowed: false, message: "已上传视频,只能上传一个视频,不可再上传" };
+  }
+  if (existingHasImage && isVideo) {
+    return { allowed: false, message: "已上传图片,后续只能上传图片,不能上传视频" };
+  }
+  return { allowed: true };
+};
+
 // 图片/视频预览
 const handlePicturePreview = (uploadFile: UploadUserFile) => {
   previewImageUrl.value = uploadFile.url!;

+ 1 - 1
src/views/dynamicManagement/reviewAppeal.vue

@@ -194,7 +194,7 @@
             type="textarea"
             :rows="6"
             placeholder="请输入回复内容"
-            maxlength="500"
+            maxlength="300"
             show-word-limit
           />
         </el-form-item>

+ 26 - 25
src/views/home/components/go-examine.vue

@@ -8,12 +8,12 @@
       <div class="expire">店铺到期时间:{{ expireDate }}</div>
     </div>
 
-    <div class="verify-row">
+    <!-- <div class="verify-row">
       <el-input outline="none" v-model="voucherCode" placeholder="请输入劵码" class="verify-input" maxlength="30" clearable />
       <el-button type="primary" class="verify-btn" @click="handleVerify"> 验 券 </el-button>
-    </div>
+    </div> -->
 
-    <div class="stats">
+    <!-- <div class="stats">
       <div class="wallet">
         <div class="stat-card">
           <div class="stat-title">店铺钱包(元)</div>
@@ -41,7 +41,7 @@
         </div>
         <el-image :src="homeIncome" class="walletImg" />
       </div>
-    </div>
+    </div> -->
   </div>
 </template>
 
@@ -72,32 +72,33 @@ const walletAmount = ref(0);
 const todayOrders = ref(0);
 const todayRevenue = ref(0);
 onMounted(() => {
+  storeName.value = localGet("storeName") || "店铺名称";
   getOrder();
   getInCome();
-  getMyMoney();
-  getStoreDetail();
+  // getMyMoney();
+  // getStoreDetail();
 });
 
-const getStoreDetail = async () => {
-  let param = {
-    id: userInfo.storeId
-  };
-  const res: any = await getDetail(param as any);
-  if (res.code == 200) {
-    console.log(1);
-    storeName.value = res.data.storeName;
-  }
-};
+// const getStoreDetail = async () => {
+//   let param = {
+//     id: userInfo.storeId
+//   };
+//   const res: any = await getDetail(param as any);
+//   if (res.code == 200) {
+//     console.log(res.data)
+//     storeName.value = res.data.storeName;
+//   }
+// };
 //可提现金额
-const getMyMoney = async () => {
-  let param = {
-    storeId: userInfo.storeId
-  };
-  const res: any = await getAccountBalance(param as any);
-  if (res.code == 200) {
-    walletAmount.value = res.data.cashOutMoney; //cashOutMoney  可提现金额减未审核通过的金额
-  }
-};
+// const getMyMoney = async () => {
+//   let param = {
+//     storeId: userInfo.storeId
+//   };
+//   const res: any = await getAccountBalance(param as any);
+//   if (res.code == 200) {
+//     walletAmount.value = res.data.cashOutMoney; //cashOutMoney  可提现金额减未审核通过的金额
+//   }
+// };
 // const getMyMoney = async () => {
 //   const res: any = await getMerchantByPhone({ phone: userInfo.phone });
 //   if (res.code == 200) {

+ 29 - 7
src/views/home/components/go-flow.vue

@@ -40,7 +40,8 @@
             >
               <template v-if="idCardFrontList.length === 0">
                 <div class="upload-placeholder">
-                  <el-image src="/src/assets/images/idCard1.png" />
+                  <!-- <el-image src="/src/assets/images/idCard1.png" /> -->
+                  <span class="placeholder-text">身份证正面示例-带有国徽一面</span>
                 </div>
               </template>
             </el-upload>
@@ -65,7 +66,7 @@
               <template v-if="idCardBackList.length === 0">
                 <div class="upload-placeholder">
                   <span class="placeholder-text">
-                    <el-image src="/src/assets/images/idCard2.png" />
+                    <span>身份证反面示例-带有人像一面</span>
                   </span>
                 </div>
               </template>
@@ -423,7 +424,25 @@ const step2Rules: FormRules = {
   storeBlurb: [{ required: true, message: "请输入门店简介", trigger: "change" }],
   storeDetailAddress: [{ required: true, message: "请输入详细地址", trigger: "blur" }],
   businessSection: [{ required: true, message: "请选择经营板块", trigger: "change" }],
-  storeTickets: [{ required: true, message: "请选择标签", trigger: "change" }],
+  storeTickets: [
+    {
+      required: true,
+      message: "请选择标签",
+      trigger: "change",
+      validator: (_rule: any, value: any, callback: (err?: Error) => void) => {
+        // 仅当经营板块为「生活服务」时校验标签必选
+        if (step2Form.businessSection != 3) {
+          callback();
+          return;
+        }
+        if (value !== "" && value !== undefined && value !== null) {
+          callback();
+        } else {
+          callback(new Error("请选择标签"));
+        }
+      }
+    }
+  ],
   businessTypeName: [{ required: true, message: "请输入经营种类", trigger: "change" }],
   businessCategoryName: [{ required: true, message: "请选择经营类目", trigger: "change" }],
   address: [{ required: true, message: "请输入经纬度", trigger: "blur" }],
@@ -440,8 +459,8 @@ const step2Rules: FormRules = {
         else callback(new Error("请完整填写三项店铺评价"));
       }
     }
-  ],
-  disportLicenceImgList: [{ required: true, message: "请上传其他资质证明", trigger: "change" }]
+  ]
+  // disportLicenceImgList: [{ required: true, message: "请上传其他资质证明", trigger: "change" }]
 };
 
 //地址集合
@@ -485,10 +504,13 @@ watch(
 
 const changeBusinessSection = () => {
   if (step2Form.businessSection == 3) {
+    // 生活服务:显示标签,默认选「其他类型」(dictId=0)
     showDisportLicence.value = true;
+    (step2Form as { storeTickets: string | number }).storeTickets = 0;
   } else {
+    // 非生活服务:隐藏标签,不选任何类型
     showDisportLicence.value = false;
-    step2Form.storeTickets = "0";
+    step2Form.storeTickets = "";
   }
 };
 // 隐藏财务管理菜单的函数
@@ -608,7 +630,7 @@ const step2Form = reactive({
   storeAddress: "",
   storeBlurb: "",
   businessSection: 1,
-  storeTickets: "0" as string,
+  storeTickets: "",
   businessSecondLevel: [] as string[],
   businessTypes: "" as string,
   businessTypesList: [] as string[],

+ 1 - 1
src/views/login/index.vue

@@ -564,7 +564,7 @@
           />
         </el-form-item>
       </el-form>
-      <!-- 用户协议 -->login
+      <!-- 用户协议 -->
       <div class="agreement-wrapper">
         <el-checkbox v-model="registerAgreed" />
         <span class="agreement-text">

+ 60 - 25
src/views/operationManagement/activityDetail.vue

@@ -37,7 +37,7 @@
           </div>
 
           <!-- 评论有礼相关字段 -->
-          <template v-if="activityModel.activityType == 2">
+          <template v-if="activityModel.activityType == 'COMMENT'">
             <!-- 用户可参与次数 -->
             <div class="detail-item">
               <div class="detail-label">用户可参与次数</div>
@@ -45,31 +45,34 @@
                 {{ activityModel.participationLimit || "--" }}
               </div>
             </div>
-            <!-- 活动规则 -->
+            <!-- 优惠券/代金券名称 -->
             <div class="detail-item">
-              <div class="detail-label">活动规则</div>
-              <div class="detail-value">
-                {{ getRuleDisplayText(activityModel.activityRule) || "--" }}
+              <div class="detail-label">
+                {{ activityModel.rewardType === "VOUCHER" ? "代金券" : "优惠券" }}
               </div>
-            </div>
-            <!-- 优惠券名称 -->
-            <div class="detail-item">
-              <div class="detail-label">优惠券</div>
               <div class="detail-value">
-                {{ activityModel.couponName || "--" }}
+                {{
+                  activityModel.rewardType === "VOUCHER" ? activityModel.voucherName || "--" : activityModel.couponName || "--"
+                }}
               </div>
             </div>
-            <!-- 优惠券发放数量 -->
+            <!-- 优惠券/代金券发放数量 -->
             <div class="detail-item">
-              <div class="detail-label">优惠券发放数量</div>
+              <div class="detail-label">
+                {{ (activityModel.rewardType === "VOUCHER" ? "代金券" : "优惠券") + "发放数量" }}
+              </div>
               <div class="detail-value">
-                {{ activityModel.couponQuantity || "--" }}
+                {{
+                  activityModel.rewardType === "VOUCHER"
+                    ? activityModel.voucherQuantity || "--"
+                    : activityModel.couponQuantity || "--"
+                }}
               </div>
             </div>
           </template>
 
           <!-- 营销活动相关字段 -->
-          <template v-if="activityModel.activityType == 1">
+          <template v-if="activityModel.activityType == 'MARKETING'">
             <!-- 报名时间 -->
             <div class="detail-item">
               <div class="detail-label">报名时间</div>
@@ -110,7 +113,7 @@
             </div>
           </div>
           <!-- 审核拒绝原因 -->
-          <div class="detail-item" v-if="activityModel.approvalComments">
+          <div class="detail-item" v-if="activityModel.auditStatus == 2">
             <div class="detail-label">审核拒绝原因</div>
             <div class="detail-value" style="word-break: break-word; white-space: pre-wrap">
               {{ activityModel.approvalComments }}
@@ -207,8 +210,8 @@ const id = ref<string>("");
 
 // ==================== 活动信息数据模型 ====================
 const activityModel = ref<any>({
-  // 活动类型:1-评论有礼,2-营销活动
-  activityType: 1,
+  // 活动类型:'COMMENT'-评论有礼,'MARKETING'-营销活动
+  activityType: "MARKETING",
   // 活动名称
   activityName: "",
   // 活动开始时间
@@ -217,14 +220,20 @@ const activityModel = ref<any>({
   endTime: "",
   // 用户可参与次数(评论有礼)
   participationLimit: 0,
-  // 活动规则(评论有礼)
-  activityRule: "",
+  // 券类型:'COUPON'-优惠券,'VOUCHER'-代金券(评论有礼)
+  rewardType: "COUPON",
   // 优惠券ID(评论有礼)
   couponId: "",
   // 优惠券名称(评论有礼)
   couponName: "",
   // 优惠券发放数量(评论有礼)
   couponQuantity: 0,
+  // 代金券ID(评论有礼)
+  voucherId: "",
+  // 代金券名称(评论有礼)
+  voucherName: "",
+  // 代金券发放数量(评论有礼)
+  voucherQuantity: 0,
   // 报名开始时间(营销活动)
   signupStartTime: "",
   // 报名结束时间(营销活动)
@@ -280,7 +289,25 @@ const loadDetailData = async () => {
     const res: any = await getActivityDetail({ id: id.value });
     if (res && res.code == 200) {
       // 合并主数据
-      activityModel.value = { ...activityModel.value, ...res.data };
+      const data = res.data || {};
+
+      // 兼容旧数据格式:如果后端返回的是 couponType(数字),转换为 rewardType(字符串)
+      let rewardType = "COUPON";
+      if (data.rewardType) {
+        rewardType = data.rewardType;
+      } else if (data.couponType !== undefined) {
+        rewardType = data.couponType === 4 ? "VOUCHER" : "COUPON";
+      }
+
+      // 兼容旧数据:如果后端返回的是数字 activityType,转换为新格式
+      if (data.activityType == 1 || data.activityType == "1") {
+        data.activityType = "MARKETING";
+      } else if (data.activityType == 2 || data.activityType == "2") {
+        data.activityType = "COMMENT";
+      }
+
+      // 合并数据,并根据 rewardType 设置对应字段
+      activityModel.value = { ...activityModel.value, ...data, rewardType };
     } else {
       ElMessage.error("加载详情数据失败");
     }
@@ -316,12 +343,20 @@ const formatDateTime = (dateTime: string) => {
 /**
  * 获取活动类型标签
  */
-const getActivityTypeLabel = (activityType: number) => {
-  const typeMap: Record<number, string> = {
-    1: "营销活动",
-    2: "评论有礼"
+const getActivityTypeLabel = (activityType: string | number) => {
+  // 兼容旧数据:如果传入的是数字,转换为字符串
+  let typeValue = activityType;
+  if (typeValue == 1 || typeValue == "1") {
+    typeValue = "MARKETING";
+  } else if (typeValue == 2 || typeValue == "2") {
+    typeValue = "COMMENT";
+  }
+
+  const typeMap: Record<string, string> = {
+    MARKETING: "营销活动",
+    COMMENT: "评论有礼"
   };
-  return typeMap[activityType] || "--";
+  return typeMap[typeValue] || "--";
 };
 
 /**

+ 12 - 5
src/views/operationManagement/activityList.vue

@@ -230,8 +230,8 @@ const statusEnum = [
 
 // 活动类型选项(用于搜索)
 const activityTypeEnum = [
-  { label: "评论有礼", value: 2 },
-  { label: "营销活动", value: 1 }
+  { label: "评论有礼", value: "COMMENT" },
+  { label: "营销活动", value: "MARKETING" }
 ];
 
 // 操作按钮权限配置(根据状态-操作映射表)
@@ -313,7 +313,14 @@ const columns = reactive<ColumnProps<any>[]>([
       order: 1
     },
     render: (scope: any) => {
-      return scope.row.activityType == 1 ? "营销活动" : "评论有礼";
+      // 兼容旧数据:如果后端返回的是数字,转换为字符串
+      const activityType = scope.row.activityType;
+      if (activityType == 1 || activityType == "1") {
+        return "营销活动";
+      } else if (activityType == 2 || activityType == "2") {
+        return "评论有礼";
+      }
+      return activityType == "MARKETING" ? "营销活动" : "评论有礼";
     }
   },
   {
@@ -391,10 +398,10 @@ const getTableList = async (params: any) => {
       pageSize: params.pageSize ? String(params.pageSize) : undefined,
       status: params.status !== undefined && params.status !== null && params.status !== "" ? Number(params.status) : undefined,
       storeId: params.storeId ? String(params.storeId) : undefined,
-      // 处理 activityType:如果存在且不为空,转换为 number 类型
+      // 处理 activityType:如果存在且不为空,保持字符串类型('MARKETING' 或 'COMMENT')
       activityType:
         params.activityType !== undefined && params.activityType !== null && params.activityType !== ""
-          ? Number(params.activityType)
+          ? String(params.activityType)
           : undefined
     };
     const result = await getActivityList(newParams);

+ 294 - 175
src/views/operationManagement/newActivity.vue

@@ -12,8 +12,8 @@
             <!-- 活动类型 -->
             <el-form-item label="活动类型" prop="activityType">
               <el-select v-model="activityModel.activityType" class="form-input" clearable placeholder="请选择">
-                <el-option label="评论有礼" :value="2" />
-                <el-option label="营销活动" :value="1" />
+                <el-option label="评论有礼" value="COMMENT" />
+                <el-option label="营销活动" value="MARKETING" />
               </el-select>
             </el-form-item>
 
@@ -38,40 +38,57 @@
             </el-form-item>
 
             <!-- 评论有礼相关字段 -->
-            <template v-if="activityModel.activityType === 2">
+            <template v-if="activityModel.activityType === 'COMMENT'">
               <!-- 用户可参与次数 -->
               <el-form-item label="用户可参与次数" prop="participationLimit">
                 <el-input v-model="activityModel.participationLimit" placeholder="请输入" maxlength="4" />
               </el-form-item>
 
-              <!-- 活动规则 -->
-              <el-form-item label="活动规则" prop="activityRule">
-                <el-cascader
-                  v-model="activityModel.activityRule"
-                  :options="ruleCascaderOptions"
-                  :props="cascaderProps"
+              <!-- 券类型 -->
+              <el-form-item label="券类型" prop="rewardType">
+                <el-radio-group v-model="activityModel.rewardType">
+                  <el-radio label="COUPON"> 优惠券 </el-radio>
+                  <el-radio label="VOUCHER"> 代金券 </el-radio>
+                </el-radio-group>
+              </el-form-item>
+
+              <!-- 优惠券/代金券 -->
+              <el-form-item
+                :label="activityModel.rewardType === 'VOUCHER' ? '代金券' : '优惠券'"
+                :prop="activityModel.rewardType === 'VOUCHER' ? 'voucherId' : 'couponId'"
+              >
+                <el-select
+                  v-if="activityModel.rewardType === 'VOUCHER'"
+                  v-model="activityModel.voucherId"
                   class="form-input"
                   clearable
+                  filterable
                   placeholder="请选择"
-                  style="width: 100%"
-                />
-              </el-form-item>
-
-              <!-- 优惠券 -->
-              <el-form-item label="优惠券" prop="couponId">
-                <el-select v-model="activityModel.couponId" class="form-input" clearable filterable placeholder="请选择">
+                >
+                  <el-option v-for="item in couponList" :key="item.id" :label="item.name" :value="item.id" />
+                </el-select>
+                <el-select v-else v-model="activityModel.couponId" class="form-input" clearable filterable placeholder="请选择">
                   <el-option v-for="item in couponList" :key="item.id" :label="item.name" :value="item.id" />
                 </el-select>
               </el-form-item>
 
-              <!-- 优惠券发放数量 -->
-              <el-form-item label="优惠券发放数量" prop="couponQuantity">
-                <el-input v-model="activityModel.couponQuantity" placeholder="请输入" maxlength="5" />
+              <!-- 优惠券/代金券发放数量 -->
+              <el-form-item
+                :label="(activityModel.rewardType === 'VOUCHER' ? '代金券' : '优惠券') + '发放数量'"
+                :prop="activityModel.rewardType === 'VOUCHER' ? 'voucherQuantity' : 'couponQuantity'"
+              >
+                <el-input
+                  v-if="activityModel.rewardType === 'VOUCHER'"
+                  v-model="activityModel.voucherQuantity"
+                  placeholder="请输入"
+                  maxlength="5"
+                />
+                <el-input v-else v-model="activityModel.couponQuantity" placeholder="请输入" maxlength="5" />
               </el-form-item>
             </template>
 
             <!-- 营销活动相关字段 -->
-            <template v-if="activityModel.activityType === 1">
+            <template v-if="activityModel.activityType === 'MARKETING'">
               <!-- 报名时间 -->
               <el-form-item class="activity-time-item" label="报名时间" prop="signupTimeRange">
                 <el-date-picker
@@ -118,7 +135,7 @@
                 <el-radio :label="1"> 本地上传 </el-radio>
                 <el-radio :label="2"> AI生成 </el-radio>
               </el-radio-group>
-              <div v-if="type !== 'add'" style=" margin-top: 4px;font-size: 12px; color: #909399">
+              <div v-if="type !== 'add'" style="margin-top: 4px; font-size: 12px; color: #909399">
                 编辑模式下固定为"本地上传",以便编辑图片
               </div>
             </el-form-item>
@@ -227,13 +244,7 @@ import type { FormInstance, UploadFile, UploadProps } from "element-plus";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 import { useRoute, useRouter } from "vue-router";
-import {
-  addActivity,
-  getActivityDetail,
-  getActivityRuleOptions,
-  getCouponList,
-  updateActivity
-} from "@/api/modules/operationManagement";
+import { addActivity, getActivityDetail, getCouponList, updateActivity } from "@/api/modules/operationManagement";
 import { uploadContractImage } from "@/api/modules/licenseManagement";
 import { localGet } from "@/utils";
 
@@ -264,22 +275,6 @@ const imageViewerVisible = ref(false);
 const imageViewerUrlList = ref<string[]>([]);
 const imageViewerInitialIndex = ref(0);
 
-// 活动规则级联选择器选项
-const ruleCascaderOptions = ref<any[]>([]);
-
-// 级联选择器配置
-const cascaderProps = {
-  expandTrigger: "hover" as const,
-  emitPath: true,
-  disabled: (data: any) => {
-    // 除了 "当用户 > 核销并评论 > 优惠券" 这个路径,其余选项不可选择
-    if (data.disabled !== undefined) {
-      return data.disabled;
-    }
-    return false;
-  }
-};
-
 // 是否有未上传的图片
 const hasUnuploadedImages = computed(() => {
   return (
@@ -309,18 +304,18 @@ const rules = reactive({
         const end = new Date(endTime);
         const today = new Date();
         today.setHours(0, 0, 0, 0);
-        if (start < today) {
-          callback(new Error("活动开始时间不能早于当前时间"));
-          return;
-        }
-        if (start >= end) {
-          callback(new Error("活动开始时间必须早于活动结束时间"));
-          return;
-        }
+        // if (start < today) {
+        //   callback(new Error("活动开始时间不能早于当前时间"));
+        //   return;
+        // }
+        // if (start >= end) {
+        //   callback(new Error("活动开始时间必须早于活动结束时间"));
+        //   return;
+        // }
         callback();
         // 活动时间验证通过后,重新验证报名时间(如果已设置)
         if (
-          activityModel.value.activityType === 1 &&
+          activityModel.value.activityType === "MARKETING" &&
           activityModel.value.signupTimeRange &&
           Array.isArray(activityModel.value.signupTimeRange) &&
           activityModel.value.signupTimeRange.length === 2
@@ -337,7 +332,7 @@ const rules = reactive({
     { required: true, message: "请输入用户可参与次数", trigger: "blur" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 2) {
+        if (activityModel.value.activityType === "COMMENT") {
           if (!value) {
             callback(new Error("请输入用户可参与次数"));
             return;
@@ -357,13 +352,13 @@ const rules = reactive({
       trigger: "blur"
     }
   ],
-  activityRule: [
-    { required: true, message: "请选择活动规则", trigger: "change" },
+  rewardType: [{ required: true, message: "请选择券类型", trigger: "change" }],
+  couponId: [
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 2) {
-          if (!value || !Array.isArray(value) || value.length < 2) {
-            callback(new Error("请选择完整的活动规则(至少选择角色和行为)"));
+        if (activityModel.value.activityType === "COMMENT" && activityModel.value.rewardType === "COUPON") {
+          if (!value) {
+            callback(new Error("请选择优惠券"));
             return;
           }
         }
@@ -372,13 +367,35 @@ const rules = reactive({
       trigger: "change"
     }
   ],
-  couponId: [
-    { required: true, message: "请选择优惠券", trigger: "change" },
+  couponQuantity: [
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 2) {
+        if (activityModel.value.activityType === "COMMENT" && activityModel.value.rewardType === "COUPON") {
           if (!value) {
-            callback(new Error("请选择优惠券"));
+            callback(new Error("请输入优惠券发放数量"));
+            return;
+          }
+          const numValue = Number(value);
+          if (isNaN(numValue) || !Number.isInteger(numValue) || numValue <= 0) {
+            callback(new Error("优惠券发放数量必须为正整数"));
+            return;
+          }
+          if (numValue > 99999) {
+            callback(new Error("优惠券发放数量必须小于99999"));
+            return;
+          }
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  voucherId: [
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        if (activityModel.value.activityType === "COMMENT" && activityModel.value.rewardType === "VOUCHER") {
+          if (!value) {
+            callback(new Error("请选择代金券"));
             return;
           }
         }
@@ -387,22 +404,21 @@ const rules = reactive({
       trigger: "change"
     }
   ],
-  couponQuantity: [
-    { required: true, message: "请输入优惠券发放数量", trigger: "blur" },
+  voucherQuantity: [
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 2) {
+        if (activityModel.value.activityType === "COMMENT" && activityModel.value.rewardType === "VOUCHER") {
           if (!value) {
-            callback(new Error("请输入优惠券发放数量"));
+            callback(new Error("请输入代金券发放数量"));
             return;
           }
           const numValue = Number(value);
           if (isNaN(numValue) || !Number.isInteger(numValue) || numValue <= 0) {
-            callback(new Error("优惠券发放数量必须为正整数"));
+            callback(new Error("代金券发放数量必须为正整数"));
             return;
           }
           if (numValue > 99999) {
-            callback(new Error("优惠券发放数量必须小于99999"));
+            callback(new Error("代金券发放数量必须小于99999"));
             return;
           }
         }
@@ -415,7 +431,7 @@ const rules = reactive({
     { required: true, message: "请选择报名时间", trigger: "change" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === "MARKETING") {
           if (!value || !Array.isArray(value) || value.length !== 2) {
             callback(new Error("请选择报名时间"));
             return;
@@ -427,28 +443,29 @@ const rules = reactive({
           }
           const start = new Date(startTime);
           const end = new Date(endTime);
-          if (start >= end) {
-            callback(new Error("报名开始时间必须早于报名结束时间"));
-            return;
-          }
-          // 检查报名时间是否在活动开始时间之前
+          start.setHours(0, 0, 0, 0);
+          end.setHours(0, 0, 0, 0);
+          // if (start.getTime() >= end.getTime()) {
+          //   callback(new Error("报名开始时间必须早于报名结束时间"));
+          //   return;
+          // }
+          // 报名时间必须在活动时间范围内(活动开始日期~活动结束日期)
           if (
             activityModel.value.activityTimeRange &&
             Array.isArray(activityModel.value.activityTimeRange) &&
             activityModel.value.activityTimeRange.length === 2
           ) {
-            const activityStartTime = new Date(activityModel.value.activityTimeRange[0]);
-            activityStartTime.setHours(0, 0, 0, 0);
-            start.setHours(0, 0, 0, 0);
-            end.setHours(0, 0, 0, 0);
-
-            // 报名开始时间和结束时间都必须在活动开始时间之前(不能等于)
-            if (start >= activityStartTime) {
-              callback(new Error("报名开始时间必须在活动开始时间之前"));
+            const activityStart = new Date(activityModel.value.activityTimeRange[0]);
+            activityStart.setHours(0, 0, 0, 0);
+            const activityEnd = new Date(activityModel.value.activityTimeRange[1]);
+            activityEnd.setHours(0, 0, 0, 0);
+
+            if (start.getTime() < activityStart.getTime()) {
+              callback(new Error("报名开始时间必须在活动时间范围内"));
               return;
             }
-            if (end >= activityStartTime) {
-              callback(new Error("报名结束时间必须在活动开始时间之前"));
+            if (end.getTime() > activityEnd.getTime()) {
+              callback(new Error("报名结束时间必须在活动时间范围内"));
               return;
             }
           }
@@ -462,7 +479,7 @@ const rules = reactive({
     { required: true, message: "请输入活动详情", trigger: ["blur", "change"] },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === "MARKETING") {
           if (!value || value.trim() === "") {
             callback(new Error("请输入活动详情"));
             return;
@@ -525,8 +542,8 @@ const rules = reactive({
 
 // ==================== 活动信息数据模型 ====================
 const activityModel = ref<any>({
-  // 活动类型:1-营销活动,2-评论有礼
-  activityType: 1,
+  // 活动类型:'MARKETING'-营销活动,'COMMENT'-评论有礼
+  activityType: "MARKETING",
   // 活动宣传图(包含标题和详情)
   promotionImages: null,
   // 活动标题图片
@@ -543,12 +560,20 @@ const activityModel = ref<any>({
   activityTimeRange: [],
   // 用户可参与次数(评论有礼)
   participationLimit: "",
-  // 活动规则(级联选择器的值数组)(评论有礼)
-  activityRule: [],
+  // 券类型:'COUPON'-优惠券,'VOUCHER'-代金券(评论有礼)
+  rewardType: "COUPON",
   // 优惠券ID(评论有礼)
   couponId: "",
+  // 优惠券名称(评论有礼)
+  couponName: "",
   // 优惠券发放数量(评论有礼)
   couponQuantity: "",
+  // 代金券ID(评论有礼)
+  voucherId: "",
+  // 代金券名称(评论有礼)
+  voucherName: "",
+  // 代金券发放数量(评论有礼)
+  voucherQuantity: "",
   // 报名时间范围(营销活动)
   signupTimeRange: [],
   // 活动限制人数(营销活动)
@@ -570,24 +595,24 @@ const disabledDate = (time: Date) => {
   return time.getTime() < today.getTime();
 };
 
-// 禁用报名日期(不能早于今天,且不能晚于或等于活动开始时间)
+// 禁用报名日期:只能选择活动时间范围内的日期(在活动开始日期与结束日期之间)
 const disabledSignupDate = (time: Date) => {
-  const today = new Date();
-  today.setHours(0, 0, 0, 0);
+  const t = new Date(time);
+  t.setHours(0, 0, 0, 0);
 
-  // 不能早于今天
-  if (time.getTime() < today.getTime()) {
+  // 未选择活动时间时,不允许选择报名日期
+  if (!activityModel.value.activityTimeRange || activityModel.value.activityTimeRange.length !== 2) {
     return true;
   }
 
-  // 如果已选择活动时间,报名时间不能晚于或等于活动开始时间
-  if (activityModel.value.activityTimeRange && activityModel.value.activityTimeRange.length === 2) {
-    const activityStartTime = new Date(activityModel.value.activityTimeRange[0]);
-    activityStartTime.setHours(0, 0, 0, 0);
-    // 报名时间必须早于活动开始时间(不能等于)
-    return time.getTime() >= activityStartTime.getTime();
-  }
+  const activityStart = new Date(activityModel.value.activityTimeRange[0]);
+  activityStart.setHours(0, 0, 0, 0);
+  const activityEnd = new Date(activityModel.value.activityTimeRange[1]);
+  activityEnd.setHours(0, 0, 0, 0);
 
+  // 只能选择活动时间区域内的日期:早于活动开始或晚于活动结束的日期禁用
+  if (t.getTime() < activityStart.getTime()) return true;
+  if (t.getTime() > activityEnd.getTime()) return true;
   return false;
 };
 
@@ -977,6 +1002,40 @@ const handlePictureCardPreview = (file: any) => {
   imageViewerVisible.value = true;
 };
 
+// ==================== 加载优惠券列表函数 ====================
+
+/**
+ * 根据券类型加载优惠券/代金券列表
+ */
+const loadCouponList = async () => {
+  if (activityModel.value.activityType !== "COMMENT") return;
+
+  try {
+    const params = {
+      storeId: localGet("createdId"),
+      groupType: localGet("businessSection"),
+      couponType: activityModel.value.rewardType === "VOUCHER" ? "1" : "2", // 代金券传1,优惠券传2
+      couponStatus: "1",
+      couponsFromType: 1,
+      pageNum: 1,
+      pageSize: 99999
+    };
+    const res: any = await getCouponList(params);
+    if (res && res.code == 200) {
+      const isVoucher = activityModel.value.rewardType === "VOUCHER";
+      // 代金券(couponType传1):取 data.couponList;优惠券:取 discountList.records
+      if (isVoucher) {
+        const raw = res.data.couponList;
+        couponList.value = Array.isArray(raw) ? raw : (raw?.records ?? raw?.list ?? []) || [];
+      } else {
+        couponList.value = res.data?.discountList?.records || [];
+      }
+    }
+  } catch (error) {
+    console.error("加载优惠券列表失败:", error);
+  }
+};
+
 // ==================== 监听器 ====================
 
 /**
@@ -991,21 +1050,43 @@ watch(
     if (newVal === oldVal) return;
 
     nextTick(() => {
-      if (newVal === 1) {
+      // 兼容旧数据:如果传入的是数字,转换为字符串
+      let typeValue = newVal;
+      if (typeValue == 1 || typeValue == "1") {
+        typeValue = "MARKETING";
+      } else if (typeValue == 2 || typeValue == "2") {
+        typeValue = "COMMENT";
+      }
+
+      if (typeValue === "MARKETING") {
         // 切换到营销活动:清除评论有礼相关字段
         activityModel.value.participationLimit = "";
-        activityModel.value.activityRule = [];
+        activityModel.value.rewardType = "COUPON";
         activityModel.value.couponId = "";
+        activityModel.value.couponName = "";
         activityModel.value.couponQuantity = "";
+        activityModel.value.voucherId = "";
+        activityModel.value.voucherName = "";
+        activityModel.value.voucherQuantity = "";
+        couponList.value = [];
         // 清除评论有礼字段的验证状态
-        ruleFormRef.value?.clearValidate(["participationLimit", "activityRule", "couponId", "couponQuantity"]);
-      } else if (newVal === 2) {
+        ruleFormRef.value?.clearValidate([
+          "participationLimit",
+          "rewardType",
+          "couponId",
+          "couponQuantity",
+          "voucherId",
+          "voucherQuantity"
+        ]);
+      } else if (typeValue === "COMMENT") {
         // 切换到评论有礼:清除营销活动相关字段
         activityModel.value.signupTimeRange = [];
         activityModel.value.activityLimitPeople = "";
         activityModel.value.activityDetails = "";
         // 清除营销活动字段的验证状态
         ruleFormRef.value?.clearValidate(["signupTimeRange", "activityLimitPeople", "activityDetails"]);
+        // 加载优惠券列表
+        loadCouponList();
       }
     });
   },
@@ -1013,6 +1094,36 @@ watch(
 );
 
 /**
+ * 监听券类型变化,切换时重新加载列表并清空已选券
+ */
+watch(
+  () => activityModel.value.rewardType,
+  (newVal, oldVal) => {
+    if (oldVal === undefined) return; // 初始化时不处理
+    if (newVal === oldVal) return;
+    if (activityModel.value.activityType !== "COMMENT") return; // 只有评论有礼才处理
+
+    nextTick(() => {
+      // 清空已选券(根据类型清空对应字段)
+      if (newVal === "VOUCHER") {
+        activityModel.value.voucherId = "";
+        activityModel.value.voucherName = "";
+        activityModel.value.voucherQuantity = "";
+        ruleFormRef.value?.clearValidate(["voucherId", "voucherQuantity"]);
+      } else {
+        activityModel.value.couponId = "";
+        activityModel.value.couponName = "";
+        activityModel.value.couponQuantity = "";
+        ruleFormRef.value?.clearValidate(["couponId", "couponQuantity"]);
+      }
+      couponList.value = [];
+      // 重新加载列表
+      loadCouponList();
+    });
+  }
+);
+
+/**
  * 监听上传图片方式变化,切换时清除相关数据并重新验证
  */
 watch(
@@ -1048,7 +1159,7 @@ watch(
 );
 
 /**
- * 监听活动时间变化,如果报名时间不符合要求则清空并提示
+ * 监听活动时间变化,如果报名时间不在新的活动时间范围内则清空并提示
  */
 watch(
   () => activityModel.value.activityTimeRange,
@@ -1057,7 +1168,7 @@ watch(
     if (oldVal === undefined || oldVal === null) return;
 
     // 只有营销活动才需要检查报名时间
-    if (activityModel.value.activityType !== 1) return;
+    if (activityModel.value.activityType !== "MARKETING") return;
 
     // 如果活动时间已选择且报名时间已选择
     if (
@@ -1066,22 +1177,24 @@ watch(
       activityModel.value.signupTimeRange &&
       activityModel.value.signupTimeRange.length === 2
     ) {
-      const activityStartTime = new Date(newVal[0]);
-      activityStartTime.setHours(0, 0, 0, 0);
-      const signupStartTime = new Date(activityModel.value.signupTimeRange[0]);
-      signupStartTime.setHours(0, 0, 0, 0);
-      const signupEndTime = new Date(activityModel.value.signupTimeRange[1]);
-      signupEndTime.setHours(0, 0, 0, 0);
-
-      // 如果报名开始时间或结束时间晚于或等于活动开始时间,清空报名时间并提示
-      if (signupStartTime >= activityStartTime || signupEndTime >= activityStartTime) {
+      const activityStart = new Date(newVal[0]);
+      activityStart.setHours(0, 0, 0, 0);
+      const activityEnd = new Date(newVal[1]);
+      activityEnd.setHours(0, 0, 0, 0);
+      const signupStart = new Date(activityModel.value.signupTimeRange[0]);
+      signupStart.setHours(0, 0, 0, 0);
+      const signupEnd = new Date(activityModel.value.signupTimeRange[1]);
+      signupEnd.setHours(0, 0, 0, 0);
+
+      // 报名时间必须在活动时间范围内,否则清空报名时间并提示
+      const outOfRange = signupStart.getTime() < activityStart.getTime() || signupEnd.getTime() > activityEnd.getTime();
+      if (outOfRange) {
         activityModel.value.signupTimeRange = [];
         nextTick(() => {
           ruleFormRef.value?.clearValidate("signupTimeRange");
-          ElMessage.warning("活动时间已调整,报名时间必须在活动开始时间之前,请重新选择报名时间");
+          ElMessage.warning("活动时间已调整,报名时间必须在活动时间范围内,请重新选择报名时间");
         });
       } else {
-        // 如果报名时间符合要求,重新验证
         nextTick(() => {
           ruleFormRef.value?.validateField("signupTimeRange");
         });
@@ -1099,34 +1212,9 @@ onMounted(async () => {
   id.value = (route.query.id as string) || "";
   type.value = (route.query.type as string) || "";
 
-  // 加载优惠券列表
-  try {
-    const params = {
-      storeId: localGet("createdId"),
-      groupType: localGet("businessSection"),
-      couponType: "2",
-      couponStatus: "1",
-      couponsFromType: 1,
-      pageNum: 1,
-      pageSize: 99999
-    };
-    const res: any = await getCouponList(params);
-    if (res && res.code == 200) {
-      couponList.value = res.data?.discountList?.records || [];
-    }
-  } catch (error) {
-    console.error("加载优惠券列表失败:", error);
-  }
-
-  // 加载活动规则级联选择器选项
-  try {
-    const res: any = await getActivityRuleOptions({ page: 1, size: 99999 });
-    console.log("ruleCascaderOptions:", res.data);
-    if (res && res.code == 200) {
-      ruleCascaderOptions.value = res.data || [];
-    }
-  } catch (error) {
-    console.error("加载活动规则选项失败:", error);
+  // 如果是评论有礼类型,初始化时加载优惠券列表
+  if (activityModel.value.activityType === "COMMENT") {
+    await loadCouponList();
   }
 
   // 编辑模式下加载数据
@@ -1134,9 +1222,15 @@ onMounted(async () => {
     try {
       const res: any = await getActivityDetail({ id: id.value });
       if (res && res.code == 200) {
-        // 先设置 activityType 为数字类型,确保下拉框正确显示
+        // 兼容旧数据:如果后端返回的是数字,转换为字符串
         if (res.data.activityType !== undefined) {
-          activityModel.value.activityType = Number(res.data.activityType);
+          let activityTypeValue = res.data.activityType;
+          if (activityTypeValue == 1 || activityTypeValue == "1") {
+            activityTypeValue = "MARKETING";
+          } else if (activityTypeValue == 2 || activityTypeValue == "2") {
+            activityTypeValue = "COMMENT";
+          }
+          activityModel.value.activityType = activityTypeValue;
         }
 
         // 合并其他数据
@@ -1151,7 +1245,7 @@ onMounted(async () => {
         }
 
         // 根据活动类型加载对应字段
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === "MARKETING") {
           // 营销活动:加载报名时间、活动限制人数、活动详情
           if (res.data.signupStartTime && res.data.signupEndTime) {
             // 只取日期部分(去掉时分秒)
@@ -1169,21 +1263,35 @@ onMounted(async () => {
           }
           // 加载活动详情
           activityModel.value.activityDetails = res.data.activityDetails || "";
-        } else if (activityModel.value.activityType === 2) {
-          // 评论有礼:加载活动规则、优惠券、优惠券发放数量、用户可参与次数
-          // 加载活动规则
-          if (res.data.activityRule) {
-            activityModel.value.activityRule = res.data.activityRule.split(",");
+        } else if (activityModel.value.activityType === "COMMENT") {
+          // 评论有礼:加载活动规则、券类型、优惠券/代金券、发放数量、用户可参与次数
+          // 加载券类型('COUPON'-优惠券,'VOUCHER'-代金券)
+          // 兼容旧数据:如果后端返回的是数字,转换为新格式
+          if (res.data.rewardType) {
+            activityModel.value.rewardType = res.data.rewardType;
+          } else if (res.data.couponType !== undefined) {
+            // 兼容旧数据格式:1->COUPON, 4->VOUCHER
+            activityModel.value.rewardType = res.data.couponType === 4 ? "VOUCHER" : "COUPON";
           } else {
-            activityModel.value.activityRule = [];
+            activityModel.value.rewardType = "COUPON";
           }
-          // 加载优惠券ID
-          activityModel.value.couponId = res.data.couponId || "";
-          // 加载优惠券发放数量
-          if (res.data.couponQuantity !== undefined && res.data.couponQuantity !== null) {
-            activityModel.value.couponQuantity = String(res.data.couponQuantity);
+          // 根据 rewardType 加载对应的字段
+          if (activityModel.value.rewardType === "VOUCHER") {
+            activityModel.value.voucherId = res.data.voucherId || "";
+            activityModel.value.voucherName = res.data.voucherName || "";
+            if (res.data.voucherQuantity !== undefined && res.data.voucherQuantity !== null) {
+              activityModel.value.voucherQuantity = String(res.data.voucherQuantity);
+            } else {
+              activityModel.value.voucherQuantity = "";
+            }
           } else {
-            activityModel.value.couponQuantity = "";
+            activityModel.value.couponId = res.data.couponId || "";
+            activityModel.value.couponName = res.data.couponName || "";
+            if (res.data.couponQuantity !== undefined && res.data.couponQuantity !== null) {
+              activityModel.value.couponQuantity = String(res.data.couponQuantity);
+            } else {
+              activityModel.value.couponQuantity = "";
+            }
           }
           // 加载用户可参与次数
           if (res.data.participationLimit !== undefined && res.data.participationLimit !== null) {
@@ -1191,6 +1299,8 @@ onMounted(async () => {
           } else {
             activityModel.value.participationLimit = "";
           }
+          // 根据券类型加载对应的列表
+          await loadCouponList();
         }
         // 编辑模式:默认使用本地上传模式(uploadImgType=1),以便编辑图片和banner图
         activityModel.value.uploadImgType = 1; // 编辑时固定为1-本地上传
@@ -1281,7 +1391,10 @@ const handleSubmit = async () => {
 
   await ruleFormRef.value.validate(async valid => {
     if (valid) {
-      const [startTime, endTime] = activityModel.value.activityTimeRange || [];
+      const [activityStartDate, activityEndDate] = activityModel.value.activityTimeRange || [];
+      // 活动时间:开始日期传当天 00:00:00,结束日期传当天 23:59:59
+      const startTime = activityStartDate ? `${activityStartDate} 00:00:00` : "";
+      const endTime = activityEndDate ? `${activityEndDate} 23:59:59` : "";
       const auditParam = {
         text: `${activityModel.value.activityName}, ${activityModel.value.imgDescribe || ""}`,
         image_urls: [titleImageUrl.value, detailImageUrl.value]
@@ -1289,8 +1402,8 @@ const handleSubmit = async () => {
       const params: any = {
         activityType: activityModel.value.activityType,
         activityName: activityModel.value.activityName,
-        startTime: startTime,
-        endTime: endTime,
+        startTime,
+        endTime,
         uploadImgType: activityModel.value.uploadImgType,
         storeId: localGet("createdId"),
         groupType: localGet("businessSection"),
@@ -1299,24 +1412,30 @@ const handleSubmit = async () => {
       };
 
       // 根据活动类型添加不同的字段,确保只提交对应类型的字段
-      if (activityModel.value.activityType === 1) {
-        // 营销活动:只添加营销活动相关字段
-        const [signupStartTime, signupEndTime] = activityModel.value.signupTimeRange || [];
-        params.signupStartTime = signupStartTime;
-        params.signupEndTime = signupEndTime;
+      if (activityModel.value.activityType === "MARKETING") {
+        // 营销活动:只添加营销活动相关字段;报名时间开始传当天 00:00:00,结束传当天 23:59:59
+        const [signupStartDate, signupEndDate] = activityModel.value.signupTimeRange || [];
+        params.signupStartTime = signupStartDate ? `${signupStartDate} 00:00:00` : "";
+        params.signupEndTime = signupEndDate ? `${signupEndDate} 23:59:59` : "";
         params.activityLimitPeople = activityModel.value.activityLimitPeople;
         params.activityDetails = activityModel.value.activityDetails;
         // 确保不包含评论有礼的字段
         delete params.participationLimit;
-        delete params.activityRule;
+        delete params.rewardType;
         delete params.couponId;
         delete params.couponQuantity;
-      } else if (activityModel.value.activityType === 2) {
+      } else if (activityModel.value.activityType === "COMMENT") {
         // 评论有礼:只添加评论有礼相关字段
         params.participationLimit = activityModel.value.participationLimit;
-        params.activityRule = activityModel.value.activityRule.join(",");
-        params.couponId = activityModel.value.couponId;
-        params.couponQuantity = activityModel.value.couponQuantity;
+        params.rewardType = activityModel.value.rewardType; // 'COUPON'-优惠券,'VOUCHER'-代金券
+        // 根据 rewardType 提交对应的字段
+        if (activityModel.value.rewardType === "VOUCHER") {
+          params.voucherId = activityModel.value.voucherId;
+          params.voucherQuantity = activityModel.value.voucherQuantity;
+        } else {
+          params.couponId = activityModel.value.couponId;
+          params.couponQuantity = activityModel.value.couponQuantity;
+        }
         // 确保不包含营销活动的字段
         delete params.signupStartTime;
         delete params.signupEndTime;

+ 1 - 1
src/views/priceList/edit.vue

@@ -579,7 +579,7 @@ function applyDetailToForm(data: any) {
   formModel.reserveRule = d.reserveRule ?? "";
   formModel.peopleLimit = d.peopleLimit != null ? String(d.peopleLimit) : "";
   formModel.usageRule = d.usageRule ?? "";
-  formModel.createTime = d.createTime ?? d.submitTime ?? "";
+  formModel.createTime = d.createdTime ?? "";
   formModel.auditTime = d.auditTime ?? d.updateTime ?? "";
   formModel.auditStatus = d.status !== undefined && d.status !== null ? d.status : d.auditStatus != null ? d.auditStatus : null;
   formModel.rejectionReason = d.rejectionReason ?? d.rejectReason ?? "";

+ 0 - 2
src/views/priceList/index.vue

@@ -327,7 +327,6 @@ const confirmDelete = async () => {
     }
   } catch (error) {
     console.error("删除失败:", error);
-    ElMessage.error("删除失败");
   }
 };
 
@@ -350,7 +349,6 @@ const handleShelfToggle = async (row: PriceListRow, shelfStatus: number) => {
         }
       } catch (error) {
         console.error("操作失败:", error);
-        ElMessage.error("操作失败");
       }
     })
     .catch(() => {

+ 1 - 1
src/views/storeDecoration/basicStoreInformation/index.vue

@@ -113,7 +113,7 @@
               type="textarea"
               :rows="3"
               placeholder="请输入详细地址"
-              maxlength="200"
+              maxlength="300"
               show-word-limit
             />
           </el-form-item>

+ 3 - 3
src/views/storeDecoration/facilitiesAndServices/components/FacilityManagement.vue

@@ -303,9 +303,9 @@ const editItem = async (item: Facility) => {
       formData.facilityCategory = detail.facilityCategory || 1;
       formData.facilityName = detail.facilityName || "";
       // 新API中没有这些字段,设置为默认值
-      formData.quantity = undefined;
-      formData.brand = "";
-      formData.description = "";
+      formData.quantity = detail.quantity || 0;
+      formData.brand = detail.brand || "";
+      formData.description = detail.description || "";
       formData.displayInStoreDetail = detail.displayInStoreDetail !== undefined ? detail.displayInStoreDetail : 1;
       dialogVisible.value = true;
     } else {

+ 70 - 185
src/views/storeDecoration/personnelConfig/index.vue

@@ -239,6 +239,7 @@
               :width="'150px'"
               :height="'150px'"
               :border-radius="'8px'"
+              :compress="true"
               :api="handleCustomUpload"
               :show-success-notification="false"
               :on-success="handleBackgroundImageSuccess"
@@ -709,7 +710,7 @@ const formData = reactive({
   description: ""
 });
 
-// 同步组件内部文件列表到 formData
+// 同步组件内部文件列表到 formData(避免上传中因过滤 blob 导致列表被清空或重复)
 const syncFileListFromComponent = async () => {
   if (!backgroundImagesUploadRef.value || !dialogVisible.value) return;
 
@@ -718,47 +719,43 @@ const syncFileListFromComponent = async () => {
     if (componentInstance && componentInstance._fileList) {
       const internalFileList = componentInstance._fileList.value || componentInstance._fileList;
       if (Array.isArray(internalFileList)) {
+        // 若组件内仍有 blob URL,说明还有文件在上传中,不覆盖 formData,避免清空或重复
+        const hasUploading = internalFileList.some((file: UploadUserFile) => file?.url && String(file.url).startsWith("blob:"));
+        if (hasUploading) return;
+
         // 同步内部文件列表到 formData(排除 blob URL,只保留服务器 URL)
         const validFiles = internalFileList
           .filter((file: UploadUserFile) => {
             const hasUrl = file && file.url && typeof file.url === "string" && file.url.trim() !== "";
-            // 排除 blob URL,只保留服务器 URL
-            const isBlobUrl = hasUrl && file.url.startsWith("blob:");
+            const isBlobUrl = hasUrl && (file.url as string).startsWith("blob:");
             return hasUrl && !isBlobUrl;
           })
-          .map((file: UploadUserFile) => {
-            // 确保每个文件对象都有必要的属性
-            return {
-              ...file,
-              url: file.url!.trim(),
-              name: file.name || file.url!.split("/").pop() || "image",
-              uid: file.uid || `file-${Date.now()}-${Math.random()}`
-            };
-          });
+          .map((file: UploadUserFile) => ({
+            ...file,
+            url: file.url!.trim(),
+            name: file.name || file.url!.split("/").pop() || "image",
+            uid: file.uid ?? `file-${Date.now()}-${Math.random()}`
+          }));
+
+        // 按 URL 去重,避免同一张图出现多次
+        const seenUrls = new Set<string>();
+        const uniqueFiles = validFiles.filter((f: { url?: string }) => {
+          const u = f.url?.trim();
+          if (!u || seenUrls.has(u)) return false;
+          seenUrls.add(u);
+          return true;
+        });
 
-        // 检查是否需要更新(比较 URL 列表,避免不必要的更新)
-        // 只比较非 blob URL 的 URLs
         const currentUrls = formData.backgroundImages
-          .filter((f: UploadUserFile) => f && f.url && !f.url.startsWith("blob:"))
+          .filter((f: UploadUserFile) => f && f.url && !(f.url as string).startsWith("blob:"))
           .map((f: UploadUserFile) => f.url!.trim())
           .sort();
-        const newUrls = validFiles.map((f: UploadUserFile) => f.url!.trim()).sort();
+        const newUrls = uniqueFiles.map((f: { url?: string }) => (f.url || "").trim()).sort();
 
-        // 未传背景时不以组件内残留数据覆盖:表单为空且组件有项,说明是上次编辑的残留,不覆盖
-        if (formData.backgroundImages.length === 0 && validFiles.length > 0) return;
+        if (formData.backgroundImages.length === 0 && uniqueFiles.length > 0) return;
 
-        // 如果不一致,更新 formData(直接使用组件内部的文件列表)
         if (JSON.stringify(currentUrls) !== JSON.stringify(newUrls)) {
-          // 直接使用组件内部的文件列表,确保数据一致
-          formData.backgroundImages = [...validFiles];
-
-          console.log("同步文件列表成功,更新数量:", validFiles.length);
-          console.log(
-            "同步后的文件列表 URLs:",
-            formData.backgroundImages.map(f => f.url)
-          );
-
-          // 触发验证
+          formData.backgroundImages = [...uniqueFiles] as UploadUserFile[];
           await nextTick();
           if (formRef.value) {
             formRef.value.validateField("backgroundImages");
@@ -771,158 +768,43 @@ const syncFileListFromComponent = async () => {
   }
 };
 
-// 背景图片上传成功回调
-const handleBackgroundImageSuccess = async (url: string) => {
-  console.log("=== 背景图片上传成功回调 ===");
-  console.log("上传成功的URL:", url);
-  console.log("当前 formData.backgroundImages 数量:", formData.backgroundImages.length);
-  console.log(
-    "当前 formData.backgroundImages URLs:",
-    formData.backgroundImages.map(f => f?.url)
-  );
-
-  // 立即将新上传的 URL 添加到 formData(如果还没有的话)
-  const urlExists = formData.backgroundImages.some((file: UploadUserFile) => {
-    return file && file.url && file.url.trim() === url.trim();
-  });
-
-  if (!urlExists) {
-    console.log("新上传的 URL 不在 formData 中,立即添加");
-    const newFile: UploadUserFile = {
-      uid: `upload-${Date.now()}-${Math.random()}`,
-      name: url.split("/").pop() || "image.jpg",
-      url: url.trim(),
-      status: "success" as const,
-      response: { fileUrl: url }
-    };
-    formData.backgroundImages = [...formData.backgroundImages, newFile];
-    console.log("添加新文件后的 formData.backgroundImages 数量:", formData.backgroundImages.length);
-    console.log(
-      "添加新文件后的 formData.backgroundImages URLs:",
-      formData.backgroundImages.map(f => f.url)
-    );
-  }
-
-  // 等待组件更新完成(el-upload 的 onSuccess 可能需要时间)
-  await nextTick();
-  await nextTick();
-  await nextTick();
-
-  // 使用轮询方式,确保组件内部已完全更新
-  let retryCount = 0;
-  const maxRetries = 15;
-
-  while (retryCount < maxRetries) {
-    if (backgroundImagesUploadRef.value) {
-      try {
-        const componentInstance = backgroundImagesUploadRef.value;
-        if (componentInstance && componentInstance._fileList) {
-          const internalFileList = componentInstance._fileList.value || componentInstance._fileList;
-          if (Array.isArray(internalFileList)) {
-            // 获取所有有效文件(排除 blob URL)
-            const validFiles = internalFileList
-              .filter((file: UploadUserFile) => {
-                const hasUrl = file && file.url && typeof file.url === "string" && file.url.trim() !== "";
-                const isBlobUrl = hasUrl && file.url.startsWith("blob:");
-                return hasUrl && !isBlobUrl;
-              })
-              .map((file: UploadUserFile) => ({
-                ...file,
-                url: file.url!.trim(),
-                name: file.name || file.url!.split("/").pop() || "image",
-                uid: file.uid || `file-${Date.now()}-${Math.random()}`
-              }));
-
-            // 检查新上传的 URL 是否已经在组件内部的文件列表中
-            const hasNewUrl = validFiles.some((file: UploadUserFile) => {
-              return file && file.url && file.url.trim() === url.trim();
-            });
-
-            console.log(`轮询第 ${retryCount + 1} 次 - 组件内部文件数量: ${validFiles.length}, 是否包含新URL: ${hasNewUrl}`);
-            console.log(
-              `轮询第 ${retryCount + 1} 次 - 组件内部文件 URLs:`,
-              validFiles.map(f => f.url)
-            );
-
-            if (hasNewUrl || retryCount >= maxRetries - 1) {
-              // 合并 formData 和组件内部的文件,确保包含所有图片
-              const allUrls = new Set<string>();
-              const mergedFiles: UploadUserFile[] = [];
-
-              // 先添加 formData 中的文件
-              formData.backgroundImages.forEach((file: UploadUserFile) => {
-                if (file && file.url && !file.url.startsWith("blob:")) {
-                  const fileUrl = file.url.trim();
-                  if (!allUrls.has(fileUrl)) {
-                    allUrls.add(fileUrl);
-                    mergedFiles.push({
-                      ...file,
-                      url: fileUrl
-                    });
-                  }
-                }
-              });
-
-              // 再添加组件内部的文件(确保新上传的文件被包含)
-              validFiles.forEach((file: UploadUserFile) => {
-                const fileUrl = file.url;
-                if (fileUrl && !fileUrl.startsWith("blob:") && !allUrls.has(fileUrl)) {
-                  allUrls.add(fileUrl);
-                  mergedFiles.push(file);
-                  console.log("从组件内部添加文件:", fileUrl);
-                }
-              });
-
-              // 更新 formData,确保包含新上传的图片
-              formData.backgroundImages = [...mergedFiles];
-
-              console.log("上传成功后同步的文件列表,数量:", mergedFiles.length);
-              console.log(
-                "上传成功后同步的文件列表 URLs:",
-                mergedFiles.map(f => f.url)
-              );
-
-              // 触发验证
-              await nextTick();
-              if (formRef.value) {
-                formRef.value.validateField("backgroundImages");
-              }
-              break;
-            }
-          }
-        }
-      } catch (error) {
-        console.error("上传成功后同步文件列表失败:", error);
-        break;
-      }
-    }
-
-    // 如果还没找到新上传的 URL,等待一下再重试
-    retryCount++;
-    if (retryCount < maxRetries) {
-      await new Promise(resolve => setTimeout(resolve, 150)); // 等待 150ms
-      await nextTick();
+// 背景图片上传成功回调:用接口返回的服务器 URL 直接修正 formData(把 blob 替换为服务器地址),再触发校验
+const handleBackgroundImageSuccess = async (serverUrl: string) => {
+  if (!serverUrl || typeof serverUrl !== "string") return;
+  const url = serverUrl.trim();
+  const list = formData.backgroundImages;
+  let replaceIndex = -1;
+  for (let i = 0; i < list.length; i++) {
+    const item = list[i];
+    if (!item) continue;
+    const u = item.url && typeof item.url === "string" ? item.url.trim() : "";
+    if (!u || u.startsWith("blob:")) {
+      replaceIndex = i;
+      break;
     }
   }
-
-  if (retryCount >= maxRetries) {
-    console.warn("⚠️ 上传成功后,等待组件更新超时,但已同步当前文件列表");
-    // 即使超时,也确保新上传的 URL 在 formData 中
-    const urlExists = formData.backgroundImages.some((file: UploadUserFile) => {
-      return file && file.url && file.url.trim() === url.trim();
-    });
-    if (!urlExists) {
-      const newFile: UploadUserFile = {
-        uid: `upload-${Date.now()}-${Math.random()}`,
-        name: url.split("/").pop() || "image.jpg",
-        url: url.trim(),
-        status: "success" as const,
-        response: { fileUrl: url }
-      };
-      formData.backgroundImages = [...formData.backgroundImages, newFile];
-      console.log("超时后强制添加新文件到 formData");
+  if (replaceIndex >= 0) {
+    formData.backgroundImages = list.map((item, i) =>
+      i === replaceIndex ? { ...item, url, status: "success" as const } : item
+    ) as UploadUserFile[];
+  } else {
+    const hasUrl = list.some((f: UploadUserFile) => f?.url && String(f.url).trim() === url);
+    if (!hasUrl) {
+      formData.backgroundImages = [
+        ...list,
+        {
+          uid: `bg-${Date.now()}-${Math.random()}`,
+          name: url.split("/").pop() || "image",
+          url,
+          status: "success" as const
+        } as unknown as UploadUserFile
+      ];
     }
   }
+  await nextTick();
+  if (formRef.value) {
+    formRef.value.validateField("backgroundImages");
+  }
 };
 
 // 监听背景图片列表变化,确保文件列表正确更新
@@ -990,14 +872,17 @@ watch(
                   lastFileListUrls = currentUrls;
                   lastFileListLength = currentLength;
 
-                  // 直接更新 formData,确保计数正确
-                  const mappedFiles = validFiles.map((file: UploadUserFile) => ({
-                    ...file,
-                    url: file.url!.trim()
-                  }));
-                  formData.backgroundImages = [...mappedFiles];
-
-                  console.log("定时检查检测到文件列表变化,数量:", mappedFiles.length);
+                  // 按 URL 去重后更新 formData,避免同一张图出现多次
+                  const seen = new Set<string>();
+                  const mappedFiles = validFiles
+                    .map((file: UploadUserFile) => ({ ...file, url: (file.url || "").trim() }))
+                    .filter((f: UploadUserFile) => {
+                      const u = f?.url;
+                      if (!u || seen.has(u)) return false;
+                      seen.add(u);
+                      return true;
+                    });
+                  formData.backgroundImages = [...mappedFiles] as UploadUserFile[];
 
                   // 触发验证
                   await nextTick();

+ 4 - 2
src/views/storeDecoration/storeHeadMap/index.vue

@@ -96,7 +96,7 @@
               :show-success-notification="false"
             >
               <template #tip>
-                <div class="upload-tip">上传图片 ({{ formData.singleImage ? "1" : "0" }}/1)</div>
+                <div class="upload-tip">{{ formData }} ({{ formData.singleImage ? "1" : "0" }}/1)</div>
               </template>
             </UploadImg>
           </el-form-item>
@@ -119,7 +119,9 @@
                 :show-success-notification="false"
               >
                 <template #tip>
-                  <div class="upload-tip">上传图片 ({{ formData.multipleImages.length }}/6)</div>
+                  <div class="upload-tip">
+                    上传图片{{ formData.multipleImages.length }} ({{ formData.multipleImages.length }}/6)
+                  </div>
                 </template>
               </UploadImgs>
             </div>