Browse Source

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

刘云鑫 2 months ago
parent
commit
1cf9f6330d

+ 64 - 0
src/api/modules/businessData.ts

@@ -147,6 +147,8 @@ export interface GetHistoryDetailData {
   summary?: string;
   optimizationSuggestions?: string;
   aiAnalysisCompleted?: number;
+  /** 对比数据 JSON 字符串,解析后结构同 StoreOperationalStatisticsComparisonVo */
+  statisticsData?: string;
 }
 
 /**
@@ -157,3 +159,65 @@ export const getHistoryDetail = (params: GetHistoryDetailParams) => {
     loading: false
   });
 };
+
+/** 历史分析报告列表 - 请求参数 */
+export interface GetHistoryReportListParams {
+  storeId: string | number;
+  page: number;
+  size: number;
+  created_time?: string; // 生成时间筛选 YYYY-MM-DD
+}
+
+/** 接口返回的历史记录项(含 queryTime) */
+export interface HistoryReportRecord {
+  id?: number | string;
+  queryTime?: string; // 完整年月日时分秒,如 "2026-02-05 13:48:35"
+  createdTime?: string;
+  [key: string]: any;
+}
+
+/** 历史分析报告列表 - 接口响应 */
+export interface GetHistoryReportListData {
+  records?: HistoryReportRecord[];
+  list?: HistoryReportRecord[];
+  total?: number;
+}
+
+/**
+ * 获取历史分析报告列表 /store/operational/statistics/history/list
+ */
+export const getHistoryReportList = (params: GetHistoryReportListParams) => {
+  return httpApi.get<GetHistoryReportListData>(`/alienStore/store/operational/statistics/history/list`, params, {
+    loading: false
+  });
+};
+
+/** 批量删除历史分析报告 - 请求参数 */
+export interface BatchDeleteHistoryParams {
+  ids: string; // 列表中 id 用英文逗号分隔,如 "235,234,233"
+}
+
+/**
+ * 批量删除历史分析报告 /store/operational/statistics/history/batchDelete(DELETE)
+ */
+export const batchDeleteHistoryReport = (params: BatchDeleteHistoryParams) => {
+  return httpApi.delete<unknown>(`/alienStore/store/operational/statistics/history/batchDelete`, params, {
+    loading: true
+  });
+};
+
+/** 根据历史记录生成对比报告 PDF - 请求参数 */
+export interface GenerateStatisticsComparisonPdfByHistoryIdParams {
+  historyId: number | string;
+  storeId: string | number;
+}
+
+/**
+ * 根据历史记录 id 生成对比报告 PDF /store/operational/statistics/generateStatisticsComparisonPdfByHistoryId
+ * 返回 data 为 PDF 文件地址(字符串)
+ */
+export const generateStatisticsComparisonPdfByHistoryId = (params: GenerateStatisticsComparisonPdfByHistoryIdParams) => {
+  return httpApi.get<string>(`/alienStore/store/operational/statistics/generateStatisticsComparisonPdfByHistoryId`, params, {
+    loading: true
+  });
+};

+ 285 - 0
src/api/modules/priceList.ts

