Просмотр исходного кода

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

LuTong 2 дней назад
Родитель
Сommit
882fc4e421
55 измененных файлов с 1166 добавлено и 1565 удалено
  1. 47 0
      src/api/helper/handleAuthExpired.ts
  2. 7 12
      src/api/index.ts
  3. 4 7
      src/api/indexApi.ts
  4. 4 6
      src/api/indexLogin.ts
  5. 7 12
      src/api/indexStore.ts
  6. 5 0
      src/api/modules/couponManagement.ts
  7. 0 48
      src/api/modules/friendRelation.ts
  8. 3 7
      src/api/modules/homeEntry.ts
  9. 3 3
      src/api/modules/newLoginApi.ts
  10. 3 8
      src/api/modules/operationManagement.ts
  11. 3 7
      src/api/modules/performance.ts
  12. 3 9
      src/api/modules/priceList.ts
  13. BIN
      src/assets/images/headImg.png
  14. 0 14
      src/assets/json/authMenuList.json
  15. 2 2
      src/components/Upload/Img.vue
  16. 11 6
      src/components/Upload/Imgs.vue
  17. 2 0
      src/enums/httpEnum.ts
  18. 7 2
      src/utils/errorHandler.ts
  19. 18 0
      src/views/appoinmentManagement/classifyManagement.vue
  20. 1 1
      src/views/businessInfo/manageInfo.vue
  21. 4 4
      src/views/businessInfo/subjectInfo.vue
  22. 5 5
      src/views/businessInfo/zfbIndex.vue
  23. 50 21
      src/views/dynamicManagement/friendCoupon.vue
  24. 39 181
      src/views/dynamicManagement/friendCouponDetail.vue
  25. 0 683
      src/views/dynamicManagement/friendRelation.vue
  26. 3 3
      src/views/dynamicManagement/index.vue
  27. 49 6
      src/views/dynamicManagement/myDynamic.vue
  28. 15 15
      src/views/dynamicManagement/publishDynamic.vue
  29. 19 0
      src/views/dynamicManagement/reviewAppeal.vue
  30. 26 9
      src/views/dynamicManagement/userDynamic.vue
  31. 2 2
      src/views/groupPackageManagement/index.vue
  32. 16 0
      src/views/groupPackageManagement/newGroup.vue
  33. 45 11
      src/views/licenseManagement/businessLicense.vue
  34. 94 34
      src/views/licenseManagement/contractManagement.vue
  35. 34 7
      src/views/licenseManagement/entertainmentLicense.vue
  36. 45 11
      src/views/licenseManagement/foodBusinessLicense.vue
  37. 2 0
      src/views/login/components/LoginForm.vue
  38. 2 0
      src/views/login/index.vue
  39. 6 6
      src/views/operationManagement/newActivity.vue
  40. 2 2
      src/views/performance/components/PerformanceFormDialog.vue
  41. 2 2
      src/views/performance/edit.vue
  42. 2 2
      src/views/priceList/edit.vue
  43. 3 3
      src/views/storeDecoration/add.vue
  44. 13 0
      src/views/storeDecoration/decorationChat.vue
  45. 3 3
      src/views/storeDecoration/facilitiesAndServices/components/FacilityManagement.vue
  46. 1 1
      src/views/storeDecoration/facilitiesAndServices/components/ServiceManagement.vue
  47. 1 1
      src/views/storeDecoration/menuManagement/index.vue
  48. 2 2
      src/views/storeDecoration/officialPhotoAlbum/index.vue
  49. 6 3
      src/views/storeDecoration/personnelConfig/index.vue
  50. 303 114
      src/views/storeDecoration/storeCoverMap/index.vue
  51. 1 1
      src/views/storeDecoration/storeEntranceMap/index.vue
  52. 1 1
      src/views/storeDecoration/wineMenuManagement/index.vue
  53. 33 60
      src/views/ticketManagement/couponDetail.vue
  54. 58 95
      src/views/ticketManagement/index.vue
  55. 149 133
      src/views/ticketManagement/newCoupon.vue

+ 47 - 0
src/api/helper/handleAuthExpired.ts

@@ -0,0 +1,47 @@
+import { ElMessage } from "element-plus";
+import router from "@/routers";
+import { LOGIN_URL } from "@/config";
+import { useUserStore } from "@/stores/modules/user";
+import { ResultEnum } from "@/enums/httpEnum";
+
+let dedupeAuthExpired = false;
+
+/** 登录成功后调用,避免踢下线去重标志影响新会话 */
+export function resetAuthExpiredDedupe() {
+  dedupeAuthExpired = false;
+}
+
+export function isAuthExpiredCode(code: unknown): boolean {
+  return code == ResultEnum.OVERDUE || code == ResultEnum.KICK_OUT;
+}
+
+/**
+ * 登录失效 / 账号在别处登录:清 token、跳转登录;仅首次执行副作用。
+ * 异地挤下线(666)使用整页跳转,避免仍停留在业务页;401 仍走路由 + 提示。
+ * 多个接口同时失败时其余请求静默 reject。
+ */
+export function applyAuthExpiredSideEffects(msg?: string, code?: unknown): void {
+  if (dedupeAuthExpired) return;
+  dedupeAuthExpired = true;
+  const userStore = useUserStore();
+  userStore.setToken("");
+
+  const tip = msg || "登录已失效";
+  const isKickOut = code == ResultEnum.KICK_OUT;
+
+  if (isKickOut) {
+    const href = router.resolve({ path: LOGIN_URL }).href;
+    window.location.replace(href);
+    window.setTimeout(() => {
+      dedupeAuthExpired = false;
+    }, 2000);
+    return;
+  }
+
+  ElMessage.error(tip);
+  void router.replace(LOGIN_URL).finally(() => {
+    window.setTimeout(() => {
+      dedupeAuthExpired = false;
+    }, 1000);
+  });
+}

+ 7 - 12
src/api/index.ts

@@ -1,6 +1,5 @@
 import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
 import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
-import { LOGIN_URL } from "@/config";
 import { ElMessage } from "element-plus";
 import { ResultData } from "@/api/interface";
 import { ResultEnum } from "@/enums/httpEnum";
@@ -11,6 +10,7 @@ import { localGet } from "@/utils";
 import router from "@/routers";
 import { cryptoUtil } from "@/utils/crypto";
 import { cryptoStrategy } from "@/utils/cryptoStrategy";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
   loading?: boolean;
