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