Prechádzať zdrojové kódy

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

# Conflicts:
#	src/layouts/components/Header/components/Avatar.vue
spy 3 týždňov pred
rodič
commit
242b0a9981
41 zmenil súbory, kde vykonal 4613 pridanie a 1099 odobranie
  1. 95 0
      src/api/index.ts
  2. 11 17
      src/api/modules/couponManagement.ts
  3. 33 1
      src/api/modules/homeEntry.ts
  4. 10 5
      src/api/modules/licenseManagement.ts
  5. 3 21
      src/api/modules/orderManagement.ts
  6. 1 1
      src/api/modules/voucherManagement.ts
  7. BIN
      src/assets/financial/zfb-icon.png
  8. 111 13
      src/assets/json/authMenuList.json
  9. 3 2
      src/components/ProTable/index.vue
  10. 3 49
      src/layouts/LayoutClassic/index.vue
  11. 3 0
      src/layouts/LayoutColumns/index.vue
  12. 3 49
      src/layouts/LayoutVertical/index.vue
  13. 88 5
      src/layouts/components/Header/components/Avatar.vue
  14. 6 0
      src/layouts/components/Header/components/InfoDialog.vue
  15. 23 4
      src/layouts/components/Header/components/Message.vue
  16. 65 0
      src/layouts/hooks/useOpenedMenus.ts
  17. 2 2
      src/stores/modules/auth.ts
  18. 30 1
      src/utils/eleValidate.ts
  19. 22 0
      src/utils/formatCurrency.ts
  20. 458 0
      src/views/financialManagement/cashApply.vue
  21. 227 26
      src/views/financialManagement/index.vue
  22. 621 140
      src/views/financialManagement/realName.vue
  23. 194 0
      src/views/financialManagement/reconciled.vue
  24. 233 0
      src/views/financialManagement/todayIncomeList.vue
  25. 208 0
      src/views/financialManagement/unposted.vue
  26. 407 0
      src/views/financialManagement/withdrawaRecord.vue
  27. 82 27
      src/views/groupPackageManagement/newGroup.vue
  28. 33 3
      src/views/home/components/go-flow.vue
  29. 22 17
      src/views/home/index.vue
  30. 21 7
      src/views/home/notice.vue
  31. 20 24
      src/views/licenseManagement/businessLicense.vue
  32. 523 176
      src/views/licenseManagement/contractManagement.vue
  33. 515 152
      src/views/licenseManagement/foodBusinessLicense.vue
  34. 11 5
      src/views/login/index.vue
  35. 95 77
      src/views/orderManagement/detail.vue
  36. 47 38
      src/views/orderManagement/index.vue
  37. 13 32
      src/views/ticketManagement/couponDetail.vue
  38. 25 25
      src/views/ticketManagement/detail.vue
  39. 212 106
      src/views/ticketManagement/index.vue
  40. 74 26
      src/views/ticketManagement/newCoupon.vue
  41. 60 48
      src/views/ticketManagement/newVoucher.vue

+ 95 - 0
src/api/index.ts

@@ -7,6 +7,7 @@ import { ResultEnum } from "@/enums/httpEnum";
 import { checkStatus } from "./helper/checkStatus";
 import { AxiosCanceler } from "./helper/axiosCancel";
 import { useUserStore } from "@/stores/modules/user";
+import { localGet } from "@/utils";
 import router from "@/routers";
 
 export interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {
@@ -118,6 +119,100 @@ class RequestHttp {
   download(url: string, params?: object, _object = {}): Promise<BlobPart> {
     return this.service.post(url, params, { ..._object, responseType: "blob" });
   }
+
+  /**
+   * @description 文件上传方法(基于 XMLHttpRequest)
+   * @param url 上传地址
+   * @param formData FormData 对象
+   * @param onProgress 上传进度回调函数 (progress: number) => void
+   * @param baseURL 可选的基础URL,不传则使用默认baseURL
+   * @returns Promise<ResultData<T>>
+   */
+  upload<T = any>(
+    url: string,
+    formData: FormData,
+    onProgress?: (progress: number) => void,
+    baseURL?: string
+  ): Promise<ResultData<T>> {
+    return new Promise((resolve, reject) => {
+      const xhr = new XMLHttpRequest();
+      const userStore = useUserStore();
+      // 如果传入了 baseURL,使用传入的 baseURL;如果 URL 是完整 URL(以 http 开头),直接使用;否则使用默认 baseURL
+      const fullUrl = baseURL ? `${baseURL}${url}` : url.startsWith("http") ? url : `${config.baseURL}${url}`;
+
+      // 监听上传进度
+      if (onProgress) {
+        xhr.upload.addEventListener("progress", e => {
+          if (e.lengthComputable) {
+            const percentComplete = Math.round((e.loaded / e.total) * 100);
+            onProgress(percentComplete);
+          }
+        });
+      }
+
+      // 监听请求完成
+      xhr.addEventListener("load", () => {
+        if (xhr.status >= 200 && xhr.status < 300) {
+          try {
+            const response = JSON.parse(xhr.responseText);
+            // 统一处理响应,与 axios 拦截器保持一致
+            if (response.code == ResultEnum.OVERDUE) {
+              userStore.setToken("");
+              router.replace(LOGIN_URL);
+              ElMessage.error(response.msg);
+              reject(response);
+              return;
+            }
+            if (response.code && response.code !== ResultEnum.SUCCESS) {
+              ElMessage.error(response.msg);
+              reject(response);
+              return;
+            }
+            resolve(response);
+          } catch (error) {
+            reject(new Error("响应解析失败"));
+          }
+        } else {
+          const errorMsg = `上传失败: ${xhr.status} ${xhr.statusText}`;
+          ElMessage.error(errorMsg);
+          reject(new Error(errorMsg));
+        }
+      });
+
+      // 监听请求错误
+      xhr.addEventListener("error", () => {
+        const errorMsg = "网络错误!请您稍后重试";
+        ElMessage.error(errorMsg);
+        reject(new Error(errorMsg));
+      });
+
+      // 监听请求中止
+      xhr.addEventListener("abort", () => {
+        reject(new Error("上传已取消"));
+      });
+
+      // 打开请求
+      xhr.open("POST", fullUrl, true);
+
+      // 设置请求头
+      const token = userStore.token || localGet("geeker-user")?.token;
+      if (token) {
+        xhr.setRequestHeader("Authorization", token);
+      }
+
+      // 设置超时
+      xhr.timeout = config.timeout;
+      xhr.addEventListener("timeout", () => {
+        const errorMsg = "请求超时!请您稍后重试";
+        ElMessage.error(errorMsg);
+        reject(new Error(errorMsg));
+      });
+
+      // 发送请求
+      xhr.withCredentials = config.withCredentials;
+      xhr.send(formData);
+    });
+  }
 }
 
 export default new RequestHttp(config);

+ 11 - 17
src/api/modules/couponManagement.ts

@@ -2,27 +2,21 @@ import { ResPage, StoreUser } from "@/api/interface/index";
 import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
 
-export const getThaliList = params => {
-  return http.get<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/PcGroupBuy/getThaliList`, params);
+export const delCouponById = (params: { id: string }) => {
+  return http.get(PORT_NONE + `/discountCouponPlatform/deleteDiscountCoupon`, params);
 };
-export const delThaliById = (params: { id: string }) => {
-  return http.get(PORT_NONE + `/PcGroupBuy/deleteThali`, params);
+export const updateCouponStatus = params => {
+  return http.get(PORT_NONE + `/discountCouponPlatform/cutDiscountCouponStatus`, params);
 };
-export const updateStatus = params => {
-  return http.get(PORT_NONE + `/PcGroupBuy/updateStatus1`, params);
+export const updateCouponSingleQty = params => {
+  return http.get(PORT_NONE + `/discountCouponPlatform/updateCouponSingleQty`, params);
 };
-export const updateNum = params => {
-  return http.get(PORT_NONE + `/PcGroupBuy/updateNum1`, params);
+export const addDiscountCoupon = params => {
+  return http.post(PORT_NONE + `/discountCouponPlatform/addDiscountCoupon`, params);
 };
-export const saveDraft = params => {
-  return http.post(PORT_NONE + `/PcGroupBuy/saveDraft`, params);
-};
-export const getMenuByStoreId = params => {
-  return http.get(PORT_NONE + `/menuPlatform/getMenuByStoreId`, params);
-};
-export const getHolidayList = (params: any) => {
-  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/couponPlatform/getHolidayList`, params);
+export const editDiscountCoupon = params => {
+  return http.post(PORT_NONE + `/discountCouponPlatform/editDiscountCoupon`, params);
 };
 export const getCouponDetail = (params: StoreUser.ReqUserParams) => {
-  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/store/info/getStoreDetail`, params);
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/discountCouponPlatform/getCounponDetailById`, params);
 };

+ 33 - 1
src/api/modules/homeEntry.ts

