Преглед изворни кода

feat(ar): 添加房间管理功能- 新增房间管理页面,包括房间列表、新增房间、编辑房间和删除房间等功能
- 实现房间图片上传和预览功能- 添加面包屑导航和返回按钮
- 优化用户体验,增加表单验证和友好的提示信息

nahida пре 11 месеци
родитељ
комит
64de4f7ee4
2 измењених фајлова са 610 додато и 0 уклоњено
  1. 28 0
      src/utils/request.ts
  2. 582 0
      src/views/ar/room.vue

+ 28 - 0
src/utils/request.ts

@@ -28,6 +28,7 @@ instance.interceptors.request.use(
 // 响应拦截器
 instance.interceptors.response.use(
   (response: AxiosResponse) => {
+    console.log(response)
     return response.data; // 返回响应数据
   },
   (error:AxiosError) => {
@@ -68,3 +69,30 @@ export const clientPostFormData = <T, R>(url: string, data: T, config?: AxiosReq
       'Content-Type': 'multipart/form-data', // 明确设置为 multipart/form-data
     },
   });
+
+export const clientDownloadExcel = async (url:string,config?:AxiosRequestConfig) =>{
+  const res = await axios.get(import.meta.env.VITE_APP_BASE_API+url, {
+    responseType: 'blob',
+    ...config
+  })
+  const contentDisposition = res.headers['content-disposition']
+  const disposition = decodeURIComponent(contentDisposition);
+  console.log(res)
+  let filename = new Date().getTime() + '.xlsx'
+
+  if (disposition) {
+    const fileNameMatch = disposition.match(/filename="?([^"]+)"?/)
+    if (fileNameMatch && fileNameMatch.length > 1) {
+      filename = fileNameMatch[1]
+    }
+  }
+  const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+  const downloadUrl = window.URL.createObjectURL(blob)
+  const link = document.createElement('a')
+  link.href = downloadUrl
+  link.setAttribute('download', filename)
+  document.body.appendChild(link)
+  link.click()
+  link.remove()
+  window.URL.revokeObjectURL(downloadUrl)
+}

+ 582 - 0
src/views/ar/room.vue

