浏览代码

Merge branch 'development' into development_cxs

congxuesong 1 周之前
父节点
当前提交
99a3eebbb8

+ 131 - 2
src/api/modules/storeDecoration.ts

@@ -66,6 +66,11 @@ export const getBusinessTimeList = (params: any) => {
   return httpApi.get(`/alienStorePlatform/storePlatformBusinessInfo/getByStoreId`, params);
 };
 
+//根据门店ID和区域分类查询设施列表
+export const getListByStoreIdAndCategory = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/bathFacilityService/getListByStoreIdAndCategory`, params);
+};
+
 //创建/更新官方相册
 export const createOrUpdateOfficialAlbum = (params: any) => {
   return httpApi.post(`/alienStorePlatform/storePlatformOfficial/createOrUpdateOfficialAlbum`, params);
@@ -87,8 +92,8 @@ export const deleteOfficialImg = (params: any) => {
   return httpApi.post(`alienStore/img/delete`, params);
 };
 //相册内图片查询
-export const getOfficialImgList = (businessId, storeId) => {
-  return httpApi.get(`/alienStore/img/getByBusinessId?businessId=${businessId}&imgType=2&storeId=${storeId}`);
+export const getOfficialImgList = (businessId, storeId, imgType = 2) => {
+  return httpApi.get(`/alienStore/img/getByBusinessId?businessId=${businessId}&imgType=${imgType}&storeId=${storeId}`);
 };
 //新建或修改菜品
 export const createOrUpdateDish = (params: any) => {
@@ -106,6 +111,35 @@ export const deleteDish = (params: any) => {
 export const getDishDetail = (params: any) => {
   return httpApi.get(`/alienStorePlatform/menuPlatform/getMenuInfo`, params);
 };
+// ==================== 酒单管理相关接口(新接口) ====================
+//根据门店ID和菜单类型查询酒单列表
+export const getBarMenuList = (params: { storeId: number; dishMenuType?: number }) => {
+  return httpApi.get(`/alienStorePlatform/barMenu/getListByStoreIdAndType`, params);
+};
+//新增或修改酒单内容
+export const saveOrUpdateBarMenu = (params: any) => {
+  return httpApi.post(`/alienStorePlatform/barMenu/saveOrUpdate`, params);
+};
+//删除酒单
+export const deleteBarMenu = (params: { id: number }) => {
+  return httpApi.get(`/alienStorePlatform/barMenu/delete`, params);
+};
+//下载酒单Excel模板
+export const downloadBarMenuTemplate = () => {
+  return httpApi.get(`/alienStorePlatform/barMenu/downloadTemplate`, {}, { responseType: "blob" });
+};
+//导入酒单Excel
+export const importBarMenuExcel = (formData: FormData, storeId: string | number) => {
+  return httpApi.service.request({
+    method: "POST",
+    url: `/alienStorePlatform/barMenu/importMenu`,
+    params: { storeId },
+    data: formData,
+    headers: {
+      "Content-Type": "multipart/form-data"
+    }
+  });
+};
 //获取所有标签类型
 export const getAllTagType = (params?: any) => {
   return httpApi.get(`/alienStorePlatform/storePlatformTag/getBusinessRelationTagList`, params);
@@ -118,3 +152,98 @@ export const getAllTag = (params: any) => {
 export const saveTag = (params: any) => {
   return httpApi.post(`/alienStorePlatform/storePlatformTag/createOrUpdateTagStoreRelation`, params);
 };
+//新建或修改人员配置
+export const createOrUpdatePersonnel = (params: any) => {
+  return httpApi.post(`/alienStorePlatform/storeStaffConfig/saveOrUpdate`, params);
+};
+//获取门店人员列表
+export const getPersonnelList = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/storeStaffConfig/getListByStoreId`, params);
+};
+//删除人员
+export const deletePersonnel = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/storeStaffConfig/delete`, params);
+};
+//获取人员详情
+export const getPersonnelDetail = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/storeStaffConfig/getById`, params);
+};
+// ==================== 设施与服务相关接口 ====================
+// 服务管理相关接口
+//获取服务列表
+export const getServiceList = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/servicePlatform/getServiceList`, params);
+};
+//新建或修改服务
+export const createOrUpdateService = (params: any) => {
+  return httpApi.post(`/alienStorePlatform/bathFacilityService/saveOrUpdate`, params);
+};
+//获取服务详情
+export const getServiceDetail = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/bathFacilityService/getById`, params);
+};
+//删除服务
+export const deleteService = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/bathFacilityService/delete`, params);
+};
+//下载服务Excel模板
+export const downloadServiceTemplate = () => {
+  return httpApi.get(`/alienStorePlatform/bathFacilityService/downloadTemplate`, {}, { responseType: "blob" });
+};
+//导入服务Excel
+export const importServiceExcel = (formData: FormData, storeId: string | number) => {
+  return httpApi.service.request({
+    method: "POST",
+    url: `/alienStorePlatform/bathFacilityService/importFacility`,
+    params: { storeId },
+    data: formData
+  });
+};
+// 设施管理相关接口
+//获取健身设施列表
+export const getFacilityList = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/sportsEquipmentFacility/getListByStoreIdAndCategory`, params);
+};
+//新建或修改健身设施
+export const createOrUpdateFacility = (params: any) => {
+  return httpApi.post(`/alienStorePlatform/sportsEquipmentFacility/saveOrUpdate`, params);
+};
+//获取健身设施详情
+export const getFacilityDetail = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/bathFacilityService/getById`, params);
+};
+//删除设施
+export const deleteFacility = (params: any) => {
+  return httpApi.get(`/alienStorePlatform/bathFacilityService/delete`, params);
+};
+//下载设施Excel模板
+export const downloadFacilityTemplate = () => {
+  return httpApi.get(`/alienStorePlatform/bathFacilityService/downloadTemplate`, {}, { responseType: "blob" });
+};
+//导入设施Excel
+export const importFacilityExcel = (formData: FormData, storeId: string | number) => {
+  return httpApi.service.request({
+    method: "POST",
+    url: `/alienStorePlatform/bathFacilityService/importFacility`,
+    params: { storeId },
+    data: formData
+  });
+};
+//下载菜单Excel模板
+export const downloadExcelTemplate = () => {
+  return httpApi.get(`/alienStorePlatform/menuPlatform/downloadTemplate`, {}, { responseType: "blob" });
+};
+//导入菜单Excel
+export const importExcel = (formData: FormData, storeId: string | number) => {
+  // 对于GET请求发送FormData,需要直接使用axios实例
+  // axios的get方法可以在config中设置data属性(虽然不符合HTTP规范,但技术上可行)
+  return httpApi.service.request({
+    method: "POST",
+    url: `/alienStorePlatform/menuPlatform/importMenu`,
+    params: { storeId },
+    data: formData,
+    headers: {
+      "Content-Type": "multipart/form-data"
+    }
+  });
+};

+ 28 - 0
src/assets/json/authMenuList.json

@@ -295,6 +295,34 @@
           }
         },
         {
+          "path": "/storeDecoration/personnelConfig",
+          "name": "personnelConfig",
+          "component": "/storeDecoration/personnelConfig/index",
+          "meta": {
+            "icon": "User",
+            "title": "人员配置",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/storeDecoration/facilitiesAndServices",
+          "name": "facilitiesAndServices",
+          "component": "/storeDecoration/facilitiesAndServices/index",
+          "meta": {
+            "icon": "Tools",
+            "title": "设施与服务",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
           "path": "/storeDecoration/storeLabel",
           "name": "storeLabel",
           "component": "/storeDecoration/storeLabel/index",

+ 4 - 1
src/stores/interface/index.ts

@@ -29,7 +29,10 @@ export interface GlobalState {
 /* UserState */
 export interface UserState {
   token: string;
-  userInfo: { name: string };
+  userInfo: {
+    name: string;
+    [key: string]: any;
+  };
 }
 
 /* tabsMenuProps */

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

@@ -1,7 +1,7 @@
 import { defineStore } from "pinia";
 import { AuthState } from "@/stores/interface";
 import { getAuthButtonListApi, getAuthMenuListApi } from "@/api/modules/login";
-import { getFlatMenuList, getShowMenuList, getAllBreadcrumbList } from "@/utils";
+import { getFlatMenuList, getShowMenuList, getAllBreadcrumbList, localGet } from "@/utils";
 import { usePermission } from "@/utils/permission";
 export const useAuthStore = defineStore({
   id: "geeker-auth",
@@ -38,6 +38,10 @@ export const useAuthStore = defineStore({
       const hasPermission = await usePermission();
       const hideMenuNames = ["storeDecoration", "financialManagement", "licenseManagement"];
 
+      // 获取用户信息和businessSection
+      const userInfo = localGet("geeker-user")?.userInfo || {};
+      const businessSection = userInfo.businessSection || localGet("businessSection") || 0;
+
       // 递归处理菜单的显示/隐藏状态
       const processMenus = (menuList: any[]) => {
         menuList.forEach(menu => {
@@ -55,8 +59,45 @@ export const useAuthStore = defineStore({
               }
             }
           }
-          // 递归处理子菜单
+
+          // 根据businessSection判断菜单显示
+          if (menu.meta && menu.meta.title) {
+            switch (menu.meta.title) {
+              case "酒单管理":
+                // 为11时显示酒单管理
+                menu.meta.isHide = businessSection !== 12;
+                break;
+              case "菜单管理":
+                // 为11时不显示菜单管理
+                menu.meta.isHide = businessSection === 12;
+                break;
+              case "人员配置":
+                // 为11时显示人员配置(即人员管理),否则不显示
+                menu.meta.isHide = businessSection !== 12;
+                break;
+              case "设施与服务":
+                // 为4和7时显示设施与服务,否则不显示
+                menu.meta.isHide = ![15, 17].includes(businessSection);
+                break;
+            }
+          }
+
+          // 处理子菜单
           if (menu.children && menu.children.length > 0) {
+            // 检查子菜单中是否有需要显示的菜单项
+            let hasVisibleChild = false;
+            menu.children.forEach((child: any) => {
+              if (!child.meta?.isHide) {
+                hasVisibleChild = true;
+              }
+            });
+
+            // 如果父菜单本身被隐藏,但有可见子菜单,则显示父菜单
+            if (menu.meta?.isHide && hasVisibleChild) {
+              menu.meta.isHide = false;
+            }
+
+            // 递归处理子菜单
             processMenus(menu.children);
           }
         });

+ 1 - 1
src/views/home/components/go-flow.vue

@@ -500,7 +500,7 @@ const handleNextStep = async () => {
       name: ocrResult.value.name,
       appType: 1
     });
-    if (res && res.code == 200 && res.data) {
+    if (res.code === 200) {
       ElMessage.success("身份证识别成功");
       setStep(2);
     } else {

+ 7 - 2
src/views/home/index.vue

@@ -103,10 +103,15 @@ const getUserInfo = async () => {
       phone: geekerUser.userInfo.phone
     };
     const res: any = await getMerchantByPhone(param);
+    console.log("getMerchantByPhone返回数据:", res.data);
     storeId.value = res.data.storeId;
     if (res && res.code == 200 && res.data) {
-      // 更新缓存中的 storeId
-      geekerUser.userInfo.storeId = res.data.storeId;
+      // 更新缓存中的 storeId,确保保留businessSection字段
+      geekerUser.userInfo = {
+        ...geekerUser.userInfo,
+        ...res.data,
+        businessSection: geekerUser.userInfo.businessSection
+      };
       localSet("geeker-user", geekerUser);
       // 同时更新 createdId 缓存
       if (res.data.storeId) {

+ 4 - 2
src/views/login/index.vue

@@ -936,12 +936,14 @@ const handleLogin = async () => {
           try {
             const merchantRes: any = await getMerchantByPhone({ phone: res.data.phone });
             if (merchantRes && merchantRes.code == 200 && merchantRes.data) {
-              // 更新用户信息到本地存储
+              // 更新用户信息到本地存储,确保保留businessSection字段
               const updatedUserInfo = {
                 ...userInfo,
                 userInfo: {
                   ...userInfo.userInfo,
-                  ...merchantRes.data
+                  ...merchantRes.data,
+                  // 确保保留businessSection字段
+                  businessSection: userInfo.userInfo.businessSection
                 }
               };
               localSet("geeker-user", updatedUserInfo);

+ 20 - 20
src/views/storeDecoration/basicStoreInformation/index.vue

@@ -195,26 +195,26 @@
           </el-form-item>
 
           <!-- 到期时间(只读) -->
-          <el-form-item label="到期时间">
-            <el-input v-model="formData.expirationTime" disabled class="readonly-input">
-              <template #prefix>
-                <el-icon>
-                  <Lock />
-                </el-icon>
-              </template>
-            </el-input>
-          </el-form-item>
-
-          <!-- 食品经营许可证到期时间(只读) -->
-          <el-form-item label="食品经营许可证到期时间">
-            <el-input v-model="formData.foodLicenceExpirationTime" disabled class="readonly-input">
-              <template #prefix>
-                <el-icon>
-                  <Lock />
-                </el-icon>
-              </template>
-            </el-input>
-          </el-form-item>
+          <!--          <el-form-item label="到期时间">-->
+          <!--            <el-input v-model="formData.expirationTime" disabled class="readonly-input">-->
+          <!--              <template #prefix>-->
+          <!--                <el-icon>-->
+          <!--                  <Lock />-->
+          <!--                </el-icon>-->
+          <!--              </template>-->
+          <!--            </el-input>-->
+          <!--          </el-form-item>-->
+
+          <!--          &lt;!&ndash; 食品经营许可证到期时间(只读) &ndash;&gt;-->
+          <!--          <el-form-item label="食品经营许可证到期时间">-->
+          <!--            <el-input v-model="formData.foodLicenceExpirationTime" disabled class="readonly-input">-->
+          <!--              <template #prefix>-->
+          <!--                <el-icon>-->
+          <!--                  <Lock />-->
+          <!--                </el-icon>-->
+          <!--              </template>-->
+          <!--            </el-input>-->
+          <!--          </el-form-item>-->
         </div>
       </div>
 

+ 829 - 0
src/views/storeDecoration/facilitiesAndServices/components/FacilityManagement.vue

@@ -0,0 +1,829 @@
+<template>
+  <div class="facility-management-container">
+    <!-- 标签页 -->
+    <div class="header-section">
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <el-tab-pane v-for="tab in tabs" :key="tab.value" :label="tab.label" :name="tab.value" />
+      </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 class="image-upload-section">
+      <div class="section-title">添加实景图片</div>
+      <div class="upload-area">
+        <el-upload
+          v-model:file-list="imageFileList"
+          list-type="picture-card"
+          :limit="20"
+          :on-preview="handlePictureCardPreview"
+          :on-remove="handleRemoveImage"
+          :before-upload="beforeImageUpload"
+          :http-request="handleImageUpload"
+          accept="image/*"
+        >
+          <el-icon><Plus /></el-icon>
+          <div class="upload-tip">({{ imageFileList.length }}/20)</div>
+        </el-upload>
+      </div>
+    </div>
+
+    <!-- 设施列表 -->
+    <div class="facility-list-section">
+      <div class="section-title">设施列表</div>
+      <el-table :data="paginatedList" border style="width: 100%">
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column prop="facilityName" label="名称" min-width="120" />
+        <el-table-column prop="quantity" label="数量" width="100" align="center" />
+        <el-table-column prop="brand" label="品牌" min-width="120" />
+        <el-table-column prop="displayInStoreDetail" label="展示在店铺详情" width="150" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.displayInStoreDetail === 1 ? 'success' : 'info'">
+              {{ row.displayInStoreDetail === 1 ? "显示" : "隐藏" }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="editItem(row)"> 编辑 </el-button>
+            <el-button type="primary" link @click="deleteItem(row)"> 删除 </el-button>
+            <el-button type="primary" link @click="toggleDisplay(row)">
+              {{ row.displayInStoreDetail === 1 ? "隐藏" : "显示" }}
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <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>
+
+    <!-- 新建/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" @close="resetForm">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
+        <el-form-item label="选择分类*" prop="facilityCategory">
+          <el-select v-model="formData.facilityCategory" placeholder="请选择" style="width: 100%">
+            <el-option v-for="tab in tabs" :key="tab.value" :label="tab.label" :value="tab.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="名称*" prop="facilityName">
+          <el-input v-model="formData.facilityName" placeholder="请输入" maxlength="50" clearable />
+        </el-form-item>
+        <el-form-item label="数量" prop="quantity">
+          <el-input-number v-model="formData.quantity" :min="0" :max="9999" placeholder="请输入" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="品牌" prop="brand">
+          <el-input v-model="formData.brand" placeholder="请输入" maxlength="50" clearable />
+        </el-form-item>
+        <el-form-item label="描述" prop="description">
+          <el-input
+            v-model="formData.description"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入"
+            maxlength="500"
+            show-word-limit
+            clearable
+          />
+        </el-form-item>
+        <el-form-item label="展示在店铺详情" prop="displayInStoreDetail">
+          <el-radio-group v-model="formData.displayInStoreDetail">
+            <el-radio :label="1"> 显示 </el-radio>
+            <el-radio :label="0"> 隐藏 </el-radio>
+          </el-radio-group>
+        </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">
+      <div class="import-steps">
+        <div class="import-step">
+          <div class="step-header">
+            <div class="step-number">1</div>
+            <div class="step-title">下载模板</div>
+          </div>
+          <el-button type="primary" @click="downloadTemplate"> 下载excel模板 </el-button>
+        </div>
+        <div class="import-step">
+          <div class="step-header">
+            <div class="step-number">2</div>
+            <div class="step-title">上传文件</div>
+          </div>
+          <el-upload
+            ref="uploadRef"
+            :auto-upload="false"
+            :on-change="handleFileChange"
+            :on-remove="handleFileRemove"
+            :limit="1"
+            accept=".xlsx,.xls"
+            drag
+            class="import-upload"
+          >
+            <el-icon class="el-icon--upload">
+              <UploadFilled />
+            </el-icon>
+            <div class="el-upload__text">点击上传文件或拖拽上传文件</div>
+          </el-upload>
+        </div>
+      </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>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="imageViewerVisible"
+      :url-list="imageViewerUrlList"
+      :initial-index="imageViewerInitialIndex"
+      @close="imageViewerVisible = false"
+    />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import type { FormInstance, FormRules, UploadFile, UploadRequestOptions } from "element-plus";
+import { Plus, UploadFilled } from "@element-plus/icons-vue";
+import { uploadImg } from "@/api/modules/newLoginApi";
+import { localGet } from "@/utils";
+import {
+  getFacilityList,
+  createOrUpdateFacility,
+  getFacilityDetail,
+  deleteFacility,
+  downloadFacilityTemplate,
+  importFacilityExcel,
+  getOfficialImgList,
+  saveOfficialImg
+} from "@/api/modules/storeDecoration";
+
+// 设施接口
+interface Facility {
+  id?: string | number;
+  facilityCategory: number; // 1:有氧区, 2:力量区, 3:单功能机械区
+  facilityName: string;
+  quantity?: number;
+  brand?: string;
+  description?: string;
+  displayInStoreDetail: number; // 0:隐藏, 1:显示
+}
+
+const dialogVisible = ref(false);
+const formRef = ref<FormInstance>();
+const submitLoading = ref(false);
+const activeTab = ref(1); // 有氧区
+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 imageFileList = ref<any[]>([]);
+const imageViewerVisible = ref(false);
+const imageViewerUrlList = ref<string[]>([]);
+const imageViewerInitialIndex = ref(0);
+
+// 标签页配置
+const tabs = [
+  { label: "有氧区", value: 1 },
+  { label: "力量区", value: 2 },
+  { label: "单功能机械区", value: 3 }
+];
+
+// 设施列表
+const facilityList = ref<Facility[]>([]);
+
+// 分页数据
+const pageable = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 分页后的列表
+const paginatedList = computed(() => {
+  const list = facilityList.value.filter(item => item.facilityCategory === activeTab.value);
+  const start = (pageable.pageNum - 1) * pageable.pageSize;
+  const end = start + pageable.pageSize;
+  return list.slice(start, end);
+});
+
+// 弹窗标题
+const dialogTitle = computed(() => (editId.value !== null ? "编辑" : "添加"));
+
+// 表单数据
+const formData = reactive<Facility>({
+  facilityCategory: 1,
+  facilityName: "",
+  quantity: undefined,
+  brand: "",
+  description: "",
+  displayInStoreDetail: 1
+});
+
+// 表单校验规则
+const rules = reactive<FormRules>({
+  facilityCategory: [{ required: true, message: "请选择分类", trigger: "change" }],
+  facilityName: [{ required: true, message: "请输入名称", trigger: "blur" }]
+});
+
+// Tab切换
+const handleTabClick = async () => {
+  pageable.pageNum = 1;
+  // 调用loadFacilityList获取当前选中分类的数据
+  await loadFacilityList(activeTab.value);
+  updatePagination();
+  // 重新获取当前分区的图片列表
+  getImageList();
+};
+
+// 更新分页总数
+const updatePagination = () => {
+  const list = facilityList.value.filter(item => item.facilityCategory === activeTab.value);
+  pageable.total = list.length;
+};
+
+// 分页大小改变
+const handleSizeChange = (size: number) => {
+  pageable.pageSize = size;
+  pageable.pageNum = 1;
+  updatePagination();
+};
+
+// 当前页改变
+const handleCurrentChange = (page: number) => {
+  pageable.pageNum = page;
+};
+
+// 打开新建弹窗
+const openCreateDialog = () => {
+  editId.value = null;
+  resetForm();
+  dialogVisible.value = true;
+};
+
+// 编辑
+const editItem = async (item: Facility) => {
+  if (!item.id) {
+    ElMessage.warning("设施ID不存在");
+    return;
+  }
+
+  try {
+    const res: any = await getFacilityDetail({ id: item.id });
+    if (res && (res.code === 200 || res.code === "200") && res.data) {
+      const detail = res.data;
+      editId.value = detail.id;
+      formData.facilityCategory = detail.facilityCategory || 1;
+      formData.facilityName = detail.facilityName || "";
+      // 新API中没有这些字段,设置为默认值
+      formData.quantity = undefined;
+      formData.brand = "";
+      formData.description = "";
+      formData.displayInStoreDetail = detail.displayInStoreDetail !== undefined ? detail.displayInStoreDetail : 1;
+      dialogVisible.value = true;
+    } else {
+      ElMessage.error(res?.msg || "获取设施详情失败");
+    }
+  } catch (error: any) {
+    console.error("获取设施详情失败:", error);
+    ElMessage.error(error?.msg || "获取设施详情失败,请重试");
+  }
+};
+
+// 删除
+const deleteItem = async (item: Facility) => {
+  try {
+    await ElMessageBox.confirm("确认删除该设施吗?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+
+    if (!item.id) {
+      ElMessage.error("设施ID不存在");
+      return;
+    }
+
+    const res: any = await deleteFacility({ id: item.id });
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("删除成功");
+      await loadFacilityList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "删除失败");
+    }
+  } catch (error: any) {
+    if (error !== "cancel") {
+      console.error("删除设施失败:", error);
+      ElMessage.error(error?.msg || "删除失败,请重试");
+    }
+  }
+};
+
+// 切换显示状态
+const toggleDisplay = async (item: Facility) => {
+  try {
+    if (!item.id) {
+      ElMessage.error("设施ID不存在");
+      return;
+    }
+
+    const newStatus = item.displayInStoreDetail === 1 ? 0 : 1;
+    const res: any = await createOrUpdateFacility({
+      ...item,
+      displayInStoreDetail: newStatus
+    });
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success(newStatus === 1 ? "已显示" : "已隐藏");
+      await loadFacilityList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "操作失败");
+    }
+  } catch (error: any) {
+    console.error("切换状态失败:", error);
+    ElMessage.error(error?.msg || "操作失败,请重试");
+  }
+};
+
+// 图片上传前验证
+const beforeImageUpload = (file: File) => {
+  const isImage = file.type.startsWith("image/");
+  const isLt10M = file.size / 1024 / 1024 < 10;
+
+  if (!isImage) {
+    ElMessage.error("只能上传图片文件!");
+    return false;
+  }
+  if (!isLt10M) {
+    ElMessage.error("图片大小不能超过10MB!");
+    return false;
+  }
+  if (imageFileList.value.length >= 20) {
+    ElMessage.error("最多只能上传20张图片!");
+    return false;
+  }
+  return true;
+};
+
+// 图片上传
+const handleImageUpload = async (options: UploadRequestOptions) => {
+  try {
+    const formData = new FormData();
+    formData.append("file", options.file);
+    const res: any = await uploadImg(formData);
+    if (res && res.code === 200 && res.data) {
+      const imageUrl = Array.isArray(res.data) ? res.data[0] : res.data;
+      options.onSuccess({ fileUrl: imageUrl });
+
+      // 调用saveOfficialImg接口保存图片信息
+      const userInfo: any = localGet("geeker-user")?.userInfo || {};
+      const storeId = userInfo.storeId;
+      if (storeId) {
+        const saveRes: any = await saveOfficialImg([
+          {
+            imgUrl: imageUrl,
+            imgType: 28,
+            businessId: activeTab.value, // 1:有氧区, 2:力量区, 3:单功能机械区
+            storeId: Number(storeId),
+            imgSort: 0 // 增加排序参数,默认值为0
+          }
+        ]);
+        if (!(saveRes && (saveRes.code === 200 || saveRes.code === "200"))) {
+          console.error("保存图片信息失败:", saveRes);
+        }
+      }
+
+      ElMessage.success("图片上传成功");
+    } else {
+      options.onError(new Error(res?.msg || "图片上传失败"));
+    }
+  } catch (error: any) {
+    options.onError(error);
+    ElMessage.error(error?.msg || "图片上传失败");
+  }
+};
+
+// 图片预览
+const handlePictureCardPreview = (file: any) => {
+  const url = file.url || file.response?.fileUrl;
+  if (url) {
+    imageViewerUrlList.value = imageFileList.value.map((item: any) => item.url || item.response?.fileUrl).filter(Boolean);
+    imageViewerInitialIndex.value = imageViewerUrlList.value.indexOf(url);
+    imageViewerVisible.value = true;
+  }
+};
+
+// 删除图片
+const handleRemoveImage = async (file: any) => {
+  const index = imageFileList.value.findIndex(item => item.uid === file.uid);
+  if (index > -1) {
+    try {
+      const userInfo: any = localGet("geeker-user")?.userInfo || {};
+      const storeId = userInfo.storeId;
+      if (storeId) {
+        // 调用删除图片的API,这里假设API为deleteOfficialImg
+        // 注意:实际使用时需要替换为真实的API
+        // await deleteOfficialImg({ id: file.uid, storeId: Number(storeId) });
+      }
+    } catch (error) {
+      console.error("删除图片失败:", error);
+      ElMessage.error("删除图片失败,请重试");
+    }
+    imageFileList.value.splice(index, 1);
+  }
+};
+
+// 重置表单
+const resetForm = () => {
+  formData.facilityCategory = activeTab.value;
+  formData.facilityName = "";
+  formData.quantity = undefined;
+  formData.brand = "";
+  formData.description = "";
+  formData.displayInStoreDetail = 1;
+  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),
+      facilityCategory: formData.facilityCategory,
+      facilityName: formData.facilityName,
+      quantity: formData.quantity,
+      brand: formData.brand || "",
+      description: formData.description || "",
+      displayInStoreDetail: formData.displayInStoreDetail
+    };
+
+    if (editId.value) {
+      params.id = editId.value;
+    }
+
+    const res: any = await createOrUpdateFacility(params);
+    if (res && (res.code === 200 || res.code === "200")) {
+      // 获取保存后的设施ID
+      const facilityId = res.data?.id || (editId.value ? editId.value : null);
+
+      // 如果有图片列表,调用saveOfficialImg接口上传文件信息
+      if (facilityId && imageFileList.value.length > 0) {
+        const userInfo: any = localGet("geeker-user")?.userInfo || {};
+        const storeId = userInfo.storeId;
+        if (storeId) {
+          // 准备图片数据
+          const imageData = imageFileList.value.map((file: any, index: number) => ({
+            imgUrl: file.url || file.response?.fileUrl,
+            imgType: 28, // 设施图片类型
+            businessId: facilityId, // 设施ID
+            storeId: Number(storeId),
+            imgSort: index // 图片排序
+          }));
+
+          // 调用saveOfficialImg接口
+          const saveRes: any = await saveOfficialImg(imageData);
+          if (!(saveRes && (saveRes.code === 200 || saveRes.code === "200"))) {
+            console.error("保存图片信息失败:", saveRes);
+            ElMessage.warning("图片信息保存失败,请稍后重试");
+          }
+        }
+      }
+
+      ElMessage.success(editId.value ? "编辑成功" : "添加成功");
+      dialogVisible.value = false;
+      resetForm();
+      await loadFacilityList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || (editId.value ? "编辑失败" : "添加失败"));
+    }
+  } catch (error: any) {
+    console.error("操作失败:", error);
+    ElMessage.error(error?.msg || "操作失败,请重试");
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+// 加载设施列表
+const loadFacilityList = async (category?: number) => {
+  try {
+    const userInfo: any = localGet("geeker-user")?.userInfo || {};
+    const storeId = userInfo.storeId;
+    if (!storeId) {
+      console.warn("未找到店铺ID");
+      return;
+    }
+
+    // 如果传入了分类则使用传入的分类,否则使用当前选中的标签页分类
+    const facilityCategory = category !== undefined ? category : activeTab.value;
+
+    const res: any = await getFacilityList({
+      storeId: Number(storeId),
+      facilityCategory: facilityCategory
+    });
+    if (res && (res.code === 200 || res.code === "200") && res.data) {
+      const dataList = Array.isArray(res.data) ? res.data : [];
+
+      // 如果是加载指定分类的数据,则只更新该分类的数据
+      if (category !== undefined) {
+        // 先移除该分类的旧数据
+        facilityList.value = facilityList.value.filter(item => item.facilityCategory !== category);
+        // 再添加新数据
+        facilityList.value.push(
+          ...dataList.map((item: any) => ({
+            id: item.id,
+            facilityCategory: item.facilityCategory || category,
+            facilityName: item.facilityName || "",
+            quantity: item.quantity,
+            brand: item.brand || "",
+            description: item.description || "",
+            displayInStoreDetail: item.displayInStoreDetail !== undefined ? item.displayInStoreDetail : 1
+          }))
+        );
+      } else {
+        // 如果是加载所有数据,则直接替换
+        facilityList.value = dataList.map((item: any) => ({
+          id: item.id,
+          facilityCategory: item.facilityCategory || 1,
+          facilityName: item.facilityName || "",
+          quantity: item.quantity,
+          brand: item.brand || "",
+          description: item.description || "",
+          displayInStoreDetail: item.displayInStoreDetail !== undefined ? item.displayInStoreDetail : 1
+        }));
+      }
+    }
+  } catch (error) {
+    console.error("获取设施列表失败:", error);
+  }
+};
+
+// 批量导入
+const handleBatchImport = () => {
+  batchImportVisible.value = true;
+};
+
+// 文件选择
+const handleFileChange = (file: UploadFile) => {
+  importFile.value = file;
+};
+
+// 文件移除
+const handleFileRemove = () => {
+  importFile.value = null;
+};
+
+// 下载模板
+const downloadTemplate = async () => {
+  try {
+    const res: any = await downloadFacilityTemplate();
+    const blob =
+      res instanceof Blob
+        ? res
+        : new Blob([res], {
+            type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+          });
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = `设施导入模板_${new Date().getTime()}.xlsx`;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+    ElMessage.success("模板下载成功");
+  } catch (error: any) {
+    console.error("下载模板失败:", error);
+    ElMessage.error(error?.msg || "模板下载失败,请重试");
+  }
+};
+
+// 提交导入
+const handleImportSubmit = async () => {
+  if (!importFile.value || !importFile.value.raw) {
+    ElMessage.warning("请先选择文件");
+    return;
+  }
+
+  const userInfo: any = localGet("geeker-user")?.userInfo || {};
+  const storeId = userInfo.storeId;
+  if (!storeId) {
+    ElMessage.error("未找到店铺ID");
+    return;
+  }
+
+  importLoading.value = true;
+  try {
+    const formData = new FormData();
+    formData.append("file", importFile.value.raw);
+    const res: any = await importFacilityExcel(formData, storeId);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("导入成功");
+      batchImportVisible.value = false;
+      importFile.value = null;
+      if (uploadRef.value) {
+        uploadRef.value.clearFiles();
+      }
+      await loadFacilityList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "导入失败");
+    }
+  } catch (error: any) {
+    console.error("导入失败:", error);
+    ElMessage.error(error?.msg || "导入失败,请重试");
+  } finally {
+    importLoading.value = false;
+  }
+};
+
+// 获取图片列表
+const getImageList = async () => {
+  try {
+    const userInfo: any = localGet("geeker-user")?.userInfo || {};
+    const storeId = userInfo.storeId;
+    if (storeId) {
+      const res: any = await getOfficialImgList(activeTab.value, Number(storeId), 28);
+      if (res && res.code === 200 && Array.isArray(res.data)) {
+        // 将返回的图片数据转换为el-upload组件所需的格式
+        imageFileList.value = res.data.map(item => ({
+          name: item.imgUrl?.split("/").pop() || "",
+          url: item.imgUrl,
+          uid: item.id || Math.random().toString(36).substr(2, 9)
+        }));
+      }
+    }
+  } catch (error) {
+    console.error("获取图片列表失败:", error);
+  }
+};
+
+// 页面初始化
+onMounted(async () => {
+  await loadFacilityList();
+  updatePagination();
+  getImageList();
+});
+</script>
+
+<style scoped lang="scss">
+.facility-management-container {
+  .header-section {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    :deep(.el-tabs) {
+      flex: 1;
+      .el-tabs__header {
+        margin: 0;
+      }
+    }
+    .action-buttons {
+      display: flex;
+      gap: 10px;
+    }
+  }
+  .image-upload-section {
+    padding: 20px;
+    margin-bottom: 30px;
+    background-color: var(--el-bg-color-page);
+    border-radius: 8px;
+    .section-title {
+      margin-bottom: 16px;
+      font-size: 16px;
+      font-weight: 500;
+      color: var(--el-text-color-primary);
+    }
+    .upload-area {
+      :deep(.el-upload--picture-card) {
+        position: relative;
+        .upload-tip {
+          position: absolute;
+          bottom: 8px;
+          left: 50%;
+          font-size: 12px;
+          color: var(--el-text-color-secondary);
+          transform: translateX(-50%);
+        }
+      }
+    }
+  }
+  .facility-list-section {
+    .section-title {
+      margin-bottom: 16px;
+      font-size: 16px;
+      font-weight: 500;
+      color: var(--el-text-color-primary);
+    }
+    .pagination-section {
+      display: flex;
+      justify-content: flex-end;
+      margin-top: 20px;
+    }
+  }
+  .dialog-footer {
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+  }
+  .import-steps {
+    .import-step {
+      margin-bottom: 30px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+      .step-header {
+        display: flex;
+        gap: 12px;
+        align-items: center;
+        margin-bottom: 16px;
+        .step-number {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 24px;
+          height: 24px;
+          font-size: 14px;
+          font-weight: 500;
+          color: white;
+          background-color: var(--el-color-primary);
+          border-radius: 50%;
+        }
+        .step-title {
+          font-size: 16px;
+          font-weight: 500;
+          color: var(--el-text-color-primary);
+        }
+      }
+    }
+    .import-upload {
+      :deep(.el-upload-dragger) {
+        width: 100%;
+        padding: 40px 20px;
+        .el-icon--upload {
+          font-size: 48px;
+          color: var(--el-text-color-placeholder);
+        }
+        .el-upload__text {
+          margin-top: 16px;
+          font-size: 14px;
+          color: var(--el-text-color-regular);
+          em {
+            font-style: normal;
+            color: var(--el-color-primary);
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 790 - 0
src/views/storeDecoration/facilitiesAndServices/components/ServiceManagement.vue

@@ -0,0 +1,790 @@
+<template>
+  <div class="service-management-container">
+    <!-- 标签页 -->
+    <div class="header-section">
+      <el-tabs v-model="activeTab" @tab-click="handleTabClick">
+        <el-tab-pane v-for="tab in tabs" :key="tab.value" :label="tab.label" :name="tab.value" />
+      </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 class="table-section">
+      <el-table :data="paginatedList" border style="width: 100%">
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column prop="name" label="名称" min-width="120" />
+        <el-table-column label="图片" width="100" align="center">
+          <template #default="{ row }">
+            <el-image
+              v-if="row.imageUrl"
+              :src="row.imageUrl"
+              :preview-src-list="[row.imageUrl]"
+              fit="cover"
+              style="width: 60px; height: 60px; border-radius: 4px"
+            >
+              <template #error>
+                <div class="image-slot">
+                  <el-icon><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
+            <div v-else class="image-placeholder">
+              <el-icon><Picture /></el-icon>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="price" label="收费标准(元)" width="120" align="center" />
+        <el-table-column prop="openingHours" label="开放时间" width="150" align="center" />
+        <el-table-column prop="status" label="状态" width="100" align="center">
+          <template #default="{ row }">
+            <el-tag :type="row.status === 1 ? 'success' : 'info'">
+              {{ row.status === 1 ? "显示" : "隐藏" }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="createTime" label="创建时间" width="180" align="center" />
+        <el-table-column label="操作" width="180" align="center" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" link @click="toggleStatus(row)">
+              {{ row.status === 1 ? "隐藏" : "显示" }}
+            </el-button>
+            <el-button type="primary" link @click="editItem(row)"> 编辑 </el-button>
+            <el-button type="primary" link @click="deleteItem(row)"> 删除 </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <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>
+
+    <!-- 新建/编辑弹窗 -->
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" @close="resetForm">
+      <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
+        <el-form-item label="选择区域*" prop="areaType">
+          <el-select v-model="formData.areaType" placeholder="请选择" style="width: 100%">
+            <el-option v-for="tab in tabs" :key="tab.value" :label="tab.label" :value="tab.value" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="名称*" prop="name">
+          <el-input v-model="formData.name" placeholder="请输入" maxlength="50" clearable />
+        </el-form-item>
+        <el-form-item label="收费标准(元)*" prop="price">
+          <el-input-number
+            v-model="formData.price"
+            :min="0"
+            :max="99999.99"
+            :precision="2"
+            :step="0.01"
+            placeholder="请输入"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="开放时间" prop="openingHoursType">
+          <el-radio-group v-model="formData.openingHoursType">
+            <el-radio label="allDay"> 全天 </el-radio>
+            <el-radio label="custom"> 选择时间 </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item v-if="formData.openingHoursType === 'custom'" label="选择时间*" prop="openingHours">
+          <el-time-picker
+            v-model="formData.openingHours"
+            is-range
+            range-separator="至"
+            start-placeholder="开始时间"
+            end-placeholder="结束时间"
+            format="HH:mm"
+            value-format="HH:mm"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="图片*" prop="imageUrl">
+          <UploadImg
+            v-model:image-url="formData.imageUrl"
+            :width="'150px'"
+            :height="'150px'"
+            :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="status">
+          <el-radio-group v-model="formData.status">
+            <el-radio :label="1"> 显示 </el-radio>
+            <el-radio :label="0"> 隐藏 </el-radio>
+          </el-radio-group>
+        </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">
+      <div class="import-steps">
+        <div class="import-step">
+          <div class="step-header">
+            <div class="step-number">1</div>
+            <div class="step-title">下载模板</div>
+          </div>
+          <el-button type="primary" @click="downloadTemplate"> 下载excel模板 </el-button>
+        </div>
+        <div class="import-step">
+          <div class="step-header">
+            <div class="step-number">2</div>
+            <div class="step-title">上传文件</div>
+          </div>
+          <el-upload
+            ref="uploadRef"
+            :auto-upload="false"
+            :on-change="handleFileChange"
+            :on-remove="handleFileRemove"
+            :limit="1"
+            accept=".xlsx,.xls"
+            drag
+            class="import-upload"
+          >
+            <el-icon class="el-icon--upload">
+              <UploadFilled />
+            </el-icon>
+            <div class="el-upload__text">点击上传文件或拖拽上传文件</div>
+          </el-upload>
+        </div>
+      </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 } 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 {
+  getServiceList,
+  createOrUpdateService,
+  getServiceDetail,
+  deleteService,
+  downloadServiceTemplate,
+  importServiceExcel,
+  getListByStoreIdAndCategory,
+  saveOfficialImg
+} from "@/api/modules/storeDecoration";
+
+// 服务接口
+interface Service {
+  id?: string | number;
+  areaType: string;
+  name: string;
+  price: number;
+  openingHoursType?: "allDay" | "custom";
+  openingHours: string | string[]; // 表格显示时为字符串,表单编辑时为数组
+  imageUrl: string;
+  status: number; // 0:隐藏, 1:显示
+  createTime?: string;
+  storeId?: number; // 店铺ID
+  // 新API字段
+  facilityCategory?: number; // 区域分类
+  facilityName?: string; // 名称
+  chargingStandard?: string; // 收费标准
+  usageTimeType?: number; // 使用时间类型
+  usageStartTime?: string; // 使用开始时间
+  usageEndTime?: string; // 使用结束时间
+  imgUrl?: string; // 图片id
+  displayInStoreDetail?: number; // 是否显示在店铺详情
+}
+
+const dialogVisible = ref(false);
+const formRef = ref<FormInstance>();
+const submitLoading = ref(false);
+const activeTab = ref("bath"); // 洗浴区
+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 tabs = [
+  { label: "洗浴区", value: "bath" },
+  { label: "汗蒸区", value: "sauna" },
+  { label: "休闲区", value: "leisure" },
+  { label: "餐饮区", value: "dining" }
+];
+
+// 服务列表
+const serviceList = ref<Service[]>([]);
+
+// 分页数据
+const pageable = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 分页后的列表
+const paginatedList = computed(() => {
+  const list = serviceList.value.filter(item => item.areaType === activeTab.value);
+  const start = (pageable.pageNum - 1) * pageable.pageSize;
+  const end = start + pageable.pageSize;
+  return list.slice(start, end);
+});
+
+// 弹窗标题
+const dialogTitle = computed(() => (editId.value !== null ? "编辑" : "新建"));
+
+// 表单数据
+const formData = reactive({
+  areaType: "bath",
+  name: "",
+  price: 0,
+  openingHoursType: "allDay" as "allDay" | "custom",
+  openingHours: [] as string[],
+  imageUrl: "",
+  status: 1
+});
+
+// 表单校验规则
+const rules = reactive<FormRules>({
+  areaType: [{ required: true, message: "请选择区域", trigger: "change" }],
+  name: [{ required: true, message: "请输入名称", trigger: "blur" }],
+  price: [{ required: true, message: "请输入收费标准", trigger: "blur" }],
+  openingHours: [
+    {
+      validator: (rule, value, callback) => {
+        if (formData.openingHoursType === "custom" && (!value || value.length !== 2)) {
+          callback(new Error("请选择开放时间"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ],
+  imageUrl: [{ required: true, message: "请上传图片", trigger: "change" }]
+});
+
+// Tab切换
+const handleTabClick = async () => {
+  pageable.pageNum = 1;
+  await loadServiceList();
+  updatePagination();
+};
+
+// 更新分页总数
+const updatePagination = () => {
+  const list = serviceList.value.filter(item => item.areaType === activeTab.value);
+  pageable.total = list.length;
+};
+
+// 分页大小改变
+const handleSizeChange = (size: number) => {
+  pageable.pageSize = size;
+  pageable.pageNum = 1;
+  updatePagination();
+};
+
+// 当前页改变
+const handleCurrentChange = (page: number) => {
+  pageable.pageNum = page;
+};
+
+// 打开新建弹窗
+const openCreateDialog = () => {
+  editId.value = null;
+  resetForm();
+  dialogVisible.value = true;
+};
+
+// 编辑
+const editItem = async (item: Service) => {
+  if (!item.id) {
+    ElMessage.warning("服务ID不存在");
+    return;
+  }
+
+  try {
+    const res: any = await getServiceDetail({ id: item.id });
+    if (res && (res.code === 200 || res.code === "200") && res.data) {
+      const detail = res.data;
+      editId.value = detail.id;
+      // 区域分类映射:1-洗浴区, 2-汗蒸区, 3-休闲区, 4-餐饮区
+      formData.areaType = detail.facilityCategory
+        ? detail.facilityCategory === 1
+          ? "bath"
+          : detail.facilityCategory === 2
+            ? "sauna"
+            : detail.facilityCategory === 3
+              ? "leisure"
+              : "food"
+        : "bath";
+      formData.name = detail.facilityName || "";
+      formData.price = detail.chargingStandard || 0;
+      formData.openingHoursType = detail.usageTimeType === 0 ? "allDay" : "custom";
+      if (detail.usageTimeType === 1 && detail.usageStartTime && detail.usageEndTime) {
+        formData.openingHours = [detail.usageStartTime, detail.usageEndTime];
+      } else {
+        formData.openingHours = [];
+      }
+      formData.imageUrl = detail.imgUrl || "";
+      formData.status = 1; // 新API中没有status字段,默认为1
+      dialogVisible.value = true;
+    } else {
+      ElMessage.error(res?.msg || "获取服务详情失败");
+    }
+  } catch (error: any) {
+    console.error("获取服务详情失败:", error);
+    ElMessage.error(error?.msg || "获取服务详情失败,请重试");
+  }
+};
+
+// 删除
+const deleteItem = async (item: Service) => {
+  try {
+    await ElMessageBox.confirm("确认删除该服务吗?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+
+    if (!item.id) {
+      ElMessage.error("服务ID不存在");
+      return;
+    }
+
+    const res: any = await deleteService({ id: item.id });
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("删除成功");
+      await loadServiceList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "删除失败");
+    }
+  } catch (error: any) {
+    if (error !== "cancel") {
+      console.error("删除服务失败:", error);
+      ElMessage.error(error?.msg || "删除失败,请重试");
+    }
+  }
+};
+
+// 切换状态
+const toggleStatus = async (item: Service) => {
+  try {
+    if (!item.id) {
+      ElMessage.error("服务ID不存在");
+      return;
+    }
+
+    const newStatus = item.status === 1 ? 0 : 1;
+    // 构建符合新接口的参数
+    const params: any = {
+      id: item.id,
+      storeId: item.storeId,
+      facilityCategory: item.facilityCategory,
+      facilityName: item.facilityName || item.name,
+      chargingStandard: item.chargingStandard || item.price,
+      usageTimeType: item.usageTimeType || (item.openingHoursType && item.openingHoursType === "allDay" ? 0 : 1),
+      usageStartTime: item.usageStartTime,
+      usageEndTime: item.usageEndTime,
+      imgUrl: item.imgUrl || item.imageUrl,
+      displayInStoreDetail: newStatus // 新的状态字段
+    };
+
+    const res: any = await createOrUpdateService(params);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success(newStatus === 1 ? "已显示" : "已隐藏");
+      await loadServiceList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "操作失败");
+    }
+  } catch (error: any) {
+    console.error("切换状态失败:", error);
+    ElMessage.error(error?.msg || "操作失败,请重试");
+  }
+};
+
+// 重置表单
+const resetForm = () => {
+  formData.areaType = activeTab.value;
+  formData.name = "";
+  formData.price = 0;
+  formData.openingHoursType = "allDay";
+  formData.openingHours = [];
+  formData.imageUrl = "";
+  formData.status = 1;
+  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;
+    }
+
+    // 将areaType转换为facilityCategory
+    const categoryMap: Record<string, number> = {
+      bath: 1, // 洗浴区
+      sauna: 2, // 汗蒸区
+      leisure: 3, // 休闲区
+      dining: 4 // 餐饮区
+    };
+
+    // 解析开放时间
+    let usageStartTime = "";
+    let usageEndTime = "";
+    if (formData.openingHoursType !== "allDay" && formData.openingHours && formData.openingHours.length === 2) {
+      usageStartTime = formData.openingHours[0];
+      usageEndTime = formData.openingHours[1];
+    }
+
+    const params: any = {
+      storeId: Number(storeId),
+      facilityCategory: categoryMap[formData.areaType], // 区域分类
+      facilityName: formData.name, // 名称
+      chargingStandard: formData.price, // 收费标准
+      usageTimeType: formData.openingHoursType === "allDay" ? 0 : 1, // 使用时间类型
+      usageStartTime: usageStartTime, // 使用开始时间
+      usageEndTime: usageEndTime, // 使用结束时间
+      imgUrl: formData.imageUrl, // 图片id
+      displayInStoreDetail: formData.status // 是否显示在店铺详情
+    };
+
+    if (editId.value) {
+      params.id = editId.value;
+    }
+
+    const res: any = await createOrUpdateService(params);
+    if (res && (res.code === 200 || res.code === "200")) {
+      // 获取保存后的设施ID
+      const facilityId = res.data?.id || (editId.value ? editId.value : null);
+
+      // 如果有图片URL,调用saveOfficialImg接口上传图片信息
+      if (facilityId && formData.imageUrl) {
+        const userInfo: any = localGet("geeker-user")?.userInfo || {};
+        const storeId = userInfo.storeId;
+        if (storeId) {
+          // 准备图片数据
+          const imageData = [
+            {
+              imgUrl: formData.imageUrl,
+              imgType: 29, // 设施图片类型
+              businessId: facilityId, // 设施ID
+              storeId: Number(storeId),
+              imgSort: 0 // 图片排序,这里只有一张图片,所以是0
+            }
+          ];
+
+          // 调用saveOfficialImg接口
+          const saveRes: any = await saveOfficialImg(imageData);
+          if (!(saveRes && (saveRes.code === 200 || saveRes.code === "200"))) {
+            console.error("保存图片信息失败:", saveRes);
+            ElMessage.warning("图片信息保存失败,请稍后重试");
+          }
+        }
+      }
+
+      ElMessage.success(editId.value ? "编辑成功" : "新增成功");
+      dialogVisible.value = false;
+      resetForm();
+      await loadServiceList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || (editId.value ? "编辑失败" : "新增失败"));
+    }
+  } catch (error: any) {
+    console.error("操作失败:", error);
+    ElMessage.error(error?.msg || "操作失败,请重试");
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+// 加载服务列表
+const loadServiceList = async () => {
+  try {
+    const userInfo: any = localGet("geeker-user")?.userInfo || {};
+    const storeId = userInfo.storeId;
+    if (!storeId) {
+      console.warn("未找到店铺ID");
+      return;
+    }
+
+    // 将areaType转换为facilityCategory
+    const categoryMap: Record<string, number> = {
+      bath: 1, // 洗浴区
+      sauna: 2, // 汗蒸区
+      leisure: 3, // 休闲区
+      dining: 4 // 餐饮区
+    };
+    const facilityCategory = categoryMap[activeTab.value];
+
+    const res: any = await getListByStoreIdAndCategory({
+      storeId: Number(storeId),
+      facilityCategory
+    });
+    if (res && (res.code === 200 || res.code === "200") && res.data) {
+      const dataList = Array.isArray(res.data) ? res.data : [];
+      serviceList.value = dataList.map((item: any) => ({
+        id: item.id,
+        areaType: Object.keys(categoryMap).find(key => categoryMap[key] === item.facilityCategory) || "bath",
+        name: item.facilityName || "",
+        price: parseFloat(item.chargingStandard) || 0,
+        openingHoursType: item.usageTimeType === 0 ? "allDay" : "custom",
+        openingHours: item.usageTimeType === 0 ? "全天" : `${item.usageStartTime}-${item.usageEndTime}`,
+        imageUrl: item.imgUrl || "",
+        status: item.displayInStoreDetail !== undefined ? item.displayInStoreDetail : 1,
+        createTime: item.createdTime || "",
+        storeId: item.storeId || 0, // 添加storeId映射
+        // 保留新API的原始字段
+        facilityCategory: item.facilityCategory,
+        facilityName: item.facilityName,
+        chargingStandard: item.chargingStandard,
+        usageTimeType: item.usageTimeType,
+        usageStartTime: item.usageStartTime,
+        usageEndTime: item.usageEndTime,
+        imgUrl: item.imgUrl,
+        displayInStoreDetail: item.displayInStoreDetail
+      }));
+    }
+  } catch (error) {
+    console.error("获取服务列表失败:", error);
+  }
+};
+
+// 批量导入
+const handleBatchImport = () => {
+  batchImportVisible.value = true;
+};
+
+// 文件选择
+const handleFileChange = (file: UploadFile) => {
+  importFile.value = file;
+};
+
+// 文件移除
+const handleFileRemove = () => {
+  importFile.value = null;
+};
+
+// 下载模板
+const downloadTemplate = async () => {
+  try {
+    const res: any = await downloadServiceTemplate();
+    const blob =
+      res instanceof Blob
+        ? res
+        : new Blob([res], {
+            type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+          });
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = `服务导入模板_${new Date().getTime()}.xlsx`;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+    ElMessage.success("模板下载成功");
+  } catch (error: any) {
+    console.error("下载模板失败:", error);
+    ElMessage.error(error?.msg || "模板下载失败,请重试");
+  }
+};
+
+// 提交导入
+const handleImportSubmit = async () => {
+  if (!importFile.value || !importFile.value.raw) {
+    ElMessage.warning("请先选择文件");
+    return;
+  }
+
+  const userInfo: any = localGet("geeker-user")?.userInfo || {};
+  const storeId = userInfo.storeId;
+  if (!storeId) {
+    ElMessage.error("未找到店铺ID");
+    return;
+  }
+
+  importLoading.value = true;
+  try {
+    const formData = new FormData();
+    formData.append("file", importFile.value.raw);
+    const res: any = await importServiceExcel(formData, storeId);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("导入成功");
+      batchImportVisible.value = false;
+      importFile.value = null;
+      if (uploadRef.value) {
+        uploadRef.value.clearFiles();
+      }
+      await loadServiceList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "导入失败");
+    }
+  } catch (error: any) {
+    console.error("导入失败:", error);
+    ElMessage.error(error?.msg || "导入失败,请重试");
+  } finally {
+    importLoading.value = false;
+  }
+};
+
+// 页面初始化
+onMounted(async () => {
+  await loadServiceList();
+  updatePagination();
+});
+</script>
+
+<style scoped lang="scss">
+.service-management-container {
+  .header-section {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    :deep(.el-tabs) {
+      flex: 1;
+      .el-tabs__header {
+        margin: 0;
+      }
+    }
+    .action-buttons {
+      display: flex;
+      gap: 10px;
+    }
+  }
+  .table-section {
+    .pagination-section {
+      display: flex;
+      justify-content: flex-end;
+      margin-top: 20px;
+    }
+    .image-placeholder {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 60px;
+      height: 60px;
+      color: #909399;
+      background-color: #f0f0f0;
+      border-radius: 4px;
+      .el-icon {
+        font-size: 24px;
+      }
+    }
+    .image-slot {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: 100%;
+      color: #909399;
+      background-color: #f0f0f0;
+    }
+  }
+  .dialog-footer {
+    display: flex;
+    gap: 12px;
+    justify-content: flex-end;
+  }
+  .import-steps {
+    .import-step {
+      margin-bottom: 30px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+      .step-header {
+        display: flex;
+        gap: 12px;
+        align-items: center;
+        margin-bottom: 16px;
+        .step-number {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 24px;
+          height: 24px;
+          font-size: 14px;
+          font-weight: 500;
+          color: white;
+          background-color: var(--el-color-primary);
+          border-radius: 50%;
+        }
+        .step-title {
+          font-size: 16px;
+          font-weight: 500;
+          color: var(--el-text-color-primary);
+        }
+      }
+    }
+    .import-upload {
+      :deep(.el-upload-dragger) {
+        width: 100%;
+        padding: 40px 20px;
+        .el-icon--upload {
+          font-size: 48px;
+          color: var(--el-text-color-placeholder);
+        }
+        .el-upload__text {
+          margin-top: 16px;
+          font-size: 14px;
+          color: var(--el-text-color-regular);
+          em {
+            font-style: normal;
+            color: var(--el-color-primary);
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 44 - 0
src/views/storeDecoration/facilitiesAndServices/index.vue

@@ -0,0 +1,44 @@
+<template>
+  <div class="facilities-and-services-container">
+    <!-- 根据用户业务类型判断使用哪个组件 -->
+    <ServiceManagement v-if="activeComponent === 'service'" />
+    <FacilityManagement v-else-if="activeComponent === 'facility'" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue";
+import { localGet } from "@/utils";
+import ServiceManagement from "./components/ServiceManagement.vue";
+import FacilityManagement from "./components/FacilityManagement.vue";
+
+// 根据用户业务类型判断使用哪个组件
+// 运动健身 -> FacilityManagement (设施管理)
+// 洗浴汗蒸 -> ServiceManagement (服务管理)
+const activeComponent = computed<"service" | "facility">(() => {
+  const geekerUser = localGet("geeker-user");
+  console.log("完整geeker-user:", geekerUser);
+  const userInfo: any = geekerUser?.userInfo || {};
+  console.log("userInfo.businessSection:", userInfo.businessSection);
+  const businessSection = userInfo.businessSection || "";
+
+  // 如果是"运动健身",显示设施管理
+  if (businessSection == "17") {
+    return "facility";
+  }
+  // 如果是"洗浴汗蒸",显示服务管理
+  if (businessSection == "15") {
+    return "service";
+  }
+  // 默认显示服务管理
+  return "facility";
+});
+</script>
+
+<style scoped lang="scss">
+.facilities-and-services-container {
+  min-height: 100%;
+  padding: 20px;
+  background-color: white;
+}
+</style>

+ 202 - 4
src/views/storeDecoration/menuManagement/index.vue

@@ -6,7 +6,10 @@
         <el-tab-pane label="菜单" name="menu" />
         <el-tab-pane label="推荐菜" name="recommended" />
       </el-tabs>
-      <el-button type="primary" @click="openCreateDialog"> 新建菜品 </el-button>
+      <div class="action-buttons">
+        <el-button type="primary" @click="openCreateDialog"> 新建菜品 </el-button>
+        <el-button type="primary" @click="handleBatchImport"> 批量导入 </el-button>
+      </div>
     </div>
 
     <!-- 内容区域 -->
@@ -127,18 +130,68 @@
         </div>
       </template>
     </el-dialog>
+
+    <!-- 批量导入弹窗 -->
+    <el-dialog v-model="batchImportVisible" title="批量导入" width="500px">
+      <div class="import-steps">
+        <!-- 第一步:下载模板 -->
+        <div class="import-step">
+          <div class="step-header">
+            <div class="step-number">1</div>
+            <div class="step-title">下载模板</div>
+          </div>
+          <el-button type="primary" @click="downloadTemplate"> 下载excel模板 </el-button>
+        </div>
+
+        <!-- 第二步:上传文件 -->
+        <div class="import-step">
+          <div class="step-header">
+            <div class="step-number">2</div>
+            <div class="step-title">上传文件</div>
+          </div>
+          <el-upload
+            ref="uploadRef"
+            :auto-upload="false"
+            :on-change="handleFileChange"
+            :on-remove="handleFileRemove"
+            :limit="1"
+            accept=".xlsx,.xls"
+            drag
+            class="import-upload"
+          >
+            <el-icon class="el-icon--upload">
+              <UploadFilled />
+            </el-icon>
+            <div class="el-upload__text">点击上传文件或拖拽上传文件</div>
+          </el-upload>
+        </div>
+      </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 } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
-import type { FormInstance, FormRules } from "element-plus";
-import { Picture } from "@element-plus/icons-vue";
+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";
+import {
+  createOrUpdateDish,
+  getDishList,
+  getDishDetail,
+  deleteDish,
+  downloadExcelTemplate,
+  importExcel
+} from "@/api/modules/storeDecoration";
 
 // 菜品接口
 interface Dish {
@@ -159,6 +212,10 @@ const submitLoading = ref(false);
 const activeTab = ref<"menu" | "recommended">("menu");
 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 = [
@@ -551,6 +608,93 @@ const loadDishList = async () => {
   }
 };
 
+// 批量导入
+const handleBatchImport = () => {
+  batchImportVisible.value = true;
+};
+
+// 文件选择
+const handleFileChange = (file: UploadFile) => {
+  importFile.value = file;
+};
+
+// 文件移除
+const handleFileRemove = () => {
+  importFile.value = null;
+};
+
+// 下载模板
+const downloadTemplate = async () => {
+  try {
+    const res: any = await downloadExcelTemplate();
+    // 如果响应是blob类型,直接使用;否则尝试从data中获取
+    const blob =
+      res instanceof Blob
+        ? res
+        : new Blob([res], {
+            type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+          });
+    // 创建下载链接
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = `菜单导入模板_${new Date().getTime()}.xlsx`;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+    ElMessage.success("模板下载成功");
+  } catch (error: any) {
+    console.error("下载模板失败:", error);
+    ElMessage.error(error?.msg || "模板下载失败,请重试");
+  }
+};
+
+// 提交导入
+const handleImportSubmit = async () => {
+  if (!importFile.value || !importFile.value.raw) {
+    ElMessage.warning("请先选择文件");
+    return;
+  }
+
+  const userInfo: any = localGet("geeker-user")?.userInfo || {};
+  const storeId = userInfo.storeId;
+  if (!storeId) {
+    ElMessage.error("未找到店铺ID");
+    return;
+  }
+
+  importLoading.value = true;
+  try {
+    // 创建FormData对象,只包含file
+    const formData = new FormData();
+    console.log(importFile.value.raw, "xx");
+    formData.append("file", importFile.value.raw);
+    console.log(formData.get("file"), "formData--x");
+    // storeId作为query参数传递
+    const res: any = await importExcel(formData, storeId);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("导入成功");
+      batchImportVisible.value = false;
+      importFile.value = null;
+      // 清空上传组件的文件列表
+      if (uploadRef.value) {
+        uploadRef.value.clearFiles();
+      }
+      // 重新加载菜品列表
+      await loadDishList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "导入失败");
+    }
+  } catch (error: any) {
+    console.error("导入失败:", error);
+    ElMessage.error(error?.msg || "导入失败,请重试");
+  } finally {
+    importLoading.value = false;
+  }
+};
+
 // 页面初始化
 onMounted(async () => {
   await loadDishList();
@@ -574,6 +718,10 @@ onMounted(async () => {
         margin: 0;
       }
     }
+    .action-buttons {
+      display: flex;
+      gap: 10px;
+    }
   }
   .content-section {
     .dish-grid {
@@ -665,5 +813,55 @@ onMounted(async () => {
     gap: 12px;
     justify-content: flex-end;
   }
+  .import-steps {
+    .import-step {
+      margin-bottom: 30px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+      .step-header {
+        display: flex;
+        gap: 12px;
+        align-items: center;
+        margin-bottom: 16px;
+        .step-number {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 24px;
+          height: 24px;
+          font-size: 14px;
+          font-weight: 500;
+          color: white;
+          background-color: var(--el-color-primary);
+          border-radius: 50%;
+        }
+        .step-title {
+          font-size: 16px;
+          font-weight: 500;
+          color: var(--el-text-color-primary);
+        }
+      }
+    }
+    .import-upload {
+      :deep(.el-upload-dragger) {
+        width: 100%;
+        padding: 40px 20px;
+        .el-icon--upload {
+          font-size: 48px;
+          color: var(--el-text-color-placeholder);
+        }
+        .el-upload__text {
+          margin-top: 16px;
+          font-size: 14px;
+          color: var(--el-text-color-regular);
+          em {
+            font-style: normal;
+            color: var(--el-color-primary);
+          }
+        }
+      }
+    }
+  }
 }
 </style>

+ 511 - 0
src/views/storeDecoration/personnelConfig/index.vue

@@ -0,0 +1,511 @@
+<template>
+  <div class="personnel-config-container">
+    <!-- 页面标题和新建按钮 -->
+    <div class="header-section">
+      <h2 class="page-title">人员配置</h2>
+      <el-button type="primary" @click="openCreateDialog"> 新建 </el-button>
+    </div>
+
+    <!-- 信息提示横幅 -->
+    <el-alert :closable="false" type="info" show-icon :description="'添加的人员将会出现在店铺详情页展示'" class="info-banner" />
+
+    <!-- 内容区域 -->
+    <div v-if="personnelList.length > 0" class="content-section">
+      <!-- 人员卡片网格 -->
+      <div class="personnel-grid">
+        <div v-for="(person, index) in paginatedPersonnelList" :key="person.id" class="personnel-card">
+          <div class="personnel-avatar-wrapper">
+            <img v-if="person.avatar" :src="person.avatar" alt="头像" class="personnel-avatar" />
+            <div v-else class="avatar-placeholder">
+              <el-icon><Picture /></el-icon>
+            </div>
+          </div>
+          <div class="personnel-info">
+            <div class="personnel-name">
+              {{ person.name }}
+            </div>
+            <div class="personnel-position">
+              {{ person.position }}
+            </div>
+          </div>
+          <div class="personnel-actions">
+            <el-button type="primary" link @click="editPersonnel(person, index)"> 编辑 </el-button>
+            <el-button type="primary" link @click="deletePersonnelHandler(person.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="avatar">
+          <UploadImg
+            v-model:image-url="formData.avatar"
+            :width="'150px'"
+            :height="'150px'"
+            :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="name">
+          <el-input v-model="formData.name" placeholder="请输入" maxlength="20" show-word-limit clearable />
+        </el-form-item>
+        <el-form-item label="人员职位*" prop="position">
+          <el-input v-model="formData.position" placeholder="请输入" maxlength="50" clearable />
+        </el-form-item>
+        <el-form-item label="擅长" prop="specialty">
+          <el-input
+            v-model="formData.specialty"
+            type="textarea"
+            :rows="3"
+            placeholder="请输入"
+            maxlength="200"
+            show-word-limit
+            clearable
+          />
+        </el-form-item>
+        <el-form-item label="标签" prop="tags">
+          <div class="tags-container">
+            <div v-for="(tag, index) in formData.tags" :key="index" class="tag-input-wrapper">
+              <el-input v-model="formData.tags[index]" placeholder="请输入" maxlength="20" clearable />
+            </div>
+            <div class="tag-actions">
+              <el-button v-if="formData.tags.length < 5" :icon="Plus" circle @click="addTag" />
+              <el-button v-if="formData.tags.length > 1" :icon="Delete" circle @click="removeTag" />
+            </div>
+          </div>
+        </el-form-item>
+        <el-form-item label="个人简介" prop="description">
+          <el-input
+            v-model="formData.description"
+            type="textarea"
+            :rows="4"
+            placeholder="请输入"
+            maxlength="500"
+            show-word-limit
+            clearable
+          />
+        </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>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import type { FormInstance, FormRules } from "element-plus";
+import { Picture, Plus, Delete } from "@element-plus/icons-vue";
+import UploadImg from "@/components/Upload/Img.vue";
+import { uploadImg } from "@/api/modules/newLoginApi";
+import { localGet } from "@/utils";
+import { getPersonnelList, createOrUpdatePersonnel, getPersonnelDetail, deletePersonnel } from "@/api/modules/storeDecoration";
+
+// 人员接口
+interface Personnel {
+  id?: string | number;
+  name: string;
+  position: string;
+  avatar: string;
+  specialty?: string;
+  tags?: string[];
+  description?: string;
+}
+
+const dialogVisible = ref(false);
+const formRef = ref<FormInstance>();
+const submitLoading = ref(false);
+const editId = ref<string | number | null>(null);
+
+// 人员列表
+const personnelList = ref<Personnel[]>([]);
+
+// 分页数据
+const pageable = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+});
+
+// 分页后的人员列表
+const paginatedPersonnelList = computed(() => {
+  const list = personnelList.value;
+  const start = (pageable.pageNum - 1) * pageable.pageSize;
+  const end = start + pageable.pageSize;
+  return list.slice(start, end);
+});
+
+// 弹窗标题
+const dialogTitle = computed(() => (editId.value !== null ? "编辑" : "新建"));
+
+// 表单数据
+const formData = reactive({
+  name: "",
+  position: "",
+  avatar: "",
+  specialty: "",
+  tags: [""] as string[],
+  description: ""
+});
+
+// 表单校验规则
+const rules = reactive<FormRules>({
+  name: [{ required: true, message: "请输入姓名/昵称", trigger: "blur" }],
+  position: [{ required: true, message: "请输入人员职位", trigger: "blur" }],
+  avatar: [{ required: true, message: "请上传头像", trigger: "change" }]
+});
+
+// 分页大小改变
+const handleSizeChange = (size: number) => {
+  pageable.pageSize = size;
+  pageable.pageNum = 1;
+  updatePagination();
+};
+
+// 当前页改变
+const handleCurrentChange = (page: number) => {
+  pageable.pageNum = page;
+};
+
+// 更新分页总数
+const updatePagination = () => {
+  pageable.total = personnelList.value.length;
+};
+
+// 打开新建弹窗
+const openCreateDialog = () => {
+  editId.value = null;
+  resetForm();
+  dialogVisible.value = true;
+};
+
+// 编辑人员
+const editPersonnel = async (person: Personnel, index: number) => {
+  if (!person.id) {
+    ElMessage.warning("人员ID不存在");
+    return;
+  }
+
+  try {
+    // 调用获取人员详情接口
+    const res: any = await getPersonnelDetail({ id: person.id });
+    if (res && (res.code === 200 || res.code === "200") && res.data) {
+      const personDetail = res.data;
+      editId.value = personDetail.id;
+      formData.name = personDetail.name || "";
+      formData.position = personDetail.staffPosition || "";
+      formData.avatar = personDetail.staffImage || "";
+      formData.specialty = personDetail.proficientProjects || "";
+      formData.tags = personDetail.tag ? personDetail.tag.split(",").map((tag: string) => tag.trim()) : [""];
+      formData.description = personDetail.personalIntroduction || "";
+      dialogVisible.value = true;
+    } else {
+      ElMessage.error(res?.msg || "获取人员详情失败");
+    }
+  } catch (error: any) {
+    console.error("获取人员详情失败:", error);
+    ElMessage.error(error?.msg || "获取人员详情失败,请重试");
+  }
+};
+
+// 删除人员
+const deletePersonnelHandler = async (id: string | number, index: number) => {
+  try {
+    await ElMessageBox.confirm("确认删除该人员吗?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+
+    const person = personnelList.value.find(p => p.id === id);
+    if (!person) {
+      ElMessage.error("未找到要删除的人员");
+      return;
+    }
+
+    const res: any = await deletePersonnel({ id });
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("删除成功");
+      await loadPersonnelList();
+      updatePagination();
+      // 如果当前页没有数据了,回到上一页
+      if (paginatedPersonnelList.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 addTag = () => {
+  if (formData.tags.length < 5) {
+    formData.tags.push("");
+  }
+};
+
+// 删除标签
+const removeTag = () => {
+  if (formData.tags.length > 1) {
+    formData.tags.pop();
+  }
+};
+
+// 重置表单
+const resetForm = () => {
+  formData.name = "";
+  formData.position = "";
+  formData.avatar = "";
+  formData.specialty = "";
+  formData.tags = [""];
+  formData.description = "";
+  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),
+      name: formData.name,
+      staffPosition: formData.position,
+      staffImage: formData.avatar,
+      proficientProjects: formData.specialty || "",
+      tag: formData.tags.filter(tag => tag.trim() !== "").join(","),
+      personalIntroduction: formData.description || ""
+    };
+
+    // 如果是编辑,添加id
+    if (editId.value) {
+      params.id = editId.value;
+    }
+
+    const res: any = await createOrUpdatePersonnel(params);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success(editId.value ? "编辑成功" : "新建成功");
+      dialogVisible.value = false;
+      resetForm();
+      // 重新加载人员列表
+      await loadPersonnelList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || (editId.value ? "编辑失败" : "新建失败"));
+    }
+  } catch (error: any) {
+    console.error("操作失败:", error);
+    ElMessage.error(error?.msg || "操作失败,请重试");
+  } finally {
+    submitLoading.value = false;
+  }
+};
+
+// 加载人员列表
+const loadPersonnelList = async () => {
+  try {
+    const userInfo: any = localGet("geeker-user")?.userInfo || {};
+    const storeId = userInfo.storeId;
+    if (!storeId) {
+      console.warn("未找到店铺ID");
+      return;
+    }
+
+    const res: any = await getPersonnelList({ storeId: Number(storeId) });
+
+    if (res && (res.code === 200 || res.code === "200") && res.data) {
+      const dataList = Array.isArray(res.data) ? res.data : [];
+      personnelList.value = dataList.map((person: any) => ({
+        id: person.id,
+        name: person.name || "",
+        position: person.staffPosition || "",
+        avatar: person.staffImage || "",
+        specialty: person.proficientProjects || "",
+        tags: person.tag ? person.tag.split(",").map((tag: string) => tag.trim()) : [],
+        description: person.personalIntroduction || ""
+      }));
+    }
+  } catch (error) {
+    console.error("获取人员列表失败:", error);
+  }
+};
+
+// 页面初始化
+onMounted(async () => {
+  await loadPersonnelList();
+  updatePagination();
+});
+</script>
+
+<style scoped lang="scss">
+.personnel-config-container {
+  min-height: 100%;
+  padding: 20px;
+  background-color: white;
+  .header-section {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 20px;
+    .page-title {
+      margin: 0;
+      font-size: 20px;
+      font-weight: 600;
+      color: var(--el-text-color-primary);
+    }
+  }
+  .info-banner {
+    margin-bottom: 24px;
+  }
+  .content-section {
+    .personnel-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+      gap: 20px;
+      margin-bottom: 24px;
+      .personnel-card {
+        overflow: hidden;
+        background-color: white;
+        border: 1px solid var(--el-border-color-lighter);
+        border-radius: 8px;
+        transition: all 0.3s;
+        &:hover {
+          box-shadow: 0 4px 16px 0 rgb(0 0 0 / 10%);
+        }
+        .personnel-avatar-wrapper {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 100%;
+          height: 200px;
+          overflow: hidden;
+          background-color: #f0f0f0;
+          .personnel-avatar {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+          }
+          .avatar-placeholder {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            width: 100%;
+            height: 100%;
+            color: #909399;
+            .el-icon {
+              font-size: 64px;
+            }
+          }
+        }
+        .personnel-info {
+          padding: 16px;
+          text-align: center;
+          .personnel-name {
+            margin-bottom: 8px;
+            font-size: 16px;
+            font-weight: 500;
+            color: var(--el-text-color-primary);
+          }
+          .personnel-position {
+            font-size: 14px;
+            color: var(--el-text-color-regular);
+          }
+        }
+        .personnel-actions {
+          display: flex;
+          gap: 8px;
+          justify-content: center;
+          padding: 12px 16px;
+          border-top: 1px solid var(--el-border-color-lighter);
+        }
+      }
+    }
+    .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;
+  }
+  .tags-container {
+    width: 100%;
+    .tag-input-wrapper {
+      margin-bottom: 10px;
+      &:last-of-type {
+        margin-bottom: 0;
+      }
+    }
+    .tag-actions {
+      display: flex;
+      gap: 10px;
+      margin-top: 10px;
+    }
+  }
+}
+</style>

+ 345 - 183
src/views/storeDecoration/wineMenuManagement/index.vue

@@ -17,21 +17,21 @@
       <el-button
         :type="activeCategory === 'drink' ? 'primary' : ''"
         :plain="activeCategory !== 'drink'"
-        @click="activeCategory = 'drink'"
+        @click="handleCategoryClick('drink')"
       >
         酒水
       </el-button>
       <el-button
         :type="activeCategory === 'food' ? 'primary' : ''"
         :plain="activeCategory !== 'food'"
-        @click="activeCategory = 'food'"
+        @click="handleCategoryClick('food')"
       >
         餐食
       </el-button>
     </div>
 
     <!-- 内容区域 -->
-    <div v-if="filteredDishList.length > 0" class="content-section">
+    <div v-if="dishList.length > 0" class="content-section">
       <!-- 菜品卡片网格 -->
       <div class="dish-grid">
         <div v-for="(dish, index) in paginatedDishList" :key="dish.id" class="dish-card">
@@ -81,87 +81,89 @@
     </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>
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" class="wine-menu-dialog" @close="resetForm">
+      <div class="dialog-content-wrapper">
+        <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
+          <el-form-item label="类型" prop="dishMenuType">
+            <el-radio-group v-model="formData.dishMenuType">
+              <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.dishMenuType === 'drink'" label="品类" prop="category">
+            <el-input v-model="formData.category" placeholder="请输入" clearable />
+          </el-form-item>
+          <el-form-item v-if="formData.dishMenuType === '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.dishMenuType === '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>
+      </div>
       <template #footer>
         <div class="dialog-footer">
           <el-button @click="dialogVisible = false"> 取消 </el-button>
@@ -174,30 +176,43 @@
 
     <!-- 批量导入弹窗 -->
     <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 class="import-steps">
+        <!-- 第一步:下载模板 -->
+        <div class="import-step">
+          <div class="step-header">
+            <div class="step-number">1</div>
+            <div class="step-title">下载模板</div>
+          </div>
+          <el-button type="primary" @click="downloadTemplate"> 下载excel模板 </el-button>
+        </div>
+
+        <!-- 第二步:上传文件 -->
+        <div class="import-step">
+          <div class="step-header">
+            <div class="step-number">2</div>
+            <div class="step-title">上传文件</div>
+          </div>
+          <el-upload
+            ref="uploadRef"
+            :auto-upload="false"
+            :on-change="handleFileChange"
+            :on-remove="handleFileRemove"
+            :limit="1"
+            accept=".xlsx,.xls"
+            drag
+            class="import-upload"
+          >
+            <el-icon class="el-icon--upload">
+              <UploadFilled />
+            </el-icon>
+            <div class="el-upload__text">点击上传文件或拖拽上传文件</div>
+          </el-upload>
+        </div>
       </div>
       <template #footer>
         <div class="dialog-footer">
           <el-button @click="batchImportVisible = false"> 取消 </el-button>
-          <el-button type="primary" :loading="importLoading" @click="handleImportSubmit"> 确定 </el-button>
+          <el-button type="primary" :loading="importLoading" @click="handleImportSubmit"> 导入 </el-button>
         </div>
       </template>
     </el-dialog>
@@ -212,7 +227,15 @@ 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";
+import {
+  getBarMenuList,
+  saveOrUpdateBarMenu,
+  deleteBarMenu,
+  getDishDetail,
+  downloadBarMenuTemplate,
+  importBarMenuExcel,
+  saveOfficialImg
+} from "@/api/modules/storeDecoration";
 
 // 酒单接口
 interface Dish {
@@ -225,7 +248,7 @@ interface Dish {
   description?: string;
   dishType: number; // 0:未推荐, 1:推荐
   likeCount?: number;
-  itemType?: string; // 'drink' | 'food'
+  dishMenuType: string | number; // 'drink' | 'food' 或 1(餐食) | 2(酒水)
   category?: string;
   alcoholContent?: number;
   flavor?: string;
@@ -258,21 +281,6 @@ const unitOptions = [
 // 酒单列表
 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,
@@ -282,7 +290,7 @@ const pageable = reactive({
 
 // 分页后的酒单列表
 const paginatedDishList = computed(() => {
-  const list = filteredDishList.value;
+  const list = dishList.value;
   const start = (pageable.pageNum - 1) * pageable.pageSize;
   const end = start + pageable.pageSize;
   return list.slice(start, end);
@@ -293,7 +301,7 @@ const dialogTitle = computed(() => (editIndex.value !== null ? "编辑" : "新
 
 // 表单数据
 const formData = reactive({
-  itemType: "drink" as "drink" | "food",
+  dishMenuType: "drink" as "drink" | "food",
   dishName: "",
   dishPrice: undefined as number | undefined,
   costPrice: undefined as number | undefined,
@@ -360,23 +368,26 @@ const handleTabClick = async val => {
   });
 };
 
+// 分类点击处理
+const handleCategoryClick = async (category: "drink" | "food") => {
+  if (activeCategory.value === category) {
+    return; // 如果已经是当前分类,不重复调用
+  }
+  activeCategory.value = category;
+  await handleCategoryChange();
+};
+
 // 分类切换
-const handleCategoryChange = () => {
+const handleCategoryChange = async () => {
   pageable.pageNum = 1;
+  // 重新调用接口获取数据
+  await loadDishList();
   updatePagination();
 };
 
-// 监听分类变化
-watch(
-  () => activeCategory.value,
-  () => {
-    handleCategoryChange();
-  }
-);
-
 // 更新分页总数
 const updatePagination = () => {
-  pageable.total = filteredDishList.value.length;
+  pageable.total = dishList.value.length;
 };
 
 // 分页大小改变
@@ -407,26 +418,27 @@ const editDish = async (dish: Dish, index: number) => {
   }
 
   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;
+    // 使用列表中的数据作为详情(新接口可能没有单独的详情接口)
+    // 或者可以从列表中直接获取
+    const dishDetail = dish;
+    editId.value = dishDetail.id || null;
+    // 将dishMenuType转换为字符串格式:1=餐食(food), 2=酒水(drink)
+    if (typeof dishDetail.dishMenuType === "number") {
+      formData.dishMenuType = dishDetail.dishMenuType === 2 ? "drink" : "food";
     } else {
-      ElMessage.error(res?.msg || "获取酒单详情失败");
+      formData.dishMenuType = (dishDetail.dishMenuType || "drink") as "drink" | "food";
     }
+    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;
   } catch (error: any) {
     console.error("获取酒单详情失败:", error);
     ElMessage.error(error?.msg || "获取酒单详情失败,请重试");
@@ -448,16 +460,8 @@ const deleteDishHandler = async (id: string | number, index: number) => {
       return;
     }
 
-    // 根据当前tab确定dishType:0表示菜单, 1表示推荐
-    const dishType = activeTab.value === "recommended" ? 1 : 0;
-
-    // 调用删除接口
-    const params = {
-      dishType: dishType,
-      ids: id
-    };
-
-    const res: any = await deleteDish(params);
+    // 调用删除接口,新接口只需要id参数
+    const res: any = await deleteBarMenu({ id: Number(id) });
     if (res && (res.code === 200 || res.code === "200")) {
       ElMessage.success("删除成功");
       await loadDishList();
@@ -494,6 +498,10 @@ const setRecommend = async (id: string | number, index: number) => {
     }
 
     // 通过更新接口设置推荐
+    // 将dishMenuType转换为数字:drink=2, food=1
+    const dishMenuTypeNum =
+      typeof dish.dishMenuType === "string" ? (dish.dishMenuType === "drink" ? 2 : 1) : dish.dishMenuType || 2;
+
     const params: any = {
       id: dish.id,
       storeId: Number(storeId),
@@ -504,13 +512,13 @@ const setRecommend = async (id: string | number, index: number) => {
       imgUrl: dish.imgUrl,
       description: dish.description || "",
       dishType: 1, // 设置为推荐
-      itemType: dish.itemType || "drink",
+      dishMenuType: dishMenuTypeNum,
       category: dish.category || "",
       alcoholContent: dish.alcoholContent,
       flavor: dish.flavor || ""
     };
 
-    const res: any = await createOrUpdateDish(params);
+    const res: any = await saveOrUpdateBarMenu(params);
     if (res && (res.code === 200 || res.code === "200")) {
       ElMessage.success("设置推荐成功");
       await loadDishList();
@@ -541,6 +549,10 @@ const cancelRecommend = async (id: string | number, index: number) => {
     }
 
     // 通过更新接口取消推荐
+    // 将dishMenuType转换为数字:drink=2, food=1
+    const dishMenuTypeNum =
+      typeof dish.dishMenuType === "string" ? (dish.dishMenuType === "drink" ? 2 : 1) : dish.dishMenuType || 2;
+
     const params: any = {
       id: dish.id,
       storeId: Number(storeId),
@@ -551,13 +563,13 @@ const cancelRecommend = async (id: string | number, index: number) => {
       imgUrl: dish.imgUrl,
       description: dish.description || "",
       dishType: 0, // 设置为未推荐
-      itemType: dish.itemType || "drink",
+      dishMenuType: dishMenuTypeNum,
       category: dish.category || "",
       alcoholContent: dish.alcoholContent,
       flavor: dish.flavor || ""
     };
 
-    const res: any = await createOrUpdateDish(params);
+    const res: any = await saveOrUpdateBarMenu(params);
     if (res && (res.code === 200 || res.code === "200")) {
       ElMessage.success("取消推荐成功");
       await loadDishList();
@@ -573,7 +585,7 @@ const cancelRecommend = async (id: string | number, index: number) => {
 
 // 重置表单
 const resetForm = () => {
-  formData.itemType = "drink";
+  formData.dishMenuType = "drink";
   formData.dishName = "";
   formData.dishPrice = undefined;
   formData.costPrice = undefined;
@@ -610,6 +622,9 @@ const handleSubmit = async () => {
     }
 
     // 构建请求参数
+    // 将dishMenuType转换为数字:drink=2(酒水), food=1(餐食)
+    const dishMenuTypeNum = formData.dishMenuType === "drink" ? 2 : 1;
+
     const params: any = {
       storeId: Number(storeId),
       dishName: formData.dishName,
@@ -619,7 +634,7 @@ const handleSubmit = async () => {
       imgUrl: formData.imgUrl,
       description: formData.description || "",
       dishType: formData.isRecommended ? 1 : 0,
-      itemType: formData.itemType,
+      dishMenuType: dishMenuTypeNum,
       category: formData.category || "",
       alcoholContent: formData.alcoholContent,
       flavor: formData.flavor || ""
@@ -630,8 +645,28 @@ const handleSubmit = async () => {
       params.id = editId.value;
     }
 
-    const res: any = await createOrUpdateDish(params);
+    // 新接口需要将参数包装在storeMenu对象中
+    const res: any = await saveOrUpdateBarMenu(params);
     if (res && (res.code === 200 || res.code === "200")) {
+      // 调用saveOfficialImg接口保存图片信息
+      if (formData.imgUrl) {
+        try {
+          const saveImgParams = [
+            {
+              imgUrl: formData.imgUrl,
+              imgType: 7, // 参数imageType设置为7
+              businessId: res.data?.id || editId.value, // 使用返回的id或编辑的id作为businessId
+              storeId: Number(storeId),
+              imgSort: 0 // 默认排序
+            }
+          ];
+          await saveOfficialImg(saveImgParams);
+        } catch (error: any) {
+          console.error("保存图片信息失败:", error);
+          // 图片保存失败不影响主流程
+        }
+      }
+
       ElMessage.success(editId.value ? "编辑成功" : "新建成功");
       dialogVisible.value = false;
       resetForm();
@@ -659,11 +694,32 @@ const loadDishList = async () => {
       return;
     }
 
-    // 根据tab切换调用不同的dishType参数
-    // dishType: 0表示菜单, 1表示推荐
-    const dishType = activeTab.value === "recommended" ? 1 : 0;
-    const res: any = await getDishList({ storeId: Number(storeId), dishType });
+    // 根据分类筛选传递dishMenuType参数
+    // dishMenuType: 1-菜单(餐食), 2-酒水
+    // 根据tab传递dishType参数
+    // dishType: 0-非推荐, 1-推荐
+    let dishMenuType: number | undefined;
+    let dishType: number | undefined;
+
+    if (activeTab.value === "menu") {
+      // 在菜单tab下,根据activeCategory传递dishMenuType,dishType为0(非推荐)
+      dishMenuType = activeCategory.value === "drink" ? 2 : 1;
+      // dishType = 0;
+    } else {
+      // 在推荐tab下,不传dishMenuType(获取所有类型),dishType为1(推荐)
+      dishType = 1;
+    }
 
+    const params: any = { storeId: Number(storeId) };
+    if (dishMenuType !== undefined) {
+      params.dishMenuType = dishMenuType;
+    }
+    if (dishType !== undefined) {
+      params.dishType = dishType;
+    }
+
+    const res: any = await getBarMenuList(params);
+    console.log(res, "res--x-res--xres-res");
     if (res && (res.code === 200 || res.code === "200") && res.data) {
       const dataList = Array.isArray(res.data) ? res.data : [];
       dishList.value = dataList.map((dish: any) => ({
@@ -676,11 +732,14 @@ const loadDishList = async () => {
         description: dish.description || "",
         dishType: dish.dishType !== undefined ? dish.dishType : 0,
         likeCount: dish.likeCount || 0,
-        itemType: dish.itemType || "drink",
+        // 将后端返回的数字转换为字符串:1-餐食(food), 2-酒水(drink)
+        dishMenuType:
+          typeof dish.dishMenuType === "number" ? (dish.dishMenuType === 2 ? "drink" : "food") : dish.dishMenuType || "drink",
         category: dish.category || "",
         alcoholContent: dish.alcoholContent,
         flavor: dish.flavor || ""
       }));
+      console.log(dishList.value, "dishList-xdishList");
     }
   } catch (error) {
     console.error("获取酒单列表失败:", error);
@@ -703,24 +762,70 @@ const handleFileRemove = () => {
 };
 
 // 下载模板
-const downloadTemplate = () => {
-  ElMessage.info("模板下载功能开发中");
-  // TODO: 实现模板下载
+const downloadTemplate = async () => {
+  try {
+    const res: any = await downloadBarMenuTemplate();
+    // 如果响应是blob类型,直接使用;否则尝试从data中获取
+    const blob =
+      res instanceof Blob
+        ? res
+        : new Blob([res], {
+            type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+          });
+    // 创建下载链接
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = `酒单导入模板_${new Date().getTime()}.xlsx`;
+    document.body.appendChild(link);
+    link.click();
+    document.body.removeChild(link);
+    window.URL.revokeObjectURL(url);
+    ElMessage.success("模板下载成功");
+  } catch (error: any) {
+    console.error("下载模板失败:", error);
+    ElMessage.error(error?.msg || "模板下载失败,请重试");
+  }
 };
 
 // 提交导入
 const handleImportSubmit = async () => {
-  if (!importFile.value) {
+  if (!importFile.value || !importFile.value.raw) {
     ElMessage.warning("请先选择文件");
     return;
   }
 
+  const userInfo: any = localGet("geeker-user")?.userInfo || {};
+  const storeId = userInfo.storeId;
+  if (!storeId) {
+    ElMessage.error("未找到店铺ID");
+    return;
+  }
+
   importLoading.value = true;
   try {
-    // TODO: 实现批量导入逻辑
-    ElMessage.info("批量导入功能开发中");
-    batchImportVisible.value = false;
+    // 创建FormData对象,只包含file
+    const formData = new FormData();
+    formData.append("file", importFile.value.raw);
+
+    // storeId作为query参数传递
+    const res: any = await importBarMenuExcel(formData, storeId);
+    if (res && (res.code === 200 || res.code === "200")) {
+      ElMessage.success("导入成功");
+      batchImportVisible.value = false;
+      importFile.value = null;
+      // 清空上传组件的文件列表
+      if (uploadRef.value) {
+        uploadRef.value.clearFiles();
+      }
+      // 重新加载酒单列表
+      await loadDishList();
+      updatePagination();
+    } else {
+      ElMessage.error(res?.msg || "导入失败");
+    }
   } catch (error: any) {
+    console.error("导入失败:", error);
     ElMessage.error(error?.msg || "导入失败,请重试");
   } finally {
     importLoading.value = false;
@@ -850,9 +955,66 @@ onMounted(async () => {
     gap: 12px;
     justify-content: flex-end;
   }
-  .import-tip {
-    margin-top: 10px;
-    text-align: center;
+  .import-steps {
+    .import-step {
+      margin-bottom: 30px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+      .step-header {
+        display: flex;
+        gap: 12px;
+        align-items: center;
+        margin-bottom: 16px;
+        .step-number {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 24px;
+          height: 24px;
+          font-size: 14px;
+          font-weight: 500;
+          color: white;
+          background-color: var(--el-color-primary);
+          border-radius: 50%;
+        }
+        .step-title {
+          font-size: 16px;
+          font-weight: 500;
+          color: var(--el-text-color-primary);
+        }
+      }
+    }
+    .import-upload {
+      :deep(.el-upload-dragger) {
+        width: 100%;
+        padding: 40px 20px;
+        .el-icon--upload {
+          font-size: 48px;
+          color: var(--el-text-color-placeholder);
+        }
+        .el-upload__text {
+          margin-top: 16px;
+          font-size: 14px;
+          color: var(--el-text-color-regular);
+          em {
+            font-style: normal;
+            color: var(--el-color-primary);
+          }
+        }
+      }
+    }
+  }
+}
+:deep(.wine-menu-dialog) {
+  .el-dialog__body {
+    height: 600px;
+    padding: 20px;
+    overflow-y: auto;
+  }
+  .dialog-content-wrapper {
+    height: 100%;
+    overflow-y: auto;
   }
 }
 </style>