lxr před 2 měsíci
rodič
revize
1062c4ce1c

+ 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
+  };
+};

+ 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 });

+ 31 - 0
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",

+ 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>