@@ -21,6 +21,10 @@ export const getMerchantByPhone = params => {
 export const getNoticeList = params => {
   return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/notice/getNoticeList`, params);
 };
+//通知查看详情标记已读
+export const markNoticeAsRead = params => {
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/notice/markNoticeAsRead`, params);
+};
 //通知列表  一键已读
 export const markAllNoticesAsRead = params => {
   return http.post(PORT_NONE + `/notice/markAllNoticesAsRead`, params);
@@ -48,7 +52,23 @@ export const updateMerchantUserInfo = params => {
 
 //财务   可提现金额
 export const getAccountBalance = params => {
-  return http.post(PORT_NONE + `/incomeManage/getAccountBalance`, params);
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/incomeManage/getAccountBalance`, params);
+};
+//已到账金额/未到账金额
+export const getPaymentCycle = params => {
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/incomeManage/getPaymentCycle`, params);
+};
+//是否有支付密码
+export const checkPayPassword = params => {
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/merchantUser/checkPayPassword`, params);
+};
+//设置提现密码
+export const setPayPassword = params => {
+  return http.post(PORT_NONE + `/merchantUser/setPayPassword`, params);
+};
+//提现记录列表
+export const getCashOutRecordList = params => {
+  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/incomeManage/getCashOutRecordList`, params);
 };
 export const getUserByPhone = params => {
   return http.get(PORT_NONE + `/merchantUser/getMerchantByPhone`, params);
@@ -56,3 +76,15 @@ export const getUserByPhone = params => {
 export const getDetail = params => {
   return http.get(PORT_NONE + `/storeManage/getStoreDetail`, params);
 };
+//重置商户到刚注册状态
+export const resetToInitialStatus = () => {
+  return http.post(PORT_NONE + `/merchantUser/resetToInitialStatus`);
+};
+//添加支付宝账号
+export const addAlipayAccount = params => {
+  return http.post(PORT_NONE + `/merchantUser/addAlipayAccount`, params);
+};
+//提现申请-提交
+export const cashOut = params => {
+  return http.get(PORT_NONE + `/incomeManage/cashOut`, params);
+};

+ 10 - 5
src/api/modules/licenseManagement.ts

@@ -4,30 +4,35 @@ import http from "@/api";
 
 // 获取营业执照
 export const getBusinessLicense = params => {
-  return http.get(PORT_NONE + `/license/getBusinessLicense`, params);
+  return http.get(PORT_NONE + `/license/querybusinessLicenseList`, params);
 };
 
 // 获取食品经营许可证
 export const getFoodBusinessLicense = params => {
-  return http.get(PORT_NONE + `/license/getFoodBusinessLicense`, params);
+  return http.get(PORT_NONE + `/license/queryFoodLicenceList`, params);
 };
 
 // 获取合同图片列表
 export const getContractImages = params => {
-  return http.get(PORT_NONE + `/license/getContractImages`, params);
+  return http.get(PORT_NONE + `/license/queryContractList`, params);
 };
 
 // 获取变更记录
 export const getChangeRecords = params => {
-  return http.get(PORT_NONE + `/license/getChangeRecords`, params);
+  return http.get(PORT_NONE + `/license/queryLicenceByStatusList`, params);
 };
 
 // 提交食品经营许可证审核
 export const submitFoodLicenseReview = params => {
-  return http.post(PORT_NONE + `/license/submitFoodLicenseReview`, params);
+  return http.post(PORT_NONE + `/license/uploadfoodLicence`, params);
 };
 
 // 提交合同审核
 export const submitContractReview = params => {
   return http.post(PORT_NONE + `/license/submitContractReview`, params);
 };
+
+// 上传合同图片
+export const uploadContractImage = (formData: FormData, onProgress?: (progress: number) => void) => {
+  return http.upload("/file/uploadMore", formData, onProgress, import.meta.env.VITE_API_URL as string);
+};

+ 3 - 21
src/api/modules/orderManagement.ts

@@ -3,29 +3,11 @@ import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
 
 export const getThaliList = params => {
-  return http.get<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/PcGroupBuy/getThaliList`, params);
+  return http.get<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/userOrderPlatform/queryUserOrderList`, params);
 };
 export const getCpList = params => {
   return http.get<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/PcGroupBuy/getCpList`, params);
 };
-export const deleteThali = (params: { id: string }) => {
-  return http.delete(PORT_NONE + `/PcGroupBuy/deleteThali`, params);
-};
-export const sjxj = params => {
-  return http.get(PORT_NONE + `/PcGroupBuy/sjxj`, params);
-};
-export const xgkc = params => {
-  return http.get(PORT_NONE + `/PcGroupBuy/xgkc`, params);
-};
-export const saveDraft = params => {
-  return http.post(PORT_NONE + `/PcGroupBuy/saveDraft`, params);
-};
-export const getMenuByStoreId = params => {
-  return http.get(PORT_NONE + `/menuPlatform/getMenuByStoreId`, params);
-};
-export const getHolidayList = (params: any) => {
-  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/couponPlatform/getHolidayList`, params);
-};
-export const getStoreDetail = (params: StoreUser.ReqUserParams) => {
-  return http.get<StoreUser.ResStoreUserList>(PORT_NONE + `/store/info/getStoreDetail`, params);
+export const queryUserOrderDetail = (params: StoreUser.ReqUserParams) => {
+  return http.get(PORT_NONE + `/userOrderPlatform/queryUserOrderDetail`, params);
 };

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

@@ -3,7 +3,7 @@ import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
 
 export const getThaliList = params => {
-  return http.get<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/couponPlatform/getCouponList`, params);
+  return http.post<ResPage<StoreUser.ResStoreUserList>>(PORT_NONE + `/couponPlatform/getCouponList`, params);
 };
 export const delThaliById = (params: { id: string }) => {
   return http.get(PORT_NONE + `/couponPlatform/deleteCoupon`, params);

BIN
src/assets/financial/zfb-icon.png


+ 111 - 13
src/assets/json/authMenuList.json

@@ -37,7 +37,7 @@
         "icon": "ShoppingBag",
         "title": "团购管理",
         "isLink": "",
-        "isHide": false,
+        "isHide": true,
         "isFull": false,
         "isAffix": false,
         "isKeepAlive": false
@@ -83,7 +83,7 @@
         "icon": "Ticket",
         "title": "优惠券管理",
         "isLink": "",
-        "isHide": false,
+        "isHide": true,
         "isFull": false,
         "isAffix": false,
         "isKeepAlive": false
@@ -171,7 +171,22 @@
           "component": "/ticketManagement/detail",
           "meta": {
             "icon": "Menu",
-            "title": "券管理详情",
+            "title": "代金券详情",
+            "activeMenu": "/ticketManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/ticketManagement/couponDetail",
+          "name": "ticketManagementCouponDetail",
+          "component": "/ticketManagement/couponDetail",
+          "meta": {
+            "icon": "Menu",
+            "title": "优惠券详情",
             "activeMenu": "/ticketManagement",
             "isLink": "",
             "isHide": true,
@@ -190,7 +205,7 @@
         "icon": "Money",
         "title": "代金券管理",
         "isLink": "",
-        "isHide": false,
+        "isHide": true,
         "isFull": false,
         "isAffix": false,
         "isKeepAlive": false
@@ -392,7 +407,7 @@
           "name": "businessLicense",
           "component": "/licenseManagement/businessLicense",
           "meta": {
-            "icon": "Document",
+            "icon": "Tickets",
             "title": "营业执照",
             "isLink": "",
             "isHide": false,
@@ -406,7 +421,7 @@
           "name": "contractManagement",
           "component": "/licenseManagement/contractManagement",
           "meta": {
-            "icon": "Picture",
+            "icon": "FolderChecked",
             "title": "合同管理",
             "isLink": "",
             "isHide": false,
@@ -420,7 +435,7 @@
           "name": "foodBusinessLicense",
           "component": "/licenseManagement/foodBusinessLicense",
           "meta": {
-            "icon": "Document",
+            "icon": "Memo",
             "title": "食品经营许可证",
             "isLink": "",
             "isHide": false,
@@ -460,13 +475,96 @@
           }
         },
         {
-          "path": "/financialManagementDetail",
-          "name": "financialManagementDetail",
-          "component": "/financialManagement/detail",
+          "path": "/financialManagement/realName",
+          "name": "realName",
+          "component": "/financialManagement/realName",
           "meta": {
-            "icon": "Menu",
-            "title": "财务管理详情",
-            "activeMenu": "/financialManagement",
+            "icon": "CreditCard",
+            "title": "实名认证",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/financialManagement/cashApply",
+          "name": "cashApply",
+          "component": "/financialManagement/cashApply",
+          "meta": {
+            "icon": "CreditCard",
+            "title": "提现申请",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/financialManagement/todayIncomeList",
+          "name": "todayIncomeList",
+          "component": "/financialManagement/todayIncomeList",
+          "meta": {
+            "icon": "CreditCard",
+            "title": "今日收益",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/financialManagement/reconciled",
+          "name": "reconciled",
+          "component": "/financialManagement/reconciled",
+          "meta": {
+            "icon": "CreditCard",
+            "title": "已到账期金额",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/financialManagement/unposted",
+          "name": "unposted",
+          "component": "/financialManagement/unposted",
+          "meta": {
+            "icon": "CreditCard",
+            "title": "未到账期金额",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/financialManagement/withdrawaRecord",
+          "name": "withdrawaRecord",
+          "component": "/financialManagement/withdrawaRecord",
+          "meta": {
+            "icon": "CreditCard",
+            "title": "提现记录",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/financialManagement/withdrawaRequest",
+          "name": "withdrawaRequest",
+          "component": "/financialManagement/withdrawaRequest",
+          "meta": {
+            "icon": "CreditCard",
+            "title": "提现申请",
             "isLink": "",
             "isHide": true,
             "isFull": false,

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

@@ -19,6 +19,7 @@
         <slot name="tableHeader" :selected-list="selectedList" :selected-list-ids="selectedListIds" :is-selected="isSelected" />
       </div>
       <div v-if="toolButton" class="header-button-ri">
+        <slot name="tableHeaderRight" />
         <slot name="toolButton">
           <el-button v-if="showToolButton('refresh')" :icon="Refresh" circle @click="getTableList" />
           <el-button v-if="showToolButton('setting') && columns.length" :icon="Operation" circle @click="openColSetting" />
@@ -82,8 +83,8 @@
       <template #empty>
         <div class="table-empty">
           <slot name="empty">
-            <img src="@/assets/images/notData.png" alt="notData" style=" width: 180px;height: 180px" />
-            <div style=" font-size: 14px;font-weight: 500; color: #aaaaaa">暂无数据</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>

+ 3 - 49
src/layouts/LayoutClassic/index.vue

@@ -39,7 +39,7 @@
 </template>
 
 <script setup lang="ts" name="layoutClassic">
-import { computed, ref, watch, nextTick } from "vue";
+import { computed, ref } from "vue";
 import { useRoute } from "vue-router";
 import { useAuthStore } from "@/stores/modules/auth";
 import { useGlobalStore } from "@/stores/modules/global";
@@ -48,6 +48,7 @@ import SubMenu from "@/layouts/components/Menu/SubMenu.vue";
 import ToolBarLeft from "@/layouts/components/Header/ToolBarLeft.vue";
 import ToolBarRight from "@/layouts/components/Header/ToolBarRight.vue";
 import type { ElMenu } from "element-plus";
+import { useOpenedMenus } from "@/layouts/hooks/useOpenedMenus";
 
 const title = import.meta.env.VITE_GLOB_APP_TITLE;
 
@@ -60,54 +61,7 @@ const menuList = computed(() => authStore.showMenuListGet);
 const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
 
 const menuRef = ref<InstanceType<typeof ElMenu>>();
-const openedMenus = ref<string[]>([]);
-
-// 计算应该展开的父菜单路径
-const getParentMenuPath = (path: string) => {
-  const pathSegments = path.split("/").filter(Boolean);
-  // 只有当路径有两段或以上时才返回父菜单路径
-  // 例如: /licenseManagement/businessLicense -> /licenseManagement
-  if (pathSegments.length > 1) {
-    return `/${pathSegments[0]}`;
-  }
-  return "";
-};
-
-// 根据当前路由计算应该展开的父菜单
-const calculateOpenedMenus = () => {
-  const currentPath = (route.meta.activeMenu as string) || route.path;
-  const parentPath = getParentMenuPath(currentPath);
-
-  if (parentPath) {
-    // 如果手风琴模式开启,只保留当前父菜单
-    if (accordion.value) {
-      return [parentPath];
-    } else {
-      // 如果手风琴模式关闭,保留已有的展开菜单,并添加当前父菜单
-      const newOpenedMenus = [...openedMenus.value];
-      if (!newOpenedMenus.includes(parentPath)) {
-        newOpenedMenus.push(parentPath);
-      }
-      return newOpenedMenus;
-    }
-  }
-  return openedMenus.value;
-};
-
-// 监听路由变化,保持父菜单展开
-watch(
-  () => [route.path, route.meta.activeMenu],
-  () => {
-    nextTick(() => {
-      const newOpenedMenus = calculateOpenedMenus();
-      // 只有当展开菜单发生变化时才更新
-      if (JSON.stringify(newOpenedMenus.sort()) !== JSON.stringify(openedMenus.value.sort())) {
-        openedMenus.value = newOpenedMenus;
-      }
-    });
-  },
-  { immediate: true }
-);
+const { openedMenus } = useOpenedMenus({ route, accordion });
 </script>
 
 <style scoped lang="scss">

+ 3 - 0
src/layouts/LayoutColumns/index.vue

@@ -30,6 +30,7 @@
         <el-menu
           :router="false"
           :default-active="activeMenu"
+          :default-openeds="openedMenus"
           :collapse="isCollapse"
           :unique-opened="accordion"
           :collapse-transition="false"
@@ -57,6 +58,7 @@ import Main from "@/layouts/components/Main/index.vue";
 import ToolBarLeft from "@/layouts/components/Header/ToolBarLeft.vue";
 import ToolBarRight from "@/layouts/components/Header/ToolBarRight.vue";
 import SubMenu from "@/layouts/components/Menu/SubMenu.vue";
+import { useOpenedMenus } from "@/layouts/hooks/useOpenedMenus";
 
 const title = import.meta.env.VITE_GLOB_APP_TITLE;
 
@@ -71,6 +73,7 @@ const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu
 
 const subMenuList = ref<Menu.MenuOptions[]>([]);
 const splitActive = ref("");
+const { openedMenus } = useOpenedMenus({ route, accordion });
 watch(
   () => [menuList, route],
   () => {

+ 3 - 49
src/layouts/LayoutVertical/index.vue

@@ -33,7 +33,7 @@
 </template>
 
 <script setup lang="ts" name="layoutVertical">
-import { computed, ref, watch, nextTick } from "vue";
+import { computed, ref } from "vue";
 import { useRoute } from "vue-router";
 import { useAuthStore } from "@/stores/modules/auth";
 import { useGlobalStore } from "@/stores/modules/global";
@@ -42,6 +42,7 @@ import ToolBarLeft from "@/layouts/components/Header/ToolBarLeft.vue";
 import ToolBarRight from "@/layouts/components/Header/ToolBarRight.vue";
 import SubMenu from "@/layouts/components/Menu/SubMenu.vue";
 import type { ElMenu } from "element-plus";
+import { useOpenedMenus } from "@/layouts/hooks/useOpenedMenus";
 
 const title = import.meta.env.VITE_GLOB_APP_TITLE;
 
@@ -54,54 +55,7 @@ const menuList = computed(() => authStore.showMenuListGet);
 const activeMenu = computed(() => (route.meta.activeMenu ? route.meta.activeMenu : route.path) as string);
 
 const menuRef = ref<InstanceType<typeof ElMenu>>();
-const openedMenus = ref<string[]>([]);
-
-// 计算应该展开的父菜单路径
-const getParentMenuPath = (path: string) => {
-  const pathSegments = path.split("/").filter(Boolean);
-  // 只有当路径有两段或以上时才返回父菜单路径
-  // 例如: /licenseManagement/businessLicense -> /licenseManagement
-  if (pathSegments.length > 1) {
-    return `/${pathSegments[0]}`;
-  }
-  return "";
-};
-
-// 根据当前路由计算应该展开的父菜单
-const calculateOpenedMenus = () => {
-  const currentPath = (route.meta.activeMenu as string) || route.path;
-  const parentPath = getParentMenuPath(currentPath);
-
-  if (parentPath) {
-    // 如果手风琴模式开启,只保留当前父菜单
-    if (accordion.value) {
-      return [parentPath];
-    } else {
-      // 如果手风琴模式关闭,保留已有的展开菜单,并添加当前父菜单
-      const newOpenedMenus = [...openedMenus.value];
-      if (!newOpenedMenus.includes(parentPath)) {
-        newOpenedMenus.push(parentPath);
-      }
-      return newOpenedMenus;
-    }
-  }
-  return openedMenus.value;
-};
-
-// 监听路由变化,保持父菜单展开
-watch(
-  () => [route.path, route.meta.activeMenu],
-  () => {
-    nextTick(() => {
-      const newOpenedMenus = calculateOpenedMenus();
-      // 只有当展开菜单发生变化时才更新
-      if (JSON.stringify(newOpenedMenus.sort()) !== JSON.stringify(openedMenus.value.sort())) {
-        openedMenus.value = newOpenedMenus;
-      }
-    });
-  },
-  { immediate: true }
-);
+const { openedMenus } = useOpenedMenus({ route, accordion });
 </script>
 
 <style scoped lang="scss">

+ 88 - 5
src/layouts/components/Header/components/Avatar.vue

@@ -1,7 +1,7 @@
 <template>
   <el-dropdown trigger="click">
     <div class="avatar">
-      <img src="@/assets/images/avatar.gif" alt="avatar" />
+      <img :src="avatarUrl" alt="avatar" />
     </div>
     <template #dropdown>
       <el-dropdown-menu>
@@ -18,24 +18,96 @@
     </template>
   </el-dropdown>
   <!-- infoDialog -->
-  <InfoDialog ref="infoRef" />
+  <InfoDialog ref="infoRef" @avatar-updated="getUserInfo" />
   <!-- passwordDialog -->
   <PasswordDialog ref="passwordRef" />
 </template>
 
 <script setup lang="ts">
-import { ref } from "vue";
+import { ref, onMounted, computed } from "vue";
 import { LOGIN_URL } from "@/config";
 import { useRouter } from "vue-router";
 import { logoutApi } from "@/api/modules/login";
 import { useUserStore } from "@/stores/modules/user";
+import { useAuthStore } from "@/stores/modules/auth";
 import { ElMessageBox, ElMessage } from "element-plus";
 import InfoDialog from "./InfoDialog.vue";
 import PasswordDialog from "./PasswordDialog.vue";
+import { getMerchantByPhone } from "@/api/modules/homeEntry";
+import { localGet, localSet } from "@/utils/index";
+import { resetRouter } from "@/routers/index";
+import defaultAvatar from "@/assets/images/avatar.gif";
 import { localClear } from "@/utils";
 
 const router = useRouter();
 const userStore = useUserStore();
+const authStore = useAuthStore();
+
+// 头像URL,先从本地存储读取,避免刷新后显示默认图片
+const getCachedAvatar = () => {
+  const userInfo = localGet("geeker-user")?.userInfo || {};
+  const phone = userInfo.phone;
+  if (phone) {
+    const cachedAvatar = localGet(`avatar_${phone}`);
+    return cachedAvatar || "";
+  }
+  return "";
+};
+
+const headImg = ref<string>(getCachedAvatar());
+
+// 计算头像URL,如果有上传的头像则使用上传的,否则使用默认头像
+const avatarUrl = computed(() => {
+  console.log(headImg.value);
+  return headImg.value && headImg.value !== null && headImg.value !== "" ? headImg.value : defaultAvatar;
+});
+
+// 获取用户信息
+const getUserInfo = async () => {
+  const userInfo = localGet("geeker-user")?.userInfo || {};
+  if (userInfo.phone) {
+    try {
+      const res: any = await getMerchantByPhone({ phone: userInfo.phone });
+      console.log("API返回数据:", res);
+      if (res.code == 200 && res.data) {
+        // 兼容 headImg 和 avatar 两种字段名
+        const avatarUrl = res.data.headImg || res.data.avatar || "";
+        if (avatarUrl && avatarUrl.trim() !== "") {
+          headImg.value = avatarUrl;
+          // 缓存头像URL到本地存储
+          localSet(`avatar_${userInfo.phone}`, avatarUrl);
+          console.log("设置头像URL:", headImg.value);
+        } else {
+          console.warn("API返回的头像URL为空,保持使用缓存或默认头像");
+          // 如果API返回空,但本地有缓存,保持使用缓存
+          if (!headImg.value) {
+            const cached = getCachedAvatar();
+            if (cached) {
+              headImg.value = cached;
+            }
+          }
+        }
+      } else {
+        console.warn("获取用户信息失败,响应码:", res.code);
+      }
+    } catch (error) {
+      // API调用失败时,如果本地有缓存,继续使用缓存的头像
+      if (!headImg.value) {
+        const cached = getCachedAvatar();
+        if (cached) {
+          headImg.value = cached;
+        }
+      }
+    }
+  } else {
+    console.warn("用户信息中没有phone字段");
+  }
+};
+
+// 组件挂载时获取用户信息
+onMounted(() => {
+  getUserInfo();
+});
 
 // 退出登录
 const logout = () => {
@@ -47,10 +119,21 @@ const logout = () => {
     // 1.执行退出登录接口
     // await logoutApi();
 
-    // 2.清除 Token
+    // 2.清除动态路由
+    resetRouter();
+
+    // 3.清除菜单列表和权限信息
+    authStore.authMenuList = [];
+    authStore.authButtonList = {};
+    authStore.routeName = "";
+
+    // 4.清除 Token 和用户信息
     userStore.setToken("");
-    localClear();
     // 3.重定向到登陆页
+    userStore.setUserInfo({ name: "" });
+    localClear();
+
+    // 5.重定向到登陆页
     router.replace(LOGIN_URL);
     ElMessage.success("退出登录成功!");
   });

+ 6 - 0
src/layouts/components/Header/components/InfoDialog.vue

@@ -42,6 +42,10 @@ import { localGet } from "@/utils/index";
 import defaultAvatar from "@/assets/images/avatar.gif";
 import { head } from "lodash";
 
+const emit = defineEmits<{
+  avatarUpdated: [];
+}>();
+
 const userInfo = localGet("geeker-user")?.userInfo || {};
 const dialogVisible = ref(false);
 
@@ -121,6 +125,8 @@ const getSaveUserInfo = async () => {
       dialogVisible.value = false;
       // 可以刷新用户信息
       getUserInfo();
+      // 通知父组件头像已更新
+      emit("avatarUpdated");
     } else {
       ElMessage.error(res.msg || "保存失败,请重试");
     }

+ 23 - 4
src/layouts/components/Header/components/Message.vue

@@ -18,11 +18,18 @@ const unreadCount = ref<number>(0);
 // 获取未读消息数量
 const getUnreadCount = async () => {
   try {
-    const receiverId = localGet("iphone") || localGet("geeker-user")?.userInfo?.phone || "";
-    if (!receiverId) return;
+    const phone = localGet("iphone") || localGet("geeker-user")?.userInfo?.phone || "";
+    if (!phone) return;
 
-    const res: any = await getNoticeList({ pageNum: 1, pageSize: 100, receiverId });
-    if (res.code == "200") {
+    // 使用与 notice.vue 相同的 receiverId 格式
+    const receiverId = "store_" + phone;
+    const res: any = await getNoticeList({
+      pageNum: 1,
+      pageSize: 100,
+      receiverId,
+      noticeType: 1
+    });
+    if (res.code == 200 || res.code == "200") {
       const noticeList = res.data?.records || res.data?.list || [];
       // 计算未读消息数量
       unreadCount.value = noticeList.filter((item: any) => !item.isRead).length;
@@ -51,3 +58,15 @@ onBeforeUnmount(() => {
   mittBus.off("updateUnreadCount");
 });
 </script>
+
+<style scoped lang="scss">
+.message {
+  display: flex;
+  align-items: center;
+  .item {
+    :deep(.el-badge__content) {
+      border: none;
+    }
+  }
+}
+</style>

+ 65 - 0
src/layouts/hooks/useOpenedMenus.ts

@@ -0,0 +1,65 @@
+import { ref, watch } from "vue";
+import type { Ref } from "vue";
+import type { RouteLocationNormalizedLoaded } from "vue-router";
+
+interface UseOpenedMenusOptions {
+  route: RouteLocationNormalizedLoaded;
+  accordion: Ref<boolean>;
+}
+
+export const useOpenedMenus = ({ route, accordion }: UseOpenedMenusOptions) => {
+  const openedMenus = ref<string[]>([]);
+
+  const stringify = (arr: string[]) => JSON.stringify([...arr].sort());
+
+  const normalizePath = (path: string) => (path.startsWith("/") ? path : `/${path}`);
+
+  const collectAncestorPaths = (path?: string) => {
+    if (!path) return [];
+    const normalized = normalizePath(path);
+    const segments = normalized.split("/").filter(Boolean);
+    return segments.reduce<string[]>((acc, _, index) => {
+      if (index === segments.length - 1) return acc;
+      acc.push(`/${segments.slice(0, index + 1).join("/")}`);
+      return acc;
+    }, []);
+  };
+
+  const buildTargetOpenedPaths = () => {
+    const targetSet = new Set<string>();
+    collectAncestorPaths(route.path).forEach(path => targetSet.add(path));
+    const activeMenuPath = route.meta.activeMenu as string | undefined;
+    if (activeMenuPath) {
+      const normalizedActive = normalizePath(activeMenuPath);
+      collectAncestorPaths(normalizedActive).forEach(path => targetSet.add(path));
+      targetSet.add(normalizedActive);
+    }
+    return Array.from(targetSet);
+  };
+
+  const calculateOpenedMenus = () => {
+    const targetPaths = buildTargetOpenedPaths();
+    if (!targetPaths.length) {
+      return accordion.value ? [] : [...openedMenus.value];
+    }
+    if (accordion.value) return targetPaths;
+    const menuSet = new Set(openedMenus.value);
+    targetPaths.forEach(path => menuSet.add(path));
+    return Array.from(menuSet);
+  };
+
+  watch(
+    () => [route.path, route.meta.activeMenu, accordion.value],
+    () => {
+      const newOpenedMenus = calculateOpenedMenus();
+      if (stringify(newOpenedMenus) !== stringify(openedMenus.value)) {
+        openedMenus.value = newOpenedMenus;
+      }
+    },
+    { immediate: true }
+  );
+
+  return {
+    openedMenus
+  };
+};

+ 2 - 2
src/stores/modules/auth.ts

@@ -37,8 +37,8 @@ export const useAuthStore = defineStore({
 
       const hasPermission = await usePermission();
       if (!hasPermission) {
-        // 根据权限隐藏"门店装修"和"财务管理"菜单
-        const hideMenuNames = ["storeDecoration", "financialManagement"];
+        // 根据权限隐藏"门店装修"、"财务管理"和"证照管理"菜单
+        const hideMenuNames = ["storeDecoration", "financialManagement", "licenseManagement"];
         // 递归查找并隐藏指定菜单
         const hideMenus = (menuList: any[]) => {
           menuList.forEach(menu => {

+ 30 - 1
src/utils/eleValidate.ts

@@ -205,12 +205,37 @@ export function validateDateRange(
   rangeErrorMessage: string = "开始时间必须早于结束时间",
   checkToday: boolean = true
 ) {
-  return (rule: any, value: any, callback: any, isStartDate: boolean = true) => {
+  return (rule: any, value: any, callback: any) => {
     if (!value) {
       callback();
       return;
     }
     const selectedDate = new Date(value);
+    selectedDate.setHours(0, 0, 0, 0);
+
+    // 自动判断是开始日期还是结束日期
+    // 优先通过值比较判断(最可靠),其次通过字段名判断
+    const startDateValue = getStartDate();
+    const endDateValue = getEndDate ? getEndDate() : null;
+    const fieldName = rule?.field || "";
+
+    let isStartDate = false;
+
+    // 优先通过值比较判断
+    if (value === startDateValue) {
+      isStartDate = true;
+    } else if (endDateValue && value === endDateValue) {
+      isStartDate = false;
+    } else if (fieldName) {
+      // 如果值比较无法判断,则通过字段名判断
+      // 如果字段名包含 "begin"、"start"、"开始" 等关键词,认为是开始日期
+      const lowerFieldName = fieldName.toLowerCase();
+      isStartDate = lowerFieldName.includes("begin") || lowerFieldName.includes("start") || lowerFieldName.includes("开始");
+      // 如果字段名包含 "end"、"结束" 等关键词,明确认为是结束日期
+      if (lowerFieldName.includes("end") || lowerFieldName.includes("结束")) {
+        isStartDate = false;
+      }
+    }
 
     // 验证不能早于今天
     if (checkToday) {
@@ -224,18 +249,22 @@ export function validateDateRange(
 
     // 验证日期范围
     if (isStartDate && getEndDate) {
+      // 验证开始日期:开始日期必须早于结束日期
       const endDate = getEndDate();
       if (endDate) {
         const end = new Date(endDate);
+        end.setHours(0, 0, 0, 0);
         if (selectedDate >= end) {
           callback(new Error(rangeErrorMessage));
           return;
         }
       }
     } else if (!isStartDate && getStartDate) {
+      // 验证结束日期:结束日期必须晚于开始日期
       const startDate = getStartDate();
       if (startDate) {
         const start = new Date(startDate);
+        start.setHours(0, 0, 0, 0);
         if (selectedDate <= start) {
           callback(new Error(rangeErrorMessage));
           return;

+ 22 - 0
src/utils/formatCurrency.ts

@@ -0,0 +1,22 @@
+export function formatCurrency(num, decimalPlaces = 2, prefix = "", suffix = "") {
+  // 处理非数字情况
+  if (isNaN(num)) return "0.00";
+
+  // 转换为数字并四舍五入
+  const number = Number(num);
+  const rounded = Math.round(number * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces);
+
+  // 分割整数和小数部分
+  const parts = rounded.toString().split(".");
+  const integerPart = parts[0];
+  const decimalPart = parts[1] || "";
+
+  // 整数部分添加千位分隔符
+  const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+
+  // 处理小数部分(补零)
+  const formattedDecimal = decimalPart.padEnd(decimalPlaces, "0").slice(0, decimalPlaces);
+
+  // 组合结果
+  return `${prefix}${formattedInteger}${decimalPlaces > 0 ? "." + formattedDecimal : ""}${suffix}`;
+}

+ 458 - 0
src/views/financialManagement/cashApply.vue

@@ -0,0 +1,458 @@
+<template>
+  <div class="cash-apply-page">
+    <!-- 头部 -->
+    <div class="header">
+      <el-button class="back-btn" @click="goBack"> 返回 </el-button>
+      <h2 class="title">提现申请</h2>
+    </div>
+
+    <!-- 内容区域 -->
+    <div class="content">
+      <!-- 可提现金额 -->
+      <div class="section">
+        <div class="section-title">可提现金额</div>
+        <div class="available-amount">¥{{ formatCurrency(availableAmount) }}</div>
+      </div>
+
+      <!-- 提现金额输入 -->
+      <div class="section">
+        <div class="section-title">提现金额</div>
+        <div class="amount-input-wrapper">
+          <el-input
+            v-model="withdrawAmount"
+            placeholder="请输入提现金额"
+            class="amount-input"
+            type="number"
+            :min="0.01"
+            :max="availableAmount"
+            @input="handleAmountInput"
+          />
+          <el-button type="primary" class="withdraw-all-btn" @click="handleWithdrawAll"> 全部提现 </el-button>
+        </div>
+        <div class="min-amount-tip">最低提现金额为0.01元</div>
+      </div>
+
+      <!-- 提现账户 -->
+      <div class="section">
+        <div class="section-title">提现账户</div>
+        <div class="account-list">
+          <div class="account-item" @click="selectAccount">
+            <div class="account-left">
+              <img :src="zfbIcon" alt="" class="account-icon" />
+              <div class="account-info">
+                <div class="account-name">
+                  {{ userInfo.name }}
+                </div>
+                <div class="account-number">
+                  {{ userInfo.alipayAccount }}
+                </div>
+              </div>
+            </div>
+            <div class="account-right">
+              <div class="selected-indicator" />
+            </div>
+          </div>
+        </div>
+      </div>
+      <!-- 底部按钮 -->
+      <div class="footer">
+        <el-button type="primary" class="submit-btn" @click="handleSubmit"> 提交申请11 </el-button>
+      </div>
+    </div>
+    <!-- 确认对话框 -->
+    <el-dialog
+      v-model="cashDialogVisible"
+      title="提现金额域账户确认"
+      width="500px"
+      :close-on-click-modal="false"
+      class="confirm-dialog"
+    >
+      <div class="confirm-content">
+        <div class="confirm-item">
+          <span class="confirm-label">提现金额:</span>
+          <span class="confirm-value">¥ {{ withdrawAmount || "0" }}</span>
+        </div>
+        <div class="confirm-item">
+          <span class="confirm-label">提现账户:</span>
+          <span class="confirm-value">支付宝账户 ({{ userInfo.alipayAccount }})</span>
+        </div>
+        <div class="password-section">
+          <div class="password-label">请输入提现密码</div>
+          <el-input
+            v-model="withdrawPassword"
+            type="password"
+            placeholder="请输入6位提现密码"
+            maxlength="6"
+            show-password
+            class="password-input"
+            @input="handlePasswordInput"
+          />
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button class="dialog-cancel-btn" @click="handleCancelConfirm"> 取消 </el-button>
+          <el-button type="primary" class="dialog-confirm-btn" @click="handleConfirmSubmit" :loading="submitLoading">
+            确认
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { getAccountBalance, cashOut } from "@/api/modules/homeEntry";
+import zfbIcon from "@/assets/financial/zfb-icon.png";
+import { localGet } from "@/utils";
+const userInfo = localGet("geeker-user").userInfo;
+
+const router = useRouter();
+const cashDialogVisible = ref(false);
+const withdrawPassword = ref("");
+// 提现金额
+const withdrawAmount = ref<string>("");
+// 密码输入处理(只允许数字)
+const handlePasswordInput = (value: string) => {
+  withdrawPassword.value = value.replace(/\D/g, "");
+};
+// 取消确认
+const handleCancelConfirm = () => {
+  cashDialogVisible.value = false;
+  withdrawPassword.value = "";
+};
+const handleConfirmSubmit = async () => {
+  const res: any = await cashOut({
+    storeId: userInfo.storeId,
+    payPassword: withdrawPassword.value,
+    withdrawalMoney: Number(withdrawAmount.value) * 100
+  });
+  if (res.code == 200) {
+    ElMessage.success("提现申请已发起成功,请耐心等待");
+    cashDialogVisible.value = false;
+  } else {
+    ElMessage.error(res.message);
+  }
+};
+// 可提现金额
+const availableAmount = ref<number>(0);
+
+// 账户列表
+const accountList = ref({});
+
+// 选中的账户ID
+const selectedAccountId = ref<number>(1);
+
+// 返回
+const goBack = () => {
+  router.back();
+};
+
+// 格式化金额
+const formatCurrency = (value: number) => {
+  return value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+};
+
+// 全部提现
+const handleWithdrawAll = () => {
+  withdrawAmount.value = availableAmount.value.toString();
+};
+
+// 金额输入处理
+const handleAmountInput = (value: string) => {
+  const numValue = parseFloat(value);
+  if (isNaN(numValue) || numValue < 0) {
+    withdrawAmount.value = "";
+    return;
+  }
+  if (numValue > availableAmount.value) {
+    withdrawAmount.value = availableAmount.value.toString();
+    ElMessage.warning("提现金额不能超过可提现金额");
+  }
+};
+
+// 选择账户
+const selectAccount = (id: number) => {
+  selectedAccountId.value = id;
+};
+
+// 提交申请
+const handleSubmit = () => {
+  const amount = parseFloat(withdrawAmount.value);
+
+  // 验证提现金额
+  if (!withdrawAmount.value || isNaN(amount) || amount <= 0) {
+    ElMessage.warning("请输入提现金额");
+    return;
+  }
+
+  if (amount < 0.01) {
+    ElMessage.warning("最低提现金额为0.01元");
+    return;
+  }
+
+  if (amount > availableAmount.value) {
+    ElMessage.warning("提现金额不能超过可提现金额");
+    return;
+  }
+
+  // 验证账户
+  if (!selectedAccountId.value) {
+    ElMessage.warning("请选择提现账户");
+    return;
+  }
+  cashDialogVisible.value = true;
+};
+
+// 获取可提现金额
+const fetchAccountBalance = async () => {
+  console.log();
+  let param = {
+    storeId: userInfo.storeId
+  };
+  const res: any = await getAccountBalance(param as any);
+  console.log(res);
+  if (res.code == 200 && res.data) {
+    availableAmount.value = res.data.cashOutMoney;
+    // 根据实际API返回的数据结构更新
+    // availableAmount.value = res.data.availableAmount || res.data.amount || 586;
+  }
+};
+
+onMounted(() => {
+  fetchAccountBalance();
+});
+</script>
+
+<style scoped lang="scss">
+.cash-apply-page {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  min-height: 100vh;
+  background-color: #ffffff;
+}
+.header {
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  border-bottom: 1px solid #e4e7ed;
+  .back-btn {
+    padding: 8px 16px;
+    font-size: 14px;
+    color: #606266;
+    background-color: #f5f7fa;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    &:hover {
+      color: #409eff;
+      background-color: #ecf5ff;
+      border-color: #b3d8ff;
+    }
+  }
+  .title {
+    position: absolute;
+    left: 50%;
+    margin: 0;
+    font-size: 18px;
+    font-weight: bold;
+    color: #333333;
+    transform: translateX(-50%);
+  }
+}
+.content {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  gap: 32px;
+  padding: 24px 20px;
+}
+.section {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  .section-title {
+    font-size: 16px;
+    font-weight: 500;
+    color: #333333;
+  }
+}
+.available-amount {
+  margin-top: 8px;
+  font-size: 36px;
+  font-weight: 600;
+  color: #333333;
+}
+.amount-input-wrapper {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  .amount-input {
+    flex: 1;
+    :deep(.el-input__wrapper) {
+      padding: 12px 16px;
+      border-radius: 4px;
+    }
+  }
+  .withdraw-all-btn {
+    padding: 12px 20px;
+    white-space: nowrap;
+    background-color: #409eff;
+    border: none;
+    border-radius: 4px;
+  }
+}
+.min-amount-tip {
+  margin-top: 4px;
+  font-size: 12px;
+  color: #909399;
+}
+.account-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  margin-top: 8px;
+}
+.account-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px;
+  cursor: pointer;
+  background-color: #ffffff;
+  border: 1px solid #dcdfe6;
+  border-radius: 8px;
+  transition: all 0.3s;
+  &:hover {
+    border-color: #409eff;
+  }
+  &.active {
+    background-color: #ecf5ff;
+    border-color: #409eff;
+  }
+  .account-left {
+    display: flex;
+    flex: 1;
+    gap: 12px;
+    align-items: center;
+    .account-icon {
+      width: 40px;
+      height: 40px;
+      object-fit: contain;
+    }
+    .account-info {
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+      .account-name {
+        font-size: 16px;
+        font-weight: 500;
+        color: #333333;
+      }
+      .account-number {
+        font-size: 14px;
+        color: #909399;
+      }
+    }
+  }
+  .account-right {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 24px;
+    height: 24px;
+    .selected-indicator {
+      width: 16px;
+      height: 16px;
+      background-color: #409eff;
+      border: 3px solid #ffffff;
+      border-radius: 50%;
+      box-shadow: 0 0 0 2px #409eff;
+    }
+  }
+}
+.footer {
+  display: flex;
+  justify-content: center;
+  margin-top: 100px;
+  .cancel-btn {
+    padding: 20px;
+    font-size: 16px;
+    color: #606266;
+    background-color: #f5f7fa;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    &:hover {
+      color: #409eff;
+      background-color: #ecf5ff;
+      border-color: #b3d8ff;
+    }
+  }
+  .submit-btn {
+    padding: 20px;
+    font-size: 16px;
+    color: #ffffff;
+    background-color: #409eff;
+    border: none;
+    border-radius: 4px;
+    &:hover {
+      background-color: #66b1ff;
+    }
+  }
+}
+:deep(.confirm-dialog) {
+  .el-dialog__header {
+    padding: 20px 24px;
+    border-bottom: 1px solid #e4e7ed;
+  }
+  .el-dialog__title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #333333;
+  }
+  .el-dialog__body {
+    padding: 24px;
+  }
+}
+.confirm-content {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+  .confirm-item {
+    display: flex;
+    gap: 12px;
+    align-items: center;
+    font-size: 14px;
+    .confirm-label {
+      min-width: 80px;
+      font-weight: 500;
+      color: #606266;
+    }
+    .confirm-value {
+      font-weight: 500;
+      color: #333333;
+    }
+  }
+  .password-section {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+    margin-top: 8px;
+    .password-label {
+      font-size: 14px;
+      font-weight: 500;
+      color: #606266;
+    }
+    .password-input {
+      :deep(.el-input__wrapper) {
+        padding: 12px 16px;
+        border-radius: 4px;
+      }
+    }
+  }
+}
+</style>

+ 227 - 26
src/views/financialManagement/index.vue

@@ -4,18 +4,21 @@
     <el-card class="summary-card" shadow="hover">
       <div class="card-img">
         <div class="summary-header">
-          <span class="title">今日收益1</span>
-          <el-tooltip content="今日收益为今天入账的金额" placement="top">
+          <span class="title">今日收益</span>
+          <el-tooltip content="今日收益=今日已核销到账金额-技术服务费" placement="top">
             <el-icon class="question-icon">
               <QuestionFilled />
             </el-icon>
           </el-tooltip>
+          <el-icon class="right-icon" @click="handleTodayIncomeList">
+            <ArrowRight />
+          </el-icon>
         </div>
         <div class="summary-body">
           <el-image :src="gold" class="gold" />
           <div class="summary-amount">
             <span class="currency">¥</span>
-            <span class="amount">{{ formatCurrency(summaryData.todayIncome) }}</span>
+            <span class="amount">{{ todayIncome }}</span>
             <span class="unit">元</span>
           </div>
         </div>
@@ -34,57 +37,204 @@
           </el-tooltip>
         </div>
         <div class="stat-amount">
-          {{ formatCurrency(stat.amount) }}
+          {{ stat.amount }}
         </div>
         <el-button type="primary" class="stat-btn" @click="handleAction(stat.key)">
           {{ stat.buttonText }}
         </el-button>
       </el-card>
     </div>
+
+    <!-- 重置按钮 -->
+    <el-card class="reset-card" shadow="hover">
+      <div class="reset-content">
+        <div class="reset-info">
+          <h3 class="reset-title">重置到刚注册状态</h3>
+          <p class="reset-desc">
+            此操作将删除所有入住申请数据、订单、优惠券、验券、消息等相关数据,恢复到刚注册时的初始状态。此操作不可恢复,请谨慎操作!
+          </p>
+        </div>
+        <el-button type="danger" class="reset-btn" @click="handleResetToInitialStatus" :loading="resetLoading">
+          重置到刚注册状态
+        </el-button>
+      </div>
+    </el-card>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, watch, onMounted } from "vue";
-import { QuestionFilled } from "@element-plus/icons-vue";
+import { ref, reactive, watch, onMounted, nextTick } from "vue";
+import { QuestionFilled, ArrowRight } from "@element-plus/icons-vue";
 import gold from "../../assets/financial/gold.png";
-import { getAccountBalance } from "@/api/modules/homeEntry";
+import {
+  getAccountBalance,
+  getTodayIncome,
+  getPaymentCycle,
+  checkPayPassword,
+  resetToInitialStatus
+} from "@/api/modules/homeEntry";
 import { localGet } from "@/utils/index";
 import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import en from "@/languages/modules/en";
+import { pa } from "element-plus/es/locale";
 const router = useRouter();
+const userInfo = localGet("geeker-user")?.userInfo || {};
 onMounted(() => {
+  fetchGetTodayIncome();
   fetchAccountBalance();
+  fetchGetPaymentCycle0();
+  fetchGetPaymentCycle1();
 });
+
+const stats = reactive([
+  {
+    key: "withdraw",
+    title: "可提现金额",
+    amount: "",
+    buttonText: "提现",
+    tooltip: "收益产生后3天可提现,28天自动付收益产生日期+3个自然日可提现,若未提现,则按收益产生日期+28个自然日自动付"
+  },
+  { key: "settled", title: "已到账期金额", amount: "", buttonText: "查看明细", tooltip: "已到达可提现时间的金额总额" },
+  {
+    key: "pending",
+    title: "未到账期金额",
+    amount: "",
+    buttonText: "查看明细",
+    tooltip: "未到达可提现时间的账单,您需要等待账单到达结算周期后发起提现"
+  }
+]);
+const handleTodayIncomeList = async () => {
+  router.push({ path: "/financialManagement/todayIncomeList" });
+};
+//已到账/未到账金额
+const fetchGetPaymentCycle0 = async () => {
+  let param = {
+    storeId: userInfo.storeId,
+    startTime: null,
+    endTime: null,
+    paymentType: 0,
+    incomeType: 0,
+    pageNum: 1,
+    pageSize: 999999
+  };
+  const res: any = await getPaymentCycle(param as any);
+  if (res.code == 200) {
+    nextTick(() => {
+      stats[1].amount = res.data.money;
+    });
+  }
+};
+const fetchGetPaymentCycle1 = async () => {
+  let param = {
+    storeId: userInfo.storeId,
+    startTime: null,
+    endTime: null,
+    paymentType: 1,
+    incomeType: 1,
+    pageNum: 1,
+    pageSize: 999999
+  };
+  const res: any = await getPaymentCycle(param as any);
+  if (res.code == 200) {
+    nextTick(() => {
+      stats[2].amount = res.data.money;
+    });
+  }
+};
 //可提现金额
 const fetchAccountBalance = async () => {
   let param = {
-    storeId: 138
+    storeId: userInfo.storeId
   };
   const res: any = await getAccountBalance(param as any);
   if (res.code == 200) {
+    nextTick(() => {
+      stats[0].amount = res.data.balance;
+    });
   }
 };
-const summaryData = {
-  todayIncome: 65411
+//今日收益
+const todayIncome = ref("");
+const fetchGetTodayIncome = async () => {
+  let param = {
+    storeId: userInfo.storeId
+  };
+  const res: any = await getTodayIncome(param as any);
+  if (res.code == 200) {
+    todayIncome.value = res.data;
+  }
 };
 
-const stats = [
-  { key: "withdraw", title: "可提现金额", amount: 932110, buttonText: "提现", tooltip: "可用于提现的金额" },
-  { key: "settled", title: "已到账期金额", amount: 932110, buttonText: "查看明细", tooltip: "已结算到账的金额" },
-  { key: "pending", title: "未到账期金额", amount: 932110, buttonText: "查看明细", tooltip: "正在结算中的金额" }
-];
-
-const formatCurrency = (value: number) => {
-  return value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+const handleAction = async (key: string) => {
+  if (key == "withdraw") {
+    if (userInfo.idCard == null) {
+      router.push({ path: "/financialManagement/realName" });
+    } else {
+      let param = {
+        storeUserId: userInfo.id
+      };
+      const res: any = await checkPayPassword(param as any);
+      if (res.code == 200) {
+        ElMessage.error(res.data.message);
+        router.push({ path: "/financialManagement/realName" });
+        return;
+        if (res.data.data == "false") {
+          ElMessage.error(res.data.message);
+          router.push({ path: "/financialManagement/realName" });
+        } else {
+          router.push({ path: "/financialManagement/withdrawaRequest" });
+        }
+      }
+    }
+  }
+  if (key == "settled") {
+    router.push({ path: "/financialManagement/reconciled" });
+  }
+  if (key == "pending") {
+    router.push({ path: "/financialManagement/unposted" });
+  }
 };
 
-const handleAction = (key: string) => {
-  switch (key) {
-    case "withdraw":
-      router.push("/financialManagement/realName");
-      break;
-    default:
-      console.log("查看明细");
+// 重置到刚注册状态
+const resetLoading = ref(false);
+const handleResetToInitialStatus = async () => {
+  try {
+    // 二次确认
+    await ElMessageBox.confirm(
+      "此操作将删除所有入住申请数据、订单、优惠券、验券、消息等相关数据,恢复到刚注册时的初始状态。此操作不可恢复,是否继续?",
+      "确认重置",
+      {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+        dangerouslyUseHTMLString: false
+      }
+    );
+
+    resetLoading.value = true;
+    const res: any = await resetToInitialStatus();
+
+    if (res.code === 200 && res.success) {
+      ElMessage.success(res.msg || "重置成功,已退回到刚注册状态");
+      // 刷新页面数据或跳转到注册成功页面
+      setTimeout(() => {
+        // 可以跳转到注册成功页面或刷新当前页面
+        // router.push({ path: '/register-success' });
+        window.location.reload();
+      }, 1500);
+    } else {
+      ElMessage.error(res.msg || "重置失败,请稍后重试");
+    }
+  } catch (error: any) {
+    // 用户取消操作
+    if (error === "cancel") {
+      return;
+    }
+    console.error("重置失败:", error);
+    ElMessage.error(error?.response?.data?.msg || error?.message || "重置失败,请稍后重试");
+  } finally {
+    resetLoading.value = false;
   }
 };
 </script>
@@ -117,7 +267,13 @@ const handleAction = (key: string) => {
     font-size: 16px;
     color: #333333;
     .question-icon {
-      color: #9aa4c2;
+      color: #7293f8;
+      cursor: pointer;
+    }
+    .right-icon {
+      margin-left: 30px;
+      font-weight: bold;
+      color: #7293f8;
       cursor: pointer;
     }
   }
@@ -193,4 +349,49 @@ const handleAction = (key: string) => {
     grid-template-columns: repeat(1, 1fr);
   }
 }
+.reset-card {
+  width: 100%;
+  border: none;
+  border-radius: 12px;
+  .reset-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 20px;
+    .reset-info {
+      flex: 1;
+      .reset-title {
+        margin: 0 0 8px;
+        font-size: 18px;
+        font-weight: 600;
+        color: #222222;
+      }
+      .reset-desc {
+        margin: 0;
+        font-size: 14px;
+        line-height: 1.6;
+        color: #666666;
+      }
+    }
+    .reset-btn {
+      padding: 12px 32px;
+      margin-left: 24px;
+      font-size: 16px;
+    }
+  }
+}
+
+@media (width <= 768px) {
+  .reset-card {
+    .reset-content {
+      flex-direction: column;
+      gap: 16px;
+      align-items: flex-start;
+      .reset-btn {
+        width: 100%;
+        margin-left: 0;
+      }
+    }
+  }
+}
 </style>

+ 621 - 140
src/views/financialManagement/realName.vue

@@ -8,7 +8,8 @@
           <span class="shop-name">每天汪汪的店铺</span>
         </div>
         <div class="auth-status">
-          <span class="status-badge">未认证</span>
+          <span class="status-badge" v-if="!userInfo.idCard">未认证</span>
+          <span class="status-badge status-badge-success" v-if="userInfo.idCard">已认证</span>
         </div>
       </div>
     </div>
@@ -24,16 +25,64 @@
 
     <!-- 实名认证 -->
     <div class="content-section" v-if="activeTab === 'realName'">
-      <div class="content-wrapper">
-        <el-button type="primary" class="auth-button" @click="handleGoAuth"> 去实名认证 </el-button>
+      <div class="content-wrapper-realName">
+        <div class="goAuthBtn" v-if="goAuth">
+          <el-button type="primary" class="auth-button" @click="handleGoAuth">
+            <el-icon><CirclePlus /></el-icon> &nbsp;去实名认证
+          </el-button>
+        </div>
+        <div class="auth-div" v-if="!goAuth">
+          <el-row class="auth-row">
+            <el-col :span="2"> 姓名 </el-col>
+            <el-col :span="12">
+              {{ userInfo.name }}
+            </el-col>
+          </el-row>
+          <el-row class="auth-row">
+            <el-col :span="2"> 身份证号 </el-col>
+            <el-col :span="12">
+              {{ userInfo.idCard }}
+            </el-col>
+          </el-row>
+          <el-row class="auth-row">
+            <el-col :span="2"> 认证时间 </el-col>
+            <el-col :span="12">
+              {{ userInfo.createdTime }}
+            </el-col>
+          </el-row>
+        </div>
       </div>
     </div>
+    <!-- 实名认证对话框 -->
+    <el-dialog v-model="dialogVisible" title="实名认证" width="500px" :close-on-click-modal="false">
+      <el-form ref="authFormRef" :model="authForm" :rules="authRules" label-width="100px" label-position="left">
+        <el-form-item label="姓名" prop="name">
+          <el-input v-model="authForm.name" placeholder="请输入姓名" clearable />
+        </el-form-item>
+        <el-form-item label="身份证号码" prop="idCard">
+          <el-input v-model="authForm.idCard" placeholder="请输入身份证号码" clearable maxlength="18" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCancel">取消</el-button>
+          <el-button type="primary" :loading="loading" @click="handleConfirm">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
 
     <!-- 提现密码 -->
     <div class="content-section" v-if="activeTab === 'withdrawPassword'">
       <div class="content-wrapper">
-        <!-- <div class="password-form-wrapper">
-          <el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="120px" label-position="left">
+        <div class="password-form-wrapper" v-if="withdrawPass">
+          <el-form
+            ref="passwordFormRef"
+            :model="passwordForm"
+            :rules="passwordRules"
+            label-width="120px"
+            label-position="left"
+            class="form-wrapper"
+          >
             <el-form-item label="设置提现密码" prop="password">
               <el-input
                 v-model="passwordForm.password"
@@ -57,13 +106,14 @@
               />
             </el-form-item>
             <el-form-item>
-              <el-button type="primary" class="submit-button" :loading="passwordLoading" @click="handlePasswordSubmit">
+              <el-button type="primary" class="submit-button" :loading="passwordLoading" @click="handlePasswordOneSubmit">
                 确认
               </el-button>
             </el-form-item>
           </el-form>
-        </div> -->
-        <div class="password-form-wrapper-step">
+        </div>
+        <!---第二次进入此页面--->
+        <div class="password-form-wrapper-step" v-if="twoEnterPage">
           <el-steps :active="currentStep" style="max-width: 600px">
             <el-step v-for="(item, index) in passList" :key="index">
               <template #title>
@@ -80,10 +130,34 @@
             label-width="100px"
             label-position="left"
             class="form-wrapper"
+            v-if="currentStep == 1"
           >
             <el-form-item label="验证原密码" prop="password">
               <el-input
-                v-model="passwordForm.password"
+                v-model="oldPasswordForm.password"
+                type="password"
+                placeholder="请输入6位提现密码"
+                clearable
+                maxlength="6"
+                show-password
+                class="password-input"
+              />
+            </el-form-item>
+            <div class="forget-password" @click="getForgetPassword">忘记密码</div>
+            <el-button type="primary" class="submit-button" @click="nextStept"> 下一步 </el-button>
+          </el-form>
+          <el-form
+            ref="passwordFormRef"
+            :model="oldTwoPasswordForm"
+            :rules="passwordRulesTwo"
+            label-width="120px"
+            label-position="left"
+            class="form-wrapper"
+            v-if="currentStep == 2"
+          >
+            <el-form-item label="设置提现密码" prop="password">
+              <el-input
+                v-model="oldTwoPasswordForm.password"
                 type="password"
                 placeholder="请输入6位提现密码"
                 clearable
@@ -92,12 +166,63 @@
                 class="password-input"
               />
             </el-form-item>
+            <el-form-item label="请确认密码" prop="confirmPassword">
+              <el-input
+                v-model="oldTwoPasswordForm.confirmPassword"
+                type="password"
+                placeholder="请输入6位提现密码"
+                clearable
+                maxlength="6"
+                show-password
+                class="password-input"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" class="submit-button" :loading="passwordLoading" @click="handlePasswordTwoSubmit">
+                确认
+              </el-button>
+            </el-form-item>
           </el-form>
-          <div class="forget-password">忘记密码</div>
         </div>
       </div>
     </div>
+    <!--收款账户-->
+    <div class="content-section" v-if="activeTab === 'receivingAccount'">
+      <div class="content-wrapper">
+        <el-button type="primary" class="auth-button" @click="handleGoZFB" v-if="userInfo.alipayAccount == null">
+          添加支付宝账号
+        </el-button>
 
+        <div class="zfb-wrapper" v-else>
+          <div class="zfb-left">
+            <el-image :src="zfbIcon" class="zfbIcon" />
+            <div>
+              <div>支付宝</div>
+              <div>{{ userInfo.phone }}</div>
+            </div>
+          </div>
+          <div class="operate">
+            <div @click="getEditZfb">编辑</div>
+            <div @click="getDelZfb">删除</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <el-button type="primary" @click="goToCashApply"> 去提现 </el-button>
+    <!--添加支付宝账号--->
+    <el-dialog v-model="dialogVisibleZFB" :title="zfbTitle" width="500px" :close-on-click-modal="false">
+      <el-form ref="authFormRef1" :model="authForm1" :rules="authRules1" label-width="100px" label-position="left">
+        <el-form-item label="支付宝账号" prop="zfbName">
+          <el-input v-model="authForm1.zfbName" placeholder="请输入支付宝账号" clearable />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="handleCancelZFB">取消</el-button>
+          <el-button type="primary" :loading="loading" @click="handleConfirmZFB">确认</el-button>
+        </span>
+      </template>
+    </el-dialog>
     <!-- 忘记密码对话框 -->
     <el-dialog v-model="forgotPasswordVisible" title="忘记密码" width="500px" draggable :close-on-click-modal="false">
       <el-form
@@ -111,11 +236,11 @@
           <span>{{ maskedPhone }}</span>
         </el-form-item>
 
-        <el-form-item label="" prop="verificationCode">
+        <el-form-item label="验证码">
           <div style="display: flex; gap: 12px; width: 100%">
             <el-input v-model="forgotPasswordForm.verificationCode" placeholder="请输入" clearable style="flex: 1" />
-            <el-button type="primary" :disabled="codeCountdown > 0" @click="sendLoginCode">
-              {{ codeCountdown > 0 ? `${codeCountdown}秒后重新获取` : "获取验证码" }}
+            <el-button :disabled="codeCountdown > 0" @click="sendForgotCode" class="code-button">
+              {{ codeCountdown > 0 ? `${codeCountdown}秒后重` : "获取验证码" }}
             </el-button>
           </div>
         </el-form-item>
@@ -124,28 +249,25 @@
           <el-input
             v-model="forgotPasswordForm.newPassword"
             type="password"
-            placeholder="请输入6-16位的密码,包含字母和数字"
+            placeholder="请输入6位提现密码"
             show-password
             clearable
           />
         </el-form-item>
-      </el-form>
-    </el-dialog>
-
-    <!-- 实名认证对话框 -->
-    <el-dialog v-model="dialogVisible" title="实名认证" width="500px" :close-on-click-modal="false">
-      <el-form ref="authFormRef" :model="authForm" :rules="authRules" label-width="100px" label-position="left">
-        <el-form-item label="姓名" prop="name">
-          <el-input v-model="authForm.name" placeholder="请输入姓名" clearable />
-        </el-form-item>
-        <el-form-item label="身份证号码" prop="idCard">
-          <el-input v-model="authForm.idCard" placeholder="请输入身份证号码" clearable maxlength="18" />
+        <el-form-item label="确认密码" prop="newPassword">
+          <el-input
+            v-model="forgotPasswordForm.confireNewPassword"
+            type="password"
+            placeholder="请输入6位提现密码"
+            show-password
+            clearable
+          />
         </el-form-item>
       </el-form>
       <template #footer>
         <span class="dialog-footer">
-          <el-button @click="handleCancel">取消</el-button>
-          <el-button type="primary" :loading="loading" @click="handleConfirm">确认</el-button>
+          <el-button @click="handleCancelZFB">取消</el-button>
+          <el-button type="primary" :loading="loading" @click="handleConfirmZFB">确认</el-button>
         </span>
       </template>
     </el-dialog>
@@ -153,64 +275,31 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from "vue";
+import { ref, reactive, computed, onMounted } from "vue";
 import { useRouter } from "vue-router";
-import { ElMessage, type FormInstance, type FormRules } from "element-plus";
-import { localGet } from "@/utils/index";
+import { ElMessage, type FormInstance, ElMessageBox, type FormRules } from "element-plus";
+import { verifyIdInfo, checkPayPassword, setPayPassword, getMerchantByPhone, addAlipayAccount } from "@/api/modules/homeEntry";
+import { localGet, localSet } from "@/utils/index";
 import homeIcon from "../../assets/images/home-icon.png";
-
+import zfbIcon from "../../assets/financial/zfb-icon.png";
+import { lo } from "element-plus/es/locale";
+import { getPhoneCode } from "@/api/modules/newLoginApi";
+const userInfo = localGet("geeker-user").userInfo;
 const router = useRouter();
 const activeTab = ref("realName");
 const dialogVisible = ref(false);
 const loading = ref(false);
+//实名认证
+const goAuth = ref(!userInfo.idCard);
 const authFormRef = ref<FormInstance>();
-const passwordFormRef = ref<FormInstance>();
-const passwordLoading = ref(false);
-const currentStep = ref(0);
-const forgotPasswordVisible = ref(false);
-const forgotPasswordFormRef = ref<FormInstance>();
-const forgotPasswordForm = reactive({
-  verificationCode: "",
-  newPassword: ""
-});
-// 忘记密码表单验证规则
-const forgotPasswordRules: FormRules = {
-  verificationCode: [{ required: true, message: "请输入验证码", trigger: "blur" }]
-};
-const passList = [
-  {
-    title: "验证原密码"
-  },
-  {
-    title: "设置新密码"
-  }
-];
-// 获取用户手机号
-const userPhone = computed(() => {
-  return localGet("iphone") || localGet("geeker-user")?.userInfo?.phone || "";
-});
-// 掩码显示手机号
-const maskedPhone = computed(() => {
-  if (!userPhone.value || userPhone.value.length !== 11) {
-    return "";
-  }
-  return userPhone.value.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
-});
-// 实名认证表单数据
 const authForm = reactive({
   name: "",
   idCard: ""
 });
-
-// 提现密码表单数据
-const passwordForm = reactive({
-  password: "",
-  confirmPassword: ""
-});
-
 // 实名认证表单验证规则
 const authRules = reactive<FormRules>({
   name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
+  zfbName: [{ required: true, message: "请输入支付宝账号", trigger: "blur" }],
   idCard: [
     { required: true, message: "请输入身份证号码", trigger: "blur" },
     {
@@ -220,7 +309,66 @@ const authRules = reactive<FormRules>({
     }
   ]
 });
+//去实名认证
+const handleGoAuth = () => {
+  // 打开实名认证对话框
+  dialogVisible.value = true;
+  // 重置表单
+  if (authFormRef.value) {
+    authFormRef.value.resetFields();
+  }
+};
+//实名认证 确认
+const shimingInfo = ref(null);
+const handleConfirm = async () => {
+  if (!authFormRef.value) return;
 
+  // 表单验证
+  await authFormRef.value.validate(async valid => {
+    if (valid) {
+      loading.value = true;
+      try {
+        const params = {
+          name: authForm.name,
+          idCard: authForm.idCard,
+          appType: 1
+        };
+        const res: any = await verifyIdInfo(params);
+        console.log(res);
+        if (res && res.code == 200) {
+          ElMessage.success(res.msg || "实名认证提交成功");
+          let param = {
+            phone: userInfo.phone
+          };
+          const res1: any = await getMerchantByPhone(param);
+          if (res1.code == 200) {
+            dialogVisible.value = false;
+            goAuth.value = false;
+            // 重置表单
+            authFormRef.value?.resetFields();
+            shimingInfo.value = res1.data;
+            // 这里可以更新认证状态或刷新页面数据
+          }
+        } else {
+          ElMessage.error(res.msg || "实名认证失败,请重试");
+        }
+      } catch (error) {
+        console.error("实名认证失败:", error);
+        ElMessage.error("实名认证失败,请重试");
+      } finally {
+        loading.value = false;
+      }
+    }
+  });
+};
+
+//提现密码
+const withdrawPass = ref(true);
+const passwordFormRef = ref<FormInstance>();
+const passwordForm = reactive({
+  password: "",
+  confirmPassword: ""
+});
 // 提现密码表单验证规则
 const passwordRules = reactive<FormRules>({
   password: [
@@ -245,68 +393,359 @@ const passwordRules = reactive<FormRules>({
     }
   ]
 });
+// 第二步修改密码的验证规则
+const passwordRulesTwo = reactive<FormRules>({
+  password: [
+    { required: true, message: "请输入6位提现密码", trigger: "blur" },
+    {
+      pattern: /^\d{6}$/,
+      message: "请输入6位数字密码",
+      trigger: "blur"
+    }
+  ],
+  confirmPassword: [
+    { required: true, message: "请确认密码", trigger: "blur" },
+    {
+      validator: (rule: any, value: any, callback: any) => {
+        if (value !== oldTwoPasswordForm.password) {
+          callback(new Error("两次输入的密码不一致"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "blur"
+    }
+  ]
+});
+//提现密码 确认
+const passwordLoading = ref(false);
+const handlePasswordOneSubmit = async () => {
+  if (!passwordFormRef.value) return;
 
-const handleTabClick = (tab: any) => {
-  // 根据不同的标签页可以跳转到不同的页面或显示不同的内容
-  console.log("切换到标签:", tab.props.name);
-  // 这里可以根据需要实现路由跳转或内容切换
+  // 表单验证
+  await passwordFormRef.value.validate(async valid => {
+    if (valid) {
+      passwordLoading.value = true;
+      try {
+        const res: any = await setPayPassword({
+          payPassword: passwordForm.password,
+          id: userInfo.id
+        });
+        if (res.code == 200) {
+          ElMessage.success(res.msg || "提现密码设置成功");
+
+          // 更新本地存储中的提现密码
+          try {
+            const geekerUser = localGet("geeker-user");
+            if (geekerUser && geekerUser.userInfo) {
+              geekerUser.userInfo.payPassword = passwordForm.password;
+              localSet("geeker-user", geekerUser);
+            }
+          } catch (error) {
+            console.error("更新本地存储失败:", error);
+          }
+
+          // 重置表单
+          passwordFormRef.value?.resetFields();
+          // 如果是在修改密码流程中(twoEnterPage为true),设置状态为已设置
+          if (twoEnterPage.value) {
+            twoEnterPage.value = true;
+            withdrawPass.value = false;
+            currentStep.value = 1; // 重置步骤
+          } else {
+            // 首次设置密码,跳转到收款账户
+            activeTab.value = "receivingAccount";
+          }
+        } else {
+          ElMessage.error(res.msg || "提现密码设置失败");
+        }
+      } catch (error) {
+        console.error("设置提现密码失败:", error);
+        ElMessage.error("提现密码设置失败,请重试");
+      } finally {
+        passwordLoading.value = false;
+      }
+    }
+  });
 };
+//二次进入显示
+const twoEnterPage = ref(false);
+//提现密码    验证原密码
+const currentStep = ref(1);
+const passList = [
+  {
+    title: "验证原密码"
+  },
+  {
+    title: "设置新密码"
+  }
+];
+//忘记密码
 
-const handleGoAuth = () => {
-  // 打开实名认证对话框
-  dialogVisible.value = true;
-  // 重置表单
-  if (authFormRef.value) {
-    authFormRef.value.resetFields();
+const forgotPasswordVisible = ref(false);
+const forgotPasswordFormRef = ref<FormInstance>();
+// 验证码倒计时
+const codeCountdown = ref(0);
+let countdownTimer: NodeJS.Timeout | null = null;
+const forgotPasswordForm = reactive({
+  newPassword: "",
+  confireNewPassword: "",
+  verificationCode: ""
+});
+const getForgetPassword = async () => {
+  forgotPasswordVisible.value = true;
+};
+// 发送短信验证码
+const sendForgotCode = async () => {
+  let phoneCode: any = await getPhoneCode({ phone: userInfo.phone, appType: "2", businessType: "6" });
+  if (phoneCode.code === 200) {
+    ElMessage.success("验证码已发送");
+    // 开始倒计时
+    codeCountdown.value = 60;
+    countdownTimer = setInterval(() => {
+      codeCountdown.value--;
+      if (codeCountdown.value <= 0) {
+        if (countdownTimer) {
+          clearInterval(countdownTimer);
+          countdownTimer = null;
+        }
+      }
+    }, 1000);
   }
 };
+//下一步
 
-const handleCancel = () => {
-  dialogVisible.value = false;
-  // 重置表单
-  if (authFormRef.value) {
-    authFormRef.value.resetFields();
+const oldPasswordForm = reactive({
+  password: ""
+});
+
+const oldTwoPasswordForm = reactive({
+  password: "",
+  confirmPassword: ""
+});
+
+const nextStept = async () => {
+  if (oldPasswordForm.password === userInfo.payPassword) {
+    currentStep.value = 2;
+  } else {
+    ElMessage.error("原密码错误");
   }
 };
 
-const handleConfirm = async () => {
-  if (!authFormRef.value) return;
+const handlePasswordTwoSubmit = async () => {
+  if (!passwordFormRef.value) return;
 
   // 表单验证
-  await authFormRef.value.validate(valid => {
+  await passwordFormRef.value.validate(async valid => {
+    if (valid) {
+      passwordLoading.value = true;
+      try {
+        const res: any = await setPayPassword({
+          payPassword: oldTwoPasswordForm.password,
+          id: userInfo.id
+        });
+        if (res.code == 200) {
+          ElMessage.success(res.msg || "提现密码修改成功");
+
+          // 更新本地存储中的提现密码
+          try {
+            const geekerUser = localGet("geeker-user");
+            if (geekerUser && geekerUser.userInfo) {
+              geekerUser.userInfo.payPassword = oldTwoPasswordForm.password;
+              localSet("geeker-user", geekerUser);
+            }
+          } catch (error) {
+            console.error("更新本地存储失败:", error);
+          }
+
+          // 重置表单
+          passwordFormRef.value?.resetFields();
+          oldPasswordForm.password = "";
+          oldTwoPasswordForm.password = "";
+          oldTwoPasswordForm.confirmPassword = "";
+          // 重置步骤
+          currentStep.value = 1;
+        } else {
+          ElMessage.error(res.msg || "提现密码修改失败");
+        }
+      } catch (error) {
+        console.error("修改提现密码失败:", error);
+        ElMessage.error("提现密码修改失败,请重试");
+      } finally {
+        passwordLoading.value = false;
+      }
+    }
+  });
+};
+
+//收款账户
+const authFormRef1 = ref<FormInstance>();
+const authRules1 = reactive<FormRules>({
+  zfbName: [{ required: true, message: "请输入支付宝账号", trigger: "blur" }]
+});
+const authForm1 = reactive({
+  zfbName: ""
+});
+const zfbShow = ref(false);
+const zfbTitle = ref("");
+const dialogVisibleZFB = ref(false);
+const handleGoZFB = async () => {
+  zfbTitle.value = "添加支付宝账号";
+  dialogVisibleZFB.value = true;
+};
+const handleConfirmZFB = async () => {
+  if (!authFormRef1.value) return;
+  await authFormRef1.value.validate(async valid => {
     if (valid) {
       loading.value = true;
-      // 这里可以调用API提交实名认证信息
-      // 模拟API调用
-      setTimeout(() => {
+      try {
+        const params = {
+          alipayAccount: authForm1.zfbName
+        };
+        const res: any = await addAlipayAccount(params);
+        if (res && res.code == 200) {
+          ElMessage.success(res.msg || "添加支付宝账号成功");
+
+          // 更新本地存储中的支付宝账号
+          try {
+            const geekerUser = localGet("geeker-user");
+            if (geekerUser && geekerUser.userInfo) {
+              geekerUser.userInfo.aliPayAccount = authForm1.zfbName;
+              localSet("geeker-user", geekerUser);
+            }
+          } catch (error) {
+            console.error("更新本地存储失败:", error);
+          }
+
+          dialogVisibleZFB.value = false;
+          zfbShow.value = true;
+          // 重置表单
+          authFormRef1.value?.resetFields();
+        } else {
+          ElMessage.error(res.msg || "添加支付宝账号失败,请重试");
+        }
+      } catch (error) {
+        console.error("添加支付宝账号失败:", error);
+        ElMessage.error("添加支付宝账号失败,请重试");
+      } finally {
         loading.value = false;
-        ElMessage.success("实名认证提交成功");
-        dialogVisible.value = false;
-        // 重置表单
-        authFormRef.value?.resetFields();
-        // 这里可以更新认证状态或刷新页面数据
-      }, 1000);
+      }
     }
   });
 };
+const getEditZfb = async () => {
+  zfbTitle.value = "编辑支付宝账号";
+  dialogVisibleZFB.value = true;
+  authForm1.zfbName = userInfo.phone;
+};
+const getDelZfb = async () => {
+  ElMessageBox.confirm("确定要删除该账号,删除后无法进行提现操作", "删除账号", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消"
+  })
+    .then(() => {})
+    .catch(() => {});
+};
 
-const handlePasswordSubmit = async () => {
-  if (!passwordFormRef.value) return;
+// 跳转到提现申请页面
+const goToCashApply = () => {
+  router.push("/financialManagement/cashApply");
+};
 
-  // 表单验证
-  await passwordFormRef.value.validate(valid => {
-    if (valid) {
-      passwordLoading.value = true;
-      // 这里可以调用API设置提现密码
-      // 模拟API调用
-      setTimeout(() => {
-        passwordLoading.value = false;
-        ElMessage.success("提现密码设置成功");
-        // 重置表单
-        passwordFormRef.value?.resetFields();
-      }, 1000);
+// 忘记密码表单验证规则
+const forgotPasswordRules: FormRules = {
+  verificationCode: [{ required: true, message: "请输入验证码", trigger: "blur" }]
+};
+
+// 检查提现密码状态
+const checkPasswordStatus = async () => {
+  try {
+    if (userInfo.idCard && userInfo.id) {
+      const param = {
+        storeUserId: userInfo.id
+      };
+      const res: any = await checkPayPassword(param as any);
+      if (res.code == 200) {
+        // 如果已设置提现密码,显示修改密码界面
+        if (res.data.data == "true") {
+          twoEnterPage.value = true;
+          withdrawPass.value = false;
+          currentStep.value = 1; // 重置步骤
+        } else {
+          // 如果未设置提现密码,显示首次设置界面
+          twoEnterPage.value = false;
+          withdrawPass.value = true;
+        }
+      }
     }
-  });
+  } catch (error) {
+    console.error("检查提现密码状态失败:", error);
+  }
+};
+
+// 检查提现密码并自动跳转
+const checkWithdrawPassword = async () => {
+  try {
+    // 如果已实名认证,检查是否设置了提现密码
+    if (userInfo.idCard) {
+      const param = {
+        storeUserId: userInfo.id
+      };
+      const res: any = await checkPayPassword(param as any);
+      if (res.code == 200) {
+        // 如果未设置提现密码,自动跳转到提现密码标签页
+        if (res.data.data == "false") {
+          activeTab.value = "withdrawPassword";
+        } else {
+          activeTab.value = "receivingAccount";
+        }
+      }
+    }
+  } catch (error) {
+    console.error("检查提现密码失败:", error);
+  }
+};
+
+// 页面加载时检查
+onMounted(() => {
+  checkWithdrawPassword();
+});
+
+// 获取用户手机号
+const userPhone = computed(() => {
+  return localGet("iphone") || localGet("geeker-user")?.userInfo?.phone || "";
+});
+// 掩码显示手机号
+const maskedPhone = computed(() => {
+  if (!userPhone.value || userPhone.value.length !== 11) {
+    return "";
+  }
+  return userPhone.value.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
+});
+
+const handleTabClick = async (tab: any) => {
+  // 根据不同的标签页可以跳转到不同的页面或显示不同的内容
+  console.log("切换到标签:", tab.props.name);
+  // 如果切换到提现密码标签页,检查提现密码状态
+  if (tab.props.name === "withdrawPassword") {
+    await checkPasswordStatus();
+  }
+};
+
+const handleCancel = () => {
+  dialogVisible.value = false;
+  // 重置表单
+  if (authFormRef.value) {
+    authFormRef.value.resetFields();
+  }
+};
+
+const handleCancelZFB = () => {
+  dialogVisibleZFB.value = false;
+  // 重置表单
+  if (authFormRef1.value) {
+    authFormRef1.value.resetFields();
+  }
 };
 </script>
 
@@ -366,6 +805,9 @@ const handlePasswordSubmit = async () => {
         background: #f81515;
         border-radius: 39px;
       }
+      .status-badge-success {
+        background: #6c8ff8;
+      }
     }
   }
 }
@@ -414,62 +856,101 @@ const handlePasswordSubmit = async () => {
     }
   }
 }
+.auth-button {
+  padding: 22px 35px;
+  font-size: 16px;
+  font-weight: 500;
+  background-color: #6c8ff8;
+  border: none;
+  border-radius: 6px;
+  transition: all 0.3s;
+}
 
 // 主内容区
 .content-section {
   display: flex;
   flex: 1;
-
-  // align-items: center;
   justify-content: center;
   min-height: 400px;
   background-color: #ffffff;
+  .content-wrapper-realName {
+    width: 100%;
+    .goAuthBtn {
+      display: flex;
+      justify-content: center;
+      padding: 180px 0 0;
+    }
+    .auth-div {
+      padding-top: 10px;
+      padding-left: 41px;
+      .auth-row {
+        padding: 10px 0;
+      }
+    }
+  }
   .content-wrapper {
     display: flex;
-
-    // align-items: center;
     justify-content: center;
     width: 100%;
     padding: 40px 24px;
-    .auth-button {
-      padding: 22px 35px;
-      font-size: 16px;
-      font-weight: 500;
-      background-color: #6c8ff8;
-      border: none;
-      border-radius: 6px;
-      transition: all 0.3s;
+    .zfb-wrapper {
+      display: flex;
+      justify-content: space-between;
+      width: 100%;
+      height: 80px;
+      padding: 0 20px;
+      border: 1px solid #dddddd;
+      border-radius: 8px;
+      .operate {
+        display: flex;
+        align-items: center;
+        color: #6c8ff8;
+        div {
+          margin-left: 20px;
+        }
+      }
+      .zfb-left {
+        display: flex;
+        align-items: center;
+      }
+      .zfbIcon {
+        width: 60px;
+        height: 60px;
+        margin-right: 10px;
+      }
     }
   }
   .password-form-wrapper {
     width: 100%;
-    max-width: 500px;
-    padding: 40px;
+    padding-left: 15px;
     background-color: #ffffff;
-    border: 1px solid red;
     border-radius: 8px;
-    .password-input {
-      width: 100%;
+    .form-wrapper {
+      width: 400px;
+      margin: 30px auto;
     }
-    .submit-button {
+    .password-input {
       width: 100%;
-      padding: 12px;
-      margin-top: 20px;
-      font-size: 16px;
-      font-weight: 500;
-      background-color: #6c8ff8;
-      border: none;
-      border-radius: 6px;
     }
     :deep(.el-form-item__label) {
       font-weight: 500;
       color: #303133;
     }
   }
+  .submit-button {
+    width: 100%;
+    padding: 22px 35px;
+    margin-top: 20px;
+    font-size: 16px;
+    font-weight: 500;
+    background-color: #6c8ff8;
+    border: none;
+    border-radius: 6px;
+  }
   .password-form-wrapper-step {
-    width: 600px;
+    width: 480px;
     .form-wrapper {
-      width: 600px;
+      width: 400px;
       margin-top: 40px;
     }
     .forget-password {

+ 194 - 0
src/views/financialManagement/reconciled.vue

@@ -0,0 +1,194 @@
+<template>
+  <!-- 头部 -->
+  <div class="header">
+    <el-button class="back-btn" @click="goBack"> 返回 </el-button>
+    <h2 class="title">未到账期金额</h2>
+  </div>
+  <div class="table-box">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button type="primary" link @click="toDetail(scope.row)"> 查看详情 </el-button>
+      </template>
+    </ProTable>
+    <el-dialog v-model="dialogVisible" title="详情" width="500px">
+      <h3>火锅一人惨</h3>
+      <el-row>
+        <el-col :span="12"> 实际收益:+138.32 </el-col>
+        <el-col :span="12"> 技术服务费(3%):-138.32 </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12"> 售价:+138.32 </el-col>
+      </el-row>
+      <h3>订单信息</h3>
+      <div>
+        <div>券码:</div>
+        <div>下单时间:</div>
+        <div>验券时间:</div>
+        <div>入账时间:</div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="tsx" name="voucherManagement">
+import { onActivated, onMounted, reactive, ref } from "vue";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { getPaymentCycle } from "@/api/modules/homeEntry";
+import { localGet } from "@/utils";
+import { useRouter } from "vue-router";
+const router = useRouter();
+const dialogVisible = ref(true);
+// 返回
+const goBack = () => {
+  router.back();
+};
+const toDetail = (row: any) => {
+  dialogVisible.value = true;
+};
+// ProTable 实例(需要在使用它的地方之前定义)
+const proTable = ref<ProTableInstance>();
+
+// 表格配置项
+const columns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "name",
+    label: "商品名称",
+    search: {
+      el: "input"
+    }
+  },
+  {
+    prop: "price",
+    label: "券码",
+    render: (scope: any) => {
+      return scope.row.price ? `¥${scope.row.price}` : "--";
+    }
+  },
+  {
+    prop: "saleNum",
+    label: "入账时间",
+    render: scope => {
+      return scope.row.saleNum === null || !scope.row.saleNum ? "--" : scope.row.saleNum;
+    }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 330 }
+]);
+
+// 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
+const initParam = reactive({
+  storeId: localGet("createdId"),
+  incomeType: 0,
+  paymentType: 1,
+  startTime: null,
+  endTime: null
+});
+// 页面加载时触发查询
+onMounted(async () => {
+  proTable.value?.getTableList();
+});
+
+// 从其他页面返回时触发查询
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+
+// 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 getPaymentCycle(newParams);
+};
+</script>
+
+<style lang="scss" scoped>
+.header {
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  background: #ffffff;
+  border-bottom: 1px solid #e4e7ed;
+  .back-btn {
+    padding: 8px 16px;
+    font-size: 14px;
+    color: #606266;
+    background-color: #f5f7fa;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    &:hover {
+      color: #409eff;
+      background-color: #ecf5ff;
+      border-color: #b3d8ff;
+    }
+  }
+  .title {
+    position: absolute;
+    left: 50%;
+    margin: 0;
+    font-size: 18px;
+    font-weight: bold;
+    color: #333333;
+    transform: translateX(-50%);
+  }
+}
+
+// 在组件样式中添加
+.date-range {
+  display: block; // 确保换行生效
+  padding: 0 8px; // 可选:增加内边距
+  word-wrap: break-word; // 长单词内换行
+  white-space: normal; // 允许自然换行
+}
+.table-header-btn {
+  .button {
+    margin-bottom: 10px;
+  }
+  .tabs {
+    :deep(.el-tabs__nav-wrap::after) {
+      height: 0;
+    }
+  }
+}
+.reject-reason-content {
+  padding: 20px 0;
+  .reject-reason-item {
+    display: flex;
+    margin-bottom: 20px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+    .reject-reason-label {
+      flex-shrink: 0;
+      min-width: 100px;
+      font-size: 14px;
+      font-weight: 500;
+      color: #606266;
+    }
+    .reject-reason-value {
+      flex: 1;
+      font-size: 14px;
+      color: #303133;
+      word-break: break-word;
+      &.reject-reason-text {
+        min-height: 80px;
+        padding: 12px;
+        line-height: 1.6;
+        white-space: pre-wrap;
+        background-color: #f5f7fa;
+        border-radius: 4px;
+      }
+    }
+  }
+}
+</style>

+ 233 - 0
src/views/financialManagement/todayIncomeList.vue

@@ -0,0 +1,233 @@
+<template>
+  <div class="todayIncomeList">
+    <!-- 头部 -->
+    <div class="header">
+      <el-button class="back-btn" @click="goBack"> 返回 </el-button>
+      <h2 class="title">今日可收益金额</h2>
+    </div>
+    <div class="table-box">
+      <ProTable
+        ref="proTable"
+        :columns="columns"
+        :request-api="getTableList"
+        :init-param="initParam"
+        :data-callback="dataCallback"
+        :pagination="true"
+      >
+        <!-- 表格操作 -->
+        <template #operation="scope">
+          <el-button type="primary" link @click="toDetail(scope.row)"> 查看详情 </el-button>
+        </template>
+      </ProTable>
+      <el-dialog v-model="dialogVisible" title="详情" width="500px">
+        <h2>{{ unposted.couponName }}</h2>
+        <el-row class="couponRow">
+          <el-col :span="12"> 实际收益:+138.32 </el-col>
+          <el-col :span="12"> {{ `技术服务费(${unposted.commissionRate || 3}%)` }}:{{ unposted.commissionStr }} </el-col>
+        </el-row>
+        <el-row class="couponRow">
+          <el-col :span="12"> 售价:{{ unposted.incomeMoney || ((unposted?.money || 0) / 100).toFixed(2) || "0.00" }} </el-col>
+        </el-row>
+        <h3 class="orderInfo">订单信息</h3>
+        <div>
+          <div class="couponRow">下单时间:{{ unposted.orderTime }}</div>
+          <div class="couponRow">验券时间:{{ unposted.checkTime }}</div>
+          <div class="couponRow">入账时间:</div>
+        </div>
+      </el-dialog>
+    </div>
+  </div>
+</template>
+
+<script setup lang="tsx" name="voucherManagement">
+import { computed, onActivated, onMounted, reactive, ref, watch } from "vue";
+import { useRouter } from "vue-router";
+import type { FormInstance, FormRules } from "element-plus";
+import { ElMessage } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { getPaymentCycle } from "@/api/modules/homeEntry";
+import { ElMessageBox } from "element-plus/es";
+import { localGet } from "@/utils";
+const router = useRouter();
+const dialogVisible = ref(true);
+const unposted = ref<any>({});
+const toDetail = (row: any) => {
+  dialogVisible.value = true;
+  unposted.value = row;
+};
+// 返回
+const goBack = () => {
+  router.back();
+};
+// ProTable 实例(需要在使用它的地方之前定义)
+const proTable = ref<ProTableInstance>();
+
+// 表格配置项
+const columns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "couponName",
+    label: "商品名称"
+  },
+  {
+    prop: "price",
+    label: "券码",
+    render: (scope: any) => {
+      return scope.row.price ? `¥${scope.row.price}` : "--";
+    }
+  },
+  {
+    prop: "createdTime",
+    label: "实际收益",
+    render: scope => {
+      return scope.row.saleNum === null || !scope.row.saleNum ? "--" : scope.row.saleNum;
+    }
+  },
+  {
+    prop: "createdTime",
+    label: "售价",
+    render: scope => {
+      return scope.row.saleNum === null || !scope.row.saleNum ? "--" : scope.row.saleNum;
+    }
+  },
+  {
+    prop: "createdTime",
+    label: "技术服务费",
+    render: scope => {
+      return scope.row.saleNum === null || !scope.row.saleNum ? "--" : scope.row.saleNum;
+    }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 330 }
+]);
+
+// 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
+const initParam = reactive({
+  storeId: localGet("createdId"),
+  incomeType: 0,
+  paymentType: 0,
+  startTime: null,
+  endTime: null
+});
+// 页面加载时触发查询
+onMounted(async () => {
+  proTable.value?.getTableList();
+});
+
+// 从其他页面返回时触发查询
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+
+// dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 list && total 这些字段,可以在这里进行处理成这些字段
+// 或者直接去 hooks/useTable.ts 文件中把字段改为你后端对应的就行
+const dataCallback = (data: any) => {
+  return {
+    list: data.data?.records || data.records || [],
+    total: data.data?.total || data.total || 0
+  };
+};
+
+// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
+// 默认不做操作就直接在 ProTable 组件上绑定	:requestApi="getUserList"
+const getTableList = (params: any) => {
+  let newParams = JSON.parse(JSON.stringify(params));
+  return getPaymentCycle(newParams);
+};
+</script>
+
+<style lang="scss" scoped>
+.todayIncomeList {
+  height: 100%;
+  border: 1px solid red;
+}
+.header {
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  background: #ffffff;
+  border-bottom: 1px solid #e4e7ed;
+  .back-btn {
+    padding: 8px 16px;
+    font-size: 14px;
+    color: #606266;
+    background-color: #f5f7fa;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    &:hover {
+      color: #409eff;
+      background-color: #ecf5ff;
+      border-color: #b3d8ff;
+    }
+  }
+  .title {
+    position: absolute;
+    left: 50%;
+    margin: 0;
+    font-size: 18px;
+    font-weight: bold;
+    color: #333333;
+    transform: translateX(-50%);
+  }
+}
+
+// 在组件样式中添加
+.date-range {
+  display: block; // 确保换行生效
+  padding: 0 8px; // 可选:增加内边距
+  word-wrap: break-word; // 长单词内换行
+  white-space: normal; // 允许自然换行
+}
+.table-header-btn {
+  .button {
+    margin-bottom: 10px;
+  }
+  .tabs {
+    :deep(.el-tabs__nav-wrap::after) {
+      height: 0;
+    }
+  }
+}
+.orderInfo {
+  padding-bottom: 20px;
+  font-size: 18px;
+  font-weight: bold;
+}
+.couponRow {
+  padding-bottom: 20px;
+}
+.couponRow:last-child {
+  padding-bottom: 0;
+}
+.reject-reason-content {
+  padding: 20px 0;
+  .reject-reason-item {
+    display: flex;
+    margin-bottom: 20px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+    .reject-reason-label {
+      flex-shrink: 0;
+      min-width: 100px;
+      font-size: 14px;
+      font-weight: 500;
+      color: #606266;
+    }
+    .reject-reason-value {
+      flex: 1;
+      font-size: 14px;
+      color: #303133;
+      word-break: break-word;
+      &.reject-reason-text {
+        min-height: 80px;
+        padding: 12px;
+        line-height: 1.6;
+        white-space: pre-wrap;
+        background-color: #f5f7fa;
+        border-radius: 4px;
+      }
+    }
+  }
+}
+</style>

+ 208 - 0
src/views/financialManagement/unposted.vue

@@ -0,0 +1,208 @@
+<template>
+  <!-- 头部 -->
+  <div class="header">
+    <el-button class="back-btn" @click="goBack"> 返回 </el-button>
+    <h2 class="title">未到账期金额</h2>
+  </div>
+  <div class="table-box">
+    <ProTable
+      ref="proTable"
+      :columns="columns"
+      :request-api="getTableList"
+      :init-param="initParam"
+      :data-callback="dataCallback"
+      :pagination="true"
+    >
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button type="primary" link @click="toDetail(scope.row)"> 查看详情 </el-button>
+      </template>
+    </ProTable>
+    <el-dialog v-model="dialogVisible" title="详情" width="500px">
+      <h2>{{ unposted.couponName }}</h2>
+      <el-row class="couponRow">
+        <el-col :span="12"> 实际收益:+138.32 </el-col>
+        <el-col :span="12"> {{ `技术服务费(${unposted.commissionRate || 3}%)` }}:{{ unposted.commissionStr }} </el-col>
+      </el-row>
+      <el-row class="couponRow">
+        <el-col :span="12"> 售价:{{ unposted.incomeMoney || ((unposted?.money || 0) / 100).toFixed(2) || "0.00" }} </el-col>
+      </el-row>
+      <h3>订单信息</h3>
+      <div>
+        <div class="couponRow">下单时间:{{ unposted.orderTime }}</div>
+        <div class="couponRow">验券时间:{{ unposted.checkTime }}</div>
+        <div class="couponRow">入账时间:</div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="tsx" name="voucherManagement">
+import { computed, onActivated, onMounted, reactive, ref, watch } from "vue";
+import { useRouter } from "vue-router";
+import type { FormInstance, FormRules } from "element-plus";
+import { ElMessage } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { getPaymentCycle } from "@/api/modules/homeEntry";
+import { ElMessageBox } from "element-plus/es";
+import { localGet } from "@/utils";
+const router = useRouter();
+const dialogVisible = ref(false);
+const unposted = ref<any>({});
+const toDetail = (row: any) => {
+  dialogVisible.value = true;
+  unposted.value = row;
+};
+// ProTable 实例(需要在使用它的地方之前定义)
+const proTable = ref<ProTableInstance>();
+// 返回
+const goBack = () => {
+  router.back();
+};
+
+// 表格配置项
+const columns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "couponName",
+    label: "商品名称",
+    search: {
+      el: "input"
+    }
+  },
+  // {
+  //   prop: "price",
+  //   label: "券码",
+  //   render: (scope: any) => {
+  //     return scope.row.price ? `¥${scope.row.price}` : "--";
+  //   }
+  // },
+  {
+    prop: "createdTime",
+    label: "入账时间",
+    render: scope => {
+      return scope.row.saleNum === null || !scope.row.saleNum ? "--" : scope.row.saleNum;
+    }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 330 }
+]);
+
+// 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
+const initParam = reactive({
+  storeId: localGet("createdId"),
+  incomeType: 0,
+  paymentType: 0,
+  startTime: null,
+  endTime: null
+});
+// 页面加载时触发查询
+onMounted(async () => {
+  proTable.value?.getTableList();
+});
+
+// 从其他页面返回时触发查询
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+
+// dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 list && total 这些字段,可以在这里进行处理成这些字段
+// 或者直接去 hooks/useTable.ts 文件中把字段改为你后端对应的就行
+const dataCallback = (data: any) => {
+  return {
+    list: data.data?.records || data.records || [],
+    total: data.data?.total || data.total || 0
+  };
+};
+
+// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
+// 默认不做操作就直接在 ProTable 组件上绑定	:requestApi="getUserList"
+const getTableList = (params: any) => {
+  let newParams = JSON.parse(JSON.stringify(params));
+  return getPaymentCycle(newParams);
+};
+</script>
+
+<style lang="scss" scoped>
+.header {
+  position: relative;
+  display: flex;
+  align-items: center;
+  padding: 20px;
+  background: #ffffff;
+  border-bottom: 1px solid #e4e7ed;
+  .back-btn {
+    padding: 8px 16px;
+    font-size: 14px;
+    color: #606266;
+    background-color: #f5f7fa;
+    border: 1px solid #dcdfe6;
+    border-radius: 4px;
+    &:hover {
+      color: #409eff;
+      background-color: #ecf5ff;
+      border-color: #b3d8ff;
+    }
+  }
+  .title {
+    position: absolute;
+    left: 50%;
+    margin: 0;
+    font-size: 18px;
+    font-weight: bold;
+    color: #333333;
+    transform: translateX(-50%);
+  }
+}
+
+// 在组件样式中添加
+.date-range {
+  display: block; // 确保换行生效
+  padding: 0 8px; // 可选:增加内边距
+  word-wrap: break-word; // 长单词内换行
+  white-space: normal; // 允许自然换行
+}
+.table-header-btn {
+  .button {
+    margin-bottom: 10px;
+  }
+  .tabs {
+    :deep(.el-tabs__nav-wrap::after) {
+      height: 0;
+    }
+  }
+}
+.couponRow {
+  padding-bottom: 10px;
+}
+.reject-reason-content {
+  padding: 20px 0;
+  .reject-reason-item {
+    display: flex;
+    margin-bottom: 20px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+    .reject-reason-label {
+      flex-shrink: 0;
+      min-width: 100px;
+      font-size: 14px;
+      font-weight: 500;
+      color: #606266;
+    }
+    .reject-reason-value {
+      flex: 1;
+      font-size: 14px;
+      color: #303133;
+      word-break: break-word;
+      &.reject-reason-text {
+        min-height: 80px;
+        padding: 12px;
+        line-height: 1.6;
+        white-space: pre-wrap;
+        background-color: #f5f7fa;
+        border-radius: 4px;
+      }
+    }
+  }
+}
+</style>

+ 407 - 0
src/views/financialManagement/withdrawaRecord.vue

@@ -0,0 +1,407 @@
+<template>
+  <div class="table-box">
+    <ProTable
+      ref="proTable"
+      :columns="columns"
+      :request-api="getTableList"
+      :init-param="initParam"
+      :data-callback="dataCallback"
+      :pagination="true"
+    >
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button type="primary" link @click="toDetail(scope.row)"> 查看详情 </el-button>
+      </template>
+    </ProTable>
+    <el-dialog v-model="dialogVisible" title="提现详情" width="500px">
+      <div class="withdrawal-detail">
+        <!-- 提现进度 -->
+        <div class="progress-section">
+          <h4 class="section-title">提现进度</h4>
+          <el-steps :active="currentStep" direction="vertical" :process-status="processStatus" :finish-status="finishStatus">
+            <el-step>
+              <template #title>
+                <div class="step-title">发起提现</div>
+              </template>
+              <template #description>
+                <div class="step-time">
+                  {{ withdrawalDetail.createdTime || "2025/10/1 12:00:00" }}
+                </div>
+              </template>
+            </el-step>
+            <el-step>
+              <template #title>
+                <div class="step-title">等待审核</div>
+              </template>
+            </el-step>
+            <el-step>
+              <template #title>
+                <div class="step-title">
+                  {{ paymentName(withdrawalDetail.paymentStatus) }}
+                </div>
+              </template>
+            </el-step>
+          </el-steps>
+        </div>
+
+        <!-- 提现详情 -->
+        <div class="detail-section">
+          <div class="detail-list">
+            <div class="detail-item">
+              <span class="detail-label">收款账户</span>
+              <span class="detail-value">{{ withdrawalDetail.settlementAccount || "支付宝账户 (130****1234)" }}</span>
+            </div>
+            <div class="detail-item">
+              <span class="detail-label">提现金额</span>
+              <span class="detail-value amount">¥{{ withdrawalDetail.money || "6146.6" }}</span>
+            </div>
+            <div class="detail-item">
+              <span class="detail-label">发起提现时间</span>
+              <span class="detail-value">{{ withdrawalDetail.createdTime || "2025/10/1 12:00:00" }}</span>
+            </div>
+            <div class="detail-item">
+              <span class="detail-label">结算账户</span>
+              <span class="detail-value">{{ withdrawalDetail.settlementAccount || "重庆老火锅_钱包" }}</span>
+            </div>
+            <div class="detail-item">
+              <span class="detail-label">提现状态</span>
+              <span class="detail-value status">{{ paymentName(withdrawalDetail.paymentStatus) }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="tsx" name="voucherManagement">
+import { computed, onActivated, onMounted, reactive, ref, watch } from "vue";
+import { useRouter } from "vue-router";
+import type { FormInstance, FormRules } from "element-plus";
+import { ElMessage } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { getCashOutRecordList } from "@/api/modules/homeEntry";
+import { ElMessageBox } from "element-plus/es";
+import { localGet } from "@/utils";
+const dialogVisible = ref(false);
+const withdrawalDetail = ref<any>({});
+// ProTable 实例(需要在使用它的地方之前定义)
+const proTable = ref<ProTableInstance>();
+
+// 提现状态枚举
+const paymentStatusEnum = [
+  { label: "提现中", value: 0 },
+  { label: "提现成功", value: 1 },
+  { label: "提现失败", value: 2 },
+  { label: "提现待审核", value: 3 },
+  { label: "提现拒绝", value: 4 },
+  { label: "提现待审核通过", value: 5 }
+];
+
+// 表格配置项
+const columns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "settlementAccount",
+    label: "收款账号",
+    render: (scope: any) => {
+      return "支付宝账号" + "(" + scope.row.settlementAccount + ")";
+    }
+  },
+  {
+    prop: "money",
+    label: "提现金额",
+    render: (scope: any) => {
+      return scope.row.money ? `¥${scope.row.money}` : "--";
+    }
+  },
+  {
+    prop: "cashOutType",
+    label: "提现方式",
+    render: scope => {
+      return scope.row.cashOutType ? "自动提现" : "手动提现";
+    }
+  },
+  {
+    prop: "createdTime",
+    label: "发起提现时间"
+  },
+  {
+    prop: "paymentStatus",
+    label: "提现状态",
+    render: scope => {
+      return scope.row.paymentStatus === 0
+        ? "提现中"
+        : scope.row.paymentStatus === 1
+          ? "提现成功"
+          : scope.row.paymentStatus === 2
+            ? "提现失败"
+            : scope.row.paymentStatus === 3
+              ? "提现待审核"
+              : scope.row.paymentStatus === 4
+                ? "提现拒绝"
+                : "提现待审核通过";
+    },
+    search: {
+      el: "select",
+      props: { placeholder: "请选择", clearable: true }
+    },
+    enum: paymentStatusEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 330 }
+]);
+
+// 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
+const initParam = reactive({
+  storeId: localGet("createdId"),
+  cashOutEndTime: null,
+  cashOutStartTime: null
+});
+// 页面加载时触发查询
+onMounted(async () => {
+  proTable.value?.getTableList();
+});
+
+// 从其他页面返回时触发查询
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+
+// dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 list && total 这些字段,可以在这里进行处理成这些字段
+// 或者直接去 hooks/useTable.ts 文件中把字段改为你后端对应的就行
+const dataCallback = (data: any) => {
+  return {
+    list: data.cashOutRecordList || data.list || [],
+    total: data.total || 0
+  };
+};
+
+// 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
+// 默认不做操作就直接在 ProTable 组件上绑定	:requestApi="getUserList"
+const getTableList = (params: any) => {
+  let newParams = JSON.parse(JSON.stringify(params));
+  return getCashOutRecordList(newParams);
+};
+
+// 查看详情
+const toDetail = (row: any) => {
+  withdrawalDetail.value = row;
+  dialogVisible.value = true;
+};
+const paymentName = computed(() => {
+  return status => {
+    switch (status) {
+      case 0:
+        return "提现中";
+      case 1:
+        return "提现成功";
+      case 2:
+        return "提现失败";
+      case 3:
+        return "提现待审核";
+      case 4:
+        return "提现拒绝";
+      case 5:
+        return "提现待审核通过";
+      default:
+        return "";
+    }
+  };
+});
+
+// 计算当前步骤
+// active 表示当前激活的步骤索引(从0开始)
+// 步骤0: 发起提现(总是已完成)
+// 步骤1: 等待审核(根据状态判断)
+// 步骤2: 提现成功/拒绝(根据状态判断)
+const currentStep = computed(() => {
+  const status = withdrawalDetail.value.paymentStatus;
+  // 0: 提现中 -> 步骤1(发起提现已完成,等待审核进行中)
+  // 1: 提现成功 -> 步骤2(所有步骤都已完成)
+  // 2: 提现失败 -> 步骤1(等待审核失败)
+  // 3: 提现待审核 -> 步骤1(等待审核进行中)
+  // 4: 提现拒绝 -> 步骤2(审核已完成,在第三步显示拒绝状态)
+  // 5: 提现待审核通过 -> 步骤2(所有步骤都已完成)
+  if (status === 1 || status === 5) {
+    return 2; // 提现成功,所有步骤都完成
+  } else if (status === 4) {
+    return 2; // 提现拒绝,在第三步显示错误状态
+  } else if (status === 0 || status === 3) {
+    return 1; // 等待审核进行中
+  } else if (status === 2) {
+    return 1; // 提现失败,在等待审核阶段失败
+  }
+  return 0; // 默认在第一步
+});
+
+// 处理状态(用于失败情况)
+const processStatus = computed<"error" | "process" | "finish" | "wait">(() => {
+  const status = withdrawalDetail.value.paymentStatus;
+  if (status === 4) {
+    return "error" as const; // 提现拒绝,在第三步显示错误状态
+  } else if (status === 2) {
+    return "error" as const; // 提现失败,在等待审核阶段显示错误状态
+  }
+  return "process" as const; // 进行中
+});
+
+// 完成状态
+const finishStatus = computed<"finish">(() => {
+  return "finish" as const; // 完成状态
+});
+// 获取状态文本
+const getStatusText = (status: string) => {
+  const statusMap: Record<string, string> = {
+    processing: "提现中",
+    pending: "等待审核",
+    success: "提现成功",
+    failed: "提现失败",
+    rejected: "审核拒绝"
+  };
+  return statusMap[status] || "提现中";
+};
+</script>
+
+<style lang="scss" scoped>
+// 在组件样式中添加
+.date-range {
+  display: block; // 确保换行生效
+  padding: 0 8px; // 可选:增加内边距
+  word-wrap: break-word; // 长单词内换行
+  white-space: normal; // 允许自然换行
+}
+.table-header-btn {
+  .button {
+    margin-bottom: 10px;
+  }
+  .tabs {
+    :deep(.el-tabs__nav-wrap::after) {
+      height: 0;
+    }
+  }
+}
+.reject-reason-content {
+  padding: 20px 0;
+  .reject-reason-item {
+    display: flex;
+    margin-bottom: 20px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+    .reject-reason-label {
+      flex-shrink: 0;
+      min-width: 100px;
+      font-size: 14px;
+      font-weight: 500;
+      color: #606266;
+    }
+    .reject-reason-value {
+      flex: 1;
+      font-size: 14px;
+      color: #303133;
+      word-break: break-word;
+      &.reject-reason-text {
+        min-height: 80px;
+        padding: 12px;
+        line-height: 1.6;
+        white-space: pre-wrap;
+        background-color: #f5f7fa;
+        border-radius: 4px;
+      }
+    }
+  }
+}
+.withdrawal-detail {
+  .section-title {
+    margin: 0 0 20px;
+    font-size: 16px;
+    font-weight: 600;
+    color: #303133;
+  }
+  .progress-section {
+    margin-bottom: 30px;
+    :deep(.el-steps) {
+      .el-step {
+        margin-bottom: 24px;
+        &:last-child {
+          margin-bottom: 0;
+        }
+      }
+      .el-step__head {
+        .el-step__icon {
+          width: 20px;
+          height: 20px;
+          font-size: 14px;
+        }
+      }
+      .el-step__main {
+        padding-bottom: 8px;
+        .el-step__title {
+          margin-bottom: 8px;
+          font-size: 14px;
+          line-height: 1.5;
+          .step-title {
+            font-size: 14px;
+            font-weight: 500;
+            color: #303133;
+          }
+        }
+        .el-step__description {
+          .step-time {
+            margin-top: 4px;
+            font-size: 12px;
+            color: #909399;
+          }
+        }
+      }
+      .el-step__line {
+        top: 20px;
+        bottom: -24px;
+        left: 9px;
+        height: calc(100% + 4px);
+        border-left: 2px dashed #dcdfe6;
+      }
+      .el-step__line-inner {
+        height: 100%;
+        border: none;
+      }
+      .el-step:last-child .el-step__line {
+        display: none;
+      }
+    }
+  }
+  .detail-section {
+    .detail-list {
+      .detail-item {
+        display: flex;
+        align-items: flex-start;
+        margin-bottom: 16px;
+        &:last-child {
+          margin-bottom: 0;
+        }
+        .detail-label {
+          flex-shrink: 0;
+          width: 100px;
+          font-size: 14px;
+          color: #606266;
+        }
+        .detail-value {
+          flex: 1;
+          font-size: 14px;
+          color: #303133;
+          word-break: break-word;
+          &.amount {
+            font-size: 16px;
+            font-weight: 600;
+            color: #303133;
+          }
+          &.status {
+            color: #409eff;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 82 - 27
src/views/groupPackageManagement/newGroup.vue

@@ -14,33 +14,33 @@
             <h3 style="font-weight: bold">基础信息:</h3>
             <!-- 团购图片上传 prop="imageValueStr" 本地服务测不了上传图片 先去掉必填-->
             <el-form-item label="图片">
-              <el-upload
-                ref="uploadRef"
-                v-model:file-list="storeInfoModel.imageValueStr"
-                list-type="picture-card"
-                :accept="'.jpg,.png'"
-                :limit="uploadMaxCount"
-                :auto-upload="false"
-                :disabled="hasUnuploadedImages"
-                multiple
-                :on-change="handleUploadChange"
-                :on-exceed="handleUploadExceed"
-                :on-preview="handlePictureCardPreview"
-                :before-remove="handleBeforeRemove"
-                :on-remove="handleRemove"
-                :show-file-list="true"
-              >
-                <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>
+              <div class="upload-area" :class="{ 'upload-full': uploadedImageCount >= uploadMaxCount }">
+                <el-upload
+                  ref="uploadRef"
+                  v-model:file-list="storeInfoModel.imageValueStr"
+                  list-type="picture-card"
+                  :accept="'.jpg,.png'"
+                  :limit="uploadMaxCount"
+                  :auto-upload="false"
+                  :disabled="hasUnuploadedImages"
+                  multiple
+                  :on-change="handleUploadChange"
+                  :on-exceed="handleUploadExceed"
+                  :on-preview="handlePictureCardPreview"
+                  :before-remove="handleBeforeRemove"
+                  :on-remove="handleRemove"
+                  :show-file-list="true"
+                >
+                  <template #trigger>
+                    <div v-if="uploadedImageCount < uploadMaxCount" class="upload-trigger-card el-upload--picture-card">
+                      <el-icon>
+                        <Plus />
+                      </el-icon>
+                      <div class="upload-tip">({{ uploadedImageCount }}/{{ uploadMaxCount }})</div>
+                    </div>
+                  </template>
+                </el-upload>
+              </div>
             </el-form-item>
             <!-- 团购名称 -->
             <el-form-item label="团购名称" prop="groupName">
@@ -991,6 +991,11 @@ const visibleGroups = computed(() => {
   return lifeGroupBuyThalis.value.map((group, index) => ({ group, originalIndex: index }));
 });
 
+// 计算属性:获取已成功上传的图片数量
+const uploadedImageCount = computed(() => {
+  return storeInfoModel.value.imageValueStr.filter((file: any) => file.status === "success" && file.imageId).length;
+});
+
 // 计算属性:检查是否有未上传完成的图片
 const hasUnuploadedImages = computed(() => {
   // 检查是否有正在上传的文件
@@ -2232,15 +2237,65 @@ const handleImageParam = (list: any[], result: any[]) => {
       opacity: 0.5;
     }
   }
+
+  /* 上传项悬停和聚焦时显示状态标签 */
+  .el-upload-list__item:hover .el-upload-list__item-status-label {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  .el-upload-list__item.is-success:focus .el-upload-list__item-status-label {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+
+  /* 隐藏关闭提示图标 */
+  .el-icon--close-tip {
+    display: none !important;
+  }
 }
 .upload-trigger-card {
   display: flex;
+  flex-direction: column;
   align-items: center;
   justify-content: center;
   width: 100%;
   height: 100%;
   font-size: 28px;
   color: #8c939d;
+  .upload-tip {
+    margin-top: 8px;
+    font-size: 14px;
+    color: #8c939d;
+  }
+}
+
+/* 上传区域样式 */
+.upload-area {
+  min-height: 300px;
+  padding: 20px;
+  :deep(.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label) {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  :deep(.el-upload-list__item.is-success:focus .el-upload-list__item-status-label) {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  :deep(.el-upload-list--picture-card .el-icon--close-tip) {
+    display: none !important;
+  }
+  &.upload-full {
+    :deep(.el-upload--picture-card) {
+      display: none !important;
+    }
+  }
+}
+
+/* 上传滚动条样式 */
+.upload-scrollbar {
+  :deep(.el-scrollbar__wrap) {
+    overflow-x: hidden;
+  }
 }
 
 /* 表单容器 */

+ 33 - 3
src/views/home/components/go-flow.vue

@@ -210,9 +210,9 @@ import {
 } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 
-import { verifyIdInfo, applyStore } from "@/api/modules/homeEntry";
+import { verifyIdInfo, applyStore, getMerchantByPhone } from "@/api/modules/homeEntry";
 import { getBusinessSection, getBusinessSectionTypes, getInputPrompt, getDistrict, uploadImg } from "@/api/modules/newLoginApi";
-import { localGet } from "@/utils/index";
+import { localGet, localSet } from "@/utils/index";
 const userInfo = localGet("geeker-user")?.userInfo || {};
 const latShow = ref(false);
 onMounted(() => {
@@ -327,7 +327,7 @@ const step2Form = reactive({
   administrativeRegionDistrictAdcode: "", //区
   storeAddress: "", //门店地址(完整地址)
   storeBlurb: "", //门店简介
-  businessSection: "", //经营板块
+  businessSection: "1", //经营板块
   businessSectionName: "KTV",
   businessTypes: [], //经营种类
   businessTypesList: [], //经营种类集合
@@ -503,6 +503,36 @@ const handleNextStep = async () => {
       if (res && res.code == 200) {
         ElMessage.success(res.msg);
 
+        // 更新本地存储中的idCard
+        try {
+          const geekerUser = localGet("geeker-user");
+          if (geekerUser && geekerUser.userInfo) {
+            // 获取最新的用户信息
+            const phone = geekerUser.userInfo.phone;
+            if (phone) {
+              const userRes: any = await getMerchantByPhone({ phone });
+              if (userRes && userRes.code == 200 && userRes.data) {
+                // 更新本地存储中的用户信息,特别是idCard
+                geekerUser.userInfo.idCard = userRes.data.idCard || step1Form.idNumber;
+                geekerUser.userInfo.name = userRes.data.name || step1Form.name;
+                localSet("geeker-user", geekerUser);
+              } else {
+                // 如果获取失败,至少更新idCard字段
+                geekerUser.userInfo.idCard = step1Form.idNumber;
+                geekerUser.userInfo.name = step1Form.name;
+                localSet("geeker-user", geekerUser);
+              }
+            } else {
+              // 如果没有phone,直接更新idCard
+              geekerUser.userInfo.idCard = step1Form.idNumber;
+              geekerUser.userInfo.name = step1Form.name;
+              localSet("geeker-user", geekerUser);
+            }
+          }
+        } catch (error) {
+          console.error("更新本地存储失败:", error);
+        }
+
         setStep(2);
       }
     } else {

+ 22 - 17
src/views/home/index.vue

@@ -3,10 +3,10 @@
     <!--已入驻-->
     <go-examine v-if="isExaime" />
     <!-- 第一步  未入驻 -->
-    <go-enter :current-step="currentStep" @update:current-step="handleUpdateCurrentStep" v-if="isEntry" />
-
+    <go-enter :current-step="currentStep" @update:current-step="handleUpdateCurrentStep" v-if="isEntry && currentStep === 0" />
     <!-- 第二步 -->
     <go-flow
+      v-if="isEntry && currentStep > 0"
       :current-step="currentStep"
       @update:current-step="handleUpdateCurrentStep"
       @update:get-user-info="getUserInfo"
@@ -22,6 +22,7 @@ import goEnter from "./components/go-enter.vue";
 import goFlow from "./components/go-flow.vue";
 import goExamine from "./components/go-examine.vue";
 import { getMerchantByPhone, getDetail } from "@/api/modules/homeEntry";
+import { is } from "@/utils/is";
 const isEntry = ref<boolean>(false);
 const isExaime = ref<boolean>(false);
 
@@ -49,21 +50,25 @@ const getUserInfo = async () => {
     };
     const res: any = await getMerchantByPhone(param);
     storeId.value = res.data.storeId;
-    if (res && res.code == 200 && res.data && res.data.storeId) {
-      let param1 = {
-        id: res.data.storeId
-      };
-      const resStore: any = await getDetail(param1);
-      if (resStore && resStore.code == 200 && resStore.data) {
-        storeApplicationStatus.value = resStore.data.storeApplicationStatus;
-        if (storeApplicationStatus.value == 2) {
-          currentStep.value = 3;
-        }
-
-        if (resStore.data.storeApplicationStatus == 2 && res.data.storeId != null) {
-          isEntry.value = true;
-        } else {
-          isExaime.value = true;
+    if (res && res.code == 200 && res.data) {
+      if (res.data.storeId == null) {
+        isEntry.value = true;
+      }
+      if (res.data.storeId != null) {
+        let param1 = {
+          id: res.data.storeId
+        };
+        const resStore: any = await getDetail(param1);
+        if (resStore && resStore.code == 200 && resStore.data) {
+          storeApplicationStatus.value = resStore.data.storeApplicationStatus;
+          if (storeApplicationStatus.value == 2) {
+            currentStep.value = 3;
+          }
+          if (resStore.data.storeApplicationStatus == 2 && res.data.storeId != null) {
+            isEntry.value = true;
+          } else {
+            isExaime.value = true;
+          }
         }
       }
     }

+ 21 - 7
src/views/home/notice.vue

@@ -60,7 +60,7 @@
 import { ref, onMounted } from "vue";
 import { ElMessage } from "element-plus";
 import { CircleCheck } from "@element-plus/icons-vue";
-import { getNoticeList, markAllNoticesAsRead } from "@/api/modules/homeEntry";
+import { getNoticeList, markAllNoticesAsRead, markNoticeAsRead } from "@/api/modules/homeEntry";
 import { useRouter } from "vue-router";
 const router = useRouter();
 import mittBus from "@/utils/mittBus";
@@ -70,7 +70,8 @@ import { get } from "lodash";
 interface NoticeItem {
   id: number;
   title: string;
-  time: string;
+  time?: string;
+  createdTime?: string;
   content: string;
   context?: string;
   isRead: boolean;
@@ -151,12 +152,25 @@ const parseContext = (context: string | undefined): string => {
 };
 
 // 查看详情
-const handleViewDetail = (item: NoticeItem) => {
-  // 标记为已读
+const handleViewDetail = async (item: NoticeItem) => {
+  // 如果未读,调用接口标记为已读
   if (!item.isRead) {
-    item.isRead = true;
-    // 通知 Message 组件更新未读数量
-    mittBus.emit("updateUnreadCount");
+    try {
+      const res: any = await markNoticeAsRead({
+        id: item.id
+      });
+      if (res.code == 200) {
+        // 标记为已读
+        item.isRead = true;
+        // 通知 Message 组件更新未读数量
+        mittBus.emit("updateUnreadCount");
+      } else {
+        ElMessage.error(res.message || "标记已读失败");
+      }
+    } catch (error) {
+      console.error("标记已读失败:", error);
+      ElMessage.error("标记已读失败,请重试");
+    }
   }
   // 显示详情对话框
   currentNotice.value = item;

+ 20 - 24
src/views/licenseManagement/businessLicense.vue

@@ -1,9 +1,5 @@
 <template>
   <div class="card content-box">
-    <div class="page-header">
-      <h1 class="store-title">重庆老火锅</h1>
-    </div>
-    <div class="divider" />
     <div class="tip-text">如需更换请联系客服</div>
     <div class="license-container">
       <div class="license-display">
@@ -28,22 +24,24 @@
 import { ref, onMounted } from "vue";
 import { ElMessage } from "element-plus";
 import { Picture } from "@element-plus/icons-vue";
+import { getBusinessLicense } from "@/api/modules/licenseManagement";
+import { localGet } from "@/utils";
 
 const licenseImage = ref<string>("");
 
+const id = localGet("createdId");
+
 onMounted(async () => {
   await initData();
 });
 
 const initData = async () => {
-  try {
-    // TODO: 调用API获取营业执照图片
-    // const response = await getBusinessLicense();
-    // if (response.code === 200) {
-    //   licenseImage.value = response.data.imageUrl;
-    // }
-  } catch (error) {
-    ElMessage.error("获取营业执照失败");
+  const params = {
+    id: id
+  };
+  const res: any = await getBusinessLicense(params);
+  if (res.code === 200) {
+    licenseImage.value = res.data[0].imgUrl;
   }
 };
 </script>
@@ -54,7 +52,7 @@ const initData = async () => {
 }
 .store-title {
   margin: 0;
-  font-size: 24px;
+  font-size: 28px;
   font-weight: 600;
   color: var(--el-text-color-primary);
 }
@@ -65,13 +63,12 @@ const initData = async () => {
   background-color: var(--el-border-color-lighter);
 }
 .tip-text {
-  margin-bottom: 30px;
-  font-size: 14px;
+  margin-top: 20px;
+  margin-bottom: 50px;
+  font-size: 18px;
   color: var(--el-text-color-regular);
 }
 .license-container {
-  // width: 100%;
-  // min-height: 500px;
   padding: 20px;
   background-color: var(--el-bg-color-page);
   border-radius: 8px;
@@ -80,13 +77,12 @@ const initData = async () => {
   display: flex;
   align-items: center;
   justify-content: center;
-
-  // width: 100%;
-  // min-height: 500px;
+  width: 700px;
+  height: 500px;
 }
 .license-image {
-  // max-width: 100%;
-  // max-height: 600px;
+  width: 100%;
+  height: 100%;
   border-radius: 8px;
   box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
 }
@@ -94,8 +90,8 @@ const initData = async () => {
   display: flex;
   align-items: center;
   justify-content: center;
-  width: 300px;
-  height: 200px;
+  width: 100%;
+  height: 100%;
   background-color: var(--el-fill-color-lighter);
   border-radius: 8px;
   .empty-icon {

+ 523 - 176
src/views/licenseManagement/contractManagement.vue

@@ -1,11 +1,8 @@
 <template>
   <div class="card content-box">
-    <div class="page-header">
-      <h1 class="store-title">重庆老火锅</h1>
-    </div>
     <div class="content-section">
       <div class="tip-text">
-        如需续约合同或更改合同图片请在此处上传,重新上传之后需要重新进行审核,审核通过后,新的合同图片将会覆盖之前的合同
+        如需续约合同或更改合同图片请在此处上传,重新上传之后需要重新进行审核,审核通过后,新的合同图片将会覆盖之前的合同
       </div>
       <div class="action-buttons">
         <el-button type="primary" @click="handleReplace"> 更换 </el-button>
@@ -17,10 +14,10 @@
         <el-col v-for="(item, index) in contractList" :key="index" :xs="12" :sm="8" :md="6" :lg="4" :xl="4">
           <div class="contract-item">
             <el-image
-              :src="item.url"
-              fit="cover"
+              :src="item.imgUrl"
+              fit=""
               class="contract-image"
-              :preview-src-list="contractList.map(img => img.url)"
+              :preview-src-list="contractList.map(img => img.imgUrl)"
               :initial-index="index"
             >
               <template #error>
@@ -31,59 +28,55 @@
             </el-image>
           </div>
         </el-col>
-        <template v-for="n in Math.max(0, 8 - contractList.length)" :key="`empty-${n}`">
-          <el-col :xs="12" :sm="8" :md="6" :lg="4" :xl="4">
-            <div class="contract-item empty-item">
-              <el-icon class="empty-icon">
-                <Picture />
-              </el-icon>
-            </div>
-          </el-col>
-        </template>
       </el-row>
     </div>
 
     <!-- 更换合同弹窗 -->
-    <el-dialog v-model="replaceDialogVisible" title="更换合同" width="800px" @close="handleReplaceDialogClose">
-      <div class="replace-upload-area">
-        <el-upload
-          ref="uploadRef"
-          v-model:file-list="fileList"
-          action="#"
-          list-type="picture-card"
-          :multiple="true"
-          :limit="20"
-          :http-request="handleHttpUpload"
-          :before-upload="beforeUpload"
-          :on-exceed="handleExceed"
-          :on-remove="handleRemove"
-          :on-success="handleUploadSuccess"
-        >
-          <el-icon><Plus /></el-icon>
-          <template #tip>
-            <div class="upload-tip">({{ fileList.length }}/20)</div>
-          </template>
-        </el-upload>
-      </div>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="handleCancelReplace"> 取消 </el-button>
-          <el-button type="primary" @click="handleSubmitReplace"> 去审核 </el-button>
+    <el-dialog v-model="replaceDialogVisible" title="更换合同" width="860px" :before-close="handleReplaceDialogClose">
+      <el-scrollbar height="400px" class="replace-upload-scrollbar">
+        <div class="replace-upload-area" :class="{ 'upload-full': uploadedImageCount >= uploadMaxCount }">
+          <el-upload
+            v-model:file-list="fileList"
+            list-type="picture-card"
+            :accept="'.jpg,.png'"
+            :limit="uploadMaxCount"
+            :auto-upload="false"
+            :disabled="hasUnuploadedImages"
+            multiple
+            :on-change="handleUploadChange"
+            :on-exceed="handleUploadExceed"
+            :on-preview="handlePictureCardPreview"
+            :before-remove="handleBeforeRemove"
+            :on-remove="handleRemove"
+            :show-file-list="true"
+          >
+            <template #trigger>
+              <div v-if="uploadedImageCount < uploadMaxCount" class="upload-trigger-card el-upload--picture-card">
+                <el-icon>
+                  <Plus />
+                </el-icon>
+                <div class="upload-tip">({{ uploadedImageCount }}/{{ uploadMaxCount }})</div>
+              </div>
+            </template>
+          </el-upload>
         </div>
-      </template>
-    </el-dialog>
-
-    <!-- 取消确认弹窗 -->
-    <el-dialog v-model="cancelConfirmVisible" title="提示" width="400px" :close-on-click-modal="false">
-      <div class="confirm-text">确定要取消本次图片上传吗?已上传的图片将不保存</div>
+      </el-scrollbar>
       <template #footer>
         <div class="dialog-footer">
-          <el-button @click="cancelConfirmVisible = false"> 取消 </el-button>
-          <el-button type="primary" @click="handleConfirmCancel"> 确定 </el-button>
+          <el-button @click="handleCancelReplace" :disabled="hasUnuploadedImages"> 取消 </el-button>
+          <el-button type="primary" @click="handleSubmitReplace" :disabled="hasUnuploadedImages"> 去审核 </el-button>
         </div>
       </template>
     </el-dialog>
 
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :url-list="imageViewerUrlList"
+      :initial-index="imageViewerInitialIndex"
+      @close="imageViewerVisible = false"
+    />
+
     <!-- 变更记录弹窗 -->
     <el-dialog v-model="changeRecordDialogVisible" title="变更记录" width="900px" :close-on-click-modal="false">
       <div class="change-record-content">
@@ -91,10 +84,23 @@
           {{ currentRecordDate }}
         </div>
         <div class="record-items">
-          <div v-for="(item, index) in changeRecordList" :key="index" class="record-item" :class="getStatusClass(item.status)">
-            <div class="record-status-text">
-              {{ getStatusText(item.status) }}
+          <div v-for="(item, index) in changeRecordList" :key="index" class="record-item">
+            <div class="record-status-badge" :class="getStatusClass(item.status)">
+              {{ getStatusName(item.status) }}
             </div>
+            <el-image
+              :src="item.imgUrl"
+              fit="cover"
+              class="record-image"
+              :preview-src-list="changeRecordList.map(record => record.imgUrl)"
+              :initial-index="index"
+            >
+              <template #error>
+                <div class="image-slot">
+                  <el-icon><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
           </div>
         </div>
         <div v-if="rejectionReason" class="rejection-reason">拒绝原因: {{ rejectionReason }}</div>
@@ -109,52 +115,87 @@
 </template>
 
 <script setup lang="ts" name="contractManagement">
-import { ref, onMounted } from "vue";
+import { ref, onMounted, computed } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { Plus, Picture } from "@element-plus/icons-vue";
-import { uploadImg } from "@/api/modules/upload";
-import type { UploadProps, UploadFile, UploadRequestOptions } from "element-plus";
-
-interface ContractItem {
-  id: string;
-  url: string;
-  name: string;
-}
+import type { UploadProps, UploadFile } from "element-plus";
+import { localGet } from "@/utils";
+import { getContractImages, uploadContractImage, submitContractReview } from "@/api/modules/licenseManagement";
 
 interface ChangeRecordItem {
   id: string;
-  status: "pending" | "success" | "failed";
+  status: number; // 状态:0-审核中,1-审核通过,2-审核拒绝
+  imgUrl: string; // 图片URL
   rejectionReason?: string;
 }
 
-const contractList = ref<ContractItem[]>([]);
+// 状态映射对象
+const statusMap: Record<number, { name: string; class: string }> = {
+  0: { name: "审核中", class: "status-pending" },
+  1: { name: "审核通过", class: "status-success" },
+  2: { name: "审核拒绝", class: "status-failed" }
+};
+
+const contractList = ref<any>([]);
 const replaceDialogVisible = ref(false);
-const cancelConfirmVisible = ref(false);
 const changeRecordDialogVisible = ref(false);
 const fileList = ref<UploadFile[]>([]);
-const uploadRef = ref();
 const currentRecordDate = ref("2025.08.01 10:29");
 const changeRecordList = ref<ChangeRecordItem[]>([]);
 const rejectionReason = ref("");
 
+const id = localGet("createdId");
+
+// ==================== 图片上传相关变量 ====================
+const uploadMaxCount = 20;
+const uploading = ref(false);
+const pendingUploadFiles = ref<UploadFile[]>([]);
+const imageUrlList = ref<string[]>([]); // 存储图片URL列表
+
+// 图片预览相关
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
+
+// 计算属性:获取已成功上传的图片数量
+const uploadedImageCount = computed(() => {
+  return fileList.value.filter((file: any) => file.status === "success" && file.url).length;
+});
+
+// 计算属性:检查是否有未上传完成的图片
+const hasUnuploadedImages = computed(() => {
+  // 检查是否有正在上传的文件
+  if (uploading.value || pendingUploadFiles.value.length > 0) {
+    return true;
+  }
+  // 检查文件列表中是否有状态为 "ready"(待上传)或 "uploading"(上传中)的图片
+  if (fileList.value && fileList.value.length > 0) {
+    return fileList.value.some((file: any) => {
+      return file.status === "ready" || file.status === "uploading";
+    });
+  }
+  return false;
+});
+
 onMounted(async () => {
   await initData();
 });
 
 const initData = async () => {
-  try {
-    // TODO: 调用API获取合同图片列表
-    // const response = await getContractImages();
-    // if (response.code === 200) {
-    //   contractList.value = response.data;
-    // }
-  } catch (error) {
-    ElMessage.error("获取合同图片失败");
+  const params = {
+    id: id
+  };
+  const res: any = await getContractImages(params);
+  if (res.code === 200) {
+    contractList.value = res.data;
   }
 };
 
 const handleReplace = () => {
   fileList.value = [];
+  imageUrlList.value = [];
+  pendingUploadFiles.value = [];
+  uploading.value = false;
   replaceDialogVisible.value = true;
 };
 
@@ -167,13 +208,13 @@ const handleViewChangeRecord = async () => {
     //   currentRecordDate.value = response.data.date;
     //   rejectionReason.value = response.data.rejectionReason || "";
     // }
-    // 模拟数据
+    // 模拟数据(假数据)
     changeRecordList.value = [
-      { id: "1", status: "pending" },
-      { id: "2", status: "pending" },
-      { id: "3", status: "pending" },
-      { id: "4", status: "pending" },
-      { id: "5", status: "pending" }
+      { id: "1", status: 0, imgUrl: "https://picsum.photos/150/150?random=1" },
+      { id: "2", status: 1, imgUrl: "https://picsum.photos/150/150?random=2" },
+      { id: "3", status: 0, imgUrl: "https://picsum.photos/150/150?random=3" },
+      { id: "4", status: 2, imgUrl: "https://picsum.photos/150/150?random=4" },
+      { id: "5", status: 1, imgUrl: "https://picsum.photos/150/150?random=5" }
     ];
     rejectionReason.value = "";
     changeRecordDialogVisible.value = true;
@@ -182,107 +223,340 @@ const handleViewChangeRecord = async () => {
   }
 };
 
-const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
-  const imgSize = rawFile.size / 1024 / 1024 < 10;
-  const imgType = ["image/jpeg", "image/png", "image/gif"].includes(rawFile.type);
-  if (!imgType) {
-    ElMessage.warning("上传图片不符合所需的格式!");
+/**
+ * 检查文件是否在排队中(未上传)
+ * @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;
+};
+
+/**
+ * 图片上传 - 删除前确认
+ * @param uploadFile 要删除的文件对象
+ * @param uploadFiles 当前文件列表
+ * @returns Promise<boolean>,true 允许删除,false 阻止删除
+ */
+const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
+  // 如果文件在排队中(未上传),禁止删除
+  if (isFilePending(uploadFile)) {
+    ElMessage.warning("图片尚未上传,请等待上传完成后再删除");
     return false;
   }
-  if (!imgSize) {
-    ElMessage.warning("上传图片大小不能超过 10M!");
+  try {
+    await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+    // 用户确认删除,返回 true 允许删除
+    return true;
+  } catch {
+    // 用户取消删除,返回 false 阻止删除
     return false;
   }
-  return true;
 };
 
-const handleHttpUpload = async (options: UploadRequestOptions) => {
-  const formData = new FormData();
-  formData.append("file", options.file);
-  try {
-    const { data } = await uploadImg(formData);
-    const fileUrl = data.fileUrl ? data.fileUrl : data[0];
-    options.onSuccess({ fileUrl }, options.file as any);
-  } catch (error) {
-    options.onError(error as any);
+/**
+ * 图片上传 - 移除图片回调(删除成功后调用)
+ * @param uploadFile 已删除的文件对象
+ * @param uploadFiles 删除后的文件列表
+ */
+const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
+  // 从被删除的文件对象中获取 url
+  const file = uploadFile as any;
+  const imageUrl = file.url;
+
+  if (imageUrl) {
+    // 从 imageUrl 数组中删除对应的 URL
+    const urlIndex = imageUrlList.value.indexOf(imageUrl);
+    if (urlIndex > -1) {
+      imageUrlList.value.splice(urlIndex, 1);
+    }
   }
+
+  if (file.url && file.url.startsWith("blob:")) {
+    URL.revokeObjectURL(file.url);
+  }
+  // 同步文件列表
+  fileList.value = [...uploadFiles];
+  // 删除成功后提示
+  ElMessage.success("图片已删除");
 };
 
-const handleExceed = () => {
-  ElMessage.warning("最多只能上传 20 张图片");
+/**
+ * 上传文件超出限制提示
+ */
+const handleUploadExceed: UploadProps["onExceed"] = () => {
+  ElMessage.warning(`最多只能上传${uploadMaxCount}张图片`);
 };
 
-const handleRemove = (file: UploadFile) => {
-  const index = fileList.value.findIndex(item => item.uid === file.uid);
-  if (index > -1) {
-    fileList.value.splice(index, 1);
+/**
+ * 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 = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (index > -1) {
+        fileList.value.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 = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+  if (existingIndex === -1) {
+    fileList.value.push(uploadFile);
+  }
+
+  const readyFiles = fileList.value.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 handleUploadSuccess = (response: any, uploadFile: UploadFile) => {
-  if (response && response.fileUrl) {
-    uploadFile.url = response.fileUrl;
+/**
+ * 处理上传队列 - 逐个上传文件
+ */
+const processUploadQueue = async () => {
+  if (uploading.value || pendingUploadFiles.value.length === 0) {
+    return;
+  }
+  // 每次只取一个文件进行上传
+  const file = pendingUploadFiles.value.shift();
+  if (file) {
+    await uploadSingleFile(file);
+    // 继续处理队列中的下一个文件
+    processUploadQueue();
   }
 };
 
-const handleCancelReplace = () => {
+/**
+ * 单文件上传图片
+ * @param file 待上传的文件
+ */
+const uploadSingleFile = async (file: UploadFile) => {
+  if (!file.raw) {
+    return;
+  }
+  const rawFile = file.raw as File;
+  const formData = new FormData();
+  formData.append("file", rawFile);
+  formData.append("user", "text");
+  file.status = "uploading";
+  file.percentage = 0;
+  uploading.value = true;
+
+  try {
+    // 上传过程中先保持进度为 0,避免接口异常时进度条误显示 100%
+    const result: any = await uploadContractImage(formData);
+    if (result?.code === 200 && result.data) {
+      // 处理单个文件的上传结果
+      let imageUrl = result.data[0];
+      if (!imageUrl) {
+        throw new Error("上传成功但未获取到图片URL");
+      }
+
+      file.status = "success";
+      file.percentage = 100;
+      // 保存图片URL到文件对象
+      file.url = imageUrl;
+      file.response = { url: imageUrl };
+
+      // 保存图片URL
+      if (!Array.isArray(imageUrlList.value)) {
+        imageUrlList.value = [];
+      }
+      if (!imageUrlList.value.includes(imageUrl)) {
+        imageUrlList.value.push(imageUrl);
+      }
+    } else {
+      throw new Error(result?.msg || "图片上传失败");
+    }
+  } catch (error: any) {
+    // 上传失败时保持进度条为 0
+    file.percentage = 0;
+    file.status = "fail";
+    if (file.url && file.url.startsWith("blob:")) {
+      URL.revokeObjectURL(file.url);
+    }
+    // 从文件列表中移除失败的文件
+    const index = fileList.value.findIndex((f: any) => f.uid === file.uid);
+    if (index > -1) {
+      fileList.value.splice(index, 1);
+    }
+  } finally {
+    uploading.value = false;
+    // 触发视图更新
+    fileList.value = [...fileList.value];
+  }
+};
+
+/**
+ * 图片预览 - 使用 el-image-viewer 预览功能
+ * @param file 上传文件对象
+ */
+const handlePictureCardPreview = (file: any) => {
+  // 如果文件在排队中(未上传),禁止预览
+  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 = fileList.value
+    .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 || file.response?.data));
+  if (currentIndex < 0) {
+    ElMessage.warning("图片尚未上传完成,无法预览");
+    return;
+  }
+  imageViewerUrlList.value = urlList;
+  imageViewerInitialIndex.value = currentIndex;
+  imageViewerVisible.value = true;
+};
+
+const handleCancelReplace = async () => {
+  // 如果有图片正在上传,阻止关闭
+  if (hasUnuploadedImages.value) {
+    ElMessage.warning("请等待图片上传完成后再关闭");
+    return;
+  }
   if (fileList.value.length > 0) {
-    cancelConfirmVisible.value = true;
+    try {
+      await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      });
+      // 用户确认取消
+      fileList.value = [];
+      imageUrlList.value = [];
+      pendingUploadFiles.value = [];
+      uploading.value = false;
+      replaceDialogVisible.value = false;
+    } catch {
+      // 用户取消操作,不做任何处理
+    }
   } else {
     replaceDialogVisible.value = false;
   }
 };
 
-const handleConfirmCancel = () => {
-  fileList.value = [];
-  cancelConfirmVisible.value = false;
-  replaceDialogVisible.value = false;
-};
-
-const handleReplaceDialogClose = () => {
+const handleReplaceDialogClose = async (done: () => void) => {
+  // 如果有图片正在上传,阻止关闭
+  if (hasUnuploadedImages.value) {
+    ElMessage.warning("请等待图片上传完成后再关闭");
+    return; // 不调用 done(),阻止关闭弹窗
+  }
   if (fileList.value.length > 0) {
-    cancelConfirmVisible.value = true;
+    try {
+      await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      });
+      // 用户确认取消,清空数据并关闭弹窗
+      fileList.value = [];
+      imageUrlList.value = [];
+      pendingUploadFiles.value = [];
+      uploading.value = false;
+      done(); // 调用 done() 允许关闭弹窗
+    } catch {
+      // 用户取消操作,不调用 done(),阻止关闭弹窗
+    }
+  } else {
+    // 没有文件,直接关闭
+    done();
   }
 };
 
 const handleSubmitReplace = async () => {
+  // 检查是否有未上传完成的图片
+  if (hasUnuploadedImages.value) {
+    ElMessage.warning("请等待图片上传完成后再提交");
+    return;
+  }
   if (fileList.value.length === 0) {
     ElMessage.warning("请至少上传一张图片");
     return;
   }
-  const uploadedFiles = fileList.value.filter(file => file.url);
+  const uploadedFiles = fileList.value.filter(file => file.status === "success");
   if (uploadedFiles.length === 0) {
     ElMessage.warning("请先上传图片");
     return;
   }
   try {
-    // TODO: 调用API提交审核
-    // const urls = uploadedFiles.map(file => file.url);
-    // await submitContractReview(urls);
+    // 根据文件列表顺序,生成带排序的图片数据(排序从0开始)
+    const imageDataWithSort = uploadedFiles.map((file, index) => ({
+      url: file.url,
+      sort: index
+    }));
+    await submitContractReview({ images: imageDataWithSort });
     ElMessage.success("提交审核成功");
     replaceDialogVisible.value = false;
     fileList.value = [];
+    imageUrlList.value = [];
+    pendingUploadFiles.value = [];
+    uploading.value = false;
     await initData();
   } catch (error) {
     ElMessage.error("提交审核失败");
   }
 };
 
-const getStatusClass = (status: string) => {
-  return {
-    "status-pending": status === "pending",
-    "status-success": status === "success",
-    "status-failed": status === "failed"
-  };
+const getStatusClass = (status: number) => {
+  const statusInfo = statusMap[status];
+  return statusInfo ? statusInfo.class : "";
 };
 
-const getStatusText = (status: string) => {
-  const map: Record<string, string> = {
-    pending: "审核中",
-    success: "审核通过",
-    failed: "审核拒绝"
-  };
-  return map[status] || "未知";
+const getStatusName = (status: number) => {
+  const statusInfo = statusMap[status];
+  return statusInfo ? statusInfo.name : "未知";
 };
 </script>
 
@@ -298,15 +572,14 @@ const getStatusText = (status: string) => {
 }
 .content-section {
   display: flex;
-  gap: 20px;
-  align-items: flex-start;
+  gap: 50px;
+  align-items: center;
   justify-content: space-between;
-  margin-bottom: 30px;
+  margin-top: 20px;
+  margin-bottom: 50px;
 }
 .tip-text {
-  flex: 1;
-  font-size: 14px;
-  line-height: 1.6;
+  font-size: 18px;
   color: var(--el-text-color-regular);
 }
 .action-buttons {
@@ -316,6 +589,7 @@ const getStatusText = (status: string) => {
 }
 .contract-container {
   width: 100%;
+  min-height: 570px;
   padding: 20px;
   background-color: var(--el-bg-color-page);
   border-radius: 8px;
@@ -345,31 +619,84 @@ const getStatusText = (status: string) => {
     color: var(--el-text-color-placeholder);
     background: var(--el-fill-color-light);
   }
-  &.empty-item {
-    background-color: var(--el-fill-color-lighter);
-    border: 1px dashed var(--el-border-color);
-    .empty-icon {
-      font-size: 48px;
-      color: var(--el-text-color-placeholder);
-    }
+}
+.replace-upload-scrollbar {
+  :deep(.el-scrollbar__wrap) {
+    overflow-x: hidden;
   }
 }
 .replace-upload-area {
   min-height: 300px;
-  padding: 20px 0;
+  padding: 20px;
+  :deep(.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label) {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  :deep(.el-upload-list__item.is-success:focus .el-upload-list__item-status-label) {
+    display: inline-flex !important;
+    opacity: 1 !important;
+  }
+  :deep(.el-upload-list--picture-card .el-icon--close-tip) {
+    display: none !important;
+  }
+  &.upload-full {
+    :deep(.el-upload--picture-card) {
+      display: none !important;
+    }
+  }
+}
+.dialog-footer {
+  display: flex;
+  gap: 10px;
+  justify-content: center;
+}
+
+/* el-upload 图片预览铺满容器 */
+:deep(.el-upload-list--picture-card) {
+  .el-upload-list__item {
+    overflow: hidden;
+    .el-upload-list__item-thumbnail {
+      width: 100%;
+      height: 100%;
+      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;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  font-size: 28px;
+  color: #8c939d;
   .upload-tip {
-    margin-top: 10px;
+    margin-top: 8px;
     font-size: 14px;
-    color: var(--el-text-color-secondary);
-    text-align: center;
+    color: #8c939d;
   }
 }
-.confirm-text {
-  padding: 10px 0;
-  font-size: 14px;
-  line-height: 1.6;
-  color: var(--el-text-color-primary);
-}
 .change-record-content {
   padding: 20px 0;
   .record-date {
@@ -385,31 +712,51 @@ const getStatusText = (status: string) => {
     margin-bottom: 20px;
   }
   .record-item {
-    display: flex;
-    align-items: center;
-    justify-content: center;
+    position: relative;
     width: 150px;
-    height: 100px;
-    border: 1px solid var(--el-border-color-light);
+    height: 150px;
+    overflow: hidden;
     border-radius: 8px;
-    .record-status-text {
-      font-size: 14px;
+    .record-status-badge {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      z-index: 1;
+      padding: 4px 8px;
+      font-size: 12px;
       font-weight: 500;
+      text-align: center;
+      border-radius: 0 0 8px 8px;
+      &.status-pending {
+        color: #e6a23c;
+        background-color: rgb(253 246 236 / 95%);
+        border-top: 1px solid #e6a23c;
+      }
+      &.status-success {
+        color: #67c23a;
+        background-color: rgb(240 249 255 / 95%);
+        border-top: 1px solid #67c23a;
+      }
+      &.status-failed {
+        color: #f56c6c;
+        background-color: rgb(254 240 240 / 95%);
+        border-top: 1px solid #f56c6c;
+      }
     }
-    &.status-pending {
-      color: #e6a23c;
-      background-color: #fdf6ec;
-      border-color: #e6a23c;
-    }
-    &.status-success {
-      color: #67c23a;
-      background-color: #f0f9ff;
-      border-color: #67c23a;
-    }
-    &.status-failed {
-      color: #f56c6c;
-      background-color: #fef0f0;
-      border-color: #f56c6c;
+    .record-image {
+      width: 100%;
+      height: 100%;
+      .image-slot {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 100%;
+        font-size: 30px;
+        color: var(--el-text-color-placeholder);
+        background: var(--el-fill-color-light);
+      }
     }
   }
   .rejection-reason {

+ 515 - 152
src/views/licenseManagement/foodBusinessLicense.vue

@@ -1,8 +1,5 @@
 <template>
   <div class="card content-box">
-    <div class="page-header">
-      <h1 class="store-title">重庆老火锅</h1>
-    </div>
     <div class="content-section">
       <div class="tip-text">
         如需变更请在此处上传,重新上传之后需要重新进行审核,审核通过后,新的食品经营许可证将会覆盖之前的食品经营许可证
@@ -30,48 +27,47 @@
     </div>
 
     <!-- 更换食品经营许可证弹窗 -->
-    <el-dialog v-model="replaceDialogVisible" title="更换食品经营许可证" width="600px" @close="handleReplaceDialogClose">
-      <div class="replace-upload-area">
+    <el-dialog v-model="replaceDialogVisible" title="更换食品经营许可证" width="600px" :before-close="handleReplaceDialogClose">
+      <div class="replace-upload-area" :class="{ 'upload-full': uploadedImageCount >= 1 }">
         <el-upload
-          ref="uploadRef"
-          action="#"
-          :show-file-list="false"
-          :http-request="handleHttpUpload"
-          :before-upload="beforeUpload"
-          drag
-          class="upload-box"
+          v-model:file-list="fileList"
+          list-type="picture-card"
+          :accept="'.jpg,.png'"
+          :limit="1"
+          :auto-upload="false"
+          :disabled="hasUnuploadedImages"
+          :on-change="handleUploadChange"
+          :on-exceed="handleUploadExceed"
+          :on-preview="handlePictureCardPreview"
+          :before-remove="handleBeforeRemove"
+          :on-remove="handleRemove"
+          :show-file-list="true"
         >
-          <el-icon class="el-icon--upload">
-            <upload-filled />
-          </el-icon>
-          <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
-          <template #tip>
-            <div class="upload-tip">({{ uploadedCount }}/1)</div>
+          <template #trigger>
+            <div v-if="uploadedImageCount < 1" class="upload-trigger-card el-upload--picture-card">
+              <el-icon>
+                <Plus />
+              </el-icon>
+              <div class="upload-tip">({{ uploadedImageCount }}/1)</div>
+            </div>
           </template>
         </el-upload>
-        <div v-if="newLicenseUrl" class="preview-section">
-          <div class="preview-label">预览</div>
-          <el-image :src="newLicenseUrl" fit="contain" class="preview-image" />
-        </div>
       </div>
       <template #footer>
         <div class="dialog-footer">
-          <el-button @click="handleCancelReplace"> 取消 </el-button>
-          <el-button type="primary" @click="handleSubmitReplace"> 去审核 </el-button>
+          <el-button @click="handleCancelReplace" :disabled="hasUnuploadedImages"> 取消 </el-button>
+          <el-button type="primary" @click="handleSubmitReplace" :disabled="hasUnuploadedImages"> 去审核 </el-button>
         </div>
       </template>
     </el-dialog>
 
-    <!-- 取消确认弹窗 -->
-    <el-dialog v-model="cancelConfirmVisible" title="提示" width="400px" :close-on-click-modal="false">
-      <div class="confirm-text">确定要取消本次图片上传吗?已上传的图片将不保存</div>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button @click="cancelConfirmVisible = false"> 取消 </el-button>
-          <el-button type="primary" @click="handleConfirmCancel"> 确定 </el-button>
-        </div>
-      </template>
-    </el-dialog>
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :url-list="imageViewerUrlList"
+      :initial-index="imageViewerInitialIndex"
+      @close="imageViewerVisible = false"
+    />
 
     <!-- 变更记录弹窗 -->
     <el-dialog v-model="changeRecordDialogVisible" title="变更记录" width="900px" :close-on-click-modal="false">
@@ -80,10 +76,23 @@
           {{ currentRecordDate }}
         </div>
         <div class="record-items">
-          <div v-for="(item, index) in changeRecordList" :key="index" class="record-item" :class="getStatusClass(item.status)">
-            <div class="record-status-text">
+          <div v-for="(item, index) in changeRecordList" :key="index" class="record-item">
+            <div class="record-status-badge" :class="getStatusClass(item.status)">
               {{ getStatusText(item.status) }}
             </div>
+            <el-image
+              :src="item.imgUrl"
+              fit="cover"
+              class="record-image"
+              :preview-src-list="changeRecordList.map(record => record.imgUrl)"
+              :initial-index="index"
+            >
+              <template #error>
+                <div class="image-slot">
+                  <el-icon><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
           </div>
         </div>
         <div v-if="rejectionReason" class="rejection-reason">拒绝原因: {{ rejectionReason }}</div>
@@ -99,47 +108,77 @@
 
 <script setup lang="ts" name="foodBusinessLicense">
 import { ref, computed, onMounted } from "vue";
-import { ElMessage } from "element-plus";
-import { Picture, UploadFilled } from "@element-plus/icons-vue";
-import { uploadImg } from "@/api/modules/upload";
-import type { UploadProps, UploadRequestOptions } from "element-plus";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Picture, Plus } from "@element-plus/icons-vue";
+import type { UploadProps, UploadFile } from "element-plus";
+import { getFoodBusinessLicense, uploadContractImage, submitFoodLicenseReview } from "@/api/modules/licenseManagement";
+import { localGet } from "@/utils";
 
 interface ChangeRecordItem {
   id: string;
   status: "pending" | "success" | "failed";
+  imgUrl: string; // 图片URL
   rejectionReason?: string;
 }
+const id = localGet("createdId");
 
 const licenseImage = ref<string>("");
 const replaceDialogVisible = ref(false);
-const cancelConfirmVisible = ref(false);
 const changeRecordDialogVisible = ref(false);
-const uploadRef = ref();
-const newLicenseUrl = ref("");
+const fileList = ref<UploadFile[]>([]);
 const currentRecordDate = ref("2025.08.01 10:29");
 const changeRecordList = ref<ChangeRecordItem[]>([]);
 const rejectionReason = ref("");
 
-const uploadedCount = computed(() => (newLicenseUrl.value ? 1 : 0));
+// ==================== 图片上传相关变量 ====================
+const uploading = ref(false);
+const pendingUploadFiles = ref<UploadFile[]>([]);
+const imageUrlList = ref<string[]>([]); // 存储图片URL列表
+
+// 图片预览相关
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
+
+// 计算属性:获取已成功上传的图片数量
+const uploadedImageCount = computed(() => {
+  return fileList.value.filter((file: any) => file.status === "success" && file.url).length;
+});
+
+// 计算属性:检查是否有未上传完成的图片
+const hasUnuploadedImages = computed(() => {
+  // 检查是否有正在上传的文件
+  if (uploading.value || pendingUploadFiles.value.length > 0) {
+    return true;
+  }
+  // 检查文件列表中是否有状态为 "ready"(待上传)或 "uploading"(上传中)的图片
+  if (fileList.value && fileList.value.length > 0) {
+    return fileList.value.some((file: any) => {
+      return file.status === "ready" || file.status === "uploading";
+    });
+  }
+  return false;
+});
 
 onMounted(async () => {
   await initData();
 });
 
 const initData = async () => {
-  try {
-    // TODO: 调用API获取食品经营许可证图片
-    // const response = await getFoodBusinessLicense();
-    // if (response.code === 200) {
-    //   licenseImage.value = response.data.imageUrl;
-    // }
-  } catch (error) {
-    ElMessage.error("获取食品经营许可证失败");
+  const params = {
+    id: id
+  };
+  const res: any = await getFoodBusinessLicense(params);
+  if (res.code === 200) {
+    licenseImage.value = res.data[0].imgUrl;
   }
 };
 
 const handleReplace = () => {
-  newLicenseUrl.value = "";
+  fileList.value = [];
+  imageUrlList.value = [];
+  pendingUploadFiles.value = [];
+  uploading.value = false;
   replaceDialogVisible.value = true;
 };
 
@@ -152,8 +191,12 @@ const handleViewChangeRecord = async () => {
     //   currentRecordDate.value = response.data.date;
     //   rejectionReason.value = response.data.rejectionReason || "";
     // }
-    // 模拟数据 - 根据图片,可能是单个记录
-    changeRecordList.value = [{ id: "1", status: "pending" }];
+    // 模拟数据(假数据)
+    changeRecordList.value = [
+      { id: "1", status: "pending", imgUrl: "https://picsum.photos/150/150?random=1" },
+      { id: "2", status: "success", imgUrl: "https://picsum.photos/150/150?random=2" },
+      { id: "3", status: "failed", imgUrl: "https://picsum.photos/150/150?random=3" }
+    ];
     rejectionReason.value = "";
     changeRecordDialogVisible.value = true;
   } catch (error) {
@@ -161,63 +204,330 @@ const handleViewChangeRecord = async () => {
   }
 };
 
-const beforeUpload: UploadProps["beforeUpload"] = rawFile => {
-  const imgSize = rawFile.size / 1024 / 1024 < 10;
-  const imgType = ["image/jpeg", "image/png", "image/gif"].includes(rawFile.type);
-  if (!imgType) {
-    ElMessage.warning("上传图片不符合所需的格式!");
+/**
+ * 检查文件是否在排队中(未上传)
+ * @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;
+};
+
+/**
+ * 图片上传 - 删除前确认
+ * @param uploadFile 要删除的文件对象
+ * @param uploadFiles 当前文件列表
+ * @returns Promise<boolean>,true 允许删除,false 阻止删除
+ */
+const handleBeforeRemove = async (uploadFile: any, uploadFiles: any[]): Promise<boolean> => {
+  // 如果文件在排队中(未上传),禁止删除
+  if (isFilePending(uploadFile)) {
+    ElMessage.warning("图片尚未上传,请等待上传完成后再删除");
     return false;
   }
-  if (!imgSize) {
-    ElMessage.warning("上传图片大小不能超过 10M!");
+  try {
+    await ElMessageBox.confirm("确定要删除这张图片吗?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+    // 用户确认删除,返回 true 允许删除
+    return true;
+  } catch {
+    // 用户取消删除,返回 false 阻止删除
     return false;
   }
-  return true;
 };
 
-const handleHttpUpload = async (options: UploadRequestOptions) => {
+/**
+ * 图片上传 - 移除图片回调(删除成功后调用)
+ * @param uploadFile 已删除的文件对象
+ * @param uploadFiles 删除后的文件列表
+ */
+const handleRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) => {
+  // 从被删除的文件对象中获取 url
+  const file = uploadFile as any;
+  const imageUrl = file.url;
+
+  if (imageUrl) {
+    // 从 imageUrl 数组中删除对应的 URL
+    const urlIndex = imageUrlList.value.indexOf(imageUrl);
+    if (urlIndex > -1) {
+      imageUrlList.value.splice(urlIndex, 1);
+    }
+  }
+
+  if (file.url && file.url.startsWith("blob:")) {
+    URL.revokeObjectURL(file.url);
+  }
+  // 同步文件列表
+  fileList.value = [...uploadFiles];
+  // 删除成功后提示
+  ElMessage.success("图片已删除");
+};
+
+/**
+ * 上传文件超出限制提示
+ */
+const handleUploadExceed: UploadProps["onExceed"] = () => {
+  ElMessage.warning("最多只能上传1张图片");
+};
+
+/**
+ * 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 = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+      if (index > -1) {
+        fileList.value.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 = fileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+  if (existingIndex === -1) {
+    fileList.value.push(uploadFile);
+  }
+
+  const readyFiles = fileList.value.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 processUploadQueue = async () => {
+  if (uploading.value || pendingUploadFiles.value.length === 0) {
+    return;
+  }
+  // 每次只取一个文件进行上传
+  const file = pendingUploadFiles.value.shift();
+  if (file) {
+    await uploadSingleFile(file);
+    // 继续处理队列中的下一个文件
+    processUploadQueue();
+  }
+};
+
+/**
+ * 单文件上传图片
+ * @param file 待上传的文件
+ */
+const uploadSingleFile = async (file: UploadFile) => {
+  if (!file.raw) {
+    return;
+  }
+  const rawFile = file.raw as File;
   const formData = new FormData();
-  formData.append("file", options.file);
+  formData.append("file", rawFile);
+  formData.append("user", "text");
+  file.status = "uploading";
+  file.percentage = 0;
+  uploading.value = true;
+
   try {
-    const { data } = await uploadImg(formData);
-    newLicenseUrl.value = data.fileUrl ? data.fileUrl : data[0];
-    ElMessage.success("上传成功");
-  } catch (error) {
-    ElMessage.error("上传失败");
+    // 上传过程中保持进度为 0,避免接口异常时进度条误显示 100%
+    const result: any = await uploadContractImage(formData);
+    if (result?.code === 200 && result.data) {
+      // 处理单个文件的上传结果
+      let imageUrl = result.data[0];
+      if (!imageUrl) {
+        throw new Error("上传成功但未获取到图片URL");
+      }
+
+      file.status = "success";
+      file.percentage = 100;
+      // 保存图片URL到文件对象
+      file.url = imageUrl;
+      file.response = { url: imageUrl };
+
+      // 保存图片URL
+      if (!Array.isArray(imageUrlList.value)) {
+        imageUrlList.value = [];
+      }
+      if (!imageUrlList.value.includes(imageUrl)) {
+        imageUrlList.value.push(imageUrl);
+      }
+    } else {
+      throw new Error(result?.msg || "图片上传失败");
+    }
+  } catch (error: any) {
+    file.status = "fail";
+    // 上传失败时保持进度条为 0
+    file.percentage = 0;
+    if (file.url && file.url.startsWith("blob:")) {
+      URL.revokeObjectURL(file.url);
+    }
+    // 从文件列表中移除失败的文件
+    const index = fileList.value.findIndex((f: any) => f.uid === file.uid);
+    if (index > -1) {
+      fileList.value.splice(index, 1);
+    }
+    // Error message handled by global upload method, except for specific business logic errors
+    if (error?.message && error.message.includes("未获取到图片URL")) {
+      ElMessage.error(error.message);
+    }
+  } finally {
+    uploading.value = false;
+    // 触发视图更新
+    fileList.value = [...fileList.value];
   }
 };
 
-const handleCancelReplace = () => {
-  if (newLicenseUrl.value) {
-    cancelConfirmVisible.value = true;
-  } else {
-    replaceDialogVisible.value = false;
+/**
+ * 图片预览 - 使用 el-image-viewer 预览功能
+ * @param file 上传文件对象
+ */
+const handlePictureCardPreview = (file: any) => {
+  // 如果文件在排队中(未上传),禁止预览
+  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 = fileList.value
+    .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 || file.response?.data));
+  if (currentIndex < 0) {
+    ElMessage.warning("图片尚未上传完成,无法预览");
+    return;
   }
+  imageViewerUrlList.value = urlList;
+  imageViewerInitialIndex.value = currentIndex;
+  imageViewerVisible.value = true;
 };
 
-const handleConfirmCancel = () => {
-  newLicenseUrl.value = "";
-  cancelConfirmVisible.value = false;
-  replaceDialogVisible.value = false;
+const handleCancelReplace = async () => {
+  // 如果有图片正在上传,阻止关闭
+  if (hasUnuploadedImages.value) {
+    ElMessage.warning("请等待图片上传完成后再关闭");
+    return;
+  }
+  if (fileList.value.length > 0) {
+    try {
+      await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      });
+      // 用户确认取消
+      fileList.value = [];
+      imageUrlList.value = [];
+      pendingUploadFiles.value = [];
+      uploading.value = false;
+      replaceDialogVisible.value = false;
+    } catch {
+      // 用户取消操作,不做任何处理
+    }
+  } else {
+    replaceDialogVisible.value = false;
+  }
 };
 
-const handleReplaceDialogClose = () => {
-  if (newLicenseUrl.value) {
-    cancelConfirmVisible.value = true;
+const handleReplaceDialogClose = async (done: () => void) => {
+  // 如果有图片正在上传,阻止关闭
+  if (hasUnuploadedImages.value) {
+    ElMessage.warning("请等待图片上传完成后再关闭");
+    return; // 不调用 done(),阻止关闭弹窗
+  }
+  if (fileList.value.length > 0) {
+    try {
+      await ElMessageBox.confirm("确定要取消本次图片上传吗?已上传的图片将不保存", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      });
+      // 用户确认取消,清空数据并关闭弹窗
+      fileList.value = [];
+      imageUrlList.value = [];
+      pendingUploadFiles.value = [];
+      uploading.value = false;
+      done(); // 调用 done() 允许关闭弹窗
+    } catch {
+      // 用户取消操作,不调用 done(),阻止关闭弹窗
+    }
+  } else {
+    // 没有文件,直接关闭
+    done();
   }
 };
 
 const handleSubmitReplace = async () => {
-  if (!newLicenseUrl.value) {
+  // 检查是否有未上传完成的图片
+  if (hasUnuploadedImages.value) {
+    ElMessage.warning("请等待图片上传完成后再提交");
+    return;
+  }
+  if (fileList.value.length === 0) {
+    ElMessage.warning("请先上传图片");
+    return;
+  }
+  const uploadedFiles = fileList.value.filter(file => file.status === "success");
+  if (uploadedFiles.length === 0) {
     ElMessage.warning("请先上传图片");
     return;
   }
   try {
-    // TODO: 调用API提交审核
-    // await submitFoodLicenseReview(newLicenseUrl.value);
+    // 只提交单张图片,排序为0
+    const imageDataWithSort = uploadedFiles.map((file, index) => ({
+      url: file.url,
+      sort: index
+    }));
+    await submitFoodLicenseReview({ images: imageDataWithSort });
     ElMessage.success("提交审核成功");
     replaceDialogVisible.value = false;
-    newLicenseUrl.value = "";
+    fileList.value = [];
+    imageUrlList.value = [];
+    pendingUploadFiles.value = [];
+    uploading.value = false;
     await initData();
   } catch (error) {
     ElMessage.error("提交审核失败");
@@ -225,11 +535,12 @@ const handleSubmitReplace = async () => {
 };
 
 const getStatusClass = (status: string) => {
-  return {
-    "status-pending": status === "pending",
-    "status-success": status === "success",
-    "status-failed": status === "failed"
+  const statusMap: Record<string, string> = {
+    pending: "status-pending",
+    success: "status-success",
+    failed: "status-failed"
   };
+  return statusMap[status] || "";
 };
 
 const getStatusText = (status: string) => {
@@ -254,15 +565,14 @@ const getStatusText = (status: string) => {
 }
 .content-section {
   display: flex;
-  gap: 20px;
-  align-items: flex-start;
+  gap: 50px;
+  align-items: center;
   justify-content: space-between;
-  margin-bottom: 30px;
+  margin-top: 20px;
+  margin-bottom: 50px;
 }
 .tip-text {
-  flex: 1;
-  font-size: 14px;
-  line-height: 1.6;
+  font-size: 18px;
   color: var(--el-text-color-regular);
 }
 .action-buttons {
@@ -271,8 +581,6 @@ const getStatusText = (status: string) => {
   gap: 10px;
 }
 .license-container {
-  // width: 100%;
-  // min-height: 500px;
   padding: 20px;
   background-color: var(--el-bg-color-page);
   border-radius: 8px;
@@ -281,13 +589,12 @@ const getStatusText = (status: string) => {
   display: flex;
   align-items: center;
   justify-content: center;
-
-  // width: 100%;
-  // min-height: 500px;
+  width: 700px;
+  height: 500px;
 }
 .license-image {
-  // max-width: 100%;
-  // max-height: 600px;
+  width: 100%;
+  height: 100%;
   border-radius: 8px;
   box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
 }
@@ -295,8 +602,8 @@ const getStatusText = (status: string) => {
   display: flex;
   align-items: center;
   justify-content: center;
-  width: 300px;
-  height: 200px;
+  width: 100%;
+  height: 100%;
   background-color: var(--el-fill-color-lighter);
   border-radius: 8px;
   .empty-icon {
@@ -306,38 +613,75 @@ const getStatusText = (status: string) => {
 }
 .replace-upload-area {
   min-height: 300px;
-  padding: 20px 0;
-  .upload-box {
-    width: 100%;
-    min-height: 200px;
+  padding: 20px;
+  :deep(.el-upload-list--picture-card .el-upload-list__item:hover .el-upload-list__item-status-label) {
+    display: inline-flex !important;
+    opacity: 1 !important;
   }
-  .upload-tip {
-    margin-top: 10px;
-    font-size: 14px;
-    color: var(--el-text-color-secondary);
-    text-align: center;
+  :deep(.el-upload-list__item.is-success:focus .el-upload-list__item-status-label) {
+    display: inline-flex !important;
+    opacity: 1 !important;
   }
-  .preview-section {
-    padding: 20px;
-    margin-top: 30px;
-    background-color: var(--el-fill-color-lighter);
-    border-radius: 8px;
-    .preview-label {
-      margin-bottom: 15px;
-      font-size: 14px;
-      color: var(--el-text-color-secondary);
+  :deep(.el-upload-list--picture-card .el-icon--close-tip) {
+    display: none !important;
+  }
+  &.upload-full {
+    :deep(.el-upload--picture-card) {
+      display: none !important;
     }
-    .preview-image {
+  }
+}
+.dialog-footer {
+  display: flex;
+  gap: 10px;
+  justify-content: center;
+}
+
+/* el-upload 图片预览铺满容器 */
+:deep(.el-upload-list--picture-card) {
+  .el-upload-list__item {
+    overflow: hidden;
+    .el-upload-list__item-thumbnail {
       width: 100%;
-      max-height: 300px;
+      height: 100%;
+      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;
     }
   }
 }
-.confirm-text {
-  padding: 10px 0;
-  font-size: 14px;
-  line-height: 1.6;
-  color: var(--el-text-color-primary);
+.upload-trigger-card {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+  font-size: 28px;
+  color: #8c939d;
+  .upload-tip {
+    margin-top: 8px;
+    font-size: 14px;
+    color: #8c939d;
+  }
 }
 .change-record-content {
   padding: 20px 0;
@@ -354,32 +698,51 @@ const getStatusText = (status: string) => {
     margin-bottom: 20px;
   }
   .record-item {
-    display: flex;
-    align-items: center;
-    justify-content: center;
+    position: relative;
     width: 150px;
-    height: 100px;
-    background-color: var(--el-fill-color-lighter);
-    border: 1px solid var(--el-border-color-light);
+    height: 150px;
+    overflow: hidden;
     border-radius: 8px;
-    .record-status-text {
-      font-size: 14px;
+    .record-status-badge {
+      position: absolute;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      z-index: 1;
+      padding: 4px 8px;
+      font-size: 12px;
       font-weight: 500;
+      text-align: center;
+      border-radius: 0 0 8px 8px;
+      &.status-pending {
+        color: #e6a23c;
+        background-color: rgb(253 246 236 / 95%);
+        border-top: 1px solid #e6a23c;
+      }
+      &.status-success {
+        color: #67c23a;
+        background-color: rgb(240 249 255 / 95%);
+        border-top: 1px solid #67c23a;
+      }
+      &.status-failed {
+        color: #f56c6c;
+        background-color: rgb(254 240 240 / 95%);
+        border-top: 1px solid #f56c6c;
+      }
     }
-    &.status-pending {
-      color: #e6a23c;
-      background-color: #fdf6ec;
-      border-color: #e6a23c;
-    }
-    &.status-success {
-      color: #67c23a;
-      background-color: #f0f9ff;
-      border-color: #67c23a;
-    }
-    &.status-failed {
-      color: #f56c6c;
-      background-color: #fef0f0;
-      border-color: #f56c6c;
+    .record-image {
+      width: 100%;
+      height: 100%;
+      .image-slot {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 100%;
+        height: 100%;
+        font-size: 30px;
+        color: var(--el-text-color-placeholder);
+        background: var(--el-fill-color-light);
+      }
     }
   }
   .rejection-reason {

+ 11 - 5
src/views/login/index.vue

@@ -579,7 +579,7 @@
 </template>
 
 <script setup lang="ts" name="login">
-import { ref, reactive, watch, onMounted, onBeforeUnmount } from "vue";
+import { ref, reactive, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { HOME_URL } from "@/config";
 import { Login } from "@/api/interface/index";
@@ -859,15 +859,21 @@ const handleLogin = async () => {
         if (localGet("geeker-user").userInfo.phone) {
           localSet("iphone", localGet("geeker-user").userInfo.phone);
         }
+        if (localGet("geeker-user").userInfo.businessSection) {
+          localSet("businessSection", localGet("geeker-user").userInfo.businessSection);
+        }
         await initDynamicRouter();
 
         // 3.清空 tabs、keepAlive 数据
         await tabsStore.setTabs([]);
         await keepAliveStore.setKeepAliveName([]);
-        console.log("调整");
-        console.log(router.getRoutes(), route);
-        // 4.跳转到首页
-        router.push(HOME_URL);
+
+        // 4.等待路由完全初始化后再跳转
+        // 使用 nextTick 确保路由已完全添加
+        await nextTick();
+
+        // 5.跳转到首页,使用 replace 避免历史记录问题
+        await router.replace(HOME_URL);
 
         ElNotification({
           title: "登录成功",

+ 95 - 77
src/views/orderManagement/detail.vue

@@ -15,7 +15,7 @@
           <div class="detail-item">
             <div class="detail-label">订单名称</div>
             <div class="detail-value">
-              {{ formData.productName || "--" }}
+              {{ formData.couponName || "--" }}
             </div>
           </div>
           <!-- 订单编号 -->
@@ -29,56 +29,54 @@
           <div class="detail-item">
             <div class="detail-label">下单时间</div>
             <div class="detail-value">
-              {{ formData.orderTime || "--" }}
+              {{ formData.createdTime?.replace(/-/g, "/") || "--" }}
             </div>
           </div>
           <!-- 原价 -->
           <div class="detail-item">
             <div class="detail-label">原价</div>
             <div class="detail-value">
-              {{ formData.originalPrice ? `¥${formData.originalPrice}` : "--" }}
+              {{ formatCurrency(formData.originalPrice, 2, "¥") }}
             </div>
           </div>
           <!-- 优惠价 -->
           <div class="detail-item">
             <div class="detail-label">优惠价</div>
             <div class="detail-value">
-              {{ formData.discountedPrice ? `¥${formData.discountedPrice}` : "--" }}
+              {{ formatCurrency(formData.price, 2, "¥") }}
             </div>
           </div>
           <!-- 数量 -->
           <div class="detail-item">
             <div class="detail-label">数量</div>
-            <div class="detail-value">
-              {{ formData.quantity || "--" }}
-            </div>
+            <div class="detail-value">x{{ formData.orderCouponMiddleList ? formData.orderCouponMiddleList.length : 0 }}</div>
           </div>
           <!-- 优惠券减免 -->
           <div class="detail-item">
             <div class="detail-label">优惠券减免</div>
             <div class="detail-value">
-              {{ formData.couponDiscount ? `¥${formData.couponDiscount}` : "--" }}
+              {{ formatCurrency(formData.nominalValue, 2, "¥") }}
             </div>
           </div>
           <!-- 优惠券类型 -->
           <div class="detail-item">
             <div class="detail-label">优惠券类型</div>
             <div class="detail-value">
-              {{ formData.couponType || "--" }}
+              {{ getCouponType(formData.type) }}
             </div>
           </div>
           <!-- 预留手机号 -->
           <div class="detail-item">
             <div class="detail-label">预留手机号</div>
             <div class="detail-value">
-              {{ formData.phone || "--" }}
+              {{ formData.userPhone || "--" }}
             </div>
           </div>
           <!-- 预计收入 -->
           <div class="detail-item">
             <div class="detail-label">预计收入</div>
             <div class="detail-value">
-              {{ formData.estimatedIncome ? `¥${formData.estimatedIncome}` : "--" }}
+              {{ formatCurrency(formData.expectIncome, 2, "¥") }}
             </div>
           </div>
         </div>
@@ -97,29 +95,31 @@
                   {{ getCouponStatusName(coupon.status) }}
                 </div>
               </div>
-              <!-- 券码 -->
-              <div class="detail-item">
-                <div class="detail-label">券码</div>
-                <div class="detail-value">
-                  {{ coupon.code || "--" }}
+              <template v-for="(item, index) in coupon.list" :key="index">
+                <!-- 券码 -->
+                <div class="detail-item">
+                  <div class="detail-label">券码</div>
+                  <div class="detail-value">
+                    {{ item.couponCode || "--" }}
+                  </div>
                 </div>
-              </div>
-              <!-- 核销时间 -->
-              <div v-if="coupon.status === '2'" class="detail-item">
-                <div class="detail-label">核销时间</div>
-                <div class="detail-value">
-                  {{ coupon.verifyTime || "--" }}
+                <!-- 核销时间 -->
+                <div v-if="item.usedTime && coupon.status == 2" class="detail-item">
+                  <div class="detail-label">核销时间</div>
+                  <div class="detail-value">
+                    {{ item.usedTime || "--" }}
+                  </div>
                 </div>
-              </div>
-              <!-- 退款时间 -->
-              <div v-if="coupon.status === '3'" class="detail-item">
-                <div class="detail-label">退款时间</div>
-                <div class="detail-value">
-                  {{ coupon.refundTime || "--" }}
+                <!-- 退款时间 -->
+                <div v-if="item.refundTime && coupon.status == 5" class="detail-item">
+                  <div class="detail-label">退款时间</div>
+                  <div class="detail-value">
+                    {{ item.refundTime || "--" }}
+                  </div>
                 </div>
-              </div>
-              <!-- 分隔线 -->
-              <el-divider v-if="index < couponList.length - 1" style="margin: 20px 0" />
+                <!-- 分隔线 -->
+                <el-divider v-if="index < couponList.length - 1" style="margin: 20px 0" />
+              </template>
             </div>
           </div>
           <div v-else class="empty-text">--</div>
@@ -134,10 +134,12 @@
  * 订单管理 - 详情页面
  * 功能:显示订单的详细信息
  */
-import { ref, onMounted } from "vue";
+import { ref, onMounted, computed } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
 import { getStaffConfigDeatail } from "@/api/modules/staffConfig";
+import { queryUserOrderDetail } from "@/api/modules/orderManagement";
+import { formatCurrency } from "@/utils/formatCurrency";
 
 // ==================== 响应式数据定义 ====================
 
@@ -157,15 +159,40 @@ const couponList = ref<any[]>([]);
 /**
  * 获取券码状态名称
  */
-const getCouponStatusName = (status: string) => {
-  const statusMap: Record<string, string> = {
-    "1": "未核销",
-    "2": "已核销",
-    "3": "已退款"
+const getCouponStatusName = computed(() => {
+  return status => {
+    switch (status) {
+      case 0:
+        return "待支付";
+      case 1:
+        return "未核销";
+      case 2:
+        return "已核销";
+      case 3:
+        return "已过期";
+      case 4:
+        return "已取消";
+      case 5:
+        return "已退款";
+      default:
+        return "--";
+    }
   };
-  return statusMap[status] || "--";
-};
-
+});
+const getCouponType = computed(() => {
+  return status => {
+    switch (status) {
+      case 1:
+        return "优惠券";
+      case 2:
+        return "红包";
+      case 3:
+        return "平台优惠券";
+      default:
+        return "--";
+    }
+  };
+});
 // ==================== 生命周期钩子 ====================
 
 /**
@@ -193,48 +220,39 @@ const goBack = () => {
 const initData = async () => {
   if (id.value) {
     try {
-      // TODO: 根据实际API获取订单详情
-      // const response = await getOrderDetail({ id: id.value });
-      // if (response.code === 200) {
-      //   formData.value = response.data;
-      //   couponList.value = response.data.couponList || [];
-      // }
-
-      // 临时模拟数据
-      formData.value = {
-        productName: "4拼冰淇淋蛋糕套餐",
-        orderNo: "17554469202966300062",
-        orderTime: "2025/01/01 12:00:00",
-        originalPrice: 400,
-        discountedPrice: 380,
-        quantity: 3,
-        couponDiscount: 20,
-        couponType: "商家优惠券/平台优惠券",
-        phone: "18900000000",
-        estimatedIncome: 380
-      };
-
-      couponList.value = [
-        {
-          status: "1",
-          code: "B844556562220"
-        },
-        {
-          status: "2",
-          code: "B844556562220",
-          verifyTime: "2025/01/01 12:00:00"
-        },
-        {
-          status: "3",
-          code: "B844556562220",
-          refundTime: "2025/01/01 12:00:00"
-        }
-      ];
+      const res: any = await queryUserOrderDetail({ orderId: id.value });
+      if (res.code === 200) {
+        formData.value = res.data;
+        couponList.value = groupByA(formData.value.orderCouponMiddleList) || [];
+      }
     } catch (error) {
       ElMessage.error("获取详情失败");
     }
   }
 };
+const groupByA = arr => {
+  // 创建一个映射表存储每个status值对应的条目
+  const map = {};
+
+  // 遍历原始数组
+  arr.forEach(item => {
+    // 从当前项中提取status字段
+    const { status, ...rest } = item;
+    // 如果当前status值在映射表中不存在,则创建一个新条目
+    if (!map[status]) {
+      map[status] = {
+        status: status,
+        list: []
+      };
+    }
+
+    // 将剩余的所有字段添加到对应status的list中
+    map[status].list.push(rest);
+  });
+
+  // 将映射表的值转换为数组并返回
+  return Object.values(map);
+};
 </script>
 
 <style scoped lang="scss">

+ 47 - 38
src/views/orderManagement/index.vue

@@ -38,6 +38,7 @@ 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";
+import { formatCurrency } from "@/utils/formatCurrency";
 
 const router = useRouter();
 const proTable = ref<ProTableInstance>();
@@ -50,21 +51,19 @@ const allTabOptions = [
   { label: "全部", name: "" },
   { label: "待使用", name: "1" },
   { label: "已完成", name: "2" },
-  { label: "已退款", name: "3" }
-];
-
-// 商品类型选项
-const statusEnum = [
-  { label: "团购", value: "1" },
-  { label: "代金券", value: "2" },
-  { label: "优惠券", value: "3" }
+  { label: "已退款", name: "5" }
 ];
 
 // 状态映射
 const statusMap: Record<string, string> = {
-  "1": "待使用",
-  "2": "已完成",
-  "3": "已退款"
+  0: "待支付",
+  1: "待使用",
+  2: "已完成",
+  3: "已过期",
+  4: "已取消",
+  5: "已退款",
+  6: "退款失败",
+  7: "已完成"
 };
 
 // 表格配置项
@@ -81,53 +80,41 @@ const columns = reactive<ColumnProps<any>[]>([
     prop: "orderNo",
     label: "订单编号",
     search: {
-      el: "input",
-      props: { placeholder: "订单编号" }
+      el: "input"
     }
   },
   {
-    prop: "productName",
+    prop: "couponName",
     label: "商品名称",
     search: {
-      el: "input",
-      props: { placeholder: "商品名称" }
+      el: "input"
     }
   },
-  // {
-  //   prop: "productType",
-  //   label: "商品类型",
-  //   render: (scope: any) => {
-  //     const type = statusEnum.find(item => item.value === scope.row.productType);
-  //     return type ? type.label : "--";
-  //   },
-  //   search: {
-  //     el: "select",
-  //     props: { placeholder: "请选择" }
-  //   },
-  //   enum: statusEnum,
-  //   fieldNames: { label: "label", value: "value" }
-  // },
   {
-    prop: "quantity",
-    label: "数量"
+    prop: "couponCount",
+    label: "数量",
+    render: (scope: any) => {
+      return scope.row.couponCount ? `x${scope.row.couponCount}` : "--";
+    }
   },
   {
-    prop: "paidAmount",
+    prop: "price",
     label: "实付款",
     render: (scope: any) => {
-      return scope.row.paidAmount ? `¥${scope.row.paidAmount}` : "--";
+      return formatCurrency(scope.row.price, 2, "¥") || "--";
     }
   },
   {
     prop: "estimatedIncome",
     label: "本单预计收入",
     render: (scope: any) => {
-      return scope.row.estimatedIncome ? `¥${scope.row.estimatedIncome}` : "--";
+      return formatCurrency(scope.row.expectIncome, 2, "¥") || "--";
     }
   },
   {
     prop: "orderTime",
     label: "下单时间",
+    isShow: false,
     search: {
       el: "date-picker",
       props: {
@@ -139,6 +126,13 @@ const columns = reactive<ColumnProps<any>[]>([
     }
   },
   {
+    prop: "buyTime",
+    label: "下单时间",
+    render: (scope: any) => {
+      return scope.row.buyTime?.replace(/-/g, "/") || "--";
+    }
+  },
+  {
     prop: "status",
     label: "状态",
     render: (scope: any) => {
@@ -150,8 +144,8 @@ const columns = reactive<ColumnProps<any>[]>([
 
 // 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
 const initParam = reactive({
-  storeId: localGet("createdId") || "",
-  groupType: localGet("businessSection") || "1",
+  storeId: localGet("createdId"),
+  groupType: localGet("businessSection"),
   status: activeName
 });
 const type = ref(false);
@@ -174,6 +168,15 @@ const dataCallback = (data: any) => {
   };
 };
 
+// 格式化日期为 yyyy/mm/dd
+const formatDate = (date: string | Date) => {
+  const d = new Date(date);
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, "0");
+  const day = String(d.getDate()).padStart(2, "0");
+  return `${year}-${month}-${day}`;
+};
+
 // 获取表格列表
 const getTableList = (params: any) => {
   let newParams = JSON.parse(JSON.stringify(params));
@@ -181,12 +184,18 @@ const getTableList = (params: any) => {
   if (activeName.value) {
     newParams.status = activeName.value;
   }
+  // 处理 orderTime 参数,转换为 startTime 和 endTime
+  if (newParams.orderTime && Array.isArray(newParams.orderTime) && newParams.orderTime.length === 2) {
+    newParams.startTime = formatDate(newParams.orderTime[0]);
+    newParams.endTime = formatDate(newParams.orderTime[1]);
+    delete newParams.orderTime;
+  }
   return getThaliList(newParams);
 };
 
 // 状态标签切换
 const handleClick = () => {
-  proTable.value?.getTableList();
+  // proTable.value?.getTableList();
 };
 
 // 显示菜品弹窗

+ 13 - 32
src/views/ticketManagement/couponDetail.vue

@@ -22,7 +22,7 @@
           <div class="detail-item">
             <div class="detail-label">面值(元)</div>
             <div class="detail-value">
-              {{ couponModel.nominalValue ? `¥${couponModel.nominalValue}` : "--" }}
+              {{ formatCurrency(couponModel.nominalValue, 2, "¥") }}
             </div>
           </div>
           <!-- 开始领取时间 -->
@@ -112,6 +112,7 @@ import { ref, onMounted } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { ElMessage } from "element-plus";
 import { getCouponDetail } from "@/api/modules/couponManagement";
+import { formatCurrency } from "@/utils/formatCurrency";
 
 // ==================== 响应式数据定义 ====================
 
@@ -159,8 +160,7 @@ onMounted(async () => {
   if (id.value) {
     await loadDetailData();
   } else {
-    // 如果没有ID,使用假数据展示
-    loadMockData();
+    ElMessage.warning("缺少优惠券ID参数");
   }
 });
 
@@ -180,45 +180,26 @@ const goBack = () => {
  */
 const loadDetailData = async () => {
   try {
-    const params: any = {
-      id: id.value
-    };
-    const res: any = await getCouponDetail(params);
+    const res: any = await getCouponDetail({ counponId: id.value });
     if (res && res.code == 200) {
       // 合并主数据
       couponModel.value = { ...couponModel.value, ...res.data };
+      // 根据最低消费金额设置是否有低消
+      const amount = Number(couponModel.value.minimumSpendingAmount);
+      if (!isNaN(amount) && amount > 0) {
+        couponModel.value.hasMinimumSpend = 1;
+      } else {
+        couponModel.value.hasMinimumSpend = 0;
+      }
     } else {
       ElMessage.error("加载详情数据失败");
-      // 加载失败时使用假数据
-      loadMockData();
     }
   } catch (error) {
     console.error("加载详情数据出错:", error);
     ElMessage.error("加载详情数据出错");
-    // 加载失败时使用假数据
-    loadMockData();
   }
 };
 
-/**
- * 加载假数据(用于展示)
- */
-const loadMockData = () => {
-  couponModel.value = {
-    name: "5元通用优惠券",
-    nominalValue: "5",
-    beginGetDate: "2025-01-01",
-    endGetDate: "2025-04-20",
-    specifiedDay: "30",
-    singleQty: "2000",
-    claimRule: "day",
-    attentionCanReceived: 0,
-    hasMinimumSpend: 1,
-    minimumSpendingAmount: "100",
-    supplementaryInstruction: "本优惠券适用于全场商品,不可与其他优惠叠加使用。"
-  };
-};
-
 // ==================== 工具函数 ====================
 
 /**
@@ -228,7 +209,7 @@ const loadMockData = () => {
  */
 const formatDate = (date: string) => {
   if (!date) return "--";
-  return date.replace(/-/g, ".");
+  return date.replace(/-/g, "/");
 };
 
 /**
@@ -314,7 +295,7 @@ const getClaimRuleText = () => {
 }
 .detail-label {
   flex-shrink: 0;
-  min-width: 120px;
+  min-width: 200px;
   font-size: 14px;
   font-weight: 500;
   line-height: 32px;

+ 25 - 25
src/views/ticketManagement/detail.vue

@@ -22,14 +22,14 @@
           <div class="detail-item">
             <div class="detail-label">抵扣价格</div>
             <div class="detail-value">
-              {{ voucherModel.offprice ? `¥${voucherModel.offprice}` : "--" }}
+              {{ formatCurrency(voucherModel.price, 2, "¥") || "--" }}
             </div>
           </div>
           <!-- 售卖价格 -->
           <div class="detail-item">
             <div class="detail-label">售卖价格</div>
             <div class="detail-value">
-              {{ voucherModel.price ? `¥${voucherModel.price}` : "--" }}
+              {{ formatCurrency(voucherModel.offprice, 2, "¥") || "--" }}
             </div>
           </div>
           <!-- 开始售卖时间 -->
@@ -75,7 +75,7 @@
           <div class="detail-item">
             <div class="detail-label">库存</div>
             <div class="detail-value">
-              {{ voucherModel.singleQty || "--" }}
+              {{ voucherModel.singleQty + "张" || "--" }}
             </div>
           </div>
         </div>
@@ -126,6 +126,7 @@ import { ref, onMounted } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { ElMessage } from "element-plus";
 import { getVoucherDetail, getHolidayList } from "@/api/modules/voucherManagement";
+import { formatCurrency } from "@/utils/formatCurrency";
 
 // ==================== 响应式数据定义 ====================
 
@@ -141,9 +142,9 @@ const voucherModel = ref<any>({
   // 代金券名称
   name: "",
   // 抵扣价格
-  offprice: "",
-  // 售卖价格
   price: "",
+  // 售卖价格
+  offprice: "",
   // 开始售卖时间
   startDate: "",
   // 结束售卖时间
@@ -152,14 +153,14 @@ const voucherModel = ref<any>({
   buyUseStartTime: "",
   // 使用时间 - 结束时间
   buyUseEndTime: "",
-  // 有效期类型:0-指定天数,1-指定时间段内可用
-  expirationType: "0",
-  // 有效期天数(当expirationType为0时使用)
+  // 有效期类型:1-指定天数,2-指定时间段内可用
+  expirationType: "1",
+  // 有效期天数(当expirationType为1时使用)
   expirationDate: 0,
-  // 有效期时间段(当expirationType为1时使用)
+  // 有效期时间段(当expirationType为2时使用)
   validityPeriod: [],
-  // 不可用日期类型:0-全部日期可用,1-限制日期
-  unusedType: "0",
+  // 不可用日期类型:1-全部日期可用,2-限制日期
+  unusedType: "1",
   // 限制日期 - 星期选择(数组,存储选中的星期值)
   unavailableWeekdays: [],
   // 限制日期 - 节日选择(数组,存储选中的节日值)
@@ -257,7 +258,7 @@ const loadDetailData = async () => {
       voucherModel.value = { ...voucherModel.value, ...res.data };
 
       // 处理有效期时间段:将时间戳字符串转换为日期数组
-      if (voucherModel.value.validityPeriod && voucherModel.value.expirationType == 1) {
+      if (voucherModel.value.validityPeriod && voucherModel.value.expirationType == 2) {
         const periodArray = voucherModel.value.validityPeriod.split(",");
         voucherModel.value.validityPeriod = periodArray
           .map((item: string) => {
@@ -274,12 +275,12 @@ const loadDetailData = async () => {
       }
 
       // 处理不可用日期
-      if (voucherModel.value.unusedType == 1) {
+      if (voucherModel.value.unusedType == 2) {
         // 限制日期类型:格式为 "星期;节日"
         const listVal = voucherModel.value.unusedDate ? voucherModel.value.unusedDate.split(";") : [];
         voucherModel.value.unavailableWeekdays = listVal[0] ? listVal[0].split(",").filter((item: string) => item) : [];
         voucherModel.value.unavailableHolidays = listVal[1] ? listVal[1].split(",").filter((item: string) => item) : [];
-      } else if (voucherModel.value.unusedType === 2) {
+      } else if (voucherModel.value.unusedType === 3) {
         // 自定义不可用日期类型:格式为 "开始日期,结束日期;开始日期,结束日期"
         if (voucherModel.value.unusedDate) {
           const dateRanges = voucherModel.value.unusedDate.split(";");
@@ -314,7 +315,7 @@ const loadDetailData = async () => {
  */
 const formatDate = (date: string) => {
   if (!date) return "--";
-  return date.replace(/-/g, ".");
+  return date.replace(/-/g, "/");
 };
 
 /**
@@ -324,7 +325,7 @@ const getUsageTimeText = () => {
   if (voucherModel.value.buyUseStartTime && voucherModel.value.buyUseEndTime) {
     const startHour = formatHour(voucherModel.value.buyUseStartTime);
     const endHour = formatHour(voucherModel.value.buyUseEndTime);
-    return `${startHour}-${endHour}`;
+    return `${startHour}${endHour}`;
   }
   return "--";
 };
@@ -332,24 +333,24 @@ const getUsageTimeText = () => {
 /**
  * 格式化小时
  * @param hour 小时值(字符串或数字)
- * @returns 格式化后的小时文本(如:7:00
+ * @returns 格式化后的小时文本(如:7
  */
 const formatHour = (hour: string | number) => {
   if (!hour && hour !== 0) return "";
   const hourNum = typeof hour === "string" ? parseInt(hour) : hour;
-  return `${hourNum}:00`;
+  return `${hourNum}`;
 };
 
 /**
  * 获取有效期文本
  */
 const getExpirationText = () => {
-  if (voucherModel.value.expirationType === "0" || voucherModel.value.expirationType == 0) {
+  if (voucherModel.value.expirationType == "1") {
     if (voucherModel.value.expirationDate) {
       return `购买后${voucherModel.value.expirationDate}天`;
     }
     return "--";
-  } else if (voucherModel.value.expirationType === "1" || voucherModel.value.expirationType == 1) {
+  } else if (voucherModel.value.expirationType == "2") {
     if (
       voucherModel.value.validityPeriod &&
       Array.isArray(voucherModel.value.validityPeriod) &&
@@ -357,7 +358,7 @@ const getExpirationText = () => {
     ) {
       const startDate = formatDate(voucherModel.value.validityPeriod[0]);
       const endDate = formatDate(voucherModel.value.validityPeriod[1]);
-      return `${startDate}-${endDate}`;
+      return `${startDate}${endDate}`;
     }
     return "--";
   }
@@ -368,12 +369,11 @@ const getExpirationText = () => {
  * 获取不可用日期文本
  */
 const getUnavailableDateText = () => {
-  if (voucherModel.value.unusedType === "0" || voucherModel.value.unusedType == 0) {
+  if (voucherModel.value.unusedType == "1") {
     return "全部日期可用";
-  } else if (voucherModel.value.unusedType === "1" || voucherModel.value.unusedType == 1) {
+  } else if (voucherModel.value.unusedType === "2") {
     const weekdays: string[] = [];
     const holidays: string[] = [];
-
     // 处理星期
     if (voucherModel.value.unavailableWeekdays && voucherModel.value.unavailableWeekdays.length > 0) {
       voucherModel.value.unavailableWeekdays.forEach((day: string) => {
@@ -403,7 +403,7 @@ const getUnavailableDateText = () => {
     }
 
     return parts.length > 0 ? parts.join("、") : "--";
-  } else if (voucherModel.value.unusedType === "2" || voucherModel.value.unusedType == 2) {
+  } else if (voucherModel.value.unusedType == "3") {
     if (voucherModel.value.disableDateList && voucherModel.value.disableDateList.length > 0) {
       const dateStrings = voucherModel.value.disableDateList
         .filter((date: any) => date && Array.isArray(date) && date.length === 2)

+ 212 - 106
src/views/ticketManagement/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="table-box">
+  <div class="table-box button-table">
     <ProTable
       ref="proTable"
       :columns="columns"
@@ -15,18 +15,20 @@
             <el-tabs v-model="activeName" class="tabs" @tab-click="handleClick">
               <el-tab-pane v-for="tab in allTabOptions" :key="tab.name" :label="tab.label" :name="tab.name" />
             </el-tabs>
-            <div class="action-buttons">
-              <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying" v-if="type"> 新建代金券 </el-button>
-              <el-button :icon="Plus" class="button" type="primary" @click="newCoupon" v-if="typeCoupon"> 新建优惠券 </el-button>
-            </div>
           </div>
         </div>
       </template>
+      <template #tableHeaderRight="scope">
+        <div class="action-buttons">
+          <el-button :icon="Plus" class="button" type="primary" @click="newGroupBuying" v-if="type"> 新建代金券 </el-button>
+          <el-button :icon="Plus" class="button" type="primary" @click="newCoupon" v-if="typeCoupon"> 新建优惠券 </el-button>
+        </div>
+      </template>
       <template #status="scope">
         <!-- 代金券:显示状态和审核状态两行 -->
         <template v-if="activeName === '1'">
-          <p style="margin: 0; line-height: 1.5">状态:{{ getStatusLabel(scope.row.status) || "--" }}</p>
-          <p style="margin: 0; line-height: 1.5">审核状态:{{ getReviewStatusLabel(scope.row.reviewType) || "--" }}</p>
+          <p style="margin: 0; line-height: 1.5">状态:{{ getStatusLabel(scope.row.status, scope.row.dataType) || "--" }}</p>
+          <p style="margin: 0; line-height: 1.5">审核状态:{{ scope.row.statusName || "--" }}</p>
         </template>
         <!-- 优惠券:只显示状态一行 -->
         <template v-else>
@@ -73,7 +75,11 @@
         </el-button>
         <!-- 查看详情按钮 -->
         <el-button
-          v-if="canShowButton(scope.row.status, currentOperationPermissions.查看详情) && (isCoupon || scope.row.dataType != 1)"
+          v-if="
+            isVoucher
+              ? canShowButton(scope.row.status, currentOperationPermissions.查看详情) && scope.row.dataType != 1
+              : canShowButton(scope.row.status, currentOperationPermissions.查看详情)
+          "
           link
           type="primary"
           @click="toDetail(scope.row)"
@@ -82,7 +88,12 @@
         </el-button>
         <!-- 编辑按钮 -->
         <el-button
-          v-if="canShowButton(scope.row.status, currentOperationPermissions.编辑) || (isVoucher && scope.row.dataType == 1)"
+          v-if="
+            isVoucher
+              ? canShowButton(scope.row.status, currentOperationPermissions.编辑) ||
+                (scope.row.dataType == 1 && scope.row.status == 0)
+              : canShowButton(scope.row.status, currentOperationPermissions.编辑)
+          "
           link
           type="primary"
           @click="editRow(scope.row)"
@@ -91,7 +102,12 @@
         </el-button>
         <!-- 删除按钮 -->
         <el-button
-          v-if="canShowButton(scope.row.status, currentOperationPermissions.删除) || (isVoucher && scope.row.dataType == 1)"
+          v-if="
+            isVoucher
+              ? canShowButton(scope.row.status, currentOperationPermissions.删除) ||
+                (scope.row.dataType == 1 && scope.row.status == 0)
+              : canShowButton(scope.row.status, currentOperationPermissions.删除)
+          "
           link
           type="primary"
           @click="deleteRow(scope.row)"
@@ -105,9 +121,7 @@
         <el-form-item label="套餐名">
           {{ formInventory.name }}
         </el-form-item>
-        <el-form-item label="剩余库存">
-          {{ formInventory.singleQty }}
-        </el-form-item>
+        <el-form-item label="剩余库存"> {{ formInventory.singleQty }}张 </el-form-item>
         <el-form-item label="修改库存" prop="newInventory">
           <el-input v-model="formInventory.newInventory" placeholder="请输入" />
         </el-form-item>
@@ -145,24 +159,21 @@
 </template>
 
 <script setup lang="tsx" name="voucherManagement">
-import { computed, onActivated, onMounted, reactive, ref } from "vue";
-import { useRouter } from "vue-router";
+import { computed, onActivated, onMounted, reactive, ref, watch } from "vue";
+import { useRouter, useRoute } from "vue-router";
 import type { FormInstance, FormRules } from "element-plus";
 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 {
-  delThaliById as delVoucherById,
-  getThaliList,
-  updateNum,
-  updateStatus as updateVoucherStatus
-} from "@/api/modules/voucherManagement";
-import { delThaliById as delCouponById, updateStatus as updateCouponStatus } from "@/api/modules/couponManagement";
+import { delThaliById, getThaliList, updateNum, updateStatus } from "@/api/modules/voucherManagement";
+import { delCouponById, updateCouponSingleQty, updateCouponStatus } from "@/api/modules/couponManagement";
 import { ElMessageBox } from "element-plus/es";
 import { localGet, usePermission } from "@/utils";
+import { formatCurrency } from "@/utils/formatCurrency";
 
 const router = useRouter();
+const route = useRoute();
 const dialogFormVisible = ref(false);
 const formInventory: any = ref({
   id: "",
@@ -198,6 +209,12 @@ const rules = reactive<FormRules<RuleForm>>({
             callback(new Error("库存不得大于10000"));
             return;
           }
+          // 验证新库存值不能小于剩余库存
+          const currentQty = Number(formInventory.value.singleQty) || 0;
+          if (numValue < currentQty) {
+            callback(new Error(`库存不能小于剩余库存(${currentQty})`));
+            return;
+          }
         }
         callback();
       },
@@ -206,7 +223,6 @@ const rules = reactive<FormRules<RuleForm>>({
   ]
 });
 const statusEnum = [
-  { label: "全部", value: "" },
   { label: "草稿", value: "0" },
   { label: "进行中", value: "5" },
   { label: "未开始", value: "2" },
@@ -214,6 +230,11 @@ const statusEnum = [
   { label: "已售罄", value: "4" },
   { label: "已结束", value: "7" }
 ];
+const statusEnumY = [
+  { label: "草稿", value: "3" },
+  { label: "进行中", value: "1" },
+  { label: "已结束", value: "2" }
+];
 // ProTable 实例(需要在使用它的地方之前定义)
 const proTable = ref<ProTableInstance>();
 
@@ -230,26 +251,31 @@ const voucherColumns = reactive<ColumnProps<any>[]>([
     prop: "price",
     label: "价格",
     render: (scope: any) => {
-      return scope.row.price ? `¥${scope.row.price}` : "--";
+      return formatCurrency(scope.row.price, 2, "¥") || "--";
     }
   },
   {
     prop: "saleNum",
     label: "已售",
     render: scope => {
-      return scope.row.saleNum === null || scope.row.saleNum === undefined ? 0 : scope.row.saleNum;
+      return scope.row.saleNum === null || scope.row.saleNum === undefined || scope.row.saleNum === "" ? 0 : scope.row.saleNum;
     }
   },
   {
     prop: "singleQty",
     label: "剩余库存",
     render: scope => {
-      return scope.row.singleQty === null || scope.row.singleQty === undefined ? 0 : scope.row.singleQty;
+      return scope.row.singleQty === null || scope.row.singleQty === undefined || scope.row.singleQty === ""
+        ? 0
+        : scope.row.singleQty + "张";
     }
   },
   {
     prop: "endDate",
-    label: "结束时间"
+    label: "结束时间",
+    render: (scope: any) => {
+      return scope.row.endDate?.replace(/-/g, "/") || "--";
+    }
   },
   {
     prop: "status",
@@ -274,33 +300,45 @@ const couponColumns = reactive<ColumnProps<any>[]>([
     }
   },
   {
-    prop: "saleNum",
+    prop: "quantityClaimed",
     label: "已领",
     render: scope => {
-      return scope.row.saleNum === null || scope.row.saleNum === undefined ? 0 : scope.row.saleNum;
+      return scope.row.quantityClaimed === null || scope.row.quantityClaimed === undefined || scope.row.quantityClaimed === ""
+        ? 0
+        : scope.row.quantityClaimed;
     }
   },
   {
     prop: "singleQty",
     label: "剩余库存",
     render: scope => {
-      return scope.row.singleQty === null || scope.row.singleQty === undefined ? 0 : scope.row.singleQty;
+      return scope.row.singleQty === null || scope.row.singleQty === undefined || scope.row.singleQty === ""
+        ? 0
+        : scope.row.singleQty + "张";
     }
   },
   {
-    prop: "endDate",
-    label: "结束时间"
+    prop: "endGetDate",
+    label: "结束时间",
+    render: (scope: any) => {
+      return scope.row.endGetDate?.replace(/-/g, "/") || "--";
+    }
   },
   {
-    prop: "status",
+    prop: "couponStatus",
     label: "状态",
+    isShow: false,
     search: {
       el: "select",
       props: { placeholder: "请选择" }
     },
-    enum: statusEnum,
+    enum: statusEnumY,
     fieldNames: { label: "label", value: "value" }
   },
+  {
+    prop: "status",
+    label: "状态"
+  },
   { prop: "operation", label: "操作", fixed: "right", width: 330 }
 ]);
 
@@ -314,9 +352,9 @@ const allTabOptions = [
   { label: "优惠券", name: "2" }
 ];
 
-// 状态枚举:0草稿 1待审核 2未开始 3审核拒绝 4已售罄 5进行中 6已下架 7已结束
-const STATUS = {
-  草稿: 0,
+// 状态枚举:1草稿 1待审核 2未开始 3审核拒绝 4已售罄 5进行中 6已下架 7已结束
+const VO_STATUS = {
+  草稿: 1,
   待审核: 1,
   未开始: 2,
   审核拒绝: 3,
@@ -325,39 +363,47 @@ const STATUS = {
   已下架: 6,
   已结束: 7
 } as const;
-
+// 状态枚举:1草稿 1待审核 2未开始 3审核拒绝 4已售罄 5进行中 6已下架 7已结束
+const CO_STATUS = {
+  进行中: 0,
+  已结束: 1,
+  未开始: 2,
+  已下架: 3,
+  已售罄: 4,
+  草稿: 5
+} as const;
 // 代金券操作按钮权限配置:定义每个操作按钮在哪些状态下显示
 const VOUCHER_OPERATION_PERMISSIONS = {
   // 查看详情:待审核、未开始、审核拒绝、进行中、已售罄、已下架
-  查看详情: [STATUS.待审核, STATUS.未开始, STATUS.审核拒绝, STATUS.进行中, STATUS.已售罄, STATUS.已下架],
+  查看详情: [VO_STATUS.待审核, VO_STATUS.未开始, VO_STATUS.审核拒绝, VO_STATUS.进行中, VO_STATUS.已售罄, VO_STATUS.已下架],
   // 上架:未开始、已下架
-  上架: [STATUS.未开始, STATUS.已下架],
+  上架: [VO_STATUS.未开始, VO_STATUS.已下架],
   // 下架:进行中
-  下架: [STATUS.进行中],
+  下架: [VO_STATUS.进行中],
   // 修改库存:未开始、进行中、已售罄
-  修改库存: [STATUS.未开始, STATUS.进行中, STATUS.已售罄],
+  修改库存: [VO_STATUS.未开始, VO_STATUS.进行中, VO_STATUS.已售罄],
   // 编辑:草稿、审核拒绝、已售罄、已下架、已结束
-  编辑: [STATUS.草稿, STATUS.审核拒绝, STATUS.已售罄, STATUS.已下架, STATUS.已结束],
+  编辑: [VO_STATUS.草稿, VO_STATUS.审核拒绝, VO_STATUS.已售罄, VO_STATUS.已下架, VO_STATUS.已结束],
   // 删除:草稿、未开始、审核拒绝、已售罄、已结束
-  删除: [STATUS.草稿, STATUS.未开始, STATUS.审核拒绝, STATUS.已售罄, STATUS.已结束],
+  删除: [VO_STATUS.草稿, VO_STATUS.未开始, VO_STATUS.审核拒绝, VO_STATUS.已售罄, VO_STATUS.已结束],
   // 查看拒绝原因:审核拒绝
-  查看拒绝原因: [STATUS.审核拒绝]
+  查看拒绝原因: [VO_STATUS.审核拒绝]
 } as const;
 
 // 优惠券操作按钮权限配置
 const COUPON_OPERATION_PERMISSIONS = {
   // 查看详情:草稿、未开始、进行中、已下架、已结束、已售罄
-  查看详情: [STATUS.草稿, STATUS.未开始, STATUS.进行中, STATUS.已下架, STATUS.已结束, STATUS.已售罄],
+  查看详情: [CO_STATUS.草稿, CO_STATUS.未开始, CO_STATUS.进行中, CO_STATUS.已下架, CO_STATUS.已结束, CO_STATUS.已售罄],
   // 上架:未开始、已下架
-  上架: [STATUS.未开始, STATUS.已下架],
+  上架: [CO_STATUS.未开始, CO_STATUS.已下架],
   // 下架:进行中
-  下架: [STATUS.进行中],
+  下架: [CO_STATUS.进行中],
   // 修改库存:未开始、进行中、已售罄
-  修改库存: [STATUS.未开始, STATUS.进行中, STATUS.已售罄],
+  修改库存: [CO_STATUS.未开始, CO_STATUS.进行中, CO_STATUS.已售罄],
   // 编辑:草稿、未开始、进行中、已下架、已结束、已售罄
-  编辑: [STATUS.草稿, STATUS.未开始, STATUS.进行中, STATUS.已下架, STATUS.已结束, STATUS.已售罄],
+  编辑: [CO_STATUS.草稿, CO_STATUS.未开始, CO_STATUS.进行中, CO_STATUS.已下架, CO_STATUS.已结束, CO_STATUS.已售罄],
   // 删除:草稿、已结束、已售罄
-  删除: [STATUS.草稿, STATUS.已结束, STATUS.已售罄]
+  删除: [CO_STATUS.草稿, CO_STATUS.已结束, CO_STATUS.已售罄]
 } as const;
 
 // 判断按钮是否显示的工具函数
@@ -377,73 +423,118 @@ const isVoucher = computed(() => activeName.value === "1");
 const isCoupon = computed(() => activeName.value === "2");
 
 // 获取状态标签
-const getStatusLabel = (status: number) => {
-  const statusMap: Record<number, string> = {
-    0: "草稿",
-    1: "待审核",
-    2: "未开始",
-    3: "审核拒绝",
-    4: "已售罄",
-    5: "进行中",
-    6: "已下架",
-    7: "已结束"
-  };
-  return statusMap[status] || "--";
-};
-
-// 获取审核状态标签
-const getReviewStatusLabel = (reviewType: string | number) => {
-  if (reviewType === null || reviewType === undefined || reviewType === "") {
-    return "";
+const getStatusLabel = (status: number, dataType?: number) => {
+  // 代金券且状态为1时,根据dataType判断
+  if (isVoucher.value && status === 1) {
+    if (dataType === 0) {
+      return "待审核";
+    } else if (dataType === 1) {
+      return "草稿";
+    }
   }
-  const reviewStatusMap: Record<string, string> = {
-    "0": "待审核",
-    "1": "审核通过",
-    "2": "审核驳回"
-  };
-  return reviewStatusMap[String(reviewType)] || "";
+  // 根据当前选中的tab选择对应的状态枚举
+  const statusEnum = isVoucher.value ? VO_STATUS : CO_STATUS;
+  // 从状态枚举中查找对应的标签
+  for (const [label, value] of Object.entries(statusEnum)) {
+    if (value === status) {
+      return label;
+    }
+  }
+  return "--";
 };
 
-// 数据类型:1-草稿,0-已发布,2-全部
-const dataType = computed(() => {
-  if (!activeName.value) return 2;
-  return activeName.value === "0" ? 1 : 0;
-});
-
 // 如果表格需要初始化请求参数,直接定义传给 ProTable (之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
 const initParam = reactive({
   storeId: localGet("createdId"),
   groupType: localGet("businessSection"),
-  status: activeName,
-  dataType: dataType
+  couponType: activeName
 });
 const type = ref(false);
 const typeCoupon = ref(false);
+// 恢复 activeName 的函数
+const restoreActiveName = () => {
+  const savedActiveName = sessionStorage.getItem("ticketManagement_activeName");
+  if (savedActiveName && (savedActiveName === "1" || savedActiveName === "2")) {
+    activeName.value = savedActiveName;
+    // 恢复后清除 sessionStorage,避免影响其他场景
+    sessionStorage.removeItem("ticketManagement_activeName");
+  }
+};
+
 // 页面加载时触发查询
 onMounted(async () => {
   type.value = await usePermission("新建代金券");
   typeCoupon.value = await usePermission("新建优惠券");
+  // 从 sessionStorage 恢复 activeName
+  restoreActiveName();
   proTable.value?.getTableList();
 });
 
 // 从其他页面返回时触发查询
 onActivated(() => {
+  // 从 sessionStorage 恢复 activeName
+  restoreActiveName();
   proTable.value?.getTableList();
 });
 
+// 监听路由变化,当从详情页或编辑页返回时恢复 activeName
+watch(
+  () => route.path,
+  (newPath, oldPath) => {
+    // 如果当前路径是列表页,且之前路径是详情页或编辑页,则恢复 activeName
+    if (
+      newPath.includes("/ticketManagement") &&
+      !newPath.includes("/detail") &&
+      !newPath.includes("/newVoucher") &&
+      !newPath.includes("/newCoupon") &&
+      (oldPath?.includes("/ticketManagement/detail") ||
+        oldPath?.includes("/ticketManagement/couponDetail") ||
+        oldPath?.includes("/ticketManagement/newVoucher") ||
+        oldPath?.includes("/ticketManagement/newCoupon"))
+    ) {
+      restoreActiveName();
+    }
+  },
+  { immediate: false }
+);
+
 // dataCallback 是对于返回的表格数据做处理,如果你后台返回的数据不是 list && total 这些字段,可以在这里进行处理成这些字段
 // 或者直接去 hooks/useTable.ts 文件中把字段改为你后端对应的就行
 const dataCallback = (data: any) => {
-  return {
-    list: data.records,
-    total: data.total
-  };
+  // 代金券从 couponList.records 取值,优惠券从 discountList.records 取值
+  if (activeName.value === "1") {
+    // 代金券
+    return {
+      list: data.couponList?.records || [],
+      total: data.couponList?.total || 0
+    };
+  } else {
+    // 优惠券
+    return {
+      list: data.discountList?.records || [],
+      total: data.discountList?.total || 0
+    };
+  }
 };
 
 // 如果你想在请求之前对当前请求参数做一些操作,可以自定义如下函数:params 为当前所有的请求参数(包括分页),最后返回请求列表接口
 // 默认不做操作就直接在 ProTable 组件上绑定	:requestApi="getUserList"
 const getTableList = (params: any) => {
   let newParams = JSON.parse(JSON.stringify(params));
+  if (activeName.value === "1") {
+    // 代金券
+    if (newParams.status === "0") {
+      newParams.dataType = 1; // 草稿
+    } else if (newParams.status) {
+      newParams.dataType = 0;
+    } else {
+      newParams.dataType = 2;
+    }
+  } else {
+    // 优惠券
+    delete newParams.dataType;
+    newParams.couponsFromType = 1;
+  }
   return getThaliList(newParams);
 };
 const newGroupBuying = () => {
@@ -454,11 +545,15 @@ const newCoupon = () => {
 };
 // 跳转详情页 - 根据类型跳转不同页面
 const toDetail = (row: any) => {
+  // 保存当前 activeName 到 sessionStorage
+  sessionStorage.setItem("ticketManagement_activeName", activeName.value);
   const path = isVoucher.value ? `/ticketManagement/detail?id=${row.id}` : `/ticketManagement/couponDetail?id=${row.id}`;
   router.push(path);
 };
 // 编辑行数据
 const editRow = (row: any) => {
+  // 保存当前 activeName 到 sessionStorage
+  sessionStorage.setItem("ticketManagement_activeName", activeName.value);
   const path = isVoucher.value
     ? `/ticketManagement/newVoucher?id=${row.id}&type=edit`
     : `/ticketManagement/newCoupon?id=${row.id}&type=edit`;
@@ -478,7 +573,7 @@ const deleteRow = (row: any) => {
           id: row.id,
           groupType: localGet("businessSection")
         };
-        return delVoucherById(params);
+        return delThaliById(params);
       } else {
         // 优惠券删除逻辑
         const params = {
@@ -498,21 +593,22 @@ const deleteRow = (row: any) => {
 };
 // Tab切换处理
 const handleClick = () => {
-  proTable.value?.getTableList();
+  // initParam 中的 couponType 是响应式的,当 activeName 变化时会自动触发 ProTable 内部的 watch 监听,无需手动调用
+  // proTable.value?.getTableList();
 };
 
 // 修改状态(上架/下架)
 const changeTypes = async (row: any, status: number) => {
   if (isVoucher.value) {
     // 代金券上架/下架逻辑
-    const res = await updateVoucherStatus({ id: row.id, status: status, approvalComments: "" });
+    const res = await updateStatus({ id: row.id, status: status, approvalComments: "" });
     if (res && res.code == 200) {
       ElMessage.success("操作成功");
       proTable.value?.getTableList();
     }
   } else {
     // 优惠券上架/下架逻辑
-    const res = await updateCouponStatus({ id: row.id, status: status, approvalComments: "" });
+    const res = await updateCouponStatus({ counponId: row.id });
     if (res && res.code == 200) {
       ElMessage.success("操作成功");
       proTable.value?.getTableList();
@@ -535,14 +631,24 @@ const handleSubmit = async () => {
   if (!ruleFormRef.value) return;
   await ruleFormRef.value.validate(async valid => {
     if (valid) {
-      const res = await updateNum({
+      const params = {
         id: formInventory.value.id,
         singleQty: formInventory.value.newInventory
-      });
-      if (res && res.code == 200) {
-        ElMessage.success("修改成功");
-        closeDialog();
-        proTable.value?.getTableList();
+      };
+      if (isVoucher.value) {
+        const res = await updateNum(params);
+        if (res && res.code == 200) {
+          ElMessage.success("修改成功");
+          closeDialog();
+          proTable.value?.getTableList();
+        }
+      } else {
+        const res = await updateCouponSingleQty(params);
+        if (res && res.code == 200) {
+          ElMessage.success("修改成功");
+          closeDialog();
+          proTable.value?.getTableList();
+        }
       }
     }
   });
@@ -585,7 +691,6 @@ const closeRejectReasonDialog = () => {
   white-space: normal; // 允许自然换行
 }
 .table-header-btn {
-  width: 160vh;
   .header-actions {
     display: flex;
     justify-content: space-between;
@@ -596,14 +701,15 @@ const closeRejectReasonDialog = () => {
         height: 0;
       }
     }
-    .action-buttons {
-      display: flex;
-      flex: 0 0 auto;
-      gap: 10px;
-      .button {
-        margin-bottom: 0;
-      }
-    }
+  }
+}
+.action-buttons {
+  display: flex;
+  flex: 0 0 auto;
+  gap: 10px;
+  margin-right: 20px;
+  .button {
+    margin-bottom: 0;
   }
 }
 .reject-reason-content {

+ 74 - 26
src/views/ticketManagement/newCoupon.vue

@@ -3,7 +3,7 @@
   <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
     <div class="header">
       <el-button @click="goBack"> 返回 </el-button>
-      <h2 class="title">新建优惠券</h2>
+      <h2 class="title">{{ type == "add" ? "新建" : "编辑" }}优惠券</h2>
     </div>
     <el-form :model="couponModel" ref="ruleFormRef" :rules="rules" label-width="200px" class="formBox">
       <div class="content">
@@ -93,7 +93,7 @@
     <!-- 底部按钮区域 -->
     <div class="button-container">
       <el-button @click="() => handleSubmit('draft')"> 存草稿 </el-button>
-      <el-button type="primary" @click="() => handleSubmit()"> 新建优惠券 </el-button>
+      <el-button type="primary" @click="() => handleSubmit()"> {{ type == "add" ? "新建" : "编辑" }}优惠券 </el-button>
     </div>
   </div>
 </template>
@@ -107,9 +107,10 @@ import { ref, reactive, watch, nextTick, onMounted } from "vue";
 import { ElMessage } from "element-plus";
 import { useRoute, useRouter } from "vue-router";
 import type { FormInstance } from "element-plus";
-import { getCouponDetail } from "@/api/modules/couponManagement";
+import { getCouponDetail, addDiscountCoupon, editDiscountCoupon } from "@/api/modules/couponManagement";
 import { validatePositiveNumber, validatePositiveInteger, validateDateRange } from "@/utils/eleValidate";
 import { localGet } from "@/utils";
+import { getVoucherDetail } from "@/api/modules/voucherManagement";
 // ==================== 响应式数据定义 ====================
 
 // 路由相关
@@ -222,7 +223,7 @@ const couponModel = ref<any>({
   // 用户是否需要收藏店铺领取:1-是,0-否
   attentionCanReceived: 1,
   // 是否有低消:1-是,0-否
-  hasMinimumSpend: 1,
+  hasMinimumSpend: 0,
   // 最低消费金额
   minimumSpendingAmount: "",
   // 补充说明
@@ -231,6 +232,9 @@ const couponModel = ref<any>({
 
 // ==================== 监听器 ====================
 
+// 初始化标志,用于防止初始化时触发验证
+const isInitializing = ref(true);
+
 /**
  * 监听开始领取时间变化
  * 当开始时间改变时,重新验证结束时间
@@ -238,6 +242,7 @@ const couponModel = ref<any>({
 watch(
   () => couponModel.value.beginGetDate,
   () => {
+    if (isInitializing.value) return;
     if (couponModel.value.endGetDate) {
       nextTick(() => {
         ruleFormRef.value?.validateField("endGetDate");
@@ -253,6 +258,7 @@ watch(
 watch(
   () => couponModel.value.endGetDate,
   () => {
+    if (isInitializing.value) return;
     if (couponModel.value.beginGetDate) {
       nextTick(() => {
         ruleFormRef.value?.validateField("beginGetDate");
@@ -268,6 +274,7 @@ watch(
 watch(
   () => couponModel.value.hasMinimumSpend,
   newVal => {
+    if (isInitializing.value) return;
     if (newVal === 0) {
       couponModel.value.minimumSpendingAmount = "";
       nextTick(() => {
@@ -277,6 +284,23 @@ watch(
   }
 );
 
+/**
+ * 监听最低消费金额变化
+ * 当最低消费金额大于0时,自动设置为"是",否则设置为"否"
+ */
+watch(
+  () => couponModel.value.minimumSpendingAmount,
+  newVal => {
+    if (isInitializing.value) return;
+    const amount = Number(newVal);
+    if (!isNaN(amount) && amount > 0) {
+      couponModel.value.hasMinimumSpend = 1;
+    } else {
+      couponModel.value.hasMinimumSpend = 0;
+    }
+  }
+);
+
 // ==================== 事件处理函数 ====================
 /**
  * 组件挂载时初始化
@@ -286,9 +310,20 @@ onMounted(async () => {
   id.value = (route.query.id as string) || "";
   type.value = (route.query.type as string) || "";
   if (type.value != "add") {
-    // TODO: 加载优惠券详情数据
-    // let res: any = await getCouponDetail({ id: id.value });
+    let res: any = await getCouponDetail({ counponId: id.value });
+    couponModel.value = { ...couponModel.value, ...res.data };
+    // 根据最低消费金额设置是否有低消
+    const amount = Number(couponModel.value.minimumSpendingAmount);
+    if (!isNaN(amount) && amount > 0) {
+      couponModel.value.hasMinimumSpend = 1;
+    } else {
+      couponModel.value.hasMinimumSpend = 0;
+    }
   }
+
+  await nextTick();
+  ruleFormRef.value?.clearValidate();
+  isInitializing.value = false;
 });
 /**
  * 返回上一页
@@ -304,31 +339,44 @@ const ruleFormRef = ref<FormInstance>(); // 表单引用
  * 提交数据(新增)
  * 验证表单,通过后调用相应的API接口
  */
-const handleSubmit = (submitType?: string) => {
+const handleSubmit = async (submitType?: string) => {
+  // 组装提交参数
+  let params: any = { ...couponModel.value };
+  params.storeId = localGet("createdId");
+  params.couponId = "";
+  params.couponStatus = submitType ? 0 : 1;
+  params.restrictedQuantity = 0;
+  params.expirationDate = 0;
+  params.getStatus = 1;
+  params.type = 1;
+  if (submitType) {
+    if (!couponModel.value.name) {
+      ElMessage.warning("请填写优惠券名称");
+      return;
+    }
+    let res: any = await addDiscountCoupon(params);
+    if (res && res.code == 200) {
+      ElMessage.success("保存成功");
+      goBack();
+    }
+    return;
+  }
   // 验证表单
   ruleFormRef.value!.validate(async (valid: boolean) => {
     if (!valid) return;
-
-    // 组装提交参数
-    let params: any = { ...couponModel.value };
-    params.storeId = localGet("createdId");
-    params.status = 3;
-    params.couponId = "couponId";
-    console.log("提交参数:", params);
-
-    // TODO: 调用API保存数据
-    // if (submitType === 'draft') {
-    //   await saveCouponDraft(params);
-    // } else {
-    //   await saveCoupon(params);
-    // }
-
-    if (submitType === "draft") {
-      ElMessage.success("草稿保存成功");
+    if (type.value == "add") {
+      let res: any = await addDiscountCoupon(params);
+      if (res && res.code == 200) {
+        ElMessage.success("创建成功");
+      }
     } else {
-      ElMessage.success("优惠券创建成功");
-      router.go(-1);
+      params.couponId = id.value;
+      let res: any = await editDiscountCoupon(params);
+      if (res && res.code == 200) {
+        ElMessage.success("修改成功");
+      }
     }
+    goBack();
   });
 };
 

+ 60 - 48
src/views/ticketManagement/newVoucher.vue

@@ -3,7 +3,7 @@
   <div class="table-box" style="width: 100%; min-height: 100%; background-color: white">
     <div class="header">
       <el-button @click="goBack"> 返回 </el-button>
-      <h2 class="title">新建代金券</h2>
+      <h2 class="title">{{ type == "add" ? "新建" : "编辑" }}代金券</h2>
     </div>
     <el-form :model="voucherModel" ref="ruleFormRef" :rules="rules" label-width="120px" class="formBox">
       <div class="content">
@@ -17,12 +17,12 @@
               <el-input maxlength="20" v-model="voucherModel.name" placeholder="请输入" clearable />
             </el-form-item>
             <!-- 抵扣价格 -->
-            <el-form-item label="抵扣价格(¥)" prop="offprice">
-              <el-input v-model="voucherModel.offprice" maxlength="15" placeholder="请输入" clearable />
+            <el-form-item label="抵扣价格(¥)" prop="price">
+              <el-input v-model="voucherModel.price" maxlength="15" placeholder="请输入" clearable />
             </el-form-item>
             <!-- 售卖价格 -->
-            <el-form-item label="售卖价格(¥)" prop="price">
-              <el-input v-model="voucherModel.price" maxlength="15" placeholder="请输入" clearable />
+            <el-form-item label="售卖价格(¥)" prop="offprice">
+              <el-input v-model="voucherModel.offprice" maxlength="15" placeholder="请输入" clearable />
             </el-form-item>
             <!-- 开始售卖时间 -->
             <el-form-item label="开始售卖时间" prop="startDate">
@@ -68,7 +68,7 @@
                 </el-radio>
               </el-radio-group>
             </el-form-item>
-            <el-form-item label="" prop="expirationDate" v-if="voucherModel.expirationType == 0">
+            <el-form-item label="" prop="expirationDate" v-if="voucherModel.expirationType == 1">
               <div class="expiration-date-container">
                 <span class="expiration-label">用户购买</span>
                 <el-input-number
@@ -100,7 +100,7 @@
                 </el-radio>
               </el-radio-group>
             </el-form-item>
-            <template v-if="voucherModel.unusedType == 1">
+            <template v-if="voucherModel.unusedType == 2">
               <el-form-item label="" prop="unavailableWeekdays">
                 <div class="unavailable-dates-container">
                   <!-- 星期选择 -->
@@ -140,7 +140,7 @@
                 </div>
               </el-form-item>
             </template>
-            <el-form-item label="" prop="customUnavailableDates" v-else-if="voucherModel.unusedType == 2">
+            <el-form-item label="" prop="customUnavailableDates" v-else-if="voucherModel.unusedType == 3">
               <div class="date-picker-container">
                 <el-button :icon="Plus" class="add-date-btn" type="primary" @click="addDate"> 添加日期 </el-button>
                 <div v-for="(item, index) in voucherModel.disableDateList" :key="index" class="date-item">
@@ -227,7 +227,7 @@
     <!-- 底部按钮区域 -->
     <div class="button-container">
       <el-button @click="handleSubmit('draft')"> 存草稿 </el-button>
-      <el-button type="primary" @click="handleSubmit()"> 新建代金券 </el-button>
+      <el-button type="primary" @click="handleSubmit()"> {{ type == "add" ? "新建" : "编辑" }}代金券 </el-button>
     </div>
   </div>
 </template>
@@ -269,7 +269,7 @@ const id = ref<string>(""); // 页面ID参数
 // ==================== 表单验证规则 ====================
 const rules = reactive({
   name: [{ required: true, message: "请输入代金券名称" }],
-  offprice: [
+  price: [
     { required: true, message: "请输入抵扣价格" },
     {
       validator: validatePositiveNumber("抵扣价格必须为正数"),
@@ -281,14 +281,14 @@ const rules = reactive({
     },
     {
       validator: validatePriceComparison(
-        () => voucherModel.value.offprice,
         () => voucherModel.value.price,
+        () => voucherModel.value.offprice,
         "抵扣价格不能低于售卖价格"
       ),
       trigger: "blur"
     }
   ],
-  price: [
+  offprice: [
     { required: true, message: "请输入售卖价格" },
     {
       validator: validatePositiveNumber("售卖价格必须为正数"),
@@ -300,8 +300,8 @@ const rules = reactive({
     },
     {
       validator: validatePriceComparison(
-        () => voucherModel.value.offprice,
         () => voucherModel.value.price,
+        () => voucherModel.value.offprice,
         "抵扣价格不能低于售卖价格"
       ),
       trigger: "blur"
@@ -385,7 +385,7 @@ const rules = reactive({
     {
       required: true,
       validator: (rule: any, value: any, callback: any) => {
-        if (voucherModel.value.expirationType == 0) {
+        if (voucherModel.value.expirationType == 1) {
           if (value === null || value === undefined || value === "") {
             callback(new Error("请输入用户购买天数"));
             return;
@@ -419,7 +419,7 @@ const rules = reactive({
     {
       required: true,
       validator: (rule: any, value: any, callback: any) => {
-        if (voucherModel.value.expirationType == 1) {
+        if (voucherModel.value.expirationType == 2) {
           if (!value || value.length !== 2) {
             callback(new Error("请选择指定时间段"));
             return;
@@ -435,13 +435,13 @@ const rules = reactive({
   unusedType: [{ required: true, message: "请选择不可用日期类型" }],
   unavailableWeekdays: [
     {
-      validator: validateConditionalRequired(() => voucherModel.value.unusedType == 1, "至少需要选择一个星期"),
+      validator: validateConditionalRequired(() => voucherModel.value.unusedType == 2, "至少需要选择一个星期"),
       trigger: "change"
     }
   ],
   unavailableHolidays: [
     {
-      validator: validateConditionalRequired(() => voucherModel.value.unusedType == 1, "至少需要选择一个节日"),
+      validator: validateConditionalRequired(() => voucherModel.value.unusedType == 2, "至少需要选择一个节日"),
       trigger: "change"
     }
   ],
@@ -449,7 +449,7 @@ const rules = reactive({
     {
       required: true,
       validator: (rule: any, value: any, callback: any) => {
-        if (voucherModel.value.unusedType == 2) {
+        if (voucherModel.value.unusedType == 3) {
           if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
             callback(new Error("至少需要添加一个自定义不可用日期"));
             return;
@@ -587,9 +587,9 @@ const voucherModel = ref<any>({
   // 代金券名称
   name: "",
   // 抵扣价格
-  offprice: "",
-  // 售卖价格
   price: "",
+  // 售卖价格
+  offprice: "",
   // 开始售卖时间
   startDate: "",
   // 结束售卖时间
@@ -600,14 +600,14 @@ const voucherModel = ref<any>({
   buyUseEndTime: "",
   // 使用时间(虚拟字段,用于表单验证)
   usageTime: null,
-  // 有效期类型:0-指定天数,1-指定时间段内可用
-  expirationType: "0",
-  // 有效期天数(当expirationType为0时使用)
+  // 有效期类型:1-指定天数,2-指定时间段内可用
+  expirationType: "1",
+  // 有效期天数(当expirationType为1时使用)
   expirationDate: 0,
-  // 有效期时间段(当expirationType为1时使用)
+  // 有效期时间段(当expirationType为2时使用)
   validityPeriod: [],
-  // 不可用日期类型:0-全部日期可用,1-限制日期
-  unusedType: "0",
+  // 不可用日期类型:1-全部日期可用,2-限制日期
+  unusedType: "1",
   // 限制日期 - 星期选择(数组,存储选中的星期值)
   unavailableWeekdays: [],
   // 限制日期 - 节日选择(数组,存储选中的节日值)
@@ -630,9 +630,9 @@ const voucherModel = ref<any>({
 
 // ==================== 下拉选项数据 ====================
 
-// 小时选项列表(0-23点)
+// 小时选项列表(0-24点)
 const hourOptions = ref(
-  Array.from({ length: 24 }, (_, i) => ({
+  Array.from({ length: 25 }, (_, i) => ({
     value: String(i),
     label: `${i}点`
   }))
@@ -640,15 +640,15 @@ const hourOptions = ref(
 
 // 有效期类型列表
 const validityPeriodList = ref([
-  { value: "0", label: "指定天数" },
-  { value: "1", label: "指定时间段内可用" }
+  { value: "1", label: "指定天数" },
+  { value: "2", label: "指定时间段内可用" }
 ]);
 
 // 不可用日期类型列表
 const unavailableDateTypeList = ref([
-  { value: "0", label: "全部日期可用" },
-  { value: "1", label: "限制日期" }
-  // { value: '2', label: "自定义不可用日期" }
+  { value: "1", label: "全部日期可用" },
+  { value: "2", label: "限制日期" }
+  // { value: "3", label: "自定义不可用日期" }
 ]);
 
 // 适用范围类型列表
@@ -673,6 +673,9 @@ const holidayList: any = ref([]);
 
 // ==================== 监听器 ====================
 
+// 初始化标志,用于防止初始化时触发验证
+const isInitializing = ref(true);
+
 /**
  * 监听开始售卖时间变化
  * 当开始时间改变时,重新验证结束时间
@@ -680,6 +683,7 @@ const holidayList: any = ref([]);
 watch(
   () => voucherModel.value.startDate,
   () => {
+    if (isInitializing.value) return;
     if (voucherModel.value.endDate) {
       nextTick(() => {
         ruleFormRef.value?.validateField("endDate");
@@ -695,6 +699,7 @@ watch(
 watch(
   () => voucherModel.value.endDate,
   () => {
+    if (isInitializing.value) return;
     if (voucherModel.value.startDate) {
       nextTick(() => {
         ruleFormRef.value?.validateField("startDate");
@@ -708,11 +713,12 @@ watch(
  * 当抵扣价格改变时,重新验证售卖价格
  */
 watch(
-  () => voucherModel.value.offprice,
+  () => voucherModel.value.price,
   () => {
-    if (voucherModel.value.price) {
+    if (isInitializing.value) return;
+    if (voucherModel.value.offprice) {
       nextTick(() => {
-        ruleFormRef.value?.validateField("price");
+        ruleFormRef.value?.validateField("offprice");
       });
     }
   }
@@ -723,11 +729,12 @@ watch(
  * 当售卖价格改变时,重新验证抵扣价格
  */
 watch(
-  () => voucherModel.value.price,
+  () => voucherModel.value.offprice,
   () => {
-    if (voucherModel.value.offprice) {
+    if (isInitializing.value) return;
+    if (voucherModel.value.price) {
       nextTick(() => {
-        ruleFormRef.value?.validateField("offprice");
+        ruleFormRef.value?.validateField("price");
       });
     }
   }
@@ -740,6 +747,7 @@ watch(
 watch(
   () => voucherModel.value.singleQty,
   () => {
+    if (isInitializing.value) return;
     if (voucherModel.value.singleCanUse) {
       nextTick(() => {
         ruleFormRef.value?.validateField("singleCanUse");
@@ -790,7 +798,7 @@ watch(
 watch(
   () => voucherModel.value.unusedType,
   newVal => {
-    if (newVal == 2) {
+    if (newVal == 3) {
       // 切换到自定义不可用日期时,如果disableDateList为空,则添加一个默认项
       if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
         voucherModel.value.disableDateList = [null];
@@ -828,7 +836,7 @@ onMounted(async () => {
     let res: any = await getVoucherDetail({ id: id.value });
     voucherModel.value = { ...voucherModel.value, ...res.data };
     // 处理有效期时间段:将时间戳字符串转换为数字数组
-    if (voucherModel.value.validityPeriod && voucherModel.value.expirationType == 1) {
+    if (voucherModel.value.validityPeriod && voucherModel.value.expirationType == 2) {
       const periodArray = voucherModel.value.validityPeriod.split(",");
       voucherModel.value.validityPeriod = periodArray
         .map((item: string) => Number(item.trim()))
@@ -837,19 +845,23 @@ onMounted(async () => {
       voucherModel.value.validityPeriod = [];
     }
     // 确保星期和节日字段存在;
-    if (voucherModel.value.unusedType == 1) {
+    if (voucherModel.value.unusedType == 2) {
       const listVal = voucherModel.value.unusedDate ? voucherModel.value.unusedDate.split(";") : [];
       voucherModel.value.unavailableWeekdays = listVal[0] ? listVal[0].split(",").filter((item: string) => item) : [];
       voucherModel.value.unavailableHolidays = listVal[1] ? listVal[1].split(",").filter((item: string) => item) : [];
     }
     // 确保自定义不可用日期字段存在;
-    if (voucherModel.value.unusedType === 2) {
+    if (voucherModel.value.unusedType === 3) {
       if (!voucherModel.value.disableDateList || voucherModel.value.disableDateList.length === 0) {
         voucherModel.value.disableDateList = [null];
       }
     }
     console.log(voucherModel.value);
   }
+
+  await nextTick();
+  ruleFormRef.value?.clearValidate();
+  isInitializing.value = false;
 });
 
 // ==================== 事件处理函数 ====================
@@ -941,17 +953,17 @@ const handleSubmit = async (submitType?: string) => {
   let params: any = { ...voucherModel.value };
   params.storeId = localGet("createdId");
   params.status = 1;
-  // 处理有效期:只有当expirationType为1(指定时间段内可用)时才处理validityPeriod
-  if (params.expirationType == 1 && params.validityPeriod && Array.isArray(params.validityPeriod)) {
+  // 处理有效期:只有当expirationType为2(指定时间段内可用)时才处理validityPeriod
+  if (params.expirationType == 2 && params.validityPeriod && Array.isArray(params.validityPeriod)) {
     params.validityPeriod = params.validityPeriod.join(",");
-  } else if (params.expirationType == 0) {
+  } else if (params.expirationType == 1) {
     // 指定天数模式,不需要validityPeriod字段
     params.validityPeriod = "";
   }
   // 处理不可用日期
-  if (params.unusedType == 1) {
+  if (params.unusedType == 2) {
     params.unusedDate = params.unavailableWeekdays.join(",") + ";" + params.unavailableHolidays.join(",");
-  } else if (params.unusedType == 2) {
+  } else if (params.unusedType == 3) {
     // 处理自定义不可用日期
     if (params.disableDateList && params.disableDateList.length > 0) {
       params.unusedDate = params.disableDateList