Bläddra i källkod

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

spy 1 månad sedan
förälder
incheckning
2ba9f99a00
42 ändrade filer med 1491 tillägg och 979 borttagningar
  1. 4 10
      src/api/modules/couponManagement.ts
  2. 4 10
      src/api/modules/groupPackageManagement.ts
  3. 29 0
      src/api/modules/homeEntry.ts
  4. 24 0
      src/api/modules/newLoginApi.ts
  5. 0 6
      src/api/modules/orderManagement.ts
  6. 4 7
      src/api/modules/voucherManagement.ts
  7. BIN
      src/assets/images/1logo.png
  8. BIN
      src/assets/images/baojia.png
  9. 0 33
      src/assets/images/login_bg.svg
  10. BIN
      src/assets/images/login_left.png
  11. BIN
      src/assets/images/login_left1.png
  12. BIN
      src/assets/images/login_left2.png
  13. BIN
      src/assets/images/login_left3.png
  14. BIN
      src/assets/images/login_left4.png
  15. BIN
      src/assets/images/login_left5.png
  16. BIN
      src/assets/images/msg01.png
  17. BIN
      src/assets/images/msg02.png
  18. BIN
      src/assets/images/msg03.png
  19. BIN
      src/assets/images/msg04.png
  20. BIN
      src/assets/images/msg05.png
  21. BIN
      src/assets/images/notData.png
  22. 15 1
      src/assets/json/authMenuList.json
  23. 2 2
      src/components/ProTable/index.vue
  24. 8 3
      src/components/SearchForm/index.vue
  25. 1 1
      src/config/index.ts
  26. 4 1
      src/layouts/components/Header/components/InfoDialog.vue
  27. 22 1
      src/stores/modules/auth.ts
  28. 420 1
      src/utils/eleValidate.ts
  29. 3 0
      src/utils/index.ts
  30. 43 0
      src/utils/permission.ts
  31. 22 14
      src/views/couponManagement/index.vue
  32. 23 109
      src/views/couponManagement/newCoupon.vue
  33. 184 180
      src/views/financialManagement/index.vue
  34. 27 33
      src/views/groupPackageManagement/index.vue
  35. 323 193
      src/views/groupPackageManagement/newGroup.vue
  36. 29 35
      src/views/home/components/go-enter.vue
  37. 205 106
      src/views/home/components/go-flow.vue
  38. 14 18
      src/views/home/notice.vue
  39. 3 27
      src/views/login/index.vue
  40. 7 4
      src/views/orderManagement/index.vue
  41. 21 13
      src/views/voucherManagement/index.vue
  42. 50 171
      src/views/voucherManagement/newVoucher.vue

+ 4 - 10
src/api/modules/couponManagement.ts

@@ -5,11 +5,11 @@ import http from "@/api";
 export const getThaliList = params => {
   return http.get<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/PcGroupBuy/getThaliList`, params);
 };
-export const deleteThali = (params: { id: string }) => {
-  return http.delete(PORT_NONE + `/PcGroupBuy/deleteThali`, params);
+export const delThaliById = (params: { id: string }) => {
+  return http.get(PORT_NONE + `/PcGroupBuy/deleteThali`, params);
 };
-export const sjxj = params => {
-  return http.get(PORT_NONE + `/PcGroupBuy/sjxj`, params);
+export const updateStatus = params => {
+  return http.get(PORT_NONE + `/PcGroupBuy/updateStatus1`, params);
 };
 export const updateNum = params => {
   return http.get(PORT_NONE + `/PcGroupBuy/updateNum1`, params);
@@ -17,12 +17,6 @@ export const updateNum = params => {
 export const saveDraft = params => {
   return http.post(PORT_NONE + `/PcGroupBuy/saveDraft`, params);
 };
-export const getUserByPhone = params => {
-  return http.get(PORT_NONE + `/store/user/getUserByPhone`, params);
-};
-export const getDetail = params => {
-  return http.get(PORT_NONE + `/store/info/getDetail`, params);
-};
 export const getMenuByStoreId = params => {
   return http.get(PORT_NONE + `/menuPlatform/getMenuByStoreId`, params);
 };

+ 4 - 10
src/api/modules/groupPackageManagement.ts

@@ -5,11 +5,11 @@ import http from "@/api";
 export const getThaliList = params => {
   return http.get<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/PcGroupBuy/getThaliList`, params);
 };
