Răsfoiți Sursa

重构设施与服务

lxr 3 săptămâni în urmă
părinte
comite
5737f55447

+ 39 - 1
src/api/modules/storeDecoration.ts

@@ -255,7 +255,45 @@ export const importServiceExcel = (formData: FormData, storeId: string | number)
     data: formData
   });
 };
-// 设施管理相关接口
+// ==================== 设施区域管理(与商家端 facilityOrService 一致,alienStore 前缀) ====================
+// 获取设施区域列表(标签) /alienStore/sports/facility/area/list
+export const getSportsFacilityAreaList = (params: { storeId: number }) => {
+  return httpApi.get(`/alienStore/sports/facility/area/list`, params, { loading: false });
+};
+// 新建设施区域
+export const createSportsFacilityArea = (params: { storeId: number; facilityCategoryName: string }) => {
+  return httpApi.post(`/alienStore/sports/facility/area/create`, params);
+};
+// 编辑设施区域
+export const updateSportsFacilityArea = (params: { areaId: number; areaName: string }) => {
+  return httpApi.post(`/alienStore/sports/facility/area/update`, params);
+};
+// 更新设施区域实景图片
+export const updateSportsFacilityAreaHeadUrl = (params: { areaId: number; areaHeadUrl: string }) => {
+  return httpApi.post(`/alienStore/sports/facility/area/updateHeadUrl`, params);
+};
+// 批量删除设施区域
+export const batchDeleteSportsFacilityArea = (params: { storeId: number; areaIds: (string | number)[] }) => {
+  return httpApi.post(`/alienStore/sports/facility/area/batchDelete`, params);
+};
+// 根据区域ID获取设施列表 /alienStore/sports/equipment/facility/listByArea
+export const getSportsEquipmentFacilityListByArea = (params: { storeId: number; areaId: number }) => {
+  return httpApi.get(`/alienStore/sports/equipment/facility/listByArea`, params);
+};
+// 新增运动设施
+export const saveSportsEquipmentFacility = (params: any) => {
+  return httpApi.post(`/alienStore/sports/equipment/facility/save`, params);
+};
+// 修改运动设施
+export const updateSportsEquipmentFacility = (params: any) => {
+  return httpApi.post(`/alienStore/sports/equipment/facility/update`, params);
+};
+// 删除运动设施
+export const deleteSportsEquipmentFacility = (params: { equipmentId: number }) => {
+  return httpApi.post(`/alienStore/sports/equipment/facility/delete`, params);
+};
+
+// 设施管理相关接口(兼容旧接口,使用 facilityCategory)
 //获取健身设施列表
 export const getFacilityList = (params: any) => {
   return httpApi.get(`/alienStorePlatform/sportsEquipmentFacility/getListByStoreIdAndCategory`, params);

+ 16 - 0
src/api/upload.js

@@ -171,3 +171,19 @@ export async function uploadFileToOss(file, fileType, options) {
   const urls = await uploadFilesToOss(file, fileType, options);
   return urls[0];
 }
+
+/**
+ * 兼容 UploadImg 等组件的 api 格式:接收 FormData,返回 { data: { fileUrl } }
+ * @param {FormData} formData 包含 file 字段
+ * @param {string} [fileType] 文件类型,默认 'image'
+ * @param {{ showLoading?: boolean }} [options]
+ * @returns {Promise<{ data: { fileUrl: string } }>}
+ */
+export async function uploadFormDataToOss(formData, fileType = "image", options = {}) {
+  const file = formData.get("file");
+  if (!file || !(file instanceof File)) {
+    throw new Error("请选择要上传的文件");
+  }
+  const url = await uploadFileToOss(file, fileType, options);
+  return { data: { fileUrl: url } };
+}

+ 720 - 652
src/views/storeDecoration/facilitiesAndServices/components/FacilityManagement.vue

@@ -1,158 +1,206 @@
 <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 v-if="facilityAreas.length === 0" class="empty-page-state">
+      <el-empty description="暂无数据" />
+      <el-button type="primary" :loading="saving" @click="openCreateAreaDialog"> 新建区域 </el-button>
     </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>
+    <template v-else>
+      <!-- 区域选择与操作(标签样式 + 下拉菜单) -->
+      <div class="header-section">
+        <div class="area-tabs">
+          <div
+            v-for="(area, index) in facilityAreas"
+            :key="area.id"
+            class="area-tab-item"
+            :class="{ active: activeTab === index }"
+            @click="
+              activeTab = index;
+              handleAreaChange();
+            "
+          >
+            <span class="tab-text">{{ area.facilityAreaName || area.areaName }}</span>
+          </div>
+          <el-dropdown trigger="click" class="area-dropdown">
+            <span class="dropdown-trigger">
+              <el-icon><ArrowDown /></el-icon>
+            </span>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item @click="openEditAreaDialog">
+                  <span class="dropdown-item-edit">编辑</span>
+                </el-dropdown-item>
+                <el-dropdown-item @click="openDeleteAreaDialog">
+                  <span class="dropdown-item-delete">删除</span>
+                </el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+        </div>
+        <el-button type="primary" @click="openCreateAreaDialog"> 新建区域 </el-button>
+      </div>
+
+      <!-- 添加实景图片 -->
+      <div class="image-upload-section">
+        <div class="section-header">
+          <span class="section-title">添加实景图片</span>
+          <el-button type="primary" link @click="openImageUpload"> 上传 </el-button>
+        </div>
+        <div class="panorama-list">
+          <div v-if="panoramaImages.length > 0" class="panorama-images">
+            <div v-for="(img, idx) in panoramaImages" :key="idx" class="panorama-item" @click="previewImage(img)">
+              <el-image :src="img" fit="cover" class="panorama-img" />
+            </div>
+          </div>
+          <div class="upload-trigger" @click="openImageUpload">
+            <el-icon><Plus /></el-icon>
+            <span>上传图片 ({{ panoramaImages.length }}/20)</span>
+          </div>
+        </div>
+        <div v-if="panoramaImages.length > 0" class="image-actions">
+          <el-button @click="handleRemoveAllImages"> 删除 </el-button>
+          <el-button type="primary" :loading="saving" @click="handleSaveImages"> 保存 </el-button>
+        </div>
       </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 class="facility-list-section">
+        <div class="section-header">
+          <span class="section-title">设施列表</span>
+          <div class="add-hint">
+            <el-button type="primary" @click="openCreateFacilityDialog"> 新增 </el-button>
+            <span class="hint-text">(点击弹窗, 超过10条可滑动)</span>
+          </div>
+        </div>
+        <el-table :data="facilityList" border style="width: 100%">
+          <el-table-column type="index" label="序号" width="60" align="center" />
+          <el-table-column label="图片" width="100" align="center">
+            <template #default="{ row }">
+              <el-image
+                v-if="row.equipmentImage || row.imgUrl"
+                :src="row.equipmentImage || row.imgUrl"
+                fit="cover"
+                class="facility-thumb"
+                :preview-src-list="[row.equipmentImage || row.imgUrl]"
+              >
+                <template #error>
+                  <div class="image-placeholder">
+                    <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="facilityName" label="名称" min-width="120">
+            <template #default="{ row }">
+              {{ row.equipmentName || row.facilityName }}
+            </template>
+          </el-table-column>
+          <el-table-column label="数量" width="100" align="center">
+            <template #default="{ row }">
+              {{ row.equipmentNums ?? row.quantity ?? 0 }}
+            </template>
+          </el-table-column>
+          <el-table-column label="是否显示" width="100" align="center">
+            <template #default="{ row }">
+              {{ row.displayInStoreDetail == 1 ? "是" : "否" }}
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="150" align="center" fixed="right">
+            <template #default="{ row }">
+              <el-button type="primary" link @click="editFacility(row)"> 编辑 </el-button>
+              <el-button type="primary" link @click="deleteFacility(row)"> 删除 </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
       </div>
-    </div>
+    </template>
 
-    <!-- 新建/编辑弹窗 -->
-    <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-dialog v-model="areaDialogVisible" :title="areaDialogTitle" width="400px" @close="resetAreaForm">
+      <el-form ref="areaFormRef" :model="areaFormData" :rules="areaRules" label-width="0">
+        <el-form-item prop="name">
+          <el-input v-model="areaFormData.name" placeholder="请输入区域名称" maxlength="20" show-word-limit clearable />
         </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>
+      <template #footer>
+        <el-button @click="areaDialogVisible = false"> 取消 </el-button>
+        <el-button type="primary" :loading="saving" @click="submitAreaForm"> 确定 </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 新增/编辑设施弹窗 -->
+    <el-dialog v-model="facilityDialogVisible" :title="facilityDialogTitle" width="400px" @close="resetFacilityForm">
+      <el-form ref="facilityFormRef" :model="facilityFormData" :rules="facilityRules" label-width="120px">
+        <el-form-item label="名称*" prop="name">
+          <el-input v-model="facilityFormData.name" placeholder="请输入" maxlength="50" clearable />
         </el-form-item>
-        <el-form-item label="品牌" prop="brand">
-          <el-input v-model="formData.brand" placeholder="请输入" maxlength="50" clearable />
+        <el-form-item label="数量*" prop="quantity">
+          <el-input-number v-model="facilityFormData.quantity" :min="1" :max="99" style="width: 100%" />
         </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 label="图片*" prop="imageUrl">
+          <UploadImg
+            v-model:image-url="facilityFormData.imageUrl"
+            :width="'120px'"
+            :height="'120px'"
+            :file-size="20"
+            :api="formData => uploadFormDataToOss(formData, 'image')"
+            :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
           />
         </el-form-item>
-        <el-form-item label="展示在店铺详情" prop="displayInStoreDetail">
-          <el-radio-group v-model="formData.displayInStoreDetail">
+        <el-form-item label="显示在店铺详情" prop="showInDetail">
+          <el-radio-group v-model="facilityFormData.showInDetail">
             <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>
+        <el-button @click="facilityDialogVisible = false"> 取消 </el-button>
+        <el-button type="primary" :loading="saving" @click="submitFacilityForm"> 确定 </el-button>
       </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>
+    <!-- 删除区域弹窗 -->
+    <el-dialog v-model="deleteAreaDialogVisible" title="请选择需要删除的区域" width="400px">
+      <el-checkbox-group v-model="selectedAreaIds">
+        <div v-for="area in facilityAreas" :key="area.id" class="delete-item">
+          <el-checkbox :label="area.id">
+            {{ area.facilityAreaName || area.areaName }}
+          </el-checkbox>
         </div>
+      </el-checkbox-group>
+      <template #footer>
+        <el-button @click="deleteAreaDialogVisible = false"> 取消 </el-button>
+        <el-button type="primary" :loading="saving" @click="confirmDeleteAreas"> 确定 </el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 实景图片上传弹窗 -->
+    <el-dialog v-model="imageUploadDialogVisible" title="上传图片" width="600px" @close="resetImageUpload">
+      <div class="panorama-upload-dialog">
+        <el-upload
+          v-model:file-list="tempPanoramaFileList"
+          action="#"
+          list-type="picture-card"
+          :limit="20"
+          :on-preview="handlePanoramaPreview"
+          :on-remove="handlePanoramaRemove"
+          :before-upload="beforePanoramaUpload"
+          :http-request="handlePanoramaHttpUpload"
+          accept="image/*"
+        >
+          <el-icon><Plus /></el-icon>
+        </el-upload>
+        <div class="upload-tip">上传图片 ({{ tempPanoramaFileList.length }}/20)</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>
+        <el-button @click="imageUploadDialogVisible = false"> 取消 </el-button>
+        <el-button type="primary" :loading="saving" @click="confirmImageUpload"> 确定 </el-button>
       </template>
     </el-dialog>
 
@@ -169,661 +217,681 @@
 <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 type { FormInstance, FormRules, UploadRequestOptions, UploadUserFile } from "element-plus";
+import { Plus, Picture, Delete, ArrowDown } from "@element-plus/icons-vue";
+import UploadImg from "@/components/Upload/Img.vue";
 import { localGet } from "@/utils";
+import { uploadFileToOss, uploadFormDataToOss } from "@/api/upload.js";
 import {
-  getFacilityList,
-  createOrUpdateFacility,
-  getFacilityDetail,
-  deleteFacility,
-  downloadFacilityTemplate,
-  importFacilityExcel,
-  getOfficialImgList,
-  saveOfficialImg
+  getSportsFacilityAreaList,
+  createSportsFacilityArea,
+  updateSportsFacilityArea,
+  updateSportsFacilityAreaHeadUrl,
+  batchDeleteSportsFacilityArea,
+  getSportsEquipmentFacilityListByArea,
+  saveSportsEquipmentFacility,
+  updateSportsEquipmentFacility,
+  deleteSportsEquipmentFacility
 } from "@/api/modules/storeDecoration";
 
-// 设施接口
-interface Facility {
-  id?: string | number;
-  facilityCategory: number; // 1:有氧区, 2:力量区, 3:单功能机械区
-  facilityName: string;
+// 区域类型
+interface FacilityArea {
+  id: number;
+  areaName?: string;
+  facilityAreaName?: string;
+  areaHeadUrl?: string;
+}
+
+// 设施类型(与商家端一致)
+interface FacilityItem {
+  id: number;
+  areaId?: number;
+  equipmentName?: string;
+  facilityName?: string;
+  equipmentNums?: number;
   quantity?: number;
-  brand?: string;
-  description?: string;
-  displayInStoreDetail: number; // 0:隐藏, 1:显示
+  equipmentImage?: string;
+  imgUrl?: string;
+  displayInStoreDetail?: number;
 }
 
-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 storeId = ref<number | null>(null);
+const facilityAreas = ref<FacilityArea[]>([]);
+const activeTab = ref(0);
+const facilityList = ref<FacilityItem[]>([]);
+const panoramaImages = ref<string[]>([]);
+const saving = ref(false);
+
+// 区域弹窗
+const areaDialogVisible = ref(false);
+const areaFormRef = ref<FormInstance>();
+const areaFormData = reactive({ name: "" });
+const areaRules: FormRules = { name: [{ required: true, message: "请输入区域名称", trigger: "blur" }] };
+const isEditingArea = ref(false);
+const editingAreaId = ref<number | null>(null);
+const areaDialogTitle = computed(() => (isEditingArea.value ? "编辑" : "新建") + "区域");
+
+// 设施弹窗
+const facilityDialogVisible = ref(false);
+const facilityFormRef = ref<FormInstance>();
+const facilityFormData = reactive({
+  name: "",
+  quantity: 1,
+  imageUrl: "",
+  showInDetail: 1
+});
+const facilityRules: FormRules = {
+  name: [{ required: true, message: "请输入名称", trigger: "blur" }],
+  quantity: [{ required: true, message: "请输入数量", trigger: "blur" }],
+  imageUrl: [{ required: true, message: "请上传图片", trigger: "change" }]
+};
+const isEditingFacility = ref(false);
+const editingFacilityId = ref<number | null>(null);
+const facilityDialogTitle = computed(() => (isEditingFacility.value ? "编辑" : "新增"));
+
+// 删除区域弹窗
+const deleteAreaDialogVisible = ref(false);
+const selectedAreaIds = ref<(string | number)[]>([]);
+
+// 实景图片上传
+const imageUploadDialogVisible = ref(false);
+const tempPanoramaFileList = ref<UploadUserFile[]>([]);
+
+// 图片预览
 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 currentArea = computed(() => facilityAreas.value[activeTab.value]);
 
-// 弹窗标题
-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" }]
-});
+// 获取区域列表
+const fetchFacilityAreas = async () => {
+  if (!storeId.value) return;
+  try {
+    const res: any = await getSportsFacilityAreaList({ storeId: storeId.value });
+    if (res?.code === 200 && res?.data) {
+      facilityAreas.value = (res.data || []).map((a: any) => ({
+        ...a,
+        facilityAreaName: a.areaName ?? a.facilityAreaName
+      }));
+      if (facilityAreas.value.length > 0 && activeTab.value >= facilityAreas.value.length) {
+        activeTab.value = 0;
+      }
+    } else {
+      facilityAreas.value = [];
+    }
+  } catch (e) {
+    console.error("获取设施区域列表失败:", e);
+    facilityAreas.value = [];
+  }
+};
 