@@ -0,0 +1,285 @@
+import axios from "axios";
+import { PORT_NONE } from "@/api/config/servicePort";
+import { localGet } from "@/utils";
+import { ResultEnum } from "@/enums/httpEnum";
+import { useUserStore } from "@/stores/modules/user";
+import { ElMessage } from "element-plus";
+import { LOGIN_URL } from "@/config";
+import router from "@/routers";
+
+// 创建专门用于价目表(STORE)接口的 axios 实例,前缀使用 alienStore
+const priceListAxios = axios.create({
+  baseURL: import.meta.env.VITE_API_URL_STORE as string, // /api/alienStore
+  timeout: ResultEnum.TIMEOUT as number,
+  withCredentials: true
+});
+
+// 请求拦截:补充 token
+priceListAxios.interceptors.request.use(
+  config => {
+    const userStore = useUserStore();
+    if (config.headers) {
+      (config.headers as any).Authorization = userStore.token;
+    }
+    return config;
+  },
+  error => Promise.reject(error)
+);
+
+// 响应拦截:与平台 http 保持一致的基础处理
+priceListAxios.interceptors.response.use(
+  response => {
+    const data = response.data;
+    const userStore = useUserStore();
+
+    // 登录失效
+    if (data.code == ResultEnum.OVERDUE) {
+      userStore.setToken("");
+      router.replace(LOGIN_URL);
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+
+    // 全局错误信息拦截
+    if (data.code && data.code !== ResultEnum.SUCCESS) {
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+
+    return data;
+  },
+  error => {
+    ElMessage.error("请求失败,请稍后重试");
+    return Promise.reject(error);
+  }
+);
+
+// 后端状态:0=审核中, 1=审核通过, 2=审核拒绝(根据商家端价目表说明推断)
+export const AUDIT_STATUS_LABEL: Record<number, string> = {
+  0: "审核中",
+  1: "审核通过",
+  2: "审核拒绝"
+};
+
+// 经营板块对应的模块类型(与商家端保持一致)
+export const MODULE_TYPES = {
+  FOOD: "food",
+  GENERAL_PACKAGE: "generalPackage"
+} as const;
+
+export type ModuleType = (typeof MODULE_TYPES)[keyof typeof MODULE_TYPES];
+
+export interface RawPriceItem {
+  id: number | string;
+  // 通用字段
+  name?: string;
+  prodName?: string;
+  totalPrice?: number;
+  images?: string;
+  status?: number;
+  shelfStatus?: number;
+  rejectionReason?: string;
+  // 美食特有字段
+  cuisineType?: number;
+  // 兼容旧版 storeProductItem 结构
+  subList?: Array<{
+    price?: number;
+    unit?: string;
+    quantity?: number;
+  }>;
+  prodType?: number;
+}
+
+export interface PriceListRow {
+  id: number | string;
+  // 名称
+  dishName: string;
+  // 展示价格
+  dishPrice: string;
+  // 单位
+  dishUnit: string;
+  // 列表首图
+  imgUrl: string;
+  // 审核状态
+  auditStatus: number | null;
+  // 上下架状态:1=上架,2=下架
+  shelfStatus: number;
+  // 审核拒绝原因
+  auditReason: string;
+  // 业务类型
+  prodType?: number;
+  cuisineType?: number;
+}
+
+export interface PriceListQuery {
+  pageNum?: number;
+  pageSize?: number;
+  storeId?: number | string;
+  prodName?: string;
+  status?: number | string;
+  shelfStatus?: number | string;
+  businessSection?: string | number;
+}
+
+// 根据经营板块获取模块类型(与商家端 config.js 保持一致)
+export const getModuleTypeByBusinessSection = (businessSection?: string | number): ModuleType => {
+  const section = businessSection !== undefined && businessSection !== null ? String(businessSection) : "";
+  return section === "1" ? MODULE_TYPES.FOOD : MODULE_TYPES.GENERAL_PACKAGE;
+};
+
+// 获取当前模块类型(通过本地缓存的 businessSection)
+export const getCurrentModuleType = (): ModuleType => {
+  const businessSection = localGet("businessSection");
+  return getModuleTypeByBusinessSection(businessSection);
+};
+
+// 获取价目表分页列表
+// 不同经营板块调不同接口:
+// - 美食板块:/store/cuisine/getPage
+// - 通用板块:/store/price/getPage
+export const getPriceListPage = (params: PriceListQuery) => {
+  const moduleType = getModuleTypeByBusinessSection(params.businessSection ?? localGet("businessSection") ?? "1");
+  const storeId = params.storeId ?? (localGet("createdId") as string) ?? "";
+  const pageNum = params.pageNum ?? 1;
+  const pageSize = params.pageSize ?? 10;
+
+  if (moduleType === MODULE_TYPES.FOOD) {
+    const query: any = {
+      pageNum,
+      pageSize,
+      storeId,
+      type: 1, // 1-美食
+      origin: 1
+    };
+
+    if (params.prodName) query.name = params.prodName;
+    if (params.status !== undefined && params.status !== null && params.status !== "") {
+      query.status = params.status;
+    }
+
+    return priceListAxios.get<any>(PORT_NONE + "/store/cuisine/getPage", { params: query });
+  }
+
+  const query: any = {
+    pageNum,
+    pageSize,
+    storeId,
+    origin: 1
+  };
+
+  if (params.prodName) query.name = params.prodName;
+  if (params.status !== undefined && params.status !== null && params.status !== "") {
+    query.status = params.status;
+  }
+  if (params.shelfStatus !== undefined && params.shelfStatus !== null && params.shelfStatus !== "") {
+    query.shelfStatus = params.shelfStatus;
+  }
+
+  return priceListAxios.get<any>(PORT_NONE + "/store/price/getPage", { params: query });
+};
+
+// 删除价目表条目(按经营板块走不同接口)
+export const deletePriceItem = (id: number | string): Promise<any> => {
+  const moduleType = getCurrentModuleType();
+  if (moduleType === MODULE_TYPES.FOOD) {
+    return priceListAxios.post(PORT_NONE + `/store/cuisine/delete?id=${id}`);
+  }
+  return priceListAxios.post(PORT_NONE + `/store/price/delete?id=${id}`);
+};
+
+// 上架/下架价目表条目(按经营板块走不同接口)
+// @param id - 价目表ID
+// @param shelfStatus - 上下架状态:1-上架,2-下架
+export const updateShelfStatus = (id: number | string, shelfStatus: number): Promise<any> => {
+  const moduleType = getCurrentModuleType();
+  if (moduleType === MODULE_TYPES.FOOD) {
+    return priceListAxios.post(PORT_NONE + `/store/cuisine/changeShelfStatus?id=${id}&shelfStatus=${shelfStatus}`);
+  }
+  return priceListAxios.post(PORT_NONE + `/store/price/changeShelfStatus?id=${id}&shelfStatus=${shelfStatus}`);
+};
+
+// 美食类型:1-单品(菜品),2-套餐
+export const CUISINE_TYPE = { SINGLE: 1, COMBO: 2 } as const;
+
+// 获取价目表详情(美食板块用 cuisineType,通用板块不传)
+export const getPriceListDetail = (id: string, businessSection?: string, cuisineType?: number): Promise<any> => {
+  const moduleType = getModuleTypeByBusinessSection(businessSection ?? localGet("businessSection") ?? "1");
+  const storeId = localGet("createdId") as string;
+  if (moduleType === MODULE_TYPES.FOOD) {
+    const type = cuisineType !== undefined && cuisineType !== null ? cuisineType : 1;
+    return priceListAxios.get<any>(PORT_NONE + "/store/cuisine/getByCuisineType", {
+      params: { id, cuisineType: type }
+    });
+  }
+  return priceListAxios.get<any>(PORT_NONE + "/store/price/getById", { params: { id } });
+};
+
+// 新增价目表(与商家端 addPriceListItem 一致:美食 addCuisineCombo,通用 save)
+export const addPriceItem = (params: any): Promise<any> => {
+  const moduleType = getCurrentModuleType();
+  if (moduleType === MODULE_TYPES.FOOD) {
+    return priceListAxios.post(PORT_NONE + "/store/cuisine/addCuisineCombo", params);
+  }
+  return priceListAxios.post(PORT_NONE + "/store/price/save", params);
+};
+
+// 更新价目表(与商家端 addPriceListItem 一致:美食 updateCuisineCombo,通用 update)
+export const updatePriceItem = (params: any): Promise<any> => {
+  const moduleType = getCurrentModuleType();
+  if (moduleType === MODULE_TYPES.FOOD) {
+    return priceListAxios.post(PORT_NONE + "/store/cuisine/updateCuisineCombo", params);
+  }
+  return priceListAxios.post(PORT_NONE + "/store/price/update", params);
+};
+
+// 获取美食单品名称列表(套餐编辑时选择菜品用)
+export const getCuisineSingleNameList = (): Promise<any> => {
+  const storeId = localGet("createdId") as string;
+  return priceListAxios.get<any>(PORT_NONE + "/store/cuisine/getSingleName", {
+    params: { storeId }
+  });
+};
+
+// 推荐价格(根据原料成本,可选使用)
+export const getRecommendedPrice = (dishName: string, location?: string): Promise<any> => {
+  return priceListAxios.post<any>(PORT_NONE + "/store/cuisine/getPrice", null, {
+    params: { dish_name: dishName, location: location || "" }
+  });
+};
+
+// 根据接口返回结构转换为表格展示行
+// 兼容:
+// - /store/cuisine/getPage 返回的美食价目
+// - /store/price/getPage 返回的通用价目
+export const transformListItem = (item: RawPriceItem): PriceListRow => {
+  const imgUrl = item.images ? item.images.split(",")[0] : "";
+
+  // 名称字段兼容 prodName/name
+  const name = item.prodName || item.name || "";
+
+  // 价格:
+  // - 优先使用 totalPrice
+  // - 兼容旧版 storeProductItem.subList[0].price
+  let displayPrice: number | string | undefined = item.totalPrice;
+  if (
+    (displayPrice === undefined || displayPrice === null || (typeof displayPrice === "string" && displayPrice === "")) &&
+    Array.isArray(item.subList)
+  ) {
+    displayPrice = item.subList[0]?.price;
+  }
+
+  return {
+    id: item.id,
+    dishName: name,
+    dishPrice: displayPrice !== undefined && displayPrice !== null && String(displayPrice) !== "" ? String(displayPrice) : "",
+    dishUnit: "份",
+    imgUrl: imgUrl || "",
+    // 审核状态:0=审核中, 1=审核通过, 2=审核拒绝
+    auditStatus: typeof item.status === "number" ? item.status : null,
+    // 上下架状态:1-上架,2-下架;默认 1
+    shelfStatus: typeof item.shelfStatus === "number" ? item.shelfStatus : 1,
+    auditReason: item.rejectionReason || "",
+    prodType: item.prodType,
+    cuisineType: item.cuisineType
+  };
+};

+ 26 - 0
src/api/modules/storeDecoration.ts

@@ -2,6 +2,7 @@ import { ResPage, StoreUser } from "@/api/interface/index";
 import { PORT_NONE } from "@/api/config/servicePort";
 import http from "@/api";
 import httpApi from "@/api/indexApi";
+import { Upload } from "@/api/interface/index";
 
 /**
  * @name 商铺用户模块
@@ -30,6 +31,31 @@ export const getStoreDetail = params => {
   return httpApi.get(`/alienStorePlatform/storePlatformRenovation/getDecorationDetail`, params);
 };
 
+// 获取门店装修需求列表(分页)
+export const getDecorationPage = (params: any) => {
+  return httpApi.get(`/alienStore/renovation/requirement/getPage`, params);
+};
+
+// 获取装修需求详情
+export const getDecorationDetail = (params: { id: number | string }) => {
+  return httpApi.get(`/alienStore/renovation/requirement/getDetail`, params);
+};
+
+// 保存或更新装修需求
+export const saveOrUpdateDecoration = (params: any) => {
+  return httpApi.post(`/alienStore/renovation/requirement/saveOrUpdate`, params);
+};
+
+// 上传房屋图纸 - 使用 /alienStore/file/uploadMore 接口
+export const uploadDecorationImage = (params: FormData) => {
+  return httpApi.post<Upload.ResFileUrl>(`/alienStore/file/uploadMore`, params, { cancel: false });
+};
+
+// 删除装修需求
+export const deleteDecoration = (params: { id: number | string }) => {
+  return httpApi.post(`/alienStore/renovation/requirement/delete`, {}, { params });
+};
+
 //保存店铺信息
 export const saveStoreInfo = (params: any) => {
   return httpApi.post(`/alienStore/store/info/saveOrUpdate`, params);

+ 45 - 1
src/api/modules/upload.ts

@@ -3,15 +3,59 @@ import { PORT1 } from "@/api/config/servicePort";
 import { PORT_NONE } from "@/api/config/servicePort";
 import httpStore from "@/api/indexStore";
 import http from "@/api";
+import axios from "axios";
+import { ResultEnum } from "@/enums/httpEnum";
+import { useUserStore } from "@/stores/modules/user";
+import { ElMessage } from "element-plus";
+import { LOGIN_URL } from "@/config";
+import router from "@/routers";
+
+// 使用 alienStore 前缀的 axios 实例(用于价目表等页面上传)
+const httpStoreAlienStore = axios.create({
+  baseURL: import.meta.env.VITE_API_URL_STORE as string,
+  timeout: ResultEnum.TIMEOUT as number,
+  withCredentials: true
+});
+httpStoreAlienStore.interceptors.request.use(
+  config => {
+    const userStore = useUserStore();
+    if (config.headers) (config.headers as any).Authorization = userStore.token;
+    return config;
+  },
+  error => Promise.reject(error)
+);
+httpStoreAlienStore.interceptors.response.use(
+  response => {
+    const data = response.data;
+    const userStore = useUserStore();
+    if (data.code == ResultEnum.OVERDUE) {
+      userStore.setToken("");
+      router.replace(LOGIN_URL);
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+    if (data.code && data.code !== ResultEnum.SUCCESS) {
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+    return data;
+  },
+  error => Promise.reject(error)
+);
 
 /**
  * @name 文件上传模块
  */
-// 图片上传
+// 图片上传(默认使用 alienStorePlatform)
 export const uploadImg = (params: FormData) => {
   return httpStore.post<Upload.ResFileUrl>(PORT_NONE + `/file/uploadMore`, params, { cancel: false });
 };
 
+// 图片上传(使用 alienStore 前缀,用于价目表等页面)
+export const uploadImgStore = (params: FormData) => {
+  return httpStoreAlienStore.post<Upload.ResFileUrl>(PORT_NONE + `/file/uploadMore`, params, { cancel: false });
+};
+
 // 视频上传
 export const uploadVideo = (params: FormData) => {
   return http.post<Upload.ResFileUrl>(PORT_NONE + `/file/upload/video`, params, { cancel: false });

+ 126 - 5
src/assets/json/authMenuList.json

@@ -152,6 +152,37 @@
       ]
     },
     {
+      "path": "/priceList",
+      "name": "priceList",
+      "component": "/priceList/index",
+      "meta": {
+        "icon": "List",
+        "title": "价目表",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      },
+      "children": [
+        {
+          "path": "/priceList/edit",
+          "name": "priceListEdit",
+          "component": "/priceList/edit",
+          "meta": {
+            "icon": "Menu",
+            "title": "编辑价目表",
+            "activeMenu": "/priceList",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        }
+      ]
+    },
+    {
       "path": "/orderManagement",
       "name": "orderManagement",
       "component": "/orderManagement/index",
@@ -205,22 +236,22 @@
             "title": "数据概况",
             "activeMenu": "/businessData",
             "isLink": "",
-            "isHide": false,
+            "isHide": true,
             "isFull": false,
             "isAffix": false,
             "isKeepAlive": false
           }
         },
         {
-          "path": "/businessData/history",
-          "name": "businessDataHistory",
-          "component": "/businessData/history",
+          "path": "/businessData/historicalAnalysis",
+          "name": "businessDataHistoricalAnalysis",
+          "component": "/businessData/historicalAnalysis",
           "meta": {
             "icon": "TrendCharts",
             "title": "历史分析",
             "activeMenu": "/businessData",
             "isLink": "",
-            "isHide": false,
+            "isHide": true,
             "isFull": false,
             "isAffix": false,
             "isKeepAlive": false
@@ -928,6 +959,96 @@
       ]
     },
     {
+      "path": "/storeDecorationManagement",
+      "name": "storeDecorationManagement",
+      "redirect": "/storeDecorationManagement/decorationManagement",
+      "meta": {
+        "icon": "Brush",
+        "title": "门店装修",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      },
+      "children": [
+        {
+          "path": "/storeDecorationManagement/decorationManagement",
+          "name": "decorationManagement",
+          "component": "/storeDecoration/decorationManagement",
+          "meta": {
+            "icon": "Menu",
+            "title": "门店装修",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/storeDecorationManagement/add",
+          "name": "decorationManagementAdd",
+          "component": "/storeDecoration/add",
+          "meta": {
+            "icon": "Menu",
+            "title": "新建装修需求",
+            "activeMenu": "/storeDecorationManagement/decorationManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/storeDecorationManagement/detail",
+          "name": "decorationManagementDetail",
+          "component": "/storeDecoration/detail",
+          "meta": {
+            "icon": "Menu",
+            "title": "装修详情",
+            "activeMenu": "/storeDecorationManagement/decorationManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/storeDecorationManagement/decorationCompany",
+          "name": "decorationCompany",
+          "component": "/storeDecoration/decorationCompany",
+          "meta": {
+            "icon": "OfficeBuilding",
+            "title": "装修公司",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/storeDecorationManagement/decorationCompanyDetail",
+          "name": "decorationCompanyDetail",
+          "component": "/storeDecoration/decorationCompanyDetail",
+          "meta": {
+            "icon": "Menu",
+            "title": "装修详情",
+            "activeMenu": "/storeDecorationManagement/decorationCompany",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        }
+      ]
+    },
+
+    {
       "path": "/accountRoleManagement",
       "name": "accountRoleManagement",
       "redirect": "/accountRoleManagement/subAccountManagement",

+ 50 - 1
src/views/businessData/compare.vue

@@ -302,10 +302,15 @@ const queryEnd = (route.query.end as string) || "";
 const queryCompareStart = (route.query.compareStart as string) || "";
 const queryCompareEnd = (route.query.compareEnd as string) || "";
 const compareType = (route.query.compareType as string) || "lastPeriod";
+const historyIdFromRoute = route.query.historyId as string | undefined;
 
 const compareLabel = computed(() => (compareType === "samePeriod" ? "同期" : "上期"));
 
+// 历史详情模式下的日期 PK 文案(由 history/detail 返回的 statisticsData 解析得到)
+const historyDateRangeText = ref("");
+
 const dateRangeText = computed(() => {
+  if (historyIdFromRoute && historyDateRangeText.value) return historyDateRangeText.value;
   if (!queryStart || !queryEnd) return "2026/01/07-2026/01/08 PK 2025/01/07-2025/01/08";
   const currentStr = `${queryStart.replace(/-/g, "/")}-${queryEnd.replace(/-/g, "/")}`;
 
@@ -807,9 +812,53 @@ async function fetchComparisonData() {
   }
 }
 
+// 根据历史 id 拉取详情并展示(statisticsData 为 JSON 字符串,结构同 StoreOperationalStatisticsComparisonVo)
+async function fetchHistoryDetail(id: string | number) {
+  loading.value = true;
+  try {
+    const res: any = await getHistoryDetail({ id });
+    const data = res?.data ?? res;
+    const rawStats = data?.statisticsData;
+    if (!rawStats || typeof rawStats !== "string") {
+      ElMessage.warning("暂无对比数据");
+      return;
+    }
+    let stats: any;
+    try {
+      stats = JSON.parse(rawStats);
+    } catch (e) {
+      ElMessage.warning("对比数据解析失败");
+      return;
+    }
+    comparisonHistoryId.value = Number(id) || stats?.historyId;
+    const curStart = stats?.currentStartTime ?? "";
+    const curEnd = stats?.currentEndTime ?? "";
+    const prevStart = stats?.previousStartTime ?? "";
+    const prevEnd = stats?.previousEndTime ?? "";
+    historyDateRangeText.value =
+      curStart && curEnd && prevStart && prevEnd
+        ? `${String(curStart).replace(/-/g, "/")}-${String(curEnd).replace(/-/g, "/")} PK ${String(prevStart).replace(/-/g, "/")}-${String(prevEnd).replace(/-/g, "/")}`
+        : "";
+    setTrafficCompare(stats?.trafficData);
+    setInteractionCompare(stats?.interactionData);
+    setCouponCompare(stats?.couponData);
+    setVoucherCompare(stats?.voucherData);
+    setServiceCompare(stats?.serviceQualityData);
+    priceListRankingData.value = stats?.priceListRanking ?? [];
+  } catch (e) {
+    ElMessage.error("获取历史详情失败");
+  } finally {
+    loading.value = false;
+  }
+}
+
 // 页面加载时获取数据
 onMounted(() => {
-  fetchComparisonData();
+  if (historyIdFromRoute) {
+    fetchHistoryDetail(historyIdFromRoute);
+  } else {
+    fetchComparisonData();
+  }
 });
 </script>
 

+ 373 - 0
src/views/businessData/historicalAnalysis.vue

@@ -0,0 +1,373 @@
+<template>
+  <div class="table-box historical-analysis-page">
+    <!-- 筛选区:生成时间、重置、查询 -->
+    <div class="filter-bar">
+      <div class="filter-left">
+        <span class="filter-label">生成时间</span>
+        <el-date-picker
+          v-model="filterDate"
+          type="date"
+          placeholder="选择日期"
+          value-format="YYYY-MM-DD"
+          format="YYYY/MM/DD"
+          class="filter-date-picker"
+          clearable
+        />
+      </div>
+      <div class="filter-actions">
+        <el-button @click="handleReset"> 重置 </el-button>
+        <el-button type="primary" :loading="loading" @click="handleQuery"> 查询 </el-button>
+      </div>
+    </div>
+
+    <!-- 历史分析报告列表 -->
+    <el-card class="list-card" shadow="hover" v-loading="loading">
+      <template #header>
+        <div class="card-header">
+          <span class="card-title">历史分析</span>
+          <div class="card-actions">
+            <el-button link type="primary" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
+              <el-icon><Delete /></el-icon>
+              删除
+            </el-button>
+          </div>
+        </div>
+      </template>
+
+      <!-- 报告列表 -->
+      <div v-if="list.length > 0" class="report-list">
+        <div v-for="item in list" :key="item.id" class="report-item" :class="{ selected: selectedIds.includes(item.id) }">
+          <el-checkbox
+            :model-value="selectedIds.includes(item.id)"
+            @update:model-value="(val: boolean) => toggleSelect(item.id, val)"
+          />
+          <div class="report-content">
+            <div class="report-title">
+              {{ item.title }}
+            </div>
+            <div class="report-meta">生成时间: {{ item.createTime }}</div>
+          </div>
+          <div class="report-item-actions">
+            <el-button type="primary" size="small" @click="handleView(item)"> 查看 </el-button>
+            <el-button type="primary" link size="small" @click="handleExport(item)">
+              <el-icon><Download /></el-icon>
+              导出
+            </el-button>
+          </div>
+        </div>
+      </div>
+
+      <el-empty v-else description="暂无历史分析报告" />
+
+      <!-- 分页 -->
+      <div v-if="total > 0" class="pagination-wrap">
+        <el-pagination
+          v-model:current-page="pagination.page"
+          v-model:page-size="pagination.pageSize"
+          :page-sizes="[10, 20, 50]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="fetchList"
+          @current-change="fetchList"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts" name="businessDataHistoricalAnalysis">
+import { ref, reactive, onMounted } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Delete, Download } from "@element-plus/icons-vue";
+import { localGet } from "@/utils";
+import {
+  getHistoryReportList,
+  batchDeleteHistoryReport,
+  generateStatisticsComparisonPdfByHistoryId
+} from "@/api/modules/businessData";
+import type { HistoryReportRecord } from "@/api/modules/businessData";
+
+/** 页面展示用的历史报告项 */
+interface HistoryReportItem {
+  id: number | string;
+  title: string;
+  createTime: string;
+  pdfUrl?: string | null;
+}
+
+function getTodayString() {
+  const d = new Date();
+  const y = d.getFullYear();
+  const m = String(d.getMonth() + 1).padStart(2, "0");
+  const day = String(d.getDate()).padStart(2, "0");
+  return `${y}-${m}-${day}`;
+}
+
+/** 将 queryTime 转为 yyyy-MM-dd(列表名称用) */
+function formatQueryTimeDate(qt: string | undefined): string {
+  if (qt == null || qt === "") return "--";
+  const part = String(qt).trim().split(/\s/)[0];
+  return part && /^\d{4}-\d{2}-\d{2}$/.test(part) ? part : "--";
+}
+
+/** 列表项:名称 = queryTime 的 yyyy-MM-dd + 对比报告,生成时间 = queryTime 完整年月日时分秒 */
+function recordToItem(record: HistoryReportRecord): HistoryReportItem {
+  const qt = record.queryTime ?? record.createdTime ?? "";
+  const dateStr = formatQueryTimeDate(qt);
+  const fullTime = qt && dateStr !== "--" ? qt : "--";
+  return {
+    id: record.id ?? "",
+    title: `${dateStr} 对比报告`,
+    createTime: fullTime,
+    pdfUrl: record.pdfUrl ?? null
+  };
+}
+
+const router = useRouter();
+const loading = ref(false);
+const filterDate = ref<string | null>(getTodayString());
+const list = ref<HistoryReportItem[]>([]);
+const total = ref(0);
+const selectedIds = ref<(number | string)[]>([]);
+
+const pagination = reactive({
+  page: 1,
+  pageSize: 10
+});
+
+/** 获取列表 */
+async function fetchList() {
+  const storeId = localGet("createdId");
+  if (!storeId) {
+    ElMessage.warning("请先选择店铺");
+    return;
+  }
+  loading.value = true;
+  try {
+    const res: any = await getHistoryReportList({
+      storeId,
+      page: pagination.page,
+      size: pagination.pageSize,
+      created_time: filterDate.value ?? undefined
+    });
+    const data = res?.data ?? res;
+    const rawList = data?.records ?? data?.list ?? [];
+    const totalNum = data?.total ?? 0;
+    list.value = rawList.map((r: HistoryReportRecord) => recordToItem(r));
+    total.value = totalNum;
+  } catch (e) {
+    list.value = [];
+    total.value = 0;
+  } finally {
+    loading.value = false;
+  }
+}
+
+function handleReset() {
+  filterDate.value = null;
+  pagination.page = 1;
+  fetchList();
+}
+
+function handleQuery() {
+  pagination.page = 1;
+  fetchList();
+}
+
+function toggleSelect(id: number | string, checked: boolean) {
+  if (checked) {
+    if (!selectedIds.value.includes(id)) selectedIds.value.push(id);
+  } else {
+    selectedIds.value = selectedIds.value.filter(i => i !== id);
+  }
+}
+
+function handleBatchDelete() {
+  if (selectedIds.value.length === 0) return;
+  ElMessageBox.confirm(`确定删除选中的 ${selectedIds.value.length} 条报告?`, "提示", {
+    type: "warning"
+  })
+    .then(async () => {
+      const ids = selectedIds.value.join(",");
+      await batchDeleteHistoryReport({ ids });
+      ElMessage.success("删除成功");
+      selectedIds.value = [];
+      fetchList();
+    })
+    .catch(() => {});
+}
+
+function downloadPdfUrl(url: string, filename: string) {
+  const link = document.createElement("a");
+  link.href = url;
+  link.setAttribute("download", filename);
+  link.setAttribute("target", "_blank");
+  link.setAttribute("rel", "noopener noreferrer");
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  ElMessage.success("开始下载");
+}
+
+async function handleExport(item?: HistoryReportItem) {
+  if (!item) return;
+  let url = item.pdfUrl?.trim() || "";
+  if (!url) {
+    const storeId = localGet("createdId");
+    if (!storeId) {
+      ElMessage.warning("请先选择店铺");
+      return;
+    }
+    try {
+      const res: any = await generateStatisticsComparisonPdfByHistoryId({ historyId: item.id, storeId });
+      const data = res?.data ?? res;
+      url = typeof data === "string" ? data.trim() : "";
+      if (!url) {
+        ElMessage.warning("生成 PDF 失败,暂无下载地址");
+        return;
+      }
+    } catch (e) {
+      ElMessage.error("生成 PDF 失败");
+      return;
+    }
+  }
+  downloadPdfUrl(url, `${item.title || "对比报告"}.pdf`);
+}
+
+function handleView(item: HistoryReportItem) {
+  router.push({
+    path: "/businessData/compare",
+    query: { historyId: item.id }
+  });
+}
+
+onMounted(() => {
+  fetchList();
+});
+</script>
+
+<style lang="scss" scoped>
+/* 与数据概况等页一致的 table-box 容器 */
+.table-box {
+  display: flex;
+  flex-direction: column;
+  height: auto !important;
+  min-height: 100%;
+}
+.historical-analysis-page {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  min-height: 100%;
+}
+.filter-bar {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px;
+  background: var(--el-bg-color);
+  border-radius: 8px;
+  .filter-left {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 12px;
+    align-items: center;
+  }
+  .filter-label {
+    font-size: 14px;
+    color: var(--el-text-color-regular);
+    white-space: nowrap;
+  }
+  .filter-date-picker {
+    width: 240px;
+  }
+  .filter-actions {
+    display: flex;
+    gap: 8px;
+  }
+}
+.list-card {
+  flex: 1;
+  min-height: 0;
+  border-radius: 8px;
+  :deep(.el-card__header) {
+    padding: 14px 20px;
+  }
+  :deep(.el-card__body) {
+    padding: 0 20px 20px;
+  }
+}
+.card-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  .card-title {
+    font-size: 16px;
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+  }
+  .card-actions {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+  }
+}
+.report-list {
+  display: flex;
+  flex-direction: column;
+  gap: 0;
+}
+.report-item {
+  display: flex;
+  gap: 16px;
+  align-items: center;
+  padding: 14px 0;
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  &:last-child {
+    border-bottom: none;
+  }
+  &.selected {
+    padding-right: 20px;
+    padding-left: 20px;
+    margin: 0 -20px;
+    background: var(--el-fill-color-light);
+  }
+  .report-content {
+    flex: 1;
+    min-width: 0;
+  }
+  .report-title {
+    margin-bottom: 4px;
+    font-size: 14px;
+    font-weight: 500;
+    color: var(--el-text-color-primary);
+  }
+  .report-meta {
+    font-size: 12px;
+    color: var(--el-text-color-secondary);
+  }
+  .report-item-actions {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+  }
+}
+.pagination-wrap {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 16px;
+}
+
+@media (width <= 768px) {
+  .filter-bar .filter-left {
+    width: 100%;
+  }
+  .filter-bar .filter-actions {
+    justify-content: flex-end;
+    width: 100%;
+  }
+}
+</style>

+ 128 - 33
src/views/operationManagement/activityDetail.vue

@@ -37,7 +37,7 @@
           </div>
 
           <!-- 评论有礼相关字段 -->
-          <template v-if="activityModel.activityType == 1">
+          <template v-if="activityModel.activityType == 2">
             <!-- 用户可参与次数 -->
             <div class="detail-item">
               <div class="detail-label">用户可参与次数</div>
@@ -69,13 +69,13 @@
           </template>
 
           <!-- 营销活动相关字段 -->
-          <template v-if="activityModel.activityType == 2">
+          <template v-if="activityModel.activityType == 1">
             <!-- 报名时间 -->
             <div class="detail-item">
               <div class="detail-label">报名时间</div>
               <div class="detail-value">
                 <span v-if="activityModel.signupStartTime && activityModel.signupEndTime">
-                  {{ activityModel.signupStartTime.split(' ')[0] }} - {{ activityModel.signupEndTime.split(' ')[0] }}
+                  {{ activityModel.signupStartTime.split(" ")[0] }} - {{ activityModel.signupEndTime.split(" ")[0] }}
                 </span>
                 <span v-else>--</span>
               </div>
@@ -90,16 +90,30 @@
             <!-- 活动详情 -->
             <div class="detail-item">
               <div class="detail-label">活动详情</div>
-              <div class="detail-value" style=" word-break: break-word;white-space: pre-wrap">
+              <div class="detail-value" style="word-break: break-word; white-space: pre-wrap">
                 {{ activityModel.activityDetails || "--" }}
               </div>
             </div>
           </template>
-          <!-- 状态 -->
+          <!-- 审核状态 -->
           <div class="detail-item">
-            <div class="detail-label">状态</div>
+            <div class="detail-label">审核状态</div>
+            <div class="detail-value">
+              {{ getAuditStatusLabel(activityModel.status) }}
+            </div>
+          </div>
+          <!-- 审核时间 -->
+          <div class="detail-item" v-if="activityModel.auditTime">
+            <div class="detail-label">审核时间</div>
             <div class="detail-value">
-              {{ getStatusLabel(activityModel.status) }}
+              {{ formatDateTime(activityModel.auditTime) }}
+            </div>
+          </div>
+          <!-- 审核拒绝原因 -->
+          <div class="detail-item" v-if="activityModel.approvalComments">
+            <div class="detail-label">审核拒绝原因</div>
+            <div class="detail-value" style="word-break: break-word; white-space: pre-wrap">
+              {{ activityModel.approvalComments }}
             </div>
           </div>
         </div>
@@ -123,7 +137,9 @@
                   >
                     <template #error>
                       <div class="image-slot">
-                        <el-icon><Picture /></el-icon>
+                        <el-icon>
+                          <Picture />
+                        </el-icon>
                       </div>
                     </template>
                   </el-image>
@@ -135,20 +151,29 @@
             <div class="promotion-image-item">
               <h3 style="font-weight: bold">活动详情图:</h3>
               <div class="image-container">
-                <div v-if="getImageUrl(activityModel.activityDetailImgUrl)" class="image-item">
-                  <el-image
-                    :src="getImageUrl(activityModel.activityDetailImgUrl)"
-                    fit=""
-                    class="promotion-image"
-                    :preview-src-list="getPreviewImageList()"
-                    :initial-index="getImageUrl(activityModel.activityTitleImgUrl) ? 1 : 0"
+                <div v-if="detailImageList.length > 0" class="detail-images-grid">
+                  <div
+                    v-for="(imgUrl, index) in detailImageList"
+                    :key="index"
+                    class="detail-image-item"
+                    @click="previewDetailImage(index)"
                   >
-                    <template #error>
-                      <div class="image-slot">
-                        <el-icon><Picture /></el-icon>
-                      </div>
-                    </template>
-                  </el-image>
+                    <el-image
+                      :src="imgUrl"
+                      fit="cover"
+                      class="detail-image"
+                      :preview-src-list="getPreviewImageList()"
+                      :initial-index="getImageUrl(activityModel.activityTitleImgUrl) ? index + 1 : index"
+                    >
+                      <template #error>
+                        <div class="image-slot">
+                          <el-icon>
+                            <Picture />
+                          </el-icon>
+                        </div>
+                      </template>
+                    </el-image>
+                  </div>
                 </div>
                 <div v-else class="empty-text">--</div>
               </div>
@@ -165,7 +190,7 @@
  * 运营活动管理 - 详情页面
  * 功能:显示运营活动的详细信息
  */
-import { ref, onMounted } from "vue";
+import { ref, onMounted, computed } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { ElMessage } from "element-plus";
 import { Picture } from "@element-plus/icons-vue";
@@ -213,7 +238,12 @@ const activityModel = ref<any>({
   // 活动详情图片
   activityDetailImgUrl: null,
   // 状态
-  status: 0
+  status: 0,
+  // 审核时间
+  approvalTime: "",
+  auditTime: "",
+  // 审核拒绝原因
+  approvalComments: ""
 });
 
 // ==================== 生命周期钩子 ====================
@@ -273,6 +303,17 @@ const formatDate = (date: string) => {
 };
 
 /**
+ * 格式化日期时间
+ * @param dateTime 日期时间字符串
+ * @returns 格式化后的日期时间字符串
+ */
+const formatDateTime = (dateTime: string) => {
+  if (!dateTime) return "--";
+  // 将 - 替换为 /,并保持时间部分
+  return dateTime.replace(/-/g, "/");
+};
+
+/**
  * 获取活动类型标签
  */
 const getActivityTypeLabel = (activityType: number) => {
@@ -300,6 +341,20 @@ const getStatusLabel = (status: number) => {
 };
 
 /**
+ * 获取审核状态标签(只显示审核相关的状态)
+ */
+const getAuditStatusLabel = (status: number) => {
+  const auditStatusMap: Record<number, string> = {
+    1: "待审核",
+    2: "待审核",
+    3: "审核驳回",
+    5: "审核通过",
+    8: "审核通过"
+  };
+  return auditStatusMap[status] || "--";
+};
+
+/**
  * 获取活动规则显示文本
  */
 const getRuleDisplayText = (ruleValue: any) => {
@@ -338,20 +393,51 @@ const getImageUrl = (img: any): string => {
 };
 
 /**
+ * 处理活动详情图列表(支持字符串、数组、对象数组)
+ */
+const detailImageList = computed(() => {
+  const imgData = activityModel.value.activityDetailImgUrl || activityModel.value.activityDetailImgUrlList;
+  if (!imgData) return [];
+
+  // 如果是字符串,可能是逗号分隔的多张图片
+  if (typeof imgData === "string") {
+    return imgData.split(",").filter((url: string) => url.trim());
+  }
+
+  // 如果是数组
+  if (Array.isArray(imgData)) {
+    return imgData
+      .map((item: any) => {
+        if (typeof item === "string") return item;
+        if (item && item.url) return item.url;
+        return "";
+      })
+      .filter((url: string) => url);
+  }
+
+  return [];
+});
+
+/**
  * 获取预览图片列表
  */
 const getPreviewImageList = () => {
   const list: string[] = [];
   const titleUrl = getImageUrl(activityModel.value.activityTitleImgUrl);
-  const detailUrl = getImageUrl(activityModel.value.activityDetailImgUrl);
   if (titleUrl) {
     list.push(titleUrl);
   }
-  if (detailUrl) {
-    list.push(detailUrl);
-  }
+  // 添加所有详情图
+  list.push(...detailImageList.value);
   return list;
 };
+
+/**
+ * 预览详情图
+ */
+const previewDetailImage = (index: number) => {
+  // 预览功能由 el-image 的 preview-src-list 自动处理
+};
 </script>
 
 <style scoped lang="scss">
@@ -493,20 +579,29 @@ const getPreviewImageList = () => {
   }
 }
 
-/* 活动详情图 - 竖版样式 */
+/* 活动详情图 - 网格布局 */
 .promotion-image-item:last-child {
   .image-container {
     width: 100%;
-    max-width: 300px;
   }
-  .image-item {
+  .detail-images-grid {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+    gap: 12px;
     width: 100%;
-    aspect-ratio: 3 / 4;
   }
-  .promotion-image {
+  .detail-image-item {
+    width: 150px;
+    height: 150px;
+    overflow: hidden;
+    cursor: pointer;
+    background: #f5f7fa;
+    border-radius: 8px;
+  }
+  .detail-image {
     width: 100%;
     height: 100%;
-    object-fit: contain;
+    object-fit: cover;
   }
 }
 .image-slot {

+ 89 - 52
src/views/operationManagement/activityList.vue

@@ -172,13 +172,13 @@
 <script setup lang="tsx" name="activityList">
 import { reactive, ref, onMounted, computed, nextTick } from "vue";
 import { useRouter } from "vue-router";
-import { ElMessage, ElMessageBox } from "element-plus";
+import { ElMessage, ElMessageBox, ElLoading } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
 import type { UploadFile, UploadFiles, UploadProps } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { getActivityList, deleteActivity, updateActivityStatus, uploadActivityResult } from "@/api/modules/operationManagement";
-import { uploadImg } from "@/api/modules/upload";
+import { uploadContractImage } from "@/api/modules/licenseManagement";
 import { localGet, usePermission } from "@/utils";
 
 const router = useRouter();
@@ -290,10 +290,21 @@ const getStatusLabel = (status: number) => {
 // 表格列配置
 const columns = reactive<ColumnProps<any>[]>([
   {
+    prop: "activityName",
+    label: "活动名称",
+    search: {
+      el: "input",
+      props: { placeholder: "请输入" }
+    }
+  },
+  {
+    prop: "id",
+    label: "活动ID"
+  },
+  {
     prop: "activityType",
     label: "活动类型",
     width: 120,
-    isShow: false,
     enum: activityTypeEnum,
     fieldNames: { label: "label", value: "value" },
     search: {
@@ -302,44 +313,40 @@ const columns = reactive<ColumnProps<any>[]>([
       order: 1
     },
     render: (scope: any) => {
-      const type = scope.row.activityType;
-      const typeItem = activityTypeEnum.find(item => item.value === type);
-      return typeItem ? typeItem.label : "-";
+      return scope.row.activityType == 1 ? "营销活动" : "评论有礼";
     }
   },
   {
-    prop: "activityName",
-    label: "活动名称",
-    search: {
-      el: "input",
-      props: { placeholder: "请输入" }
-    }
-  },
-  {
-    prop: "id",
-    label: "活动ID"
-  },
-  {
     prop: "startTime",
-    label: "活动开始时间",
+    label: "活动开始时间/结束时间",
+    width: 300,
     render: (scope: any) => {
-      return scope.row.startTime?.replace(/-/g, "/") || "--";
+      const formatDate = (dateStr: string) => {
+        if (!dateStr) return "--";
+        // 只取日期部分(去掉时分秒)
+        const datePart = dateStr.split(" ")[0];
+        return datePart?.replace(/-/g, "/") || "--";
+      };
+      return `${formatDate(scope.row.startTime)} - ${formatDate(scope.row.endTime)}`;
     }
   },
   {
-    prop: "endTime",
-    label: "活动结束时间",
+    prop: "signupStartTime",
+    label: "报名开始时间/结束时间",
+    width: 300,
     render: (scope: any) => {
-      return scope.row.endTime?.replace(/-/g, "/") || "--";
+      const formatDate = (dateStr: string) => {
+        if (!dateStr) return "--";
+        // 只取日期部分(去掉时分秒)
+        const datePart = dateStr.split(" ")[0];
+        return datePart?.replace(/-/g, "/") || "--";
+      };
+      return `${formatDate(scope.row.signupStartTime)} - ${formatDate(scope.row.signupEndTime)}`;
     }
   },
   {
-    prop: "couponName",
-    label: "优惠券名称"
-  },
-  {
     prop: "status",
-    label: "活动状态",
+    label: "状态",
     render: (scope: any) => {
       // 优先使用返回的 statusName,如果没有则使用 getStatusLabel
       return scope.row.statusName || getStatusLabel(scope.row.status);
@@ -368,21 +375,34 @@ const dataCallback = (data: any) => {
 };
 
 // 获取表格列表
-const getTableList = (params: any) => {
-  // 处理参数:确保 status 是 number 类型,pageNum 和 pageSize 转换为 string
-  const newParams: any = {
-    ...params,
-    pageNum: params.pageNum ? String(params.pageNum) : undefined,
-    pageSize: params.pageSize ? String(params.pageSize) : undefined,
-    status: params.status !== undefined && params.status !== null && params.status !== "" ? Number(params.status) : undefined,
-    storeId: params.storeId ? String(params.storeId) : undefined,
-    // 处理 activityType:如果存在且不为空,转换为 number 类型
-    activityType:
-      params.activityType !== undefined && params.activityType !== null && params.activityType !== ""
-        ? Number(params.activityType)
-        : undefined
-  };
-  return getActivityList(newParams);
+const getTableList = async (params: any) => {
+  // 显示全屏loading
+  const loadingInstance = ElLoading.service({
+    lock: true,
+    text: "加载中...",
+    background: "rgba(0, 0, 0, 0.7)"
+  });
+
+  try {
+    // 处理参数:确保 status 是 number 类型,pageNum 和 pageSize 转换为 string
+    const newParams: any = {
+      ...params,
+      pageNum: params.pageNum ? String(params.pageNum) : undefined,
+      pageSize: params.pageSize ? String(params.pageSize) : undefined,
+      status: params.status !== undefined && params.status !== null && params.status !== "" ? Number(params.status) : undefined,
+      storeId: params.storeId ? String(params.storeId) : undefined,
+      // 处理 activityType:如果存在且不为空,转换为 number 类型
+      activityType:
+        params.activityType !== undefined && params.activityType !== null && params.activityType !== ""
+          ? Number(params.activityType)
+          : undefined
+    };
+    const result = await getActivityList(newParams);
+    return result;
+  } finally {
+    // 关闭loading
+    loadingInstance.close();
+  }
 };
 
 // 新建活动
@@ -496,23 +516,40 @@ const handleResultImageChange: UploadProps["onChange"] = async (uploadFile: Uplo
       return;
     }
 
+    // 确保文件在列表中
+    const existingIndex = resultImageFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
+    if (existingIndex === -1) {
+      resultImageFileList.value.push(uploadFile);
+    }
+
     // 自动上传图片
     try {
       uploadFile.status = "uploading";
+      uploadFile.percentage = 0;
       const formData = new FormData();
       formData.append("file", uploadFile.raw);
-      const res: any = await uploadImg(formData);
-      const imageUrl = (res?.data && Array.isArray(res.data) ? res.data[0] : null) || res?.data?.fileUrl || res?.fileUrl;
-      if (imageUrl) {
-        uploadFile.status = "success";
-        uploadFile.url = imageUrl;
-        uploadResultForm.value.resultMediaUrl = imageUrl;
+      formData.append("user", "text");
+      const res: any = await uploadContractImage(formData);
+      if (res?.code === 200 && res.data) {
+        const imageUrl = res.data[0];
+        if (imageUrl) {
+          uploadFile.status = "success";
+          uploadFile.percentage = 100;
+          uploadFile.url = imageUrl;
+          uploadFile.response = { url: imageUrl };
+          uploadResultForm.value.resultMediaUrl = imageUrl;
+          // 触发视图更新
+          resultImageFileList.value = [...resultImageFileList.value];
+        } else {
+          throw new Error("上传失败:未获取到图片URL");
+        }
       } else {
-        throw new Error("上传失败:未获取到图片URL");
+        throw new Error(res?.msg || "图片上传失败");
       }
-    } catch (error) {
+    } catch (error: any) {
       uploadFile.status = "fail";
-      ElMessage.error("图片上传失败,请重试");
+      uploadFile.percentage = 0;
+      ElMessage.error(error?.message || "图片上传失败,请重试");
       const index = resultImageFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
       if (index > -1) {
         resultImageFileList.value.splice(index, 1);

+ 110 - 36
src/views/operationManagement/caseDetail.vue

@@ -31,22 +31,42 @@
               <div class="detail-label">更新时间 : {{ formatTime(detail.achievementList[0].updatedTime) }}</div>
             </div>
             <div class="detail-item">
-              <div class="detail-label">成果描述 : {{ detail.achievementList[0].achievementDesc || '-' }}</div>
+              <div class="detail-label">成果描述 : {{ detail.achievementList[0].achievementDesc || "-" }}</div>
             </div>
-            <div class="detail-item" v-if="detail.achievementList[0].mediaUrlList">
+            <div class="detail-item" v-if="mediaList.length > 0">
               <div class="detail-label">图片与视频 :</div>
               <div class="media-grid">
-                <template v-for="(item, index) in detail.achievementList[0].mediaUrlList" :key="index">
+                <template v-for="(item, index) in mediaList" :key="index">
                   <div v-if="item.type === 'video'" class="media-item video-item" @click="playVideo(item.url)">
-                    <div class="media-placeholder">
+                    <el-image v-if="item.coverUrl" :src="item.coverUrl" fit="cover" class="media-image">
+                      <template #error>
+                        <div class="media-placeholder">
+                          <el-icon class="play-icon">
+                            <VideoPlay />
+                          </el-icon>
+                        </div>
+                      </template>
+                    </el-image>
+                    <div v-else class="media-placeholder">
                       <el-icon class="play-icon">
                         <VideoPlay />
                       </el-icon>
                     </div>
+                    <!-- 视频播放图标覆盖层 -->
+                    <div class="video-overlay">
+                      <el-icon class="play-icon-overlay">
+                        <VideoPlay />
+                      </el-icon>
+                    </div>
                   </div>
-                  <div v-else class="media-item image-item" @click="previewImage(item, index)">
-                    <el-image :src="item" fit="cover" class="media-image"
-                      :preview-src-list="detail.achievementList[0].mediaUrlList" :initial-index="getImageIndex(item)">
+                  <div v-else class="media-item image-item" @click="previewImage(item.url, index)">
+                    <el-image
+                      :src="item.url"
+                      fit="cover"
+                      class="media-image"
+                      :preview-src-list="imageList"
+                      :initial-index="getImageIndex(item.url)"
+                    >
                       <template #error>
                         <div class="image-slot">
                           <el-icon>
@@ -110,24 +130,77 @@ const formatTime = (time: string | null | undefined) => {
   }
 };
 
-type MediaItem = { type: "image" | "video"; url: string };
+type MediaItem = { type: "image" | "video"; url: string; coverUrl?: string };
 
 const mediaList = computed<MediaItem[]>(() => {
   if (!detail.value) return [];
   const list: MediaItem[] = [];
 
+  // 优先处理 achievementList[0].mediaUrlList
+  if (detail.value.achievementList && detail.value.achievementList[0]?.mediaUrlList) {
+    const mediaUrlList = detail.value.achievementList[0].mediaUrlList;
+    const arr = Array.isArray(mediaUrlList) ? mediaUrlList : [mediaUrlList];
+    for (const it of arr) {
+      if (typeof it === "string") {
+        // 检查是否包含 | 分隔符(视频格式:xxx.mp4 | XXX.jpg)
+        if (it.includes("|")) {
+          const parts = it
+            .split("|")
+            .map(s => s.trim())
+            .filter(s => s);
+          if (parts.length >= 2) {
+            // 第一部分是视频URL,第二部分是封面URL
+            list.push({ type: "video", url: parts[0], coverUrl: parts[1] });
+          } else if (parts.length === 1) {
+            // 只有视频URL,没有封面
+            list.push({ type: "video", url: parts[0] });
+          }
+        } else {
+          // 如果是字符串,根据文件扩展名判断类型
+          const isVideo = /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it);
+          list.push({ type: isVideo ? "video" : "image", url: it });
+        }
+      } else if (it?.url) {
+        // 如果是对象,使用 type 字段或根据 url 判断
+        const isVideo = it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.url);
+        list.push({ type: isVideo ? "video" : "image", url: it.url, coverUrl: it.coverUrl });
+      } else if (it?.mediaUrl) {
+        // 处理 mediaUrl 字段
+        const isVideo = it.mediaType === "video" || it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.mediaUrl);
+        list.push({ type: isVideo ? "video" : "image", url: it.mediaUrl, coverUrl: it.coverUrl });
+      }
+    }
+  }
+
   // 处理 mediaList 字段(可能是数组或对象数组)
   const raw = detail.value.mediaList ?? detail.value.images ?? detail.value.videos ?? [];
   const arr = Array.isArray(raw) ? raw : [raw];
   for (const it of arr) {
     if (typeof it === "string") {
-      list.push({ type: "image", url: it });
+      // 检查是否包含 | 分隔符(视频格式:xxx.mp4 | XXX.jpg)
+      if (it.includes("|")) {
+        const parts = it
+          .split("|")
+          .map(s => s.trim())
+          .filter(s => s);
+        if (parts.length >= 2) {
+          // 第一部分是视频URL,第二部分是封面URL
+          list.push({ type: "video", url: parts[0], coverUrl: parts[1] });
+        } else if (parts.length === 1) {
+          // 只有视频URL,没有封面
+          list.push({ type: "video", url: parts[0] });
+        }
+      } else {
+        const isVideo = /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it);
+        list.push({ type: isVideo ? "video" : "image", url: it });
+      }
     } else if (it?.url) {
-      list.push({ type: it.type === "video" ? "video" : "image", url: it.url });
+      const isVideo = it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.url);
+      list.push({ type: isVideo ? "video" : "image", url: it.url, coverUrl: it.coverUrl });
     } else if (it?.mediaUrl) {
       // 处理 mediaUrl 字段
-      const mediaType = it.mediaType === "video" || it.type === "video" ? "video" : "image";
-      list.push({ type: mediaType, url: it.mediaUrl });
+      const isVideo = it.mediaType === "video" || it.type === "video" || /\.(mp4|avi|mov|wmv|flv|webm)$/i.test(it.mediaUrl);
+      list.push({ type: isVideo ? "video" : "image", url: it.mediaUrl, coverUrl: it.coverUrl });
     }
   }
 
@@ -178,97 +251,86 @@ onMounted(async () => {
   padding: 16px;
   background: #ffffff;
 }
-
 .header {
   display: flex;
   gap: 16px;
   align-items: center;
   margin-bottom: 16px;
 }
-
 .title {
   margin: 0;
   font-size: 18px;
   font-weight: 600;
 }
-
 .detail-list {
   display: flex;
   flex-direction: column;
   gap: 16px;
 }
-
 .detail-item {
   display: flex;
   flex-direction: column;
   gap: 8px;
 }
-
 .detail-label {
   font-size: 14px;
-  color: #606266;
   font-weight: 500;
+  color: #606266;
 }
-
 .detail-value {
   font-size: 14px;
   color: #303133;
   word-break: break-all;
 }
-
 .result-section {
   margin-top: 24px;
 }
-
 .result-title {
   margin-bottom: 12px;
   font-size: 16px;
   font-weight: 600;
 }
-
 .result-desc {
   font-size: 14px;
-  color: #303133;
   line-height: 1.6;
+  color: #303133;
   word-break: break-all;
   white-space: pre-wrap;
 }
-
 .media-grid {
   display: grid;
-  grid-template-columns: repeat(3, 1fr);
+  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
   gap: 12px;
   margin-top: 8px;
 }
-
 .media-item {
   position: relative;
-  aspect-ratio: 1;
+  width: 200px;
+  height: 200px;
   overflow: hidden;
   cursor: pointer;
   background: #f5f7fa;
   border-radius: 8px;
 }
-
 .image-item {
-  width: 100%;
-  height: 100%;
+  width: 200px;
+  height: 200px;
 }
-
 .media-image {
   display: block;
   width: 100%;
   height: 100%;
   object-fit: cover;
 }
-
 .video-item {
+  position: relative;
   display: flex;
   align-items: center;
   justify-content: center;
+  width: 200px;
+  height: 200px;
   background: #000000;
 }
-
 .media-placeholder {
   display: flex;
   align-items: center;
@@ -277,11 +339,24 @@ onMounted(async () => {
   height: 100%;
   color: #909399;
 }
-
 .play-icon {
   font-size: 40px;
 }
-
+.video-overlay {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  pointer-events: none;
+  background: rgb(0 0 0 / 30%);
+  border-radius: 8px;
+}
+.play-icon-overlay {
+  font-size: 48px;
+  color: #ffffff;
+  opacity: 0.9;
+}
 .image-slot {
   display: flex;
   align-items: center;
@@ -291,7 +366,6 @@ onMounted(async () => {
   color: #909399;
   background: #f5f7fa;
 }
-
 .dialog-video {
   display: block;
   width: 100%;

+ 29 - 16
src/views/operationManagement/cases.vue

@@ -1,7 +1,6 @@
 <template>
   <div class="table-box button-table">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam"
-      :data-callback="dataCallback">
+    <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>
@@ -12,6 +11,7 @@
 <script setup lang="tsx" name="cases">
 import { reactive, ref } from "vue";
 import { useRouter } from "vue-router";
+import { ElLoading } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { getPersonCaseList } from "@/api/modules/operationManagement";
@@ -64,23 +64,36 @@ const dataCallback = (data: any) => ({
   total: data?.total ?? 0
 });
 
-const getTableList = (params: any) => {
-  // 转换参数格式以匹配新接口
-  const newParams: any = {
-    storeId: params.storeId || localGet("createdId"),
-    pageNum: params.page || params.pageNum || 1,
-    pageSize: params.size || params.pageSize || 10
-  };
+const getTableList = async (params: any) => {
+  // 显示全屏loading
+  const loadingInstance = ElLoading.service({
+    lock: true,
+    text: "加载中...",
+    background: "rgba(0, 0, 0, 0.7)"
+  });
 
-  // 上传情况筛选(未上传0 / 已上传1)
-  newParams.hasResult = params.hasResult;
+  try {
+    // 转换参数格式以匹配新接口
+    const newParams: any = {
+      storeId: params.storeId || localGet("createdId"),
+      pageNum: params.page || params.pageNum || 1,
+      pageSize: params.size || params.pageSize || 10
+    };
 
-  // 所属活动(活动名称)筛选
-  if (params.activityName) {
-    newParams.activityName = params.activityName.trim();
-  }
+    // 上传情况筛选(未上传0 / 已上传1)
+    newParams.hasResult = params.hasResult;
+
+    // 所属活动(活动名称)筛选
+    if (params.activityName) {
+      newParams.activityName = params.activityName.trim();
+    }
 
-  return getPersonCaseList(newParams);
+    const result = await getPersonCaseList(newParams);
+    return result;
+  } finally {
+    // 关闭loading
+    loadingInstance.close();
+  }
 };
 
 const toDetail = (row: any) => {

+ 1 - 1
src/views/operationManagement/couponDetail.vue

@@ -43,7 +43,7 @@
           <div class="detail-item">
             <div class="detail-label">有效期</div>
             <div class="detail-value">
-              {{ couponModel.specifiedDay ? `购买后${couponModel.specifiedDay}天` : "--" }}
+              {{ couponModel.specifiedDay ? `${couponModel.specifiedDay}天` : "--" }}
             </div>
           </div>
           <!-- 库存 -->

+ 211 - 93
src/views/operationManagement/newActivity.vue

@@ -38,7 +38,7 @@
             </el-form-item>
 
             <!-- 评论有礼相关字段 -->
-            <template v-if="activityModel.activityType === 1">
+            <template v-if="activityModel.activityType === 2">
               <!-- 用户可参与次数 -->
               <el-form-item label="用户可参与次数" prop="participationLimit">
                 <el-input v-model="activityModel.participationLimit" placeholder="请输入" maxlength="4" />
@@ -71,7 +71,7 @@
             </template>
 
             <!-- 营销活动相关字段 -->
-            <template v-if="activityModel.activityType === 2">
+            <template v-if="activityModel.activityType === 1">
               <!-- 报名时间 -->
               <el-form-item class="activity-time-item" label="报名时间" prop="signupTimeRange">
                 <el-date-picker
@@ -89,7 +89,12 @@
 
               <!-- 活动限制人数 -->
               <el-form-item label="活动限制人数">
-                <el-input v-model="activityModel.activityLimitPeople" placeholder="请输入" maxlength="6" />
+                <el-input
+                  v-model="activityModel.activityLimitPeople"
+                  placeholder="请输入"
+                  maxlength="20"
+                  @input="handlePositiveIntegerInput('activityLimitPeople', $event)"
+                />
               </el-form-item>
 
               <!-- 活动详情 -->
@@ -152,30 +157,30 @@
                     </template>
                   </el-upload>
                 </div>
-                <div class="upload-hint">请上传21:9尺寸图片效果更佳,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
+                <div class="upload-hint">请上传21:9尺寸图片效果更佳,支持jpg、jpeg、png格式,上传图片不得超过5M</div>
               </div>
             </el-form-item>
 
             <!-- 活动详情图 -->
             <el-form-item v-if="activityModel.uploadImgType === 1" label="活动详情图" prop="activityDetailImage">
               <div class="upload-item-wrapper">
-                <div class="upload-area upload-area-vertical" :class="{ 'upload-full': detailFileList.length >= 1 }">
+                <div class="upload-area upload-area-vertical" :class="{ 'upload-full': detailFileList.length >= 9 }">
                   <el-upload
                     v-model:file-list="detailFileList"
                     :accept="'.jpg,.jpeg,.png'"
                     :auto-upload="false"
                     :before-remove="handleBeforeRemove"
                     :disabled="hasUnuploadedImages"
-                    :limit="1"
+                    :limit="9"
                     :on-change="handleDetailUploadChange"
-                    :on-exceed="handleUploadExceed"
+                    :on-exceed="handleDetailUploadExceed"
                     :on-preview="handlePictureCardPreview"
                     :on-remove="handleDetailRemove"
                     :show-file-list="true"
                     list-type="picture-card"
                   >
                     <template #trigger>
-                      <div v-if="detailFileList.length < 1" class="upload-trigger-card el-upload--picture-card">
+                      <div v-if="detailFileList.length < 9" class="upload-trigger-card el-upload--picture-card">
                         <el-icon>
                           <Plus />
                         </el-icon>
@@ -183,7 +188,7 @@
                     </template>
                   </el-upload>
                 </div>
-                <div class="upload-hint">请上传竖版图片,支持jpg、jpeg、png格式,上传图片不得超过20M</div>
+                <div class="upload-hint">请上传竖版图片,支持jpg、jpeg、png格式,最多上传9张,单张图片不得超过5M</div>
               </div>
             </el-form-item>
           </div>
@@ -308,6 +313,17 @@ const rules = reactive({
           return;
         }
         callback();
+        // 活动时间验证通过后,重新验证报名时间(如果已设置)
+        if (
+          activityModel.value.activityType === 1 &&
+          activityModel.value.signupTimeRange &&
+          Array.isArray(activityModel.value.signupTimeRange) &&
+          activityModel.value.signupTimeRange.length === 2
+        ) {
+          nextTick(() => {
+            ruleFormRef.value?.validateField("signupTimeRange");
+          });
+        }
       },
       trigger: "change"
     }
@@ -316,7 +332,7 @@ const rules = reactive({
     { required: true, message: "请输入用户可参与次数", trigger: "blur" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === 2) {
           if (!value) {
             callback(new Error("请输入用户可参与次数"));
             return;
@@ -340,7 +356,7 @@ const rules = reactive({
     { required: true, message: "请选择活动规则", trigger: "change" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === 2) {
           if (!value || !Array.isArray(value) || value.length < 2) {
             callback(new Error("请选择完整的活动规则(至少选择角色和行为)"));
             return;
@@ -355,7 +371,7 @@ const rules = reactive({
     { required: true, message: "请选择优惠券", trigger: "change" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === 2) {
           if (!value) {
             callback(new Error("请选择优惠券"));
             return;
@@ -370,7 +386,7 @@ const rules = reactive({
     { required: true, message: "请输入优惠券发放数量", trigger: "blur" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 1) {
+        if (activityModel.value.activityType === 2) {
           if (!value) {
             callback(new Error("请输入优惠券发放数量"));
             return;
@@ -394,7 +410,7 @@ const rules = reactive({
     { required: true, message: "请选择报名时间", trigger: "change" },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 2) {
+        if (activityModel.value.activityType === 1) {
           if (!value || !Array.isArray(value) || value.length !== 2) {
             callback(new Error("请选择报名时间"));
             return;
@@ -410,6 +426,21 @@ const rules = reactive({
             callback(new Error("报名开始时间必须早于报名结束时间"));
             return;
           }
+          // 检查报名结束时间是否在活动开始时间之前
+          if (
+            activityModel.value.activityTimeRange &&
+            Array.isArray(activityModel.value.activityTimeRange) &&
+            activityModel.value.activityTimeRange.length === 2
+          ) {
+            const [activityStartTime] = activityModel.value.activityTimeRange;
+            if (activityStartTime) {
+              const activityStart = new Date(activityStartTime);
+              if (end >= activityStart) {
+                callback(new Error("报名结束时间必须在活动开始时间之前"));
+                return;
+              }
+            }
+          }
         }
         callback();
       },
@@ -420,7 +451,7 @@ const rules = reactive({
     { required: true, message: "请输入活动详情", trigger: ["blur", "change"] },
     {
       validator: (rule: any, value: any, callback: any) => {
-        if (activityModel.value.activityType === 2) {
+        if (activityModel.value.activityType === 1) {
           if (!value || value.trim() === "") {
             callback(new Error("请输入活动详情"));
             return;
@@ -467,7 +498,9 @@ const rules = reactive({
       required: true,
       validator: (rule: any, value: any, callback: any) => {
         if (activityModel.value.uploadImgType === 1) {
-          if (!detailImageUrl.value) {
+          // 检查是否有成功上传的图片
+          const successFiles = detailFileList.value.filter((f: any) => f.status === "success" && f.url);
+          if (successFiles.length === 0) {
             callback(new Error("请上传活动详情图"));
             return;
           }
@@ -481,8 +514,8 @@ const rules = reactive({
 
 // ==================== 活动信息数据模型 ====================
 const activityModel = ref<any>({
-  // 活动类型:1-评论有礼,2-营销活动
-  activityType: 2,
+  // 活动类型:1-营销活动,2-评论有礼
+  activityType: 1,
   // 活动宣传图(包含标题和详情)
   promotionImages: null,
   // 活动标题图片
@@ -613,15 +646,17 @@ const handleDetailRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) =>
   const file = uploadFile as any;
   const imageUrl = file.url;
 
-  if (imageUrl) {
-    detailImageUrl.value = "";
-    activityModel.value.activityDetailImg = null;
-    activityModel.value.activityDetailImage = null;
-    // 触发表单验证
-    nextTick(() => {
-      ruleFormRef.value?.validateField("activityDetailImage");
-    });
-  }
+  // 更新图片URL列表(移除已删除的图片)
+  const successFiles = uploadFiles.filter((f: any) => f.status === "success" && f.url);
+  const imageUrls = successFiles.map((f: any) => f.url);
+  detailImageUrl.value = imageUrls.length > 0 ? imageUrls.join(",") : "";
+  activityModel.value.activityDetailImg = imageUrls.length > 0 ? { url: imageUrls.join(",") } : null;
+  activityModel.value.activityDetailImage = imageUrls.length > 0 ? imageUrls.join(",") : "";
+
+  // 触发表单验证
+  nextTick(() => {
+    ruleFormRef.value?.validateField("activityDetailImage");
+  });
 
   if (file.url && file.url.startsWith("blob:")) {
     URL.revokeObjectURL(file.url);
@@ -631,13 +666,20 @@ const handleDetailRemove: UploadProps["onRemove"] = (uploadFile, uploadFiles) =>
 };
 
 /**
- * 上传文件超出限制提示
+ * 上传文件超出限制提示(标题图)
  */
 const handleUploadExceed: UploadProps["onExceed"] = () => {
   ElMessage.warning("最多只能上传1张图片");
 };
 
 /**
+ * 活动详情图上传超出限制提示
+ */
+const handleDetailUploadExceed: UploadProps["onExceed"] = () => {
+  ElMessage.warning("最多只能上传9张图片");
+};
+
+/**
  * 活动标题图片上传 - 文件变更
  */
 const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uploadFiles) => {
@@ -668,8 +710,8 @@ const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uplo
       return;
     }
 
-    // 检查文件大小,不得超过20M
-    const maxSize = 20 * 1024 * 1024; // 20MB
+    // 检查文件大小,不得超过5M
+    const maxSize = 5 * 1024 * 1024; // 5MB
     if (uploadFile.raw.size > maxSize) {
       // 从文件列表中移除超过大小的文件
       const index = titleFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
@@ -685,7 +727,7 @@ const handleTitleUploadChange: UploadProps["onChange"] = async (uploadFile, uplo
       if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
         URL.revokeObjectURL(uploadFile.url);
       }
-      ElMessage.warning("上传图片不得超过20M");
+      ElMessage.warning("上传图片不得超过5M");
       return;
     }
   }
@@ -737,8 +779,8 @@ const handleDetailUploadChange: UploadProps["onChange"] = async (uploadFile, upl
       return;
     }
 
-    // 检查文件大小,不得超过20M
-    const maxSize = 20 * 1024 * 1024; // 20MB
+    // 检查文件大小,不得超过5M
+    const maxSize = 5 * 1024 * 1024; // 5MB
     if (uploadFile.raw.size > maxSize) {
       // 从文件列表中移除超过大小的文件
       const index = detailFileList.value.findIndex((f: any) => f.uid === uploadFile.uid);
@@ -754,7 +796,7 @@ const handleDetailUploadChange: UploadProps["onChange"] = async (uploadFile, upl
       if (uploadFile.url && uploadFile.url.startsWith("blob:")) {
         URL.revokeObjectURL(uploadFile.url);
       }
-      ElMessage.warning("上传图片不得超过20M");
+      ElMessage.warning("上传图片不得超过5M");
       return;
     }
   }
@@ -826,9 +868,13 @@ const uploadSingleFile = async (file: UploadFile, uploadType: string) => {
           ruleFormRef.value?.validateField("activityTitleImage");
         });
       } else if (uploadType === "detail") {
-        detailImageUrl.value = imageUrl;
-        activityModel.value.activityDetailImg = { url: imageUrl };
-        activityModel.value.activityDetailImage = imageUrl;
+        // 支持多张图片,将所有成功上传的图片URL组合
+        const successFiles = detailFileList.value.filter((f: any) => f.status === "success" && f.url);
+        const imageUrls = successFiles.map((f: any) => f.url);
+        // 如果有多张图片,用逗号连接;如果只有一张,保持字符串格式
+        detailImageUrl.value = imageUrls.length > 0 ? imageUrls.join(",") : "";
+        activityModel.value.activityDetailImg = imageUrls.length > 0 ? { url: imageUrls.join(",") } : null;
+        activityModel.value.activityDetailImage = imageUrls.length > 0 ? imageUrls.join(",") : "";
         // 触发表单验证
         nextTick(() => {
           ruleFormRef.value?.validateField("activityDetailImage");
@@ -914,13 +960,6 @@ watch(
 
     nextTick(() => {
       if (newVal === 1) {
-        // 切换到评论有礼:清除营销活动相关字段
-        activityModel.value.signupTimeRange = [];
-        activityModel.value.activityLimitPeople = "";
-        activityModel.value.activityDetails = "";
-        // 清除营销活动字段的验证状态
-        ruleFormRef.value?.clearValidate(["signupTimeRange", "activityLimitPeople", "activityDetails"]);
-      } else if (newVal === 2) {
         // 切换到营销活动:清除评论有礼相关字段
         activityModel.value.participationLimit = "";
         activityModel.value.activityRule = [];
@@ -928,6 +967,13 @@ watch(
         activityModel.value.couponQuantity = "";
         // 清除评论有礼字段的验证状态
         ruleFormRef.value?.clearValidate(["participationLimit", "activityRule", "couponId", "couponQuantity"]);
+      } else if (newVal === 2) {
+        // 切换到评论有礼:清除营销活动相关字段
+        activityModel.value.signupTimeRange = [];
+        activityModel.value.activityLimitPeople = "";
+        activityModel.value.activityDetails = "";
+        // 清除营销活动字段的验证状态
+        ruleFormRef.value?.clearValidate(["signupTimeRange", "activityLimitPeople", "activityDetails"]);
       }
     });
   },
@@ -969,6 +1015,26 @@ watch(
   }
 );
 
+/**
+ * 监听活动时间变化,重新验证报名时间
+ */
+watch(
+  () => activityModel.value.activityTimeRange,
+  () => {
+    // 当活动时间改变时,如果已设置报名时间,重新验证报名时间
+    if (
+      activityModel.value.activityType === 1 &&
+      activityModel.value.signupTimeRange &&
+      Array.isArray(activityModel.value.signupTimeRange) &&
+      activityModel.value.signupTimeRange.length === 2
+    ) {
+      nextTick(() => {
+        ruleFormRef.value?.validateField("signupTimeRange");
+      });
+    }
+  }
+);
+
 // ==================== 生命周期钩子 ====================
 
 /**
@@ -1013,32 +1079,63 @@ onMounted(async () => {
     try {
       const res: any = await getActivityDetail({ id: id.value });
       if (res && res.code == 200) {
-        activityModel.value = { ...activityModel.value, ...res.data };
+        // 先设置 activityType 为数字类型,确保下拉框正确显示
+        if (res.data.activityType !== undefined) {
+          activityModel.value.activityType = Number(res.data.activityType);
+        }
+
+        // 合并其他数据
+        activityModel.value.activityName = res.data.activityName || "";
+
         // 处理活动时间范围
         if (res.data.startTime && res.data.endTime) {
-          activityModel.value.activityTimeRange = [res.data.startTime, res.data.endTime];
-        }
-        // 加载活动规则(评论有礼)
-        if (res.data.activityRule) {
-          activityModel.value.activityRule = res.data.activityRule.split(",");
-        } else {
-          activityModel.value.activityRule = [];
+          // 只取日期部分(去掉时分秒)
+          const startDate = res.data.startTime.split(" ")[0];
+          const endDate = res.data.endTime.split(" ")[0];
+          activityModel.value.activityTimeRange = [startDate, endDate];
         }
-        // 加载报名开始时间(营销活动)
-        if (res.data.signupStartTime) {
-          activityModel.value.signupStartTime = res.data.signupStartTime;
-        }
-        // 加载报名结束时间(营销活动)
-        if (res.data.signupEndTime) {
-          activityModel.value.signupEndTime = res.data.signupEndTime;
-        }
-        // 加载活动限制人数(营销活动)
-        if (res.data.activityLimitPeople !== undefined) {
-          activityModel.value.activityLimitPeople = res.data.activityLimitPeople;
-        }
-        // 加载活动详情(营销活动)
-        if (res.data.activityDetails) {
-          activityModel.value.activityDetails = res.data.activityDetails;
+
+        // 根据活动类型加载对应字段
+        if (activityModel.value.activityType === 1) {
+          // 营销活动:加载报名时间、活动限制人数、活动详情
+          if (res.data.signupStartTime && res.data.signupEndTime) {
+            // 只取日期部分(去掉时分秒)
+            const signupStartDate = res.data.signupStartTime.split(" ")[0];
+            const signupEndDate = res.data.signupEndTime.split(" ")[0];
+            activityModel.value.signupTimeRange = [signupStartDate, signupEndDate];
+          } else {
+            activityModel.value.signupTimeRange = [];
+          }
+          // 加载活动限制人数
+          if (res.data.activityLimitPeople !== undefined && res.data.activityLimitPeople !== null) {
+            activityModel.value.activityLimitPeople = String(res.data.activityLimitPeople);
+          } else {
+            activityModel.value.activityLimitPeople = "";
+          }
+          // 加载活动详情
+          activityModel.value.activityDetails = res.data.activityDetails || "";
+        } else if (activityModel.value.activityType === 2) {
+          // 评论有礼:加载活动规则、优惠券、优惠券发放数量、用户可参与次数
+          // 加载活动规则
+          if (res.data.activityRule) {
+            activityModel.value.activityRule = res.data.activityRule.split(",");
+          } else {
+            activityModel.value.activityRule = [];
+          }
+          // 加载优惠券ID
+          activityModel.value.couponId = res.data.couponId || "";
+          // 加载优惠券发放数量
+          if (res.data.couponQuantity !== undefined && res.data.couponQuantity !== null) {
+            activityModel.value.couponQuantity = String(res.data.couponQuantity);
+          } else {
+            activityModel.value.couponQuantity = "";
+          }
+          // 加载用户可参与次数
+          if (res.data.participationLimit !== undefined && res.data.participationLimit !== null) {
+            activityModel.value.participationLimit = String(res.data.participationLimit);
+          } else {
+            activityModel.value.participationLimit = "";
+          }
         }
         // 加载上传图片方式
         if (res.data.uploadImgType !== undefined) {
@@ -1065,15 +1162,22 @@ onMounted(async () => {
               titleFileList.value = [titleFile];
             }
           }
-          // 如果有详情图片,添加到文件列表
+          // 如果有详情图片,添加到文件列表(支持多张图片,可能是字符串或数组)
           if (res.data.activityDetailImgUrl) {
-            const detailImgUrl = res.data.activityDetailImgUrl;
-            if (detailImgUrl) {
-              detailImageUrl.value = detailImgUrl;
+            let detailImgUrls: string[] = [];
+            // 如果是字符串,可能是逗号分隔的多张图片
+            if (typeof res.data.activityDetailImgUrl === "string") {
+              detailImgUrls = res.data.activityDetailImgUrl.split(",").filter((url: string) => url.trim());
+            } else if (Array.isArray(res.data.activityDetailImgUrl)) {
+              detailImgUrls = res.data.activityDetailImgUrl.filter((url: any) => url);
+            }
+
+            if (detailImgUrls.length > 0) {
+              detailImageUrl.value = detailImgUrls.join(",");
               activityModel.value.activityDetailImg = res.data.activityDetailImgUrl;
-              activityModel.value.activityDetailImage = detailImgUrl;
-              const detailFile = handleImageParam(detailImgUrl);
-              detailFileList.value = [detailFile];
+              activityModel.value.activityDetailImage = detailImgUrls.join(",");
+              // 将多张图片添加到文件列表
+              detailFileList.value = detailImgUrls.map((url: string) => handleImageParam(url));
             }
           }
         }
@@ -1090,6 +1194,24 @@ onMounted(async () => {
 // ==================== 事件处理函数 ====================
 
 /**
+ * 处理正整数输入(只允许输入正整数)
+ */
+const handlePositiveIntegerInput = (field: string, value: string) => {
+  // 移除所有非数字字符
+  let filteredValue = value.replace(/[^\d]/g, "");
+
+  // 限制最大长度为20
+  if (filteredValue.length > 20) {
+    filteredValue = filteredValue.substring(0, 20);
+  }
+
+  // 更新对应字段的值
+  if (field === "activityLimitPeople") {
+    activityModel.value.activityLimitPeople = filteredValue;
+  }
+};
+
+/**
  * 返回上一页
  */
 const goBack = () => {
@@ -1129,17 +1251,6 @@ const handleSubmit = async () => {
 
       // 根据活动类型添加不同的字段,确保只提交对应类型的字段
       if (activityModel.value.activityType === 1) {
-        // 评论有礼:只添加评论有礼相关字段
-        params.participationLimit = activityModel.value.participationLimit;
-        params.activityRule = activityModel.value.activityRule.join(",");
-        params.couponId = activityModel.value.couponId;
-        params.couponQuantity = activityModel.value.couponQuantity;
-        // 确保不包含营销活动的字段
-        delete params.signupStartTime;
-        delete params.signupEndTime;
-        delete params.activityLimitPeople;
-        delete params.activityDetails;
-      } else if (activityModel.value.activityType === 2) {
         // 营销活动:只添加营销活动相关字段
         const [signupStartTime, signupEndTime] = activityModel.value.signupTimeRange || [];
         params.signupStartTime = signupStartTime;
@@ -1151,6 +1262,17 @@ const handleSubmit = async () => {
         delete params.activityRule;
         delete params.couponId;
         delete params.couponQuantity;
+      } else if (activityModel.value.activityType === 2) {
+        // 评论有礼:只添加评论有礼相关字段
+        params.participationLimit = activityModel.value.participationLimit;
+        params.activityRule = activityModel.value.activityRule.join(",");
+        params.couponId = activityModel.value.couponId;
+        params.couponQuantity = activityModel.value.couponQuantity;
+        // 确保不包含营销活动的字段
+        delete params.signupStartTime;
+        delete params.signupEndTime;
+        delete params.activityLimitPeople;
+        delete params.activityDetails;
       }
 
       // 根据上传图片方式设置不同的参数
@@ -1330,18 +1452,14 @@ const handleSubmit = async () => {
 
   // 竖版图片样式
   &.upload-area-vertical {
-    max-width: 300px;
     :deep(.el-upload--picture-card) {
-      width: 100%;
-      height: 400px;
-      aspect-ratio: 3 / 4;
+      width: 150px;
+      height: 150px;
     }
     :deep(.el-upload-list--picture-card) {
-      width: 100%;
       .el-upload-list__item {
-        width: 100%;
-        height: 400px;
-        aspect-ratio: 3 / 4;
+        width: 150px;
+        height: 150px;
         margin: 0;
       }
     }

+ 39 - 27
src/views/operationManagement/personnel.vue

@@ -1,7 +1,6 @@
 <template>
   <div class="table-box button-table">
-    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam"
-      :data-callback="dataCallback">
+    <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>
         <!-- 待审核状态显示通过和拒绝按钮 -->
@@ -17,7 +16,7 @@
 <script setup lang="tsx" name="personnel">
 import { reactive, ref } from "vue";
 import { useRouter } from "vue-router";
-import { ElMessage, ElMessageBox } from "element-plus";
+import { ElMessage, ElMessageBox, ElLoading } from "element-plus";
 import ProTable from "@/components/ProTable/index.vue";
 import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
 import { getPersonnelList, approvePersonnel, rejectPersonnel } from "@/api/modules/operationManagement";
@@ -45,7 +44,7 @@ const columns = reactive<ColumnProps<any>[]>([
       el: "input",
       props: { placeholder: "请输入活动名称" },
       order: 2
-    },
+    }
   },
   {
     prop: "status",
@@ -74,30 +73,43 @@ const dataCallback = (data: any) => ({
   total: data?.total ?? 0
 });
 
-const getTableList = (params: any) => {
-  // 转换参数格式以匹配新接口
-  const newParams: any = {
-    storeId: params.storeId || localGet("createdId"),
-    pageNum: params.page || params.pageNum || 1,
-    pageSize: params.size || params.pageSize || 10
-  };
-
-  // 如果有状态筛选,添加状态参数(注意:搜索字段名是 status)
-  if (params.status) {
-    newParams.status = params.status;
-  }
+const getTableList = async (params: any) => {
+  // 显示全屏loading
+  const loadingInstance = ElLoading.service({
+    lock: true,
+    text: "加载中...",
+    background: "rgba(0, 0, 0, 0.7)"
+  });
 
-  // 如果有活动类型筛选,添加活动类型参数
-  if (params.activityType) {
-    newParams.activityType = params.activityType;
-  }
+  try {
+    // 转换参数格式以匹配新接口
+    const newParams: any = {
+      storeId: params.storeId || localGet("createdId"),
+      pageNum: params.page || params.pageNum || 1,
+      pageSize: params.size || params.pageSize || 10
+    };
 
-  // 如果有活动名称筛选,添加活动名称参数
-  if (params.activityName) {
-    newParams.activityName = params.activityName;
-  }
+    // 如果有状态筛选,添加状态参数(注意:搜索字段名是 status)
+    if (params.status) {
+      newParams.status = params.status;
+    }
 
-  return getPersonnelList(newParams);
+    // 如果有活动类型筛选,添加活动类型参数
+    if (params.activityType) {
+      newParams.activityType = params.activityType;
+    }
+
+    // 如果有活动名称筛选,添加活动名称参数
+    if (params.activityName) {
+      newParams.activityName = params.activityName;
+    }
+
+    const result = await getPersonnelList(newParams);
+    return result;
+  } finally {
+    // 关闭loading
+    loadingInstance.close();
+  }
 };
 
 const toDetail = (row: any) => {
@@ -141,13 +153,13 @@ const handleReject = async (row: any) => {
         return true;
       }
     });
-    
+
     // 再次验证拒绝原因(防止用户绕过验证)
     if (!rejectReason || rejectReason.trim() === "") {
       ElMessage.warning("拒绝原因不能为空");
       return;
     }
-    
+
     const res: any = await rejectPersonnel({
       id: Number(row.id),
       rejectReason: rejectReason.trim()

+ 1091 - 0
src/views/priceList/edit.vue

@@ -0,0 +1,1091 @@
+<template>
+  <div class="price-edit-page">
+    <!-- 顶部:标题 + 关闭 -->
+    <div class="page-header">
+      <div class="header-left" @click="goBack">
+        <el-icon class="back-icon">
+          <ArrowLeft />
+        </el-icon>
+        <span class="back-text">返回</span>
+      </div>
+      <h1 class="page-title">
+        {{ viewMode ? "详情" : id ? "编辑" : "新建" }}
+      </h1>
+      <el-button v-if="viewMode" type="primary" @click="goEdit"> 编辑 </el-button>
+      <div v-else class="header-right" @click="goBack">
+        <el-icon class="close-icon">
+          <Close />
+        </el-icon>
+      </div>
+    </div>
+
+    <div class="page-content">
+      <el-form
+        ref="ruleFormRef"
+        :model="formModel"
+        :rules="formRules"
+        label-width="120px"
+        class="price-form two-column-form"
+        @submit.prevent
+      >
+        <!-- 左列 -->
+        <div class="form-column form-column-left">
+          <!-- 类型(仅美食类别显示菜品/套餐) -->
+          <el-form-item v-if="isFoodModule" label="类型" prop="formType" required>
+            <el-radio-group v-model="formModel.formType" :disabled="viewMode">
+              <el-radio value="dish"> 菜品 </el-radio>
+              <el-radio value="package"> 套餐 </el-radio>
+            </el-radio-group>
+          </el-form-item>
+
+          <!-- 图片 -->
+          <el-form-item label="图片" prop="images" required>
+            <div class="image-upload-wrap">
+              <UploadImgs
+                v-model:file-list="imageFileList"
+                :api="uploadImgStore"
+                :limit="9"
+                :file-size="5"
+                :disabled="viewMode"
+                class="price-list-upload"
+                @update:file-list="onImageListChange"
+              />
+              <div class="image-tip">({{ (formModel.images || "").split(",").filter(Boolean).length }}/9)</div>
+            </div>
+          </el-form-item>
+
+          <!-- 名称:美食为菜品/套餐名称,非美食为名称 -->
+          <el-form-item
+            :label="isFoodModule ? (formModel.formType === 'dish' ? '菜品名称' : '套餐名称') : '名称'"
+            prop="name"
+            required
+          >
+            <el-input
+              v-model="formModel.name"
+              placeholder="请输入"
+              maxlength="50"
+              show-word-limit
+              clearable
+              :disabled="viewMode"
+              class="form-input"
+            />
+          </el-form-item>
+
+          <!-- 价格 -->
+          <el-form-item label="价格(¥)" prop="totalPrice" required>
+            <el-input
+              v-model="formModel.totalPrice"
+              placeholder="请输入"
+              maxlength="15"
+              clearable
+              :disabled="viewMode"
+              class="form-input price-input"
+            />
+          </el-form-item>
+
+          <!-- 以下仅美食类别显示:菜品原料 / 价目表内容 -->
+          <template v-if="isFoodModule">
+            <!-- 菜品原料(仅美食-菜品) -->
+            <div v-if="formModel.formType === 'dish'" class="form-block">
+              <div class="block-head">
+                <span class="block-title">菜品原料</span>
+                <el-link v-if="!viewMode" type="primary" class="btn-add" @click="addIngredient"> 添加项目 </el-link>
+                <span v-if="!viewMode" class="block-tip">默认显示一个</span>
+              </div>
+              <div v-if="!formModel.foodIngredients.length" class="empty-tip">暂无原料,请添加项目</div>
+              <div v-for="(item, idx) in formModel.foodIngredients" :key="'ing-' + idx" class="block-item ingredient-item">
+                <el-form-item label="原料名称" required>
+                  <el-input
+                    v-model="item.ingredientName"
+                    placeholder="请输入"
+                    clearable
+                    :disabled="viewMode"
+                    class="form-input"
+                  />
+                </el-form-item>
+                <el-form-item label="所需重量(g)" required>
+                  <el-input v-model="item.weight" placeholder="请输入" clearable :disabled="viewMode" class="form-input" />
+                </el-form-item>
+                <el-form-item label="成本价(¥)" required>
+                  <el-input v-model="item.costPrice" placeholder="请输入" clearable :disabled="viewMode" class="form-input" />
+                </el-form-item>
+                <el-link v-if="!viewMode" type="primary" class="link-delete" @click="removeIngredient(idx)"> 删除 </el-link>
+              </div>
+              <el-link v-if="!viewMode" type="primary" class="btn-recommend" @click="calcRecommendedPrice"> 推荐价格 </el-link>
+            </div>
+
+            <!-- 价目表内容(仅套餐) -->
+            <div v-if="formModel.formType === 'package'" class="form-block">
+              <div class="block-head">
+                <span class="block-title">价目表内容</span>
+                <el-link v-if="!viewMode" type="primary" class="btn-add" @click="addPriceListGroup"> 添加项目 </el-link>
+                <span v-if="!viewMode" class="block-tip">默认显示一个</span>
+              </div>
+              <div
+                v-for="(group, gIdx) in formModel.foodPackageCategories"
+                :key="'group-' + gIdx"
+                class="block-item price-list-group"
+              >
+                <el-form-item label="类别" required>
+                  <el-input v-model="group.category" placeholder="请输入" clearable :disabled="viewMode" class="form-input" />
+                </el-form-item>
+                <template v-for="(dish, dIdx) in group.dishes" :key="'d-' + gIdx + '-' + dIdx">
+                  <div class="dish-row">
+                    <el-form-item label="菜品名称" required>
+                      <el-select
+                        v-model="dish.itemId"
+                        placeholder="请选择"
+                        filterable
+                        clearable
+                        :disabled="viewMode"
+                        class="form-input dish-select"
+                        @change="onPackageDishSelect(gIdx, dIdx, $event)"
+                      >
+                        <el-option v-for="opt in dishOptions" :key="opt.id" :label="opt.name" :value="String(opt.id)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="数量" required>
+                      <el-input
+                        v-model="dish.quantity"
+                        placeholder="请输入"
+                        clearable
+                        :disabled="viewMode"
+                        class="form-input qty-input"
+                      />
+                    </el-form-item>
+                    <el-form-item label="单位" required>
+                      <el-select
+                        v-model="dish.unit"
+                        placeholder="请选择单位"
+                        clearable
+                        :disabled="viewMode"
+                        class="form-input unit-select package-unit-select"
+                        style="width: 100%; min-width: 120px"
+                      >
+                        <el-option v-for="u in UNIT_OPTIONS" :key="u" :label="u" :value="u" />
+                      </el-select>
+                    </el-form-item>
+                    <el-link v-if="!viewMode" type="primary" class="link-delete" @click="removePackageDish(gIdx, dIdx)">
+                      删除菜品
+                    </el-link>
+                  </div>
+                </template>
+                <div v-if="!viewMode" class="group-actions">
+                  <el-link type="primary" class="btn-add-dish" @click="addPackageDish(gIdx)"> 添加菜品 </el-link>
+                  <el-link type="primary" class="link-delete" @click="removePriceListGroup(gIdx)"> 删除项目 </el-link>
+                </div>
+              </div>
+            </div>
+          </template>
+
+          <!-- 休闲娱乐/生活服务:服务项目(添加项目) -->
+          <template v-if="!isFoodModule">
+            <div class="form-block">
+              <div class="block-head">
+                <span class="block-title">服务项目</span>
+                <el-link v-if="!viewMode" type="primary" class="btn-add" @click="addGeneralServiceItem"> 添加项目 </el-link>
+                <span v-if="!viewMode" class="block-tip">默认显示一个</span>
+              </div>
+              <div v-if="!formModel.generalServiceItems.length" class="empty-tip">暂无服务项目,请添加项目</div>
+              <div v-for="(item, idx) in formModel.generalServiceItems" :key="'svc-' + idx" class="block-item service-item">
+                <el-form-item label="服务名称" required>
+                  <el-input v-model="item.serviceName" placeholder="请输入" clearable :disabled="viewMode" class="form-input" />
+                </el-form-item>
+                <el-form-item label="数量" required>
+                  <el-input
+                    v-model="item.quantity"
+                    placeholder="请输入"
+                    clearable
+                    :disabled="viewMode"
+                    class="form-input qty-input"
+                  />
+                </el-form-item>
+                <el-form-item label="单位" required>
+                  <el-select
+                    v-model="item.unit"
+                    placeholder="请选择单位"
+                    clearable
+                    :disabled="viewMode"
+                    class="form-input unit-select service-unit-select"
+                    style="width: 100%; min-width: 120px"
+                  >
+                    <el-option v-for="u in GENERAL_SERVICE_UNIT_OPTIONS" :key="u" :label="u" :value="u" />
+                  </el-select>
+                </el-form-item>
+                <el-form-item label="服务说明">
+                  <el-input
+                    v-model="item.description"
+                    type="textarea"
+                    :rows="2"
+                    placeholder="请输入"
+                    width="100%"
+                    maxlength="200"
+                    show-word-limit
+                    resize="none"
+                    :disabled="viewMode"
+                    class="form-textarea"
+                  />
+                </el-form-item>
+                <el-link v-if="!viewMode" type="primary" class="link-delete" @click="removeGeneralServiceItem(idx)">
+                  删除
+                </el-link>
+              </div>
+            </div>
+          </template>
+
+          <!-- 图文详情图片 -->
+          <el-form-item label="图文详情图片">
+            <div class="image-upload-wrap">
+              <UploadImgs
+                v-model:file-list="detailImageFileList"
+                :api="uploadImgStore"
+                :limit="9"
+                :file-size="5"
+                :disabled="viewMode"
+                class="price-list-upload"
+                @update:file-list="onDetailImageListChange"
+              />
+              <div class="image-tip">({{ (formModel.imageContent || "").split(",").filter(Boolean).length }}/9)</div>
+            </div>
+          </el-form-item>
+
+          <!-- 图文详情描述 -->
+          <el-form-item label="图文详情描述">
+            <el-input
+              v-model="formModel.detailContent"
+              type="textarea"
+              :rows="4"
+              placeholder="请输入"
+              maxlength="500"
+              show-word-limit
+              resize="none"
+              :disabled="viewMode"
+              class="form-textarea"
+            />
+          </el-form-item>
+        </div>
+
+        <!-- 右列 -->
+        <div class="form-column form-column-right">
+          <el-form-item label="补充说明">
+            <el-input
+              v-model="formModel.extraNote"
+              type="textarea"
+              :rows="4"
+              placeholder="请输入"
+              maxlength="300"
+              show-word-limit
+              resize="none"
+              :disabled="viewMode"
+              class="form-textarea"
+            />
+          </el-form-item>
+          <el-form-item label="预约">
+            <el-radio-group v-model="formModel.needReserve" :disabled="viewMode">
+              <el-radio :value="0"> 无需预约 </el-radio>
+              <el-radio :value="1"> 需要预约 </el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item label="预约规则">
+            <el-input
+              v-model="formModel.reserveRule"
+              type="textarea"
+              :rows="3"
+              placeholder="请输入"
+              maxlength="200"
+              show-word-limit
+              resize="none"
+              :disabled="viewMode"
+              class="form-textarea"
+            />
+          </el-form-item>
+          <el-form-item label="适用人数">
+            <el-input v-model="formModel.peopleLimit" placeholder="请输入" clearable :disabled="viewMode" class="form-input" />
+          </el-form-item>
+          <el-form-item label="使用规则">
+            <el-input
+              v-model="formModel.usageRule"
+              type="textarea"
+              :rows="4"
+              placeholder="请输入"
+              maxlength="300"
+              show-word-limit
+              resize="none"
+              :disabled="viewMode"
+              class="form-textarea"
+            />
+          </el-form-item>
+          <!-- 详情模式:提交时间、审核状态、审核时间、拒绝原因、在线状态 -->
+          <template v-if="viewMode">
+            <el-form-item label="提交时间">
+              <span class="view-only-value">{{ formModel.createTime || "--" }}</span>
+            </el-form-item>
+            <el-form-item label="审核状态">
+              <span :class="['view-only-value', 'audit-status', 'audit-status--' + (formModel.auditStatus ?? '')]">
+                {{ auditStatusText(formModel.auditStatus) }}
+              </span>
+            </el-form-item>
+            <el-form-item label="审核时间">
+              <span class="view-only-value">{{ formModel.auditTime || "--" }}</span>
+            </el-form-item>
+            <el-form-item label="拒绝原因">
+              <span class="view-only-value">{{ formModel.rejectionReason || "无" }}</span>
+            </el-form-item>
+            <el-form-item label="在线状态">
+              <span class="view-only-value">{{ shelfStatusText(formModel.shelfStatus) }}</span>
+            </el-form-item>
+          </template>
+        </div>
+      </el-form>
+
+      <!-- 底部操作:详情模式仅显示返回+编辑,编辑模式显示返回+确定 -->
+      <div class="page-footer">
+        <div class="footer-inner">
+          <el-button @click="goBack" size="large"> 返回 </el-button>
+          <template v-if="viewMode">
+            <el-button type="primary" size="large" @click="goEdit"> 编辑 </el-button>
+          </template>
+          <template v-else>
+            <el-button type="primary" size="large" :loading="submitting" @click="handleSubmit"> 确定 </el-button>
+          </template>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="priceListEdit">
+import { ref, reactive, computed, onMounted, watch } from "vue";
+import { ElMessage } from "element-plus";
+import { useRoute, useRouter } from "vue-router";
+import type { FormInstance, UploadUserFile } from "element-plus";
+import { ArrowLeft, Close } from "@element-plus/icons-vue";
+import {
+  getPriceListDetail,
+  addPriceItem,
+  updatePriceItem,
+  getCuisineSingleNameList,
+  CUISINE_TYPE,
+  getModuleTypeByBusinessSection,
+  MODULE_TYPES
+} from "@/api/modules/priceList";
+import { localGet } from "@/utils";
+import UploadImgs from "@/components/Upload/Imgs.vue";
+import { uploadImgStore } from "@/api/modules/upload";
+
+const router = useRouter();
+const route = useRoute();
+const ruleFormRef = ref<FormInstance>();
+
+const id = ref<string>("");
+const businessSection = ref<string>("1");
+const cuisineType = ref<number | undefined>(undefined);
+const submitting = ref(false);
+const dishOptions = ref<{ id: number | string; name: string }[]>([]);
+
+// 详情模式(从列表点「详情」进入,同一编辑页只读展示)
+const viewMode = computed(() => route.query.mode === "detail");
+
+const UNIT_OPTIONS = ["份", "人", "元", "个", "瓶", "杯", "碗", "盘", "只", "斤", "两", "克", "千克"];
+
+// 通用套餐-服务项目单位(休闲娱乐、生活服务,与商家端一致)
+const GENERAL_SERVICE_UNIT_OPTIONS = [
+  "份",
+  "次",
+  "人",
+  "个",
+  "瓶",
+  "杯",
+  "碗",
+  "盘",
+  "只",
+  "斤",
+  "两",
+  "克",
+  "千克",
+  "天",
+  "小时"
+];
+
+// 表单数据
+const formModel = reactive({
+  formType: "dish" as "dish" | "package",
+  name: "",
+  totalPrice: "",
+  images: "",
+  imageContent: "",
+  detailContent: "",
+  extraNote: "",
+  needReserve: 0 as 0 | 1,
+  reserveRule: "",
+  peopleLimit: "",
+  usageRule: "",
+  // 详情只读字段(详情模式显示)
+  createTime: "",
+  auditTime: "",
+  auditStatus: null as number | null,
+  rejectionReason: "",
+  shelfStatus: null as number | null,
+  // 菜品原料(菜品模式)
+  foodIngredients: [] as { ingredientName: string; weight: string; costPrice: string }[],
+  // 价目表内容(套餐模式)
+  foodPackageCategories: [] as {
+    category: string;
+    dishes: { itemId: string; dishName: string; quantity: string; unit: string }[];
+  }[],
+  // 通用套餐-服务项目(休闲娱乐、生活服务)
+  generalServiceItems: [] as {
+    serviceName: string;
+    quantity: string;
+    unit: string;
+    description: string;
+  }[]
+});
+
+const unitOptions = ref<string[]>(UNIT_OPTIONS);
+const imageFileList = ref<UploadUserFile[]>([]);
+const detailImageFileList = ref<UploadUserFile[]>([]);
+
+// 仅美食类别(经营板块为 1)才显示菜品/套餐分类
+const isFoodModule = computed(() => getModuleTypeByBusinessSection(businessSection.value) === MODULE_TYPES.FOOD);
+
+const formRules = reactive({
+  name: [{ required: true, message: "请输入名称", trigger: "blur" }],
+  totalPrice: [
+    { required: true, message: "请输入价格", trigger: "blur" },
+    {
+      pattern: /^(?:\d+|\d+\.\d{1,2})$/,
+      message: "请输入有效价格(最多两位小数)",
+      trigger: "blur"
+    }
+  ]
+});
+
+// 切换类型时初始化对应块
+watch(
+  () => formModel.formType,
+  val => {
+    if (val === "dish" && !formModel.foodIngredients.length) {
+      formModel.foodIngredients = [{ ingredientName: "", weight: "", costPrice: "" }];
+    }
+    if (val === "package" && !formModel.foodPackageCategories.length) {
+      formModel.foodPackageCategories = [{ category: "", dishes: [{ itemId: "", dishName: "", quantity: "1", unit: "份" }] }];
+    }
+  },
+  { immediate: false }
+);
+
+function addIngredient() {
+  formModel.foodIngredients.push({ ingredientName: "", weight: "", costPrice: "" });
+}
+
+function removeIngredient(idx: number) {
+  formModel.foodIngredients.splice(idx, 1);
+}
+
+function addPriceListGroup() {
+  formModel.foodPackageCategories.push({
+    category: "",
+    dishes: [{ itemId: "", dishName: "", quantity: "1", unit: "份" }]
+  });
+}
+
+function removePriceListGroup(gIdx: number) {
+  formModel.foodPackageCategories.splice(gIdx, 1);
+}
+
+function addPackageDish(gIdx: number) {
+  formModel.foodPackageCategories[gIdx].dishes.push({
+    itemId: "",
+    dishName: "",
+    quantity: "1",
+    unit: "份"
+  });
+}
+
+function removePackageDish(gIdx: number, dIdx: number) {
+  formModel.foodPackageCategories[gIdx].dishes.splice(dIdx, 1);
+}
+
+function onPackageDishSelect(gIdx: number, dIdx: number, itemId: string) {
+  const opt = dishOptions.value.find(o => String(o.id) === itemId);
+  if (opt) formModel.foodPackageCategories[gIdx].dishes[dIdx].dishName = opt.name;
+}
+
+function normalizeServiceUnit(unit: string | undefined): string {
+  const u = (unit || "份").trim();
+  return GENERAL_SERVICE_UNIT_OPTIONS.includes(u) ? u : "份";
+}
+
+function normalizePackageUnit(unit: string | undefined): string {
+  const u = (unit || "份").trim();
+  return UNIT_OPTIONS.includes(u) ? u : "份";
+}
+
+function addGeneralServiceItem() {
+  formModel.generalServiceItems.push({
+    serviceName: "",
+    quantity: "",
+    unit: "份",
+    description: ""
+  });
+}
+
+function removeGeneralServiceItem(idx: number) {
+  formModel.generalServiceItems.splice(idx, 1);
+}
+
+function calcRecommendedPrice() {
+  const total = formModel.foodIngredients.reduce((sum, it) => sum + (parseFloat(it.costPrice) || 0), 0) * 1.2;
+  if (total > 0) {
+    formModel.totalPrice = total.toFixed(2);
+    ElMessage.success("已根据原料成本计算推荐价格(约1.2倍)");
+  } else {
+    ElMessage.warning("请先填写原料成本");
+  }
+}
+
+function onImageListChange(list: UploadUserFile[]) {
+  const urls = list.map(f => (typeof f.url === "string" ? f.url : "")).filter(Boolean);
+  formModel.images = urls.join(",");
+}
+
+function onDetailImageListChange(list: UploadUserFile[]) {
+  const urls = list.map(f => (typeof f.url === "string" ? f.url : "")).filter(Boolean);
+  formModel.imageContent = urls.join(",");
+}
+
+function syncImageFileListFromModel() {
+  const str = formModel.images || "";
+  const urls = str ? str.split(",").filter(Boolean) : [];
+  imageFileList.value = urls.map((url, i) => ({ uid: i, name: `img-${i}`, url }));
+  const detailStr = formModel.imageContent || "";
+  const detailUrls = detailStr ? detailStr.split(",").filter(Boolean) : [];
+  detailImageFileList.value = detailUrls.map((url, i) => ({ uid: 1000 + i, name: `detail-${i}`, url }));
+}
+
+// 详情接口返回转表单(美食接口返回 data.data.data 结构,cuisineType 1=菜品 2=套餐;非美食仅通用字段)
+function applyDetailToForm(data: any) {
+  const d = data?.data?.data ?? data?.data ?? data;
+  if (!d) return;
+  const isFood = getModuleTypeByBusinessSection(businessSection.value) === MODULE_TYPES.FOOD;
+  formModel.name = d.name ?? "";
+  formModel.totalPrice = d.totalPrice !== undefined && d.totalPrice !== null ? String(d.totalPrice) : "";
+  formModel.images = d.images ? String(d.images).trim() : "";
+  formModel.imageContent = d.imageContent ? String(d.imageContent).trim() : "";
+  formModel.detailContent = d.detailContent ?? "";
+  formModel.extraNote = d.extraNote ?? "";
+  formModel.needReserve = d.needReserve === 1 ? 1 : 0;
+  formModel.reserveRule = d.reserveRule ?? "";
+  formModel.peopleLimit = d.peopleLimit != null ? String(d.peopleLimit) : "";
+  formModel.usageRule = d.usageRule ?? "";
+  formModel.createTime = d.createTime ?? d.submitTime ?? "";
+  formModel.auditTime = d.auditTime ?? d.updateTime ?? "";
+  formModel.auditStatus = d.status !== undefined && d.status !== null ? d.status : d.auditStatus != null ? d.auditStatus : null;
+  formModel.rejectionReason = d.rejectionReason ?? d.rejectReason ?? "";
+  formModel.shelfStatus = typeof d.shelfStatus === "number" ? d.shelfStatus : d.shelfStatus != null ? d.shelfStatus : null;
+  if (!isFood) {
+    try {
+      const serviceJson = d.serviceJson || "[]";
+      const items = JSON.parse(serviceJson);
+      formModel.generalServiceItems = Array.isArray(items)
+        ? items.map((it: any) => ({
+            serviceName: it.name ?? "",
+            quantity: it.num != null ? String(it.num) : "",
+            unit: normalizeServiceUnit(it.unit),
+            description: it.details ?? ""
+          }))
+        : [];
+    } catch {
+      formModel.generalServiceItems = [];
+    }
+    if (!formModel.generalServiceItems.length) {
+      formModel.generalServiceItems = [{ serviceName: "", quantity: "", unit: "份", description: "" }];
+    }
+    syncImageFileListFromModel();
+    return;
+  }
+  const type = d.cuisineType;
+  if (type === CUISINE_TYPE.SINGLE) {
+    formModel.formType = "dish";
+    try {
+      const raw = d.rawJson ? JSON.parse(d.rawJson) : [];
+      formModel.foodIngredients = Array.isArray(raw)
+        ? raw.map((it: any) => ({
+            ingredientName: it.name ?? "",
+            weight: it.height ?? "",
+            costPrice: it.cost != null ? String(it.cost) : ""
+          }))
+        : [{ ingredientName: "", weight: "", costPrice: "" }];
+    } catch {
+      formModel.foodIngredients = [{ ingredientName: "", weight: "", costPrice: "" }];
+    }
+    if (!formModel.foodIngredients.length) formModel.foodIngredients = [{ ingredientName: "", weight: "", costPrice: "" }];
+  } else {
+    formModel.formType = "package";
+    try {
+      const groups = d.groupJson ? JSON.parse(d.groupJson) : [];
+      formModel.foodPackageCategories = Array.isArray(groups)
+        ? groups.map((g: any) => ({
+            category: g.categoryName ?? "",
+            dishes: (g.items || []).map((it: any) => ({
+              itemId: it.cuisineId != null ? String(it.cuisineId) : "",
+              dishName: it.cuisineName ?? "",
+              quantity: it.quantity != null ? String(it.quantity) : "1",
+              unit: normalizePackageUnit(it.unit)
+            }))
+          }))
+        : [];
+    } catch {
+      formModel.foodPackageCategories = [];
+    }
+    if (!formModel.foodPackageCategories.length) {
+      formModel.foodPackageCategories = [{ category: "", dishes: [{ itemId: "", dishName: "", quantity: "1", unit: "份" }] }];
+    }
+  }
+  syncImageFileListFromModel();
+}
+
+const fetchDetail = async () => {
+  try {
+    const res: any = await getPriceListDetail(id.value, businessSection.value, cuisineType.value);
+    if (res && res.code === 200 && res.data) {
+      applyDetailToForm(res);
+    } else {
+      const data = res?.data;
+      if (data) applyDetailToForm(res);
+      else ElMessage.error(res?.msg || "获取详情失败");
+    }
+  } catch (error) {
+    console.error("获取价目表详情失败:", error);
+    ElMessage.error("获取详情失败");
+  }
+};
+
+function buildSubmitPayload(isDraft: boolean) {
+  const storeId = localGet("createdId") as string;
+  const payload: any = {
+    storeId: storeId ? parseInt(storeId, 10) : undefined,
+    name: formModel.name.trim(),
+    totalPrice: parseFloat(formModel.totalPrice) || 0,
+    images: formModel.images || undefined,
+    imageContent: formModel.imageContent || undefined,
+    detailContent: formModel.detailContent?.trim() || undefined,
+    extraNote: formModel.extraNote?.trim() || undefined,
+    needReserve: formModel.needReserve,
+    reserveRule: formModel.reserveRule?.trim() || undefined,
+    peopleLimit: formModel.peopleLimit?.trim() || undefined,
+    usageRule: formModel.usageRule?.trim() || undefined
+  };
+  const moduleType = getModuleTypeByBusinessSection(businessSection.value);
+  if (moduleType === MODULE_TYPES.FOOD) {
+    if (formModel.formType === "dish") {
+      payload.cuisineType = CUISINE_TYPE.SINGLE;
+      payload.rawJson = JSON.stringify(
+        formModel.foodIngredients.map(it => ({
+          name: (it.ingredientName || "").trim(),
+          height: (it.weight || "").trim(),
+          cost: parseFloat(it.costPrice) || 0,
+          suggest: parseFloat(it.costPrice) || 0
+        }))
+      );
+    } else {
+      payload.cuisineType = CUISINE_TYPE.COMBO;
+      payload.groupJson = JSON.stringify(
+        formModel.foodPackageCategories
+          .filter(g => (g.dishes || []).length > 0)
+          .map(g => ({
+            categoryName: (g.category || "").trim(),
+            items: (g.dishes || []).map(d => ({
+              cuisineId: d.itemId || undefined,
+              cuisineName: (d.dishName || "").trim(),
+              quantity: parseInt(d.quantity, 10) || 1,
+              unit: (d.unit || "份").trim()
+            }))
+          }))
+      );
+    }
+  } else {
+    // 休闲娱乐、生活服务:服务项目 JSON(与商家端 generalPrice 一致)
+    payload.serviceJson = JSON.stringify(
+      formModel.generalServiceItems.map(it => ({
+        name: (it.serviceName || "").trim(),
+        num: parseInt(it.quantity, 10) || 1,
+        unit: (it.unit || "份").trim(),
+        details: (it.description || "").trim()
+      }))
+    );
+  }
+  if (id.value) payload.id = parseInt(id.value, 10);
+  return payload;
+}
+
+async function submit(isDraft: boolean) {
+  if (!ruleFormRef.value) return;
+  await ruleFormRef.value.validate(async valid => {
+    if (!valid) return;
+    submitting.value = true;
+    try {
+      const payload = buildSubmitPayload(isDraft);
+      const api = id.value ? updatePriceItem : addPriceItem;
+      const res: any = await api(payload);
+      if (res && res.code === 200) {
+        ElMessage.success(id.value ? "保存成功" : "新建成功");
+        router.back();
+      } else {
+        ElMessage.error(res?.msg || "操作失败");
+      }
+    } catch (error) {
+      console.error("提交失败:", error);
+      ElMessage.error("操作失败");
+    } finally {
+      submitting.value = false;
+    }
+  });
+}
+
+const handleSubmit = () => submit(false);
+const handleSaveDraft = () => submit(false); // 存草稿与确定共用提交,若后端有草稿状态可再区分
+
+const goBack = () => router.back();
+
+// 详情模式下点击「编辑」:去掉 mode=detail 进入编辑
+function goEdit() {
+  const query: Record<string, string> = {
+    id: id.value,
+    businessSection: businessSection.value
+  };
+  if (cuisineType.value !== undefined && cuisineType.value !== null) {
+    query.cuisineType = String(cuisineType.value);
+  }
+  router.replace({ path: "/priceList/edit", query });
+}
+
+function auditStatusText(status: number | null): string {
+  if (status === null || status === undefined) return "--";
+  const map: Record<number, string> = { 0: "审核中", 1: "已通过", 2: "已拒绝" };
+  return map[status] ?? "--";
+}
+
+function shelfStatusText(status: number | null): string {
+  if (status === 1) return "上架";
+  if (status === 2) return "下架";
+  return "--";
+}
+
+async function loadDishOptions() {
+  const moduleType = getModuleTypeByBusinessSection(businessSection.value);
+  if (moduleType !== MODULE_TYPES.FOOD) return;
+  try {
+    const res: any = await getCuisineSingleNameList();
+    if (res?.code === 200 && Array.isArray(res?.data)) {
+      dishOptions.value = (res.data as any[]).map((it: any) => ({
+        id: it.id ?? it.value,
+        name: it.name ?? it.label ?? ""
+      }));
+    }
+  } catch (e) {
+    console.error("获取单品列表失败", e);
+  }
+}
+
+onMounted(() => {
+  id.value = (route.query.id as string) || "";
+  businessSection.value = (route.query.businessSection as string) || localGet("businessSection") || "1";
+  const ct = route.query.cuisineType;
+  cuisineType.value = ct !== undefined && ct !== null && ct !== "" ? Number(ct) : undefined;
+
+  // 特色美食新建:从列表页「新建」选择菜品/套餐带入的 createMode
+  const createMode = route.query.createMode as string;
+  if (!id.value && getModuleTypeByBusinessSection(businessSection.value) === MODULE_TYPES.FOOD) {
+    if (createMode === "package" || createMode === "dish") {
+      formModel.formType = createMode;
+    }
+  }
+
+  if (formModel.formType === "dish" && !formModel.foodIngredients.length) {
+    formModel.foodIngredients = [{ ingredientName: "", weight: "", costPrice: "" }];
+  }
+  if (formModel.formType === "package" && !formModel.foodPackageCategories.length) {
+    formModel.foodPackageCategories = [{ category: "", dishes: [{ itemId: "", dishName: "", quantity: "1", unit: "份" }] }];
+  }
+
+  if (id.value) {
+    fetchDetail();
+  } else if (getModuleTypeByBusinessSection(businessSection.value) !== MODULE_TYPES.FOOD) {
+    // 休闲娱乐/生活服务新建时默认显示一个服务项
+    if (!formModel.generalServiceItems.length) {
+      formModel.generalServiceItems = [{ serviceName: "", quantity: "", unit: "份", description: "" }];
+    }
+  }
+  if (getModuleTypeByBusinessSection(businessSection.value) === MODULE_TYPES.FOOD) {
+    loadDishOptions();
+  }
+});
+</script>
+
+<style scoped lang="scss">
+.price-edit-page {
+  min-height: 100vh;
+  padding-bottom: 80px;
+  background-color: #f5f6f8;
+}
+.page-header {
+  display: flex;
+  align-items: center;
+  height: 56px;
+  padding: 0 24px;
+  background: #ffffff;
+  border-bottom: 1px solid #ebeef5;
+  .header-left {
+    display: flex;
+    gap: 6px;
+    align-items: center;
+    font-size: 14px;
+    color: #606266;
+    cursor: pointer;
+    &:hover {
+      color: #409eff;
+    }
+  }
+  .back-icon {
+    font-size: 18px;
+  }
+  .page-title {
+    flex: 1;
+    margin: 0;
+    margin-left: 24px;
+    font-size: 18px;
+    font-weight: 600;
+    color: #303133;
+    text-align: center;
+  }
+  .header-right {
+    padding: 4px;
+    color: #909399;
+    cursor: pointer;
+    &:hover {
+      color: #303133;
+    }
+  }
+  .close-icon {
+    font-size: 20px;
+  }
+}
+.page-content {
+  padding: 24px;
+}
+.two-column-form {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 32px;
+  padding: 24px 32px 32px;
+  background: #ffffff;
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
+}
+.form-column {
+  flex: 1;
+  min-width: 320px;
+  max-width: 80%;
+}
+.form-column-left {
+  padding-right: 24px;
+  border-right: 1px solid #ebeef5;
+}
+.price-form {
+  :deep(.el-form-item) {
+    margin-bottom: 20px;
+  }
+  :deep(.el-form-item__label) {
+    font-size: 14px;
+    color: #606266;
+  }
+  .form-input {
+    width: 100%;
+    max-width: 360px;
+    &.price-input {
+      max-width: 200px;
+    }
+    &.dish-select,
+    &.unit-select {
+      max-width: 200px;
+    }
+    &.qty-input {
+      max-width: 100px;
+    }
+  }
+  .form-textarea {
+    width: 100%;
+    max-width: 100%;
+  }
+}
+.form-block {
+  padding: 16px 0;
+  margin-bottom: 24px;
+  border-top: 1px solid #f0f0f0;
+  .block-head {
+    display: flex;
+    gap: 12px;
+    align-items: center;
+    margin-bottom: 12px;
+  }
+  .block-title {
+    font-size: 14px;
+    font-weight: 500;
+    color: #303133;
+  }
+  .btn-add {
+    font-size: 14px;
+  }
+  .block-tip {
+    margin-left: auto;
+    font-size: 12px;
+    color: #909399;
+  }
+  .empty-tip {
+    padding: 12px 0;
+    font-size: 13px;
+    color: #909399;
+  }
+  .block-item {
+    position: relative;
+    padding: 12px 0;
+    padding: 12px 16px;
+    margin-bottom: 12px;
+    background: #fafafa;
+    border: 1px solid #ebeef5;
+    border-radius: 6px;
+  }
+  .ingredient-item {
+    display: flex;
+    flex-flow: column wrap;
+    gap: 0 16px;
+    align-items: flex-start;
+    :deep(.el-form-item) {
+      margin-bottom: 12px;
+    }
+    .link-delete {
+      position: absolute;
+      top: 12px;
+      right: 12px;
+      font-size: 13px;
+    }
+  }
+  .service-item {
+    display: flex;
+    flex-flow: column wrap;
+    gap: 0 16px;
+    align-items: flex-start;
+    :deep(.el-form-item) {
+      margin-bottom: 12px;
+    }
+    .form-textarea {
+      max-width: 360px;
+    }
+    .service-unit-select {
+      min-width: 120px;
+      :deep(.el-select__wrapper),
+      :deep(.el-input__wrapper) {
+        min-width: 120px;
+      }
+      :deep(.el-input__inner) {
+        min-width: 80px;
+      }
+    }
+    .link-delete {
+      position: absolute;
+      top: 12px;
+      right: 12px;
+      font-size: 13px;
+    }
+  }
+  .btn-recommend {
+    display: inline-block;
+    margin-top: 8px;
+    font-size: 14px;
+  }
+  .price-list-group {
+    .dish-row {
+      display: flex;
+      flex-flow: column wrap;
+      gap: 0 12px;
+      align-items: flex-start;
+      padding: 8px 0;
+      margin-bottom: 8px;
+      border-bottom: 1px dashed #ebeef5;
+      &:last-of-type {
+        border-bottom: none;
+      }
+    }
+    .package-unit-select {
+      min-width: 120px;
+      :deep(.el-select__wrapper),
+      :deep(.el-input__wrapper) {
+        min-width: 120px;
+      }
+      :deep(.el-input__inner) {
+        min-width: 80px;
+      }
+    }
+    .group-actions {
+      display: flex;
+      gap: 16px;
+      margin-top: 12px;
+    }
+    .link-delete {
+      font-size: 13px;
+    }
+  }
+}
+.image-upload-wrap {
+  .price-list-upload {
+    :deep(.el-upload-list--picture-card) {
+      --el-upload-list-picture-card-size: 100px;
+    }
+    :deep(.el-upload--picture-card) {
+      width: 100px;
+      height: 100px;
+    }
+  }
+  .image-tip {
+    margin-top: 4px;
+    font-size: 12px;
+    color: #909399;
+  }
+}
+.page-footer {
+  z-index: 100;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 64px;
+  background: #ffffff;
+  border-top: 1px solid #ebeef5;
+}
+.footer-inner {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  max-width: 1100px;
+  padding: 0 24px;
+}
+.view-only-value {
+  font-size: 14px;
+  color: #606266;
+}
+.audit-status {
+  font-weight: 500;
+  &.audit-status--0 {
+    color: #e6a23c;
+  }
+  &.audit-status--1 {
+    color: #67c23a;
+  }
+  &.audit-status--2 {
+    color: #f56c6c;
+  }
+}
+</style>

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

@@ -0,0 +1,396 @@
+<template>
+  <div class="table-box">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <template #tableHeader>
+        <div class="table-header">
+          <el-button type="primary" @click="handleAdd"> 新建 </el-button>
+        </div>
+      </template>
+      <template #imgUrl="{ row }">
+        <el-image v-if="row.imgUrl" :src="row.imgUrl" fit="cover" style="width: 60px; height: 60px; border-radius: 6px" />
+        <div v-else class="img-placeholder">无图</div>
+      </template>
+      <template #auditStatus="{ row }">
+        <span>{{ auditStatusLabel(row.auditStatus) }}</span>
+      </template>
+      <template #shelfStatus="{ row }">
+        <span>
+          {{ row.shelfStatus === 1 ? "已上架" : row.shelfStatus === 2 ? "已下架" : "--" }}
+        </span>
+      </template>
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button link type="primary" @click="handleDetail(scope.row)"> 查看详情 </el-button>
+        <!-- 上架按钮(仅审核通过且已下架时显示) -->
+        <el-button
+          v-if="scope.row.auditStatus === 1 && scope.row.shelfStatus === 2"
+          link
+          type="primary"
+          @click="handleShelfToggle(scope.row, 1)"
+        >
+          上架
+        </el-button>
+        <!-- 下架按钮(仅审核通过且已上架时显示) -->
+        <el-button
+          v-if="scope.row.auditStatus === 1 && scope.row.shelfStatus === 1"
+          link
+          type="primary"
+          @click="handleShelfToggle(scope.row, 2)"
+        >
+          下架
+        </el-button>
+        <!-- 编辑按钮(审核通过、审核拒绝、已下架、已结束时可编辑) -->
+        <el-button
+          v-if="scope.row.auditStatus === 1 || scope.row.auditStatus === 2"
+          link
+          type="primary"
+          @click="handleEdit(scope.row)"
+        >
+          编辑
+        </el-button>
+        <!-- 删除按钮(审核通过、审核拒绝、已下架、已结束时可删除) -->
+        <el-button
+          v-if="scope.row.auditStatus === 1 || scope.row.auditStatus === 2"
+          link
+          type="primary"
+          @click="handleDelete(scope.row)"
+        >
+          删除
+        </el-button>
+        <!-- 查看拒绝原因按钮(仅审核拒绝时显示) -->
+        <el-button
+          v-if="scope.row.auditStatus === 2 && scope.row.auditReason"
+          link
+          type="primary"
+          @click="handleViewReason(scope.row)"
+        >
+          查看原因
+        </el-button>
+      </template>
+    </ProTable>
+    <!-- 删除确认弹窗 -->
+    <el-dialog v-model="deleteDialogVisible" title="提示" width="500px">
+      <div style="padding: 20px 0">确定要删除该价目表吗?</div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="deleteDialogVisible = false"> 取消 </el-button>
+          <el-button type="primary" @click="confirmDelete"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+    <!-- 查看拒绝原因弹窗 -->
+    <el-dialog v-model="reasonDialogVisible" title="查看拒绝原因" width="600px">
+      <div class="reject-reason-content">
+        <div class="reject-reason-item">
+          <div class="reject-reason-label">价目表名称:</div>
+          <div class="reject-reason-value">
+            {{ currentReasonData.name || "--" }}
+          </div>
+        </div>
+        <div class="reject-reason-item">
+          <div class="reject-reason-label">拒绝原因:</div>
+          <div class="reject-reason-value reject-reason-text">
+            {{ currentReasonData.reason || "暂无拒绝原因" }}
+          </div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="reasonDialogVisible = false"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="priceList">
+import { reactive, ref, computed } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import type { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
+import { localGet } from "@/utils";
+import {
+  AUDIT_STATUS_LABEL,
+  getPriceListPage,
+  transformListItem,
+  deletePriceItem,
+  updateShelfStatus,
+  getModuleTypeByBusinessSection,
+  MODULE_TYPES,
+  type PriceListRow
+} from "@/api/modules/priceList";
+
+const router = useRouter();
+const proTable = ref<ProTableInstance>();
+
+// 删除确认弹窗
+const deleteDialogVisible = ref(false);
+const deletingItem = ref<PriceListRow | null>(null);
+
+// 查看拒绝原因弹窗
+const reasonDialogVisible = ref(false);
+const currentReasonData = ref<{ name: string; reason: string }>({
+  name: "",
+  reason: ""
+});
+
+// 是否特色美食(经营板块为 1,与商家端一致:仅美食才显示类型列、新建时选择菜品/套餐)
+const isFoodModule = computed(() => getModuleTypeByBusinessSection(initParam.businessSection) === MODULE_TYPES.FOOD);
+
+// 列配置(与商家端保持字段含义一致);特色美食时增加「类型」列
+const baseColumns: ColumnProps<PriceListRow>[] = [
+  {
+    prop: "dishName",
+    label: "名称",
+    search: {
+      el: "input",
+      props: {
+        placeholder: "请输入名称"
+      }
+    }
+  },
+  {
+    prop: "imgUrl",
+    label: "图片",
+    align: "center"
+  },
+  {
+    prop: "dishPrice",
+    label: "价格"
+  },
+  {
+    prop: "dishUnit",
+    label: "单位"
+  },
+  {
+    prop: "auditStatus",
+    label: "审核状态",
+    search: {
+      el: "select",
+      props: { placeholder: "请选择审核状态" }
+    },
+    enum: [
+      { label: "审核中", value: 0 },
+      { label: "审核通过", value: 1 },
+      { label: "审核拒绝", value: 2 }
+    ],
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "shelfStatus",
+    label: "上下架状态",
+    search: {
+      el: "select",
+      props: { placeholder: "请选择上下架状态" }
+    },
+    enum: [
+      { label: "已上架", value: 1 },
+      { label: "已下架", value: 2 }
+    ],
+    fieldNames: { label: "label", value: "value" }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 280 }
+];
+
+const columns = computed<ColumnProps<PriceListRow>[]>(() => {
+  if (!isFoodModule.value) return baseColumns;
+  const typeCol: ColumnProps<PriceListRow> = {
+    prop: "cuisineType",
+    label: "类型",
+    width: 88,
+    enum: [
+      { label: "菜品", value: 1 },
+      { label: "套餐", value: 2 }
+    ],
+    fieldNames: { label: "label", value: "value" }
+  };
+  return [baseColumns[0], typeCol, ...baseColumns.slice(1)];
+});
+
+// 初始查询参数:门店 + 经营板块,与其他商家端页面保持一致
+const initParam = reactive({
+  storeId: localGet("createdId") || "",
+  businessSection: localGet("businessSection") || "1"
+});
+
+// 将审核状态值映射为文案
+const auditStatusLabel = (status: number | null) => {
+  if (status === null || status === undefined) return "--";
+  return AUDIT_STATUS_LABEL[status] || "--";
+};
+
+// 表格请求方法
+// 说明:不同经营板块在 api/modules/priceList 中会走不同的后台接口
+const getTableList = (params: any) => {
+  const query: any = {
+    ...params,
+    storeId: initParam.storeId,
+    businessSection: initParam.businessSection
+  };
+
+  // shelfStatus 字段名与后台一致,直接透传
+  return getPriceListPage(query);
+};
+
+// 统一处理接口返回的数据结构(入参已是后端的 data 字段)
+const dataCallback = (payload: any) => {
+  const records = Array.isArray(payload) ? payload : payload.records || [];
+  const total = payload.total ?? records.length;
+
+  return {
+    list: records.map((item: any) => transformListItem(item)),
+    total
+  };
+};
+
+// 详情(进入编辑页只读模式,不跳转独立详情页)
+const handleDetail = (row: PriceListRow) => {
+  const query: Record<string, string> = {
+    id: String(row.id),
+    businessSection: String(initParam.businessSection),
+    mode: "detail"
+  };
+  if (row.cuisineType !== undefined && row.cuisineType !== null) {
+    query.cuisineType = String(row.cuisineType);
+  }
+  router.push({ path: "/priceList/edit", query });
+};
+
+// 新建:直接进入编辑页,美食类别在页面内选择类型(菜品/套餐)
+const handleAdd = () => {
+  router.push({
+    path: "/priceList/edit",
+    query: { businessSection: String(initParam.businessSection) }
+  });
+};
+
+// 编辑
+const handleEdit = (row: PriceListRow) => {
+  const query: Record<string, string> = {
+    id: String(row.id),
+    businessSection: String(initParam.businessSection)
+  };
+  if (row.cuisineType !== undefined && row.cuisineType !== null) {
+    query.cuisineType = String(row.cuisineType);
+  }
+  router.push({ path: "/priceList/edit", query });
+};
+
+// 删除
+const handleDelete = (row: PriceListRow) => {
+  deletingItem.value = row;
+  deleteDialogVisible.value = true;
+};
+
+// 确认删除
+const confirmDelete = async () => {
+  if (!deletingItem.value) return;
+  try {
+    const res: any = await deletePriceItem(deletingItem.value.id);
+    if (res && res.code === 200) {
+      ElMessage.success("删除成功");
+      deleteDialogVisible.value = false;
+      deletingItem.value = null;
+      proTable.value?.getTableList();
+    } else {
+      ElMessage.error(res?.msg || "删除失败");
+    }
+  } catch (error) {
+    console.error("删除失败:", error);
+    ElMessage.error("删除失败");
+  }
+};
+
+// 上架/下架切换
+const handleShelfToggle = async (row: PriceListRow, shelfStatus: number) => {
+  const actionText = shelfStatus === 1 ? "上架" : "下架";
+  ElMessageBox.confirm(`确定要${actionText}该价目表吗?`, "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning"
+  })
+    .then(async () => {
+      try {
+        const res: any = await updateShelfStatus(row.id, shelfStatus);
+        if (res && res.code === 200) {
+          ElMessage.success(`${actionText}成功`);
+          proTable.value?.getTableList();
+        } else {
+          ElMessage.error(res?.msg || "操作失败");
+        }
+      } catch (error) {
+        console.error("操作失败:", error);
+        ElMessage.error("操作失败");
+      }
+    })
+    .catch(() => {
+      // 用户取消操作,不做任何处理
+    });
+};
+
+// 查看拒绝原因
+const handleViewReason = (row: PriceListRow) => {
+  currentReasonData.value = {
+    name: row.dishName || "",
+    reason: row.auditReason || "暂无原因"
+  };
+  reasonDialogVisible.value = true;
+};
+</script>
+
+<style scoped lang="scss">
+.table-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  padding: 12px 0;
+  .table-header-title {
+    font-size: 16px;
+    font-weight: 600;
+  }
+}
+.img-placeholder {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 60px;
+  height: 60px;
+  font-size: 12px;
+  color: #909399;
+  background-color: #f2f3f5;
+  border-radius: 6px;
+}
+.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>

+ 665 - 0
src/views/storeDecoration/add.vue

@@ -0,0 +1,665 @@
+<template>
+  <div class="add-container">
+    <el-dialog v-model="dialogVisible" title="新建" width="800px" :close-on-click-modal="false" @close="handleClose">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="140px" label-position="right">
+        <!-- 需求标题 -->
+        <el-form-item label="需求标题" prop="requirementTitle" required>
+          <el-input v-model="formData.requirementTitle" placeholder="请输入" maxlength="100" clearable />
+        </el-form-item>
+
+        <!-- 装修类型 -->
+        <el-form-item label="装修类型" prop="renovationType">
+          <el-radio-group v-model="formData.renovationType">
+            <el-radio :label="1">新房装修</el-radio>
+            <el-radio :label="2">旧房改造</el-radio>
+            <el-radio :label="3">局部装修</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <!-- 房屋面积 -->
+        <el-form-item label="房屋面积(㎡)" prop="houseArea" required>
+          <el-input-number
+            v-model="formData.houseArea"
+            :min="0"
+            :max="99999"
+            :precision="2"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+
+        <!-- 装修预算 -->
+        <el-form-item label="装修预算(万元)" prop="renovationBudget" required>
+          <el-input-number
+            v-model="formData.renovationBudget"
+            :min="0"
+            :max="99999"
+            :precision="2"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+
+        <!-- 详细需求 -->
+        <el-form-item label="详细需求" prop="detailedRequirement">
+          <el-input
+            v-model="formData.detailedRequirement"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入"
+            maxlength="500"
+            show-word-limit
+            clearable
+          />
+        </el-form-item>
+
+        <!-- 期望装修时间 -->
+        <el-form-item label="期望装修时间" prop="expectedRenovationTime" required>
+          <el-date-picker
+            v-model="formData.expectedRenovationTime"
+            type="date"
+            placeholder="请选择"
+            value-format="YYYY-MM-DD"
+            style="width: 100%"
+          />
+        </el-form-item>
+
+        <!-- 上传房屋图纸 -->
+        <el-form-item label="上传房屋图纸" prop="attachmentUrls" required>
+          <el-upload
+            v-model:file-list="fileList"
+            action="#"
+            list-type="picture-card"
+            :limit="9"
+            :on-preview="handlePictureCardPreview"
+            :on-remove="handleRemove"
+            :on-exceed="handleExceed"
+            :before-upload="beforeUpload"
+            :http-request="handleImageUpload"
+            accept="image/*"
+            multiple
+          >
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+          <div class="upload-tip">({{ fileList.length }}/9)</div>
+        </el-form-item>
+
+        <!-- 联系人 -->
+        <el-form-item label="联系人" prop="contactName" required>
+          <el-input v-model="formData.contactName" placeholder="请输入" maxlength="50" clearable />
+        </el-form-item>
+
+        <!-- 联系电话 -->
+        <el-form-item label="联系电话" prop="contactPhone" required>
+          <el-input v-model="formData.contactPhone" placeholder="请输入" maxlength="20" clearable />
+        </el-form-item>
+
+        <!-- 所在城市 -->
+        <el-form-item label="所在城市" prop="city" required>
+          <el-input
+            v-model="formData.city"
+            placeholder="请选择"
+            readonly
+            @click="showCityDialog = true"
+            clearable
+            @clear="handleCityClear"
+          >
+            <template #suffix>
+              <el-icon class="cursor-pointer" @click="showCityDialog = true"><ArrowDown /></el-icon>
+            </template>
+          </el-input>
+        </el-form-item>
+
+        <!-- 详细地址 -->
+        <el-form-item label="详细地址" prop="detailedAddress">
+          <el-input
+            v-model="formData.detailedAddress"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入"
+            maxlength="200"
+            show-word-limit
+            clearable
+          />
+        </el-form-item>
+
+        <!-- 服务协议确认 -->
+        <el-form-item prop="agreementConfirmed">
+          <el-checkbox v-model="formData.agreementConfirmed" :true-label="1" :false-label="0">
+            我已阅读并同意
+            <el-link type="primary" :underline="false" @click="handleShowAgreement">《用户服务协议》</el-link>
+            和
+            <el-link type="primary" :underline="false" @click="handleShowPrivacy">《隐私政策》</el-link>
+            ,允许装修商家查看我的需求信息并与我联系
+          </el-checkbox>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="handleClose">返回</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 城市选择对话框 -->
+    <el-dialog v-model="showCityDialog" title="选择城市" width="600px">
+      <div class="city-selector">
+        <el-select
+          v-model="selectedProvince"
+          placeholder="请选择省"
+          clearable
+          style="width: 100%; margin-bottom: 10px"
+          @change="handleProvinceChange"
+        >
+          <el-option v-for="province in provinceOptions" :key="province.adcode" :label="province.name" :value="province.adcode" />
+        </el-select>
+        <el-select
+          v-model="selectedCity"
+          placeholder="请选择市"
+          clearable
+          style="width: 100%; margin-bottom: 10px"
+          :disabled="!selectedProvince"
+          @change="handleCitySelect"
+        >
+          <el-option v-for="city in cityOptions" :key="city.adcode" :label="city.name" :value="city.adcode" />
+        </el-select>
+        <el-select
+          v-model="selectedDistrict"
+          placeholder="请选择区"
+          clearable
+          style="width: 100%"
+          :disabled="!selectedCity"
+          @change="handleDistrictSelect"
+        >
+          <el-option v-for="district in districtOptions" :key="district.adcode" :label="district.name" :value="district.adcode" />
+        </el-select>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="showCityDialog = false">取消</el-button>
+          <el-button type="primary" @click="handleConfirmCity">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :url-list="imageViewerUrlList"
+      :initial-index="imageViewerInitialIndex"
+      @close="imageViewerVisible = false"
+    />
+  </div>
+</template>
+
+<script setup lang="ts" name="decorationAdd">
+import { ref, reactive, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { ElMessage, type FormInstance, type UploadFile, type UploadProps, type UploadRequestOptions } from "element-plus";
+import { Plus, ArrowDown } from "@element-plus/icons-vue";
+import { saveOrUpdateDecoration, getDistrict, uploadDecorationImage } from "@/api/modules/storeDecoration";
+import { localGet } from "@/utils";
+
+const route = useRoute();
+const router = useRouter();
+
+const dialogVisible = ref(true);
+const formRef = ref<FormInstance>();
+const submitLoading = ref(false);
+
+const formData = reactive({
+  requirementTitle: "",
+  renovationType: 1,
+  houseArea: null as number | null,
+  renovationBudget: null as number | null,
+  detailedRequirement: "",
+  expectedRenovationTime: "",
+  attachmentUrls: [] as string[],
+  contactName: "",
+  contactPhone: "",
+  city: "",
+  cityAdcode: "",
+  detailedAddress: "",
+  agreementConfirmed: 0,
+  storeId: localGet("createdId") || 0
+});
+
+const fileList = ref<UploadFile[]>([]);
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
+
+// 城市选择相关
+const showCityDialog = ref(false);
+const selectedProvince = ref("");
+const selectedCity = ref("");
+const selectedDistrict = ref("");
+const provinceOptions = ref<any[]>([]);
+const cityOptions = ref<any[]>([]);
+const districtOptions = ref<any[]>([]);
+const selectedProvinceName = ref("");
+const selectedCityName = ref("");
+const selectedDistrictName = ref("");
+
+// 表单验证规则
+const rules = reactive({
+  requirementTitle: [{ required: true, message: "请输入需求标题", trigger: "blur" }],
+  houseArea: [{ required: true, message: "请输入房屋面积", trigger: "blur" }],
+  renovationBudget: [{ required: true, message: "请输入装修预算", trigger: "blur" }],
+  expectedRenovationTime: [{ required: true, message: "请选择期望装修时间", trigger: "change" }],
+  attachmentUrls: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        // 检查所有文件是否都已上传成功
+        const successFiles = fileList.value.filter((file: UploadFile) => file.status === "success");
+        if (successFiles.length === 0) {
+          callback(new Error("请上传房屋图纸"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ],
+  contactName: [{ required: true, message: "请输入联系人", trigger: "blur" }],
+  contactPhone: [
+    { required: true, message: "请输入联系电话", trigger: "blur" },
+    { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }
+  ],
+  city: [{ required: true, message: "请选择所在城市", trigger: "change" }],
+  agreementConfirmed: [
+    {
+      required: true,
+      validator: (rule: any, value: any, callback: any) => {
+        if (formData.agreementConfirmed !== 1) {
+          callback(new Error("请阅读并同意用户服务协议和隐私政策"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ]
+});
+
+// 获取省份数据
+const getProvinceData = async () => {
+  try {
+    const res: any = await getDistrict();
+    if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
+      const chinaData = res.data.districts[0];
+      if (chinaData && chinaData.districts && Array.isArray(chinaData.districts)) {
+        provinceOptions.value = chinaData.districts;
+      }
+    }
+  } catch (error) {
+    console.error("获取省份数据失败:", error);
+  }
+};
+
+// 省份变化时获取城市数据
+const handleProvinceChange = async (provinceCode: string) => {
+  selectedCity.value = "";
+  selectedDistrict.value = "";
+  cityOptions.value = [];
+  districtOptions.value = [];
+  selectedCityName.value = "";
+  selectedDistrictName.value = "";
+
+  if (!provinceCode) {
+    selectedProvinceName.value = "";
+    return;
+  }
+
+  const province = provinceOptions.value.find(p => p.adcode === provinceCode);
+  selectedProvinceName.value = province ? province.name : "";
+
+  try {
+    const res: any = await getDistrict({ adCode: provinceCode });
+    if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
+      const provinceData = res.data.districts[0];
+      if (provinceData && provinceData.districts && Array.isArray(provinceData.districts)) {
+        cityOptions.value = provinceData.districts;
+      }
+    }
+  } catch (error) {
+    console.error("获取城市数据失败:", error);
+  }
+};
+
+// 城市变化时获取区县数据
+const handleCitySelect = async (cityCode: string) => {
+  selectedDistrict.value = "";
+  districtOptions.value = [];
+  selectedDistrictName.value = "";
+
+  if (!cityCode) {
+    selectedCityName.value = "";
+    return;
+  }
+
+  const city = cityOptions.value.find(c => c.adcode === cityCode);
+  selectedCityName.value = city ? city.name : "";
+
+  try {
+    const res: any = await getDistrict({ adCode: cityCode });
+    if (res && res.data && res.data.districts && Array.isArray(res.data.districts) && res.data.districts.length > 0) {
+      const cityData = res.data.districts[0];
+      if (cityData && cityData.districts && Array.isArray(cityData.districts)) {
+        districtOptions.value = cityData.districts;
+      }
+    }
+  } catch (error) {
+    console.error("获取区县数据失败:", error);
+  }
+};
+
+// 区县选择
+const handleDistrictSelect = (districtCode: string) => {
+  if (!districtCode) {
+    selectedDistrictName.value = "";
+    return;
+  }
+  const district = districtOptions.value.find(d => d.adcode === districtCode);
+  selectedDistrictName.value = district ? district.name : "";
+};
+
+// 确认城市选择
+const handleConfirmCity = () => {
+  if (!selectedProvince.value || !selectedCity.value) {
+    ElMessage.warning("请至少选择省和市");
+    return;
+  }
+  // 构建城市名称:省+市+区(如果有区)
+  let cityName = selectedProvinceName.value + selectedCityName.value;
+  if (selectedDistrictName.value) {
+    cityName += selectedDistrictName.value;
+  }
+  formData.city = cityName;
+  formData.cityAdcode = selectedDistrict.value || selectedCity.value || selectedProvince.value;
+  showCityDialog.value = false;
+};
+
+// 清除城市选择
+const handleCityClear = () => {
+  formData.city = "";
+  formData.cityAdcode = "";
+  selectedProvince.value = "";
+  selectedCity.value = "";
+  selectedDistrict.value = "";
+  selectedProvinceName.value = "";
+  selectedCityName.value = "";
+  selectedDistrictName.value = "";
+  cityOptions.value = [];
+  districtOptions.value = [];
+};
+
+// 图片上传前验证
+const beforeUpload: UploadProps["beforeUpload"] = (rawFile: File) => {
+  const isImage = rawFile.type.startsWith("image/");
+  const isLt10M = rawFile.size / 1024 / 1024 < 10;
+
+  if (!isImage) {
+    ElMessage.error("只能上传图片文件!");
+    return false;
+  }
+  if (!isLt10M) {
+    ElMessage.error("图片大小不能超过 10MB!");
+    return false;
+  }
+  return true;
+};
+
+// 图片上传 - 点击加号时调用 /file/uploadMore 接口
+const handleImageUpload = async (options: UploadRequestOptions) => {
+  // 获取文件对象,可能是 options.file 或 options.file.raw
+  const file = options.file.raw || options.file;
+  
+  if (!file) {
+    console.error("文件对象不存在");
+    ElMessage.error("文件对象不存在");
+    return;
+  }
+
+  console.log("开始上传文件:", file.name, "类型:", file.type, "大小:", file.size);
+
+  const uploadFormData = new FormData();
+  uploadFormData.append("file", file);
+
+  options.file.status = "uploading";
+  options.file.percentage = 0;
+
+  try {
+    console.log("调用 /alienStore/file/uploadMore 接口上传文件");
+    // 调用 /alienStore/file/uploadMore 接口上传文件
+    const result: any = await uploadDecorationImage(uploadFormData);
+    console.log("上传接口返回结果:", result);
+
+    if (result?.code === 200 || result?.code === 0) {
+      let fileUrl = "";
+
+      // 处理不同的返回格式
+      if (Array.isArray(result.data) && result.data.length > 0) {
+        fileUrl = result.data[0];
+      } else if (typeof result.data === "string") {
+        fileUrl = result.data;
+      } else if (result.data?.fileUrl) {
+        fileUrl = result.data.fileUrl;
+      } else if (result.data?.url) {
+        fileUrl = result.data.url;
+      } else if (result.fileUrl) {
+        fileUrl = result.fileUrl;
+      } else if (result.url) {
+        fileUrl = result.url;
+      }
+
+      if (fileUrl) {
+        options.file.status = "success";
+        options.file.percentage = 100;
+        options.file.url = fileUrl;
+        options.file.response = { url: fileUrl };
+
+        // 获取图片路径后,记录到 attachmentUrls 数组中,提交时会传递给保存接口
+        if (!formData.attachmentUrls.includes(fileUrl)) {
+          formData.attachmentUrls.push(fileUrl);
+          console.log("文件上传成功,路径已记录:", fileUrl);
+          console.log("当前附件列表:", formData.attachmentUrls);
+        }
+        options.onSuccess?.(result);
+        // 触发表单验证
+        formRef.value?.validateField("attachmentUrls");
+      } else {
+        console.error("上传接口返回数据格式错误:", result);
+        throw new Error("上传接口返回数据格式错误");
+      }
+    } else {
+      throw new Error(result?.msg || result?.message || "文件上传失败");
+    }
+  } catch (error: any) {
+    console.error("文件上传失败:", error);
+    options.file.status = "fail";
+    if (options.file.url && options.file.url.startsWith("blob:")) {
+      URL.revokeObjectURL(options.file.url);
+    }
+    const index = fileList.value.findIndex(f => f.uid === options.file.uid);
+    if (index > -1) {
+      fileList.value.splice(index, 1);
+    }
+    ElMessage.error(error?.message || "文件上传失败");
+    options.onError?.(error);
+  }
+};
+
+// 图片预览
+const handlePictureCardPreview = (file: UploadFile) => {
+  if (file.url) {
+    imageViewerUrlList.value = fileList.value.map((item: UploadFile) => item.url || "").filter(Boolean);
+    imageViewerInitialIndex.value = fileList.value.findIndex((item: UploadFile) => item.uid === file.uid);
+    imageViewerVisible.value = true;
+  }
+};
+
+// 删除图片
+const handleRemove = (file: UploadFile) => {
+  if (file.url) {
+    const index = formData.attachmentUrls.indexOf(file.url);
+    if (index > -1) {
+      formData.attachmentUrls.splice(index, 1);
+      console.log("删除图片,路径已移除:", file.url);
+      console.log("当前附件列表:", formData.attachmentUrls);
+    }
+  }
+  // 触发表单验证
+  formRef.value?.validateField("attachmentUrls");
+};
+
+// 超出限制
+const handleExceed = () => {
+  ElMessage.warning("最多只能上传9张图片");
+};
+
+// 显示服务协议
+const handleShowAgreement = () => {
+  // TODO: 打开服务协议页面
+  ElMessage.info("服务协议");
+};
+
+// 显示隐私政策
+const handleShowPrivacy = () => {
+  // TODO: 打开隐私政策页面
+  ElMessage.info("隐私政策");
+};
+
+// 提交表单
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+
+  await formRef.value.validate(async valid => {
+    if (!valid) return;
+
+    submitLoading.value = true;
+    try {
+      // 确保 attachmentUrls 是数组格式,包含所有上传成功的图片路径
+      const attachmentUrlsList = Array.isArray(formData.attachmentUrls) 
+        ? formData.attachmentUrls.filter(url => url && url.trim() !== "")
+        : [];
+
+      console.log("提交时的附件列表:", attachmentUrlsList);
+
+      // 构建提交参数 - attachmentUrls 字段存储图片路径数组
+      const params: any = {
+        id: 0, // 新建时传0
+        requirementTitle: formData.requirementTitle,
+        renovationType: formData.renovationType,
+        houseArea: formData.houseArea || 0,
+        renovationBudget: formData.renovationBudget || 0,
+        detailedRequirement: formData.detailedRequirement || "",
+        expectedRenovationTime: formData.expectedRenovationTime,
+        // 将上传获取的图片路径数组存入 attachmentUrls 字段,传递给保存接口
+        attachmentUrls: attachmentUrlsList,
+        contactName: formData.contactName,
+        contactPhone: formData.contactPhone,
+        city: formData.city,
+        cityAdcode: formData.cityAdcode || "",
+        detailedAddress: formData.detailedAddress || "",
+        agreementConfirmed: formData.agreementConfirmed,
+        storeId: formData.storeId || 0,
+        auditStatus: 0,
+        status: 0,
+        hasCommunicated: false,
+        inquiryCount: 0,
+        viewCount: 0,
+        createdTime: "",
+        updatedTime: "",
+        storeAddress: "",
+        storeAvatar: "",
+        storeBlurb: "",
+        storeName: "",
+        storeTel: ""
+      };
+
+      console.log("提交参数:", params);
+      const res: any = await saveOrUpdateDecoration(params);
+
+      if (res.code == 200 || res.code == 0) {
+        ElMessage.success("创建成功");
+        // 返回到列表页面
+        router.push("/storeDecorationManagement/decorationManagement");
+      } else {
+        ElMessage.error(res.msg || "创建失败");
+        // 创建失败也返回到列表页面
+        router.push("/storeDecorationManagement/decorationManagement");
+      }
+    } catch (error: any) {
+      ElMessage.error(error?.msg || error?.message || "创建失败");
+      // 创建失败也返回到列表页面
+      router.push("/storeDecorationManagement/decorationManagement");
+    } finally {
+      submitLoading.value = false;
+    }
+  });
+};
+
+// 关闭对话框
+const handleClose = () => {
+  dialogVisible.value = false;
+  // 返回到列表页面
+  router.push("/storeDecorationManagement/decorationManagement");
+};
+
+// 初始化
+onMounted(() => {
+  getProvinceData();
+});
+</script>
+
+<style lang="scss" scoped>
+.add-container {
+  :deep(.el-dialog__body) {
+    padding: 20px;
+    max-height: 70vh;
+    overflow-y: auto;
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 20px;
+  }
+
+  :deep(.el-radio-group) {
+    display: flex;
+    gap: 20px;
+  }
+
+  .upload-tip {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+  }
+
+  .dialog-footer {
+    text-align: center;
+    padding: 20px 0 0;
+  }
+
+  :deep(.el-upload--picture-card) {
+    width: 100px;
+    height: 100px;
+  }
+
+  :deep(.el-upload-list--picture-card .el-upload-list__item) {
+    width: 100px;
+    height: 100px;
+  }
+
+  .city-selector {
+    padding: 10px 0;
+  }
+
+  .cursor-pointer {
+    cursor: pointer;
+  }
+}
+</style>

+ 197 - 0
src/views/storeDecoration/decorationCompany.vue

@@ -0,0 +1,197 @@
+<template>
+  <div class="table-box button-table">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button link type="primary" @click="handleContact(scope.row)">联系业主</el-button>
+        <el-button link type="primary" @click="handleView(scope.row)">查看详情</el-button>
+      </template>
+    </ProTable>
+  </div>
+</template>
+
+<script setup lang="tsx" name="decorationCompany">
+import { reactive, ref, onMounted, onActivated } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
+import { getDecorationPage } from "@/api/modules/storeDecoration";
+import { localGet } from "@/utils";
+
+const router = useRouter();
+const proTable = ref<ProTableInstance>();
+
+// 装修类型枚举
+const decorationTypeEnum = [
+  { label: "新房装修", value: "1" },
+  { label: "旧房改造", value: "2" },
+  { label: "局部装修", value: "3" }
+];
+
+// 沟通状态枚举 (hasCommunicated: false:未沟通, true:已沟通)
+const communicationStatusEnum = [
+  { label: "未沟通", value: "false" },
+  { label: "已沟通", value: "true" }
+];
+
+// 初始化请求参数
+const initParam = reactive({});
+
+// 数据回调处理
+const dataCallback = (data: any) => {
+  if (data && data.data && data.data.records) {
+    return {
+      list: data.data.records || [],
+      total: data.data.total || 0
+    };
+  } else if (data && data.records) {
+    return {
+      list: data.records || [],
+      total: data.total || 0
+    };
+  }
+  return {
+    list: [],
+    total: 0
+  };
+};
+
+// 获取表格列表
+const getTableList = (params: any) => {
+  let newParams: any = {};
+  
+  // 只保留分页参数 page 和 size
+  if (params.pageNum) {
+    newParams.page = params.pageNum;
+  }
+  if (params.pageSize) {
+    newParams.size = params.pageSize;
+  }
+  
+  // 处理沟通状态参数 hasCommunicated(将字符串转换为布尔值)
+  // true:已沟通, false:未沟通
+  if (params.hasCommunicated !== undefined && params.hasCommunicated !== "" && params.hasCommunicated !== null) {
+    // 将字符串 "true"/"false" 转换为布尔值 true/false
+    if (params.hasCommunicated === "true" || params.hasCommunicated === true) {
+      newParams.hasCommunicated = true;
+    } else if (params.hasCommunicated === "false" || params.hasCommunicated === false) {
+      newParams.hasCommunicated = false;
+    }
+  }
+  
+  // 处理装修类型参数
+  if (params.renovationType !== undefined && params.renovationType !== "" && params.renovationType !== null) {
+    newParams.renovationType = params.renovationType;
+  }
+  
+  // 调用 /renovation/requirement/getPage 接口(只传 page、size 和筛选条件)
+  return getDecorationPage(newParams);
+};
+
+// 联系业主
+const handleContact = (row: any) => {
+  // TODO: 实现联系业主功能
+  ElMessage.info("联系业主功能待开发");
+};
+
+// 查看详情
+const handleView = (row: any) => {
+  router.push(`/storeDecorationManagement/decorationCompanyDetail?id=${row.id}`);
+};
+
+// 表格列配置
+const columns = reactive<ColumnProps<any>[]>([
+  { type: "index", fixed: "left", label: "序号", width: 80 },
+  {
+    prop: "requirementTitle",
+    label: "标题",
+    render: (scope: any) => {
+      return scope.row.requirementTitle || "--";
+    }
+  },
+  {
+    prop: "renovationType",
+    label: "装修类型",
+    render: (scope: any) => {
+      const type = decorationTypeEnum.find(item => item.value === String(scope.row.renovationType));
+      return type ? type.label : "--";
+    },
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: decorationTypeEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "houseArea",
+    label: "面积(㎡)",
+    render: (scope: any) => {
+      return scope.row.houseArea || "--";
+    }
+  },
+  {
+    prop: "renovationBudget",
+    label: "装修预算(万元)",
+    render: (scope: any) => {
+      return scope.row.renovationBudget || "--";
+    }
+  },
+  {
+    prop: "city",
+    label: "所在城市",
+    render: (scope: any) => {
+      return scope.row.city || "--";
+    }
+  },
+  {
+    prop: "hasCommunicated",
+    label: "状态",
+    render: (scope: any) => {
+      const hasCommunicated = scope.row.hasCommunicated;
+      const statusMap: Record<string, { text: string; type: string }> = {
+        true: { text: "已沟通", type: "success" },
+        false: { text: "未沟通", type: "info" }
+      };
+      const statusKey = String(hasCommunicated);
+      const statusInfo = statusMap[statusKey] || { text: "--", type: "info" };
+      return (
+        <el-tag type={statusInfo.type as any} size="small">
+          {statusInfo.text}
+        </el-tag>
+      );
+    },
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: communicationStatusEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "createdTime",
+    label: "发布时间",
+    render: (scope: any) => {
+      if (scope.row.createdTime) {
+        return scope.row.createdTime.replace(/-/g, "/");
+      }
+      return "--";
+    }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 180 }
+]);
+
+// 页面加载时触发查询
+onMounted(() => {
+  proTable.value?.getTableList();
+});
+
+// 从其他页面返回时触发查询
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 307 - 0
src/views/storeDecoration/decorationCompanyDetail.vue

@@ -0,0 +1,307 @@
+<template>
+  <div class="detail-container">
+    <div class="card content-box">
+      <div class="detail-header">
+        <h3>详情</h3>
+        <el-button text @click="handleClose">
+          <el-icon><Close /></el-icon>
+        </el-button>
+      </div>
+      <el-form ref="formRef" :model="formData" label-width="140px" label-position="right">
+        <el-form-item label="用户头像">
+          <el-image
+            v-if="formData.storeAvatar"
+            :src="formData.storeAvatar"
+            style="width: 100px; height: 100px; border-radius: 4px"
+            fit="cover"
+            :preview-src-list="[formData.storeAvatar]"
+          />
+          <span v-else style="color: #999">暂无头像</span>
+        </el-form-item>
+
+        <el-form-item label="用户昵称">
+          <el-input v-model="formData.userNickname" placeholder="请输入" disabled />
+        </el-form-item>
+
+        <el-form-item label="状态">
+          <el-tag :type="formData.hasCommunicated ? 'success' : 'info'" size="default">
+            {{ formData.hasCommunicated ? "已沟通" : "未沟通" }}
+          </el-tag>
+        </el-form-item>
+
+        <el-form-item label="发布时间">
+          <el-input v-model="formData.createdTime" placeholder="请输入" disabled />
+        </el-form-item>
+
+        <el-form-item label="装修类型">
+          <el-radio-group v-model="formData.renovationType" disabled>
+            <el-radio :label="1">新房装修</el-radio>
+            <el-radio :label="2">旧房改造</el-radio>
+            <el-radio :label="3">局部装修</el-radio>
+          </el-radio-group>
+        </el-form-item>
+
+        <el-form-item label="房屋面积(㎡)" required>
+          <el-input v-model="formData.houseArea" placeholder="请输入" disabled />
+        </el-form-item>
+
+        <el-form-item label="装修预算(万元)" required>
+          <el-input v-model="formData.renovationBudget" placeholder="请输入" disabled />
+        </el-form-item>
+
+        <el-form-item label="详细需求">
+          <el-input
+            v-model="formData.detailedRequirement"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入"
+            disabled
+          />
+        </el-form-item>
+
+        <el-form-item label="期望装修时间" required>
+          <el-date-picker
+            v-model="formData.expectedRenovationTime"
+            type="date"
+            placeholder="请选择"
+            value-format="YYYY-MM-DD"
+            style="width: 100%"
+            disabled
+          />
+        </el-form-item>
+
+        <el-form-item label="上传房屋图纸" required>
+          <el-upload
+            v-model:file-list="fileList"
+            list-type="picture-card"
+            :disabled="true"
+            :on-preview="handlePictureCardPreview"
+            :on-remove="handleRemove"
+          >
+            <el-icon><Plus /></el-icon>
+          </el-upload>
+          <div class="upload-tip">({{ fileList.length }}/9)</div>
+        </el-form-item>
+
+        <el-form-item label="联系人" required>
+          <el-input v-model="formData.contactName" placeholder="请输入" disabled />
+        </el-form-item>
+
+        <el-form-item label="联系电话" required>
+          <el-input v-model="formData.contactPhone" placeholder="请输入" disabled />
+        </el-form-item>
+
+        <el-form-item label="所在城市" required>
+          <el-input v-model="formData.city" placeholder="请选择" disabled />
+        </el-form-item>
+
+        <el-form-item label="详细地址">
+          <el-input
+            v-model="formData.detailedAddress"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入"
+            disabled
+          />
+        </el-form-item>
+      </el-form>
+
+      <div class="detail-footer">
+        <el-button @click="handleClose">返回</el-button>
+        <el-button type="primary" @click="handleContact">联系业主</el-button>
+      </div>
+    </div>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :url-list="imageViewerUrlList"
+      :initial-index="imageViewerInitialIndex"
+      @close="imageViewerVisible = false"
+    />
+  </div>
+</template>
+
+<script setup lang="ts" name="decorationCompanyDetail">
+import { ref, onMounted, watch } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { Plus, Close } from "@element-plus/icons-vue";
+import type { UploadFile } from "element-plus";
+import { getDecorationDetail } from "@/api/modules/storeDecoration";
+
+const route = useRoute();
+const router = useRouter();
+
+const formRef = ref();
+const formData = ref<any>({
+  storeAvatar: "",
+  userNickname: "",
+  hasCommunicated: false,
+  createdTime: "",
+  requirementTitle: "",
+  renovationType: 1,
+  houseArea: "",
+  renovationBudget: "",
+  detailedRequirement: "",
+  expectedRenovationTime: "",
+  contactName: "",
+  contactPhone: "",
+  city: "",
+  detailedAddress: "",
+  attachmentUrls: []
+});
+
+const fileList = ref<UploadFile[]>([]);
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
+
+// 联系业主
+const handleContact = () => {
+  // TODO: 实现联系业主功能
+  ElMessage.info("联系业主功能待开发");
+};
+
+// 获取详情数据
+const initData = async () => {
+  const id = route.query.id;
+  if (!id) {
+    handleClose();
+    return;
+  }
+
+  try {
+    // 调用 /renovation/requirement/getDetail 接口
+    const res = await getDecorationDetail({ id: id as string });
+    if (res.code == 200 || res.code == 0) {
+      const data = res.data || res.data?.data || {};
+      formData.value = {
+        storeAvatar: data.storeAvatar || "",
+        userNickname: data.storeName || data.userNickname || data.nickname || "",
+        hasCommunicated: data.hasCommunicated ?? false,
+        createdTime: data.createdTime || "",
+        requirementTitle: data.requirementTitle || "",
+        renovationType: data.renovationType || 1,
+        houseArea: data.houseArea || "",
+        renovationBudget: data.renovationBudget || "",
+        detailedRequirement: data.detailedRequirement || "",
+        expectedRenovationTime: data.expectedRenovationTime || "",
+        contactName: data.contactName || "",
+        contactPhone: data.contactPhone || "",
+        city: data.city || "",
+        detailedAddress: data.detailedAddress || "",
+        attachmentUrls: data.attachmentUrls || []
+      };
+
+      // 处理附件列表
+      if (formData.value.attachmentUrls && formData.value.attachmentUrls.length > 0) {
+        fileList.value = formData.value.attachmentUrls.map((url: string, index: number) => ({
+          uid: index,
+          name: `图片${index + 1}`,
+          url: url,
+          status: "success"
+        }));
+      }
+    }
+  } catch (error: any) {
+    console.error("获取详情失败:", error);
+    // 静默处理错误,不显示错误提示
+  }
+};
+
+// 图片预览
+const handlePictureCardPreview = (file: UploadFile) => {
+  if (file.url) {
+    imageViewerUrlList.value = fileList.value.map((item: UploadFile) => item.url || "").filter(Boolean);
+    imageViewerInitialIndex.value = fileList.value.findIndex((item: UploadFile) => item.uid === file.uid);
+    imageViewerVisible.value = true;
+  }
+};
+
+// 删除图片(详情页禁用,这里只是占位)
+const handleRemove = () => {
+  // 详情页不允许删除
+};
+
+// 关闭页面
+const handleClose = () => {
+  router.go(-1);
+};
+
+// 监听路由变化
+watch(
+  () => route.query.id,
+  () => {
+    if (route.query.id) {
+      initData();
+    }
+  },
+  { immediate: true }
+);
+
+onMounted(() => {
+  if (route.query.id) {
+    initData();
+  }
+});
+</script>
+
+<style lang="scss" scoped>
+.detail-container {
+  width: 100%;
+  min-height: 100%;
+  background-color: white;
+
+  .content-box {
+    padding: 20px;
+  }
+
+  .detail-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #ebeef5;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 500;
+    }
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 20px;
+  }
+
+  :deep(.el-radio-group) {
+    display: flex;
+    gap: 20px;
+  }
+
+  .upload-tip {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+  }
+
+  .detail-footer {
+    text-align: center;
+    padding: 20px 0 0;
+    margin-top: 20px;
+    border-top: 1px solid #ebeef5;
+  }
+
+  :deep(.el-upload--picture-card) {
+    width: 100px;
+    height: 100px;
+  }
+
+  :deep(.el-upload-list--picture-card .el-upload-list__item) {
+    width: 100px;
+    height: 100px;
+  }
+}
+</style>

+ 288 - 0
src/views/storeDecoration/decorationManagement.vue

@@ -0,0 +1,288 @@
+<template>
+  <div class="table-box button-table">
+    <ProTable ref="proTable" :columns="columns" :request-api="getTableList" :init-param="initParam" :data-callback="dataCallback">
+      <!-- 表格 header 按钮 -->
+      <template #tableHeader="scope">
+        <div class="action-buttons">
+          <el-button :icon="Plus" class="button" type="primary" @click="handleAdd"> 新增 </el-button>
+        </div>
+      </template>
+      <!-- 表格操作 -->
+      <template #operation="scope">
+        <el-button v-if="scope.row.auditStatus !== '0'" link type="primary" @click="handleDelete(scope.row)">删除</el-button>
+        <el-button link type="primary" @click="toDetail(scope.row)">查看详情</el-button>
+      </template>
+    </ProTable>
+  </div>
+</template>
+
+<script setup lang="tsx" name="decorationManagement">
+import { reactive, ref, onMounted, onActivated } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
+import ProTable from "@/components/ProTable/index.vue";
+import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
+import { getDecorationPage, deleteDecoration } from "@/api/modules/storeDecoration";
+import { localGet } from "@/utils";
+
+const router = useRouter();
+const proTable = ref<ProTableInstance>();
+
+// 装修类型枚举
+const decorationTypeEnum = [
+  { label: "新房装修", value: "1" },
+  { label: "旧房改造", value: "2" },
+  { label: "局部装修", value: "3" }
+];
+
+// 审核状态枚举 (auditStatus: 0:待审核, 1:审核通过, 2:审核失败)
+const auditStatusEnum = [
+  { label: "待审核", value: "0" },
+  { label: "审核通过", value: "1" },
+  { label: "审核失败", value: "2" }
+];
+
+// 状态枚举 (status: 0:草稿, 1:已发布, 2:已下架)
+const statusEnum = [
+  { label: "草稿", value: "0" },
+  { label: "已发布", value: "1" },
+  { label: "已下架", value: "2" }
+];
+
+// 初始化请求参数
+const initParam = reactive({
+  storeId: localGet("createdId")
+});
+
+// 数据回调处理 - 根据图四的出参格式处理
+const dataCallback = (data: any) => {
+  // 出参格式: { code, data: { current, pages, records[], searchCount, size, total }, msg, success }
+  // httpApi 的响应拦截器会处理响应,返回的 data 可能是整个响应对象或 data 字段
+  // 如果 data 有 data 属性,说明是完整响应,需要取 data.data
+  // 如果 data 直接有 records,说明已经是处理后的数据
+  if (data && data.data && data.data.records) {
+    // 完整响应格式: { code, data: { records, total, ... }, msg, success }
+    return {
+      list: data.data.records || [],
+      total: data.data.total || 0
+    };
+  } else if (data && data.records) {
+    // 已经是 data 字段: { records, total, ... }
+    return {
+      list: data.records || [],
+      total: data.total || 0
+    };
+  }
+  return {
+    list: [],
+    total: 0
+  };
+};
+
+// 获取表格列表
+const getTableList = (params: any) => {
+  let newParams = JSON.parse(JSON.stringify(params));
+  
+  // 转换 ProTable 的分页参数为后端需要的参数
+  // ProTable 使用 pageNum 和 pageSize,后端使用 page 和 size
+  if (newParams.pageNum) {
+    newParams.page = newParams.pageNum;
+    delete newParams.pageNum;
+  }
+  if (newParams.pageSize) {
+    newParams.size = newParams.pageSize;
+    delete newParams.pageSize;
+  }
+  
+  // 处理日期范围 - createdTime 是日期范围选择器返回的数组
+  // 将日期范围转换为 startTime 和 endTime 参数(格式: yyyy-MM-dd)
+  if (newParams.createdTime && Array.isArray(newParams.createdTime) && newParams.createdTime.length === 2) {
+    newParams.startTime = newParams.createdTime[0]; // 开始时间
+    newParams.endTime = newParams.createdTime[1]; // 结束时间
+    delete newParams.createdTime;
+  }
+  
+  // 清理空值参数
+  Object.keys(newParams).forEach(key => {
+    if (newParams[key] === "" || newParams[key] === null || newParams[key] === undefined) {
+      delete newParams[key];
+    }
+  });
+  
+  // 确保 storeId 存在
+  if (!newParams.storeId) {
+    newParams.storeId = localGet("createdId");
+  }
+  
+  return getDecorationPage(newParams);
+};
+
+// 新增按钮点击事件
+const handleAdd = () => {
+  // TODO: 跳转到新增页面或打开新增对话框
+  router.push(`/storeDecorationManagement/add`);
+};
+
+// 跳转详情页
+const toDetail = (row: any) => {
+  router.push(`/storeDecorationManagement/detail?id=${row.id}`);
+};
+
+// 删除装修记录
+const handleDelete = (row: any) => {
+  ElMessageBox.confirm("确定要删除这条装修记录吗?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning"
+  })
+    .then(async () => {
+      try {
+        // 调用删除接口 /renovation/requirement/delete
+        const res: any = await deleteDecoration({ id: row.id });
+        if (res.code === 200 || res.code === 0) {
+          ElMessage.success("删除成功");
+          // 刷新列表
+          proTable.value?.getTableList();
+        } else {
+          ElMessage.error(res.msg || "删除失败");
+        }
+      } catch (error: any) {
+        ElMessage.error(error?.msg || error?.message || "删除失败");
+      }
+    })
+    .catch(() => {
+      // 用户取消删除
+    });
+};
+
+// 表格列配置
+const columns = reactive<ColumnProps<any>[]>([
+  { type: "index", fixed: "left", label: "序号", width: 80 },
+  {
+    prop: "requirementTitle",
+    label: "标题",
+    render: (scope: any) => {
+      return scope.row.requirementTitle || "--";
+    },
+    search: {
+      el: "input",
+      props: { placeholder: "请输入" }
+    }
+  },
+  {
+    prop: "renovationType",
+    label: "装修类型",
+    render: (scope: any) => {
+      const type = decorationTypeEnum.find(item => item.value === String(scope.row.renovationType));
+      return type ? type.label : "--";
+    },
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: decorationTypeEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "houseArea",
+    label: "面积(㎡)",
+    render: (scope: any) => {
+      return scope.row.houseArea || "--";
+    }
+  },
+  {
+    prop: "renovationBudget",
+    label: "装修预算(万元)",
+    render: (scope: any) => {
+      return scope.row.renovationBudget || "--";
+    }
+  },
+  {
+    prop: "expectedRenovationTime",
+    label: "期望装修时间",
+    render: (scope: any) => {
+      if (scope.row.expectedRenovationTime) {
+        return scope.row.expectedRenovationTime.replace(/-/g, "/");
+      }
+      return "--";
+    }
+  },
+  {
+    prop: "auditStatus",
+    label: "状态",
+    render: (scope: any) => {
+      // 使用 auditStatus 字段判断状态
+      const auditStatus = scope.row.auditStatus;
+      // 如果 auditStatus 为空、null、undefined,显示 --
+      if (auditStatus === null || auditStatus === undefined || auditStatus === "") {
+        return "--";
+      }
+      const statusMap: Record<string, { text: string; type: string }> = {
+        "0": { text: "待审核", type: "warning" },
+        "1": { text: "审核通过", type: "success" },
+        "2": { text: "审核失败", type: "danger" }
+      };
+      const statusInfo = statusMap[String(auditStatus)];
+      if (!statusInfo) {
+        return "--";
+      }
+      return (
+        <el-tag type={statusInfo.type as any} size="small">
+          {statusInfo.text}
+        </el-tag>
+      );
+    },
+    search: {
+      el: "select",
+      props: { placeholder: "请选择" }
+    },
+    enum: auditStatusEnum,
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "createdTime",
+    label: "提交时间",
+    render: (scope: any) => {
+      if (scope.row.createdTime) {
+        return scope.row.createdTime.replace(/-/g, "/");
+      }
+      return "--";
+    },
+    search: {
+      el: "date-picker",
+      props: {
+        type: "daterange",
+        "range-separator": "至",
+        "start-placeholder": "请输入",
+        "end-placeholder": "请输入",
+        "value-format": "YYYY-MM-DD"
+      },
+      span: 2
+    }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 180 }
+]);
+
+// 页面加载时触发查询
+onMounted(() => {
+  proTable.value?.getTableList();
+});
+
+// 从其他页面返回时触发查询
+onActivated(() => {
+  proTable.value?.getTableList();
+});
+</script>
+
+<style lang="scss" scoped>
+.action-buttons {
+  display: flex;
+  flex: 0 0 auto;
+  gap: 10px;
+  margin-right: 20px;
+  .button {
+    margin-bottom: 0;
+  }
+}
+</style>

+ 302 - 63
src/views/storeDecoration/detail.vue

@@ -1,93 +1,332 @@
 <template>
-  <div class="card content-box">
-    <el-form :model="formData" label-width="140px">
-      <el-row>
-        <el-col :span="12">
-          <el-form-item label="店铺名称 :">
-            <span>{{ formData.storeName }}</span>
-          </el-form-item>
-          <el-form-item label="名称 :">
-            <span>{{ formData.name }}</span>
-          </el-form-item>
-          <el-form-item label="描述 :">
-            <span>{{ formData.description }}</span>
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="状态:">
-            <span>{{ getStatusName(formData.status) }}</span>
-          </el-form-item>
-          <el-form-item label="拒绝原因:" v-if="formData.status === '2'">
-            <span>{{ formData.rejectionReason }}</span>
-          </el-form-item>
-        </el-col>
-      </el-row>
-      <el-row class="text-center" style="margin-top: 20px">
-        <el-col :span="24">
-          <el-button type="primary" @click="goBack"> 确定 </el-button>
-        </el-col>
-      </el-row>
-    </el-form>
+  <div class="detail-container">
+    <div class="card content-box">
+      <div class="detail-header">
+        <h3>详情</h3>
+        <el-button text @click="handleClose">
+          <el-icon><Close /></el-icon>
+        </el-button>
+      </div>
+      <el-form ref="formRef" :model="formData" label-width="140px" label-position="right">
+        <el-row :gutter="40">
+          <!-- 左侧列 -->
+          <el-col :span="12">
+            <el-form-item label="需求标题" required>
+              <el-input v-model="formData.requirementTitle" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="装修类型">
+              <el-radio-group v-model="formData.renovationType" disabled>
+                <el-radio :label="1">新房装修</el-radio>
+                <el-radio :label="2">旧房改造</el-radio>
+                <el-radio :label="3">局部装修</el-radio>
+              </el-radio-group>
+            </el-form-item>
+
+            <el-form-item label="房屋面积(㎡)" required>
+              <el-input v-model="formData.houseArea" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="装修预算(万元)" required>
+              <el-input v-model="formData.renovationBudget" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="详细需求">
+              <el-input
+                v-model="formData.detailedRequirement"
+                type="textarea"
+                :rows="4"
+                placeholder="请输入"
+                disabled
+              />
+            </el-form-item>
+
+            <el-form-item label="期望装修时间" required>
+              <el-date-picker
+                v-model="formData.expectedRenovationTime"
+                type="date"
+                placeholder="请选择"
+                value-format="YYYY-MM-DD"
+                style="width: 100%"
+                disabled
+              />
+            </el-form-item>
+
+            <el-form-item label="上传房屋图纸" required>
+              <el-upload
+                v-model:file-list="fileList"
+                list-type="picture-card"
+                :disabled="true"
+                :on-preview="handlePictureCardPreview"
+                :on-remove="handleRemove"
+              >
+                <el-icon><Plus /></el-icon>
+              </el-upload>
+              <div class="upload-tip">({{ fileList.length }}/9)</div>
+            </el-form-item>
+
+            <el-form-item label="联系人" required>
+              <el-input v-model="formData.contactName" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="联系电话" required>
+              <el-input v-model="formData.contactPhone" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="所在城市" required>
+              <el-input v-model="formData.city" placeholder="请选择" disabled />
+            </el-form-item>
+
+            <el-form-item label="详细地址">
+              <el-input
+                v-model="formData.detailedAddress"
+                type="textarea"
+                :rows="3"
+                placeholder="请输入"
+                disabled
+              />
+            </el-form-item>
+          </el-col>
+
+          <!-- 右侧列 -->
+          <el-col :span="12">
+            <el-form-item label="提交时间">
+              <el-input v-model="formData.createdTime" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="审核状态">
+              <el-input v-model="auditStatusText" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item label="审核时间">
+              <el-input v-model="formData.updatedTime" placeholder="请输入" disabled />
+            </el-form-item>
+
+            <el-form-item v-if="formData.auditStatus === 2" label="拒绝原因">
+              <el-input
+                v-model="formData.rejectionReason"
+                type="textarea"
+                :rows="4"
+                placeholder="请输入"
+                disabled
+              />
+              <div class="form-tip">根据审核状态 对应展示</div>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+
+      <div class="detail-footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleConfirm">确定</el-button>
+      </div>
+    </div>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :url-list="imageViewerUrlList"
+      :initial-index="imageViewerInitialIndex"
+      @close="imageViewerVisible = false"
+    />
   </div>
 </template>
 
-<script setup lang="tsx" name="storeDecorationDetail">
-import { ref, onMounted } from "vue";
+<script setup lang="ts" name="decorationDetail">
+import { ref, computed, onMounted, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
-import { getStaffConfigDeatail } from "@/api/modules/staffConfig";
+import { Plus, Close } from "@element-plus/icons-vue";
+import type { UploadFile } from "element-plus";
+import { getDecorationDetail } from "@/api/modules/storeDecoration";
 
 const route = useRoute();
 const router = useRouter();
 
-const formData = ref({});
+const formRef = ref();
+const formData = ref<any>({
+  requirementTitle: "",
+  renovationType: 1,
+  houseArea: "",
+  renovationBudget: "",
+  detailedRequirement: "",
+  expectedRenovationTime: "",
+  contactName: "",
+  contactPhone: "",
+  city: "",
+  detailedAddress: "",
+  createdTime: "",
+  auditStatus: 0,
+  updatedTime: "",
+  rejectionReason: "",
+  attachmentUrls: []
+});
 
-const id = ref((route.query.id as string) || "");
+const fileList = ref<UploadFile[]>([]);
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
 
-const getStatusName = (status: string) => {
-  switch (status) {
-    case "0":
-      return "待审核";
-    case "1":
-      return "审核通过";
-    case "2":
-      return "审核拒绝";
-    default:
-      return "未知状态";
-  }
-};
-
-onMounted(async () => {
-  await initData();
+// 审核状态文本
+const auditStatusText = computed(() => {
+  const statusMap: Record<number, string> = {
+    0: "待审核",
+    1: "审核通过",
+    2: "审核失败"
+  };
+  return statusMap[formData.value.auditStatus] || "--";
 });
 
+// 获取详情数据
 const initData = async () => {
-  if (id.value) {
-    try {
-      const response = await getStaffConfigDeatail({ id: id.value });
-      if (response.code === 200) {
-        formData.value = response.data;
+  const id = route.query.id;
+  if (!id) {
+    handleClose();
+    return;
+  }
+
+  try {
+    const res = await getDecorationDetail({ id: id as string });
+    if (res.code == 200 || res.code == 0) {
+      const data = res.data || res.data?.data || {};
+      formData.value = {
+        requirementTitle: data.requirementTitle || "",
+        renovationType: data.renovationType || 1,
+        houseArea: data.houseArea || "",
+        renovationBudget: data.renovationBudget || "",
+        detailedRequirement: data.detailedRequirement || "",
+        expectedRenovationTime: data.expectedRenovationTime || "",
+        contactName: data.contactName || "",
+        contactPhone: data.contactPhone || "",
+        city: data.city || "",
+        detailedAddress: data.detailedAddress || "",
+        createdTime: data.createdTime || "",
+        auditStatus: data.auditStatus ?? 0,
+        updatedTime: data.updatedTime || "",
+        rejectionReason: data.rejectionReason || "",
+        attachmentUrls: data.attachmentUrls || []
+      };
+
+      // 处理附件列表
+      if (formData.value.attachmentUrls && formData.value.attachmentUrls.length > 0) {
+        fileList.value = formData.value.attachmentUrls.map((url: string, index: number) => ({
+          uid: index,
+          name: `图片${index + 1}`,
+          url: url,
+          status: "success"
+        }));
       }
-    } catch (error) {
-      ElMessage.error("获取详情失败");
     }
+  } catch (error: any) {
+    // 静默处理错误,不显示错误提示
+    console.error("获取详情失败:", error);
   }
 };
 
-const goBack = () => {
+// 图片预览
+const handlePictureCardPreview = (file: UploadFile) => {
+  if (file.url) {
+    imageViewerUrlList.value = fileList.value.map((item: UploadFile) => item.url || "").filter(Boolean);
+    imageViewerInitialIndex.value = fileList.value.findIndex((item: UploadFile) => item.uid === file.uid);
+    imageViewerVisible.value = true;
+  }
+};
+
+// 删除图片(详情页禁用,这里只是占位)
+const handleRemove = () => {
+  // 详情页不允许删除
+};
+
+// 关闭页面
+const handleClose = () => {
   router.go(-1);
 };
+
+// 确定按钮
+const handleConfirm = () => {
+  handleClose();
+};
+
+// 监听路由变化
+watch(
+  () => route.query.id,
+  () => {
+    if (route.query.id) {
+      initData();
+    }
+  },
+  { immediate: true }
+);
+
+onMounted(() => {
+  if (route.query.id) {
+    initData();
+  }
+});
 </script>
 
 <style lang="scss" scoped>
-.el-form {
+.detail-container {
   width: 100%;
-  .text-center {
+  min-height: 100%;
+  background-color: white;
+
+  .content-box {
+    padding: 20px;
+  }
+
+  .detail-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #ebeef5;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 500;
+    }
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 20px;
+  }
+
+  :deep(.el-radio-group) {
+    display: flex;
+    gap: 20px;
+  }
+
+  .upload-tip {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+  }
+
+  .form-tip {
+    font-size: 12px;
+    color: #999;
+    margin-top: 5px;
+    text-align: right;
+  }
+
+  .detail-footer {
     text-align: center;
+    padding: 20px 0 0;
+    margin-top: 20px;
+    border-top: 1px solid #ebeef5;
+  }
+
+  :deep(.el-upload--picture-card) {
+    width: 100px;
+    height: 100px;
+  }
+
+  :deep(.el-upload-list--picture-card .el-upload-list__item) {
+    width: 100px;
+    height: 100px;
   }
-}
-.el-col {
-  box-sizing: border-box;
-  padding-right: 10px;
 }
 </style>