-export const deleteThali = (params: { id: string }) => {
-  return http.delete(PORT_NONE + `/PcGroupBuy/deleteThali`, params);
+export const delThaliById = (params: { id: string }) => {
+  return http.get(PORT_NONE + `/PcGroupBuy/delThaliById`, params);
 };
-export const sjxj = params => {
-  return http.get(PORT_NONE + `/PcGroupBuy/sjxj`, params);
+export const updateStatus = params => {
+  return http.get(PORT_NONE + `/PcGroupBuy/updateStatus`, params);
 };
 export const updateNum = params => {
   return http.get(PORT_NONE + `/PcGroupBuy/updateNum`, params);
@@ -20,12 +20,6 @@ export const saveDraft = params => {
 export const saveThali = params => {
   return http.post(PORT_NONE + `/PcGroupBuy/saveThali`, params);
 };
-export const getUserByPhone = params => {
-  return http.get(PORT_NONE + `/store/user/getUserByPhone`, params);
-};
-export const getDetail = params => {
-  return http.get(PORT_NONE + `/store/info/getDetail`, params);
-};
 export const getMenuByStoreId = params => {
   return http.get(PORT_NONE + `/menuPlatform/getMenuByStoreId`, params);
 };

+ 29 - 0
src/api/modules/homeEntry.ts

@@ -0,0 +1,29 @@
+import { ResPage, StoreUser } from "@/api/interface/index";
+import { PORT_NONE } from "@/api/config/servicePort";
+import http from "@/api";
+
+//个人实名
+export const verifyIdInfo = params => {
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/merchantAuth/verifyIdInfo`, params);
+};
+
+//信息提交
+export const applyStore = params => {
+  return http.post(PORT_NONE + `/storeManage/applyStore`, params);
+};
+
+//用户信息
+export const getMerchantByPhone = params => {
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/merchantUser/getMerchantByPhone`, params);
+};
+
+//通知列表
+export const getNoticeList = params => {
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/notice/getNoticeList`, params);
+};
+export const getUserByPhone = params => {
+  return http.get(PORT_NONE + `/merchantUser/getMerchantByPhone`, params);
+};
+export const getDetail = params => {
+  return http.get(PORT_NONE + `/storeManage/getStoreDetail`, params);
+};

+ 24 - 0
src/api/modules/newLoginApi.ts

@@ -1,5 +1,6 @@
 import type { Login } from "@/api/interface";
 import httpLogin from "@/api/indexApi";
+import { Upload } from "@/api/interface/index";
 // 获取图片验证码
 export const getImgCode = () => {
   return httpLogin.get(
@@ -31,3 +32,26 @@ export const loginAccount = params => {
 export const forgetPassword = params => {
   return httpLogin.get(`/alienStorePlatform/storePlatformLogin/updatePassword`, params, { loading: false });
 };
+
+//经营板块
+export const getBusinessSection = () => {
+  return httpLogin.get(`/alienStore/store/info/getBusinessSection`);
+};
+
+//经营种类
+export const getBusinessSectionTypes = (params: { parentId: string }) => {
+  return httpLogin.get(`/alienStore/store/info/getBusinessSectionTypes`, params);
+};
+//经纬度查询
+export const getInputPrompt = params => {
+  return httpLogin.get(`/alienStore/gaode/getInputPrompt`, params);
+};
+
+//所在地区
+export const getDistrict = params => {
+  return httpLogin.get(`/alienStore/gaode/getDistrict`, params);
+};
+//文件上传
+export const uploadImg = (params: FormData) => {
+  return httpLogin.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false });
+};

+ 0 - 6
src/api/modules/orderManagement.ts

@@ -20,12 +20,6 @@ export const xgkc = params => {
 export const saveDraft = params => {
   return http.post(PORT_NONE + `/PcGroupBuy/saveDraft`, params);
 };
-export const getUserByPhone = params => {
-  return http.get(PORT_NONE + `/store/user/getUserByPhone`, params);
-};
-export const getDetail = params => {
-  return http.get(PORT_NONE + `/store/info/getDetail`, params);
-};
 export const getMenuByStoreId = params => {
   return http.get(PORT_NONE + `/menuPlatform/getMenuByStoreId`, params);
 };

+ 4 - 7
src/api/modules/voucherManagement.ts

@@ -5,11 +5,11 @@ import http from "@/api";
 export const getThaliList = params => {
   return http.get<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/PcGroupBuy/getThaliList`, params);
 };
-export const deleteThali = (params: { id: string }) => {
-  return http.delete(PORT_NONE + `/PcGroupBuy/deleteThali`, params);
+export const delThaliById = (params: { id: string }) => {
+  return http.get(PORT_NONE + `/PcGroupBuy/deleteThali`, params);
 };
-export const sjxj = params => {
-  return http.get(PORT_NONE + `/PcGroupBuy/sjxj`, params);
+export const updateStatus = params => {
+  return http.get(PORT_NONE + `/PcGroupBuy/updateStatus1`, params);
 };
 export const updateNum = params => {
   return http.get(PORT_NONE + `/PcGroupBuy/updateNum11`, params);
@@ -17,9 +17,6 @@ export const updateNum = params => {
 export const saveDraft = params => {
   return http.post(PORT_NONE + `/PcGroupBuy/saveDraft`, params);
 };
-export const getUserByPhone = params => {
-  return http.get(PORT_NONE + `/store/user/getUserByPhone`, params);
-};
 export const getVoucherDetail = params => {
   return http.get(PORT_NONE + `/store/info/getDetail`, params);
 };

BIN
src/assets/images/1logo.png


BIN
src/assets/images/baojia.png


+ 0 - 33
src/assets/images/login_bg.svg

@@ -1,33 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="100%" height="100%" viewBox="0 0 1400 800">
-
-  <rect x="1300" y="400" rx="40" ry="40" width="150" height="150" stroke="rgb(129, 201, 149)" fill="rgb(129, 201, 149)">
-    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="0 1450 550" to="360 1450 550" repeatCount="indefinite"/>
-  </rect>
-
-  <path d="M 100 350 A 150 150 0 1 1 400 350 Q400 370 380 370 L 250 370 L 120 370 Q100 370 100 350" fill="#a2b3ff">
-    <animateMotion path="M 800 -200 L 800 -300 L 800 -200" dur="20s" begin="0s" repeatCount="indefinite"/>
-    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 210 530 ; -30 210 530 ; 0 210 530" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
-  </path>
-
-  <circle cx="150" cy="150" r="180" stroke="#85FFBD" fill="#85FFBD">
-    <animateMotion path="M 0 0 L 40 20 Z" dur="5s" repeatCount="indefinite"/>
-  </circle>
-
-  <!-- 三角形 -->
-  <path d="M 165 580 L 270 580 Q275 578 270 570 L 223 483 Q220 480 217 483 L 165 570 Q160 578 165 580"  fill="#a2b3ff">
-    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="0 210 530" to="360 210 530" repeatCount="indefinite"/>
-  </path>
-
-<!--  <circle cx="1200" cy="600" r="30" stroke="rgb(241, 243, 244)" fill="rgb(241, 243, 244)">-->
-<!--    <animateMotion path="M 0 0 L -20 40 Z" dur="9s" repeatCount="indefinite"/>-->
-<!--  </circle>-->
-
-  <path d="M 100 350 A 40 40 0 1 1 180 350 L 180 430 A 40 40 0 1 1 100 430 Z" fill="#3054EB">
-    <animateMotion path="M 140 390 L 180 360 L 140 390" dur="20s" begin="0s" repeatCount="indefinite"/>
-    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 140 390; -60 140 390; 0 140 390" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
-  </path>
-
-  <rect x="400" y="600" rx="40" ry="40" width="100" height="100" stroke="rgb(129, 201, 149)" fill="#3054EB">
-    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="-30 550 750" to="330 550 750" repeatCount="indefinite"/>
-  </rect>
-</svg>

BIN
src/assets/images/login_left.png


BIN
src/assets/images/login_left1.png


BIN
src/assets/images/login_left2.png


BIN
src/assets/images/login_left3.png


BIN
src/assets/images/login_left4.png


BIN
src/assets/images/login_left5.png


BIN
src/assets/images/msg01.png


BIN
src/assets/images/msg02.png


BIN
src/assets/images/msg03.png


BIN
src/assets/images/msg04.png


BIN
src/assets/images/msg05.png


BIN
src/assets/images/notData.png


+ 15 - 1
src/assets/json/authMenuList.json

@@ -16,6 +16,20 @@
       }
     },
     {
+      "path": "/home/notice",
+      "name": "notice",
+      "component": "/home/notice",
+      "meta": {
+        "icon": "List",
+        "title": "系统通知",
+        "isLink": "",
+        "isHide": true,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      }
+    },
+    {
       "path": "/groupPackageManagement",
       "name": "groupPackageManagementIndex",
       "component": "/groupPackageManagement/index",
@@ -376,7 +390,7 @@
           "component": "/financialManagement/index",
           "meta": {
             "icon": "CreditCard",
-            "title": "财务管理",
+            "title": "账户总览",
             "isLink": "",
             "isHide": false,
             "isFull": false,

+ 2 - 2
src/components/ProTable/index.vue

@@ -82,8 +82,8 @@
       <template #empty>
         <div class="table-empty">
           <slot name="empty">
-            <img src="@/assets/images/notData.png" alt="notData" />
-            <div>暂无数据</div>
+            <img src="@/assets/images/notData.png" alt="notData" style=" width: 180px;height: 180px" />
+            <div style=" font-size: 14px;font-weight: 500; color: #aaaaaa">暂无数据</div>
           </slot>
         </div>
       </template>

+ 8 - 3
src/components/SearchForm/index.vue

@@ -18,10 +18,10 @@
         </GridItem>
         <GridItem suffix>
           <div class="operation">
-            <el-button type="primary" :icon="Search" @click="search"> 搜索 </el-button>
-            <el-button :icon="Delete" @click="reset"> 重置 </el-button>
+            <el-button type="primary" @click="search"> 搜索 </el-button>
+            <el-button @click="reset"> 重置 </el-button>
             <el-button v-if="showCollapse" type="primary" link class="search-isOpen" @click="collapsed = !collapsed">
-              {{ collapsed ? "展开" : "合并" }}
+              {{ collapsed ? "展开" : "收起" }}
               <el-icon class="el-icon--right">
                 <component :is="collapsed ? ArrowDown : ArrowUp" />
               </el-icon>
@@ -92,3 +92,8 @@ const showCollapse = computed(() => {
   return show;
 });
 </script>
+<style lang="scss" scoped>
+.search-isOpen {
+  margin-left: 18px;
+}
+</style>

+ 1 - 1
src/config/index.ts

@@ -7,7 +7,7 @@ export const HOME_URL: string = "/home/index";
 export const LOGIN_URL: string = "/login";
 
 // 默认主题颜色
-export const DEFAULT_PRIMARY: string = "#409eff";
+export const DEFAULT_PRIMARY: string = "#6c8ff8";
 
 // 路由白名单地址(本地存在的路由 staticRouter.ts 中)
 export const ROUTER_WHITE_LIST: string[] = [

+ 4 - 1
src/layouts/components/Header/components/InfoDialog.vue

@@ -37,9 +37,12 @@
 import { ref, reactive } from "vue";
 import { ElMessage, type FormInstance, type FormRules } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
+import { getMerchantByPhone } from "@/api/modules/homeEntry.ts";
 
 const dialogVisible = ref(false);
-const openDialog = () => {
+const openDialog = async () => {
+  const res = await getMerchantByPhone({ phone: "15242687180" });
+  console.log(res);
   dialogVisible.value = true;
 };
 

+ 22 - 1
src/stores/modules/auth.ts

@@ -2,6 +2,7 @@ import { defineStore } from "pinia";
 import { AuthState } from "@/stores/interface";
 import { getAuthButtonListApi, getAuthMenuListApi } from "@/api/modules/login";
 import { getFlatMenuList, getShowMenuList, getAllBreadcrumbList } from "@/utils";
+import { usePermission } from "@/utils/permission";
 export const useAuthStore = defineStore({
   id: "geeker-auth",
   state: (): AuthState => ({
@@ -33,7 +34,27 @@ export const useAuthStore = defineStore({
     // Get AuthMenuList
     async getAuthMenuList() {
       const { data } = (await getAuthMenuListApi()) as any;
-      console.log(data, "ddd-000d");
+
+      const hasPermission = await usePermission();
+      if (!hasPermission) {
+        // 根据权限隐藏"门店装修"和"财务管理"菜单
+        const hideMenuNames = ["storeDecoration", "financialManagement"];
+        // 递归查找并隐藏指定菜单
+        const hideMenus = (menuList: any[]) => {
+          menuList.forEach(menu => {
+            // 根据菜单名称判断是否需要隐藏
+            if (menu.name && hideMenuNames.includes(menu.name)) {
+              menu.meta.isHide = true;
+            }
+            // 递归处理子菜单
+            if (menu.children && menu.children.length > 0) {
+              hideMenus(menu.children);
+            }
+          });
+        };
+        hideMenus(data);
+      }
+
       this.authMenuList = data;
     },
     // Set RouteName

+ 420 - 1
src/utils/eleValidate.ts

@@ -13,7 +13,9 @@ export function checkPhoneNumber(rule: any, value: any, callback: any) {
   }
 }
 
-// 自定义校验函数:检查是否全是空格或为空
+/**
+ * 自定义校验函数:检查是否全是空格或为空
+ */
 export function validateNoEmptySpaces(rule: any, value: any, callback: any) {
   // 如果值为空或者去空格后为空字符串,则报错
   if (value === null || value === undefined || value.trim() === "") {
@@ -22,3 +24,420 @@ export function validateNoEmptySpaces(rule: any, value: any, callback: any) {
     callback(); // 校验通过
   }
 }
+
+/**
+ * 验证正整数
+ * @param errorMessage - 错误提示信息,默认为"必须为正整数"
+ * @param options - 配置选项
+ * @param options.required - 是否必填,默认为true
+ * @param options.checkLeadingZero - 是否检查前导零,默认为true(用于字符串输入)
+ * @returns 验证函数
+ */
+export function validatePositiveInteger(
+  errorMessage: string = "必须为正整数",
+  options: { required?: boolean; checkLeadingZero?: boolean } = {}
+) {
+  const { required = true, checkLeadingZero = true } = options;
+
+  return (rule: any, value: any, callback: any) => {
+    // 检查是否为空
+    if (value === null || value === undefined || value === "") {
+      if (required) {
+        callback(new Error(errorMessage));
+        return;
+      } else {
+        callback();
+        return;
+      }
+    }
+
+    // 对于数字类型(如 el-input-number),直接验证数字
+    if (typeof value === "number") {
+      if (value <= 0 || !Number.isInteger(value)) {
+        callback(new Error(errorMessage));
+        return;
+      }
+      callback();
+      return;
+    }
+
+    // 转换为字符串进行检查
+    const strValue = value.toString().trim();
+    if (strValue === "") {
+      if (required) {
+        callback(new Error(errorMessage));
+        return;
+      } else {
+        callback();
+        return;
+      }
+    }
+
+    // 检查是否有前导零(除了单独的"0")
+    if (checkLeadingZero && strValue.length > 1 && strValue.startsWith("0")) {
+      callback(new Error(errorMessage));
+      return;
+    }
+
+    // 检查是否为数字
+    const num = Number(strValue);
+    if (isNaN(num)) {
+      callback(new Error(errorMessage));
+      return;
+    }
+
+    // 检查是否为正数
+    if (num <= 0) {
+      callback(new Error(errorMessage));
+      return;
+    }
+
+    // 检查是否为整数
+    if (!Number.isInteger(num)) {
+      callback(new Error(errorMessage));
+      return;
+    }
+
+    callback();
+  };
+}
+
+/**
+ * 验证正数(可以是小数)
+ * @param errorMessage - 错误提示信息,默认为"必须为正数"
+ * @returns 验证函数
+ */
+export function validatePositiveNumber(errorMessage: string = "必须为正数") {
+  return (rule: any, value: any, callback: any) => {
+    if (!value || value.toString().trim() === "") {
+      callback();
+      return;
+    }
+    const num = Number(value);
+    if (isNaN(num) || num <= 0) {
+      callback(new Error(errorMessage));
+      return;
+    }
+    callback();
+  };
+}
+
+/**
+ * 验证密码(6-16位,必须包含字母和数字)
+ */
+export function validatePassword(rule: any, value: string, callback: any) {
+  if (!value) {
+    callback(new Error("请输入密码"));
+    return;
+  }
+  if (value.length < 6 || value.length > 16) {
+    callback(new Error("密码长度为6-16位,密码必须包含字母和数字"));
+    return;
+  }
+  if (!/^(?=.*[a-zA-Z])(?=.*\d).+$/.test(value)) {
+    callback(new Error("密码长度为6-16位,密码必须包含字母和数字"));
+    return;
+  }
+  callback();
+}
+
+/**
+ * 验证确认密码(两次输入的密码必须一致)
+ * @param originalPassword - 原始密码值或获取原始密码的函数
+ * @param errorMessage - 错误提示信息,默认为"两次输入的密码不一致"
+ * @returns 验证函数
+ */
+export function validateConfirmPassword(
+  originalPassword: string | (() => string),
+  errorMessage: string = "两次输入的密码不一致"
+) {
+  return (rule: any, value: string, callback: any) => {
+    if (!value) {
+      callback(new Error("请确认密码"));
+      return;
+    }
+    const original = typeof originalPassword === "function" ? originalPassword() : originalPassword;
+    if (value !== original) {
+      callback(new Error(errorMessage));
+      return;
+    }
+    callback();
+  };
+}
+
+/**
+ * 验证日期不能早于今天
+ * @param errorMessage - 错误提示信息,默认为"日期不能早于当前时间"
+ * @returns 验证函数
+ */
+export function validateDateNotBeforeToday(errorMessage: string = "日期不能早于当前时间") {
+  return (rule: any, value: any, callback: any) => {
+    if (!value) {
+      callback();
+      return;
+    }
+    const selectedDate = new Date(value);
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    if (selectedDate < today) {
+      callback(new Error(errorMessage));
+      return;
+    }
+    callback();
+  };
+}
+
+/**
+ * 验证日期范围(开始时间必须早于结束时间)
+ * @param getStartDate - 获取开始日期的函数
+ * @param getEndDate - 获取结束日期的函数(可选,如果不提供则使用当前值作为结束日期)
+ * @param startErrorMessage - 开始日期错误提示,默认为"开始时间不能早于当前时间"
+ * @param endErrorMessage - 结束日期错误提示,默认为"结束时间不能早于当前时间"
+ * @param rangeErrorMessage - 日期范围错误提示,默认为"开始时间必须早于结束时间"
+ * @param checkToday - 是否检查不能早于今天,默认为true
+ * @returns 验证函数
+ */
+export function validateDateRange(
+  getStartDate: () => any,
+  getEndDate?: () => any,
+  startErrorMessage: string = "开始时间不能早于当前时间",
+  endErrorMessage: string = "结束时间不能早于当前时间",
+  rangeErrorMessage: string = "开始时间必须早于结束时间",
+  checkToday: boolean = true
+) {
+  return (rule: any, value: any, callback: any, isStartDate: boolean = true) => {
+    if (!value) {
+      callback();
+      return;
+    }
+    const selectedDate = new Date(value);
+
+    // 验证不能早于今天
+    if (checkToday) {
+      const today = new Date();
+      today.setHours(0, 0, 0, 0);
+      if (selectedDate < today) {
+        callback(new Error(isStartDate ? startErrorMessage : endErrorMessage));
+        return;
+      }
+    }
+
+    // 验证日期范围
+    if (isStartDate && getEndDate) {
+      const endDate = getEndDate();
+      if (endDate) {
+        const end = new Date(endDate);
+        if (selectedDate >= end) {
+          callback(new Error(rangeErrorMessage));
+          return;
+        }
+      }
+    } else if (!isStartDate && getStartDate) {
+      const startDate = getStartDate();
+      if (startDate) {
+        const start = new Date(startDate);
+        if (selectedDate <= start) {
+          callback(new Error(rangeErrorMessage));
+          return;
+        }
+      }
+    }
+
+    callback();
+  };
+}
+
+/**
+ * 验证正整数(简化版,用于库存、数量等字段,允许空值)
+ * @param errorMessage - 错误提示信息,默认为"必须为正整数"
+ * @returns 验证函数
+ * @deprecated 请使用 validatePositiveInteger(errorMessage, { required: false })
+ */
+export function validatePositiveIntegerOptional(errorMessage: string = "必须为正整数") {
+  return validatePositiveInteger(errorMessage, { required: false, checkLeadingZero: true });
+}
+
+/**
+ * 验证日期范围(开始时间必须早于结束时间,且不能早于今天)
+ * @param getStartDate - 获取开始日期的函数
+ * @param getEndDate - 获取结束日期的函数(可选,如果不提供则使用当前值作为结束日期)
+ * @param startErrorMessage - 开始日期错误提示,默认为"开始时间不能早于当前时间"
+ * @param endErrorMessage - 结束日期错误提示,默认为"结束时间不能早于当前时间"
+ * @param rangeErrorMessage - 日期范围错误提示,默认为"开始时间必须早于结束时间"
+ * @returns 验证函数
+ * @deprecated 请使用 validateDateRange(..., true) 或 validateDateRange(..., checkToday: true)
+ */
+export function validateDateRangeWithToday(
+  getStartDate: () => any,
+  getEndDate?: () => any,
+  startErrorMessage: string = "开始时间不能早于当前时间",
+  endErrorMessage: string = "结束时间不能早于当前时间",
+  rangeErrorMessage: string = "开始时间必须早于结束时间"
+) {
+  return validateDateRange(getStartDate, getEndDate, startErrorMessage, endErrorMessage, rangeErrorMessage, true);
+}
+
+/**
+ * 验证日期范围数组(每个日期范围的开始时间必须早于结束时间)
+ * @param errorMessage - 错误提示信息,默认为"开始时间必须早于结束时间"
+ * @param checkToday - 是否检查不能早于今天,默认为false
+ * @param todayErrorMessage - 今天错误提示,默认为"时间不能早于当前时间"
+ * @returns 验证函数
+ */
+export function validateDateRangeArray(
+  errorMessage: string = "开始时间必须早于结束时间",
+  checkToday: boolean = false,
+  todayErrorMessage: string = "时间不能早于当前时间"
+) {
+  return (rule: any, value: any, callback: any) => {
+    if (!value || value.length === 0) {
+      callback();
+      return;
+    }
+    if (!Array.isArray(value) || value.length !== 2) {
+      callback(new Error("请选择完整的日期范围"));
+      return;
+    }
+
+    const startDate = new Date(value[0]);
+    const endDate = new Date(value[1]);
+
+    // 验证不能早于今天
+    if (checkToday) {
+      const today = new Date();
+      today.setHours(0, 0, 0, 0);
+      if (startDate < today) {
+        callback(new Error("开始" + todayErrorMessage));
+        return;
+      }
+      if (endDate < today) {
+        callback(new Error("结束" + todayErrorMessage));
+        return;
+      }
+    }
+
+    // 验证开始时间必须早于结束时间
+    if (startDate >= endDate) {
+      callback(new Error(errorMessage));
+      return;
+    }
+    callback();
+  };
+}
+
+/**
+ * 验证日期范围数组(带今天检查)
+ * @param checkToday - 是否检查不能早于今天,默认为true
+ * @param rangeErrorMessage - 日期范围错误提示,默认为"开始时间必须早于结束时间"
+ * @param todayErrorMessage - 今天错误提示,默认为"时间不能早于当前时间"
+ * @returns 验证函数
+ * @deprecated 请使用 validateDateRangeArray(rangeErrorMessage, checkToday, todayErrorMessage)
+ */
+export function validateDateRangeArrayWithToday(
+  checkToday: boolean = true,
+  rangeErrorMessage: string = "开始时间必须早于结束时间",
+  todayErrorMessage: string = "时间不能早于当前时间"
+) {
+  return validateDateRangeArray(rangeErrorMessage, checkToday, todayErrorMessage);
+}
+
+/**
+ * 条件必填验证(根据某个条件判断是否必填)
+ * @param condition - 判断条件的函数,返回true表示必填
+ * @param errorMessage - 错误提示信息
+ * @returns 验证函数
+ */
+export function validateConditionalRequired(condition: () => boolean, errorMessage: string) {
+  return (rule: any, value: any, callback: any) => {
+    if (condition()) {
+      if (!value || (Array.isArray(value) && value.length === 0) || (typeof value === "string" && value.trim() === "")) {
+        callback(new Error(errorMessage));
+        return;
+      }
+    }
+    callback();
+  };
+}
+
+/**
+ * 验证数组最小长度
+ * @param minLength - 最小长度
+ * @param errorMessage - 错误提示信息
+ * @returns 验证函数
+ */
+export function validateArrayMinLength(minLength: number, errorMessage: string) {
+  return (rule: any, value: any, callback: any) => {
+    if (!value || !Array.isArray(value) || value.length < minLength) {
+      callback(new Error(errorMessage));
+      return;
+    }
+    callback();
+  };
+}
+
+/**
+ * 验证日期数组列表(每个日期项的开始时间必须早于结束时间)
+ * @param getDateList - 获取日期列表的函数
+ * @param itemErrorMessage - 单个日期项错误提示,默认为"日期项未完整填写"
+ * @param rangeErrorMessage - 日期范围错误提示,默认为"开始时间必须早于结束时间"
+ * @param checkToday - 是否检查不能早于今天,默认为false
+ * @returns 验证函数
+ */
+export function validateDateListArray(
+  getDateList: () => any[],
+  itemErrorMessage: string = "日期项未完整填写",
+  rangeErrorMessage: string = "开始时间必须早于结束时间",
+  checkToday: boolean = false
+) {
+  return (rule: any, value: any, callback: any) => {
+    const dateList = getDateList();
+    if (!dateList || dateList.length === 0) {
+      callback();
+      return;
+    }
+
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+
+    for (let i = 0; i < dateList.length; i++) {
+      const dateItem = dateList[i];
+      if (!dateItem || !Array.isArray(dateItem) || dateItem.length !== 2) {
+        callback(new Error(`第${i + 1}个${itemErrorMessage}`));
+        return;
+      }
+
+      const startDate = new Date(dateItem[0]);
+      const endDate = new Date(dateItem[1]);
+
+      // 验证不能早于今天
+      if (checkToday) {
+        if (startDate < today) {
+          callback(new Error(`第${i + 1}个日期项的开始时间不能早于当前时间`));
+          return;
+        }
+        if (endDate < today) {
+          callback(new Error(`第${i + 1}个日期项的结束时间不能早于当前时间`));
+          return;
+        }
+      }
+
+      // 验证开始时间必须早于结束时间
+      if (startDate >= endDate) {
+        callback(new Error(`第${i + 1}个日期项的${rangeErrorMessage}`));
+        return;
+      }
+    }
+
+    callback();
+  };
+}
+
+/**
+ * 验证正整数(用于 el-input-number,value 已经是数字类型)
+ * @param errorMessage - 错误提示信息,默认为"必须为正整数"
+ * @returns 验证函数
+ * @deprecated 请使用 validatePositiveInteger(errorMessage, { required: false, checkLeadingZero: false })
+ */
+export function validatePositiveIntegerForNumber(errorMessage: string = "必须为正整数") {
+  return validatePositiveInteger(errorMessage, { required: false, checkLeadingZero: false });
+}

+ 3 - 0
src/utils/index.ts

@@ -340,3 +340,6 @@ export function findItemNested(enumData: any, callValue: any, value: string, chi
     if (current[children]) return findItemNested(current[children], callValue, value, children);
   }, null);
 }
+
+// 导出权限判断相关函数
+export { usePermission } from "./permission";

+ 43 - 0
src/utils/permission.ts

@@ -0,0 +1,43 @@
+import { localGet, localSet } from "@/utils/index";
+import { ElMessage } from "element-plus";
+import { getUserByPhone, getDetail } from "@/api/modules/homeEntry";
+
+/**
+ * @description 判断是否有操作权限
+ * @returns {Boolean} 是否有权限
+ */
+export async function usePermission(tip?: string) {
+  let type = true;
+  if (!localGet("createdId")) {
+    let params = {
+      phone: localGet("iphone") || "18641153170"
+    };
+    const res: any = await getUserByPhone(params);
+    if (res.data && res.data.storeId) {
+      localSet("createdId", res.data.storeId);
+      const resD: any = await getDetail({
+        id: res.data.storeId
+      });
+      if (resD.data && resD.data.commissionRate) {
+        localSet("commissionRate", resD.data.commissionRate);
+      }
+      if (resD.data && resD.data.businessSection) {
+        localSet("businessSection", resD.data.businessSection);
+      }
+    } else {
+      type = false;
+      if (tip) {
+        ElMessage.warning(`请完成商家入驻后再进行${tip}`);
+      }
+      return type;
+    }
+    if (!localGet("businessSection")) {
+      type = false;
+      if (tip) {
+        ElMessage.warning(`请完成商家入驻后重新登录再进行${tip}`);
+      }
+      return type;
+    }
+  }
+  return type;
+}

+ 22 - 14
src/views/couponManagement/index.vue

@@ -4,7 +4,7 @@
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <div class="table-header-btn">
-          <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying"> 新建优惠券 </el-button>
+          <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying" v-if="type"> 新建优惠券 </el-button>
           <el-tabs v-if="showTabs" v-model="activeName" class="tabs" @tab-click="handleClick">
             <el-tab-pane v-for="tab in filteredTabOptions" :key="tab.name" :label="tab.label" :name="tab.name" />
           </el-tabs>
@@ -15,7 +15,7 @@
         <el-button
           link
           type="primary"
-          @click="changeTypes(scope.row, 'on')"
+          @click="changeTypes(scope.row, 5)"
           v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.上架)"
         >
           上架
@@ -23,7 +23,7 @@
         <el-button
           link
           type="primary"
-          @click="changeTypes(scope.row, 'off')"
+          @click="changeTypes(scope.row, 6)"
           v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.下架)"
         >
           下架
@@ -92,8 +92,9 @@ import { ElMessage } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { Plus } from "@element-plus/icons-vue";
-import { getThaliList, sjxj, updateNum, deleteThali } from "@/api/modules/couponManagement";
+import { getThaliList, updateStatus, updateNum, delThaliById } from "@/api/modules/couponManagement";
 import { ElMessageBox } from "element-plus/es";
+import { localGet, usePermission } from "@/utils";
 
 const router = useRouter();
 const dialogFormVisible = ref(false);
@@ -239,13 +240,14 @@ watch(
 );
 // 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
 const initParam = reactive({
-  storeId: "104",
-  groupType: "1",
+  storeId: localGet("createdId") || "",
+  groupType: localGet("businessSection") || "1",
   status: activeName
 });
-
+const type = ref(false);
 // 页面加载时触发查询
-onMounted(() => {
+onMounted(async () => {
+  type.value = await usePermission("新建优惠券");
   proTable.value?.getTableList();
 });
 
@@ -282,20 +284,24 @@ const editRow = row => {
 };
 const deleteRow = row => {
   ElMessageBox.confirm("确定要删除吗?", "提示", {
-    confirmButtonText: "OK",
-    cancelButtonText: "Cancel",
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
     type: "warning"
   }).then(() => {
-    deleteThali(row.id).then(() => {
+    const params = {
+      id: row.id,
+      groupType: localGet("businessSection") || "1"
+    };
+    delThaliById(params).then(() => {
       ElMessage.success("删除成功");
       proTable.value?.getTableList();
     });
   });
 };
 const handleClick = () => {};
-const changeTypes = async (row: any, status: string) => {
+const changeTypes = async (row, status) => {
   rowData.value = row;
-  let res = await sjxj({ id: row.id, status: status });
+  let res = await updateStatus({ id: row.id, status: status, approvalComments: "" });
   if (res && res.code == 200) {
     ElMessage.success("操作成功");
     proTable.value?.getTableList();
@@ -343,8 +349,10 @@ const closeDialog = () => {
   white-space: normal; // 允许自然换行
 }
 .table-header-btn {
+  .button {
+    margin-bottom: 10px;
+  }
   .tabs {
-    margin-top: 10px;
     :deep(.el-tabs__nav-wrap::after) {
       height: 0;
     }

+ 23 - 109
src/views/couponManagement/newCoupon.vue

@@ -106,6 +106,7 @@ import { ElMessage } from "element-plus";
 import { useRoute, useRouter } from "vue-router";
 import type { FormInstance } from "element-plus";
 import { getCouponDetail } from "@/api/modules/couponManagement";
+import { validatePositiveNumber, validatePositiveInteger, validateDateRange } from "@/utils/eleValidate";
 // ==================== 响应式数据定义 ====================
 
 // 路由相关
@@ -121,108 +122,49 @@ const rules = reactive({
   faceValue: [
     { required: true, message: "请输入面值" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.toString().trim() === "") {
-          callback();
-          return;
-        }
-        const num = Number(value);
-        if (isNaN(num) || num <= 0) {
-          callback(new Error("面值必须为正数"));
-          return;
-        }
-        callback();
-      },
+      validator: validatePositiveNumber("面值必须为正数"),
       trigger: "blur"
     }
   ],
   startCollectionTime: [
     { required: true, message: "请选择开始领取时间" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value) {
-          callback();
-          return;
-        }
-        const selectedDate = new Date(value);
-        const today = new Date();
-        today.setHours(0, 0, 0, 0);
-        if (selectedDate < today) {
-          callback(new Error("开始领取时间不能早于当前时间"));
-          return;
-        }
-        if (couponModel.value.endCollectionTime) {
-          const endDate = new Date(couponModel.value.endCollectionTime);
-          if (selectedDate >= endDate) {
-            callback(new Error("开始领取时间必须早于结束领取时间"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateDateRange(
+        () => couponModel.value.startCollectionTime,
+        () => couponModel.value.endCollectionTime,
+        "开始领取时间不能早于当前时间",
+        "结束领取时间不能早于当前时间",
+        "开始领取时间必须早于结束领取时间",
+        true
+      ),
       trigger: "change"
     }
   ],
   endCollectionTime: [
     { required: true, message: "请选择结束领取时间" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value) {
-          callback();
-          return;
-        }
-        const selectedDate = new Date(value);
-        const today = new Date();
-        today.setHours(0, 0, 0, 0);
-        if (selectedDate < today) {
-          callback(new Error("结束领取时间不能早于当前时间"));
-          return;
-        }
-        if (couponModel.value.startCollectionTime) {
-          const startDate = new Date(couponModel.value.startCollectionTime);
-          if (selectedDate <= startDate) {
-            callback(new Error("结束领取时间必须晚于开始领取时间"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateDateRange(
+        () => couponModel.value.startCollectionTime,
+        () => couponModel.value.endCollectionTime,
+        "开始领取时间不能早于当前时间",
+        "结束领取时间不能早于当前时间",
+        "开始领取时间必须早于结束领取时间",
+        true
+      ),
       trigger: "change"
     }
   ],
   validityPeriod: [
     { required: true, message: "请输入有效期" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.toString().trim() === "") {
-          callback();
-          return;
-        }
-        const num = Number(value);
-        if (isNaN(num) || num <= 0 || !Number.isInteger(num)) {
-          callback(new Error("有效期必须为正整数"));
-          return;
-        }
-        callback();
-      },
+      validator: validatePositiveInteger("有效期必须为正整数", { required: false }),
       trigger: "blur"
     }
   ],
   inventory: [
     { required: true, message: "请输入库存" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.toString().trim() === "") {
-          callback();
-          return;
-        }
-        const num = Number(value);
-        if (isNaN(num) || num <= 0 || !Number.isInteger(num)) {
-          callback(new Error("库存必须为正整数"));
-          return;
-        }
-        callback();
-      },
+      validator: validatePositiveInteger("库存必须为正整数", { required: false }),
       trigger: "blur"
     }
   ],
@@ -234,13 +176,10 @@ const rules = reactive({
             callback(new Error("请输入最低消费金额"));
             return;
           }
-          const num = Number(value);
-          if (isNaN(num) || num <= 0) {
-            callback(new Error("最低消费金额必须为正数"));
-            return;
-          }
+          validatePositiveNumber("最低消费金额必须为正数")(rule, value, callback);
+        } else {
+          callback();
         }
-        callback();
       },
       trigger: "blur"
     }
@@ -329,31 +268,6 @@ watch(
 onMounted(async () => {
   id.value = (route.query.id as string) || "";
   type.value = (route.query.type as string) || "";
-  // 不要删除-开始
-  // let param = {
-  //   // phone: localGet("iphone")
-  //   phone: "18641153170"
-  // };
-  // const resP: any = await getUserByPhone(param);
-  // if (resP.data && resP.data.storeId) {
-  //   localSet("createdId", resP.data.storeId);
-  //   const resD: any = await getDetail({
-  //     id: localGet("createdId")
-  //   });
-  //   if (resD.data && resD.data.commissionRate) {
-  //     localSet("commissionRate", resD.data.commissionRate);
-  //   }
-  //   if (resD.data && resD.data.businessSection) {
-  //     localSet("businessSection", resD.data.businessSection);
-  //   }
-  // } else {
-  //   ElMessage.warning("请完成商家入驻后再进行新建团购");
-  // }
-  // if (!getGroupCombination()) {
-  //   ElMessage.warning("请完成商家入驻后重新登录再进行新建团购");
-  //   return;
-  // }
-  // 不要删除-结束
   if (type.value != "add") {
     // TODO: 加载优惠券详情数据
     // let res: any = await getCouponDetail({ id: id.value });

+ 184 - 180
src/views/financialManagement/index.vue

@@ -1,203 +1,207 @@
 <template>
-  <div class="table-box">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
-      <!-- 表格 header 按钮 -->
-      <template #tableHeader="scope">
-        <el-button type="primary" :icon="Download" @click="exportInfoExcel(scope)"> 导出 </el-button>
-      </template>
-      <template #expand="scope">
-        {{ scope.row }}
-      </template>
-      <!-- 表格操作 -->
-      <template #operation="scope">
-        <!-- 审批通过和拒绝按钮仅在状态为0时显示 -->
-        <template v-if="scope.row.status === '0'">
-          <el-button type="primary" link @click="changeTypes(scope.row, 'pass')"> 审核通过 </el-button>
-          <el-button type="primary" link @click="changeTypes(scope.row, '')"> 审核拒绝 </el-button>
-        </template>
-        <el-button type="primary" link @click="toDetail(scope.row)"> 查看详情 </el-button>
-      </template>
-    </ProTable>
-
-    <el-dialog v-model="dialogFormVisible" title="审核拒绝" width="500">
-      <el-form :model="form">
-        <el-form-item label="" label-width="0">
-          <el-input v-model="form.comment" autocomplete="off" type="textarea" maxlength="200" />
-        </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 class="financial-dashboard">
+    <h3 class="title"><el-image :src="homeIcon" class="homeIcon" />账户总览</h3>
+    <el-row :gutter="16">
+      <el-col v-for="card in cards" :key="card.key" :span="5">
+        <div class="summary-card" :class="{ 'is-disabled': card.disabled }">
+          <div class="card-header">
+            <div class="header-left">
+              <span class="card-title">{{ card.title }}</span>
+              <el-tooltip v-if="card.tooltip" :content="card.tooltip" placement="top">
+                <el-icon class="info-icon">
+                  <QuestionFilled />
+                </el-icon>
+              </el-tooltip>
+            </div>
+            <el-icon v-if="card.showArrow" class="arrow-icon" @click="handleAction(card)">
+              <ArrowRight />
+            </el-icon>
+          </div>
+          <div class="card-amount">¥{{ formatAmount(card.amount) }}</div>
+          <div class="card-button-wrapper">
+            <el-button
+              v-if="card.buttonText"
+              class="card-button"
+              size="large"
+              :type="card.disabled ? 'default' : 'primary'"
+              :plain="card.disabled"
+              :disabled="card.disabled"
+              @click="handleAction(card)"
+            >
+              {{ card.buttonText }}
+            </el-button>
+          </div>
         </div>
-      </template>
-    </el-dialog>
+      </el-col>
+    </el-row>
   </div>
 </template>
 
-<script setup lang="tsx" name="financialManagement">
-import { ref, reactive, onMounted, onActivated } from "vue";
-import { useRouter } from "vue-router";
-import { ElMessage } from "element-plus";
-import ProTable from "@/components/ProTable/index.vue";
-import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
-import { Download } from "@element-plus/icons-vue";
-import { audit, exportExcelStaffConfig, getStaffConfigList } from "@/api/modules/staffConfig";
-
-const router = useRouter();
-const dialogFormVisible = ref(false);
-const form = reactive({
-  comment: ""
-});
-
-const rowData = ref<any>();
-
-const statusEnum = [
-  { value: "0", label: "待审核" },
-  { value: "1", label: "审核通过" },
-  { value: "2", label: "审核拒绝" }
-];
-// 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
-const initParam = reactive({});
-
-// 定义 filterValues
-const filterValues = reactive({});
-
-const getStatusObj = (statusValue: string) => {
-  const statusObj = statusEnum.find(item => item.value === statusValue);
-  if (statusObj) {
-    filterValues.status = statusObj;
-  } else {
-    filterValues.status = "";
-  }
-  return statusObj;
-};
-
-// ProTable 实例
-const proTable = ref<ProTableInstance>();
-
-// 页面加载时触发查询
-onMounted(() => {
-  proTable.value?.getTableList();
-});
-
-// 从其他页面返回时触发查询
-onActivated(() => {
-  proTable.value?.getTableList();
-});
+<script setup lang="ts" name="financialManagement">
+import homeIcon from "../../assets/images/home-icon.png";
 
-// dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 list && total 这些字段,可以在这里进行处理成这些字段
-// 或者直接去 hooks/useTable.ts 文件中把字段改为你后端对应的就行
-const dataCallback = (data: any) => {
-  return {
-    list: data.records,
-    total: data.total
-  };
-};
-
-// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
-// 默认不做操作就直接在 ProTable 组件上绑定	:requestApi="getUserList"
-const getTableList = (params: any) => {
-  let newParams = JSON.parse(JSON.stringify(params));
-  return getStaffConfigList(newParams);
+import { computed } from "vue";
+import { useRouter } from "vue-router";
+import { ArrowRight, QuestionFilled } from "@element-plus/icons-vue";
+
+type FinancialCard = {
+  key: string;
+  title: string;
+  amount: number;
+  tooltip?: string;
+  buttonText?: string;
+  disabled?: boolean;
+  showArrow?: boolean;
+  route?: string;
+  action?: () => void;
 };
 
-// 跳转详情页
-const toDetail = row => {
-  router.push(`/store/financialManagementDetail?id=${row.id}`);
-};
+const router = useRouter();
 
-// 表格配置项
-const columns = reactive<ColumnProps<any>[]>([
-  { type: "index", fixed: "left", label: "序号", width: 130 },
-  { prop: "storeName", label: "所属店铺" },
-  { prop: "name", label: "名称" },
-  { prop: "description", label: "描述" },
+const cards = computed<FinancialCard[]>(() => [
+  {
+    key: "todayIncome",
+    title: "今日收益",
+    amount: 0,
+    tooltip: "今日收益=今日已核销到账金额-技术服务费",
+    showArrow: true,
+    route: "/financialManagement/detail"
+  },
   {
-    prop: "status",
-    label: "状态",
-    render: scope => {
-      const statusObj = getStatusObj(scope.row.status);
-      return statusObj ? statusObj.label : "未知状态";
-    },
-    search: {
-      el: "select"
-    },
-    enum: statusEnum,
-    fieldNames: { label: "label", value: "value" }
+    key: "withdrawable",
+    title: "可提现金额",
+    amount: 0,
+    tooltip: "收益产生后3天可提现,28天自动",
+    buttonText: "提现",
+    disabled: true
   },
-  { prop: "operation", label: "操作", fixed: "right", width: 330 }
+  {
+    key: "arrivedAmount",
+    title: "已到账期金额",
+    amount: 0,
+    tooltip: "已完成入账的金额总计",
+    buttonText: "查看",
+    route: "/financialManagement/detail"
+  },
+  {
+    key: "pendingAmount",
+    title: "未到账期金额",
+    amount: 0,
+    tooltip: "正在处理中的预计入账金额",
+    buttonText: "查看",
+    route: "/financialManagement/detail"
+  }
 ]);
 
-const changeTypes = (row: any, status: string) => {
-  rowData.value = row;
-  if (status === "pass") {
-    handleChangeStatus(row, "1");
-  } else {
-    form.comment = "";
-    dialogFormVisible.value = true;
+const handleAction = (card: FinancialCard) => {
+  if (card.disabled) return;
+  if (card.action) {
+    card.action();
+    return;
+  }
+  if (card.route) {
+    router.push(card.route);
   }
 };
 
-const handleChangeStatus = async (row: any, status: string) => {
-  try {
-    let res = await audit({ id: row.id, status: status, rejectionReason: form.comment });
-    if (res.code === 200) {
-      proTable.value?.getTableList();
-      if (status === "2") closeDialog();
-      ElMessage.success("审核成功");
-    }
-  } catch (error) {
-    ElMessage.error("操作失败");
-  }
+const formatAmount = (value: number) => {
+  return value.toLocaleString("zh-CN", { minimumFractionDigits: 0, maximumFractionDigits: 2 });
 };
+</script>
 
-// 导出信息
-const exportInfoExcel = async scope => {
-  let res;
-  // 获取原始状态值(可能为数字、字符串或 undefined)
-  const rawStatus = proTable.value.searchParam.status;
-  // 转换为字符串(处理 undefined/null 为 "" 或保留原始字符串)
-  const statusParam = rawStatus !== undefined && rawStatus !== null ? String(rawStatus) : undefined;
-  // 将筛选条件作为参数传递给后台
-  res = await exportExcelStaffConfig({ status: statusParam });
-  if (res.code === 200) {
-    if (!res.data) {
-      ElMessage.error("暂无可下载数据");
-      return;
+<style scoped lang="scss">
+.financial-dashboard {
+  box-sizing: border-box;
+  width: 100%;
+  height: calc(100vh - 105px);
+  overflow: hidden;
+  background: url("../../assets/images/home-bg.png") center center no-repeat;
+  background-size: cover;
+  .title {
+    display: flex;
+    align-items: center;
+    height: 105px;
+    font-size: 20px;
+    font-weight: 600;
+    color: #ffffff;
+    background: linear-gradient(90deg, #6c8ff8 0%, rgb(255 255 255 / 0%) 100%);
+    .homeIcon {
+      width: 76px;
+      height: 76px;
+      margin-right: 8px;
+      margin-left: 25px;
     }
-    const exportFile = document.createElement("a");
-    exportFile.style.display = "none";
-    exportFile.download = `财务管理.xlsx`;
-    exportFile.href = `${res.data}?timestamp=${new Date().getTime()}`; // 添加时间戳防止缓存
-    document.body.appendChild(exportFile);
-    exportFile.click();
-    document.body.removeChild(exportFile);
-    ElMessage.success("下载成功");
-  }
-};
 
-// 弹窗提交
-const handleSubmit = () => {
-  if (!form.comment) {
-    ElMessage.error("请输入审批意见");
-    return;
+    border-radius: 10px;
+  }
+  :deep(.el-row) {
+    display: flex;
+    margin: 0 !important;
+  }
+  :deep(.el-col) {
+    display: flex;
+  }
+}
+.summary-card {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  justify-content: space-between;
+  height: 100%;
+  min-height: 148px;
+  padding: 20px 90px;
+  background-color: #ffffff;
+  border: 1px solid #e5e6eb;
+  border-radius: 8px;
+  transition: box-shadow 0.2s;
+  &:hover {
+    box-shadow: 0 6px 12px rgb(31 41 55 / 10%);
+  }
+  &.is-disabled {
+    .card-button {
+      cursor: not-allowed;
+    }
+  }
+}
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  .header-left {
+    display: inline-flex;
+    gap: 6px;
+    align-items: center;
+  }
+}
+.card-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #1d2129;
+}
+.card-amount {
+  font-size: 28px;
+  font-weight: 600;
+  line-height: 1.2;
+  color: #1d2129;
+}
+.card-button-wrapper {
+  display: flex;
+  align-items: flex-end;
+  min-height: 40px;
+}
+.card-button {
+  width: 100%;
+}
+.info-icon {
+  font-size: 16px;
+  color: #86909c;
+}
+.arrow-icon {
+  font-size: 18px;
+  color: #c9ccd3;
+  cursor: pointer;
+  &:hover {
+    color: #165dff;
   }
-  handleChangeStatus(rowData.value, "2");
-};
-// 关闭弹窗;
-const closeDialog = () => {
-  dialogFormVisible.value = false;
-  form.comment = "";
-};
-</script>
-
-<style lang="scss" scoped>
-// 在组件样式中添加
-.date-range {
-  display: block; // 确保换行生效
-  padding: 0 8px; // 可选:增加内边距
-  word-wrap: break-word; // 长单词内换行
-  white-space: normal; // 允许自然换行
 }
 </style>

+ 27 - 33
src/views/groupPackageManagement/index.vue

@@ -4,7 +4,7 @@
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <div class="table-header-btn">
-          <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying"> 新建团购 </el-button>
+          <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying" v-if="type"> 新建团购 </el-button>
           <el-tabs v-if="showTabs" v-model="activeName" class="tabs" @tab-click="handleClick">
             <el-tab-pane v-for="tab in filteredTabOptions" :key="tab.name" :label="tab.label" :name="tab.name" />
           </el-tabs>
@@ -35,7 +35,7 @@
         <el-button
           link
           type="primary"
-          @click="changeTypes(scope.row, 'on')"
+          @click="changeTypes(scope.row, 5)"
           v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.上架)"
         >
           上架
@@ -43,7 +43,7 @@
         <el-button
           link
           type="primary"
-          @click="changeTypes(scope.row, 'off')"
+          @click="changeTypes(scope.row, 6)"
           v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.下架)"
         >
           下架
@@ -127,13 +127,7 @@
         <div class="reject-reason-item">
           <div class="reject-reason-label">拒绝原因:</div>
           <div class="reject-reason-value reject-reason-text">
-            {{
-              rejectReasonData.rejectReason ||
-              rejectReasonData.rejectionReason ||
-              rejectReasonData.refuseReason ||
-              rejectReasonData.auditReason ||
-              "暂无拒绝原因"
-            }}
+            {{ rejectReasonData.approvalComments || "暂无拒绝原因" }}
           </div>
         </div>
       </div>
@@ -154,8 +148,8 @@ import { ElMessage } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { Plus } from "@element-plus/icons-vue";
-import { deleteThali, getThaliList, sjxj, updateNum } from "@/api/modules/groupPackageManagement";
-
+import { delThaliById, getThaliList, updateStatus, updateNum } from "@/api/modules/groupPackageManagement";
+import { localGet, usePermission } from "@/utils";
 const router = useRouter();
 const dialogFormVisible = ref(false);
 const formInventory: any = ref({
@@ -171,10 +165,7 @@ const rejectReasonDialogVisible = ref(false);
 const rejectReasonData = ref<any>({
   groupName: "",
   groupNo: "",
-  rejectReason: "",
-  rejectionReason: "",
-  refuseReason: "",
-  auditReason: ""
+  approvalComments: ""
 });
 
 // 定义表单类型
@@ -367,13 +358,14 @@ watch(
 );
 // 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
 const initParam = reactive({
-  storeId: "104",
-  groupType: "1",
+  storeId: localGet("createdId") || "",
+  groupType: localGet("businessSection") || "1",
   status: activeName
 });
-
+const type = ref(false);
 // 页面加载时触发查询
-onMounted(() => {
+onMounted(async () => {
+  type.value = await usePermission("新建团购");
   proTable.value?.getTableList();
 });
 
@@ -410,21 +402,24 @@ const editRow = row => {
 };
 const deleteRow = row => {
   ElMessageBox.confirm("确定要删除吗?", "提示", {
-    confirmButtonText: "OK",
-    cancelButtonText: "Cancel",
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
     type: "warning"
   }).then(() => {
-    deleteThali(row.id).then(() => {
+    const params = {
+      id: row.id,
+      groupType: localGet("businessSection") || "1"
+    };
+    delThaliById(params).then(() => {
       ElMessage.success("删除成功");
       proTable.value?.getTableList();
     });
   });
 };
-
 const handleClick = () => {};
-const changeTypes = async (row: any, status: string) => {
+const changeTypes = async (row, status) => {
   rowData.value = row;
-  let res = await sjxj({ id: row.id, status: status });
+  let res = await updateStatus({ id: row.id, status: status, approvalComments: "" });
   if (res && res.code == 200) {
     ElMessage.success("操作成功");
     proTable.value?.getTableList();
@@ -464,12 +459,9 @@ const closeDialog = () => {
 // 查看拒绝原因
 const viewRejectReason = (row: any) => {
   rejectReasonData.value = {
-    groupName: row.groupName || "",
-    groupNo: row.groupNo || "",
-    rejectReason: row.rejectReason || "",
-    rejectionReason: row.rejectionReason || "",
-    refuseReason: row.refuseReason || "",
-    auditReason: row.auditReason || ""
+    groupName: row.groupName || "--",
+    groupNo: row.groupNo || "--",
+    approvalComments: row.approvalComments || "--"
   };
   rejectReasonDialogVisible.value = true;
 };
@@ -496,8 +488,10 @@ const closeRejectReasonDialog = () => {
   white-space: normal; // 允许自然换行
 }
 .table-header-btn {
+  .button {
+    margin-bottom: 10px;
+  }
   .tabs {
-    margin-top: 10px;
     :deep(.el-tabs__nav-wrap::after) {
       height: 0;
     }

+ 323 - 193
src/views/groupPackageManagement/newGroup.vue

@@ -15,20 +15,31 @@
             <!-- 团购图片上传 prop="imageValueStr" 本地服务测不了上传图片 先去掉必填-->
             <el-form-item label="图片">
               <el-upload
+                ref="uploadRef"
                 v-model:file-list="storeInfoModel.imageValueStr"
-                :action="uploadUrl"
                 list-type="picture-card"
                 :accept="'.jpg,.png'"
-                :limit="9"
+                :limit="uploadMaxCount"
+                :auto-upload="false"
+                :disabled="hasUnuploadedImages"
+                multiple
+                :on-change="handleUploadChange"
+                :on-exceed="handleUploadExceed"
                 :on-preview="handlePictureCardPreview"
                 :before-remove="handleBeforeRemove"
                 :on-remove="handleRemove"
-                :on-success="handleSuccess"
                 :show-file-list="true"
               >
-                <el-icon>
-                  <Plus />
-                </el-icon>
+                <template #trigger>
+                  <div
+                    v-if="(storeInfoModel.imageId?.length || 0) < uploadMaxCount"
+                    class="upload-trigger-card el-upload--picture-card"
+                  >
+                    <el-icon>
+                      <Plus />
+                    </el-icon>
+                  </div>
+                </template>
               </el-upload>
             </el-form-item>
             <!-- 团购名称 -->
@@ -387,8 +398,8 @@
     </el-form>
     <!-- 底部按钮区域 -->
     <div class="button-container">
-      <el-button @click="handleSubmit('cg')"> 存草稿 </el-button>
-      <el-button type="primary" @click="handleSubmit()"> 确定 </el-button>
+      <el-button @click="handleSubmit('cg')" :disabled="hasUnuploadedImages"> 存草稿 </el-button>
+      <el-button type="primary" @click="handleSubmit()" :disabled="hasUnuploadedImages"> 确定 </el-button>
     </div>
     <!-- 图片预览 -->
     <el-image-viewer
@@ -452,18 +463,16 @@
 import { ref, reactive, onMounted, watch, nextTick, computed } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { Plus, Delete, ArrowDown, ArrowUp, Picture } from "@element-plus/icons-vue";
-import {
-  saveDraft,
-  getStoreDetail,
-  getHolidayList,
-  getUserByPhone,
-  getMenuByStoreId,
-  getThaliById,
-  saveThali
-} from "@/api/modules/groupPackageManagement";
+import { saveDraft, getHolidayList, getMenuByStoreId, getThaliById, saveThali } from "@/api/modules/groupPackageManagement";
 import { useRouter, useRoute } from "vue-router";
-import type { UploadProps, FormInstance } from "element-plus";
-import { localGet, localSet } from "@/utils";
+import type { UploadProps, FormInstance, UploadInstance, UploadFile } from "element-plus";
+import { localGet } from "@/utils";
+import {
+  validatePositiveInteger,
+  validateDateRangeArray,
+  validateConditionalRequired,
+  validateDateListArray
+} from "@/utils/eleValidate";
 
 // ==================== 响应式数据定义 ====================
 
@@ -481,7 +490,23 @@ const type = ref<string>(""); // 页面类型:add-新增, edit-编辑
 const id = ref<string>(""); // 页面ID参数
 
 // 文件上传地址
-const uploadUrl = ref(`${import.meta.env.VITE_API_URL_PLATFORM}/file/uploadImg`);
+const uploadUrl = ref(`${import.meta.env.VITE_API_URL_STORE}/file/uploadImg`);
+
+const imgType = ref(16);
+const uploadMaxCount = 9;
+const uploadRef = ref<UploadInstance>();
+const uploading = ref(false);
+const pendingUploadFiles = ref<UploadFile[]>([]);
+const generateImgSort = (() => {
+  let seed = Date.now();
+  return () => {
+    seed += 1;
+    return seed;
+  };
+})();
+
+// ==================== 验证辅助函数 ====================
+// 验证函数已从 @/utils/eleValidate 导入
 
 // ==================== 表单验证规则 ====================
 const rules = reactive({
@@ -549,22 +574,7 @@ const rules = reactive({
   inventoryNum: [
     { required: true, message: "请填写库存数量" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.toString().trim() === "") {
-          callback();
-          return;
-        }
-        const num = Number(value);
-        if (isNaN(num) || num <= 0) {
-          callback(new Error("库存数量必须为正整数"));
-          return;
-        }
-        if (!Number.isInteger(num)) {
-          callback(new Error("库存数量必须为正整数"));
-          return;
-        }
-        callback();
-      },
+      validator: validatePositiveInteger("库存数量必须为正整数", { required: false }),
       trigger: "blur"
     }
   ],
@@ -578,17 +588,10 @@ const rules = reactive({
             callback(new Error("请输入自定义限购数量"));
             return;
           }
-          const num = Number(value);
-          if (isNaN(num) || num <= 0) {
-            callback(new Error("自定义限购数量必须为正整数"));
-            return;
-          }
-          if (!Number.isInteger(num)) {
-            callback(new Error("自定义限购数量必须为正整数"));
-            return;
-          }
+          validatePositiveInteger("自定义限购数量必须为正整数")(rule, value, callback);
+        } else {
+          callback();
         }
-        callback();
       },
       trigger: "blur"
     }
@@ -654,17 +657,19 @@ const rules = reactive({
     {
       required: true,
       validator: (rule: any, value: any, callback: any) => {
-        if (storeInfoModel.value.expirationDate === 0) {
+        if (storeInfoModel.value.effectiveDateType === 0) {
           if (value === null || value === undefined || value === "") {
             callback(new Error("请输入用户购买天数"));
             return;
           }
-          if (value <= 0) {
-            callback(new Error("天数必须大于0"));
-            return;
-          }
+          validatePositiveInteger("用户购买天数必须为正整数", { required: false, checkLeadingZero: false })(
+            rule,
+            value,
+            callback
+          );
+        } else {
+          callback();
         }
-        callback();
       },
       trigger: "blur"
     }
@@ -678,26 +683,10 @@ const rules = reactive({
             callback(new Error("请选择指定时间段"));
             return;
           }
-          // 验证开始时间和结束时间不能早于当前时间
-          const today = new Date();
-          today.setHours(0, 0, 0, 0);
-          const startDate = new Date(value[0]);
-          const endDate = new Date(value[1]);
-          if (startDate < today) {
-            callback(new Error("开始时间不能早于当前时间"));
-            return;
-          }
-          if (endDate < today) {
-            callback(new Error("结束时间不能早于当前时间"));
-            return;
-          }
-          // 验证开始时间必须早于结束时间
-          if (startDate >= endDate) {
-            callback(new Error("开始时间必须早于结束时间"));
-            return;
-          }
+          validateDateRangeArray("开始时间必须早于结束时间", true, "时间不能早于当前时间")(rule, value, callback);
+        } else {
+          callback();
         }
-        callback();
       },
       trigger: "change"
     }
@@ -706,30 +695,14 @@ const rules = reactive({
   unavailableWeekdays: [
     {
       required: true,
-      validator: (rule: any, value: any, callback: any) => {
-        if (storeInfoModel.value.disableDateType === 1) {
-          if (!value || value.length === 0) {
-            callback(new Error("至少需要选择一个星期"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateConditionalRequired(() => storeInfoModel.value.disableDateType === 1, "至少需要选择一个星期"),
       trigger: "change"
     }
   ],
   unavailableHolidays: [
     {
       required: true,
-      validator: (rule: any, value: any, callback: any) => {
-        if (storeInfoModel.value.disableDateType === 1) {
-          if (!value || value.length === 0) {
-            callback(new Error("至少需要选择一个节日"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateConditionalRequired(() => storeInfoModel.value.disableDateType === 1, "至少需要选择一个节日"),
       trigger: "change"
     }
   ],
@@ -742,33 +715,15 @@ const rules = reactive({
             callback(new Error("至少需要添加一个自定义不可用日期"));
             return;
           }
-          const today = new Date();
-          today.setHours(0, 0, 0, 0);
-          // 验证每个日期项是否已填写
-          for (let i = 0; i < storeInfoModel.value.disableDateList.length; i++) {
-            if (!storeInfoModel.value.disableDateList[i] || storeInfoModel.value.disableDateList[i].length !== 2) {
-              callback(new Error(`第${i + 1}个日期项未完整填写`));
-              return;
-            }
-            // 验证开始时间和结束时间不能早于当前时间
-            const startDate = new Date(storeInfoModel.value.disableDateList[i][0]);
-            const endDate = new Date(storeInfoModel.value.disableDateList[i][1]);
-            if (startDate < today) {
-              callback(new Error(`第${i + 1}个日期项的开始时间不能早于当前时间`));
-              return;
-            }
-            if (endDate < today) {
-              callback(new Error(`第${i + 1}个日期项的结束时间不能早于当前时间`));
-              return;
-            }
-            // 验证开始时间必须早于结束时间
-            if (startDate >= endDate) {
-              callback(new Error(`第${i + 1}个日期项的开始时间必须早于结束时间`));
-              return;
-            }
-          }
+          validateDateListArray(
+            () => storeInfoModel.value.disableDateList,
+            "日期项未完整填写",
+            "开始时间必须早于结束时间",
+            true
+          )(rule, value, callback);
+        } else {
+          callback();
         }
-        callback();
       },
       trigger: "change"
     }
@@ -778,22 +733,7 @@ const rules = reactive({
   applicableNum: [
     { required: true, message: "请输入适用人数" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.toString().trim() === "") {
-          callback();
-          return;
-        }
-        const num = Number(value);
-        if (isNaN(num) || num <= 0) {
-          callback(new Error("适用人数必须为正整数"));
-          return;
-        }
-        if (!Number.isInteger(num)) {
-          callback(new Error("适用人数必须为正整数"));
-          return;
-        }
-        callback();
-      },
+      validator: validatePositiveInteger("适用人数必须为正整数", { required: false }),
       trigger: "blur"
     }
   ],
@@ -939,6 +879,21 @@ const visibleGroups = computed(() => {
   return lifeGroupBuyThalis.value.map((group, index) => ({ group, originalIndex: index }));
 });
 
+// 计算属性:检查是否有未上传完成的图片
+const hasUnuploadedImages = computed(() => {
+  // 检查是否有正在上传的文件
+  if (uploading.value || pendingUploadFiles.value.length > 0) {
+    return true;
+  }
+  // 检查文件列表中是否有状态为 "ready"(待上传)或 "uploading"(上传中)的图片
+  if (storeInfoModel.value.imageValueStr && storeInfoModel.value.imageValueStr.length > 0) {
+    return storeInfoModel.value.imageValueStr.some((file: any) => {
+      return file.status === "ready" || file.status === "uploading";
+    });
+  }
+  return false;
+});
+
 // ==================== 监听器 ====================
 
 /**
@@ -1017,31 +972,6 @@ watch(
 onMounted(async () => {
   id.value = (route.query.id as string) || "";
   type.value = (route.query.type as string) || "";
-  // 不要删除-开始
-  // let param = {
-  //   // phone: localGet("iphone")
-  //   phone: "18641153170"
-  // };
-  // const resP: any = await getUserByPhone(param);
-  // if (resP.data && resP.data.storeId) {
-  //   localSet("createdId", resP.data.storeId);
-  //   const resD: any = await getDetail({
-  //     id: localGet("createdId")
-  //   });
-  //   if (resD.data && resD.data.commissionRate) {
-  //     localSet("commissionRate", resD.data.commissionRate);
-  //   }
-  //   if (resD.data && resD.data.businessSection) {
-  //     localSet("businessSection", resD.data.businessSection);
-  //   }
-  // } else {
-  //   ElMessage.warning("请完成商家入驻后再进行新建团购");
-  // }
-  // if (!getGroupCombination()) {
-  //   ElMessage.warning("请完成商家入驻后重新登录再进行新建团购");
-  //   return;
-  // }
-  // 不要删除-结束
   let params = {
     year: new Date().getFullYear(),
     page: 1,
@@ -1129,6 +1059,26 @@ onMounted(async () => {
 const goBack = () => {
   router.go(-1);
 };
+const beforeAvatarUpload = (file: any) => {
+  console.log(file);
+  return false;
+};
+/**
+ * 检查文件是否在排队中(未上传)
+ * @param file 文件对象
+ * @returns 是否在排队中
+ */
+const isFilePending = (file: any): boolean => {
+  // 只检查 ready 状态(排队中),不包括 uploading(正在上传)
+  if (file.status === "ready") {
+    return true;
+  }
+  // 检查是否在待上传队列中
+  if (pendingUploadFiles.value.some(item => item.uid === file.uid)) {
+    return true;
+  }
+  return false;
+};
 
 /**
  * 图片上传 - 删除前确认
@@ -1137,6 +1087,11 @@ const goBack = () => {
  * @returns Promise<boolean>,true 允许删除,false 阻止删除
  */
 const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
+  // 如果文件在排队中(未上传),禁止删除
+  if (isFilePending(uploadFile)) {
+    ElMessage.warning("图片尚未上传,请等待上传完成后再删除");
+    return false;
+  }
   try {
     await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
       confirmButtonText: "确定",
@@ -1167,28 +1122,163 @@ const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
       storeInfoModel.value.imageId.splice(index, 1);
     }
   }
+  if (file.url && file.url.startsWith("blob:")) {
+    URL.revokeObjectURL(file.url);
+  }
+  // 同步文件列表
+  storeInfoModel.value.imageValueStr = [...uploadFiles];
   // 删除成功后提示
   ElMessage.success("图片已删除");
 };
 
 /**
- * 图片上传 - 上传成功回调
- * @param response 上传响应数据
- * @param uploadFile 上传的文件对象
- * @param uploadFiles 当前文件列表
+ * 上传文件超出限制提示
+ */
+const handleUploadExceed: UploadProps["onExceed"] = () => {
+  ElMessage.warning(`最多只能上传${uploadMaxCount}张图片`);
+};
+
+/**
+ * el-upload 文件变更(选中或移除)
+ */
+const handleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
+  // 检查文件类型,只允许 jpg 和 png
+  if (uploadFile.raw) {
+    const fileType = uploadFile.raw.type.toLowerCase();
+    const fileName = uploadFile.name.toLowerCase();
+    const validTypes = ["image/jpeg", "image/jpg", "image/png"];
+    const validExtensions = [".jpg", ".jpeg", ".png"];
+
+    // 检查 MIME 类型或文件扩展名
+    const isValidType = validTypes.includes(fileType) || validExtensions.some(ext => fileName.endsWith(ext));
+
+    if (!isValidType) {
+      // 从文件列表中移除不符合类型的文件
+      const index = storeInfoModel.value.imageValueStr.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (index > -1) {
+        storeInfoModel.value.imageValueStr.splice(index, 1);
+      }
+      // 从 uploadFiles 中移除
+      const uploadIndex = uploadFiles.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (uploadIndex > -1) {
+        uploadFiles.splice(uploadIndex, 1);
+      }
+      // 如果文件有 blob URL,释放它
+      if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
+        URL.revokeObjectURL(uploadFile.url);
+      }
+      ElMessage.warning("只支持上传 JPG 和 PNG 格式的图片");
+      return;
+    }
+  }
+
+  // 同步文件列表到表单数据(只添加通过验证的文件)
+  const existingIndex = storeInfoModel.value.imageValueStr.findIndex((f: any) => f.uid === uploadFile.uid);
+  if (existingIndex === -1) {
+    storeInfoModel.value.imageValueStr.push(uploadFile);
+  }
+
+  const readyFiles = storeInfoModel.value.imageValueStr.filter(file => file.status === "ready");
+  if (readyFiles.length) {
+    readyFiles.forEach(file => {
+      if (!pendingUploadFiles.value.some(item => item.uid === file.uid)) {
+        pendingUploadFiles.value.push(file);
+      }
+    });
+  }
+  processUploadQueue();
+};
+
+/**
+ * 处理上传队列 - 逐个上传文件
  */
-const handleSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
-  const imageId = response?.data[0];
-  // 将 imageId 添加到 storeInfoModel 的 imageId 数组中
-  if (!storeInfoModel.value.imageId.includes(imageId)) {
-    storeInfoModel.value.imageId.push(imageId);
+const processUploadQueue = async () => {
+  if (uploading.value || pendingUploadFiles.value.length === 0) {
+    return;
   }
-  // 将 imageId 保存到文件对象中,以便删除时使用
-  if (uploadFile) {
-    (uploadFile as any).imageId = imageId;
+  // 每次只取一个文件进行上传
+  const file = pendingUploadFiles.value.shift();
+  if (file) {
+    await uploadSingleFile(file);
+    // 继续处理队列中的下一个文件
+    processUploadQueue();
+  }
+};
+
+/**
+ * 单文件上传图片
+ * @param file 待上传的文件
+ */
+const uploadSingleFile = async (file: UploadFile) => {
+  if (!file.raw) {
+    return;
+  }
+
+  const formData = new FormData();
+  const storeId = Number(localGet("createdId") || 104);
+  const rawFile = file.raw as File;
+  const sortValue = generateImgSort();
+
+  formData.append("file", rawFile);
+  formData.append(
+    "list",
+    JSON.stringify([
+      {
+        storeId,
+        imgType: imgType.value,
+        imgSort: sortValue
+      }
+    ])
+  );
+
+  file.status = "uploading";
+  file.percentage = 0;
+  uploading.value = true;
+
+  try {
+    const response = await fetch(uploadUrl.value, {
+      method: "POST",
+      body: formData,
+      credentials: "include"
+    });
+    if (!response.ok) {
+      throw new Error("上传失败");
+    }
+    const result = await response.json();
+
+    if (result?.code === 200 && result.data) {
+      // 处理单个文件的上传结果
+      const imageId = String(Array.isArray(result.data) ? result.data[0] : result.data);
+      file.status = "success";
+      file.percentage = 100;
+      (file as any).imageId = imageId;
+      file.response = { data: imageId };
+
+      if (!Array.isArray(storeInfoModel.value.imageId)) {
+        storeInfoModel.value.imageId = [];
+      }
+      if (!storeInfoModel.value.imageId.includes(imageId)) {
+        storeInfoModel.value.imageId.push(imageId);
+      }
+    } else {
+      throw new Error(result?.msg || "图片上传失败");
+    }
+  } catch (error: any) {
+    file.status = "fail";
+    if (file.url && file.url.startsWith("blob:")) {
+      URL.revokeObjectURL(file.url);
+    }
+    // 从文件列表中移除失败的文件
+    const index = storeInfoModel.value.imageValueStr.findIndex((f: any) => f.uid === file.uid);
+    if (index > -1) {
+      storeInfoModel.value.imageValueStr.splice(index, 1);
+    }
+    ElMessage.error(error?.message || "图片上传失败");
+  } finally {
+    uploading.value = false;
+    // 触发视图更新
+    storeInfoModel.value.imageValueStr = [...storeInfoModel.value.imageValueStr];
   }
-  ElMessage.success("图片上传成功");
-  // 上传成功后,imageValueStr 会自动更新,response.data 包含图片URL
 };
 
 /**
@@ -1196,12 +1286,30 @@ const handleSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
  * @param file 上传文件对象
  */
 const handlePictureCardPreview = (file: any) => {
-  // 获取所有图片的 URL 列表
-  const urlList = storeInfoModel.value.imageValueStr.map((item: any) => item.url || item.response?.data || item);
+  // 如果文件在排队中(未上传),禁止预览
+  if (isFilePending(file)) {
+    ElMessage.warning("图片尚未上传,请等待上传完成后再预览");
+    return;
+  }
+  // 如果文件正在上传中,允许预览(使用本地预览)
+  if (file.status === "uploading" && file.url) {
+    imageViewerUrlList.value = [file.url];
+    imageViewerInitialIndex.value = 0;
+    imageViewerVisible.value = true;
+    return;
+  }
+  // 获取所有图片的 URL 列表(只包含已上传成功的图片)
+  const urlList = storeInfoModel.value.imageValueStr
+    .filter((item: any) => item.status === "success" && (item.url || item.response?.data))
+    .map((item: any) => item.url || item.response?.data);
   // 找到当前点击的图片索引
-  const currentIndex = urlList.findIndex((url: string) => url === file.url);
+  const currentIndex = urlList.findIndex((url: string) => url === (file.url || file.response?.data));
+  if (currentIndex < 0) {
+    ElMessage.warning("图片尚未上传完成,无法预览");
+    return;
+  }
   imageViewerUrlList.value = urlList;
-  imageViewerInitialIndex.value = currentIndex >= 0 ? currentIndex : 0;
+  imageViewerInitialIndex.value = currentIndex;
   imageViewerVisible.value = true;
 };
 
@@ -1565,7 +1673,7 @@ const openDishDialog = async (groupIndex: number, dishIndex: number) => {
   currentDishIndex.value = dishIndex;
   dishSearchKeyword.value = "";
   const params = {
-    storeId: 104,
+    storeId: localGet("createdId") || "104",
     phoneId: 18641153170,
     dishType: 0
   };
@@ -1722,22 +1830,9 @@ const packageFormRules = computed(() => {
             trigger: "blur"
           },
           {
-            validator: (rule: any, value: any, callback: any) => {
-              if (!value || value.toString().trim() === "") {
-                callback();
-                return;
-              }
-              const num = Number(value);
-              if (isNaN(num) || num <= 0) {
-                callback(new Error(`第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量必须为正整数`));
-                return;
-              }
-              if (!Number.isInteger(num)) {
-                callback(new Error(`第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量必须为正整数`));
-                return;
-              }
-              callback();
-            },
+            validator: validatePositiveInteger(`第${groupIndex + 1}个分组的第${dishIndex + 1}个菜品数量必须为正整数`, {
+              required: false
+            }),
             trigger: "blur"
           }
         ];
@@ -1752,6 +1847,11 @@ const packageFormRules = computed(() => {
  * 验证表单,通过后调用相应的API接口
  */
 const handleSubmit = async (type?) => {
+  // 检查是否有未上传完成的图片
+  if (hasUnuploadedImages.value) {
+    ElMessage.warning("请等待图片上传完成后再提交");
+    return;
+  }
   let params: any = { ...storeInfoModel.value };
   // 确保 imageId 是数组,然后转换为逗号分隔的字符串
   if (Array.isArray(params.imageId)) {
@@ -1770,8 +1870,8 @@ const handleSubmit = async (type?) => {
     lifeGroupBuyThalis: lifeGroupBuyThalis.value
   };
   paramsObj.lifeGroupBuyMain.status = type ? "0" : "1";
-  paramsObj.lifeGroupBuyMain.groupType = 1;
-  paramsObj.lifeGroupBuyMain.storeId = localGet("storeId") || "104";
+  paramsObj.lifeGroupBuyMain.groupType = localGet("businessSection") || "1";
+  paramsObj.lifeGroupBuyMain.storeId = localGet("createdId") || "104";
   if (id.value) {
     paramsObj.lifeGroupBuyMain.id = id.value;
   }
@@ -1812,6 +1912,7 @@ const handleSubmit = async (type?) => {
       let res: any = await saveThali(paramsObj);
       if (res && res.code == 200) {
         ElMessage.success("保存成功");
+        goBack();
       }
     });
   }
@@ -1971,6 +2072,35 @@ const handleImageParam = (list: any[], result: any[]) => {
       object-fit: fill;
     }
   }
+
+  /* 排队中(未上传)的图片禁用样式 */
+  .el-upload-list__item[data-status="ready"],
+  .el-upload-list__item.is-ready {
+    position: relative;
+    pointer-events: none;
+    cursor: not-allowed;
+    opacity: 0.6;
+    &::after {
+      position: absolute;
+      inset: 0;
+      z-index: 1;
+      content: "";
+      background-color: rgb(0 0 0 / 30%);
+    }
+    .el-upload-list__item-actions {
+      pointer-events: none;
+      opacity: 0.5;
+    }
+  }
+}
+.upload-trigger-card {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  font-size: 28px;
+  color: #8c939d;
 }
 
 /* 表单容器 */

+ 29 - 35
src/views/home/components/go-enter.vue

@@ -4,48 +4,18 @@
     <h3 class="title"><el-image :src="homeIcon" class="homeIcon" />免费入驻店铺</h3>
     <div class="steps-container">
       <el-steps align-center>
-        <el-step>
+        <el-step v-for="(item, index) in entryList" :key="index">
           <template #title>
             <div class="step-title-wrapper">
-              <span class="step-title">个人实名</span>
-              <span class="step-time">约3分钟</span>
+              <span class="step-title">{{ item.title }}</span>
+              <span class="step-time">{{ item.time }}</span>
             </div>
           </template>
           <template #description>
-            <div class="step-desc">填写店铺经营者姓名、身份证号等</div>
-          </template>
-        </el-step>
-        <el-step>
-          <template #title>
-            <div class="step-title-wrapper">
-              <span class="step-title">填写信息</span>
-              <span class="step-time">约30分钟</span>
+            <div class="step-desc">
+              {{ item.desc }}
             </div>
           </template>
-          <template #description>
-            <div class="step-desc">上传营业执照及填写店铺信息等</div>
-          </template>
-        </el-step>
-        <el-step>
-          <template #title>
-            <div class="step-title-wrapper">
-              <span class="step-title">等待审核</span>
-              <span class="step-time">约1-3个工作日</span>
-            </div>
-          </template>
-          <template #description>
-            <div class="step-desc">平台进行资质审核</div>
-          </template>
-        </el-step>
-        <el-step>
-          <template #title>
-            <div class="step-title-wrapper">
-              <span class="step-title">入驻成功</span>
-            </div>
-          </template>
-          <template #description>
-            <div class="step-desc">入驻成功后即可管理您的店铺</div>
-          </template>
         </el-step>
       </el-steps>
     </div>
@@ -55,7 +25,31 @@
   </div>
 </template>
 <script setup lang="ts">
+import { ref, defineProps, defineEmits } from "vue";
 import homeIcon from "../../../assets/images/home-icon.png";
+
+const entryList = ref([
+  {
+    title: "个人实名",
+    time: "约3分钟",
+    desc: "填写店铺经营者姓名、身份证号等"
+  },
+  {
+    title: "填写信息",
+    time: "约30分钟",
+    desc: "上传营业执照及填写店铺信息等"
+  },
+  {
+    title: "等待审核",
+    time: "约1-3个工作日",
+    desc: "平台进行资质审核"
+  },
+  {
+    title: "入驻成功",
+    desc: "入驻成功后即可管理您的店铺"
+  }
+]);
+
 const props = defineProps({
   currentStep: {
     type: Number,

+ 205 - 106
src/views/home/components/go-flow.vue

@@ -5,34 +5,10 @@
       <el-button class="back-btn" @click="handleBack"> 返回 </el-button>
       <div class="progress-container">
         <el-steps :active="currentStep" style="max-width: 1500px" align-center>
-          <el-step>
+          <el-step v-for="(item, index) in entryList" :key="index">
             <template #title>
               <div class="step-title-wrapper">
-                <span class="step-title">个人实名</span>
-                <span class="step-time">约3分钟</span>
-              </div>
-            </template>
-          </el-step>
-          <el-step>
-            <template #title>
-              <div class="step-title-wrapper">
-                <span class="step-title">填写信息</span>
-                <span class="step-time">约30分钟</span>
-              </div>
-            </template>
-          </el-step>
-          <el-step>
-            <template #title>
-              <div class="step-title-wrapper">
-                <span class="step-title">等待审核</span>
-                <span class="step-time">约1-3个工作日</span>
-              </div>
-            </template>
-          </el-step>
-          <el-step>
-            <template #title>
-              <div class="step-title-wrapper">
-                <span class="step-title">入驻成功</span>
+                <span class="step-title">{{ item.title }}</span>
               </div>
             </template>
           </el-step>
@@ -70,94 +46,93 @@
                 <el-input v-model="step2Form.storeName" placeholder="请输入店铺名称" />
               </el-form-item>
 
-              <el-form-item label="容纳人数" prop="capacity">
-                <el-input-number v-model="step2Form.capacity" :min="1" :max="9999" />
+              <el-form-item label="容纳人数" prop="storeCapacity">
+                <el-input-number v-model="step2Form.storeCapacity" :min="1" :max="9999" />
               </el-form-item>
 
               <el-form-item label="门店面积" prop="storeArea">
                 <el-radio-group v-model="step2Form.storeArea">
-                  <el-radio label="小于20平米"> 小于20平米 </el-radio>
-                  <el-radio label="20-50平米"> 20-50平米 </el-radio>
-                  <el-radio label="50-100平米"> 50-100平米 </el-radio>
-                  <el-radio label="100-300平米"> 100-300平米 </el-radio>
-                  <el-radio label="300-500平米"> 300-500平米 </el-radio>
-                  <el-radio label="500-1000平米"> 500-1000平米 </el-radio>
-                  <el-radio label="大于1000平米"> 大于1000平米 </el-radio>
+                  <el-radio label="小于20平米" value="1"> 小于20平米 </el-radio>
+                  <el-radio label="20-50平米" value="2"> 20-50平米 </el-radio>
+                  <el-radio label="50-100平米" value="3"> 50-100平米 </el-radio>
+                  <el-radio label="100-300平米" value="4"> 100-300平米 </el-radio>
+                  <el-radio label="500-1000平米" value="5"> 500-1000平米 </el-radio>
+                  <el-radio label="大于1000平米" value="6"> 大于1000平米 </el-radio>
                 </el-radio-group>
               </el-form-item>
 
-              <el-form-item label="所在地区" prop="location">
-                <el-select v-model="step2Form.province" placeholder="请选择" style="width: 150px; margin-right: 10px">
-                  <el-option label="省份" value="province" />
-                </el-select>
-                <el-select v-model="step2Form.city" placeholder="请选择" style="width: 150px; margin-right: 10px">
-                  <el-option label="城市" value="city" />
-                </el-select>
-                <el-select v-model="step2Form.district" placeholder="请选择" style="width: 150px">
-                  <el-option label="区县" value="district" />
-                </el-select>
+              <el-form-item label="所在地区" prop="region">
+                <el-cascader :props="areaProps" v-model="step2Form.region" style="width: 100%" />
               </el-form-item>
 
-              <el-form-item label="详细地址" prop="detailedAddress">
-                <el-input v-model="step2Form.detailedAddress" type="textarea" :rows="3" placeholder="请输入" />
+              <el-form-item label="详细地址" prop="storeAddress">
+                <el-input v-model="step2Form.storeAddress" type="textarea" :rows="3" placeholder="请输入" />
               </el-form-item>
 
-              <el-form-item label="门店简介" prop="storeIntro">
-                <el-input v-model="step2Form.storeIntro" type="textarea" :rows="3" placeholder="请输入" />
+              <el-form-item label="门店简介" prop="storeBlurb">
+                <el-input v-model="step2Form.storeBlurb" type="textarea" :rows="3" placeholder="请输入" />
               </el-form-item>
 
               <el-form-item label="经营板块" prop="businessSector">
-                <el-radio-group v-model="step2Form.businessSector">
-                  <el-radio label="美食"> 美食 </el-radio>
-                  <el-radio label="酒店/民宿"> 酒店/民宿 </el-radio>
-                  <el-radio label="KTV"> KTV </el-radio>
-                  <el-radio label="洗浴汗蒸"> 洗浴汗蒸 </el-radio>
-                  <el-radio label="按摩足疗"> 按摩足疗 </el-radio>
-                  <el-radio label="丽人美发"> 丽人美发 </el-radio>
-                  <el-radio label="运动健身"> 运动健身 </el-radio>
-                  <el-radio label="医美医疗"> 医美医疗 </el-radio>
+                <el-radio-group v-model="step2Form.businessSector" @change="changeBusinessSector">
+                  <el-radio
+                    v-for="businessSection in businessSectionList"
+                    :value="businessSection.value"
+                    :key="businessSection.value"
+                  >
+                    {{ businessSection.label }}
+                  </el-radio>
                 </el-radio-group>
               </el-form-item>
 
               <el-form-item label="经营种类" prop="businessType">
                 <el-checkbox-group v-model="step2Form.businessType">
-                  <el-checkbox label="小吃快餐"> 小吃快餐 </el-checkbox>
-                  <el-checkbox label="鱼鲜海鲜"> 鱼鲜海鲜 </el-checkbox>
-                  <el-checkbox label="烧烤烤串"> 烧烤烤串 </el-checkbox>
-                  <el-checkbox label="自助餐"> 自助餐 </el-checkbox>
-                  <el-checkbox label="面包蛋糕甜品"> 面包蛋糕甜品 </el-checkbox>
-                  <el-checkbox label="火锅"> 火锅 </el-checkbox>
-                  <el-checkbox label="水果生鲜"> 水果生鲜 </el-checkbox>
-                  <el-checkbox label="特色菜"> 特色菜 </el-checkbox>
-                  <el-checkbox label="中餐"> 中餐 </el-checkbox>
-                  <el-checkbox label="西餐"> 西餐 </el-checkbox>
-                  <el-checkbox label="烤肉"> 烤肉 </el-checkbox>
-                  <el-checkbox label="韩式料理"> 韩式料理 </el-checkbox>
-                  <el-checkbox label="地方菜系"> 地方菜系 </el-checkbox>
-                  <el-checkbox label="日式料理"> 日式料理 </el-checkbox>
-                  <el-checkbox label="轻食"> 轻食 </el-checkbox>
+                  <el-checkbox
+                    v-for="businessType in businessTypeList"
+                    :key="businessType"
+                    :label="businessType.label"
+                    :value="businessType.value"
+                  />
                 </el-checkbox-group>
               </el-form-item>
             </div>
 
             <!-- 右列 -->
             <div class="form-col">
-              <el-form-item label="门店营业状态" prop="businessStatus">
-                <el-radio-group v-model="step2Form.businessStatus">
+              <el-form-item label="门店营业状态" prop="businessType">
+                <el-radio-group v-model="step2Form.businessType">
                   <el-radio label="正常营业"> 正常营业 </el-radio>
                   <el-radio label="暂停营业"> 暂停营业 </el-radio>
                   <el-radio label="筹建中"> 筹建中 </el-radio>
                 </el-radio-group>
               </el-form-item>
-
-              <el-form-item label="经纬度查询" prop="coordinates">
-                <el-input v-model="step2Form.coordinates" placeholder="请输入经纬度" />
+              <el-form-item label="经度" prop="storePositionLongitude">
+                <el-input disabled v-model="step2Form.storePositionLongitude" placeholder="请填写经度" clearable />
+              </el-form-item>
+              <el-form-item label="纬度" prop="storePositionLatitude">
+                <el-input disabled v-model="step2Form.storePositionLatitude" placeholder="请填写纬度" clearable />
+              </el-form-item>
+              <el-form-item label="经纬度查询" prop="addressName">
+                <el-select
+                  v-model="step2Form.addressName"
+                  filterable
+                  placeholder="请输入地址进行查询"
+                  remote
+                  reserve-keyword
+                  :remote-method="getLonAndLat"
+                  @change="selectAddress"
+                >
+                  <el-option v-for="item in addressList" :key="item.id" :label="item.name" :value="item.location">
+                    <span style="float: left">{{ item.name }}</span>
+                    <span style="float: right; font-size: 13px; color: var(--el-text-color-secondary)">{{ item.district }}</span>
+                  </el-option>
+                </el-select>
               </el-form-item>
 
-              <el-form-item label="营业执照" prop="businessLicense">
+              <el-form-item label="营业执照" prop="businessLicenseFiles">
                 <el-upload
                   v-model:file-list="step2Form.businessLicenseFiles"
-                  action="#"
+                  :http-request="handleHttpUpload"
                   list-type="picture-card"
                   :limit="1"
                   :on-exceed="handleExceed"
@@ -169,10 +144,10 @@
                 </el-upload>
               </el-form-item>
 
-              <el-form-item label="合同图片" prop="contractImages">
+              <el-form-item label="合同图片" prop="contractImageFiles">
                 <el-upload
                   v-model:file-list="step2Form.contractImageFiles"
-                  action="#"
+                  :http-request="handleHttpUpload"
                   list-type="picture-card"
                   :limit="20"
                   :on-exceed="handleExceed"
@@ -184,10 +159,10 @@
                 </el-upload>
               </el-form-item>
 
-              <el-form-item label="食品经营许可证" prop="foodLicense">
+              <el-form-item label="食品经营许可证" prop="foodLicenseFiles ">
                 <el-upload
                   v-model:file-list="step2Form.foodLicenseFiles"
-                  action="#"
+                  :http-request="handleHttpUpload"
                   list-type="picture-card"
                   :limit="1"
                   :on-exceed="handleExceed"
@@ -218,10 +193,40 @@
   </div>
 </template>
 <script setup lang="ts">
-import { ref, reactive, watch } from "vue";
-import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
+import { ref, reactive, watch, onMounted } from "vue";
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules, UploadProps, UploadRequestOptions } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 
+import { verifyIdInfo, applyStore } from "@/api/modules/homeEntry.ts";
+import {
+  getBusinessSection,
+  getBusinessSectionTypes,
+  getInputPrompt,
+  getDistrict,
+  uploadImg
+} from "@/api/modules/newLoginApi.ts";
+import { add } from "lodash";
+
+const entryList = ref([
+  {
+    title: "个人实名"
+  },
+  {
+    title: "填写信息"
+  },
+  {
+    title: "等待审核"
+  },
+  {
+    title: "入驻成功"
+  }
+]);
+
+//地址集合
+const addressList = ref<any[]>([]);
+//查询地址名称
+const queryAddress = ref<string>("");
+
 const props = defineProps({
   currentStep: {
     type: Number,
@@ -267,18 +272,21 @@ const step1Rules: FormRules = {
 // 第二步表单
 const step2FormRef = ref<FormInstance>();
 const step2Form = reactive({
+  region: [],
+  addressName: "",
+  storePositionLongitude: "",
+  storePositionLatitude: "",
   storeName: "",
-  capacity: 1,
+  storeCapacity: 1,
   storePhone: "",
   storeArea: "小于20平米",
   province: "",
   city: "",
   district: "",
-  detailedAddress: "",
-  storeIntro: "",
-  businessSector: "美食",
-  businessType: ["小吃快餐", "火锅"],
-  businessStatus: "正常营业",
+  storeAddress: "",
+  storeBlurb: "",
+  businessSector: "1",
+  businessType: "正常营业",
   coordinates: "",
   businessLicenseFiles: [],
   contractImageFiles: [],
@@ -308,14 +316,113 @@ const handleBack = () => {
     setStep(2);
   }
 };
+// 地区选择
+const areaProps: any = {
+  lazy: true,
+  async lazyLoad(node, resolve) {
+    const { level } = node;
+    try {
+      let param = { adCode: node.data.adCode ? node.data.adCode : "" };
+      // 调用后台接口获取数据
+      const response: any = await getDistrict(param as any);
+      // 转换数据格式
+      const nodes = (response?.data?.districts?.[0]?.districts || []).map((item: any) => ({
+        value: item.adcode,
+        adCode: item.adcode,
+        label: item.name,
+        leaf: level >= 2 // 假设最多三级,可以根据实际需求调整
+      }));
 
+      // 返回数据
+      resolve(nodes);
+    } catch (error) {
+      console.error("获取区域数据失败:", error);
+      resolve([]);
+    }
+  }
+};
+//经营板块
+const businessSectionList = ref<any[]>([]);
+const getBusinessSectionList = async () => {
+  let res: any = await getBusinessSection();
+  let addData: any[] = [];
+  (res?.data || []).forEach((element: any) => {
+    addData.push({ value: element.dictId, label: element.dictDetail, parentId: element.parentId });
+  });
+  businessSectionList.value = addData;
+};
+const changeBusinessSector = async (val: any) => {
+  getBusinessTypes(val);
+};
+
+//经营种类
+const businessTypeList = ref<any[]>([]);
+const getBusinessTypes = async (val: any) => {
+  let res: any = await getBusinessSectionTypes({ parentId: val ? val : step2Form.businessSector });
+  let addData1: any[] = [];
+  (res?.data || []).forEach((element: any) => {
+    addData1.push({ value: element.dictId, label: element.dictDetail });
+  });
+  businessTypeList.value = addData1;
+};
+
+// 经纬度查询
+const getLonAndLat = async (keyword: string) => {
+  if (keyword) {
+    console.log("地址查询", keyword);
+    let param = {
+      addressName: keyword
+    };
+    let res: any = await getInputPrompt(param as any);
+    if (res.code == "200") {
+      addressList.value = res?.data?.tips || [];
+      console.log("res", res);
+    } else {
+      ElMessage.error("新增失败!");
+    }
+  } else {
+    addressList.value = [];
+  }
+};
+const selectAddress = async (param: any) => {
+  let locationList = step2Form.addressName.split(",");
+  addressList.value.forEach((item: any) => {
+    if (item.location == step2Form.addressName) {
+      queryAddress.value = item.name;
+    }
+  });
+  step2Form.storePositionLongitude = locationList[0];
+  step2Form.storePositionLatitude = locationList[1];
+};
+//文件上传
+const handleHttpUpload = async (options: UploadRequestOptions) => {
+  let formData = new FormData();
+  formData.append("file", options.file);
+  try {
+    const res: any = await uploadImg(formData);
+    console.log(res);
+  } catch (error) {
+    options.onError(error as any);
+  }
+};
 // 下一步
 const handleNextStep = async () => {
   if (!step1FormRef.value) return;
 
-  await step1FormRef.value.validate(valid => {
+  await step1FormRef.value.validate(async valid => {
     if (valid) {
-      setStep(2);
+      const params = {
+        name: step1Form.name,
+        idCard: step1Form.idNumber,
+        appType: 1
+      };
+      const res = await verifyIdInfo(params);
+      if (res.code == "200") {
+        ElMessage.success(res.msg);
+        setStep(2);
+        getBusinessSectionList();
+        getBusinessTypes();
+      }
     } else {
       ElMessage.error("请完善表单信息");
     }
@@ -329,7 +436,9 @@ const handlePrevStep = () => {
 
 // 提交
 const handleSubmit = async () => {
-  setStep(3);
+  console.log(step2Form);
+  return;
+  let res = await applyStore(param);
   return;
   if (!step2FormRef.value) return;
 
@@ -373,7 +482,6 @@ const handleExceed = () => {
     border-color: #dcdfe6;
   }
   .progress-container {
-    padding: 0 100px;
     margin-bottom: 40px;
     :deep(.el-step__head.is-process .el-step__icon) {
       color: #909399;
@@ -399,15 +507,6 @@ const handleExceed = () => {
             font-weight: 600;
             color: #6c8ff8;
           }
-          .step-time {
-            display: inline-block;
-            padding: 4px 12px;
-            font-size: 12px;
-            color: #6c8ff8;
-            white-space: nowrap;
-            background: #eef1ff;
-            border-radius: 12px;
-          }
         }
       }
     }
@@ -416,7 +515,7 @@ const handleExceed = () => {
     max-width: 800px;
     margin: 0 auto;
     &.step2-form {
-      max-width: 1400px;
+      max-width: 100%;
       .form-row {
         display: flex;
         gap: 40px;

+ 14 - 18
src/views/home/notice.vue

@@ -2,8 +2,7 @@
   <div class="notice-page">
     <!-- 头部 -->
     <div class="header">
-      <h1 class="title">通知</h1>
-      <el-button class="mark-all-read-btn" @click="handleMarkAllRead">
+      <el-button class="mark-all-read-btn" v-if="noticeList.length != 0" @click="handleMarkAllRead">
         <el-icon><CircleCheck /></el-icon>
         一键已读
       </el-button>
@@ -40,9 +39,10 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from "vue";
+import { ref, onMounted } from "vue";
 import { ElMessage } from "element-plus";
 import { CircleCheck } from "@element-plus/icons-vue";
+import { getNoticeList } from "@/api/modules/homeEntry.ts";
 
 interface NoticeItem {
   id: number;
@@ -52,22 +52,18 @@ interface NoticeItem {
   isRead: boolean;
 }
 
-const noticeList = ref<NoticeItem[]>([
-  {
-    id: 1,
-    title: "入驻店铺申请通知",
-    time: "2025/08/01 12:00",
-    content: "您在2025-01-01 12:12:12提交的入驻店铺申请,平台已受理,1-3个工作日将审核结果发送至应用内的消息—通知中,请注意查收。",
-    isRead: false
-  },
-  {
-    id: 2,
-    title: "入驻店铺申请通知",
-    time: "2025/08/01 12:00",
-    content: "您在2025-01-01 12:12:12提交的入驻店铺申请,平台已受理,1-3个工作日将审核结果发送至应用内的消息—通知中,请注意查收。",
-    isRead: true
+onMounted(() => {
+  getList();
+});
+
+const noticeList = ref<NoticeItem[]>([]);
+const getList = async () => {
+  let res = await getNoticeList({ pageNum: 1, pageSize: 10, receiverId: "15242687180" });
+  console.log(res);
+  if (res.code == "200") {
+    noticeList.value = res.data.records;
   }
-]);
+};
 
 const dialogVisible = ref(false);
 const currentNotice = ref<NoticeItem | null>(null);

+ 3 - 27
src/views/login/index.vue

@@ -591,6 +591,7 @@ import { useKeepAliveStore } from "@/stores/modules/keepAlive";
 import { initDynamicRouter } from "@/routers/modules/dynamicRouter";
 import type { ElForm } from "element-plus";
 import md5 from "md5";
+import { validatePassword, validateConfirmPassword } from "@/utils/eleValidate";
 import {
   forgetPassword,
   getImgCode,
@@ -675,22 +676,7 @@ const codeRules = reactive({
   ],
   code: [{ required: true, message: "请输入验证码", trigger: "blur" }]
 });
-// 密码验证函数
-const validatePassword = (rule: any, value: string, callback: any) => {
-  if (!value) {
-    callback(new Error("请输入密码"));
-    return;
-  }
-  if (value.length < 6 || value.length > 16) {
-    callback(new Error("密码长度为6-16位,密码必须包含字母和数字"));
-    return;
-  }
-  if (!/^(?=.*[a-zA-Z])(?=.*\d).+$/.test(value)) {
-    callback(new Error("密码长度为6-16位,密码必须包含字母和数字"));
-    return;
-  }
-  callback();
-};
+// 密码验证函数已从 @/utils/eleValidate 导入
 
 const forgetRules = ref({
   phone: [
@@ -718,17 +704,7 @@ const registerRules = reactive({
   confirmPassword: [
     { required: true, message: "请确认密码", trigger: "blur" },
     {
-      validator: (rule: any, value: string, callback: any) => {
-        if (!value) {
-          callback(new Error("请确认密码"));
-          return;
-        }
-        if (value !== registerForm.password) {
-          callback(new Error("两次输入的密码不一致"));
-        } else {
-          callback();
-        }
-      },
+      validator: validateConfirmPassword(() => registerForm.password),
       trigger: "blur"
     }
   ]

+ 7 - 4
src/views/orderManagement/index.vue

@@ -37,6 +37,7 @@ import { useRouter } from "vue-router";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { getThaliList, getCpList } from "@/api/modules/orderManagement";
+import { localGet, usePermission } from "@/utils";
 
 const router = useRouter();
 const proTable = ref<ProTableInstance>();
@@ -147,14 +148,16 @@ const columns = reactive<ColumnProps<any>[]>([
   { prop: "operation", label: "操作", fixed: "right", width: 200 }
 ]);
 
-// 初始化请求参数
+// 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
 const initParam = reactive({
-  storeId: "104",
+  storeId: localGet("createdId") || "",
+  groupType: localGet("businessSection") || "1",
   status: activeName
 });
-
+const type = ref(false);
 // 页面加载时触发查询
-onMounted(() => {
+onMounted(async () => {
+  type.value = await usePermission("查看订单");
   proTable.value?.getTableList();
 });
 

+ 21 - 13
src/views/voucherManagement/index.vue

@@ -4,7 +4,7 @@
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <div class="table-header-btn">
-          <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying"> 新建代金券 </el-button>
+          <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying" v-if="type"> 新建代金券 </el-button>
           <el-tabs v-if="showTabs" v-model="activeName" class="tabs" @tab-click="handleClick">
             <el-tab-pane v-for="tab in filteredTabOptions" :key="tab.name" :label="tab.label" :name="tab.name" />
           </el-tabs>
@@ -19,7 +19,7 @@
         <el-button
           link
           type="primary"
-          @click="changeTypes(scope.row, 'on')"
+          @click="changeTypes(scope.row, 5)"
           v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.上架)"
         >
           上架
@@ -27,7 +27,7 @@
         <el-button
           link
           type="primary"
-          @click="changeTypes(scope.row, 'off')"
+          @click="changeTypes(scope.row, 6)"
           v-if="canShowButton(scope.row.status, OPERATION_PERMISSIONS.下架)"
         >
           下架
@@ -96,8 +96,9 @@ import { ElMessage } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { Plus } from "@element-plus/icons-vue";
-import { deleteThali, getThaliList, sjxj, updateNum } from "@/api/modules/voucherManagement";
+import { delThaliById, getThaliList, updateStatus, updateNum } from "@/api/modules/voucherManagement";
 import { ElMessageBox } from "element-plus/es";
+import { localGet, usePermission } from "@/utils";
 
 const router = useRouter();
 const dialogFormVisible = ref(false);
@@ -271,13 +272,14 @@ watch(
 );
 // 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
 const initParam = reactive({
-  storeId: "104",
-  groupType: "1",
+  storeId: localGet("createdId") || "",
+  groupType: localGet("businessSection") || "1",
   status: activeName
 });
-
+const type = ref(false);
 // 页面加载时触发查询
-onMounted(() => {
+onMounted(async () => {
+  type.value = await usePermission("新建代金券");
   proTable.value?.getTableList();
 });
 
@@ -313,11 +315,15 @@ const editRow = row => {
 };
 const deleteRow = row => {
   ElMessageBox.confirm("确定要删除吗?", "提示", {
-    confirmButtonText: "OK",
-    cancelButtonText: "Cancel",
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
     type: "warning"
   }).then(() => {
-    deleteThali(row.id).then(() => {
+    const params = {
+      id: row.id,
+      groupType: localGet("businessSection") || "1"
+    };
+    delThaliById(params).then(() => {
       ElMessage.success("删除成功");
       proTable.value?.getTableList();
     });
@@ -326,7 +332,7 @@ const deleteRow = row => {
 const handleClick = () => {};
 const changeTypes = async (row: any, status: string) => {
   rowData.value = row;
-  let res = await sjxj({ id: row.id, status: status });
+  let res = await updateStatus({ id: row.id, status: status, approvalComments: "" });
   if (res && res.code == 200) {
     ElMessage.success("操作成功");
     proTable.value?.getTableList();
@@ -374,8 +380,10 @@ const closeDialog = () => {
   white-space: normal; // 允许自然换行
 }
 .table-header-btn {
+  .button {
+    margin-bottom: 10px;
+  }
   .tabs {
-    margin-top: 10px;
     :deep(.el-tabs__nav-wrap::after) {
       height: 0;
     }

+ 50 - 171
src/views/voucherManagement/newVoucher.vue

@@ -236,6 +236,15 @@ import { ElMessage } from "element-plus";
 import { useRoute, useRouter } from "vue-router";
 import type { FormInstance } from "element-plus";
 import { getVoucherDetail } from "@/api/modules/voucherManagement";
+import {
+  validatePositiveNumber,
+  validatePositiveInteger,
+  validateDateRange,
+  validateDateRangeArray,
+  validateConditionalRequired,
+  validateArrayMinLength,
+  validateDateListArray
+} from "@/utils/eleValidate";
 
 // ==================== 响应式数据定义 ====================
 
@@ -253,109 +262,49 @@ const rules = reactive({
   discountPrice: [
     { required: true, message: "请输入抵扣价格" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.toString().trim() === "") {
-          callback();
-          return;
-        }
-        const num = Number(value);
-        if (isNaN(num) || num <= 0) {
-          callback(new Error("抵扣价格必须为正数"));
-          return;
-        }
-        callback();
-      },
+      validator: validatePositiveNumber("抵扣价格必须为正数"),
       trigger: "blur"
     }
   ],
   sellingPrice: [
     { required: true, message: "请输入售卖价格" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.toString().trim() === "") {
-          callback();
-          return;
-        }
-        const num = Number(value);
-        if (isNaN(num) || num <= 0) {
-          callback(new Error("售卖价格必须为正数"));
-          return;
-        }
-        callback();
-      },
+      validator: validatePositiveNumber("售卖价格必须为正数"),
       trigger: "blur"
     }
   ],
   startSellingTime: [
     { required: true, message: "请选择开始售卖时间" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value) {
-          callback();
-          return;
-        }
-        const selectedDate = new Date(value);
-        const today = new Date();
-        today.setHours(0, 0, 0, 0);
-        if (selectedDate < today) {
-          callback(new Error("开始售卖时间不能早于当前时间"));
-          return;
-        }
-        if (voucherModel.value.endSellingTime) {
-          const endDate = new Date(voucherModel.value.endSellingTime);
-          if (selectedDate >= endDate) {
-            callback(new Error("开始售卖时间必须早于结束售卖时间"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateDateRange(
+        () => voucherModel.value.startSellingTime,
+        () => voucherModel.value.endSellingTime,
+        "开始售卖时间不能早于当前时间",
+        "结束售卖时间不能早于当前时间",
+        "开始售卖时间必须早于结束售卖时间",
+        true
+      ),
       trigger: "change"
     }
   ],
   endSellingTime: [
     { required: true, message: "请选择结束售卖时间" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value) {
-          callback();
-          return;
-        }
-        const selectedDate = new Date(value);
-        const today = new Date();
-        today.setHours(0, 0, 0, 0);
-        if (selectedDate < today) {
-          callback(new Error("结束售卖时间不能早于当前时间"));
-          return;
-        }
-        if (voucherModel.value.startSellingTime) {
-          const startDate = new Date(voucherModel.value.startSellingTime);
-          if (selectedDate <= startDate) {
-            callback(new Error("结束售卖时间必须晚于开始售卖时间"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateDateRange(
+        () => voucherModel.value.startSellingTime,
+        () => voucherModel.value.endSellingTime,
+        "开始售卖时间不能早于当前时间",
+        "结束售卖时间不能早于当前时间",
+        "开始售卖时间必须早于结束售卖时间",
+        true
+      ),
       trigger: "change"
     }
   ],
   usageTime: [
     { required: true, message: "请选择使用时间" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.length !== 2) {
-          callback();
-          return;
-        }
-        const startDate = new Date(value[0]);
-        const endDate = new Date(value[1]);
-        if (startDate >= endDate) {
-          callback(new Error("使用开始时间必须早于结束时间"));
-          return;
-        }
-        callback();
-      },
+      validator: validateDateRangeArray("使用开始时间必须早于结束时间"),
       trigger: "change"
     }
   ],
@@ -382,20 +331,16 @@ const rules = reactive({
   validityPeriod: [
     {
       required: true,
+      validator: validateConditionalRequired(() => voucherModel.value.validityPeriodType === 1, "请选择指定时间段"),
+      trigger: "change"
+    },
+    {
       validator: (rule: any, value: any, callback: any) => {
-        if (voucherModel.value.validityPeriodType === 1) {
-          if (!value || value.length !== 2) {
-            callback(new Error("请选择指定时间段"));
-            return;
-          }
-          const startDate = new Date(value[0]);
-          const endDate = new Date(value[1]);
-          if (startDate >= endDate) {
-            callback(new Error("开始时间必须早于结束时间"));
-            return;
-          }
+        if (voucherModel.value.validityPeriodType === 1 && value && value.length === 2) {
+          validateDateRangeArray("开始时间必须早于结束时间")(rule, value, callback);
+        } else {
+          callback();
         }
-        callback();
       },
       trigger: "change"
     }
@@ -403,118 +348,52 @@ const rules = reactive({
   unavailableDateType: [{ required: true, message: "请选择不可用日期类型" }],
   unavailableWeekdays: [
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (voucherModel.value.unavailableDateType === 1) {
-          if (!value || value.length === 0) {
-            callback(new Error("至少需要选择一个星期"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateConditionalRequired(() => voucherModel.value.unavailableDateType === 1, "至少需要选择一个星期"),
       trigger: "change"
     }
   ],
   unavailableHolidays: [
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (voucherModel.value.unavailableDateType === 1) {
-          if (!value || value.length === 0) {
-            callback(new Error("至少需要选择一个节日"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateConditionalRequired(() => voucherModel.value.unavailableDateType === 1, "至少需要选择一个节日"),
       trigger: "change"
     }
   ],
   customUnavailableDates: [
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (voucherModel.value.unavailableDateType === 2) {
-          if (!dates.value || dates.value.length === 0) {
-            callback(new Error("至少需要添加一个自定义不可用日期"));
-            return;
-          }
-          for (let i = 0; i < dates.value.length; i++) {
-            if (!dates.value[i] || dates.value[i].length !== 2) {
-              callback(new Error(`第${i + 1}个日期项未完整填写`));
-              return;
-            }
-            const startDate = new Date(dates.value[i][0]);
-            const endDate = new Date(dates.value[i][1]);
-            if (startDate >= endDate) {
-              callback(new Error(`第${i + 1}个日期项的开始时间必须早于结束时间`));
-              return;
-            }
-          }
-        }
-        callback();
-      },
+      validator: validateConditionalRequired(
+        () => voucherModel.value.unavailableDateType === 2,
+        "至少需要添加一个自定义不可用日期"
+      ),
+      trigger: "change"
+    },
+    {
+      validator: validateDateListArray(() => dates.value, "日期项未完整填写", "开始时间必须早于结束时间", false),
       trigger: "change"
     }
   ],
   inventory: [
     { required: true, message: "请输入库存" },
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (!value || value.toString().trim() === "") {
-          callback();
-          return;
-        }
-        const num = Number(value);
-        if (isNaN(num) || num <= 0 || !Number.isInteger(num)) {
-          callback(new Error("库存必须为正整数"));
-          return;
-        }
-        callback();
-      },
+      validator: validatePositiveInteger("库存必须为正整数", { required: false }),
       trigger: "blur"
     }
   ],
   dailyAvailableQuantity: [
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (value && value.toString().trim() !== "") {
-          const num = Number(value);
-          if (isNaN(num) || num <= 0 || !Number.isInteger(num)) {
-            callback(new Error("单日可用数量必须为正整数"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validatePositiveInteger("单日可用数量必须为正整数", { required: false }),
       trigger: "blur"
     }
   ],
   purchaseLimitQuantity: [
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (value && value.toString().trim() !== "") {
-          const num = Number(value);
-          if (isNaN(num) || num <= 0 || !Number.isInteger(num)) {
-            callback(new Error("限购数量必须为正整数"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validatePositiveInteger("限购数量必须为正整数", { required: false }),
       trigger: "blur"
     }
   ],
   applicableScopeType: [{ required: true, message: "请选择适用范围类型" }],
   applicableScope: [
     {
-      validator: (rule: any, value: any, callback: any) => {
-        if (voucherModel.value.applicableScopeType === 1) {
-          if (!value || value.trim() === "") {
-            callback(new Error("请输入适用范围"));
-            return;
-          }
-        }
-        callback();
-      },
+      validator: validateConditionalRequired(() => voucherModel.value.applicableScopeType === 1, "请输入适用范围"),
       trigger: "blur"
     }
   ]