ソースを参照

feat(store): 新增酒单管理页面

- 实现酒单和推荐酒水的标签页切换功能
- 支持按酒水和餐食分类筛选显示
- 添加新建、编辑、删除酒单的功能
- 支持设为推荐和取消推荐操作
- 实现酒单列表分页展示
- 提供批量导入酒单的入口(待完善)
- 完成酒单详情弹窗表单设计与交互
- 集成图片
spy 2 週間 前
コミット
be90eddd09
1 ファイル変更858 行追加0 行削除
  1. 858 0
      src/views/storeDecoration/wineMenuManagement/index.vue

+ 858 - 0
src/views/storeDecoration/wineMenuManagement/index.vue

@@ -0,0 +1,858 @@
+<template>
+  <div class="menu-management-container">
+    <!-- 头部:Tabs和新建按钮 -->
+    <div class="header-section">
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <el-tab-pane label="酒单" name="menu" />
+        <el-tab-pane label="推荐酒水" name="recommended" />
+      </el-tabs>
+      <div class="action-buttons">
+        <el-button type="primary" @click="openCreateDialog"> 新建 </el-button>
+        <el-button type="primary" @click="handleBatchImport"> 批量导入 </el-button>
+      </div>
+    </div>
+
+    <!-- 分类筛选 -->
+    <div v-if="activeTab === 'menu'" class="category-filter">
+      <el-button
+        :type="activeCategory === 'drink' ? 'primary' : ''"
+        :plain="activeCategory !== 'drink'"
+        @click="activeCategory = 'drink'"
+      >
+        酒水
+      </el-button>
+      <el-button
+        :type="activeCategory === 'food' ? 'primary' : ''"
+        :plain="activeCategory !== 'food'"
+        @click="activeCategory = 'food'"
+      >
+        餐食
+      </el-button>
+    </div>
+
+    <!-- 内容区域 -->
+    <div v-if="filteredDishList.length > 0" class="content-section">
+      <!-- 菜品卡片网格 -->
+      <div class="dish-grid">
+        <div v-for="(dish, index) in paginatedDishList" :key="dish.id" class="dish-card">
+          <div class="dish-image-wrapper">
+            <img v-if="dish.imgUrl" :src="dish.imgUrl" alt="酒单图片" />
+            <div v-else class="image-placeholder">
+              <el-icon><Picture /></el-icon>
+            </div>
+            <el-tag v-if="dish.dishType === 1" type="primary" class="recommend-tag"> 推荐 </el-tag>
+          </div>
+          <div class="dish-info">
+            <div class="dish-name">
+              {{ dish.dishName }}
+            </div>
+            <div class="dish-price">¥{{ dish.dishPrice }}/{{ dish.dishesUnit }}</div>
+            <div v-if="activeTab === 'recommended'" class="dish-recommend-count">{{ dish.likeCount || 0 }}人推荐</div>
+          </div>
+          <div class="dish-actions">
+            <el-button type="primary" link @click="editDish(dish, index)"> 编辑 </el-button>
+            <el-button type="primary" link @click="deleteDishHandler(dish.id!, index)"> 删除 </el-button>
+            <el-button v-if="dish.dishType === 1" type="primary" link @click="cancelRecommend(dish.id!, index)">
+              取消推荐
+            </el-button>
+            <el-button v-else type="primary" link @click="setRecommend(dish.id!, index)"> 设为推荐 </el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 分页 -->
+      <div class="pagination-section">
+        <el-pagination
+          v-model:current-page="pageable.pageNum"
+          v-model:page-size="pageable.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="pageable.total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+
+    <!-- 空状态 -->
+    <div v-else class="empty-state">
+      <el-empty description="暂无数据" />
+      <el-button type="primary" @click="openCreateDialog"> 新建 </el-button>
+    </div>
+
+    <!-- 新建/编辑酒单弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" @close="resetForm">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
+        <el-form-item label="类型" prop="itemType">
+          <el-radio-group v-model="formData.itemType">
+            <el-radio label="drink"> 酒水 </el-radio>
+            <el-radio label="food"> 餐食 </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="名称*" prop="dishName">
+          <el-input v-model="formData.dishName" placeholder="请输入" maxlength="10" show-word-limit clearable />
+        </el-form-item>
+        <el-form-item label="价格 (¥)*" prop="dishPrice">
+          <el-input-number
+            v-model="formData.dishPrice"
+            :min="0"
+            :max="99999.99"
+            :precision="2"
+            :step="0.01"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="成本价 (¥)*" prop="costPrice">
+          <el-input-number
+            v-model="formData.costPrice"
+            :min="0"
+            :max="99999.99"
+            :precision="2"
+            :step="0.01"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item v-if="formData.itemType === 'drink'" label="品类" prop="category">
+          <el-input v-model="formData.category" placeholder="请输入" clearable />
+        </el-form-item>
+        <el-form-item v-if="formData.itemType === 'drink'" label="酒精度 (%vol)" prop="alcoholContent">
+          <el-input-number
+            v-model="formData.alcoholContent"
+            :min="0"
+            :max="100"
+            :precision="1"
+            :step="0.1"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item v-if="formData.itemType === 'drink'" label="风味" prop="flavor">
+          <el-input v-model="formData.flavor" placeholder="请输入" clearable />
+        </el-form-item>
+        <el-form-item label="单位" prop="dishesUnit">
+          <el-select v-model="formData.dishesUnit" placeholder="请选择" style="width: 100%">
+            <el-option v-for="unit in unitOptions" :key="unit.value" :label="unit.label" :value="unit.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="图片*" prop="imgUrl">
+          <UploadImg
+            v-model:image-url="formData.imgUrl"
+            :width="'200px'"
+            :height="'200px'"
+            :file-size="9999"
+            :api="uploadImg"
+            :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
+            :border-radius="'8px'"
+          />
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input
+            v-model="formData.description"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入"
+            maxlength="300"
+            show-word-limit
+            clearable
+          />
+        </el-form-item>
+        <el-form-item label="" prop="dishType">
+          <el-checkbox v-model="formData.isRecommended"> 设为推荐 </el-checkbox>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialogVisible = false"> 取消 </el-button>
+          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
+            {{ editId ? "确定" : "新建" }}
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 批量导入弹窗 -->
+    <el-dialog v-model="batchImportVisible" title="批量导入" width="500px">
+      <el-upload
+        ref="uploadRef"
+        :auto-upload="false"
+        :on-change="handleFileChange"
+        :on-remove="handleFileRemove"
+        :limit="1"
+        accept=".xlsx,.xls"
+        drag
+      >
+        <el-icon class="el-icon--upload">
+          <upload-filled />
+        </el-icon>
+        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
+        <template #tip>
+          <div class="el-upload__tip">只能上传 xlsx/xls 文件</div>
+        </template>
+      </el-upload>
+      <div class="import-tip">
+        <el-link type="primary" :underline="false" @click="downloadTemplate"> 下载导入模板 </el-link>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="batchImportVisible = false"> 取消 </el-button>
+          <el-button type="primary" :loading="importLoading" @click="handleImportSubmit"> 确定 </el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted, nextTick, watch } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import type { FormInstance, FormRules, UploadFile } from "element-plus";
+import { Picture, UploadFilled } from "@element-plus/icons-vue";
+import UploadImg from "@/components/Upload/Img.vue";
+import { uploadImg } from "@/api/modules/newLoginApi";
+import { localGet } from "@/utils";
+import { createOrUpdateDish, getDishList, getDishDetail, deleteDish } from "@/api/modules/storeDecoration";
+
+// 酒单接口
+interface Dish {
+  id?: string | number;
+  dishName: string;
+  dishPrice: number;
+  costPrice: number;
+  dishesUnit: string;
+  imgUrl: string;
+  description?: string;
+  dishType: number; // 0:未推荐, 1:推荐
+  likeCount?: number;
+  itemType?: string; // 'drink' | 'food'
+  category?: string;
+  alcoholContent?: number;
+  flavor?: string;
+}
+
+const dialogVisible = ref(false);
+const formRef = ref<FormInstance>();
+const submitLoading = ref(false);
+const activeTab = ref<"menu" | "recommended">("menu");
+const activeCategory = ref<"drink" | "food">("drink");
+const editIndex = ref<number | null>(null);
+const editId = ref<string | number | null>(null);
+const batchImportVisible = ref(false);
+const importLoading = ref(false);
+const uploadRef = ref();
+const importFile = ref<UploadFile | null>(null);
+
+// 单位选项
+const unitOptions = [
+  { label: "份", value: "份" },
+  { label: "个", value: "个" },
+  { label: "碗", value: "碗" },
+  { label: "杯", value: "杯" },
+  { label: "盘", value: "盘" },
+  { label: "次", value: "次" },
+  { label: "节", value: "节" },
+  { label: "瓶", value: "瓶" }
+];
+
+// 酒单列表
+const dishList = ref<Dish[]>([]);
+
+// 根据分类筛选后的列表
+const filteredDishList = computed(() => {
+  if (activeTab.value === "recommended") {
+    return dishList.value;
+  }
+  // 在菜单tab下,根据分类筛选
+  return dishList.value.filter(dish => {
+    if (activeCategory.value === "drink") {
+      return dish.itemType === "drink";
+    } else {
+      return dish.itemType === "food";
+    }
+  });
+});
+
+// 分页数据
+const pageable = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 分页后的酒单列表
+const paginatedDishList = computed(() => {
+  const list = filteredDishList.value;
+  const start = (pageable.pageNum - 1) * pageable.pageSize;
+  const end = start + pageable.pageSize;
+  return list.slice(start, end);
+});
+
+// 弹窗标题
+const dialogTitle = computed(() => (editIndex.value !== null ? "编辑" : "新建"));
+
+// 表单数据
+const formData = reactive({
+  itemType: "drink" as "drink" | "food",
+  dishName: "",
+  dishPrice: undefined as number | undefined,
+  costPrice: undefined as number | undefined,
+  dishesUnit: "份",
+  imgUrl: "",
+  description: "",
+  isRecommended: false,
+  category: "",
+  alcoholContent: undefined as number | undefined,
+  flavor: ""
+});
+
+// 表单校验规则
+const rules = reactive<FormRules>({
+  dishName: [
+    { required: true, message: "请输入名称", trigger: "blur" },
+    { max: 10, message: "名称不能超过10个字", trigger: "blur" }
+  ],
+  dishPrice: [
+    { required: true, message: "请输入价格", trigger: "blur" },
+    {
+      validator: (rule, value, callback) => {
+        if (value === undefined || value === null || value === "") {
+          callback(new Error("请输入价格"));
+          return;
+        }
+        if (value < 0 || value > 99999.99) {
+          callback(new Error("价格必须在0-99999.99之间"));
+          return;
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  costPrice: [
+    { required: true, message: "请输入成本价", trigger: "blur" },
+    {
+      validator: (rule, value, callback) => {
+        if (value === undefined || value === null || value === "") {
+          callback(new Error("请输入成本价"));
+          return;
+        }
+        if (value < 0 || value > 99999.99) {
+          callback(new Error("成本价必须在0-99999.99之间"));
+          return;
+        }
+        callback();
+      },
+      trigger: "blur"
+    }
+  ],
+  dishesUnit: [{ required: true, message: "请选择单位", trigger: "change" }],
+  imgUrl: [{ required: true, message: "请上传图片", trigger: "change" }],
+  description: [{ max: 300, message: "描述不能超过300个字", trigger: "blur" }]
+});
+
+// Tab切换
+const handleTabClick = async val => {
+  pageable.pageNum = 1;
+  nextTick(async () => {
+    await loadDishList();
+    updatePagination();
+  });
+};
+
+// 分类切换
+const handleCategoryChange = () => {
+  pageable.pageNum = 1;
+  updatePagination();
+};
+
+// 监听分类变化
+watch(
+  () => activeCategory.value,
+  () => {
+    handleCategoryChange();
+  }
+);
+
+// 更新分页总数
+const updatePagination = () => {
+  pageable.total = filteredDishList.value.length;
+};
+
+// 分页大小改变
+const handleSizeChange = (size: number) => {
+  pageable.pageSize = size;
+  pageable.pageNum = 1;
+  updatePagination();
+};
+
+// 当前页改变
+const handleCurrentChange = (page: number) => {
+  pageable.pageNum = page;
+};
+
+// 打开新建弹窗
+const openCreateDialog = () => {
+  editIndex.value = null;
+  editId.value = null;
+  resetForm();
+  dialogVisible.value = true;
+};
+
+// 编辑酒单
+const editDish = async (dish: Dish, index: number) => {
+  if (!dish.id) {
+    ElMessage.warning("酒单ID不存在");
+    return;
+  }
+
+  try {
+    // 调用获取酒单详情接口
+    const res: any = await getDishDetail({ id: dish.id });
+    if (res && (res.code === 200 || res.code === "200") && res.data) {
+      const dishDetail = res.data;
+      editId.value = dishDetail.id;
+      formData.itemType = dishDetail.itemType || "drink";
+      formData.dishName = dishDetail.dishName || "";
+      formData.dishPrice = dishDetail.dishPrice;
+      formData.costPrice = dishDetail.costPrice;
+      formData.dishesUnit = dishDetail.dishesUnit || "份";
+      formData.imgUrl = dishDetail.imgUrl || "";
+      formData.description = dishDetail.description || "";
+      formData.isRecommended = dishDetail.dishType === 1;
+      formData.category = dishDetail.category || "";
+      formData.alcoholContent = dishDetail.alcoholContent;
+      formData.flavor = dishDetail.flavor || "";
+      dialogVisible.value = true;
+    } else {
+      ElMessage.error(res?.msg || "获取酒单详情失败");
+    }
+  } catch (error: any) {
+    console.error("获取酒单详情失败:", error);
+    ElMessage.error(error?.msg || "获取酒单详情失败,请重试");
+  }
+};
+
+// 删除酒单
+const deleteDishHandler = async (id: string | number, index: number) => {
+  try {
+    await ElMessageBox.confirm("确认删除该酒单吗?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+
+    const dish = dishList.value.find(d => d.id === id);
+    if (!dish) {
+      ElMessage.error("未找到要删除的酒单");
+      return;
+    }
+
+    // 根据当前tab确定dishType:0表示菜单, 1表示推荐
+    const dishType = activeTab.value === "recommended" ? 1 : 0;
+
+    // 调用删除接口
+    const params = {
+      dishType: dishType,
+      ids: id
+    };
+
+    const res: any = await deleteDish(params);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("删除成功");
+      await loadDishList();
+      updatePagination();
+      // 如果当前页没有数据了,回到上一页
+      if (paginatedDishList.value.length === 0 && pageable.pageNum > 1) {
+        pageable.pageNum--;
+      }
+    } else {
+      ElMessage.error(res?.msg || "删除失败");
+    }
+  } catch (error: any) {
+    if (error !== "cancel") {
+      console.error("删除酒单失败:", error);
+      ElMessage.error(error?.msg || "删除失败,请重试");
+    }
+  }
+};
+
+// 设为推荐
+const setRecommend = async (id: string | number, index: number) => {
+  try {
+    const dish = dishList.value.find(d => d.id === id);
+    if (!dish) {
+      ElMessage.error("未找到酒单");
+      return;
+    }
+
+    const userInfo: any = localGet("geeker-user")?.userInfo || {};
+    const storeId = userInfo.storeId;
+    if (!storeId) {
+      ElMessage.error("未找到店铺ID");
+      return;
+    }
+
+    // 通过更新接口设置推荐
+    const params: any = {
+      id: dish.id,
+      storeId: Number(storeId),
+      dishName: dish.dishName,
+      dishPrice: dish.dishPrice,
+      costPrice: dish.costPrice,
+      dishesUnit: dish.dishesUnit,
+      imgUrl: dish.imgUrl,
+      description: dish.description || "",
+      dishType: 1, // 设置为推荐
+      itemType: dish.itemType || "drink",
+      category: dish.category || "",
+      alcoholContent: dish.alcoholContent,
+      flavor: dish.flavor || ""
+    };
+
+    const res: any = await createOrUpdateDish(params);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("设置推荐成功");
+      await loadDishList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "设置推荐失败");
+    }
+  } catch (error: any) {
+    console.error("设置推荐失败:", error);
+    ElMessage.error(error?.msg || "设置推荐失败,请重试");
+  }
+};
+
+// 取消推荐
+const cancelRecommend = async (id: string | number, index: number) => {
+  try {
+    const dish = dishList.value.find(d => d.id === id);
+    if (!dish) {
+      ElMessage.error("未找到酒单");
+      return;
+    }
+
+    const userInfo: any = localGet("geeker-user")?.userInfo || {};
+    const storeId = userInfo.storeId;
+    if (!storeId) {
+      ElMessage.error("未找到店铺ID");
+      return;
+    }
+
+    // 通过更新接口取消推荐
+    const params: any = {
+      id: dish.id,
+      storeId: Number(storeId),
+      dishName: dish.dishName,
+      dishPrice: dish.dishPrice,
+      costPrice: dish.costPrice,
+      dishesUnit: dish.dishesUnit,
+      imgUrl: dish.imgUrl,
+      description: dish.description || "",
+      dishType: 0, // 设置为未推荐
+      itemType: dish.itemType || "drink",
+      category: dish.category || "",
+      alcoholContent: dish.alcoholContent,
+      flavor: dish.flavor || ""
+    };
+
+    const res: any = await createOrUpdateDish(params);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("取消推荐成功");
+      await loadDishList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "取消推荐失败");
+    }
+  } catch (error: any) {
+    console.error("取消推荐失败:", error);
+    ElMessage.error(error?.msg || "取消推荐失败,请重试");
+  }
+};
+
+// 重置表单
+const resetForm = () => {
+  formData.itemType = "drink";
+  formData.dishName = "";
+  formData.dishPrice = undefined;
+  formData.costPrice = undefined;
+  formData.dishesUnit = "份";
+  formData.imgUrl = "";
+  formData.description = "";
+  formData.isRecommended = false;
+  formData.category = "";
+  formData.alcoholContent = undefined;
+  formData.flavor = "";
+  editId.value = null;
+  formRef.value?.clearValidate();
+};
+
+// 提交表单
+const handleSubmit = async () => {
+  if (!formRef.value) return;
+
+  try {
+    await formRef.value.validate();
+  } catch (error) {
+    ElMessage.warning("请完善必填项");
+    return;
+  }
+
+  submitLoading.value = true;
+  try {
+    const userInfo: any = localGet("geeker-user")?.userInfo || {};
+    const storeId = userInfo.storeId;
+    if (!storeId) {
+      ElMessage.error("未找到店铺ID");
+      submitLoading.value = false;
+      return;
+    }
+
+    // 构建请求参数
+    const params: any = {
+      storeId: Number(storeId),
+      dishName: formData.dishName,
+      dishPrice: formData.dishPrice!,
+      costPrice: formData.costPrice!,
+      dishesUnit: formData.dishesUnit,
+      imgUrl: formData.imgUrl,
+      description: formData.description || "",
+      dishType: formData.isRecommended ? 1 : 0,
+      itemType: formData.itemType,
+      category: formData.category || "",
+      alcoholContent: formData.alcoholContent,
+      flavor: formData.flavor || ""
+    };
+
+    // 如果是编辑,添加id
+    if (editId.value) {
+      params.id = editId.value;
+    }
+
+    const res: any = await createOrUpdateDish(params);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success(editId.value ? "编辑成功" : "新建成功");
+      dialogVisible.value = false;
+      resetForm();
+      // 重新加载酒单列表
+      await loadDishList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || (editId.value ? "编辑失败" : "新建失败"));
+    }
+  } catch (error: any) {
+    console.error("操作失败:", error);
+    ElMessage.error(error?.msg || "操作失败,请重试");
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+// 加载酒单列表
+const loadDishList = async () => {
+  try {
+    const userInfo: any = localGet("geeker-user")?.userInfo || {};
+    const storeId = userInfo.storeId;
+    if (!storeId) {
+      console.warn("未找到店铺ID");
+      return;
+    }
+
+    // 根据tab切换调用不同的dishType参数
+    // dishType: 0表示菜单, 1表示推荐
+    const dishType = activeTab.value === "recommended" ? 1 : 0;
+    const res: any = await getDishList({ storeId: Number(storeId), dishType });
+
+    if (res && (res.code === 200 || res.code === "200") && res.data) {
+      const dataList = Array.isArray(res.data) ? res.data : [];
+      dishList.value = dataList.map((dish: any) => ({
+        id: dish.id,
+        dishName: dish.dishName || "",
+        dishPrice: dish.dishPrice || 0,
+        costPrice: dish.costPrice || 0,
+        dishesUnit: dish.dishesUnit || "份",
+        imgUrl: dish.imgUrl || "",
+        description: dish.description || "",
+        dishType: dish.dishType !== undefined ? dish.dishType : 0,
+        likeCount: dish.likeCount || 0,
+        itemType: dish.itemType || "drink",
+        category: dish.category || "",
+        alcoholContent: dish.alcoholContent,
+        flavor: dish.flavor || ""
+      }));
+    }
+  } catch (error) {
+    console.error("获取酒单列表失败:", error);
+  }
+};
+
+// 批量导入
+const handleBatchImport = () => {
+  batchImportVisible.value = true;
+};
+
+// 文件选择
+const handleFileChange = (file: UploadFile) => {
+  importFile.value = file;
+};
+
+// 文件移除
+const handleFileRemove = () => {
+  importFile.value = null;
+};
+
+// 下载模板
+const downloadTemplate = () => {
+  ElMessage.info("模板下载功能开发中");
+  // TODO: 实现模板下载
+};
+
+// 提交导入
+const handleImportSubmit = async () => {
+  if (!importFile.value) {
+    ElMessage.warning("请先选择文件");
+    return;
+  }
+
+  importLoading.value = true;
+  try {
+    // TODO: 实现批量导入逻辑
+    ElMessage.info("批量导入功能开发中");
+    batchImportVisible.value = false;
+  } catch (error: any) {
+    ElMessage.error(error?.msg || "导入失败,请重试");
+  } finally {
+    importLoading.value = false;
+  }
+};
+
+// 页面初始化
+onMounted(async () => {
+  await loadDishList();
+  updatePagination();
+});
+</script>
+
+<style scoped lang="scss">
+.menu-management-container {
+  min-height: 100%;
+  padding: 20px;
+  background-color: white;
+  .header-section {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 24px;
+    :deep(.el-tabs) {
+      flex: 1;
+      .el-tabs__header {
+        margin: 0;
+      }
+    }
+    .action-buttons {
+      display: flex;
+      gap: 10px;
+    }
+  }
+  .category-filter {
+    display: flex;
+    gap: 10px;
+    margin-bottom: 20px;
+  }
+  .content-section {
+    .dish-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+      gap: 20px;
+      margin-bottom: 24px;
+      .dish-card {
+        overflow: hidden;
+        background-color: white;
+        border-radius: 8px;
+        box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
+        transition: all 0.3s;
+        &:hover {
+          box-shadow: 0 4px 16px 0 rgb(0 0 0 / 15%);
+        }
+        .dish-image-wrapper {
+          position: relative;
+          width: 100%;
+          height: 180px;
+          overflow: hidden;
+          background-color: #f0f0f0;
+          img {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+          .image-placeholder {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            width: 100%;
+            height: 100%;
+            color: #909399;
+            .el-icon {
+              font-size: 48px;
+            }
+          }
+          .recommend-tag {
+            position: absolute;
+            top: 8px;
+            right: 8px;
+          }
+        }
+        .dish-info {
+          padding: 12px 16px;
+          .dish-name {
+            margin-bottom: 8px;
+            font-size: 16px;
+            font-weight: bold;
+            color: #000000;
+          }
+          .dish-price {
+            margin-bottom: 4px;
+            font-size: 14px;
+            font-weight: bold;
+            color: #f56c6c;
+          }
+          .dish-recommend-count {
+            font-size: 12px;
+            color: #909399;
+          }
+        }
+        .dish-actions {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 8px;
+          padding: 12px 16px;
+          border-top: 1px solid #f0f0f0;
+        }
+      }
+    }
+    .pagination-section {
+      display: flex;
+      justify-content: flex-end;
+      margin-top: 24px;
+    }
+  }
+  .empty-state {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    align-items: center;
+    justify-content: center;
+    min-height: 400px;
+  }
+  .dialog-footer {
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+  }
+  .import-tip {
+    margin-top: 10px;
+    text-align: center;
+  }
+}
+</style>