@@ -77,7 +77,6 @@ class RequestHttp {
       (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
         const { data, config, headers } = response;
 
-        const userStore = useUserStore();
         axiosCanceler.removePending(config);
         config.loading && tryHideFullScreenLoading();
 
@@ -94,11 +93,9 @@ class RequestHttp {
         }
 
         const processedData = response.data;
-        // 登录失效
-        if (processedData.code == ResultEnum.OVERDUE) {
-          userStore.setToken("");
-          router.replace(LOGIN_URL);
-          ElMessage.error(processedData.msg);
+        // 登录失效 / 账号在别处登录
+        if (isAuthExpiredCode(processedData.code)) {
+          applyAuthExpiredSideEffects(processedData.msg, processedData.code);
           return Promise.reject(processedData);
         }
         // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
@@ -118,7 +115,7 @@ class RequestHttp {
         if (error.message.indexOf("timeout") !== -1) ElMessage.error("请求超时!请您稍后重试");
         if (error.message.indexOf("Network Error") !== -1) ElMessage.error("网络错误!请您稍后重试");
         // 根据服务器响应的错误状态码,做不同的处理
-        if (response.data.message) {
+        if (response?.data?.message) {
           ElMessage.error(response.data.message);
         } else {
           if (response) checkStatus(response.status);
@@ -185,10 +182,8 @@ class RequestHttp {
           try {
             const response = JSON.parse(xhr.responseText);
             // 统一处理响应,与 axios 拦截器保持一致
-            if (response.code == ResultEnum.OVERDUE) {
-              userStore.setToken("");
-              router.replace(LOGIN_URL);
-              ElMessage.error(response.msg);
+            if (isAuthExpiredCode(response.code)) {
+              applyAuthExpiredSideEffects(response.msg, response.code);
               reject(response);
               return;
             }

+ 4 - 7
src/api/indexApi.ts

@@ -1,6 +1,5 @@
 import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
 import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
-import { LOGIN_URL } from "@/config";
 import { ElMessage } from "element-plus";
 import { ResultData } from "@/api/interface";
 import { ResultEnum } from "@/enums/httpEnum";
@@ -10,6 +9,7 @@ import { useUserStore } from "@/stores/modules/user";
 import router from "@/routers";
 import { cryptoUtil } from "@/utils/crypto";
 import { cryptoStrategy } from "@/utils/cryptoStrategy";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
   loading?: boolean;
@@ -77,7 +77,6 @@ class RequestHttp {
     this.service.interceptors.response.use(
       (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
         const { data, config, headers } = response;
-        const userStore = useUserStore();
         axiosCanceler.removePending(config);
         config.loading && tryHideFullScreenLoading();
 
@@ -95,11 +94,9 @@ class RequestHttp {
 
         const processedData = response.data;
 
-        // 登录失效
-        if (processedData.code == ResultEnum.OVERDUE) {
-          userStore.setToken("");
-          router.replace(LOGIN_URL);
-          ElMessage.error(processedData.msg);
+        // 登录失效 / 账号在别处登录
+        if (isAuthExpiredCode(processedData.code)) {
+          applyAuthExpiredSideEffects(processedData.msg, processedData.code);
           return Promise.reject(processedData);
         }
         // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)

+ 4 - 6
src/api/indexLogin.ts

@@ -1,6 +1,5 @@
 import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
 import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
-import { LOGIN_URL } from "@/config";
 import { ElMessage } from "element-plus";
 import { ResultData } from "@/api/interface";
 import { ResultEnum } from "@/enums/httpEnum";
@@ -10,6 +9,7 @@ import { useUserStore } from "@/stores/modules/user";
 import router from "@/routers";
 import { cryptoUtil } from "@/utils/crypto";
 import { cryptoStrategy } from "@/utils/cryptoStrategy";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
   loading?: boolean;
@@ -94,11 +94,9 @@ class RequestHttp {
 
         const processedData = response.data;
 
-        // 登录失效
-        if (processedData.code == ResultEnum.OVERDUE) {
-          userStore.setToken("");
-          router.replace(LOGIN_URL);
-          ElMessage.error(processedData.msg);
+        // 登录失效 / 账号在别处登录
+        if (isAuthExpiredCode(processedData.code)) {
+          applyAuthExpiredSideEffects(processedData.msg, processedData.code);
           return Promise.reject(processedData);
         }
         // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)

+ 7 - 12
src/api/indexStore.ts

@@ -1,6 +1,5 @@
 import axios, { AxiosInstance, AxiosError, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from "axios";
 import { showFullScreenLoading, tryHideFullScreenLoading } from "@/components/Loading/fullScreen";
-import { LOGIN_URL } from "@/config";
 import { ElMessage } from "element-plus";
 import { ResultData } from "@/api/interface";
 import { ResultEnum } from "@/enums/httpEnum";
@@ -11,6 +10,7 @@ import { localGet } from "@/utils";
 import router from "@/routers";
 import { cryptoUtil } from "@/utils/crypto";
 import { cryptoStrategy } from "@/utils/cryptoStrategy";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
   loading?: boolean;
@@ -77,7 +77,6 @@ class RequestHttp {
       (response: AxiosResponse & { config: CustomAxiosRequestConfig }) => {
         const { data, config, headers } = response;
 
-        const userStore = useUserStore();
         axiosCanceler.removePending(config);
         config.loading && tryHideFullScreenLoading();
 
@@ -95,11 +94,9 @@ class RequestHttp {
 
         const processedData = response.data;
 
-        // 登录失效
-        if (processedData.code == ResultEnum.OVERDUE) {
-          userStore.setToken("");
-          router.replace(LOGIN_URL);
-          ElMessage.error(processedData.msg);
+        // 登录失效 / 账号在别处登录
+        if (isAuthExpiredCode(processedData.code)) {
+          applyAuthExpiredSideEffects(processedData.msg, processedData.code);
           return Promise.reject(processedData);
         }
         // 全局错误信息拦截(防止下载文件的时候返回数据流,没有 code 直接报错)
@@ -117,7 +114,7 @@ class RequestHttp {
         if (error.message.indexOf("timeout") !== -1) ElMessage.error("请求超时!请您稍后重试");
         if (error.message.indexOf("Network Error") !== -1) ElMessage.error("网络错误!请您稍后重试");
         // 根据服务器响应的错误状态码,做不同的处理
-        if (response.data.message) {
+        if (response?.data?.message) {
           ElMessage.error(response.data.message);
         } else {
           if (response) checkStatus(response.status);
@@ -184,10 +181,8 @@ class RequestHttp {
           try {
             const response = JSON.parse(xhr.responseText);
             // 统一处理响应,与 axios 拦截器保持一致
-            if (response.code == ResultEnum.OVERDUE) {
-              userStore.setToken("");
-              router.replace(LOGIN_URL);
-              ElMessage.error(response.msg);
+            if (isAuthExpiredCode(response.code)) {
+              applyAuthExpiredSideEffects(response.msg, response.code);
               reject(response);
               return;
             }

+ 5 - 0
src/api/modules/couponManagement.ts

@@ -16,6 +16,11 @@ export const getStoreAllCouponList = (params: {
   return httpApi.get<any>(`/alienStore/life-discount-coupon/getStoreAllCouponList`, params);
 };
 
+/** 删除优惠券 DELETE /alienStore/life-discount-coupon/deleteDiscountCoupon/{id} */
+export const deleteLifeDiscountCoupon = (id: string | number) => {
+  return httpApi.delete<any>(`/alienStore/life-discount-coupon/deleteDiscountCoupon/${id}`);
+};
+
 export const delCouponById = (params: { id: string }) => {
   return http.get(PORT_NONE + `/discountCouponPlatform/deleteDiscountCoupon`, params);
 };

+ 0 - 48
src/api/modules/friendRelation.ts

@@ -1,48 +0,0 @@
-import http from "@/api";
-
-/**
- * @name 好友关系管理模块
- */
-
-// 获取好友关系列表
-export const getFriendRelationList = (params: any) => {
-  return http.post(`/api/friendRelation/list`, params);
-};
-
-// 添加好友
-export const addFriendRelation = (params: {
-  friendName: string;
-  friendPhone: string;
-  remark?: string;
-  relationType: string | number;
-  couponList?: Array<{ couponId: string | number; quantity: number }>;
-}) => {
-  return http.post(`/api/friendRelation/add`, params);
-};
-
-// 编辑好友信息
-export const updateFriendRelation = (params: {
-  id: string | number;
-  friendName: string;
-  friendPhone: string;
-  remark?: string;
-  relationType: string | number;
-  couponList?: Array<{ couponId: string | number; quantity: number }>;
-}) => {
-  return http.post(`/api/friendRelation/update`, params);
-};
-
-// 删除好友
-export const deleteFriendRelation = (params: { id: string | number }) => {
-  return http.post(`/api/friendRelation/delete`, params);
-};
-
-// 同意好友申请
-export const approveFriend = (params: { id: string | number }) => {
-  return http.post(`/api/friendRelation/approve`, params);
-};
-
-// 拒绝好友申请
-export const rejectFriend = (params: { id: string | number; reason?: string }) => {
-  return http.post(`/api/friendRelation/reject`, params);
-};

+ 3 - 7
src/api/modules/homeEntry.ts

@@ -5,8 +5,7 @@ import http from "@/api";
 import { ResultEnum } from "@/enums/httpEnum";
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 const httpStore = axios.create({
   baseURL: import.meta.env.VITE_API_URL_STORE as string,
@@ -26,11 +25,8 @@ httpStore.interceptors.request.use(
 httpStore.interceptors.response.use(
   response => {
     const data = response.data;
-    const userStore = useUserStore();
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
+    if (isAuthExpiredCode(data.code)) {
+      applyAuthExpiredSideEffects(data.msg, data.code);
       return Promise.reject(data);
     }
     if (data.code && data.code !== ResultEnum.SUCCESS) {

+ 3 - 3
src/api/modules/newLoginApi.ts

@@ -138,7 +138,7 @@ export const setFriendCoupon = (params: any) => {
   return httpLogin.post(`/alienStore/life-discount-coupon-store-friend/setFriendCoupon`, params);
 };
 
-//好友关系管理列表
+// 好友赠券规则列表
 export const getRuleList = (params: any) => {
   return httpLogin.get(`/alienStore/life-discount-coupon-store-friend/getRuleList`, params);
 };
@@ -162,12 +162,12 @@ export const getRuleById = (params: any) => {
   return httpLogin.get(`/alienStore/life-discount-coupon-store-friend/getRuleById`, params);
 };
 
-/** 代金券列表(好友关系管理-折扣券用)与 group_merchant couponList 参数一致 */
+/** 代金券列表(好友赠券等场景)与 group_merchant couponList 参数一致 */
 export const getVoucherList = (params: { storeId: string; status?: number; size?: number; page?: number }) => {
   return httpLogin.get(`/alienStore/coupon/getCouponList`, params);
 };
 
-/** 满减券列表(好友关系管理-满减券用)与 group_merchant issueCouponList 参数一致 */
+/** 满减券列表(好友赠券等场景)与 group_merchant issueCouponList 参数一致 */
 export const getIssueCouponList = (params: {
   storeId: string;
   tab?: number;

+ 3 - 8
src/api/modules/operationManagement.ts

@@ -7,8 +7,7 @@ import axios from "axios";
 import { ResultEnum } from "@/enums/httpEnum";
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 // 创建专门用于 STORE API 的 axios 实例(单例)
 const storeAxiosInstance = axios.create({
@@ -35,13 +34,9 @@ storeAxiosInstance.interceptors.request.use(
 storeAxiosInstance.interceptors.response.use(
   (response: any) => {
     const { data } = response;
-    const userStore = useUserStore();
 
-    // 登录失效
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
+    if (isAuthExpiredCode(data.code)) {
+      applyAuthExpiredSideEffects(data.msg, data.code);
       return Promise.reject(data);
     }
     // 全局错误信息拦截

+ 3 - 7
src/api/modules/performance.ts

@@ -8,8 +8,7 @@ import { localGet } from "@/utils";
 import { ResultEnum } from "@/enums/httpEnum";
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 const performanceAxios = axios.create({
   baseURL: import.meta.env.VITE_API_URL_STORE as string,
@@ -31,11 +30,8 @@ performanceAxios.interceptors.request.use(
 performanceAxios.interceptors.response.use(
   response => {
     const data = response.data;
-    const userStore = useUserStore();
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
+    if (isAuthExpiredCode(data.code)) {
+      applyAuthExpiredSideEffects(data.msg, data.code);
       return Promise.reject(data);
     }
     if (data.code && data.code !== ResultEnum.SUCCESS) {

+ 3 - 9
src/api/modules/priceList.ts

@@ -4,8 +4,7 @@ import { localGet } from "@/utils";
 import { ResultEnum } from "@/enums/httpEnum";
 import { useUserStore } from "@/stores/modules/user";
 import { ElMessage } from "element-plus";
-import { LOGIN_URL } from "@/config";
-import router from "@/routers";
+import { applyAuthExpiredSideEffects, isAuthExpiredCode } from "@/api/helper/handleAuthExpired";
 
 // 创建专门用于价目表(STORE)接口的 axios 实例,前缀使用 alienStore
 const priceListAxios = axios.create({
@@ -30,13 +29,8 @@ priceListAxios.interceptors.request.use(
 priceListAxios.interceptors.response.use(
   response => {
     const data = response.data;
-    const userStore = useUserStore();
-
-    // 登录失效
-    if (data.code == ResultEnum.OVERDUE) {
-      userStore.setToken("");
-      router.replace(LOGIN_URL);
-      ElMessage.error(data.msg);
+    if (isAuthExpiredCode(data.code)) {
+      applyAuthExpiredSideEffects(data.msg, data.code);
       return Promise.reject(data);
     }
 

BIN
src/assets/images/headImg.png


+ 0 - 14
src/assets/json/authMenuList.json

@@ -769,20 +769,6 @@
           ]
         },
         {
-          "path": "/dynamicManagement/friendRelation",
-          "name": "friendRelation",
-          "component": "/dynamicManagement/friendRelation",
-          "meta": {
-            "icon": "User",
-            "title": "好友关系管理",
-            "isLink": "",
-            "isHide": false,
-            "isFull": false,
-            "isAffix": false,
-            "isKeepAlive": false
-          }
-        },
-        {
           "path": "/operationManagement/couponTemplate",
           "name": "operationManagementCouponTemplate",
           "component": "/operationManagement/couponTemplate",

+ 2 - 2
src/components/Upload/Img.vue

@@ -59,7 +59,7 @@ interface UploadFileProps {
   api?: (params: any) => Promise<any>; // 上传图片的 api 方法,一般项目上传都是同一个 api 方法,在组件里直接引入即可 ==> 非必传
   drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
   disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
-  fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
+  fileSize?: number; // 图片大小限制 ==> 非必传(默认为 20M)
   fileType?: File.ImageMimeType[]; // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
   height?: string; // 组件高度 ==> 非必传(默认为 150px)
   width?: string; // 组件宽度 ==> 非必传(默认为 150px)
@@ -73,7 +73,7 @@ const props = withDefaults(defineProps<UploadFileProps>(), {
   imageUrl: "",
   drag: true,
   disabled: false,
-  fileSize: 5,
+  fileSize: 20,
   fileType: () => ["image/jpeg", "image/png", "image/gif"],
   height: "150px",
   width: "150px",

+ 11 - 6
src/components/Upload/Imgs.vue

@@ -78,7 +78,9 @@ interface UploadFileProps {
   drag?: boolean; // 是否支持拖拽上传 ==> 非必传(默认为 true)
   disabled?: boolean; // 是否禁用上传组件 ==> 非必传(默认为 false)
   limit?: number; // 最大图片上传数 ==> 非必传(默认为 5张)
-  fileSize?: number; // 图片大小限制 ==> 非必传(默认为 5M)
+  fileSize?: number; // 图片大小限制(MB)==> 非必传(默认 20M)
+  /** 视频大小限制(MB);含 video/* 的 fileType 时生效;默认 200;仅发布动态/门店封面页在业务里单独放宽 */
+  videoFileSize?: number;
   fileType?: string[]; // 接受的 MIME(图片 + 可选视频)==> 非必传
   height?: string; // 组件高度 ==> 非必传(默认为 150px)
   width?: string; // 组件宽度 ==> 非必传(默认为 150px)
@@ -94,7 +96,8 @@ const props = withDefaults(defineProps<UploadFileProps>(), {
   drag: true,
   disabled: false,
   limit: 5,
-  fileSize: 5,
+  fileSize: 20,
+  videoFileSize: 200,
   fileType: () => ["image/jpeg", "image/png", "image/gif", "video/mp4", "video/webm", "video/quicktime", "video/ogg"],
   height: "150px",
   width: "150px",
@@ -197,7 +200,6 @@ const isVideoFile = (file: UploadFile) => {
  * @param rawFile 选择的文件
  * */
 const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
-  const imgSize = rawFile.size / 1024 / 1024 < props.fileSize;
   const acceptVideo = props.fileType.some(t => String(t).startsWith("video/"));
   const byNameVideo = /\.(mp4|m4v|webm|ogg|mov)(\?.*)?$/i.test(rawFile.name || "");
   const mimeVideo = typeof rawFile.type === "string" && rawFile.type.startsWith("video/");
@@ -205,21 +207,24 @@ const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
     acceptVideo && byNameVideo && (!rawFile.type || rawFile.type === "application/octet-stream" || mimeVideo);
   const typeListed = props.fileType.includes(rawFile.type);
   const okType = typeListed || looseVideoMime;
+  const isVideo = mimeVideo || looseVideoMime;
+  const limitMb = isVideo && acceptVideo && props.videoFileSize > 0 ? props.videoFileSize : props.fileSize;
+  const okSize = rawFile.size / 1024 / 1024 < limitMb;
   if (!okType)
     ElNotification({
       title: "温馨提示",
       message: "上传文件不符合所需的格式!",
       type: "warning"
     });
-  if (!imgSize)
+  if (!okSize)
     setTimeout(() => {
       ElNotification({
         title: "温馨提示",
-        message: `上传文件大小不能超过 ${props.fileSize}M!`,
+        message: `上传文件大小不能超过 ${limitMb}M!`,
         type: "warning"
       });
     }, 0);
-  return okType && imgSize;
+  return okType && okSize;
 };
 
 /**

+ 2 - 0
src/enums/httpEnum.ts

@@ -5,6 +5,8 @@ export enum ResultEnum {
   SUCCESS = 200,
   ERROR = 500,
   OVERDUE = 401,
+  /** 账号在别处登录等业务踢下线 */
+  KICK_OUT = 666,
   TIMEOUT = 300000,
   TYPE = "success"
 }

+ 7 - 2
src/utils/errorHandler.ts

@@ -5,7 +5,11 @@ import { ElNotification } from "element-plus";
  * */
 const errorHandler = (error: any) => {
   // 过滤 HTTP 请求错误
-  if (error.status || error.status == 0) return false;
+  if (error?.status || error?.status === 0) return false;
+  // 业务接口 reject 的响应体(无 stack),不在此重复弹「未知错误」
+  if (error && typeof error === "object" && !(error instanceof Error) && ("msg" in error || "code" in error)) {
+    return false;
+  }
   let errorMap: { [key: string]: string } = {
     InternalError: "Javascript引擎内部错误",
     ReferenceError: "未找到对象",
@@ -16,9 +20,10 @@ const errorHandler = (error: any) => {
     URIError: "URI错误"
   };
   let errorName = errorMap[error.name] || "未知错误";
+  const message = error instanceof Error ? error.message : String(error ?? "");
   ElNotification({
     title: errorName,
-    message: error,
+    message,
     type: "error",
     duration: 3000
   });

+ 18 - 0
src/views/appoinmentManagement/classifyManagement.vue

@@ -55,6 +55,7 @@
               v-model:file-list="form.planeImageFileList"
               list-type="picture-card"
               :limit="9"
+              :before-upload="beforePlaneImageUpload"
               :http-request="handlePlaneImageUpload"
               :on-remove="onPlaneImageRemove"
               :on-preview="onPlaneImagePreview"
@@ -122,6 +123,23 @@ import {
 import { uploadFileToOss } from "@/api/upload.js";
 import { localGet } from "@/utils";
 
+/** 平面图单张上限(与商户端 / 动态发布一致) */
+const PLANE_IMAGE_MAX_MB = 20;
+const UPLOAD_TIP_IMAGE = "图片建议不超过 20MB";
+
+const beforePlaneImageUpload = (rawFile: File) => {
+  const mime = String(rawFile?.type || "");
+  if (!mime.startsWith("image/")) {
+    ElMessage.warning("只能上传图片格式");
+    return false;
+  }
+  if (rawFile.size > PLANE_IMAGE_MAX_MB * 1024 * 1024) {
+    ElMessage.warning(UPLOAD_TIP_IMAGE);
+    return false;
+  }
+  return true;
+};
+
 export interface CategoryRow {
   id: number | string;
   _seq?: number;

+ 1 - 1
src/views/businessInfo/manageInfo.vue

@@ -757,7 +757,7 @@ const rules: FormRules = {
   ]
 };
 
-const MAX_IMG_MB = 5;
+const MAX_IMG_MB = 20;
 
 function beforeImageUpload(file: File) {
   const name = file.name?.toLowerCase() || "";

+ 4 - 4
src/views/businessInfo/subjectInfo.vue

@@ -62,7 +62,7 @@
               1.
               请上传彩色照片或彩色扫描件或加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。
             </p>
-            <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 5M。</p>
+            <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 20M。</p>
           </div>
         </el-form-item>
         <div v-if="hasBusinessLicenseOcr" class="license-ocr-result">
@@ -135,7 +135,7 @@
                 1. 请上传彩色照片 or 彩色扫描件 or
                 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。
               </p>
-              <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 5M。</p>
+              <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 20M。</p>
             </div>
           </el-form-item>
 
@@ -172,7 +172,7 @@
                 1. 请上传彩色照片 or 彩色扫描件 or
                 加盖公章鲜章的复印件,要求正面拍摄,露出证件四角且清晰、完整,所有字符清晰可识别,不得反光或遮挡。不得翻拍、截图、镜像、PS。
               </p>
-              <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 5M。</p>
+              <p>2. 图片只支持 JPG、BMP、PNG 格式,文件大小不能超过 20M。</p>
             </div>
 
             <div v-if="showIdPortraitOcrBlock" class="ocr-result-container">
@@ -622,7 +622,7 @@ watch(
   }
 );
 
-const MAX_LICENSE_MB = 5;
+const MAX_LICENSE_MB = 20;
 
 function beforeLicenseUpload(file: File) {
   const name = file.name?.toLowerCase() || "";

+ 5 - 5
src/views/businessInfo/zfbIndex.vue

@@ -1097,8 +1097,8 @@ function onSubjectTypeChange(val: string | number | boolean | undefined) {
   nextTick(() => formRef.value?.clearValidate());
 }
 
-/** 支付宝图片上传接口:原始二进制最大 10MB(与接口文档一致) */
-const MAX_ALIPAY_IMAGE_BYTES = 10 * 1024 * 1024;
+/** 商户端图片上传统一上限 20MB(若支付宝接口仍限 10MB,超大文件可能在服务端被拒) */
+const MAX_ALIPAY_IMAGE_BYTES = 20 * 1024 * 1024;
 
 /** 事业单位材料:仅 jpg / png(与页面说明一致) */
 function beforeImageUploadInstJpgPng(file: File) {
@@ -1110,7 +1110,7 @@ function beforeImageUploadInstJpgPng(file: File) {
     return false;
   }
   if (file.size > MAX_ALIPAY_IMAGE_BYTES) {
-    ElMessage.error("文件大小不能超过 10M");
+    ElMessage.error("文件大小不能超过 20M");
     return false;
   }
   return true;
@@ -1125,7 +1125,7 @@ function beforeImageUpload(file: File) {
     return false;
   }
   if (file.size > MAX_ALIPAY_IMAGE_BYTES) {
-    ElMessage.error("文件大小不能超过 10M");
+    ElMessage.error("文件大小不能超过 20M");
     return false;
   }
   return true;
@@ -1554,7 +1554,7 @@ async function handleSingleUpload(options: UploadRequestOptions, kind: UploadKin
   try {
     if (file.size > MAX_ALIPAY_IMAGE_BYTES) {
       uploadFileItem.status = "fail";
-      ElMessage.error("文件大小不能超过 10M");
+      ElMessage.error("文件大小不能超过 20M");
       return;
     }
     const imageType = resolveAlipayImageType(file);

+ 50 - 21
src/views/dynamicManagement/friendCoupon.vue

@@ -187,7 +187,7 @@ const giftRules = reactive<FormRules>({
   quantity: [{ required: true, message: "请输入赠送数量", trigger: "blur" }]
 });
 
-// 好友赠我表格列配置(与截图一致:店铺名称、类型、数量、结束时间、操作)
+// 好友赠我表格列配置
 const friendMessageColumns = reactive<ColumnProps<any>[]>([
   {
     prop: "storeName",
@@ -215,14 +215,27 @@ const friendMessageColumns = reactive<ColumnProps<any>[]>([
     }
   },
   {
+    prop: "couponName",
+    label: "优惠券名称",
+    render: (scope: any) => {
+      const r = scope.row;
+      return r.couponName ?? r.name ?? r.couponTitle ?? "--";
+    }
+  },
+  {
     prop: "couponNum",
     label: "数量"
   },
   {
-    prop: "endDate",
-    label: "结束时间",
+    prop: "longTermValid",
+    label: "有效期",
     render: (scope: any) => {
-      return scope.row.endDate?.replace(/-/g, "/") || "--";
+      const r = scope.row;
+      const lt = r.longTermValid;
+      if (lt === 1 || lt === "1" || lt === true) return "长期有效";
+      const ed = r.expirationDate;
+      if (ed === null || ed === undefined || ed === "") return "--";
+      return `${ed}天`;
     }
   },
   { prop: "operation", label: "操作", fixed: "right", width: 200 }
@@ -256,14 +269,27 @@ const myGiftColumns = reactive<ColumnProps<any>[]>([
     }
   },
   {
+    prop: "couponName",
+    label: "优惠券名称",
+    render: (scope: any) => {
+      const r = scope.row;
+      return r.couponName ?? r.name ?? r.couponTitle ?? "--";
+    }
+  },
+  {
     prop: "couponNum",
     label: "数量"
   },
   {
-    prop: "endDate",
-    label: "结束时间",
+    prop: "longTermValid",
+    label: "有效期",
     render: (scope: any) => {
-      return scope.row.endDate?.replace(/-/g, "/") || "--";
+      const r = scope.row;
+      const lt = r.longTermValid;
+      if (lt === 1 || lt === "1" || lt === true) return "长期有效";
+      const ed = r.expirationDate;
+      if (ed === null || ed === undefined || ed === "") return "--";
+      return `${ed}天`;
     }
   },
   { prop: "operation", label: "操作", fixed: "right", width: 200 }
@@ -377,24 +403,27 @@ const handleGiftSubmit = async () => {
   });
 };
 
-// 查看详情
+// 查看详情:整行写入 sessionStorage,详情页直接展示,不调接口
+const FRIEND_COUPON_DETAIL_ROW_KEY = "friendCoupon_detail_row";
+
 const viewDetail = (row: any) => {
-  let query = {};
-  if (row.voucherId) {
-    query = {
-      voucherId: row.voucherId,
-      type: activeName.value
-    };
-  } else {
-    query = {
-      couponId: row.couponId,
-      type: activeName.value,
-      storeId: row.storeId != null && row.storeId !== "" ? row.storeId : localGet("createdId")
-    };
+  try {
+    sessionStorage.setItem(FRIEND_COUPON_DETAIL_ROW_KEY, JSON.stringify(row ?? {}));
+  } catch (e) {
+    console.error(e);
+    ElMessage.warning("无法缓存行数据,请重试");
+    return;
+  }
+  const query: Record<string, string> = { type: String(activeName.value || "") };
+  if (row?.voucherId != null && row.voucherId !== "") {
+    query.voucherId = String(row.voucherId);
+  } else if (row?.couponId != null && row.couponId !== "") {
+    query.couponId = String(row.couponId);
   }
+  if (row?.storeId != null && row.storeId !== "") query.storeId = String(row.storeId);
   router.push({
     path: "/dynamicManagement/friendCouponDetail",
-    query: query
+    query
   });
 };
 

+ 39 - 181
src/views/dynamicManagement/friendCouponDetail.vue

@@ -1,5 +1,5 @@
 <template>
-  <!-- 好友优惠券 - 详情页面 -->
+  <!-- 好友优惠券 - 详情页面(数据由列表页 sessionStorage 传入,不调详情接口) -->
   <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
     <div class="header">
       <el-button @click="goBack"> 返回 </el-button>
@@ -8,7 +8,6 @@
     <div class="content">
       <!-- 左侧内容区域 -->
       <div class="contentLeft">
-        <!-- 基础信息模块(与截图一致:店铺名称、类型、优惠券名称、面值、数量、结束时间、有效期) -->
         <div class="model">
           <h3 style="font-weight: bold">基础信息:</h3>
           <!-- 店铺名称 -->
@@ -41,38 +40,25 @@
           </div>
           <div class="detail-item" v-else>
             <div class="detail-label">面值</div>
-            <div class="detail-value" v-if="couponId">
-              {{ formatCurrency(couponModel.nominalValue, 2, "¥") }}
-            </div>
-            <div class="detail-value" v-else>
+            <div class="detail-value">
               {{ formatCurrency(couponModel.nominalValue ?? couponModel.price, 2, "¥") }}
             </div>
           </div>
-          <!-- 数量:优惠券详情接口返回持有数量 ownedQuantity -->
+          <!-- 数量 -->
           <div class="detail-item">
             <div class="detail-label">数量</div>
             <div class="detail-value">
               {{ couponModel.ownedQuantity ?? couponModel.couponNum ?? couponModel.singleQty ?? "--" }}
             </div>
           </div>
-          <!-- 结束时间 -->
-          <div class="detail-item">
-            <div class="detail-label">结束时间</div>
-            <div class="detail-value" v-if="couponId">
-              {{ couponModel.endGetDate || "--" }}
-            </div>
-            <div class="detail-value" v-else>
-              {{ couponModel.endDate || "--" }}
-            </div>
-          </div>
-          <!-- 有效期:接口字段 expirationDate -->
+          <!-- 有效期:longTermValid=1 长期有效;否则展示 expirationDate(天) -->
           <div class="detail-item">
             <div class="detail-label">有效期</div>
             <div class="detail-value">
               {{ getValidityDisplay() }}
             </div>
           </div>
-          <!-- 最低消费金额:绑定接口字段 minimumSpendingAmount -->
+          <!-- 最低消费金额 -->
           <div class="detail-item">
             <div class="detail-label">最低消费金额</div>
             <div class="detail-value">
@@ -87,203 +73,97 @@
 
 <script setup lang="tsx" name="friendCouponDetail">
 /**
- * 好友优惠券 - 详情页面
- * 功能:显示好友优惠券的详细信息
+ * 好友优惠券 - 详情页:数据来自列表行(sessionStorage),不请求详情接口
  */
 import { ref, onMounted } from "vue";
-import { useRouter, useRoute } from "vue-router";
+import { useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
-import { getFriendCouponDetail, getCouponDetail } from "@/api/modules/newLoginApi";
 import { formatCurrency } from "@/utils/formatCurrency";
-import { localGet } from "@/utils";
 
-// ==================== 响应式数据定义 ====================
+const FRIEND_COUPON_DETAIL_ROW_KEY = "friendCoupon_detail_row";
 
-// 路由相关
 const router = useRouter();
-const route = useRoute();
 
-// 页面ID参数
-const couponId = ref<string>("");
-const voucherId = ref<string>("");
-/** 详情接口 getCouponDetailWithOwnedQty 所需店铺 ID(路由 storeId,缺省时为当前登录店铺) */
-const detailStoreId = ref<string>("");
-
-// 优惠券类型(好友赠我 friendMessage / 我赠好友 myGift)
-const type = ref<string>("");
-
-// ==================== 优惠券信息数据模型 ====================
 const couponModel = ref<any>({
-  // 账户名称(赠送人/接收人)
   acName: "",
-  // 优惠券名称
   couponName: "",
-  // 优惠券数量
   couponNum: 0,
-  // 删除标志
   deleteFlag: 0,
-  // 结束日期
   endDate: "",
-  // 有效期(接口)
   expirationDate: "",
-  // ID
+  longTermValid: undefined as number | undefined,
   id: 0,
-  // 图片URL
   imgUrl: "",
-  // 详细列表
   lifeDiscountCouponFriendRuleDetailVos: [],
-  // 最低消费金额(接口字段 minimumSpendingAmount;不设默认数字,未加载时由 formatCurrency 显示为 --)
   minimumSpendingAmount: undefined,
-  // 金额上限
   moneyHigh: 0,
-  // 金额下限
   moneyLow: 0,
-  // 面值
   nominalValue: 0,
-  // 折扣率(折扣券 couponType=2,接口 discountRate)
   discountRate: undefined,
-  // 状态
   status: "",
-  // 店铺ID
   storeId: 0,
-  // 店铺名称
   storeName: ""
 });
 
-// ==================== 生命周期钩子 ====================
-
-/**
- * 组件挂载时初始化
- * 从路由参数中获取couponId并加载详情数据
- */
-onMounted(async () => {
-  if (route.query.voucherId) {
-    voucherId.value = (route.query.voucherId as string) || "";
-  } else if (route.query.couponId) {
-    couponId.value = (route.query.couponId as string) || "";
-  }
-  type.value = (route.query.type as string) || "";
-  detailStoreId.value =
-    (route.query.storeId != null && String(route.query.storeId) !== ""
-      ? String(route.query.storeId)
-      : String(localGet("createdId") ?? "")) || "";
-
-  if (voucherId.value) {
-    await loadDetailData();
-  } else if (couponId.value) {
-    if (!detailStoreId.value) {
-      ElMessage.warning("缺少店铺ID参数");
-      return;
-    }
-    await loadDetailData();
-  } else {
-    ElMessage.warning("缺少优惠券ID参数");
-  }
-});
-
-// ==================== 事件处理函数 ====================
-
-/**
- * 返回上一页
- */
 const goBack = () => {
   router.go(-1);
 };
 
-// ==================== 数据加载函数 ====================
-
-/**
- * 加载详情数据
- */
-const loadDetailData = async () => {
-  try {
-    if (voucherId.value) {
-      const res: any = await getCouponDetail({
-        id: voucherId.value
-      });
-      if (res.code === 200) {
-        couponModel.value = res.data;
-        console.log(couponModel.value, "couponModel.value");
-      } else {
-        ElMessage.error(res.msg);
-      }
-    } else if (couponId.value) {
-      const res: any = await getFriendCouponDetail({
-        couponId: couponId.value,
-        storeId: detailStoreId.value
-      });
+onMounted(() => {
+  const raw = sessionStorage.getItem(FRIEND_COUPON_DETAIL_ROW_KEY);
+  sessionStorage.removeItem(FRIEND_COUPON_DETAIL_ROW_KEY);
 
-      if (res.code === 200) {
-        couponModel.value = res.data;
-      } else {
-        ElMessage.error(res.msg);
-      }
-    }
-  } catch (error) {
-    ElMessage.error("加载详情数据出错");
+  if (!raw) {
+    ElMessage.warning("缺少列表数据,请从列表重新进入");
+    router.go(-1);
+    return;
   }
-};
 
-/** 有效期是否有值(0 / "0" 视为有效,仅 null、undefined、"" 视为缺失) */
-const hasValidityValue = (v: unknown) => v !== null && v !== undefined && v !== "";
+  try {
+    const row = JSON.parse(raw);
+    couponModel.value = { ...couponModel.value, ...row };
+  } catch {
+    ElMessage.error("数据解析失败");
+    router.go(-1);
+  }
+});
 
-/**
- * 有效期展示:优先 expirationDate,且为 0 时显示 0
- */
 const getValidityDisplay = () => {
   const m = couponModel.value;
-  if (hasValidityValue(m.expirationDate)) return m.expirationDate;
-  if (hasValidityValue(m.validityPeriod)) return m.validityPeriod;
-  if (hasValidityValue(m.endDate)) return m.endDate;
-  return "--";
+  const lt = m.longTermValid;
+  if (lt === 1 || lt === "1" || lt === true) return "长期有效";
+  const ed = m.expirationDate;
+  if (ed === null || ed === undefined || ed === "") return "--";
+  const s = String(ed).trim();
+  if (/^\d{4}-\d{2}-\d{2}/.test(s) || s.includes("T")) {
+    return s.includes("-") ? (s.replace(/-/g, "/").split(" ")[0] ?? s) : s;
+  }
+  return Number.isNaN(Number(s)) ? s : `${s}天`;
 };
 
-/**
- * 折扣率展示:绑定 discountRate;接口常见为整数 11–100 表示几点几折(与 newCoupon 说明一致),否则原样加「折」
- */
 const formatDiscountRateDisplay = (rate: unknown) => {
   if (rate === null || rate === undefined || rate === "") return "--";
   const n = Number(rate);
   if (isNaN(n)) return `${rate}折`;
-  if (n > 10 && n <= 100) {
-    const x = n / 10;
-    const s = Number.isInteger(x) ? String(x) : String(Number(x.toFixed(1)));
-    return `${s.replace(/\.0$/, "")}折`;
-  }
-  return `${n}折`;
+  // 与 newCoupon 一致:接口多为 1–100(如 85 表示 8.5 折),展示时除以 10
+  const x = n / 10;
+  const s = Number.isInteger(x) ? String(x) : String(Number(x.toFixed(1)));
+  return `${s.replace(/\.0$/, "")}折`;
 };
 
-/**
- * 获取优惠券类型文案
- */
-const getCouponTypeText = (type: number | string | undefined) => {
+const getCouponTypeText = (t: number | string | undefined) => {
   const typeMap: Record<string, string> = { "1": "满减券", "2": "折扣券" };
-  return type != null && type !== "" ? (typeMap[String(type)] ?? String(type)) : "--";
-};
-
-/**
- * 获取状态文本
- */
-const getStatusText = () => {
-  const statusMap: Record<string, string> = {
-    "0": "未使用",
-    "1": "已使用",
-    "2": "已过期"
-  };
-  return statusMap[couponModel.value.status] || "--";
+  return t != null && t !== "" ? (typeMap[String(t)] ?? String(t)) : "--";
 };
 </script>
 
 <style scoped lang="scss">
-/* 页面容器 */
 .table-box {
   display: flex;
   flex-direction: column;
   height: auto !important;
   min-height: 100%;
 }
-
-/* 头部区域 */
 .header {
   display: flex;
   align-items: center;
@@ -300,8 +180,6 @@ const getStatusText = () => {
   color: #303133;
   text-align: center;
 }
-
-/* 内容区域布局 */
 .content {
   display: flex;
   flex: 1;
@@ -309,21 +187,15 @@ const getStatusText = () => {
   width: 98%;
   padding: 0 12px;
   margin: 24px auto;
-
-  /* 左侧内容区域 */
   .contentLeft {
     width: 50%;
     padding-right: 12px;
   }
-
-  /* 右侧内容区域 */
   .contentRight {
     width: 50%;
     padding-left: 12px;
   }
 }
-
-/* 模块容器 */
 .model {
   margin-bottom: 50px;
   h3 {
@@ -333,8 +205,6 @@ const getStatusText = () => {
     color: #303133;
   }
 }
-
-/* 详情项样式 */
 .detail-item {
   display: flex;
   align-items: flex-start;
@@ -356,16 +226,4 @@ const getStatusText = () => {
   color: #303133;
   word-break: break-word;
 }
-.empty-text {
-  color: #909399;
-}
-
-/* 详情卡片样式 */
-.detail-card {
-  padding: 16px;
-  margin-bottom: 16px;
-  background-color: #f5f7fa;
-  border: 1px solid #e4e7ed;
-  border-radius: 4px;
-}
 </style>

+ 0 - 683
src/views/dynamicManagement/friendRelation.vue

@@ -1,683 +0,0 @@
-<template>
-  <div class="table-box button-table friend-relation-container">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
-      <!-- 表格 header 按钮 -->
-      <template #tableHeader="scope">
-        <div class="header-button">
-          <el-button type="primary" @click="openAddDialog"> 添加活动 </el-button>
-        </div>
-      </template>
-
-      <!-- 状态列 -->
-      <template #status="scope">
-        <el-tag :type="getStatusType(scope.row.status)">
-          {{ getStatusText(scope.row.status) }}
-        </el-tag>
-      </template>
-
-      <!-- 表格操作 -->
-      <template #operation="scope">
-        <el-button link type="primary" @click="editRow(scope.row)"> 编辑 </el-button>
-        <el-button link type="primary" @click="deleteRow(scope.row)"> 删除 </el-button>
-      </template>
-    </ProTable>
-
-    <!-- 添加/编辑活动对话框 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" @close="closeDialog">
-      <el-form ref="formRef" :model="formData" :rules="formRules" label-width="140px">
-        <el-form-item label="活动名称" prop="acName">
-          <el-input v-model="formData.acName" placeholder="请输入" clearable />
-        </el-form-item>
-
-        <el-form-item label="赠送类型">
-          <el-radio-group v-model="formData.distributeType" @change="onDistributeTypeChange">
-            <el-radio :label="1"> 满减券 </el-radio>
-            <el-radio :label="2"> 折扣券 </el-radio>
-          </el-radio-group>
-        </el-form-item>
-
-        <el-form-item label="消费门槛金额(¥)">
-          <div style="display: flex; gap: 10px; align-items: center; width: 100%">
-            <el-form-item prop="moneyLow" style="flex: 1; margin-bottom: 0">
-              <el-input
-                v-model="formData.moneyLow"
-                type="number"
-                :min="0"
-                step="0.01"
-                placeholder="0.00"
-                clearable
-                @input="handleMoneyInput('moneyLow')"
-              >
-                <template #prefix> ¥ </template>
-              </el-input>
-            </el-form-item>
-            <span>~</span>
-            <el-form-item prop="moneyHigh" style="flex: 1; margin-bottom: 0">
-              <el-input
-                v-model="formData.moneyHigh"
-                type="number"
-                :min="0"
-                step="0.01"
-                placeholder="0.00"
-                clearable
-                @input="handleMoneyInput('moneyHigh')"
-              >
-                <template #prefix> ¥ </template>
-              </el-input>
-            </el-form-item>
-          </div>
-        </el-form-item>
-
-        <el-form-item label="来源商家及优惠券">
-          <div class="merchant-coupon-list">
-            <div v-for="(item, index) in formData.coupons" :key="index" class="merchant-coupon-item">
-              <el-select
-                v-model="item.merchant"
-                placeholder="选择商家"
-                style="width: 200px; margin-right: 10px"
-                filterable
-                @change="handleMerchantChange(index)"
-              >
-                <el-option
-                  v-for="(m, idx) in effectiveMerchantOptions"
-                  :key="m.label ? `${String(m.value)}_${m.label}_${idx}` : String(m.value)"
-                  :label="m.label"
-                  :value="m.value"
-                />
-              </el-select>
-              <el-select
-                v-model="item.coupon"
-                :placeholder="formData.distributeType === 2 ? '选择折扣券' : '选择满减券'"
-                style="width: 200px; margin-right: 10px"
-                filterable
-                @change="handleCouponChange(index)"
-              >
-                <el-option
-                  v-for="c in getCouponOptions(formData.distributeType)"
-                  :key="c.value"
-                  :label="c.label"
-                  :value="c.value"
-                />
-              </el-select>
-              <span v-if="item.coupon" class="remaining-text">剩余{{ item.remaining }}张</span>
-              <el-button type="danger" link @click="removeMerchantCoupon(index)" v-if="formData.coupons.length > 1">
-                删除
-              </el-button>
-            </div>
-            <el-button type="primary" link @click="addMerchantCoupon" style="margin-top: 10px">
-              <el-icon><Plus /></el-icon>
-              添加商家优惠券
-            </el-button>
-          </div>
-        </el-form-item>
-      </el-form>
-
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="closeDialog"> 取消 </el-button>
-          <el-button type="primary" @click="handleSubmit"> 确定 </el-button>
-        </div>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup lang="tsx" name="friendRelation">
-import { computed, onMounted, reactive, ref } from "vue";
-import type { FormInstance, FormRules } from "element-plus";
-import { ElMessage, ElMessageBox } from "element-plus";
-import { Plus } from "@element-plus/icons-vue";
-import ProTable from "@/components/ProTable/index.vue";
-import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
-import { localGet } from "@/utils";
-
-import {
-  getRuleList,
-  saveFriendCouponRule,
-  getMutualAttention,
-  delFriendCouponRule,
-  getRuleById,
-  getVoucherList,
-  getIssueCouponList
-} from "@/api/modules/newLoginApi";
-
-// ProTable 实例
-const proTable = ref<ProTableInstance>();
-
-// 对话框相关
-const dialogVisible = ref(false);
-const dialogTitle = computed(() => (isEdit.value ? "编辑活动" : "添加活动"));
-const isEdit = ref(false);
-const currentEditId = ref("");
-
-// 表单引用
-const formRef = ref<FormInstance>();
-
-// 商家列表(互相关注接口,仅显示 phoneId 带 store 的商家)
-const merchantList = ref<Array<{ value: string | number; label: string; raw?: any }>>([]);
-const merchantListRaw = ref<any[]>([]);
-
-// 优惠券/折扣券选项缓存 key: '1-couponList' | '2-couponList'
-const couponOptionsMap = ref<Record<string, Array<{ value: string; label: string }>>>({});
-const couponDataMap = ref<Record<string, any[]>>({});
-
-const normalizeCouponValue = (v: any) => (v != null && v !== "" ? String(v) : "");
-
-// 编辑时合并详情中的 storeName,使商家下拉能正确显示;按 label 去重,避免同一商家出现多次
-const effectiveMerchantOptions = computed(() => {
-  const base = merchantList.value || [];
-  const fromDetail = (formData.coupons || [])
-    .filter((c: any) => c.merchant != null && c.merchant !== "" && c.storeName)
-    .map((c: any) => ({ value: c.merchant, label: c.storeName }));
-  const seenValue = new Set(base.map((o: any) => o.value));
-  const extra = fromDetail.filter((o: any) => {
-    if (seenValue.has(o.value)) return false;
-    seenValue.add(o.value);
-    return true;
-  });
-  const merged = [...base, ...extra];
-  // 按 label(店名)去重:同一店名只保留一项,优先保留在 form 中已选中的 value,避免下拉重复显示
-  const usedValues = new Set((formData.coupons || []).map((c: any) => c.merchant).filter((v: any) => v != null && v !== ""));
-  const byLabel = new Map<string, { value: string | number; label: string }>();
-  for (const o of merged) {
-    const label = o.label || "";
-    const existing = byLabel.get(label);
-    if (!existing) {
-      byLabel.set(label, o);
-    } else if (usedValues.has(o.value) && !usedValues.has(existing.value)) {
-      byLabel.set(label, o);
-    }
-  }
-  return Array.from(byLabel.values());
-});
-
-const getCouponOptions = (distributeType: number) => {
-  const key = distributeType === 2 ? "2-couponList" : "1-couponList";
-  return couponOptionsMap.value[key] || [];
-};
-
-// 表单数据
-const formData = reactive({
-  acName: "",
-  distributeType: 1 as 1 | 2, // 1-满减券 2-折扣券
-  moneyLow: "" as string | number,
-  moneyHigh: "" as string | number,
-  coupons: [] as Array<{
-    merchant: string | number | null;
-    coupon: string | null;
-    remaining: number;
-    storeName?: string;
-  }>
-});
-
-// 金额验证器
-const validateMoneyLow = (rule: any, value: any, callback: any) => {
-  if (value === "" || value === null || value === undefined) {
-    callback(new Error("请输入最低消费金额"));
-    return;
-  }
-
-  const numValue = Number(value);
-  if (isNaN(numValue)) {
-    callback(new Error("请输入有效的金额"));
-    return;
-  }
-
-  if (numValue < 0) {
-    callback(new Error("金额不能为负数"));
-    return;
-  }
-
-  // 验证小数位数不超过2位
-  const decimalPart = String(value).split(".")[1];
-  if (decimalPart && decimalPart.length > 2) {
-    callback(new Error("最多支持2位小数"));
-    return;
-  }
-
-  // 如果最高金额已填写,验证最低金额不能大于最高金额
-  if (formData.moneyHigh !== "" && formData.moneyHigh !== null && formData.moneyHigh !== undefined) {
-    const highValue = Number(formData.moneyHigh);
-    if (!isNaN(highValue) && numValue > highValue) {
-      callback(new Error("最低金额不能大于最高金额"));
-      return;
-    }
-  }
-
-  callback();
-};
-
-const validateMoneyHigh = (rule: any, value: any, callback: any) => {
-  if (value === "" || value === null || value === undefined) {
-    callback(new Error("请输入最高消费金额"));
-    return;
-  }
-
-  const numValue = Number(value);
-  if (isNaN(numValue)) {
-    callback(new Error("请输入有效的金额"));
-    return;
-  }
-
-  if (numValue < 0) {
-    callback(new Error("金额不能为负数"));
-    return;
-  }
-
-  // 验证小数位数不超过2位
-  const decimalPart = String(value).split(".")[1];
-  if (decimalPart && decimalPart.length > 2) {
-    callback(new Error("最多支持2位小数"));
-    return;
-  }
-
-  // 如果最低金额已填写,验证最高金额不能小于最低金额
-  if (formData.moneyLow !== "" && formData.moneyLow !== null && formData.moneyLow !== undefined) {
-    const lowValue = Number(formData.moneyLow);
-    if (!isNaN(lowValue) && numValue < lowValue) {
-      callback(new Error("最高金额不能小于最低金额"));
-      return;
-    }
-  }
-
-  callback();
-};
-
-// 表单验证规则
-const formRules = reactive<FormRules>({
-  acName: [{ required: true, message: "请输入活动名称", trigger: "blur" }],
-  moneyLow: [{ required: true, validator: validateMoneyLow, trigger: "blur" }],
-  moneyHigh: [{ required: true, validator: validateMoneyHigh, trigger: "blur" }]
-});
-
-// 表格列配置
-const columns = reactive<ColumnProps<any>[]>([
-  {
-    prop: "acName",
-    label: "活动名称",
-    search: { el: "input", props: { placeholder: "请输入活动名称" } }
-  },
-  {
-    prop: "endDate",
-    label: "有效期至"
-  },
-  {
-    prop: "relationType",
-    label: "消费门槛",
-    render: (scope: any) => {
-      return scope.row.moneyLow + "元" + "~" + scope.row.moneyHigh + "元";
-    }
-  },
-  {
-    prop: "status",
-    label: "状态",
-    enum: [
-      { label: "启用", value: 0 },
-      { label: "禁用", value: 1 }
-    ],
-    search: { el: "select", props: { placeholder: "请选择状态" } },
-    fieldNames: { label: "label", value: "value" },
-    render: (scope: any) => {
-      return scope.row.status == 0 ? "启用" : "禁用";
-    }
-  },
-  { prop: "operation", label: "操作", fixed: "right", width: 250 }
-]);
-
-// 初始化请求参数
-const initParam = reactive({
-  storeId: localGet("createdId") || ""
-});
-
-// 数据回调处理:兼容接口返回数组或 { records/list, total } 格式,确保有数据时分页显示正确总数(iOS 上 total 需为明确数字)
-const dataCallback = (data: any) => {
-  const list = Array.isArray(data) ? data : (data?.records ?? data?.list ?? []);
-  const totalNum =
-    typeof data?.total === "number" && Number.isFinite(data.total) ? data.total : Array.isArray(list) ? list.length : 0;
-  const total = Math.max(0, Math.floor(Number(totalNum)) || 0);
-  return {
-    list,
-    total
-  };
-};
-
-// 获取表格列表
-const getTableList = (params: any) => {
-  return getRuleList(params);
-};
-
-// 获取状态文本
-const getStatusText = (status: number) => {
-  const statusMap: Record<number, string> = {
-    0: "待同意",
-    1: "已同意",
-    2: "已拒绝"
-  };
-  return statusMap[status] || "--";
-};
-
-// 获取状态类型
-const getStatusType = (status: number): "success" | "warning" | "info" | "danger" => {
-  const typeMap: Record<number, "success" | "warning" | "info" | "danger"> = {
-    0: "warning",
-    1: "success",
-    2: "info"
-  };
-  return typeMap[status] || "info";
-};
-
-// 获取商家列表(互相关注接口,仅显示 phoneId 带 store 的商家)- 与 group_merchant eachOtherInterest 参数一致
-const loadAddActivityMerchants = async () => {
-  const phone = localGet("iphone") || localGet("geeker-user")?.userInfo?.phone || "";
-  try {
-    const res: any = await getMutualAttention({
-      page: 1,
-      size: 1000,
-      fansId: `store_${phone}`
-    });
-    const data = res?.data || res;
-    const records = data?.records || data?.list || (Array.isArray(data) ? data : []);
-    const list = Array.isArray(records) ? records : [];
-    const merchantListFiltered = list.filter((item: any) => {
-      const pid = String(item.phoneId ?? item.id ?? "");
-      return pid.includes("store");
-    });
-    merchantListRaw.value = merchantListFiltered;
-    // 按 value(id/storeId/phoneId)去重,避免同一商家在接口中多次返回导致下拉重复
-    const seen = new Set<string | number>();
-    merchantList.value = merchantListFiltered
-      .map((item: any) => ({
-        value: item.id ?? item.storeId ?? item.phoneId,
-        label: item.storeName ?? item.name ?? "",
-        raw: item
-      }))
-      .filter((o: any) => {
-        if (seen.has(o.value)) return false;
-        seen.add(o.value);
-        return true;
-      });
-  } catch (error) {
-    console.error("获取商家列表失败", error);
-    merchantList.value = [];
-    merchantListRaw.value = [];
-  }
-};
-
-// 打开添加对话框
-const openAddDialog = async () => {
-  isEdit.value = false;
-  currentEditId.value = "";
-  await loadAddActivityMerchants();
-  couponOptionsMap.value = {};
-  couponDataMap.value = {};
-  formData.distributeType = 1;
-  formData.coupons = [{ merchant: null, coupon: null, remaining: 0 }];
-  formData.acName = "";
-  formData.moneyLow = "";
-  formData.moneyHigh = "";
-  dialogVisible.value = true;
-};
-
-// 处理金额输入
-const handleMoneyInput = (field: "moneyLow" | "moneyHigh") => {
-  // 当一个金额字段改变时,触发另一个字段的验证
-  if (formRef.value) {
-    const otherField = field === "moneyLow" ? "moneyHigh" : "moneyLow";
-    // 如果另一个字段有值,触发其验证
-    if (formData[otherField] !== "" && formData[otherField] !== null && formData[otherField] !== undefined) {
-      formRef.value.validateField(otherField, () => {});
-    }
-  }
-};
-
-// 关闭对话框
-const closeDialog = () => {
-  dialogVisible.value = false;
-  formRef.value?.resetFields();
-  Object.assign(formData, {
-    acName: "",
-    distributeType: 1,
-    moneyLow: "",
-    moneyHigh: "",
-    coupons: []
-  });
-  currentEditId.value = "";
-};
-
-// 添加商家优惠券
-const addMerchantCoupon = () => {
-  formData.coupons.push({
-    merchant: null,
-    coupon: null,
-    remaining: 0
-  });
-};
-
-// 移除商家优惠券
-const removeMerchantCoupon = (index: number) => {
-  formData.coupons.splice(index, 1);
-};
-
-// 赠送类型切换:清空选项缓存并重置行内选择(与 group_merchant setDistributeType 一致)
-const onDistributeTypeChange = () => {
-  couponOptionsMap.value = {};
-  couponDataMap.value = {};
-  formData.coupons = formData.coupons.map(() => ({
-    merchant: null,
-    coupon: null,
-    remaining: 0
-  }));
-  if (formData.coupons.length !== 1) formData.coupons = [{ merchant: null, coupon: null, remaining: 0 }];
-  loadAddActivityMerchants();
-};
-
-// 加载当前类型下的优惠券/折扣券选项
-const loadCouponOptionsByType = async (distributeType: number) => {
-  const key = distributeType === 2 ? "2-couponList" : "1-couponList";
-  if (couponOptionsMap.value[key]?.length) return;
-  const storeId = localGet("createdId") || "";
-  try {
-    const res: any = await getIssueCouponList({
-      storeId,
-      tab: 1,
-      size: 100,
-      page: 1,
-      couponName: "",
-      couponsFromType: 1,
-      couponType: formData.distributeType
-    });
-    const records = res?.data?.records ?? res?.data ?? [];
-    const list = Array.isArray(records) ? records : [];
-    couponDataMap.value[key] = list;
-    couponOptionsMap.value[key] = list.map((item: any) => ({
-      value: String(item.id ?? item.couponId ?? ""),
-      label: item.couponName ?? item.name ?? ""
-    }));
-  } catch (e) {
-    couponDataMap.value[key] = [];
-    couponOptionsMap.value[key] = [];
-  }
-};
-
-// 商家选择改变
-const handleMerchantChange = async (index: number) => {
-  const item = formData.coupons[index];
-  item.coupon = null;
-  item.remaining = 0;
-  if (item.merchant) {
-    const raw = merchantListRaw.value.find(
-      (m: any) => (m.id ?? m.friendStoreUserId) === item.merchant || (m.storeId ?? m.phoneId) === item.merchant
-    );
-    if (raw) item.remaining = raw.singleQty ?? raw.couponNum ?? 0;
-    await loadCouponOptionsByType(formData.distributeType);
-  }
-};
-
-// 优惠券/折扣券选择改变
-const handleCouponChange = (index: number) => {
-  const item = formData.coupons[index];
-  const key = formData.distributeType === 2 ? "2-couponList" : "1-couponList";
-  const list = couponDataMap.value[key] || [];
-  const idKey = formData.distributeType === 2 ? "voucherId" : "couponId";
-  if (item.coupon && list.length) {
-    const found = list.find((c: any) => String(c[idKey] ?? c.couponId ?? c.id ?? "") === String(item.coupon));
-    if (found) item.remaining = found.singleQty ?? found.couponNum ?? 0;
-  }
-};
-
-// 编辑行数据(与 group_merchant editActivity 逻辑及 getRuleById 回显一致)
-const editRow = async (row: any) => {
-  isEdit.value = true;
-  currentEditId.value = row.id;
-  const id = row.id;
-  if (!id) return;
-
-  await loadAddActivityMerchants();
-  const type = row.type === 2 ? 2 : 1;
-  formData.distributeType = type;
-  couponOptionsMap.value = {};
-  couponDataMap.value = {};
-
-  try {
-    const res: any = await getRuleById({ id });
-    if (res.code != 200 || !res.data) {
-      ElMessage.error(res.msg || "获取活动详情失败");
-      return;
-    }
-    const d = res.data;
-    formData.acName = d.acName;
-    formData.moneyLow = d.moneyLow;
-    formData.moneyHigh = d.moneyHigh;
-    formData.distributeType = d.couponType;
-    formData.coupons = (d.lifeDiscountCouponFriendRuleDetailVos || []).map((vo: any) => ({
-      merchant: vo.friendStoreUserId ?? null,
-      coupon: normalizeCouponValue(vo.couponId ?? vo.voucherId),
-      remaining: vo.singleQty ?? vo.couponNum ?? 0,
-      storeName: vo.storeName ?? ""
-    }));
-    if (formData.coupons.length === 0) {
-      formData.coupons = [{ merchant: null, coupon: null, remaining: 0 }];
-    }
-    await loadCouponOptionsByType(formData.distributeType);
-    // 编辑回显:从已加载的券列表中按券 id 同步剩余张数,避免详情接口未返回 singleQty/couponNum 时一直显示 0
-    const key = formData.distributeType === 2 ? "2-couponList" : "1-couponList";
-    const list = couponDataMap.value[key] || [];
-    const idKey = formData.distributeType === 2 ? "voucherId" : "couponId";
-    formData.coupons.forEach((row: any) => {
-      if (!row.coupon) return;
-      const found = list.find((c: any) => String(c[idKey] ?? c.couponId ?? c.id ?? "") === String(row.coupon));
-      if (found) row.remaining = found.singleQty ?? found.couponNum ?? 0;
-    });
-    dialogVisible.value = true;
-  } catch (error: any) {
-    ElMessage.error(error?.msg || "获取活动详情失败");
-  }
-};
-
-// 删除行数据
-const deleteRow = (row: any) => {
-  ElMessageBox.confirm("确定要删除这个活动吗?", "提示", {
-    confirmButtonText: "确定",
-    cancelButtonText: "取消",
-    type: "warning"
-  })
-    .then(async () => {
-      try {
-        const res: any = await delFriendCouponRule({ id: row.id });
-        if (res.code == 200) {
-          ElMessage.success("删除成功");
-          proTable.value?.getTableList();
-        } else {
-          ElMessage.error(res.msg || "删除失败");
-        }
-      } catch (error: any) {
-        ElMessage.error(error?.msg || "删除失败");
-      }
-    })
-    .catch(() => {
-      // 用户取消删除
-    });
-};
-
-// 提交表单(与 group_merchant saveActivity 请求参数一致)
-const handleSubmit = async () => {
-  if (!formRef.value) return;
-
-  await formRef.value.validate(async (valid: boolean) => {
-    if (!valid) return;
-
-    if (!formData.coupons.length) {
-      ElMessage.warning("请至少添加一个商家优惠券");
-      return;
-    }
-    const hasEmpty = formData.coupons.some((item: any) => !item.merchant || !item.coupon);
-    if (hasEmpty) {
-      ElMessage.warning("请完善所有商家优惠券信息");
-      return;
-    }
-
-    const isVoucher = formData.distributeType === 2;
-    const details = formData.coupons.map((item: any) =>
-      isVoucher
-        ? { voucherId: item.coupon, friendStoreUserId: item.merchant }
-        : { couponId: item.coupon, friendStoreUserId: item.merchant }
-    );
-
-    const requestData: any = {
-      storeId: localGet("createdId") || "",
-      acName: formData.acName.trim(),
-      couponType: formData.distributeType,
-      moneyHigh: Number(formData.moneyHigh),
-      moneyLow: Number(formData.moneyLow),
-      details
-    };
-    if (isEdit.value && currentEditId.value) {
-      requestData.id = currentEditId.value;
-    }
-
-    try {
-      const res: any = await saveFriendCouponRule(requestData);
-      if (res.code == 200) {
-        ElMessage.success(isEdit.value ? "编辑成功" : "添加成功");
-        closeDialog();
-        proTable.value?.getTableList();
-      } else {
-        ElMessage.error(res.msg || "操作失败");
-      }
-    } catch (error: any) {
-      ElMessage.error(error?.msg || (isEdit.value ? "编辑失败" : "添加失败"));
-    }
-  });
-};
-
-// 页面加载时触发查询
-onMounted(() => {
-  proTable.value?.getTableList();
-});
-</script>
-
-<style lang="scss" scoped>
-.friend-relation-container {
-  .header-button {
-    margin-bottom: 16px;
-  }
-  .merchant-coupon-list {
-    width: 100%;
-    .merchant-coupon-item {
-      display: flex;
-      align-items: center;
-      margin-bottom: 10px;
-    }
-    .remaining-text {
-      margin-right: 10px;
-      font-size: 13px;
-      color: var(--el-text-color-regular);
-    }
-  }
-  .dialog-footer {
-    display: flex;
-    gap: 10px;
-    justify-content: flex-end;
-  }
-}
-</style>

+ 3 - 3
src/views/dynamicManagement/index.vue

@@ -1421,14 +1421,14 @@ const handleReportRemove = (uploadFile: any, uploadFiles: any[]) => {
 // 举报图片上传前验证
 const beforeReportUpload = (file: File) => {
   const isImage = file.type.startsWith("image/");
-  const isLt5M = file.size / 1024 / 1024 < 5;
+  const isLt20M = file.size / 1024 / 1024 < 20;
 
   if (!isImage) {
     ElMessage.error("只能上传图片文件!");
     return false;
   }
-  if (!isLt5M) {
-    ElMessage.error("图片大小不能超过 5MB!");
+  if (!isLt20M) {
+    ElMessage.error("图片大小不能超过 20MB!");
     return false;
   }
   return true;

+ 49 - 6
src/views/dynamicManagement/myDynamic.vue

@@ -461,7 +461,13 @@
         <div ref="relationListScrollRef" class="relation-list" @scroll="onRelationListScroll">
           <div v-for="user in filteredRelationList" :key="user.id" class="relation-item">
             <div class="user-info-row">
-              <div class="user-avatar-small">
+              <div
+                class="user-avatar-small relation-avatar-link"
+                role="button"
+                tabindex="0"
+                @click.stop="handleRelationAvatarClick(user)"
+                @keydown.enter.prevent="handleRelationAvatarClick(user)"
+              >
                 <img v-if="user.avatar" :src="user.avatar" :alt="user.name" />
                 <el-icon v-else :size="40">
                   <Avatar />
@@ -826,7 +832,10 @@ interface RelationUser {
   avatar: string;
   description: string;
   relationStatus: "following" | "mutual" | "none"; // following: 已关注, mutual: 互相关注, none: 未关注
-  phoneId?: string; // 用户的 phoneId,用于后续操作
+  phoneId?: string; // 用户的 phoneId,用于后续操作与跳转主页
+  /** 对应他人主页 query.userId(多为 storeUserId) */
+  storeUserId?: string | number;
+  phone?: string;
 }
 
 // 响应式数据
@@ -1112,6 +1121,27 @@ const handleVideoPlay = (index: number) => {
   });
 };
 
+// 关系弹窗(好友/关注/粉丝)内点击头像进入他人主页
+const handleRelationAvatarClick = (user: RelationUser) => {
+  const phoneId = String(user.phoneId || "").trim();
+  if (!phoneId) {
+    ElMessage.warning("无法跳转:缺少用户标识");
+    return;
+  }
+  const userId = String(user.storeUserId ?? user.id ?? "");
+  relationDialogVisible.value = false;
+  router.push({
+    path: "/dynamicManagement/userDynamic",
+    query: {
+      userId,
+      phoneId,
+      userName: user.name || "",
+      userAvatar: user.avatar || "",
+      phone: user.phone || ""
+    }
+  });
+};
+
 // 查看用户主页
 const handleViewUserProfile = () => {
   // ElMessage.info("查看用户主页功能开发中");
@@ -1552,14 +1582,14 @@ const handleReportRemove = (uploadFile: any, uploadFiles: any[]) => {
 // 举报图片上传前验证
 const beforeReportUpload = (file: File) => {
   const isImage = file.type.startsWith("image/");
-  const isLt5M = file.size / 1024 / 1024 < 5;
+  const isLt20M = file.size / 1024 / 1024 < 20;
 
   if (!isImage) {
     ElMessage.error("只能上传图片文件!");
     return false;
   }
-  if (!isLt5M) {
-    ElMessage.error("图片大小不能超过 5MB!");
+  if (!isLt20M) {
+    ElMessage.error("图片大小不能超过 20MB!");
     return false;
   }
   return true;
@@ -1800,7 +1830,9 @@ const mapRecordToRelationUser = (item: any, defaultStatus: "following" | "mutual
   avatar: item.image || item.userImage || item.avatar || item.headImg || "",
   description: item.blurb || item.accountBlurb || item.description || item.bio || item.signature || "",
   relationStatus: item.isFollowThis === 1 ? ("mutual" as const) : defaultStatus,
-  phoneId: item.phoneId || item.fansId || ""
+  phoneId: item.phoneId || item.fansId || "",
+  storeUserId: item.storeUserId ?? item.storeId ?? item.storesUserId ?? item.userId ?? item.id,
+  phone: item.phone || item.mobile || item.telephone || ""
 });
 
 const handleFriendList = async (append = false) => {
@@ -2955,6 +2987,17 @@ onMounted(() => {
                 height: 100%;
                 object-fit: cover;
               }
+              &.relation-avatar-link {
+                cursor: pointer;
+                outline: none;
+                transition: opacity 0.2s;
+                &:hover {
+                  opacity: 0.88;
+                }
+                &:focus-visible {
+                  box-shadow: 0 0 0 2px rgb(64 158 255 / 35%);
+                }
+              }
             }
             .user-details {
               flex: 1;

+ 15 - 15
src/views/dynamicManagement/publishDynamic.vue

@@ -208,6 +208,12 @@ const uploading = ref(false);
 const currentDraftId = ref<number | string | null>(null); // 当前编辑的草稿ID
 const isEditMode = ref(false); // 是否为编辑模式
 
+/** 动态发布:体积上限与提示(与商户端口径一致;仅在此处弹一次,避免 before-upload 与 on-change 重复提示) */
+const DYNAMIC_IMAGE_MAX_MB = 20;
+const DYNAMIC_VIDEO_MAX_MB = 500;
+const UPLOAD_TIP_IMAGE = "图片建议不超过 20MB";
+const UPLOAD_TIP_VIDEO = "视频建议不超过 500MB";
+
 // 表单数据
 const formData = reactive<FormData>({
   title: "",
@@ -324,7 +330,7 @@ const handleFileChange = (uploadFile: UploadFile, uploadFiles: UploadFile[]) =>
       if (index > -1) {
         uploadFiles.splice(index, 1);
       }
-      ElMessage.warning("只能上传图片或视频文件");
+      // 提示由 beforeImageUpload 统一给出,避免与 before-upload 双次弹窗
       return;
     }
 
@@ -335,25 +341,22 @@ const handleFileChange = (uploadFile: UploadFile, uploadFiles: UploadFile[]) =>
     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;
+    // 体积超限:不在此弹窗(与 beforeImageUpload 重复);仅从列表移除,由 beforeImageUpload 统一提示一次
+    const maxSizeMb = isVideo ? DYNAMIC_VIDEO_MAX_MB : DYNAMIC_IMAGE_MAX_MB;
+    const isLtMaxSize = uploadFile.raw.size / 1024 / 1024 < maxSizeMb;
     if (!isLtMaxSize) {
       const index = uploadFiles.findIndex(f => f.uid === uploadFile.uid);
       if (index > -1) {
         uploadFiles.splice(index, 1);
       }
-      ElMessage.warning(`${isVideo ? "视频" : "图片"}大小不能超过 ${maxSize}MB`);
       return;
     }
   }
@@ -461,30 +464,27 @@ const uploadSingleFile = async (file: UploadFile) => {
   }
 };
 
-// 图片上传前验证
+// 图片上传前验证(与 el-upload on-change 相比先执行或后执行因版本而异,此处为唯一弹窗出口)
 const beforeImageUpload = (file: File) => {
   const isImage = file.type.startsWith("image/");
   const isVideo = file.type.startsWith("video/");
   const isValidType = isImage || isVideo;
 
   if (!isValidType) {
-    ElMessage.error("只能上传图片或视频文件!");
+    ElMessage.warning("只能上传图片或视频文件");
     return false;
   }
 
   // 权限:已上传视频则不能再传;已上传图片则只能继续传图片(排除当前文件,避免清除图片后选视频被误判)
   const permission = checkUploadPermission(file, file);
   if (!permission.allowed) {
-    ElMessage.warning(permission.message);
+    ElMessage.warning(permission.message || "当前不可上传该文件");
     return false;
   }
 
-  // 图片和视频使用不同的大小限制
-  const maxSize = isVideo ? 100 : 20; // 视频100MB,图片20MB
-  const isLtMaxSize = file.size / 1024 / 1024 < maxSize;
-
+  const isLtMaxSize = file.size / 1024 / 1024 < (isVideo ? DYNAMIC_VIDEO_MAX_MB : DYNAMIC_IMAGE_MAX_MB);
   if (!isLtMaxSize) {
-    ElMessage.error(`${isVideo ? "视频" : "图片"}大小不能超过 ${maxSize}MB!`);
+    ElMessage.warning(isVideo ? UPLOAD_TIP_VIDEO : UPLOAD_TIP_IMAGE);
     return false;
   }
   return true;

+ 19 - 0
src/views/dynamicManagement/reviewAppeal.vue

@@ -168,6 +168,7 @@
             v-model:file-list="appealFormData.fileList"
             list-type="picture-card"
             :limit="6"
+            :before-upload="beforeAppealImageUpload"
             :http-request="handleUpload"
             :on-preview="handlePreview"
             :on-remove="handleRemove"
@@ -278,6 +279,24 @@ const appealFormData = reactive({
   images: [] as string[], // 申诉凭证:OSS 图片 URL(与 fileList 顺序一致)
   fileList: [] as UploadUserFile[]
 });
+
+/** 申诉凭证仅图片,与商户端 / 动态发布一致:单张 20MB */
+const APPEAL_IMAGE_MAX_MB = 20;
+const UPLOAD_TIP_IMAGE = "图片建议不超过 20MB";
+
+const beforeAppealImageUpload = (rawFile: File) => {
+  const mime = String(rawFile?.type || "");
+  if (!mime.startsWith("image/")) {
+    ElMessage.warning("只能上传图片格式");
+    return false;
+  }
+  if (rawFile.size > APPEAL_IMAGE_MAX_MB * 1024 * 1024) {
+    ElMessage.warning(UPLOAD_TIP_IMAGE);
+    return false;
+  }
+  return true;
+};
+
 const hasStoreReply = (review: any) => {
   return review?.storeComment && review.storeComment.length > 0;
 };

+ 26 - 9
src/views/dynamicManagement/userDynamic.vue

@@ -549,7 +549,9 @@
                 :max="getGiftRowMaxQuantity(row)"
                 @input="(val: string | number) => setGiftRowQuantity(row, val)"
               />
-              <span v-if="row.couponId" class="quantity-limit-hint"> 最多可赠 {{ getGiftRowMaxQuantity(row) }} 张 </span>
+              <span v-if="row.couponId && !isGiftRowUnlimitedQty(row)" class="quantity-limit-hint">
+                最多可赠 {{ getGiftRowMaxQuantity(row) }} 张
+              </span>
             </div>
             <el-button type="danger" link :icon="Delete" circle title="删除" @click="removeGiftCouponRow(index)" />
           </div>
@@ -1187,7 +1189,7 @@ const handleCommand = (command: string) => {
 const giftCouponDialogVisible = ref(false);
 const giftFriendList = ref<{ id: number | string; name: string; phoneId?: string }[]>([]);
 const giftFriendListLoading = ref(false);
-const giftCouponList = ref<{ id: number | string; name: string; singleQty?: number }[]>([]);
+const giftCouponList = ref<{ id: number | string; name: string; singleQty?: number; unlimitedQty?: number }[]>([]);
 const giftCouponListLoaded = ref(false);
 const giftCouponListLoading = ref(false);
 let giftCouponRowKey = 0;
@@ -1249,14 +1251,16 @@ const loadGiftCouponList = async (giftType: 1 | 2 = giftCouponFormData.giftType)
         giftCouponList.value = rawList.map((item: any) => ({
           id: item.couponId ?? item.id,
           name: item.name ?? item.couponName ?? "",
-          singleQty: item.singleQty != null ? Number(item.singleQty) : 100
+          singleQty: item.singleQty != null ? Number(item.singleQty) : 100,
+          unlimitedQty: item.unlimitedQty != null ? Number(item.unlimitedQty) : 0
         }));
       } else {
         // 折扣券:id 用 voucherId,提交时传 voucherId
         giftCouponList.value = rawList.map((item: any) => ({
           id: item.voucherId ?? item.id ?? item.couponId,
           name: item.name ?? item.couponName ?? "",
-          singleQty: item.singleQty != null ? Number(item.singleQty) : 100
+          singleQty: item.singleQty != null ? Number(item.singleQty) : 100,
+          unlimitedQty: item.unlimitedQty != null ? Number(item.unlimitedQty) : 0
         }));
       }
     } else {
@@ -1297,11 +1301,24 @@ const isGiftCouponSelectedInOtherRow = (couponId: string | number, currentRowInd
   );
 };
 
-// 根据所选券的 singleQty 限制该行数量上限
+/** 赠券:所选券为不限张数(unlimitedQty=1)时不展示「最多可赠」,数量上限 9999 */
+const GIFT_UNLIMITED_QTY_MAX = 9999;
+
+const isGiftRowUnlimitedQty = (row: { couponId: string | number; quantity: number }) => {
+  if (row.couponId === "" || row.couponId == null) return false;
+  const coupon = giftCouponList.value.find((c: any) => String(c.id) === String(row.couponId));
+  const uq = coupon?.unlimitedQty;
+  return uq === 1 || uq === "1" || uq === true;
+};
+
+// 根据所选券的 singleQty 限制该行数量上限;unlimitedQty=1 时不限张数,上限 9999
 const getGiftRowMaxQuantity = (row: { couponId: string | number; quantity: number }) => {
   if (row.couponId === "" || row.couponId == null) return 100;
   const coupon = giftCouponList.value.find((c: any) => String(c.id) === String(row.couponId));
-  const max = coupon?.singleQty != null ? Number(coupon.singleQty) : 100;
+  if (!coupon) return 100;
+  const uq = coupon.unlimitedQty;
+  if (uq === 1 || uq === "1" || uq === true) return GIFT_UNLIMITED_QTY_MAX;
+  const max = coupon.singleQty != null ? Number(coupon.singleQty) : 100;
   return Math.max(1, max);
 };
 
@@ -1482,14 +1499,14 @@ const handleReportRemove = (uploadFile: any, uploadFiles: any[]) => {
 // 举报图片上传前验证
 const beforeReportUpload = (file: File) => {
   const isImage = file.type.startsWith("image/");
-  const isLt5M = file.size / 1024 / 1024 < 5;
+  const isLt20M = file.size / 1024 / 1024 < 20;
 
   if (!isImage) {
     ElMessage.error("只能上传图片文件!");
     return false;
   }
-  if (!isLt5M) {
-    ElMessage.error("图片大小不能超过 5MB!");
+  if (!isLt20M) {
+    ElMessage.error("图片大小不能超过 20MB!");
     return false;
   }
   return true;

+ 2 - 2
src/views/groupPackageManagement/index.vue

@@ -95,7 +95,7 @@
         <el-form-item label="套餐名">
           {{ formInventory.groupName }}
         </el-form-item>
-        <el-form-item label="剩余库存">
+        <el-form-item label="剩余数量">
           {{ formInventory.inventoryNum }}
         </el-form-item>
         <el-form-item label="修改库存" prop="newInventory">
@@ -275,7 +275,7 @@ const columns = reactive<ColumnProps<any>[]>([
   },
   {
     prop: "inventoryNum",
-    label: "剩余库存"
+    label: "剩余数量"
   },
   {
     prop: "goodsId",

+ 16 - 0
src/views/groupPackageManagement/newGroup.vue

@@ -1288,6 +1288,22 @@ const handleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFil
       ElMessage.warning("只支持上传 JPG 和 PNG 格式的图片");
       return;
     }
+    const maxBytes = 20 * 1024 * 1024;
+    if (uploadFile.raw.size > maxBytes) {
+      const index = storeInfoModel.value.imageValueStr.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (index > -1) {
+        storeInfoModel.value.imageValueStr.splice(index, 1);
+      }
+      const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (uploadIndex > -1) {
+        uploadFiles.splice(uploadIndex, 1);
+      }
+      if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
+        URL.revokeObjectURL(uploadFile.url);
+      }
+      ElMessage.warning("上传图片不得超过20M");
+      return;
+    }
   }
 
   // 同步文件列表到表单数据(只添加通过验证的文件)

+ 45 - 11
src/views/licenseManagement/businessLicense.vue

@@ -9,7 +9,12 @@
     </div>
     <div class="license-container" v-if="licenseImage">
       <div class="license-display">
-        <el-image :src="licenseImage" fit="contain" class="license-image" :preview-src-list="[licenseImage]">
+        <el-image
+          :src="licenseImage"
+          fit="contain"
+          class="license-image"
+          :preview-src-list="licensePreviewList.length ? licensePreviewList : [licenseImage]"
+        >
           <template #error>
             <div class="empty-image-box">
               <el-icon class="empty-icon">
@@ -82,16 +87,20 @@
               {{ item.createdDateFormat }}
             </div>
             <div class="record-items">
-              <div class="record-item">
+              <div
+                v-for="(url, uIdx) in parseCommaSeparatedImgUrls(item.imgUrl)"
+                :key="`${item.id ?? index}-${uIdx}`"
+                class="record-item"
+              >
                 <div class="record-status-badge" :class="getStatusClass(item.licenseExecuteStatus)">
-                  {{ item.licenseExecuteName }}
+                  {{ getLicenseExecuteRecordText(item) }}
                 </div>
                 <el-image
-                  :src="item.imgUrl"
+                  :src="url"
                   fit="cover"
                   class="record-image"
-                  :preview-src-list="changeRecordList.map(record => record.imgUrl)"
-                  :initial-index="index"
+                  :preview-src-list="parseCommaSeparatedImgUrls(item.imgUrl)"
+                  :initial-index="uIdx"
                 >
                   <template #error>
                     <div class="image-slot">
@@ -138,12 +147,14 @@ const userInfo = localGet("geeker-user")?.userInfo || {};
 const statusMap: Record<number, { name: string; class: string }> = {
   1: { name: "审核通过", class: "status-success" },
   2: { name: "审核中", class: "status-pending" },
-  3: { name: "审核拒绝", class: "status-failed" }
+  3: { name: "审核失败", class: "status-failed" }
 };
 
 const id = localGet("createdId");
 
 const licenseImage = ref<string>("");
+/** 接口 imgUrl 可能为多图逗号拼接 */
+const licensePreviewList = ref<string[]>([]);
 const expirationTime = ref<string>("");
 const replaceDialogVisible = ref(false);
 const changeRecordDialogVisible = ref(false);
@@ -184,13 +195,29 @@ onMounted(async () => {
   await initData();
 });
 
+/** 资质/变更记录:imgUrl 多为英文逗号分隔的多张图地址 */
+function parseCommaSeparatedImgUrls(raw: unknown): string[] {
+  if (raw == null || raw === "") return [];
+  if (Array.isArray(raw)) {
+    return raw.map(u => String(u).trim()).filter(Boolean);
+  }
+  const s = String(raw).trim();
+  if (!s) return [];
+  return s
+    .split(",")
+    .map(part => part.trim())
+    .filter(Boolean);
+}
+
 const initData = async () => {
   const params = {
     id: id
   };
   const res: any = await getBusinessLicense(params);
   if (res.code == 200) {
-    licenseImage.value = res.data[0]?.imgUrl;
+    const urls = parseCommaSeparatedImgUrls(res.data[0]?.imgUrl);
+    licensePreviewList.value = urls;
+    licenseImage.value = urls[0] || "";
     expirationTime.value = res.data[0]?.expirationTime;
   }
 };
@@ -625,16 +652,23 @@ const handleSubmitReplace = async () => {
   }
 };
 
-const getStatusClass = (status: string) => {
-  const statusInfo = statusMap[status];
+const getStatusClass = (status: string | number) => {
+  const statusInfo = statusMap[status as number];
   return statusInfo ? statusInfo.class : "";
 };
 
+/** 变更记录每条图都展示同一执行状态文案(与 licenseExecuteStatus 对齐) */
+const getLicenseExecuteRecordText = (item: { licenseExecuteStatus?: number; licenseExecuteName?: string }) => {
+  const s = item.licenseExecuteStatus;
+  if (s == null) return item.licenseExecuteName ?? "未知";
+  return statusMap[s]?.name ?? item.licenseExecuteName ?? "未知";
+};
+
 const getStatusText = (status: string) => {
   const map: Record<string, string> = {
     pending: "审核中",
     success: "审核通过",
-    failed: "审核拒绝"
+    failed: "审核失败"
   };
   return map[status] || "未知";
 };

+ 94 - 34
src/views/licenseManagement/contractManagement.vue

@@ -10,23 +10,33 @@
     </div>
     <div class="contract-container" v-if="contractList && contractList.length > 0">
       <el-row :gutter="20">
-        <el-col v-for="(item, index) in contractList" :key="index" :xs="12" :sm="8" :md="6" :lg="4" :xl="4">
-          <div class="contract-item">
-            <el-image
-              :src="item.imgUrl"
-              fit=""
-              class="contract-image"
-              :preview-src-list="contractList.map(img => img.imgUrl)"
-              :initial-index="index"
-            >
-              <template #error>
-                <div class="image-slot">
-                  <el-icon><Picture /></el-icon>
-                </div>
-              </template>
-            </el-image>
-          </div>
-        </el-col>
+        <template v-for="(item, index) in contractList" :key="index">
+          <el-col
+            v-for="(url, uIdx) in parseCommaSeparatedImgUrls(item.imgUrl)"
+            :key="`${index}-${uIdx}`"
+            :xs="12"
+            :sm="8"
+            :md="6"
+            :lg="4"
+            :xl="4"
+          >
+            <div class="contract-item">
+              <el-image
+                :src="url"
+                fit=""
+                class="contract-image"
+                :preview-src-list="contractPreviewFlatUrls"
+                :initial-index="contractFlatInitialIndex(index, uIdx)"
+              >
+                <template #error>
+                  <div class="image-slot">
+                    <el-icon><Picture /></el-icon>
+                  </div>
+                </template>
+              </el-image>
+            </div>
+          </el-col>
+        </template>
       </el-row>
     </div>
     <div v-else class="empty-contract">
@@ -95,24 +105,30 @@
               {{ item.createdDateFormat }}
             </div>
             <div class="record-items">
-              <div class="record-item" v-for="(citem, cindex) in item.licenseList" :key="cindex">
-                <el-image
-                  :src="citem.imgUrl"
-                  fit="cover"
-                  class="record-image"
-                  :preview-src-list="item.licenseList.map(v => v.imgUrl)"
-                  :initial-index="cindex"
+              <template v-for="(citem, cindex) in item.licenseList || []" :key="cindex">
+                <div
+                  v-for="(url, uIdx) in parseCommaSeparatedImgUrls(citem.imgUrl)"
+                  :key="`${cindex}-${uIdx}`"
+                  class="record-item"
                 >
-                  <template #error>
-                    <div class="image-slot">
-                      <el-icon><Picture /></el-icon>
-                    </div>
-                  </template>
-                </el-image>
-                <div class="record-status-badge" :class="getStatusClass(item.licenseExecuteStatus)">
-                  {{ item.licenseExecuteName }}
+                  <el-image
+                    :src="url"
+                    fit="cover"
+                    class="record-image"
+                    :preview-src-list="getRecordGroupPreviewUrls(item)"
+                    :initial-index="getRecordGroupFlatIndex(item, cindex, uIdx)"
+                  >
+                    <template #error>
+                      <div class="image-slot">
+                        <el-icon><Picture /></el-icon>
+                      </div>
+                    </template>
+                  </el-image>
+                  <div class="record-status-badge" :class="getStatusClass(item.licenseExecuteStatus)">
+                    {{ getStatusName(item.licenseExecuteStatus) }}
+                  </div>
                 </div>
-              </div>
+              </template>
             </div>
             <div v-if="item.reasonRefusal" class="rejection-reason">拒绝原因: {{ item.reasonRefusal }}</div>
           </div>
@@ -149,7 +165,7 @@ import {
 const statusMap: Record<number, { name: string; class: string }> = {
   1: { name: "审核通过", class: "status-success" },
   2: { name: "审核中", class: "status-pending" },
-  3: { name: "审核拒绝", class: "status-failed" }
+  3: { name: "审核失败", class: "status-failed" }
 };
 
 const contractList = ref<any>([]);
@@ -196,6 +212,50 @@ onMounted(async () => {
   await initData();
 });
 
+/** 合同/变更记录:imgUrl 多为英文逗号分隔的多张图地址 */
+function parseCommaSeparatedImgUrls(raw: unknown): string[] {
+  if (raw == null || raw === "") return [];
+  if (Array.isArray(raw)) {
+    return raw.map(u => String(u).trim()).filter(Boolean);
+  }
+  const s = String(raw).trim();
+  if (!s) return [];
+  return s
+    .split(",")
+    .map(part => part.trim())
+    .filter(Boolean);
+}
+
+const contractPreviewFlatUrls = computed(() => {
+  const list = contractList.value || [];
+  return list.flatMap((row: any) => parseCommaSeparatedImgUrls(row?.imgUrl));
+});
+
+function contractFlatInitialIndex(itemIndex: number, urlIndexInItem: number): number {
+  const list = contractList.value || [];
+  let offset = 0;
+  for (let i = 0; i < itemIndex; i++) {
+    offset += parseCommaSeparatedImgUrls(list[i]?.imgUrl).length;
+  }
+  return offset + urlIndexInItem;
+}
+
+function getRecordGroupPreviewUrls(recordItem: any): string[] {
+  const list = recordItem?.licenseList;
+  if (!Array.isArray(list)) return [];
+  return list.flatMap((v: any) => parseCommaSeparatedImgUrls(v?.imgUrl));
+}
+
+function getRecordGroupFlatIndex(recordItem: any, cindex: number, uIdx: number): number {
+  const list = recordItem?.licenseList;
+  if (!Array.isArray(list)) return 0;
+  let offset = 0;
+  for (let i = 0; i < cindex; i++) {
+    offset += parseCommaSeparatedImgUrls(list[i]?.imgUrl).length;
+  }
+  return offset + uIdx;
+}
+
 const initData = async () => {
   const params = {
     id: id

+ 34 - 7
src/views/licenseManagement/entertainmentLicense.vue

@@ -9,7 +9,12 @@
     </div>
     <div class="license-container" v-if="licenseImage">
       <div class="license-display">
-        <el-image :src="licenseImage" fit="contain" class="license-image" :preview-src-list="[licenseImage]">
+        <el-image
+          :src="licenseImage"
+          fit="contain"
+          class="license-image"
+          :preview-src-list="licensePreviewList.length ? licensePreviewList : [licenseImage]"
+        >
           <template #error>
             <div class="empty-image-box">
               <el-icon class="empty-icon">
@@ -82,16 +87,20 @@
               {{ item.createdTime || item.updatedTime || "--" }}
             </div>
             <div class="record-items">
-              <div class="record-item">
+              <div
+                v-for="(url, uIdx) in parseCommaSeparatedImgUrls(item.imgUrl)"
+                :key="`${item.id ?? index}-${uIdx}`"
+                class="record-item"
+              >
                 <div class="record-status-badge" :class="getChangeRecordStatusClass(item)">
                   {{ getChangeRecordStatusText(item) }}
                 </div>
                 <el-image
-                  :src="item.imgUrl"
+                  :src="url"
                   fit="cover"
                   class="record-image"
-                  :preview-src-list="changeRecordList.map(record => record.imgUrl).filter(Boolean)"
-                  :initial-index="Number(index)"
+                  :preview-src-list="parseCommaSeparatedImgUrls(item.imgUrl)"
+                  :initial-index="uIdx"
                 >
                   <template #error>
                     <div class="image-slot">
@@ -136,13 +145,15 @@ import PcImagePreviewViewer from "@/components/pcMediaPreview/PcImagePreviewView
 const statusMap: Record<number, { name: string; class: string }> = {
   1: { name: "审核通过", class: "status-success" },
   2: { name: "审核中", class: "status-pending" },
-  3: { name: "审核拒绝", class: "status-failed" }
+  3: { name: "审核失败", class: "status-failed" }
 };
 
 const id = localGet("createdId");
 const userInfo = localGet("geeker-user")?.userInfo || {};
 const expirationTime = ref<string>("");
 const licenseImage = ref<string>("");
+/** 接口 imgUrl 可能为多图逗号拼接,预览需完整列表 */
+const licensePreviewList = ref<string[]>([]);
 const replaceDialogVisible = ref(false);
 const changeRecordDialogVisible = ref(false);
 const fileList = ref<UploadFile[]>([]);
@@ -182,13 +193,29 @@ onMounted(async () => {
   await initData();
 });
 
+/** 资质/变更记录:imgUrl 多为英文逗号分隔的多张图地址 */
+function parseCommaSeparatedImgUrls(raw: unknown): string[] {
+  if (raw == null || raw === "") return [];
+  if (Array.isArray(raw)) {
+    return raw.map(u => String(u).trim()).filter(Boolean);
+  }
+  const s = String(raw).trim();
+  if (!s) return [];
+  return s
+    .split(",")
+    .map(part => part.trim())
+    .filter(Boolean);
+}
+
 const initData = async () => {
   const params = {
     id: id
   };
   const res: any = await getEntertainmentBusinessLicense(params);
   if (res.code == 200) {
-    licenseImage.value = res.data[0]?.imgUrl;
+    const urls = parseCommaSeparatedImgUrls(res.data[0]?.imgUrl);
+    licensePreviewList.value = urls;
+    licenseImage.value = urls[0] || "";
     expirationTime.value = res.data[0]?.expirationTime;
   }
 };

+ 45 - 11
src/views/licenseManagement/foodBusinessLicense.vue

@@ -9,7 +9,12 @@
     </div>
     <div class="license-container" v-if="licenseImage">
       <div class="license-display">
-        <el-image :src="licenseImage" fit="contain" class="license-image" :preview-src-list="[licenseImage]">
+        <el-image
+          :src="licenseImage"
+          fit="contain"
+          class="license-image"
+          :preview-src-list="licensePreviewList.length ? licensePreviewList : [licenseImage]"
+        >
           <template #error>
             <div class="empty-image-box">
               <el-icon class="empty-icon">
@@ -82,16 +87,20 @@
               {{ item.createdDateFormat }}
             </div>
             <div class="record-items">
-              <div class="record-item">
+              <div
+                v-for="(url, uIdx) in parseCommaSeparatedImgUrls(item.imgUrl)"
+                :key="`${item.id ?? index}-${uIdx}`"
+                class="record-item"
+              >
                 <div class="record-status-badge" :class="getStatusClass(item.licenseExecuteStatus)">
-                  {{ item.licenseExecuteName }}
+                  {{ getLicenseExecuteRecordText(item) }}
                 </div>
                 <el-image
-                  :src="item.imgUrl"
+                  :src="url"
                   fit="cover"
                   class="record-image"
-                  :preview-src-list="changeRecordList.map(record => record.imgUrl)"
-                  :initial-index="index"
+                  :preview-src-list="parseCommaSeparatedImgUrls(item.imgUrl)"
+                  :initial-index="uIdx"
                 >
                   <template #error>
                     <div class="image-slot">
@@ -138,12 +147,14 @@ const userInfo = localGet("geeker-user")?.userInfo || {};
 const statusMap: Record<number, { name: string; class: string }> = {
   1: { name: "审核通过", class: "status-success" },
   2: { name: "审核中", class: "status-pending" },
-  3: { name: "审核拒绝", class: "status-failed" }
+  3: { name: "审核失败", class: "status-failed" }
 };
 
 const id = localGet("createdId");
 
 const licenseImage = ref<string>("");
+/** 接口 imgUrl 可能为多图逗号拼接 */
+const licensePreviewList = ref<string[]>([]);
 const expirationTime = ref<string>("");
 const replaceDialogVisible = ref(false);
 const changeRecordDialogVisible = ref(false);
@@ -184,13 +195,29 @@ onMounted(async () => {
   await initData();
 });
 
+/** 资质/变更记录:imgUrl 多为英文逗号分隔的多张图地址 */
+function parseCommaSeparatedImgUrls(raw: unknown): string[] {
+  if (raw == null || raw === "") return [];
+  if (Array.isArray(raw)) {
+    return raw.map(u => String(u).trim()).filter(Boolean);
+  }
+  const s = String(raw).trim();
+  if (!s) return [];
+  return s
+    .split(",")
+    .map(part => part.trim())
+    .filter(Boolean);
+}
+
 const initData = async () => {
   const params = {
     id: id
   };
   const res: any = await getFoodBusinessLicense(params);
   if (res.code == 200) {
-    licenseImage.value = res.data[0]?.imgUrl;
+    const urls = parseCommaSeparatedImgUrls(res.data[0]?.imgUrl);
+    licensePreviewList.value = urls;
+    licenseImage.value = urls[0] || "";
     expirationTime.value = res.data[0]?.expirationTime;
   }
 };
@@ -628,16 +655,23 @@ const handleSubmitReplace = async () => {
   }
 };
 
-const getStatusClass = (status: string) => {
-  const statusInfo = statusMap[status];
+const getStatusClass = (status: string | number) => {
+  const statusInfo = statusMap[status as number];
   return statusInfo ? statusInfo.class : "";
 };
 
+/** 变更记录每条图都展示同一执行状态文案(与 licenseExecuteStatus 对齐,不用接口里的「审核拒绝」等旧文案) */
+const getLicenseExecuteRecordText = (item: { licenseExecuteStatus?: number; licenseExecuteName?: string }) => {
+  const s = item.licenseExecuteStatus;
+  if (s == null) return item.licenseExecuteName ?? "未知";
+  return statusMap[s]?.name ?? item.licenseExecuteName ?? "未知";
+};
+
 const getStatusText = (status: string) => {
   const map: Record<string, string> = {
     pending: "审核中",
     success: "审核通过",
-    failed: "审核拒绝"
+    failed: "审核失败"
   };
   return map[status] || "未知";
 };

+ 2 - 0
src/views/login/components/LoginForm.vue

@@ -97,6 +97,7 @@ import type { ElForm } from "element-plus";
 import md5 from "md5";
 import { aiLogin } from "@/api/indexAi";
 import { localSet } from "@/utils";
+import { resetAuthExpiredDedupe } from "@/api/helper/handleAuthExpired";
 
 const router = useRouter();
 const userStore = useUserStore();
@@ -260,6 +261,7 @@ const handleLogin = async () => {
 
       if (data) {
         userStore.setToken(data.token);
+        resetAuthExpiredDedupe();
         console.log("AI登录");
 
         // 保存用户信息到localStorage,供AI登录使用

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

@@ -612,6 +612,7 @@ import {
 } from "@/api/modules/newLoginApi";
 import { getMerchantByPhone } from "@/api/modules/homeEntry";
 import { localGet, localRemove, localSet } from "@/utils";
+import { resetAuthExpiredDedupe } from "@/api/helper/handleAuthExpired";
 import * as path from "node:path";
 import { checkMenuClickPermission } from "@/utils/permission";
 import { aiLogin } from "@/api/indexAi";
@@ -910,6 +911,7 @@ const handleLogin = async () => {
 
       if (res.data) {
         userStore.setToken(res.data.token);
+        resetAuthExpiredDedupe();
         userStore.setUserInfo(res.data);
         const userInfo = {
           userInfo: res.data,

+ 6 - 6
src/views/operationManagement/newActivity.vue

@@ -899,8 +899,8 @@ const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uplo
       return;
     }
 
-    // 检查文件大小,不得超过5M
-    const maxSize = 5 * 1024 * 1024; // 5MB
+    // 检查文件大小,不得超过20M
+    const maxSize = 20 * 1024 * 1024; // 20MB
     if (uploadFile.raw.size > maxSize) {
       // 从文件列表中移除超过大小的文件
       const index = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
@@ -916,7 +916,7 @@ const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uplo
       if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
         URL.revokeObjectURL(uploadFile.url);
       }
-      ElMessage.warning("上传图片不得超过5M");
+      ElMessage.warning("上传图片不得超过20M");
       return;
     }
   }
@@ -968,8 +968,8 @@ const handleDetailUploadChange: UploadProps["onChange"] = async (uploadFile, upl
       return;
     }
 
-    // 检查文件大小,不得超过5M
-    const maxSize = 5 * 1024 * 1024; // 5MB
+    // 检查文件大小,不得超过20M
+    const maxSize = 20 * 1024 * 1024; // 20MB
     if (uploadFile.raw.size > maxSize) {
       // 从文件列表中移除超过大小的文件
       const index = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
@@ -985,7 +985,7 @@ const handleDetailUploadChange: UploadProps["onChange"] = async (uploadFile, upl
       if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
         URL.revokeObjectURL(uploadFile.url);
       }
-      ElMessage.warning("上传图片不得超过5M");
+      ElMessage.warning("上传图片不得超过20M");
       return;
     }
   }

+ 2 - 2
src/views/performance/components/PerformanceFormDialog.vue

@@ -19,7 +19,7 @@
                 v-model:file-list="posterFileList"
                 :api="uploadImgStore"
                 :limit="1"
-                :file-size="5"
+                :file-size="20"
                 class="poster-upload"
                 @update:file-list="onPosterChange"
               />
@@ -180,7 +180,7 @@
                 v-model:file-list="detailImageFileList"
                 :api="uploadImgStore"
                 :limit="9"
-                :file-size="5"
+                :file-size="20"
                 class="detail-upload"
                 @update:file-list="onDetailImagesChange"
               />

+ 2 - 2
src/views/performance/edit.vue

@@ -46,7 +46,7 @@
                   v-model:file-list="posterFileList"
                   :api="uploadImgStore"
                   :limit="1"
-                  :file-size="5"
+                  :file-size="20"
                   :disabled="viewMode"
                   class="poster-upload"
                   @update:file-list="onPosterChange"
@@ -196,7 +196,7 @@
                   v-model:file-list="detailImageFileList"
                   :api="uploadImgStore"
                   :limit="9"
-                  :file-size="5"
+                  :file-size="20"
                   :disabled="viewMode"
                   class="detail-upload"
                   @update:file-list="onDetailImagesChange"

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

@@ -45,7 +45,7 @@
                 v-model:file-list="imageFileList"
                 :api="handleCustomImageUpload"
                 :limit="9"
-                :file-size="5"
+                :file-size="20"
                 :width="'100px'"
                 :height="'100px'"
                 :disabled="viewMode"
@@ -242,7 +242,7 @@
                 v-model:file-list="detailImageFileList"
                 :api="handleCustomImageUpload"
                 :limit="9"
-                :file-size="5"
+                :file-size="20"
                 :width="'100px'"
                 :height="'100px'"
                 :disabled="viewMode"

+ 3 - 3
src/views/storeDecoration/add.vue

@@ -797,14 +797,14 @@ const handleCityClear = () => {
 // 图片上传前验证
 const beforeUpload: UploadProps["beforeUpload"] = (rawFile: File) => {
   const isImage = rawFile.type.startsWith("image/");
-  const isLt10M = rawFile.size / 1024 / 1024 < 10;
+  const isLt20M = rawFile.size / 1024 / 1024 < 20;
 
   if (!isImage) {
     ElMessage.error("只能上传图片文件!");
     return false;
   }
-  if (!isLt10M) {
-    ElMessage.error("图片大小不能超过 10MB!");
+  if (!isLt20M) {
+    ElMessage.error("图片大小不能超过 20MB!");
     return false;
   }
   return true;

+ 13 - 0
src/views/storeDecoration/decorationChat.vue

@@ -150,6 +150,9 @@ const videoInputRef = ref<HTMLInputElement | null>(null);
 let cleanMessageFn: (() => void) | null = null;
 let statusTimer: ReturnType<typeof setInterval> | null = null;
 
+const MAX_CHAT_IMAGE_MB = 20;
+const MAX_CHAT_VIDEO_MB = 200;
+
 const userInfo = computed(() => localGet("geeker-user")?.userInfo || {});
 
 /** 与 myDynamic.vue「cachedHeadImg」一致:geeker-user 的 headImg → avatar;无有效地址则空串走图标占位 */
@@ -236,6 +239,11 @@ const handleImageSelect = async (e: Event) => {
   const file = target.files?.[0];
   target.value = "";
   if (!file || !receiverId.value) return;
+  const sizeMb = file.size / (1024 * 1024);
+  if (sizeMb > MAX_CHAT_IMAGE_MB) {
+    ElMessage.warning(`图片大小不能超过 ${MAX_CHAT_IMAGE_MB}MB`);
+    return;
+  }
   if (!isWsReady()) {
     ElMessage.warning("连接已断开,请稍后重试");
     return;
@@ -286,6 +294,11 @@ const handleVideoSelect = async (e: Event) => {
   const file = target.files?.[0];
   target.value = "";
   if (!file || !receiverId.value) return;
+  const sizeMb = file.size / (1024 * 1024);
+  if (sizeMb > MAX_CHAT_VIDEO_MB) {
+    ElMessage.warning(`视频大小不能超过 ${MAX_CHAT_VIDEO_MB}MB`);
+    return;
+  }
   if (!isWsReady()) {
     ElMessage.warning("连接已断开,请稍后重试");
     return;

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

@@ -496,13 +496,13 @@ const openImageUpload = () => {
 
 const beforePanoramaUpload = (file: File) => {
   const isImage = file.type.startsWith("image/");
-  const isLt10M = file.size / 1024 / 1024 < 10;
+  const isLt20M = file.size / 1024 / 1024 < 20;
   if (!isImage) {
     ElMessage.error("只能上传图片文件");
     return false;
   }
-  if (!isLt10M) {
-    ElMessage.error("图片大小不能超过10MB");
+  if (!isLt20M) {
+    ElMessage.error("图片大小不能超过20MB");
     return false;
   }
   return true;

+ 1 - 1
src/views/storeDecoration/facilitiesAndServices/components/ServiceManagement.vue

@@ -118,7 +118,7 @@
             v-model:image-url="formData.imageUrl"
             :width="'150px'"
             :height="'150px'"
-            :file-size="9999"
+            :file-size="20"
             :api="formData => uploadFormDataToOss(formData, 'image')"
             :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
             :border-radius="'8px'"

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

@@ -100,7 +100,7 @@
             v-model:image-url="formData.imgUrl"
             :width="'200px'"
             :height="'200px'"
-            :file-size="9999"
+            :file-size="20"
             :api="uploadImg"
             :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
             :border-radius="'8px'"

+ 2 - 2
src/views/storeDecoration/officialPhotoAlbum/index.vue

@@ -25,7 +25,7 @@
     <div class="content-section">
       <div v-if="isAlbumTab && currentAlbumRef" class="upload-tips">
         <span>照片宽高不小于500像素</span>
-        <span>大小不超过10M</span>
+        <span>大小不超过20M</span>
         <span>可拖拽排序</span>
         <span>暂不支持上传动图</span>
         <span>不可上传手机截屏、含清晰人脸/电话/二维码/第三方水印/LOGO等图片。</span>
@@ -37,7 +37,7 @@
           <UploadImgs
             v-model:file-list="currentAlbumRef.images"
             :limit="50"
-            :file-size="10"
+            :file-size="20"
             :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
             :width="'100%'"
             :height="'200px'"

+ 6 - 3
src/views/storeDecoration/personnelConfig/index.vue

@@ -256,7 +256,7 @@
             v-model:image-url="formData.avatar"
             :width="'150px'"
             :height="'150px'"
-            :file-size="9999"
+            :file-size="20"
             :api="personnelOssUploadAvatar"
             :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
             :border-radius="'8px'"
@@ -277,7 +277,8 @@
               ref="backgroundImagesUploadRef"
               v-model:file-list="formData.backgroundImages"
               :limit="9"
-              :file-size="100"
+              :file-size="20"
+              :video-file-size="200"
               :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4']"
               :width="'150px'"
               :height="'150px'"
@@ -289,7 +290,9 @@
               :on-video-preview="handleVideoPreviewInForm"
             >
               <template #tip>
-                <div class="upload-tip">上传图片或视频 ({{ formData.backgroundImages.length }}/9),单个文件不超过 100M</div>
+                <div class="upload-tip">
+                  上传图片或视频 ({{ formData.backgroundImages.length }}/9),图片单文件不超过 20M,视频单文件不超过 200M
+                </div>
               </template>
             </UploadImgs>
           </div>

+ 303 - 114
src/views/storeDecoration/storeCoverMap/index.vue

@@ -7,12 +7,11 @@
           <div class="preview-card preview-card--blue">
             <div class="preview-media">
               <img :src="previewDemoImg" alt="效果预览示例" class="preview-img preview-img--single" />
-              <div class="preview-album-pill" aria-hidden="true">
+              <img src="@/assets/images/headImg.png" alt="效果预览示例" class="preview-img-head" />
+              <!--<div class="preview-album-pill" aria-hidden="true">
                 <span>相册</span>
-                <el-icon>
-                  <ArrowRight />
-                </el-icon>
-              </div>
+                <el-icon><ArrowRight /></el-icon>
+              </div> -->
             </div>
             <div class="preview-meta">
               <div>
@@ -35,65 +34,78 @@
       <section class="upload-section">
         <h2 class="block-heading">上传封面图</h2>
         <div class="upload-layout">
-          <div
-            class="upload-slot"
-            :class="{ filled: !!draftCover, uploading: uploading }"
-            :role="draftCover ? undefined : 'button'"
-            :tabindex="draftCover ? -1 : 0"
-            @click="onUploadSlotClick"
-            @keydown.enter.prevent="onUploadSlotKeydown"
-          >
+          <div class="upload-slots-column">
             <input
               ref="fileInputRef"
               type="file"
               class="upload-input-hidden"
               :accept="fileAcceptAttr"
+              :multiple="allowMultipleOnInput"
               @change="onFileInputChange"
             />
-            <template v-if="!draftCover">
-              <el-icon class="upload-slot__plus">
-                <Plus />
-              </el-icon>
-              <div class="upload-slot__primary">上传图片/视频 {{ slotCountTip }}</div>
-              <div class="upload-slot__hint">建议尺寸 1242*640px,大小不超过 {{ maxMbDisplay }}MB</div>
-            </template>
-            <template v-else-if="draftCover.isVideo">
-              <div class="upload-slot__preview upload-slot__preview--video">
-                <video
-                  :src="draftCover.url"
-                  class="upload-slot__thumb-v"
-                  muted
-                  playsinline
-                  preload="metadata"
-                  :poster="draftCover.posterUrl || undefined"
-                />
-                <div class="upload-slot__hover-overlay">
-                  <button type="button" class="upload-slot__action" @click.stop="openCoverPreview">
-                    <el-icon><View /></el-icon>
-                    <span>查看</span>
-                  </button>
-                  <button type="button" class="upload-slot__action" @click.stop="clearDraft">
-                    <el-icon><Delete /></el-icon>
-                    <span>删除</span>
-                  </button>
-                </div>
+            <div class="upload-slots-row">
+              <div
+                v-for="(item, index) in draftCovers"
+                :key="'cover-' + index + '-' + (item.url || '')"
+                class="upload-slot upload-slot--grid filled"
+                :class="{ uploading: uploading }"
+              >
+                <template v-if="item.isVideo">
+                  <div class="upload-slot__preview upload-slot__preview--video">
+                    <video
+                      :src="item.url"
+                      class="upload-slot__thumb-v"
+                      muted
+                      playsinline
+                      preload="metadata"
+                      :poster="item.posterUrl || undefined"
+                    />
+                    <div class="upload-slot__hover-overlay">
+                      <button type="button" class="upload-slot__action" @click.stop="openCoverPreview(index)">
+                        <el-icon><View /></el-icon>
+                        <span>查看</span>
+                      </button>
+                      <button type="button" class="upload-slot__action" @click.stop="removeDraftAt(index)">
+                        <el-icon><Delete /></el-icon>
+                        <span>删除</span>
+                      </button>
+                    </div>
+                  </div>
+                </template>
+                <template v-else>
+                  <div class="upload-slot__preview">
+                    <img :src="item.url" alt="已选封面图" class="upload-slot__thumb" />
+                    <div class="upload-slot__hover-overlay">
+                      <button type="button" class="upload-slot__action" @click.stop="openCoverPreview(index)">
+                        <el-icon><View /></el-icon>
+                        <span>查看</span>
+                      </button>
+                      <button type="button" class="upload-slot__action" @click.stop="removeDraftAt(index)">
+                        <el-icon><Delete /></el-icon>
+                        <span>删除</span>
+                      </button>
+                    </div>
+                  </div>
+                </template>
               </div>
-            </template>
-            <template v-else>
-              <div class="upload-slot__preview">
-                <img :src="draftCover.url" alt="已选封面图" class="upload-slot__thumb" />
-                <div class="upload-slot__hover-overlay">
-                  <button type="button" class="upload-slot__action" @click.stop="openCoverPreview">
-                    <el-icon><View /></el-icon>
-                    <span>查看</span>
-                  </button>
-                  <button type="button" class="upload-slot__action" @click.stop="clearDraft">
-                    <el-icon><Delete /></el-icon>
-                    <span>删除</span>
-                  </button>
+
+              <div
+                v-if="showAddCoverSlot"
+                class="upload-slot upload-slot--grid"
+                :class="{ uploading: uploading }"
+                role="button"
+                tabindex="0"
+                @click="onAddCoverSlotClick"
+                @keydown.enter.prevent="onAddCoverSlotKeydown"
+              >
+                <el-icon class="upload-slot__plus">
+                  <Plus />
+                </el-icon>
+                <div class="upload-slot__primary">
+                  {{ addSlotPrimaryText }}
                 </div>
               </div>
-            </template>
+            </div>
           </div>
 
           <ul class="instruction-list">
@@ -140,11 +152,11 @@ import PcVideoPreviewDialog from "@/components/pcMediaPreview/PcVideoPreviewDial
 
 /** 与商户端封面 imgType 一致 */
 const IMG_TYPE_COVER = 38;
+/** 与商户端一致:纯图模式最多 5 张,与 1 个视频二选一 */
+const COVER_MAX_IMAGES = 5;
 
-const IMAGE_MAX_MB = 10;
-const VIDEO_MAX_MB = 80;
-/** 与设计稿文案「不超 10M」一致展示 */
-const maxMbDisplay = IMAGE_MAX_MB;
+const IMAGE_MAX_MB = 20;
+const VIDEO_MAX_MB = 500;
 
 /** 对齐官方相册 OSS 快照规则(节选) */
 function tryOssVideoSnapshotCoverUrl(videoUrl: string): string {
@@ -191,7 +203,7 @@ const loading = ref(false);
 const uploading = ref(false);
 const saving = ref(false);
 
-const draftCover = ref<DraftCover | null>(null);
+const draftCovers = ref<DraftCover[]>([]);
 
 const fileInputRef = ref<HTMLInputElement | null>(null);
 /** 图片预览(与 storeHeadMap 一致);视频用弹窗 + 快照避免删除后空引用 */
@@ -200,22 +212,43 @@ const coverImagePreviewUrls = ref<string[]>([]);
 const coverImagePreviewInitialIndex = ref(0);
 const coverVideoPreviewVisible = ref(false);
 const previewTarget = ref<DraftCover | null>(null);
-const slotCountTip = computed(() => (draftCover.value ? `(1/1)` : `(0/1)`));
+
+const draftHasVideo = computed(() => draftCovers.value.some(d => d.isVideo));
+
+const showAddCoverSlot = computed(() => !draftHasVideo.value && draftCovers.value.length < COVER_MAX_IMAGES);
+
+/** 仅已有图片时显示张数上限;空态不显示 (0/5) */
+const addSlotPrimaryText = computed(() => {
+  const n = draftCovers.value.length;
+  if (n > 0) {
+    return `上传图片 (${n}/${COVER_MAX_IMAGES})`;
+  }
+  return "上传图片或视频";
+});
+
+const fileAcceptAttr = computed(() => {
+  if (draftHasVideo.value) return "";
+  if (draftCovers.value.length > 0) {
+    return "image/jpeg,image/png,image/webp,.jpg,.jpeg,.png,.webp";
+  }
+  return "image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm,.mp4,.MOV,.mov,.webm,.m4v";
+});
+
+/** 已有图且未满时允许多选图片一次加入;空列表也可多选图片;视频与图二选一时选视频为单文件 */
+const allowMultipleOnInput = computed(() => !draftHasVideo.value && draftCovers.value.length < COVER_MAX_IMAGES);
 
 const previewReviews = ref(1853);
 const storeNameDisplay = ref("示例门店");
 
-const fileAcceptAttr = "image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm,.mp4,.MOV,.mov,.webm,.m4v";
-
 const instructionLines = [
-  "封面图将展示在门店列表、搜索结果等位置,是用户对门店的第一印象",
-  "支持JPG、PNG、WebP格式图片,或MP4、MOV格式视频",
-  "图片建议包含门店招牌、环境或特色产品,清晰美观无水印"
+  "封面请二选一:上传 1 个视频,或上传最多 5 张图片;保存后仍可重新上传替换。",
+  "视频建议不超过 500MB,避免视频出现显示不全问题。",
+  "建议上传4:3尺寸的图片,图片建议不超过 20MB,避免图片出现显示不全问题。"
 ];
 
 const canSubmit = computed(() => {
-  const u = draftCover.value?.url?.trim();
-  return Boolean(u && !uploading.value);
+  if (uploading.value) return false;
+  return draftCovers.value.some(d => Boolean(d.url?.trim()));
 });
 
 const fetchStoreDetail = async () => {
@@ -264,7 +297,7 @@ const fetchCoverDraft = async () => {
   const userInfo: any = localGet("geeker-user")?.userInfo || {};
   const storeId = userInfo.storeId;
   if (!storeId) {
-    draftCover.value = null;
+    draftCovers.value = [];
     return;
   }
   loading.value = true;
@@ -276,40 +309,55 @@ const fetchCoverDraft = async () => {
       const sorted = [...imgList].sort(
         (a: unknown, b: unknown) => (Number((a as StoreImgRow)?.imgSort) || 0) - (Number((b as StoreImgRow)?.imgSort) || 0)
       );
-      const firstRow = sorted[0];
-      draftCover.value = firstRow ? rowToDraft(firstRow as StoreImgRow) : null;
+      if (!sorted.length) {
+        draftCovers.value = [];
+        return;
+      }
+      const firstDraft = rowToDraft(sorted[0] as StoreImgRow);
+      if (firstDraft?.isVideo) {
+        draftCovers.value = [firstDraft];
+        return;
+      }
+      draftCovers.value = sorted
+        .slice(0, COVER_MAX_IMAGES)
+        .map(row => rowToDraft(row as StoreImgRow))
+        .filter((x): x is DraftCover => x != null);
     } else {
-      draftCover.value = null;
+      draftCovers.value = [];
     }
   } catch {
-    draftCover.value = null;
+    draftCovers.value = [];
   } finally {
     loading.value = false;
   }
 };
 
 function openFilePicker() {
-  if (uploading.value || saving.value || draftCover.value) return;
+  if (uploading.value || saving.value) return;
+  if (draftHasVideo.value) return;
+  if (draftCovers.value.length >= COVER_MAX_IMAGES) return;
   fileInputRef.value?.click();
 }
 
-function onUploadSlotClick() {
+function onAddCoverSlotClick() {
   openFilePicker();
 }
 
-function onUploadSlotKeydown() {
+function onAddCoverSlotKeydown() {
   openFilePicker();
 }
 
-function openCoverPreview() {
-  const d = draftCover.value;
+function openCoverPreview(index: number) {
+  const d = draftCovers.value[index];
   if (!d) return;
   if (d.isVideo) {
     previewTarget.value = { ...d };
     coverVideoPreviewVisible.value = true;
   } else {
-    coverImagePreviewUrls.value = [d.url];
-    coverImagePreviewInitialIndex.value = 0;
+    const urls = draftCovers.value.filter(x => !x.isVideo).map(x => x.url);
+    coverImagePreviewUrls.value = urls;
+    const ii = urls.indexOf(d.url);
+    coverImagePreviewInitialIndex.value = ii >= 0 ? ii : 0;
     coverImagePreviewVisible.value = true;
   }
 }
@@ -319,30 +367,28 @@ function onCoverVideoPreviewClosed() {
   previewTarget.value = null;
 }
 
-async function ingestPickedFile(file: File): Promise<boolean> {
+function isFileVideo(file: File): boolean {
   const name = String(file.name || "").toLowerCase();
-  const type = String(file.type || "");
-
-  let isVideo = /^video\//i.test(type) || /\.(mp4|mov|m4v|webm|3gp)$/i.test(name);
-
-  const imgOk = type === "image/jpeg" || type === "image/png" || type === "image/webp" || /\.(jpe?g|png|webp)$/i.test(name);
+  return /^video\//i.test(String(file.type || "")) || /\.(mp4|mov|m4v|webm|3gp)$/i.test(name);
+}
 
-  if (!imgOk && !isVideo) {
-    ElMessage.warning("仅支持 JPG、PNG、WebP、MP4、MOV");
-    return false;
-  }
+function isFileImage(file: File): boolean {
+  const name = String(file.name || "").toLowerCase();
+  const type = String(file.type || "");
+  return type === "image/jpeg" || type === "image/png" || type === "image/webp" || /\.(jpe?g|png|webp)$/i.test(name);
+}
 
+async function uploadOneToDraft(file: File, isVideo: boolean): Promise<DraftCover | null> {
   const maxMb = isVideo ? VIDEO_MAX_MB : IMAGE_MAX_MB;
   const sizeMb = file.size / (1024 * 1024);
   if (sizeMb > maxMb) {
     ElMessage.warning(`文件不能超过 ${maxMb}MB`);
-    return false;
+    return null;
   }
 
   uploading.value = true;
   try {
     const urls = await uploadFilesToOss([file], isVideo ? "video" : "image", {
-      /** 保留全局 PopupLoading 进度;上传完成不 toast,由本页统一提示(上传服务已含合规校验) */
       uploadSuccessMessage: null
     });
     const url = urls[0];
@@ -353,48 +399,122 @@ async function ingestPickedFile(file: File): Promise<boolean> {
       posterUrl = tryOssVideoSnapshotCoverUrl(url);
     }
 
-    draftCover.value = {
+    return {
       id: undefined,
       url,
       isVideo,
       ...(posterUrl ? { posterUrl } : {})
     };
-    ElMessage.success("上传成功");
-    return true;
   } catch (e: unknown) {
     if (isUploadUserCancelledError(e)) {
-      return false;
+      return null;
     }
     if (isUploadApiErrorAlreadyMessaged(e)) {
-      return false;
+      return null;
     }
     const msg =
       typeof e === "object" && e && "message" in e && typeof (e as { message?: unknown }).message === "string"
         ? String((e as Error).message)
         : "上传失败";
     ElMessage.error(msg);
-    return false;
+    return null;
   } finally {
     uploading.value = false;
   }
 }
 
+async function ingestSingleVideoFromFile(file: File): Promise<boolean> {
+  if (!isFileVideo(file)) {
+    ElMessage.warning("仅支持 MP4、MOV 等视频格式");
+    return false;
+  }
+  const draft = await uploadOneToDraft(file, true);
+  if (!draft) return false;
+  draftCovers.value = [draft];
+  ElMessage.success("上传成功");
+  return true;
+}
+
+async function ingestSingleImageAppend(file: File): Promise<boolean> {
+  if (!isFileImage(file)) {
+    ElMessage.warning("仅支持 JPG、PNG、WebP 图片");
+    return false;
+  }
+  if (draftHasVideo.value) {
+    ElMessage.warning("请先删除视频后再上传图片");
+    return false;
+  }
+  if (draftCovers.value.length >= COVER_MAX_IMAGES) {
+    ElMessage.warning(`最多上传 ${COVER_MAX_IMAGES} 张图片`);
+    return false;
+  }
+  const draft = await uploadOneToDraft(file, false);
+  if (!draft) return false;
+  draftCovers.value = [...draftCovers.value, draft];
+  return true;
+}
+
+async function ingestPickedFiles(files: File[]): Promise<void> {
+  const list = files.filter(Boolean);
+  if (!list.length) return;
+
+  const firstVideo = list.find(isFileVideo);
+  if (firstVideo) {
+    if (list.some(f => isFileImage(f)) || list.filter(isFileVideo).length > 1) {
+      ElMessage.info("已选择视频,将仅保留该视频作为封面(与图片二选一)");
+    }
+    await ingestSingleVideoFromFile(firstVideo);
+    return;
+  }
+
+  if (draftHasVideo.value) {
+    ElMessage.warning("请先删除视频后再上传图片");
+    return;
+  }
+
+  const remaining = COVER_MAX_IMAGES - draftCovers.value.length;
+  if (remaining <= 0) {
+    ElMessage.warning(`最多上传 ${COVER_MAX_IMAGES} 张图片`);
+    return;
+  }
+
+  const imageFiles = list.filter(isFileImage).slice(0, remaining);
+  if (!imageFiles.length) {
+    ElMessage.warning("仅支持 JPG、PNG、WebP、MP4、MOV");
+    return;
+  }
+
+  let okCount = 0;
+  for (const file of imageFiles) {
+    if (draftCovers.value.length >= COVER_MAX_IMAGES) break;
+    const ok = await ingestSingleImageAppend(file);
+    if (ok) okCount += 1;
+    else break;
+  }
+  if (okCount > 1) {
+    ElMessage.success(`已上传 ${okCount} 张图片`);
+  } else if (okCount === 1) {
+    ElMessage.success("上传成功");
+  }
+}
+
 async function onFileInputChange(ev: Event): Promise<void> {
   const input = ev.target as HTMLInputElement;
-  const file = input.files?.[0];
+  const files = Array.from(input.files || []);
   input.value = "";
-  if (!file) return;
-  await ingestPickedFile(file);
+  if (!files.length) return;
+  await ingestPickedFiles(files);
 }
 
-function clearDraft() {
-  draftCover.value = null;
+function removeDraftAt(index: number) {
+  const next = draftCovers.value.filter((_, i) => i !== index);
+  draftCovers.value = next;
 }
 
 async function onSave() {
   if (saving.value) return;
   if (!canSubmit.value) {
-    ElMessage.warning("请先上传一张图片或一段视频(仅 1 条)");
+    ElMessage.warning("请先上传封面:1 个视频或最多 5 张图片");
     return;
   }
   const sidRaw = localGet("geeker-user")?.userInfo?.storeId;
@@ -406,17 +526,15 @@ async function onSave() {
 
   saving.value = true;
   try {
-    const dc = draftCover.value!;
-    const storeImgList = [
-      {
-        id: dc.id,
-        imgUrl: dc.url,
-        imgType: IMG_TYPE_COVER,
-        imgSort: 1,
-        storeId: numericStoreId,
-        isExtract: 0
-      }
-    ];
+    const list = draftCovers.value;
+    const storeImgList = list.map((dc, index) => ({
+      id: dc.id,
+      imgUrl: dc.url,
+      imgType: IMG_TYPE_COVER,
+      imgSort: index + 1,
+      storeId: numericStoreId,
+      isExtract: 0
+    }));
 
     const res: any = await saveStoreHeadImg({
       storeImgList,
@@ -503,10 +621,19 @@ onMounted(async () => {
   }
 }
 .preview-img {
+  position: relative;
   width: 100%;
   height: 100%;
   object-fit: cover;
 }
+.preview-img-head {
+  position: absolute;
+  right: 5px;
+  bottom: 5px;
+  width: 86px;
+  height: 24px;
+  object-fit: cover;
+}
 .preview-img--single {
   display: block;
   width: 100%;
@@ -568,6 +695,16 @@ onMounted(async () => {
   gap: 32px 40px;
   align-items: flex-start;
 }
+.upload-slots-column {
+  flex: 0 1 auto;
+  min-width: 0;
+}
+.upload-slots-row {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 16px;
+  align-items: stretch;
+}
 .upload-slot {
   box-sizing: border-box;
   display: flex;
@@ -679,6 +816,51 @@ onMounted(async () => {
     }
   }
 }
+.upload-slot.upload-slot--grid {
+  box-sizing: border-box;
+  display: flex;
+  flex-shrink: 0;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 240px;
+  height: 180px;
+  min-height: 180px;
+  max-height: 180px;
+  padding: 6px 8px;
+  overflow: hidden;
+  border-radius: 10px;
+  .upload-slot__plus {
+    font-size: 22px;
+  }
+  .upload-slot__primary {
+    margin-top: 2px;
+    font-size: 11px;
+    line-height: 1.3;
+    color: #909399;
+    text-align: center;
+  }
+  &.filled {
+    justify-content: stretch;
+    padding: 0;
+  }
+  &.filled .upload-slot__preview {
+    flex: 1 1 auto;
+    width: 100%;
+    height: 100%;
+    min-height: 0;
+    max-height: none;
+    border-radius: 10px;
+  }
+  &.filled .upload-slot__thumb,
+  &.filled .upload-slot__thumb-v {
+    display: block;
+    width: 100%;
+    height: 100%;
+    max-height: none;
+    object-fit: cover;
+  }
+}
 .upload-input-hidden {
   position: absolute;
   width: 0;
@@ -761,5 +943,12 @@ onMounted(async () => {
     width: 100%;
     max-width: 360px;
   }
+  .upload-slot.upload-slot--grid {
+    width: calc((100% - 16px) / 2);
+    max-width: 240px;
+    height: 180px;
+    min-height: 180px;
+    max-height: 180px;
+  }
 }
 </style>

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

@@ -10,7 +10,7 @@
           <ol class="instruction-list">
             <li>入口图建议上传能够代表店铺的图片,如环境范围、菜品样式、本店特色等。</li>
             <li>建议上传1:1尺寸的图片,避免图片出现显示不全问题。</li>
-            <li>建议尺寸900*900像素以内,不超过10MB。</li>
+            <li>建议尺寸900*900像素以内,不超过20MB。</li>
           </ol>
         </div>
 

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

@@ -142,7 +142,7 @@
               v-model:image-url="formData.imgUrl"
               :width="'200px'"
               :height="'200px'"
-              :file-size="9999"
+              :file-size="20"
               :api="uploadImg"
               :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
               :border-radius="'8px'"

+ 33 - 60
src/views/ticketManagement/couponDetail.vue

@@ -38,50 +38,18 @@
             <div class="detail-label">折扣率</div>
             <div class="detail-value">{{ couponModel.discountRate || "--" }}%</div>
           </div>
-          <!-- 开始领取时间 -->
-          <div class="detail-item">
-            <div class="detail-label">开始领取时间</div>
-            <div class="detail-value">
-              {{ couponModel.beginGetDate ? formatDate(couponModel.beginGetDate) : "--" }}
-            </div>
-          </div>
-          <!-- 结束领取时间 -->
-          <div class="detail-item">
-            <div class="detail-label">结束领取时间</div>
-            <div class="detail-value">
-              {{ couponModel.endGetDate ? formatDate(couponModel.endGetDate) : "--" }}
-            </div>
-          </div>
-          <!-- 有效期 -->
+          <!-- 有效期(领取后) -->
           <div class="detail-item">
             <div class="detail-label">有效期</div>
             <div class="detail-value">
-              {{ couponModel.specifiedDay ? `${couponModel.specifiedDay}天` : "--" }}
+              {{ validityDisplay }}
             </div>
           </div>
-          <!-- 库存 -->
+          <!-- 数量 -->
           <div class="detail-item">
-            <div class="detail-label">库存</div>
+            <div class="detail-label">数量</div>
             <div class="detail-value">
-              {{ couponModel.singleQty || "--" }}
-            </div>
-          </div>
-        </div>
-        <!-- 领取规则模块 -->
-        <div class="model">
-          <h3 style="font-weight: bold">领取规则:</h3>
-          <!-- 用户领取规则 -->
-          <!-- <div class="detail-item">
-            <div class="detail-label">用户领取规则</div>
-            <div class="detail-value">
-              {{ getClaimRuleText() }}
-            </div>
-          </div> -->
-          <!-- 用户是否需要收藏店铺领取 -->
-          <div class="detail-item">
-            <div class="detail-label">用户是否需要收藏店铺领取</div>
-            <div class="detail-value">
-              {{ couponModel.attentionCanReceived === 1 ? "是" : couponModel.attentionCanReceived === 0 ? "否" : "--" }}
+              {{ quantityDisplay }}
             </div>
           </div>
         </div>
@@ -121,7 +89,7 @@
  * 优惠券管理 - 详情页面
  * 功能:显示优惠券的详细信息
  */
-import { ref, onMounted } from "vue";
+import { ref, onMounted, computed } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { ElMessage } from "element-plus";
 import { getCouponDetail } from "@/api/modules/couponManagement";
@@ -147,6 +115,9 @@ const couponModel = ref<any>({
   // 结束领取时间
   endGetDate: "",
   // 有效期
+  longTermValid: undefined as number | undefined,
+  expirationDate: "",
+  unlimitedQty: undefined as number | undefined,
   specifiedDay: "",
   // 库存
   singleQty: "",
@@ -162,6 +133,30 @@ const couponModel = ref<any>({
   supplementaryInstruction: ""
 });
 
+const validityDisplay = computed(() => {
+  const m = couponModel.value;
+  const lt = m.longTermValid;
+  if (lt === 1 || lt === "1" || lt === true) return "长期有效";
+  const days = m.expirationDate ?? m.specifiedDay;
+  const n = Number(days);
+  if (days === null || days === undefined || days === "" || Number.isNaN(n) || n === 0) {
+    const sd = m.specifiedDay;
+    const sn = Number(sd);
+    if (sd === null || sd === undefined || sd === "" || Number.isNaN(sn) || sn === 0) return "长期有效";
+    return `领取后${sd}天`;
+  }
+  return `领取后${days}天`;
+});
+
+const quantityDisplay = computed(() => {
+  const m = couponModel.value;
+  const uq = m.unlimitedQty;
+  if (uq === 1 || uq === "1" || uq === true) return "不限";
+  const sq = m.singleQty;
+  if (sq === null || sq === undefined || sq === "") return "--";
+  return `${sq}张`;
+});
+
 // ==================== 生命周期钩子 ====================
 
 /**
@@ -214,28 +209,6 @@ const loadDetailData = async () => {
 };
 
 // ==================== 工具函数 ====================
-
-/**
- * 格式化日期
- * @param date 日期字符串 (YYYY-MM-DD)
- * @returns 格式化后的日期字符串 (YYYY.MM.DD)
- */
-const formatDate = (date: string) => {
-  if (!date) return "--";
-  return date.replace(/-/g, "/");
-};
-
-/**
- * 获取用户领取规则文本
- */
-const getClaimRuleText = () => {
-  const ruleMap: Record<string, string> = {
-    day: "每日一领",
-    week: "每周一领",
-    month: "每月一领"
-  };
-  return ruleMap[couponModel.value.claimRule] || "--";
-};
 </script>
 
 <style scoped lang="scss">

+ 58 - 95
src/views/ticketManagement/index.vue

@@ -8,17 +8,6 @@
       :data-callback="dataCallback"
       :key="activeName"
     >
-      <!-- 表格 header 按钮 -->
-      <template #tableHeader="scope">
-        <div class="table-header-btn">
-          <div class="header-actions">
-            <!-- 优惠券:状态 Tab(全部、未开始、进行中、已下架、已结束、已清库,不含草稿) -->
-            <el-tabs v-if="activeName === '2'" v-model="couponStatusTab" class="status-tabs" @tab-click="onCouponStatusTabChange">
-              <el-tab-pane v-for="tab in couponStatusTabOptions" :key="tab.value" :label="tab.label" :name="tab.value" />
-            </el-tabs>
-          </div>
-        </div>
-      </template>
       <template #tableHeaderRight="scope">
         <el-button :icon="Plus" class="button" type="primary" @click="newCoupon" v-if="type"> 新建优惠券 </el-button>
         <!--        <div class="action-buttons">-->
@@ -34,10 +23,6 @@
           </p>
           <p style="margin: 0; line-height: 1.5" v-if="scope.row.dataType == 0">审核状态:{{ scope.row.reviewType || "--" }}</p>
         </template>
-        <!-- 优惠券:只显示状态一行 -->
-        <template v-else>
-          <span>{{ getStatusLabel(scope.row.status) }}</span>
-        </template>
       </template>
       <!-- 表格操作 -->
       <template #operation="scope">
@@ -47,26 +32,26 @@
         <el-button v-if="canShowAction(scope.row, '下架')" link type="primary" @click="changeTypes(scope.row, 6)">
           下架
         </el-button>
-        <el-button v-if="canShowAction(scope.row, '修改库存')" link type="primary" @click="changeInventory(scope.row)">
-          修改库存
+        <el-button v-if="canShowAction(scope.row, '修改数量')" link type="primary" @click="changeInventory(scope.row)">
+          修改数量
         </el-button>
         <el-button v-if="canShowAction(scope.row, '查看详情')" link type="primary" @click="toDetail(scope.row)">
           查看详情
         </el-button>
-        <el-button v-if="canShowAction(scope.row, '编辑')" link type="primary" @click="editRow(scope.row)"> 编辑 </el-button>
         <el-button v-if="canShowAction(scope.row, '删除')" link type="primary" @click="deleteRow(scope.row)"> 删除 </el-button>
+        <el-button v-if="canShowAction(scope.row, '编辑')" link type="primary" @click="editRow(scope.row)"> 编辑 </el-button>
         <el-button v-if="canShowAction(scope.row, '查看拒绝原因')" link type="primary" @click="viewRejectReason(scope.row)">
           查看拒绝原因
         </el-button>
       </template>
     </ProTable>
-    <el-dialog v-model="dialogFormVisible" title="修改库存" width="500" append-to-body class="inventory-dialog-ios-fix">
+    <el-dialog v-model="dialogFormVisible" title="修改数量" width="500" append-to-body class="inventory-dialog-ios-fix">
       <el-form ref="ruleFormRef" :model="formInventory" :rules="rules" @submit.prevent>
         <el-form-item label="套餐名">
           {{ formInventory.name }}
         </el-form-item>
-        <el-form-item label="剩余库存"> {{ formInventory.singleQty }}张 </el-form-item>
-        <el-form-item label="修改库存" prop="newInventory">
+        <el-form-item label="剩余数量"> {{ formInventory.singleQty }}张 </el-form-item>
+        <el-form-item label="修改数量" prop="newInventory">
           <el-input
             v-model="formInventory.newInventory"
             placeholder="请输入"
@@ -118,7 +103,12 @@ import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { Plus } from "@element-plus/icons-vue";
 import { delThaliById, getThaliList, updateNum, updateStatus } from "@/api/modules/voucherManagement";
-import { delCouponById, getStoreAllCouponList, updateCouponSingleQty, updateCouponStatus } from "@/api/modules/couponManagement";
+import {
+  deleteLifeDiscountCoupon,
+  getStoreAllCouponList,
+  updateCouponSingleQty,
+  updateCouponStatus
+} from "@/api/modules/couponManagement";
 import { ElMessageBox } from "element-plus/es";
 import { localGet, usePermission } from "@/utils";
 import { formatCurrency } from "@/utils/formatCurrency";
@@ -160,10 +150,10 @@ const rules = reactive<FormRules<RuleForm>>({
             callback(new Error("库存不得大于10000"));
             return;
           }
-          // 验证新库存值不能小于剩余库存
+          // 验证新库存值不能小于剩余数量
           const currentQty = Number(formInventory.value.singleQty) || 0;
           if (numValue < currentQty) {
-            callback(new Error(`新库存值不能小于剩余库存(${currentQty})张`));
+            callback(new Error(`新库存值不能小于剩余数量(${currentQty})张`));
             return;
           }
         }
@@ -181,24 +171,6 @@ const statusEnum = [
   { label: "已售罄", value: "4" },
   { label: "已结束", value: "7" }
 ];
-const statusEnumY = [
-  { label: "未开始", value: "2" },
-  { label: "进行中", value: "0" },
-  { label: "已下架", value: "3" },
-  { label: "已结束", value: "1" },
-  { label: "已清库", value: "4" }
-];
-// 优惠券状态 Tab 选项(不含草稿)
-const couponStatusTabOptions = [
-  { label: "全部", value: "0" },
-  { label: "进行中", value: "1" },
-  // { label: "草稿", value: "3" },
-  { label: "已结束", value: "2" },
-  { label: "已下架", value: "5" },
-  { label: "未开始", value: "4" },
-  { label: "已清库", value: "6" }
-];
-const couponStatusTab = ref("0");
 // 优惠券类型枚举(满减券、折扣券)
 const couponTypeEnum = [
   { label: "满减券", value: "1" },
@@ -232,7 +204,7 @@ const voucherColumns = reactive<ColumnProps<any>[]>([
   },
   {
     prop: "singleQty",
-    label: "剩余库存",
+    label: "剩余数量",
     render: scope => {
       return scope.row.singleQty === null || scope.row.singleQty === undefined || scope.row.singleQty === ""
         ? 0
@@ -259,7 +231,7 @@ const voucherColumns = reactive<ColumnProps<any>[]>([
   { prop: "operation", label: "操作", fixed: "right", width: 330 }
 ]);
 
-// 优惠券表格列配置(参考商家端:优惠券名称、剩余库存、结束时间、状态、类型、操作
+// 优惠券表格列配置:名称、剩余数量、类型、操作(无结束时间、无状态列
 const couponColumns = reactive<ColumnProps<any>[]>([
   {
     prop: "name",
@@ -271,25 +243,39 @@ const couponColumns = reactive<ColumnProps<any>[]>([
   },
   {
     prop: "singleQty",
-    label: "剩余库存",
-    render: scope => {
-      return scope.row.singleQty === null || scope.row.singleQty === undefined || scope.row.singleQty === ""
-        ? 0
-        : scope.row.singleQty;
+    label: "剩余数量",
+    render: (scope: any) => {
+      const row = scope.row;
+      const uq = row.unlimitedQty;
+      if (uq === 1 || uq === "1" || uq === true) return "不限";
+      if (uq === 0 || uq === "0") {
+        const sq = row.singleQty;
+        if (sq === null || sq === undefined || sq === "") return "--";
+        return `${sq}张`;
+      }
+      const sq = row.singleQty;
+      if (sq === null || sq === undefined || sq === "") return "0";
+      if (Number(sq) === 0) return "不限";
+      return `${sq}张`;
     }
   },
   {
-    prop: "validDate",
-    label: "结束时间",
+    prop: "longTermValid",
+    label: "有效期",
     render: (scope: any) => {
-      return scope.row.validDate?.replace(/-/g, "/") || "--";
+      const row = scope.row;
+      const longTerm = row.longTermValid;
+      if (longTerm === 1 || longTerm === "1" || longTerm === true) {
+        return "长期有效";
+      }
+      const days = row.expirationDate;
+      if (days === null || days === undefined || days === "") {
+        return "--";
+      }
+      return `${days}天`;
     }
   },
   {
-    prop: "status",
-    label: "状态"
-  },
-  {
     prop: "couponType",
     label: "类型",
     search: {
@@ -300,7 +286,7 @@ const couponColumns = reactive<ColumnProps<any>[]>([
     fieldNames: { label: "label", value: "value" },
     render: (scope: any) => getCouponTypeLabel(scope.row.couponType)
   },
-  { prop: "operation", label: "操作", fixed: "right", width: 280 }
+  { prop: "operation", label: "操作", fixed: "right", width: 260 }
 ]);
 
 // 根据当前选中的tab动态返回列配置
@@ -338,24 +324,24 @@ const CO_STATUS = {
 // 代金券:各状态下的操作列表(草稿用 dataType==1 单独判断)
 const VOUCHER_ACTIONS_BY_STATUS: Record<number, string[]> = {
   [VO_STATUS.待审核]: ["查看详情", "查看拒绝原因"],
-  [VO_STATUS.未开始]: ["查看详情", "上架", "修改库存", "删除"],
+  [VO_STATUS.未开始]: ["查看详情", "上架", "修改数量", "删除"],
   [VO_STATUS.审核拒绝]: ["查看详情", "编辑", "删除", "查看拒绝原因"],
-  [VO_STATUS.进行中]: ["查看详情", "下架", "修改库存"],
-  [VO_STATUS.已售罄]: ["查看详情", "编辑", "修改库存", "删除"],
+  [VO_STATUS.进行中]: ["查看详情", "下架", "修改数量"],
+  [VO_STATUS.已售罄]: ["查看详情", "编辑", "修改数量", "删除"],
   [VO_STATUS.已下架]: ["查看详情", "上架", "编辑", "删除"],
   [VO_STATUS.已结束]: ["编辑", "删除"]
 };
 // 代金券草稿(dataType==1)仅显示:编辑、删除
 const VOUCHER_DRAFT_ACTIONS = ["编辑", "删除"];
 
-// 优惠券:各状态下的操作列表(完全按产品表
+// 优惠券:各状态下的操作列表(无下架、无修改数量
 const COUPON_ACTIONS_BY_STATUS: Record<number, string[]> = {
-  [CO_STATUS.草稿]: ["查看详情", "编辑", "删除"],
-  [CO_STATUS.未开始]: ["查看详情", "修改库存"],
-  [CO_STATUS.进行中]: ["下架", "查看详情", "修改库存"],
-  [CO_STATUS.已下架]: ["上架", "查看详情", "修改库存"],
+  [CO_STATUS.草稿]: ["查看详情", "删除", "编辑"],
+  [CO_STATUS.未开始]: ["查看详情", "删除"],
+  [CO_STATUS.进行中]: ["查看详情", "删除"],
+  [CO_STATUS.已下架]: ["上架", "查看详情", "删除"],
   [CO_STATUS.已结束]: ["查看详情", "删除"],
-  [CO_STATUS.已售罄]: ["查看详情", "编辑", "删除"]
+  [CO_STATUS.已售罄]: ["查看详情", "删除", "编辑"]
 };
 
 /** 判断当前行是否显示某操作按钮(代金券/优惠券通用) */
@@ -375,17 +361,6 @@ const isVoucher = computed(() => activeName.value === "1");
 // 判断是否为优惠券
 const isCoupon = computed(() => activeName.value === "2");
 
-// 获取状态标签
-const getStatusLabel = (status: any) => {
-  const statusEnumMap = CO_STATUS;
-  for (const [label, value] of Object.entries(statusEnumMap)) {
-    if (value === status) {
-      return label;
-    }
-  }
-  return "--";
-};
-
 // 优惠券类型展示(满减券/折扣券)
 const getCouponTypeLabel = (type: number | string) => {
   const t = type === undefined || type === null ? "" : String(type);
@@ -484,7 +459,7 @@ const getTableList = (params: any) => {
     // 优惠券:使用 getStoreAllCouponList,搜索项 name、couponType 需一并传入
     return getStoreAllCouponList({
       storeId: newParams.storeId || localGet("createdId") || "",
-      tab: couponStatusTab.value || "",
+      tab: "0",
       size: newParams.pageSize || 10,
       page: newParams.pageNum || 1,
       couponName: newParams.name ?? "",
@@ -531,12 +506,8 @@ const deleteRow = (row: any) => {
         };
         return delThaliById(params);
       } else {
-        // 优惠券删除逻辑
-        const params = {
-          id: row.id,
-          groupType: localGet("businessSection")
-        };
-        return delCouponById(params);
+        // 优惠券删除:life-discount-coupon 路径参数 id
+        return deleteLifeDiscountCoupon(row.id);
       }
     })
     .then(() => {
@@ -550,14 +521,6 @@ const deleteRow = (row: any) => {
 // Tab切换处理
 const handleClick = () => {};
 
-// 优惠券状态 Tab 切换
-// 优惠券状态 Tab 切换(tab-click 触发时 v-model 尚未更新,用 nextTick 等绑定更新后再请求)
-const onCouponStatusTabChange = () => {
-  nextTick(() => {
-    proTable.value?.getTableList();
-  });
-};
-
 // 修改状态(上架/下架)
 const changeTypes = async (row: any, status: number) => {
   if (isVoucher.value) {
@@ -576,7 +539,7 @@ const changeTypes = async (row: any, status: number) => {
     }
   }
 };
-// 修改库存
+// 修改数量
 const changeInventory = (row: any) => {
   formInventory.value = {
     id: row.id,
@@ -587,7 +550,7 @@ const changeInventory = (row: any) => {
   dialogFormVisible.value = true;
 };
 
-// 提交修改库存
+// 提交修改数量
 const handleSubmit = async () => {
   if (!ruleFormRef.value) return;
   await ruleFormRef.value.validate(async valid => {
@@ -650,7 +613,7 @@ const closeRejectReasonDialog = () => {
 </script>
 
 <style lang="scss" scoped>
-/* iOS 上修改库存弹窗:避免键盘弹起时遮挡底部,弹窗可滚动 */
+/* iOS 上修改数量弹窗:避免键盘弹起时遮挡底部,弹窗可滚动 */
 :deep(.inventory-dialog-ios-fix) {
   .el-dialog__body {
     max-height: 60vh;

+ 149 - 133
src/views/ticketManagement/newCoupon.vue

@@ -1,5 +1,5 @@
 <template>
-  <!-- 优惠券管理 - 新建/编辑页面(参考 IssueCoupons 与设计图) -->
+  <!-- 优惠券管理 - 新建/编辑页面(参考 IssueCoupons;左:类型/名称/折扣或面值/低消/有效期,右:数量/补充说明) -->
   <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
     <div class="header">
       <el-button @click="goBack"> 返回 </el-button>
@@ -9,7 +9,7 @@
     </div>
     <el-form :model="couponModel" ref="ruleFormRef" :rules="rules" label-width="200px" class="formBox">
       <div class="content">
-        <!-- 左侧:优惠券类型、名称、折扣/面值、低消、时间、有效期 -->
+        <!-- 左侧:优惠券类型、名称、折扣/面值、低消、有效期(领取后天数) -->
         <div class="contentLeft">
           <!-- 优惠券类型 -->
           <el-form-item label="优惠券类型" prop="couponType">
@@ -45,62 +45,47 @@
               <template #suffix> 元可用 </template>
             </el-input>
           </el-form-item>
-          <!-- 开始领取时间 -->
-          <el-form-item label="开始领取时间" prop="beginGetDate">
-            <el-date-picker
-              v-model="couponModel.beginGetDate"
-              format="YYYY/MM/DD"
-              value-format="YYYY-MM-DD"
-              placeholder="年/月/日"
-              :disabled-date="disabledStartDate"
-              style="width: 100%"
-            />
-          </el-form-item>
-          <!-- 结束领取时间 -->
-          <el-form-item label="结束领取时间" prop="endGetDate">
-            <el-date-picker
-              v-model="couponModel.endGetDate"
-              format="YYYY/MM/DD"
-              value-format="YYYY-MM-DD"
-              placeholder="年/月/日"
-              :disabled-date="disabledEndDate"
-              style="width: 100%"
-            />
+          <!-- 有效期:长期有效 / 自定义(领取后有效天数) -->
+          <el-form-item label="有效期" prop="validityMode">
+            <el-radio-group v-model="couponModel.validityMode" class="radio-group">
+              <el-radio :value="0"> 长期有效 </el-radio>
+              <el-radio :value="1"> 自定义 </el-radio>
+            </el-radio-group>
           </el-form-item>
-          <!-- 有效期 -->
-          <el-form-item label="有效期" prop="specifiedDay">
-            <el-input v-model="couponModel.specifiedDay" maxlength="5" placeholder="请输入" clearable>
+          <el-form-item v-if="couponModel.validityMode === 1" label="领取后有效期" prop="validDaysAfterReceive">
+            <el-input
+              v-model="couponModel.validDaysAfterReceive"
+              maxlength="5"
+              placeholder="请输入正整数"
+              clearable
+              inputmode="numeric"
+              @input="onValidDaysAfterReceiveInput"
+            >
               <template #suffix> 天 </template>
             </el-input>
           </el-form-item>
         </div>
-        <!-- 右侧:库存、用户领取规则、收藏店铺、补充说明 -->
+        <!-- 右侧:数量、补充说明 -->
         <div class="contentRight">
-          <!-- 库存 -->
-          <el-form-item label="库存" prop="singleQty">
-            <el-input v-model="couponModel.singleQty" maxlength="5" placeholder="请输入" clearable>
+          <!-- 数量:不限 / 自定义 -->
+          <el-form-item label="数量" prop="quantityMode">
+            <el-radio-group v-model="couponModel.quantityMode" class="radio-group">
+              <el-radio :value="0"> 不限 </el-radio>
+              <el-radio :value="1"> 自定义 </el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item v-if="couponModel.quantityMode === 1" label="请输入数量" prop="singleQty">
+            <el-input
+              v-model="couponModel.singleQty"
+              maxlength="5"
+              placeholder="请输入正整数"
+              clearable
+              inputmode="numeric"
+              @input="onSingleQtyInput"
+            >
               <template #suffix> 张 </template>
             </el-input>
           </el-form-item>
-          <!-- 用户领取规则 -->
-          <!-- <el-form-item label="用户领取规则" prop="claimRule" class="claim-rule-item">
-            <template #label>
-              <span>用户领取规则</span>
-              <span class="label-tip">(用户间隔多久可以领取一次)</span>
-            </template>
-            <el-radio-group v-model="couponModel.claimRule" class="radio-group block">
-              <el-radio value="day"> 每日一领 </el-radio>
-              <el-radio value="week"> 每周一领 </el-radio>
-              <el-radio value="month"> 每月一领 </el-radio>
-            </el-radio-group>
-          </el-form-item> -->
-          <!-- 用户是否需要收藏店铺领取 -->
-          <el-form-item label="用户是否需要收藏店铺领取" prop="attentionCanReceived">
-            <el-radio-group v-model="couponModel.attentionCanReceived" class="radio-group">
-              <el-radio :value="1"> 是 </el-radio>
-              <el-radio :value="0"> 否 </el-radio>
-            </el-radio-group>
-          </el-form-item>
           <!-- 补充说明 -->
           <el-form-item label="补充说明" prop="supplementaryInstruction">
             <el-input
@@ -128,14 +113,15 @@
 <script setup lang="tsx" name="newCoupon">
 /**
  * 优惠券管理 - 新建/编辑页面
- * 参考:group_merchant IssueCoupons、设计图布局(左:类型/名称/折扣或面值/低消/时间/有效期,右:库存/领取规则/收藏/补充说明)
+ * 接口入参:longTermValid(1 长期有效 / 0 自定义天数)+ expirationDate(天);
+ * unlimitedQty(1 不限 / 0 自定义张数)+ singleQty(张)
  */
 import { ref, reactive, watch, nextTick, onMounted } from "vue";
 import { ElMessage } from "element-plus";
 import { useRoute, useRouter } from "vue-router";
 import type { FormInstance } from "element-plus";
 import { getCouponDetail, addDiscountCoupon, editDiscountCoupon } from "@/api/modules/couponManagement";
-import { validatePositiveNumber, validatePositiveInteger, validateDateRange, validatePriceFormat } from "@/utils/eleValidate";
+import { validatePositiveNumber, validatePositiveInteger, validatePriceFormat } from "@/utils/eleValidate";
 import { localGet } from "@/utils";
 
 const router = useRouter();
@@ -182,48 +168,40 @@ const rules = reactive({
     }
   ],
   discountRate: [{ validator: validateDiscountRate, trigger: "blur" }],
-  beginGetDate: [
-    { required: true, message: "请选择开始领取时间" },
-    {
-      validator: validateDateRange(
-        () => couponModel.value.beginGetDate,
-        () => couponModel.value.endGetDate,
-        "开始领取时间不能早于当前时间",
-        "结束领取时间不能早于当前时间",
-        "开始领取时间必须早于结束领取时间",
-        true,
-        true
-      ),
-      trigger: "change"
-    }
-  ],
-  endGetDate: [
-    { required: true, message: "请选择结束领取时间" },
+  validityMode: [{ required: true, message: "请选择有效期类型" }],
+  validDaysAfterReceive: [
     {
-      validator: validateDateRange(
-        () => couponModel.value.beginGetDate,
-        () => couponModel.value.endGetDate,
-        "开始领取时间不能早于当前时间",
-        "结束领取时间不能早于当前时间",
-        "开始领取时间必须早于结束领取时间",
-        true,
-        true
-      ),
-      trigger: "change"
-    }
-  ],
-  specifiedDay: [
-    { required: true, message: "请输入有效期" },
-    {
-      validator: validatePositiveInteger("有效期必须为正整数", { required: false }),
-      trigger: "blur"
+      validator: (_rule: any, value: any, callback: (err?: Error) => void) => {
+        if (couponModel.value.validityMode !== 1) {
+          callback();
+          return;
+        }
+        if (value === null || value === undefined || value === "") {
+          callback(new Error("请输入领取后有效期"));
+          return;
+        }
+        const next = validatePositiveInteger("领取后有效期须为正整数", { required: false });
+        next(_rule, value, callback);
+      },
+      trigger: ["blur", "change"]
     }
   ],
+  quantityMode: [{ required: true, message: "请选择数量类型" }],
   singleQty: [
-    { required: true, message: "请输入库存" },
     {
-      validator: validatePositiveInteger("库存必须为正整数", { required: false }),
-      trigger: "blur"
+      validator: (_rule: any, value: any, callback: (err?: Error) => void) => {
+        if (couponModel.value.quantityMode !== 1) {
+          callback();
+          return;
+        }
+        if (value === null || value === undefined || value === "") {
+          callback(new Error("请输入数量"));
+          return;
+        }
+        const next = validatePositiveInteger("数量须为正整数", { required: false });
+        next(_rule, value, callback);
+      },
+      trigger: ["blur", "change"]
     }
   ],
   minimumSpendingAmount: [
@@ -256,52 +234,58 @@ const rules = reactive({
   ]
 });
 
-const claimRuleOptions = [
-  { label: "每日一领", value: "day" },
-  { label: "每周一领", value: "week" },
-  { label: "每月一领", value: "month" }
-];
-
-const yesNoOptions = [
-  { label: "是", value: 1 },
-  { label: "否", value: 0 }
-];
-
 // 1-满减券 2-折扣券(与 ticketManagement index couponTypeEnum 一致)
+// validityMode:0 长期有效 → longTermValid=1;1 自定义 → longTermValid=0 + expirationDate=天
+// quantityMode:0 不限 → unlimitedQty=1;1 自定义 → unlimitedQty=0 + singleQty=张数
 const couponModel = ref<any>({
   name: "",
   couponType: 2,
   nominalValue: "",
   discountRate: "",
-  beginGetDate: "",
-  endGetDate: "",
-  specifiedDay: "",
+  validityMode: 0,
+  validDaysAfterReceive: "",
+  quantityMode: 0,
   singleQty: "",
   claimRule: "day",
-  attentionCanReceived: 1,
   hasMinimumSpend: 0,
   minimumSpendingAmount: "",
   supplementaryInstruction: ""
 });
 
+/** 仅允许数字,限制长度(正整数输入) */
+const sanitizeDigits = (raw: string, maxLen: number) =>
+  String(raw ?? "")
+    .replace(/\D/g, "")
+    .slice(0, maxLen);
+
+const onValidDaysAfterReceiveInput = (val: string) => {
+  couponModel.value.validDaysAfterReceive = sanitizeDigits(val, 5);
+};
+
+const onSingleQtyInput = (val: string) => {
+  couponModel.value.singleQty = sanitizeDigits(val, 5);
+};
+
 const isInitializing = ref(true);
 
 watch(
-  () => couponModel.value.beginGetDate,
-  () => {
+  () => couponModel.value.validityMode,
+  newVal => {
     if (isInitializing.value) return;
-    if (couponModel.value.endGetDate) {
-      nextTick(() => ruleFormRef.value?.validateField("endGetDate"));
+    if (newVal === 0) {
+      couponModel.value.validDaysAfterReceive = "";
+      nextTick(() => ruleFormRef.value?.clearValidate("validDaysAfterReceive"));
     }
   }
 );
 
 watch(
-  () => couponModel.value.endGetDate,
-  () => {
+  () => couponModel.value.quantityMode,
+  newVal => {
     if (isInitializing.value) return;
-    if (couponModel.value.beginGetDate) {
-      nextTick(() => ruleFormRef.value?.validateField("beginGetDate"));
+    if (newVal === 0) {
+      couponModel.value.singleQty = "";
+      nextTick(() => ruleFormRef.value?.clearValidate("singleQty"));
     }
   }
 );
@@ -338,15 +322,42 @@ onMounted(async () => {
   if (type.value !== "add" && id.value) {
     const res: any = await getCouponDetail({ counponId: id.value });
     const data = res.data || {};
+    const lt = data.longTermValid;
+    const isLongByFlag = lt === 1 || lt === "1" || lt === true;
+    const isLongLegacy =
+      lt === undefined &&
+      (() => {
+        const sd = data.specifiedDay;
+        const sdNum = Number(sd);
+        return sd === null || sd === undefined || sd === "" || Number.isNaN(sdNum) || sdNum === 0;
+      })();
+    const isLongValidity = isLongByFlag || isLongLegacy;
+    const expDays = data.expirationDate ?? data.specifiedDay;
+
+    const uq = data.unlimitedQty;
+    const isUnlimitedByFlag = uq === 1 || uq === "1" || uq === true;
+    const isUnlimitedLegacy =
+      uq === undefined &&
+      (data.singleQty === null || data.singleQty === undefined || data.singleQty === "" || Number(data.singleQty) === 0);
+    const isUnlimitedQty = isUnlimitedByFlag || isUnlimitedLegacy;
+
     couponModel.value = {
       ...couponModel.value,
       ...data,
       couponType: data.couponType ?? 1,
       discountRate: data.discountRate != null ? (Number(data.discountRate) / 10).toString() : "",
-      hasMinimumSpend: Number(data.minimumSpendingAmount) > 0 ? 1 : 0
+      hasMinimumSpend: Number(data.minimumSpendingAmount) > 0 ? 1 : 0,
+      validityMode: isLongValidity ? 0 : 1,
+      validDaysAfterReceive: isLongValidity ? "" : String(expDays ?? ""),
+      quantityMode: isUnlimitedQty ? 0 : 1,
+      singleQty: isUnlimitedQty ? "" : String(data.singleQty ?? "")
     };
   } else {
     couponModel.value.couponType = couponModel.value.couponType ?? 2;
+    couponModel.value.validityMode = 0;
+    couponModel.value.quantityMode = 0;
+    couponModel.value.validDaysAfterReceive = "";
+    couponModel.value.singleQty = "";
   }
   await nextTick();
   ruleFormRef.value?.clearValidate();
@@ -359,17 +370,40 @@ const goBack = () => {
 
 const ruleFormRef = ref<FormInstance>();
 
-const handleSubmit = async (submitType?: string) => {
+const buildSubmitParams = (submitType?: string) => {
+  const m = couponModel.value;
+  const longTermValid = m.validityMode === 0 ? 1 : 0;
+  const expirationDate = m.validityMode === 0 ? 0 : Number(m.validDaysAfterReceive) || 0;
+  const unlimitedQty = m.quantityMode === 0 ? 1 : 0;
+  const singleQty = m.quantityMode === 0 ? 0 : Number(m.singleQty) || 0;
   const params: any = {
-    ...couponModel.value,
+    name: m.name,
+    couponType: m.couponType,
+    nominalValue: m.nominalValue,
+    discountRate: m.discountRate,
+    longTermValid,
+    expirationDate,
+    unlimitedQty,
+    singleQty,
+    claimRule: m.claimRule || "day",
+    attentionCanReceived: 0,
+    hasMinimumSpend: m.hasMinimumSpend,
+    minimumSpendingAmount: m.minimumSpendingAmount,
+    supplementaryInstruction: m.supplementaryInstruction,
+    beginGetDate: "",
+    endGetDate: "",
     storeId: localGet("createdId"),
     couponId: type.value === "edit" ? id.value : "",
     couponStatus: submitType ? 0 : 1,
     restrictedQuantity: 0,
-    expirationDate: 0,
     getStatus: 1,
     type: 1
   };
+  return params;
+};
+
+const handleSubmit = async (submitType?: string) => {
+  const params: any = buildSubmitParams(submitType);
   // 折扣券:discountRate 后端多为 1-100(如 85 表示 8.5 折),此处按 0.1-9.9 输入则乘 10
   if (params.couponType === 2 && params.discountRate !== "" && params.discountRate != null) {
     const rate = Number(params.discountRate);
@@ -422,24 +456,6 @@ const handleSubmit = async (submitType?: string) => {
     }
   });
 };
-
-const disabledStartDate = (time: Date) => {
-  const today = new Date();
-  today.setHours(0, 0, 0, 0);
-  return time.getTime() < today.getTime();
-};
-
-const disabledEndDate = (time: Date) => {
-  const today = new Date();
-  today.setHours(0, 0, 0, 0);
-  if (time.getTime() < today.getTime()) return true;
-  if (couponModel.value.beginGetDate) {
-    const startDate = new Date(couponModel.value.beginGetDate);
-    startDate.setHours(0, 0, 0, 0);
-    return time.getTime() < startDate.getTime();
-  }
-  return false;
-};
 </script>
 
 <style scoped lang="scss">