-// Tab切换
-const handleTabClick = async () => {
-  pageable.pageNum = 1;
-  // 调用loadFacilityList获取当前选中分类的数据
-  await loadFacilityList(activeTab.value);
-  updatePagination();
-  // 重新获取当前分区的图片列表
-  getImageList();
+// 获取设施列表
+const fetchFacilityList = async () => {
+  if (!storeId.value || !currentArea.value) {
+    facilityList.value = [];
+    return;
+  }
+  try {
+    const res: any = await getSportsEquipmentFacilityListByArea({
+      storeId: storeId.value,
+      areaId: currentArea.value.id
+    });
+    if (res?.code === 200 && res?.data) {
+      facilityList.value = (res.data || []).map((item: any) => ({
+        ...item,
+        areaId: currentArea.value.id
+      }));
+    } else {
+      facilityList.value = [];
+    }
+  } catch (e) {
+    console.error("获取设施列表失败:", e);
+    facilityList.value = [];
+  }
 };
 
-// 更新分页总数
-const updatePagination = () => {
-  const list = facilityList.value.filter(item => item.facilityCategory === activeTab.value);
-  pageable.total = list.length;
+// 获取实景图片
+const fetchPanoramaImages = () => {
+  if (!currentArea.value) {
+    panoramaImages.value = [];
+    return;
+  }
+  const url = currentArea.value.areaHeadUrl;
+  if (url) {
+    panoramaImages.value = url
+      .split(",")
+      .map(s => s.trim())
+      .filter(Boolean);
+  } else {
+    panoramaImages.value = [];
+  }
 };
 
-// 分页大小改变
-const handleSizeChange = (size: number) => {
-  pageable.pageSize = size;
-  pageable.pageNum = 1;
-  updatePagination();
+// 切换区域
+const handleAreaChange = () => {
+  fetchFacilityList();
+  fetchPanoramaImages();
 };
 
-// 当前页改变
-const handleCurrentChange = (page: number) => {
-  pageable.pageNum = page;
+// 新建区域
+const openCreateAreaDialog = () => {
+  isEditingArea.value = false;
+  editingAreaId.value = null;
+  areaFormData.name = "";
+  areaDialogVisible.value = true;
 };
 
-// 打开新建弹窗
-const openCreateDialog = () => {
-  editId.value = null;
-  resetForm();
-  dialogVisible.value = true;
+// 编辑区域
+const openEditAreaDialog = () => {
+  if (!currentArea.value) return;
+  isEditingArea.value = true;
+  editingAreaId.value = currentArea.value.id;
+  areaFormData.name = currentArea.value.facilityAreaName || currentArea.value.areaName || "";
+  areaDialogVisible.value = true;
 };
 
-// 编辑
-const editItem = async (item: Facility) => {
-  if (!item.id) {
-    ElMessage.warning("设施ID不存在");
+// 提交区域表单
+const submitAreaForm = async () => {
+  if (!areaFormRef.value) return;
+  try {
+    await areaFormRef.value.validate();
+  } catch {
     return;
   }
-
+  if (!storeId.value) {
+    ElMessage.error("未找到店铺ID");
+    return;
+  }
+  saving.value = true;
   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 = detail.quantity || 0;
-      formData.brand = detail.brand || "";
-      formData.description = detail.description || "";
-      formData.displayInStoreDetail = detail.displayInStoreDetail !== undefined ? detail.displayInStoreDetail : 1;
-      dialogVisible.value = true;
+    let res: any;
+    if (isEditingArea.value && editingAreaId.value) {
+      res = await updateSportsFacilityArea({
+        areaId: editingAreaId.value,
+        areaName: areaFormData.name.trim()
+      });
+    } else {
+      res = await createSportsFacilityArea({
+        storeId: storeId.value,
+        facilityCategoryName: areaFormData.name.trim()
+      });
+    }
+    if (res?.code === 200) {
+      ElMessage.success(isEditingArea.value ? "编辑成功" : "新增成功");
+      areaDialogVisible.value = false;
+      await fetchFacilityAreas();
+      if (!isEditingArea.value) {
+        activeTab.value = 0;
+      }
+      fetchFacilityList();
+      fetchPanoramaImages();
     } else {
-      ElMessage.error(res?.msg || "获取设施详情失败");
+      ElMessage.error(res?.msg || "操作失败");
     }
-  } catch (error: any) {
-    console.error("获取设施详情失败:", error);
-    ElMessage.error(error?.msg || "获取设施详情失败,请重试");
+  } catch (e) {
+    console.error("提交区域失败:", e);
+    ElMessage.error("操作失败");
+  } finally {
+    saving.value = false;
   }
 };
 
-// 删除
-const deleteItem = async (item: Facility) => {
+// 删除区域弹窗
+const openDeleteAreaDialog = () => {
+  selectedAreaIds.value = [];
+  deleteAreaDialogVisible.value = true;
+};
+
+// 确认删除区域
+const confirmDeleteAreas = async () => {
+  if (selectedAreaIds.value.length === 0) {
+    ElMessage.warning("请选择要删除的区域");
+    return;
+  }
+  if (!storeId.value) return;
+  saving.value = true;
   try {
-    await ElMessageBox.confirm("确认删除该设施吗?", "提示", {
-      confirmButtonText: "确定",
-      cancelButtonText: "取消",
-      type: "warning"
+    const res: any = await batchDeleteSportsFacilityArea({
+      storeId: storeId.value,
+      areaIds: selectedAreaIds.value
     });
-
-    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();
+    if (res?.code === 200) {
+      ElMessage.success(`成功删除${selectedAreaIds.value.length}个区域`);
+      deleteAreaDialogVisible.value = false;
+      await fetchFacilityAreas();
+      if (activeTab.value >= facilityAreas.value.length) {
+        activeTab.value = Math.max(0, facilityAreas.value.length - 1);
+      }
+      fetchFacilityList();
+      fetchPanoramaImages();
     } else {
       ElMessage.error(res?.msg || "删除失败");
     }
-  } catch (error: any) {
-    if (error !== "cancel") {
-      console.error("删除设施失败:", error);
-      ElMessage.error(error?.msg || "删除失败,请重试");
-    }
+  } catch (e) {
+    console.error("删除区域失败:", e);
+    ElMessage.error("删除失败");
+  } finally {
+    saving.value = false;
   }
 };
 
-// 切换显示状态
-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 openImageUpload = () => {
+  if (!currentArea.value) {
+    ElMessage.warning("请先选择区域");
+    return;
   }
+  tempPanoramaFileList.value = panoramaImages.value.map((url, i) => ({
+    uid: Date.now() + i,
+    name: `img-${i}`,
+    status: "success",
+    url
+  })) as UploadUserFile[];
+  imageUploadDialogVisible.value = true;
 };
 
-// 图片上传前验证
-const beforeImageUpload = (file: File) => {
+const beforePanoramaUpload = (file: File) => {
   const isImage = file.type.startsWith("image/");
   const isLt10M = file.size / 1024 / 1024 < 10;
-
   if (!isImage) {
-    ElMessage.error("只能上传图片文件");
+    ElMessage.error("只能上传图片文件");
     return false;
   }
   if (!isLt10M) {
-    ElMessage.error("图片大小不能超过10MB!");
-    return false;
-  }
-  if (imageFileList.value.length >= 20) {
-    ElMessage.error("最多只能上传20张图片!");
+    ElMessage.error("图片大小不能超过10MB");
     return false;
   }
   return true;
 };
 
-// 图片上传
-const handleImageUpload = async (options: UploadRequestOptions) => {
+const handlePanoramaHttpUpload = async (options: UploadRequestOptions) => {
+  const rawFile = (options.file as any).raw || options.file;
+  if (!rawFile) {
+    options.onError(new Error("文件不存在") as any);
+    return;
+  }
   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 || "图片上传失败"));
+    const fileUrl = await uploadFileToOss(rawFile, "image");
+    // el-upload 列表项可能是 options.file(包装对象)或 raw,需同时设置 url 到列表项
+    const listItem = tempPanoramaFileList.value.find(f => (f as any).raw === rawFile || (f as any) === rawFile);
+    if (listItem) {
+      (listItem as any).url = fileUrl;
+      (listItem as any).status = "success";
     }
-  } catch (error: any) {
-    options.onError(error);
-    ElMessage.error(error?.msg || "图片上传失败");
+    (options.file as any).url = fileUrl;
+    (options.file as any).status = "success";
+    (options.file as any).response = { data: { fileUrl } };
+    options.onSuccess({ data: { fileUrl } });
+  } catch (e) {
+    options.onError(e as any);
   }
 };
 
-// 图片预览
-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);
+const handlePanoramaPreview = (file: UploadUserFile) => {
+  if (file.url) {
+    imageViewerUrlList.value = tempPanoramaFileList.value.map(f => f.url).filter(Boolean) as string[];
+    imageViewerInitialIndex.value = imageViewerUrlList.value.indexOf(file.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 handlePanoramaRemove = () => {
+  // 删除由 el-upload 自动处理,无需额外逻辑
 };
 
-// 重置表单
-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;
-
+const confirmImageUpload = async () => {
+  if (!currentArea.value || !storeId.value) return;
+  const urls = tempPanoramaFileList.value.map(f => f.url).filter((u): u is string => !!u && u.startsWith("http"));
   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();
+    saving.value = true;
+    const res: any = await updateSportsFacilityAreaHeadUrl({
+      areaId: currentArea.value.id,
+      areaHeadUrl: urls.join(",")
+    });
+    if (res?.code === 200) {
+      panoramaImages.value = urls;
+      if (currentArea.value) currentArea.value.areaHeadUrl = urls.join(",");
+      ElMessage.success("保存成功");
+      imageUploadDialogVisible.value = false;
     } else {
-      ElMessage.error(res?.msg || (editId.value ? "编辑失败" : "添加失败"));
+      ElMessage.error(res?.msg || "保存失败");
     }
-  } catch (error: any) {
-    console.error("操作失败:", error);
-    ElMessage.error(error?.msg || "操作失败,请重试");
+  } catch (e) {
+    console.error("保存实景图片失败:", e);
+    ElMessage.error("保存失败");
   } finally {
-    submitLoading.value = false;
+    saving.value = false;
   }
 };
 
-// 加载设施列表
-const loadFacilityList = async (category?: number) => {
+const handleRemoveAllImages = async () => {
+  if (!currentArea.value) return;
   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
+    saving.value = true;
+    const res: any = await updateSportsFacilityAreaHeadUrl({
+      areaId: currentArea.value.id,
+      areaHeadUrl: ""
     });
-    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
-        }));
-      }
+    if (res?.code === 200) {
+      panoramaImages.value = [];
+      if (currentArea.value) currentArea.value.areaHeadUrl = "";
+      ElMessage.success("删除成功");
+    } else {
+      ElMessage.error(res?.msg || "删除失败");
     }
-  } catch (error) {
-    console.error("获取设施列表失败:", error);
+  } catch (e) {
+    console.error("删除图片失败:", e);
+    ElMessage.error("删除失败");
+  } finally {
+    saving.value = false;
   }
 };
-
-// 批量导入
-const handleBatchImport = () => {
-  batchImportVisible.value = true;
+const handleSaveImages = () => {
+  if (!currentArea.value) return;
+  openImageUpload();
 };
 
-// 文件选择
-const handleFileChange = (file: UploadFile) => {
-  importFile.value = file;
+const resetImageUpload = () => {
+  tempPanoramaFileList.value = [];
 };
 
-// 文件移除
-const handleFileRemove = () => {
-  importFile.value = null;
+const previewImage = (url: string) => {
+  imageViewerUrlList.value = panoramaImages.value;
+  imageViewerInitialIndex.value = panoramaImages.value.indexOf(url);
+  imageViewerVisible.value = true;
 };
 
-// 下载模板
-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 || "模板下载失败,请重试");
+// 设施 CRUD
+const openCreateFacilityDialog = () => {
+  if (!currentArea.value) {
+    ElMessage.warning("请先选择区域");
+    return;
   }
+  isEditingFacility.value = false;
+  editingFacilityId.value = null;
+  facilityFormData.name = "";
+  facilityFormData.quantity = 1;
+  facilityFormData.imageUrl = "";
+  facilityFormData.showInDetail = 1;
+  facilityDialogVisible.value = true;
+};
+
+const editFacility = (item: FacilityItem) => {
+  isEditingFacility.value = true;
+  editingFacilityId.value = item.id;
+  facilityFormData.name = item.equipmentName || item.facilityName || "";
+  facilityFormData.quantity = item.equipmentNums ?? item.quantity ?? 1;
+  facilityFormData.imageUrl = item.equipmentImage || item.imgUrl || "";
+  facilityFormData.showInDetail = item.displayInStoreDetail ?? 1;
+  facilityDialogVisible.value = true;
 };
 
-// 提交导入
-const handleImportSubmit = async () => {
-  if (!importFile.value || !importFile.value.raw) {
-    ElMessage.warning("请先选择文件");
+const submitFacilityForm = async () => {
+  if (!facilityFormRef.value) return;
+  try {
+    await facilityFormRef.value.validate();
+  } catch {
     return;
   }
-
-  const userInfo: any = localGet("geeker-user")?.userInfo || {};
-  const storeId = userInfo.storeId;
-  if (!storeId) {
-    ElMessage.error("未找到店铺ID");
+  if (!storeId.value || !currentArea.value) {
+    ElMessage.error("未找到店铺或区域");
     return;
   }
-
-  importLoading.value = true;
+  saving.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();
+    const params: any = {
+      storeId: storeId.value,
+      areaId: currentArea.value.id,
+      equipmentName: facilityFormData.name.trim(),
+      equipmentNums: facilityFormData.quantity,
+      equipmentImage: facilityFormData.imageUrl,
+      displayInStoreDetail: facilityFormData.showInDetail
+    };
+    let res: any;
+    if (isEditingFacility.value && editingFacilityId.value) {
+      params.equipmentId = editingFacilityId.value;
+      res = await updateSportsEquipmentFacility(params);
+    } else {
+      res = await saveSportsEquipmentFacility(params);
+    }
+    if (res?.code === 200) {
+      ElMessage.success(isEditingFacility.value ? "编辑成功" : "添加成功");
+      facilityDialogVisible.value = false;
+      await fetchFacilityList();
     } else {
-      ElMessage.error(res?.msg || "导入失败");
+      ElMessage.error(res?.msg || "操作失败");
     }
-  } catch (error: any) {
-    console.error("导入失败:", error);
-    ElMessage.error(error?.msg || "导入失败,请重试");
+  } catch (e) {
+    console.error("提交设施失败:", e);
+    ElMessage.error("操作失败");
   } finally {
-    importLoading.value = false;
+    saving.value = false;
   }
 };
 
-// 获取图片列表
-const getImageList = async () => {
+const deleteFacility = async (item: FacilityItem) => {
   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)
-        }));
-      }
+    await ElMessageBox.confirm("确定删除该设施?", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning"
+    });
+  } catch {
+    return;
+  }
+  try {
+    const res: any = await deleteSportsEquipmentFacility({ equipmentId: item.id });
+    if (res?.code === 200) {
+      ElMessage.success("删除成功");
+      await fetchFacilityList();
+    } else {
+      ElMessage.error(res?.msg || "删除失败");
     }
-  } catch (error) {
-    console.error("获取图片列表失败:", error);
+  } catch (e) {
+    console.error("删除设施失败:", e);
+    ElMessage.error("删除失败");
   }
 };
 
-// 页面初始化
+const resetAreaForm = () => {
+  areaFormData.name = "";
+  areaFormRef.value?.clearValidate();
+};
+const resetFacilityForm = () => {
+  facilityFormData.name = "";
+  facilityFormData.quantity = 1;
+  facilityFormData.imageUrl = "";
+  facilityFormData.showInDetail = 1;
+  facilityFormRef.value?.clearValidate();
+};
+
 onMounted(async () => {
-  await loadFacilityList();
-  updatePagination();
-  getImageList();
+  const userInfo: any = localGet("geeker-user")?.userInfo || {};
+  storeId.value = userInfo.storeId ? Number(userInfo.storeId) : null;
+  if (!storeId.value) {
+    ElMessage.warning("未找到店铺ID");
+    return;
+  }
+  await fetchFacilityAreas();
+  if (facilityAreas.value.length > 0) {
+    await fetchFacilityList();
+    fetchPanoramaImages();
+  }
 });
 </script>
 
 <style scoped lang="scss">
 .facility-management-container {
+  .empty-page-state {
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    align-items: center;
+    padding: 60px 0;
+  }
   .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 {
+    .area-tabs {
       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) {
+      gap: 8px;
+      align-items: center;
+      .area-tab-item {
         position: relative;
-        .upload-tip {
+        padding: 8px 12px;
+        cursor: pointer;
+        .tab-text {
+          font-size: 16px;
+          font-weight: 500;
+          color: #334154;
+        }
+        &.active .tab-text {
+          font-weight: 600;
+          color: #334154;
+        }
+        .tab-underline {
           position: absolute;
-          bottom: 8px;
+          bottom: 0;
           left: 50%;
-          font-size: 12px;
-          color: var(--el-text-color-secondary);
+          width: 24px;
+          height: 4px;
+          background: var(--el-color-primary);
+          border-radius: 2px;
           transform: translateX(-50%);
         }
       }
+      .area-dropdown {
+        margin-left: 4px;
+        .dropdown-trigger {
+          display: inline-flex;
+          align-items: center;
+          padding: 4px;
+          color: #909399;
+          cursor: pointer;
+          &:hover {
+            color: var(--el-color-primary);
+          }
+        }
+      }
     }
   }
-  .facility-list-section {
-    .section-title {
+  .image-upload-section {
+    padding: 20px;
+    margin-bottom: 20px;
+    background: var(--el-bg-color-page);
+    border-radius: 8px;
+    .section-header {
+      display: flex;
+      gap: 12px;
+      align-items: center;
       margin-bottom: 16px;
-      font-size: 16px;
-      font-weight: 500;
-      color: var(--el-text-color-primary);
+      .section-title {
+        font-size: 16px;
+        font-weight: 500;
+      }
     }
-    .pagination-section {
+    .panorama-list {
       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 {
+      flex-wrap: wrap;
+      gap: 12px;
+      .panorama-images {
         display: flex;
+        flex-wrap: wrap;
         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%;
+      }
+      .panorama-item {
+        width: 120px;
+        height: 90px;
+        overflow: hidden;
+        cursor: pointer;
+        border-radius: 8px;
+        .panorama-img {
+          width: 100%;
+          height: 100%;
         }
-        .step-title {
-          font-size: 16px;
-          font-weight: 500;
-          color: var(--el-text-color-primary);
+      }
+      .upload-trigger {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+        align-items: center;
+        justify-content: center;
+        width: 120px;
+        height: 90px;
+        color: var(--el-text-color-placeholder);
+        cursor: pointer;
+        border: 2px dashed var(--el-border-color);
+        border-radius: 8px;
+        &:hover {
+          color: var(--el-color-primary);
+          border-color: var(--el-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);
-          }
+    .image-actions {
+      display: flex;
+      gap: 12px;
+      margin-top: 16px;
+    }
+  }
+  .facility-list-section {
+    .section-header {
+      display: flex;
+      gap: 12px;
+      align-items: center;
+      margin-bottom: 16px;
+      .section-title {
+        font-size: 16px;
+        font-weight: 500;
+      }
+      .add-hint {
+        display: flex;
+        gap: 8px;
+        align-items: center;
+        .hint-text {
+          font-size: 12px;
+          color: var(--el-text-color-secondary);
         }
       }
     }
+    .facility-thumb {
+      width: 60px;
+      height: 60px;
+      border-radius: 4px;
+    }
+    .image-placeholder {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 60px;
+      height: 60px;
+      color: var(--el-text-color-placeholder);
+      background: var(--el-fill-color-light);
+      border-radius: 4px;
+    }
+  }
+  .delete-item {
+    padding: 8px 0;
+  }
+}
+.panorama-upload-dialog {
+  .upload-tip {
+    margin-top: 8px;
+    font-size: 12px;
+    color: var(--el-text-color-secondary);
+    text-align: center;
+  }
+  :deep(.el-upload--picture-card) {
+    width: 120px;
+    height: 120px;
+  }
+  :deep(.el-upload-list--picture-card .el-upload-list__item) {
+    width: 120px;
+    height: 120px;
   }
 }
 </style>

+ 2 - 2
src/views/storeDecoration/facilitiesAndServices/components/ServiceManagement.vue

@@ -117,7 +117,7 @@
             :width="'150px'"
             :height="'150px'"
             :file-size="9999"
-            :api="uploadImg"
+            :api="formData => uploadFormDataToOss(formData, 'image')"
             :file-type="['image/jpeg', 'image/png', 'image/gif', 'image/webp']"
             :border-radius="'8px'"
           />
@@ -187,7 +187,7 @@ 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 { uploadFormDataToOss } from "@/api/upload.js";
 import { localGet } from "@/utils";
 import {
   getServiceList,

+ 2 - 4
src/views/storeDecoration/facilitiesAndServices/index.vue

@@ -12,14 +12,12 @@ import { localGet } from "@/utils";
 import ServiceManagement from "./components/ServiceManagement.vue";
 import FacilityManagement from "./components/FacilityManagement.vue";
 
-// 根据用户业务类型判断使用哪个组件
-// 运动健身 -> FacilityManagement (设施管理)
+// 根据用户业务类型判断使用哪个组件(与商家端 facilityOrService 一致)
+// 运动健身 -> 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 || "";
 
   // 如果是"运动健身",显示设施管理