@@ -0,0 +1,582 @@
+<script setup lang="ts">
+// 当前的户型号是262b90e13929c5d589a1c111b9fe30b7  后面会从外面传入 先暂时写死这一个
+
+import { onMounted, ref, reactive } from 'vue'
+import { clientGet, clientPost, clientPostFormData } from '@/utils/request.ts'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Edit, Delete, ArrowLeft, HomeFilled, View } from '@element-plus/icons-vue'
+import { useRoute, useRouter } from 'vue-router'
+
+interface AddAndUpdateRoomType {
+  id?: string;                  // 主键
+  roomNumber: string;         // 房间
+  multipartFile?: File;        // 图片改为可选(新增 ? 符号)
+}
+
+interface Room {
+  id?: string;                  // 主键
+  houseTypeId: string;         // 户型主键
+  roomNumber: string;         // 房间
+  roomPictureUrl?: string;     // 房间图片url
+  attribute?: string           // 按钮属性
+  createTime?: Date;           // 创建时间
+  createBy?: string;           // 创建人
+  updateTime?: Date;           // 修改时间
+  updateBy?: string;           // 修改人
+}
+
+interface RoomListResponse extends BaseResponse{
+  data: Room[];
+}
+
+// 响应式数据
+const roomList = ref<Room[]>([])
+const loading = ref(false)
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增房间')
+const currentEditId = ref('')
+const selectedIds = ref<string[]>([])
+const previewVisible = ref(false)
+const previewImageUrl = ref('')
+const localPreviewUrl = ref('') // 本地预览URL
+const currentRoom = ref<Room | null>(null) // 当前编辑的房间数据
+const MINIO_BASE_URL = import.meta.env.VITE_MINIO_BASE_URL
+const houseTypeId = ref('');
+
+const route = useRoute()
+houseTypeId.value = route.query.houseTypeId as string;
+
+const router = useRouter();
+
+
+// 表单数据(multipartFile 改为可选)
+const formData = reactive<AddAndUpdateRoomType>({
+  roomNumber: '',
+  multipartFile: null as any
+})
+
+// 表单规则(移除图片必填验证)
+const formRules = {
+  roomNumber: [
+    { required: true, message: '请输入房间', trigger: 'blur' }
+    // 移除 multipartFile 的必填规则
+  ]
+}
+
+const formRef = ref()
+const uploadRef = ref()
+
+//新增
+const addRoom = async (room: AddAndUpdateRoomType) => {
+  const formData = new FormData ()
+  formData.append('houseTypeId', houseTypeId.value);
+  formData.append('roomNumber', room.roomNumber);//这个必填
+
+  // 新增时图片为必填,需判断是否存在
+  if (room.multipartFile) {
+    formData.append('multipartFile', room.multipartFile);
+  } else {
+    ElMessage.error('新增房间时请上传图片')
+    return
+  }
+
+  const res = await clientPostFormData<FormData, BaseResponse>('/aroom/save', formData);
+  if (res.code !== 200) {
+    ElMessage.error(res.msg)
+    return
+  }
+  ElMessage.success(res.msg)
+  getRoomList()
+}
+
+//批量删除
+const deleteRoom = async (ids: string[]) => {
+  try {
+    await ElMessageBox.confirm('确定要删除选中的房间吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    })
+
+    const res = await clientPost<string, BaseResponse>('/aroom/deleteBatch', JSON.stringify(ids));
+    if (res.code !== 200) {
+      ElMessage.error(res.msg)
+      return
+    }
+    ElMessage.success(res.msg)
+    selectedIds.value = []
+    getRoomList()
+  } catch (error) {
+    // 用户取消删除
+  }
+}
+
+//查询当前户型图的房间
+const getRoomList = async () => {
+  loading.value = true
+  try {
+    const res = await clientGet<string, RoomListResponse>('/aroom/getById/'+houseTypeId.value);
+    if (res.code !== 200) {
+      ElMessage.error(res.msg)
+      return
+    }
+    //这就是当前户型下的房间
+    roomList.value = res.data || []
+  } catch (error) {
+    ElMessage.error('获取房间列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+//修改
+const updateRoom = async (roomId:string,room: AddAndUpdateRoomType) => {
+  const formData = new FormData ()
+  formData.append('id', roomId);
+  formData.append('houseTypeId', houseTypeId.value);
+  formData.append('roomNumber', room.roomNumber);//这个会修改
+
+  // 编辑时图片可选,存在时才上传
+  if (room.multipartFile) {
+    formData.append('multipartFile', room.multipartFile);
+  }
+
+  const res = await clientPostFormData<FormData, BaseResponse>('/aroom/update', formData);
+  if (res.code !== 200) {
+    ElMessage.error(res.msg)
+    return
+  }
+  ElMessage.success(res.msg)
+  getRoomList()
+}
+
+// 打开新增对话框
+const openAddDialog = () => {
+  dialogTitle.value = '新增房间'
+  currentEditId.value = ''
+  currentRoom.value = null
+  resetForm()
+  dialogVisible.value = true
+}
+
+// 打开编辑对话框
+const openEditDialog = (item: Room) => {
+  dialogTitle.value = '编辑房间'
+  currentEditId.value = item.id || ''
+  currentRoom.value = item
+
+  formData.roomNumber = item.roomNumber || ''
+  formData.multipartFile = null as any
+
+  dialogVisible.value = true
+}
+
+// 重置表单
+const resetForm = () => {
+  formData.roomNumber = ''
+  formData.multipartFile = null as any
+  localPreviewUrl.value = ''
+  formRef.value?.clearValidate()
+  uploadRef.value?.clearFiles()
+}
+
+// 提交表单(调整验证逻辑)
+const submitForm = async () => {
+  try {
+    await formRef.value.validate()
+
+    // 新增时必须上传图片,编辑时可选
+    if (!currentEditId.value && !formData.multipartFile) {
+      ElMessage.warning('新增房间时请上传房间图片')
+      return
+    }
+
+    if (currentEditId.value) {
+      await updateRoom(currentEditId.value, formData)
+    } else {
+      await addRoom(formData)
+    }
+
+    dialogVisible.value = false
+    resetForm()
+  } catch (error) {
+    console.error('表单验证失败', error)
+  }
+}
+
+// 文件上传处理(保持原有逻辑)
+const handleFileChange = (file: any) => {
+  // 检查文件大小(10MB = 10 * 1024 * 1024 bytes)
+  const maxSize = 10 * 1024 * 1024
+  if (file.raw && file.raw.size > maxSize) {
+    ElMessage.error('上传文件大小不能超过 10MB!')
+    return false
+  }
+
+  formData.multipartFile = file.raw
+
+  // 创建本地预览URL
+  if (file.raw) {
+    // 清理之前的URL
+    if (localPreviewUrl.value) {
+      URL.revokeObjectURL(localPreviewUrl.value)
+    }
+    localPreviewUrl.value = URL.createObjectURL(file.raw)
+  }
+
+  return false // 阻止自动上传
+}
+
+// 删除单个项目
+const deleteItem = (item: Room) => {
+  if (item.id) {
+    deleteRoom([item.id])
+  }
+}
+
+// 批量删除选中项
+const deleteSelected = () => {
+  if (selectedIds.value.length === 0) {
+    ElMessage.warning('请选择要删除的项目')
+    return
+  }
+  deleteRoom(selectedIds.value)
+}
+
+// 选择项目
+const toggleSelection = (id: string) => {
+  const index = selectedIds.value.indexOf(id)
+  if (index > -1) {
+    selectedIds.value.splice(index, 1)
+  } else {
+    selectedIds.value.push(id)
+  }
+}
+
+// 全选/取消全选
+const toggleSelectAll = () => {
+  if (selectedIds.value.length === roomList.value.length) {
+    selectedIds.value = []
+  } else {
+    selectedIds.value = roomList.value.map(item => item.id || '').filter(id => id)
+  }
+}
+
+// 查看房间图片
+const viewRoomImage = (item: Room) => {
+  if (item.roomPictureUrl) {
+    previewImageUrl.value = MINIO_BASE_URL + item.roomPictureUrl
+    previewVisible.value = true
+  } else {
+    ElMessage.warning('该房间暂无图片')
+  }
+}
+
+// 进入房间编辑
+// const enterRoomEdit = (item: Room) => {
+//   console.log('进入房间编辑:', item)
+//   ElMessage.info(`进入房间编辑: ${item.roomNumber}`)
+// }
+
+// 返回户型列表
+const goBack = () => {
+  router.push('/ar/edit');
+}
+
+// 清理URL对象
+const cleanupPreviewUrl = () => {
+  if (localPreviewUrl.value) {
+    URL.revokeObjectURL(localPreviewUrl.value)
+    localPreviewUrl.value = ''
+  }
+}
+
+// 关闭对话框时清理资源
+const handleDialogClose = () => {
+  cleanupPreviewUrl()
+  resetForm()
+}
+
+const init = () => {
+  getRoomList()
+}
+
+onMounted(()=>{
+  init()
+})
+</script>
+
+<template>
+  <div class="room-management-page h-screen bg-gray-50 p-6 flex flex-col overflow-hidden">
+    <!-- 页面头部 -->
+    <div class="mb-6">
+      <!-- 面包屑导航 -->
+      <div class="flex items-center mb-4">
+        <el-button
+          type="text"
+          :icon="ArrowLeft"
+          @click="goBack"
+          class="mr-3 text-gray-600 hover:text-blue-600"
+        >
+          返回户型列表
+        </el-button>
+        <div class="flex items-center gap-2 text-sm text-gray-500">
+          <span>全景图管理</span>
+          <span>/</span>
+          <span class="text-gray-800 font-medium">房间管理</span>
+        </div>
+      </div>
+
+      <div class="flex items-center justify-between mb-4">
+        <div class="flex items-center gap-3">
+          <el-icon class="text-2xl text-blue-600"><HomeFilled /></el-icon>
+          <h1 class="text-2xl font-bold text-gray-800">房间管理</h1>
+          <span class="text-sm text-gray-500 bg-gray-100 px-2 py-1 rounded">
+            户型ID: {{ houseTypeId }}
+          </span>
+        </div>
+        <div class="flex items-center gap-3">
+          <el-button type="primary" :icon="Plus" @click="openAddDialog">
+            新增房间
+          </el-button>
+        </div>
+      </div>
+
+      <!-- 操作栏 -->
+      <div class="flex items-center justify-between">
+        <div class="flex items-center gap-3">
+          <el-checkbox
+            :indeterminate="selectedIds.length > 0 && selectedIds.length < roomList.length"
+            :model-value="selectedIds.length === roomList.length && roomList.length > 0"
+            @change="toggleSelectAll"
+          >
+            全选
+          </el-checkbox>
+          <span class="text-sm text-gray-600">
+            已选择 {{ selectedIds.length }} 项
+          </span>
+          <el-button
+            v-if="selectedIds.length > 0"
+            type="danger"
+            size="small"
+            :icon="Delete"
+            @click="deleteSelected"
+          >
+            批量删除
+          </el-button>
+        </div>
+        <div class="text-sm text-gray-600">
+          共 {{ roomList.length }} 个房间
+        </div>
+      </div>
+    </div>
+
+    <!-- 房间列表 -->
+    <div v-loading="loading" class="flex-1 overflow-y-auto mb-6">
+      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
+        <div
+          v-for="item in roomList"
+          :key="item.id"
+          class="bg-white rounded-lg shadow-md hover:shadow-lg transition-all duration-300 overflow-hidden cursor-pointer group relative"
+          :class="{ 'ring-2 ring-blue-500': selectedIds.includes(item.id || '') }"
+        >
+          <!-- 房间图片 -->
+          <div class="relative h-48 bg-gray-200 overflow-hidden">
+            <!-- 图片区域的遮罩和操作按钮 -->
+            <div class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center gap-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-20">
+              <el-button
+                type="primary"
+                round
+                :icon="View"
+                @click.stop="viewRoomImage(item)"
+              >
+                查看全景图
+              </el-button>
+<!--              <el-button-->
+<!--                type="success"-->
+<!--                round-->
+<!--                :icon="Setting"-->
+<!--                @click.stop="enterRoomEdit(item)"-->
+<!--              >-->
+<!--                编辑房间-->
+<!--              </el-button>-->
+            </div>
+
+            <img
+              v-if="item.roomPictureUrl"
+              :src="MINIO_BASE_URL + item.roomPictureUrl"
+              :alt="item.roomNumber"
+              class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
+            />
+            <div v-else class="w-full h-full flex items-center justify-center text-gray-400">
+              <el-icon class="text-4xl"><HomeFilled /></el-icon>
+            </div>
+          </div>
+
+          <!-- 房间信息 -->
+          <div class="p-4">
+            <!-- 选择框 -->
+            <div class="flex items-center justify-between mb-3">
+              <el-checkbox
+                :model-value="selectedIds.includes(item.id || '')"
+                @change="toggleSelection(item.id || '')"
+                @click.stop
+              >
+                <span class="text-sm text-gray-600">选择</span>
+              </el-checkbox>
+            </div>
+
+            <div class="flex items-center justify-between mb-2">
+              <h3 class="text-lg font-semibold text-gray-800 truncate">
+                {{ item.roomNumber || '未命名房间' }}
+              </h3>
+              <!--              <span v-if="item.attribute" class="text-xs bg-green-100 text-green-600 px-2 py-1 rounded">-->
+              <!--                {{ item.attribute }}-->
+              <!--              </span>-->
+            </div>
+
+            <div class="flex items-center justify-between text-xs text-gray-500 mb-3">
+              <span>创建时间</span>
+              <span>{{ item.createTime ? new Date(item.createTime).toLocaleDateString() : '-' }}</span>
+            </div>
+
+            <!-- 操作按钮 -->
+            <div class="flex justify-end gap-2">
+              <el-button
+                type="primary"
+                size="small"
+                round
+                :icon="Edit"
+                @click.stop="openEditDialog(item)"
+              >
+                编辑
+              </el-button>
+              <el-button
+                type="danger"
+                size="small"
+                round
+                :icon="Delete"
+                @click.stop="deleteItem(item)"
+              >
+                删除
+              </el-button>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 空状态 -->
+      <div v-if="!loading && roomList.length === 0" class="text-center py-12">
+        <el-icon class="text-6xl text-gray-300 mb-4"><HomeFilled /></el-icon>
+        <p class="text-gray-500 text-lg mb-4">暂无房间数据</p>
+        <el-button type="primary" :icon="Plus" @click="openAddDialog">
+          添加第一个房间
+        </el-button>
+      </div>
+    </div>
+
+    <!-- 新增/编辑对话框 -->
+    <el-dialog
+      v-model="dialogVisible"
+      :title="dialogTitle"
+      width="600px"
+      :close-on-click-modal="false"
+      @close="handleDialogClose"
+    >
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="80px"
+        label-position="left"
+      >
+        <el-form-item label="房间" prop="roomNumber">
+          <el-input
+            v-model="formData.roomNumber"
+            placeholder="请输入房间"
+            clearable
+          />
+        </el-form-item>
+
+        <el-form-item label="房间图片"> <!-- 移除 required 属性 -->
+          <div class="w-full">
+            <!-- 当前图片预览(编辑模式下显示原图) -->
+            <div v-if="currentEditId && currentRoom?.roomPictureUrl && !localPreviewUrl" class="mb-4">
+              <div class="text-sm text-gray-600 mb-2">当前房间图片:</div>
+              <div class="w-32 h-32 border border-gray-300 rounded-lg overflow-hidden">
+                <img
+                  :src="MINIO_BASE_URL + currentRoom.roomPictureUrl"
+                  :alt="currentRoom.roomNumber"
+                  class="w-full h-full object-cover"
+                />
+              </div>
+            </div>
+
+            <!-- 本地预览图片 -->
+            <div v-if="localPreviewUrl" class="mb-4">
+              <div class="text-sm text-gray-600 mb-2">{{ currentEditId ? '新上传的图片:' : '预览图片:' }}</div>
+              <div class="w-32 h-32 border border-gray-300 rounded-lg overflow-hidden">
+                <img
+                  :src="localPreviewUrl"
+                  alt="预览图片"
+                  class="w-full h-full object-cover"
+                />
+              </div>
+            </div>
+
+            <!-- 上传组件(提示编辑时可选) -->
+            <el-upload
+              ref="uploadRef"
+              :auto-upload="false"
+              :show-file-list="true"
+              :limit="1"
+              accept="image/*"
+              :on-change="handleFileChange"
+              drag
+            >
+              <el-icon class="text-4xl text-gray-400 mb-2"><Plus /></el-icon>
+              <div class="text-gray-600">
+                {{ currentEditId ? '可选:点击或拖拽替换房间图片' : '点击或拖拽上传房间图片' }}
+              </div>
+              <template #tip>
+                <div class="text-xs text-gray-500 mt-2">
+                  支持 jpg、png 格式,文件大小不能超过 10MB(编辑时可不上传)
+                </div>
+              </template>
+            </el-upload>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="submitForm">确定</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 图片预览对话框 -->
+    <el-dialog
+      v-model="previewVisible"
+      title="房间图片预览"
+      width="80%"
+      :close-on-click-modal="true"
+      append-to-body
+    >
+      <div class="flex justify-center">
+        <img
+          :src="previewImageUrl"
+          alt="房间图片预览"
+          class="max-w-full max-h-96 object-contain"
+        />
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.room-management-page {
+  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
+}
+</style>