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