Просмотр исходного кода

feat: 完善模型管理功能

- 修复日期选择器时区问题,避免日期偏移
- 实现3D模型审核功能,支持审核通过操作
- 将是否热门和显示状态改为开关组件,提升用户体验
- 优化审核按钮显示逻辑,已审核通过的模型不显示审核按钮
- 新增OSS文件上传功能支持
- 完善各模块的CRUD操作和UI交互
wangfumin 6 месяцев назад
Родитель
Сommit
586c6dbbc9

+ 3 - 2
.env.production

@@ -1,5 +1,5 @@
 # 是否使用Hash路由
-VITE_USE_HASH = 'false'
+VITE_USE_HASH = 'true'
 
 # 资源公共路径,需要以 /开头和结尾
 VITE_PUBLIC_PATH = '/'
@@ -7,7 +7,8 @@ VITE_PUBLIC_PATH = '/'
 VITE_AXIOS_BASE_URL = '/api'  # 用于代理
 
 # 代理配置-target
-VITE_PROXY_TARGET = 'http://localhost:8085'
+# VITE_PROXY_TARGET = 'http://localhost:8085'
+VITE_PROXY_TARGET = 'http://106.53.107.102:8085'
 
 # 腾讯云COS基础URL
 VITE_COS_BASE_URL = 'https://swkz-1332577016.cos.ap-guangzhou.myqcloud.com'

Разница между файлами не показана из-за своего большого размера
+ 521 - 7
pnpm-lock.yaml


BIN
src/assets/images/avatar.jpg


+ 1 - 1
src/router/index.js

@@ -9,7 +9,7 @@
 import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
 import { basicRoutes } from './basic-routes'
 import { setupRouterGuards } from './guards'
-
+console.log(import.meta.env, 77777)
 export const router = createRouter({
   history:
     import.meta.env.VITE_USE_HASH === 'true'

+ 1 - 0
src/utils/index.js

@@ -11,4 +11,5 @@ export * from './common'
 export * from './http'
 export * from './is'
 export * from './naiveTools'
+export * from './ossUpload'
 export * from './storage'

+ 189 - 0
src/utils/ossUpload.js

@@ -0,0 +1,189 @@
+/**
+ * OSS上传工具
+ * 提供统一的OSS上传功能
+ */
+
+import { ref, readonly } from 'vue'
+import { request } from '@/utils'
+
+/**
+ * 获取OSS配置信息
+ * @param {number} fileNum - 配置类型,默认为1
+ * @returns {Promise<Object>} OSS配置信息
+ */
+export async function getOssConfig(fileNum = 1) {
+  try {
+    const response = await request.get('/file/oss/info', { params: { fileNum } })
+    return response.data?.[0] || null
+  } catch (error) {
+    console.error('获取OSS配置失败:', error)
+    window.$message?.error('获取OSS配置失败')
+    throw new Error('获取OSS配置失败')
+  }
+}
+
+/**
+ * 上传文件到OSS
+ * @param {string} actionUrl - OSS上传地址
+ * @param {File} file - 要上传的文件
+ * @returns {Promise<Response>} 上传响应
+ */
+export async function uploadFileToOss(actionUrl, file) {
+  const formData = new FormData()
+  formData.append('file', file)
+  
+  try {
+    const response = await fetch(actionUrl, {
+      method: 'PUT',
+      body: file,
+    })
+    return response
+  } catch (error) {
+    console.error('文件上传失败:', error)
+    throw new Error('文件上传失败')
+  }
+}
+
+/**
+ * OSS上传Hook
+ * 提供完整的OSS上传功能,包括配置获取、文件上传、状态管理
+ * @returns {Object} 上传相关的方法和状态
+ */
+export function useOssUpload() {
+  const ossConfig = ref(null)
+  const uploading = ref(false)
+  
+  /**
+   * 初始化OSS配置
+   */
+  const initOssConfig = async () => {
+    try {
+      ossConfig.value = await getOssConfig()
+    } catch (error) {
+      $message.error('获取OSS配置失败')
+      throw error
+    }
+    return ossConfig.value
+  }
+  
+  /**
+   * 上传文件
+   * @param {File} file - 要上传的文件
+   * @param {Object} options - 上传选项
+   * @param {Function} options.onSuccess - 成功回调
+   * @param {Function} options.onError - 失败回调
+   * @param {Function} options.onProgress - 进度回调(可选)
+   * @returns {Promise<Object>} 上传结果
+   */
+  const uploadFile = async (file, options = {}) => {
+    const { onSuccess, onError, onProgress } = options
+    
+    if (uploading.value) {
+      window.$message?.warning('正在上传中,请稍候')
+      return
+    }
+    
+    try {
+      uploading.value = true
+      
+      // 确保OSS配置已加载
+      const config = await initOssConfig()
+      if (!config) {
+        throw new Error('OSS配置未获取')
+      }
+      
+      // 上传文件
+      const response = await uploadFileToOss(config.actionUrl, file)
+      
+      if (response.ok) {
+        // 构建文件URL信息
+        const result = {
+          remoteUrl: config.dir + config.fileName,
+          localUrl: URL.createObjectURL(file),
+          fileName: config.fileName,
+          dir: config.dir,
+          fullUrl: config.dir + config.fileName
+        }
+        
+        onSuccess?.(result)
+        window.$message?.success('上传成功')
+        return result
+      } else {
+        throw new Error('上传失败')
+      }
+    } catch (error) {
+      console.error('上传失败:', error)
+      onError?.(error)
+      window.$message?.error('上传失败,请重试')
+      throw error
+    } finally {
+      uploading.value = false
+    }
+  }
+  
+  /**
+   * 验证文件类型
+   * @param {File} file - 要验证的文件
+   * @param {string|Array} acceptTypes - 允许的文件类型
+   * @returns {boolean} 是否通过验证
+   */
+  const validateFile = (file, acceptTypes = 'image/*') => {
+    if (typeof acceptTypes === 'string') {
+      if (acceptTypes === 'image/*') {
+        return file.type.startsWith('image/')
+      }
+      return file.type === acceptTypes
+    }
+    
+    if (Array.isArray(acceptTypes)) {
+      return acceptTypes.some(type => {
+        if (type.endsWith('/*')) {
+          return file.type.startsWith(type.slice(0, -1))
+        }
+        return file.type === type
+      })
+    }
+    
+    return true
+  }
+  
+  /**
+   * 处理NUpload组件的上传
+   * @param {Object} uploadOptions - NUpload的上传参数
+   * @param {string|Array} acceptTypes - 允许的文件类型
+   * @returns {Promise<void>}
+   */
+  const handleNUpload = async ({ file, onFinish, onError }, acceptTypes = 'image/*') => {
+    // 文件类型验证
+    if (!validateFile(file.file, acceptTypes)) {
+      const errorMsg = Array.isArray(acceptTypes) 
+        ? `只能上传 ${acceptTypes.join(', ')} 类型的文件`
+        : acceptTypes === 'image/*' 
+          ? '只能上传图片文件'
+          : `只能上传 ${acceptTypes} 类型的文件`
+      window.$message?.error(errorMsg)
+      onError?.()
+      return
+    }
+    
+    try {
+      const result = await uploadFile(file.file, {
+        onSuccess: () => onFinish?.(),
+        onError: () => onError?.()
+      })
+      return result
+    } catch (error) {
+      onError?.()
+      throw error
+    }
+  }
+  
+  return {
+    ossConfig: readonly(ossConfig),
+    uploading: readonly(uploading),
+    initOssConfig,
+    uploadFile,
+    validateFile,
+    handleNUpload
+  }
+}

+ 0 - 11
src/views/kzhanManage/ArtistMgt/index.vue

@@ -63,17 +63,6 @@
         <n-form-item v-if="imgList && imgList.length">
           <div class="relative pl-80">
             <NImage width="100" :src="imgList[0].url" />
-            <!-- <NButton
-              class="absolute right-0 top-0 translate-x-1/2 transform -translate-y-1/2"
-              size="tiny"
-              type="error"
-              circle
-              @click="handleClearImage"
-            >
-              <template #icon>
-                <i class="i-material-symbols:close text-12" />
-              </template>
-            </NButton> -->
           </div>
         </n-form-item>
         <n-form-item

+ 2 - 0
src/views/kzhanManage/GalleryMgt/api.js

@@ -3,6 +3,8 @@ import { request } from '@/utils'
 export default {
   create: data => request.post('/pavilion/add', data),
   read: (params = {}) => request.get('/pavilion/page', { params }),
+  getById: id => request.get(`/pavilion/${id}`),
+  getExhibitionList: (params = {}) => request.get('/exhibition/page', { params }),
   update: data => request.patch(`/pavilion/${data.id}`, data),
   delete: id => request.delete(`/pavilion/${id}`),
   getOssInfo: (fileNum = 1) => request.get('/file/oss/info', { params: { fileNum } }),

+ 436 - 102
src/views/kzhanManage/GalleryMgt/components/addAndEditGallery.vue

@@ -14,110 +14,125 @@
       :model="formData"
       :rules="rules"
     >
-      <div class="grid grid-cols-2 gap-4">
-        <n-form-item
-          label="展馆名称"
-          path="name"
-        >
-          <n-input v-model:value="formData.name" placeholder="请输入展馆名称" />
-        </n-form-item>
-        <n-form-item
-          label="英文名称"
-          path="enName"
-        >
-          <n-input v-model:value="formData.enName" placeholder="请输入英文名称" />
-        </n-form-item>
-      </div>
+      <n-form-item
+        label="展馆名称"
+        path="name"
+      >
+        <n-input v-model:value="formData.name" placeholder="请输入展馆名称" />
+      </n-form-item>
+      
+      <n-form-item
+        label="英文名称"
+        path="enName"
+      >
+        <n-input v-model:value="formData.enName" placeholder="请输入英文名称" />
+      </n-form-item>
 
-      <div class="grid grid-cols-2 gap-4">
-        <n-form-item label="展馆封面图" path="imageUrl">
-          <NUpload
-            class="w-full text-center"
-            :custom-request="(options) => handleUpload(options, 'cover')"
-            :show-file-list="false"
-            accept=".png,.jpg,.jpeg"
-            @before-upload="onBeforeUpload"
-          >
-            <NUploadDragger>
-              <div class="h-100 f-c-c flex-col">
-                <i class="i-mdi:upload mb-12 text-68 color-primary" />
-                <NText class="text-14 color-gray">
-                  点击或拖拽上传封面图
-                </NText>
-              </div>
-            </NUploadDragger>
-          </NUpload>
-          <div v-if="imgList.cover && imgList.cover.length" class="mt-2">
-            <NImage width="100" :src="imgList.cover[0].url" />
-          </div>
-        </n-form-item>
+      <n-form-item label="展馆封面图" path="imageUrl">
+        <NUpload
+          class="w-200px text-center"
+          :custom-request="(options) => handleUpload(options, 'cover')"
+          :show-file-list="false"
+          accept=".png,.jpg,.jpeg"
+          @before-upload="onBeforeUpload"
+        >
+          <NUploadDragger>
+            <div class="h-60 f-c-c flex-col">
+              <i class="i-mdi:upload mb-4 text-32 color-primary" />
+              <NText class="text-12 color-gray">
+                点击或拖拽上传封面图
+              </NText>
+            </div>
+          </NUploadDragger>
+        </NUpload>
+        <div v-if="imgList.cover && imgList.cover.length" class="ml-20">
+          <NImage width="100" :src="imgList.cover[0].url" />
+        </div>
+      </n-form-item>
 
-        <n-form-item label="展馆Logo" path="logoUrl">
-          <NUpload
-            class="w-full text-center"
-            :custom-request="(options) => handleUpload(options, 'logo')"
-            :show-file-list="false"
-            accept=".png,.jpg,.jpeg"
-            @before-upload="onBeforeUpload"
-          >
-            <NUploadDragger>
-              <div class="h-100 f-c-c flex-col">
-                <i class="i-mdi:upload mb-12 text-68 color-primary" />
-                <NText class="text-14 color-gray">
-                  点击或拖拽上传Logo
-                </NText>
-              </div>
-            </NUploadDragger>
-          </NUpload>
-          <div v-if="imgList.logo && imgList.logo.length" class="mt-2">
-            <NImage width="100" :src="imgList.logo[0].url" />
-          </div>
-        </n-form-item>
-      </div>
+      <n-form-item label="展馆Logo" path="logoUrl">
+        <NUpload
+          class="w-200px text-center"
+          :custom-request="(options) => handleUpload(options, 'logo')"
+          :show-file-list="false"
+          accept=".png,.jpg,.jpeg"
+          @before-upload="onBeforeUpload"
+        >
+          <NUploadDragger>
+            <div class="h-60 f-c-c flex-col">
+              <i class="i-mdi:upload mb-4 text-32 color-primary" />
+              <NText class="text-12 color-gray">
+                点击或拖拽上传Logo
+              </NText>
+            </div>
+          </NUploadDragger>
+        </NUpload>
+        <div v-if="imgList.logo && imgList.logo.length" class="ml-20">
+          <NImage width="100" :src="imgList.logo[0].url" />
+        </div>
+      </n-form-item>
 
       <div class="grid grid-cols-2 gap-4">
         <n-form-item
-          label="开时间"
-          path="openTime"
+          label="开时间"
+          path="startTime"
         >
           <NTimePicker
-            v-model:value="formData.openTime"
+            v-model:value="formData.startTime"
+            format="HH:mm:ss"
+            placeholder="请选择开始时间"
+            clearable
+          />
+          <span style="margin: 0 10px;"> - </span>
+          <NTimePicker
+            v-model:value="formData.endTime"
             format="HH:mm:ss"
-            placeholder="请选择开放时间"
+            placeholder="请选择结束时间"
             clearable
           />
         </n-form-item>
-
-        <n-form-item
-          label="相关展会"
-          path="exhibitionList"
-        >
+      </div>
+      
+      <n-form-item
+        label="相关展会"
+        path="exhibitionList"
+      >
+        <div style="width:100%;">
           <NSelect
             v-model:value="formData.exhibitionList"
             multiple
-            placeholder="请选择相关展会"
+            filterable
+            placeholder="搜索展会"
             :options="exhibitionOptions"
+            :loading="exhibitionLoading"
             clearable
+            remote
+            :clear-filter-after-select="false"
+            @search="handleExhibitionSearch"
           />
-        </n-form-item>
-      </div>
+        </div>
+      </n-form-item>
 
       <n-form-item
         label="地址"
         path="address"
       >
         <div class="w-full">
-          <div class="mb-2 flex gap-2">
-            <n-input
+          <div class="mb-2">
+            <NSelect
               v-model:value="formData.address"
-              placeholder="请输入地址"
-              class="flex-1"
+              filterable
+              placeholder="请输入地址进行搜索"
+              :options="addressOptions"
+              :loading="addressLoading"
+              :remote="true"
+              :clear-filter-after-select="false"
+              @search="handleAddressSearch"
+              @update:value="handleAddressSelect"
+              class="w-full"
             />
-            <NButton type="primary" @click="searchLocation">
-              搜索
-            </NButton>
           </div>
-          <div id="mapContainer" class="h-300px w-full border border-gray-300 rounded" />
+          <div id="mapContainer" class="h-300px mt-10 w-full border border-gray-300 rounded" />
         </div>
       </n-form-item>
 
@@ -158,8 +173,11 @@
 </template>
 
 <script setup>
+import { onMounted, watch } from 'vue'
 import { WangEditor } from '@/components'
-import { NButton, NImage, NSelect, NText, NTimePicker, NUpload, NUploadDragger } from 'naive-ui'
+import { NButton, NImage, NSelect, NTag, NText, NTimePicker, NUpload, NUploadDragger } from 'naive-ui'
+import { request } from '@/utils'
+import api from '../api'
 
 defineOptions({ name: 'AddAndEditGallery' })
 
@@ -180,6 +198,10 @@ const props = defineProps({
     type: Array,
     default: () => [],
   },
+  editExhibitionList: {
+    type: Array,
+    default: () => [],
+  },
 })
 
 const emit = defineEmits(['cancel', 'confirm', 'upload', 'search-location'])
@@ -187,6 +209,16 @@ const emit = defineEmits(['cancel', 'confirm', 'upload', 'search-location'])
 const formRef = ref(null)
 const editorRef = ref(null)
 
+// 地址搜索相关状态
+const addressOptions = ref([])
+const addressLoading = ref(false)
+let searchTimeout = null
+
+// 展会搜索相关状态
+const exhibitionLoading = ref(false)
+let exhibitionSearchTimeout = null
+const selectedExhibitions = ref([])
+const editExhibitionHuixian = ref([])
 // 工具栏配置 - 移除表格和待办功能
 const toolbarConfig = {
   excludeKeys: [
@@ -202,16 +234,33 @@ const toolbarConfig = {
   ]
 }
 
-// 编辑器配置 - 配置图片上传
+// 编辑器配置 - 配置图片和视频上传
 const editorConfig = {
   MENU_CONF: {
     uploadImage: {
       // 自定义上传
       async customUpload(file, insertFn) {
-        // 这里可以调用父组件的上传方法
-        // 暂时先返回一个示例URL
-        const url = URL.createObjectURL(file)
-        insertFn(url, file.name, url)
+        try {
+          const formData = new FormData()
+          formData.append('file', file)
+          
+          const response = await request.post('/file/upload', formData, {
+            headers: {
+              'Content-Type': 'multipart/form-data'
+            }
+          })
+          console.log(response)
+          if (response.data) {
+            // 插入图片到编辑器
+            insertFn(response.data, file.name, response.data)
+            $message.success('图片上传成功')
+          } else {
+            throw new Error('上传响应格式错误')
+          }
+        } catch (error) {
+          console.error('图片上传失败', error)
+          $message.error('图片上传失败:' + (error.message || '未知错误'))
+        }
       },
       // 上传错误的回调函数
       onError(file, err, res) {
@@ -226,10 +275,53 @@ const editorConfig = {
       onProgress(progress) {
         console.log('上传进度', progress)
       },
-      // 限制文件大小,默认为 2M
+      // 限制文件大小,默认为 5M
       maxFileSize: 5 * 1024 * 1024, // 5M
       // 限制文件类型
       allowedFileTypes: ['image/*'],
+    },
+    uploadVideo: {
+      // 自定义视频上传
+      async customUpload(file, insertFn) {
+        try {
+          const formData = new FormData()
+          formData.append('file', file)
+          
+          const response = await request.post('/file/upload', formData, {
+            headers: {
+              'Content-Type': 'multipart/form-data'
+            }
+          })
+          console.log(response)
+          if (response.data) {
+            // 插入视频到编辑器
+            insertFn(response.data)
+            $message.success('视频上传成功')
+          } else {
+            throw new Error('上传响应格式错误')
+          }
+        } catch (error) {
+          console.error('视频上传失败', error)
+          $message.error('视频上传失败:' + (error.message || '未知错误'))
+        }
+      },
+      // 上传错误的回调函数
+      onError(file, err, res) {
+        console.error('视频上传失败', err, res)
+        $message.error('视频上传失败')
+      },
+      // 上传成功的回调函数
+      onSuccess(file, res) {
+        console.log('视频上传成功', res)
+      },
+      // 上传进度的回调函数
+      onProgress(progress) {
+        console.log('视频上传进度', progress)
+      },
+      // 限制文件大小,默认为 50M
+      maxFileSize: 50 * 1024 * 1024, // 50M
+      // 限制文件类型
+      allowedFileTypes: ['video/*'],
     }
   }
 }
@@ -241,11 +333,11 @@ const rules = {
     message: '请输入展馆名称',
     trigger: ['input', 'blur'],
   },
-  address: {
-    required: true,
-    message: '请输入地址',
-    trigger: ['input', 'blur'],
-  },
+  // address: {
+  //   required: true,
+  //   message: '请输入地址',
+  //   trigger: ['input', 'blur'],
+  // },
 }
 
 // 上传前验证
@@ -262,10 +354,126 @@ function handleUpload(options, type) {
   emit('upload', options, type)
 }
 
-// 搜索地址
-function searchLocation() {
-  emit('search-location')
+// 处理地址搜索
+function handleAddressSearch(query) {
+  if (!query || query.length < 2) {
+    addressOptions.value = []
+    return
+  }
+
+  // 防抖处理
+  if (searchTimeout) {
+    clearTimeout(searchTimeout)
+  }
+
+  searchTimeout = setTimeout(() => {
+    searchAddressSuggestions(query)
+  }, 300)
+}
+
+// 调用腾讯位置服务suggestion API
+function searchAddressSuggestions(keyword) {
+  addressLoading.value = true
+  
+  try {
+    // 使用JSONP方式调用腾讯地图WebService API进行地址建议搜索
+    const callbackName = `jsonp_callback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
+    const url = `https://apis.map.qq.com/ws/place/v1/suggestion?keyword=${encodeURIComponent(keyword)}&region=北京&key=YCABZ-AFPRX-VD54O-TL3VN-TL7A3-KPBQJ&output=jsonp&callback=${callbackName}&page_size=10`
+
+    // 创建script标签进行JSONP请求
+    const script = document.createElement('script')
+    script.src = url
+
+    // 定义回调函数
+    window[callbackName] = function (data) {
+      try {
+        if (data.status === 0 && data.data && data.data.length > 0) {
+          // 转换为NSelect需要的格式
+          addressOptions.value = data.data.map(item => ({
+            label: item.title + (item.address ? ` - ${item.address}` : ''),
+            value: item.title + (item.address ? ` - ${item.address}` : ''),
+            location: item.location,
+            id: item.id,
+            category: item.category
+          }))
+        } else {
+          addressOptions.value = []
+        }
+      } catch (error) {
+        console.error('地址搜索处理失败:', error)
+        addressOptions.value = []
+      } finally {
+        addressLoading.value = false
+        // 清理:移除script标签和回调函数
+        document.head.removeChild(script)
+        delete window[callbackName]
+      }
+    }
+
+    // 处理加载错误
+    script.onerror = function () {
+      console.error('地址搜索JSONP请求失败')
+      addressOptions.value = []
+      addressLoading.value = false
+      // 清理
+      document.head.removeChild(script)
+      delete window[callbackName]
+    }
+
+    // 添加script标签到head中开始请求
+    document.head.appendChild(script)
+  } catch (error) {
+    console.error('地址搜索失败:', error)
+    addressLoading.value = false
+    addressOptions.value = []
+  }
+}
+
+// 处理地址选择
+function handleAddressSelect(value) {
+  if (value) {
+    // 找到选中的地址选项
+    const selectedOption = addressOptions.value.find(option => option.value === value)
+    if (selectedOption && selectedOption.location) {
+      // 触发地图更新
+      emit('search-location', {
+        address: value,
+        location: selectedOption.location
+      })
+    }
+  }
 }
+// 监听编辑的回显列表
+watch(() => props.editExhibitionList, (newValue) => {
+  if (newValue && Array.isArray(newValue) && newValue.length > 0) {
+    // 根据ID从exhibitionOptions中找到对应的展会信息
+    editExhibitionHuixian.value = newValue
+    
+  }
+}, { immediate: true })
+// 监听地址变化,在编辑模式下自动搜索和定位
+watch(() => props.formData.address, (newAddress) => {
+  if (newAddress && props.isEdit) {
+    // 延迟执行,确保地图已经初始化
+    setTimeout(() => {
+      // 直接触发地址搜索和定位
+      emit('search-location', {
+        address: newAddress
+      })
+    }, 1000)
+  }
+}, { immediate: true })
+
+// 监听exhibitionList变化,实现编辑时的回显
+watch(() => props.formData.exhibitionList, (newExhibitionList) => {
+  if (newExhibitionList && Array.isArray(newExhibitionList) && newExhibitionList.length > 0) {
+    // 根据ID从exhibitionOptions中找到对应的展会信息
+    const exhibitions = []
+    selectedExhibitions.value = newExhibitionList
+  } else {
+    selectedExhibitions.value = []
+  }
+}, { immediate: true, deep: true })
 
 // 取消操作
 function handleCancel() {
@@ -273,14 +481,141 @@ function handleCancel() {
 }
 
 // 确定操作
-function handleConfirm() {
-  formRef.value?.validate((errors) => {
+async function handleConfirm() {
+  formRef.value?.validate(async (errors) => {
     if (!errors) {
-      emit('confirm')
+      try {
+        // 获取富文本内容
+        const description = getEditorContent()
+        
+        // 构建开放时间字符串
+        let openTime = ''
+        if (props.formData.startTime && props.formData.endTime) {
+          // 将时间戳转换为HH:mm:ss格式
+          const startTimeStr = new Date(props.formData.startTime).toLocaleTimeString('zh-CN', { hour12: false })
+          const endTimeStr = new Date(props.formData.endTime).toLocaleTimeString('zh-CN', { hour12: false })
+          openTime = `${startTimeStr} - ${endTimeStr}`
+        }
+        
+        // 获取图片URL(从imgList中获取)
+        let imageUrl = props.formData.imageUrl || ''
+        let logoUrl = props.formData.logoUrl || ''
+        
+        if (props.imgList.cover && props.imgList.cover.length > 0) {
+          imageUrl = props.imgList.cover[0].remoteUrl
+        }
+        if (props.imgList.logo && props.imgList.logo.length > 0) {
+          logoUrl = props.imgList.logo[0].remoteUrl
+        }
+        
+        // 构建请求参数
+        const requestData = {
+          address: props.formData.address || '',
+          description: description || '',
+          enName: props.formData.enName || '',
+          exhibitionList: props.formData.exhibitionList || [],
+          imageUrl: imageUrl,
+          logoUrl: logoUrl,
+          name: props.formData.name || '',
+          openTime: openTime,
+          remark: props.formData.remark || ''
+        }
+        console.log(requestData, 'requestData')
+        // $message.loading('保存中...')
+        
+        // 根据编辑状态调用不同接口
+        let response
+        if (props.isEdit) {
+          // 编辑模式:添加ID参数并调用更新接口
+          requestData.id = props.formData.id
+          response = await api.update(requestData)
+        } else {
+          // 新增模式:调用新增接口
+          response = await api.create(requestData)
+        }
+        
+        if (response.code === 0) {
+          $message.success(props.isEdit ? '更新成功' : '新增成功')
+          // 添加小延迟确保消息显示后再触发父组件处理
+          setTimeout(() => {
+            emit('confirm')
+          }, 100)
+        } else {
+          $message.error(response.message || (props.isEdit ? '更新失败' : '新增失败'))
+        }
+      } catch (error) {
+        console.error(props.isEdit ? '更新失败:' : '新增失败:', error)
+        $message.error(props.isEdit ? '更新失败,请重试' : '新增失败,请重试')
+      }
     }
   })
 }
 
+// 展会搜索处理
+function handleExhibitionSearch(query) {
+  // 防抖处理
+  if (exhibitionSearchTimeout) {
+    clearTimeout(exhibitionSearchTimeout)
+  }
+
+  exhibitionSearchTimeout = setTimeout(async () => {
+    await searchExhibitions(query)
+  }, 300)
+}
+
+// 搜索展会
+async function searchExhibitions(name = '') {
+  exhibitionLoading.value = true
+  
+  try {
+    const params = {
+      pageNo: 1,
+      pageSize: name ? 100 : 20, // 有搜索条件时加载100条,否则加载20条
+      online: 1
+    }
+    
+    if (name) {
+      params.name = name
+    }
+    
+    const response = await api.getExhibitionList(params)
+    
+    if (response.code === 0 && response.data?.pageData) {
+      // 过滤掉已选中的展会
+      const filteredData = response.data.pageData.filter(item => 
+        !selectedExhibitions.value.some(selected => selected.id === item.id)
+      )
+      props.exhibitionOptions.splice(0, props.exhibitionOptions.length, ...filteredData.map(item => ({
+        label: item.name,
+        value: item.id,
+        ...item
+      })))
+    } else {
+      props.exhibitionOptions.splice(0, props.exhibitionOptions.length)
+    }
+  } catch (error) {
+    console.error('展会搜索失败:', error)
+    props.exhibitionOptions.splice(0, props.exhibitionOptions.length)
+  } finally {
+    exhibitionLoading.value = false
+  }
+}
+
+// 处理展会选择
+function handleExhibitionSelect(value) {
+  console.log(value,'value')
+  if (value) {
+    const selectedOption = props.exhibitionOptions.find(option => option.value === value[value.length - 1])
+    if (selectedOption && !selectedExhibitions.value.some(item => item.id === [value.length - 1])) {
+      console.log(selectedOption,'selectedOption')
+      editExhibitionHuixian.value.push(selectedOption)
+      // 更新formData.exhibitionList
+      props.formData.exhibitionList = editExhibitionHuixian.value.map(item => item.id)
+      searchExhibitions()
+    }
+  }
+}
+
 // 获取编辑器内容
 function getEditorContent() {
   return editorRef.value?.getHtml() || ''
@@ -293,6 +628,11 @@ function setEditorContent(content) {
   }
 }
 
+// 初始化加载展会数据
+onMounted(() => {
+  searchExhibitions() // 初始加载20条数据
+})
+
 // 暴露表单引用和编辑器方法给父组件
 defineExpose({
   formRef,
@@ -302,9 +642,3 @@ defineExpose({
 })
 </script>
 
-<style scoped>
-.add-edit-gallery {
-  max-width: 1200px;
-  margin: 0 auto;
-}
-</style>

+ 222 - 117
src/views/kzhanManage/GalleryMgt/index.vue

@@ -1,7 +1,7 @@
 <template>
   <CommonPage>
     <template #action>
-      <NButton type="primary" @click="handleAdd()">
+      <NButton v-if="!showAddEditForm" type="primary" @click="handleAdd()">
         <i class="i-material-symbols:add mr-4 text-18" />
         新增
       </NButton>
@@ -35,6 +35,7 @@
         :form-data="modalForm"
         :img-list="imgList"
         :exhibition-options="exhibitionOptions"
+        :editExhibitionList="editExhibitionList"
         @cancel="handleCancel"
         @confirm="handleConfirm"
         @upload="handleUpload"
@@ -108,6 +109,7 @@ const {
 // 设置当前操作类型
 const currentAction = ref('')
 const modalAction = computed(() => currentAction.value)
+const editExhibitionList = ref([])
 
 const columns = [
   {
@@ -256,32 +258,100 @@ async function handleOpen(options) {
 
     // 设置表单数据
     if (options.action === 'edit' && options.row) {
-      Object.assign(modalForm.value, options.row)
-
-      // 回显封面图
-      if (options.row.imageUrl) {
-        const fullImageUrl = options.row.imageUrl.startsWith('http')
-          ? options.row.imageUrl
-          : `${import.meta.env.VITE_COS_BASE_URL}/${options.row.imageUrl}`
-        const coverImgObj = {
-          fileName: '封面图片',
-          url: fullImageUrl,
-          remoteUrl: options.row.imageUrl,
+      // 调用详情接口获取完整数据
+      const detailResponse = await api.getById(options.row.id)
+      if (detailResponse.code === 0 && detailResponse.data) {
+        const detailData = detailResponse.data
+        editExhibitionList.value = detailData.exhibitionList
+        
+        // 将详情中的展会添加到exhibitionOptions中(如果不存在的话)
+        if (detailData.exhibitionList && Array.isArray(detailData.exhibitionList)) {
+          detailData.exhibitionList.forEach(exhibition => {
+            // 检查是否已存在于exhibitionOptions中
+            const exists = exhibitionOptions.value.some(option => option.value === exhibition.id)
+            // if (!exists) {
+            //   // 如果不存在,则添加到列表开头
+            //   exhibitionOptions.value.unshift({
+            //     label: exhibition.name,
+            //     value: exhibition.id,
+            //     ...exhibition
+            //   })
+            // }
+          })
+        }
+        
+        Object.assign(modalForm.value, detailData)
+        modalForm.value.exhibitionList = (detailData.exhibitionList || []).map(item => item.id) // 将展会ID数组赋值给exhibitionList
+        // 解析openTime为startTime和endTime
+        if (detailData.openTime && typeof detailData.openTime === 'string') {
+          const timeRange = detailData.openTime.split(' - ')
+          if (timeRange.length === 2) {
+            const today = new Date()
+            const todayStr = today.toISOString().split('T')[0] // 获取今天的日期字符串 YYYY-MM-DD
+            
+            // 将时间字符串转换为今天的完整时间戳
+            const startDateTime = new Date(`${todayStr}T${timeRange[0]}`)
+            const endDateTime = new Date(`${todayStr}T${timeRange[1]}`)
+            
+            modalForm.value.startTime = startDateTime.getTime()
+            modalForm.value.endTime = endDateTime.getTime()
+          }
         }
-        imgList.cover.push(coverImgObj)
-      }
 
-      // 回显Logo
-      if (options.row.logoUrl) {
-        const fullLogoUrl = options.row.logoUrl.startsWith('http')
-          ? options.row.logoUrl
-          : `${import.meta.env.VITE_COS_BASE_URL}/${options.row.logoUrl}`
-        const logoImgObj = {
-          fileName: 'Logo图片',
-          url: fullLogoUrl,
-          remoteUrl: options.row.logoUrl,
+        // 回显封面图
+        if (detailData.imageUrl) {
+          const fullImageUrl = detailData.imageUrl.startsWith('http')
+            ? detailData.imageUrl
+            : `${import.meta.env.VITE_COS_BASE_URL}/${detailData.imageUrl}`
+          const coverImgObj = {
+            fileName: '封面图片',
+            url: fullImageUrl,
+            remoteUrl: detailData.imageUrl,
+          }
+          imgList.cover.push(coverImgObj)
+        }
+
+        // 回显Logo
+        if (detailData.logoUrl) {
+          const fullLogoUrl = detailData.logoUrl.startsWith('http')
+            ? detailData.logoUrl
+            : `${import.meta.env.VITE_COS_BASE_URL}/${detailData.logoUrl}`
+          const logoImgObj = {
+            fileName: 'Logo图片',
+            url: fullLogoUrl,
+            remoteUrl: detailData.logoUrl,
+          }
+          imgList.logo.push(logoImgObj)
+        }
+      } else {
+        // 如果详情接口失败,使用列表数据作为备选
+        Object.assign(modalForm.value, options.row)
+        
+        // 回显封面图
+        if (options.row.imageUrl) {
+          const fullImageUrl = options.row.imageUrl.startsWith('http')
+            ? options.row.imageUrl
+            : `${import.meta.env.VITE_COS_BASE_URL}/${options.row.imageUrl}`
+          const coverImgObj = {
+            fileName: '封面图片',
+            url: fullImageUrl,
+            remoteUrl: options.row.imageUrl,
+          }
+          imgList.cover.push(coverImgObj)
+        }
+
+        // 回显Logo
+        if (options.row.logoUrl) {
+          const fullLogoUrl = options.row.logoUrl.startsWith('http')
+            ? options.row.logoUrl
+            : `${import.meta.env.VITE_COS_BASE_URL}/${options.row.logoUrl}`
+          const logoImgObj = {
+            fileName: 'Logo图片',
+            url: fullLogoUrl,
+            remoteUrl: options.row.logoUrl,
+          }
+          imgList.logo.push(logoImgObj)
         }
-        imgList.logo.push(logoImgObj)
       }
     }
 
@@ -290,13 +360,16 @@ async function handleOpen(options) {
 
     // 初始化地图
     nextTick(() => {
-      if (options.action === 'edit' && options.row?.description) {
-        // 设置编辑器内容
-        setTimeout(() => {
-          if (addEditRef.value) {
-            addEditRef.value.setEditorContent(options.row.description)
-          }
-        }, 200)
+      if (options.action === 'edit') {
+        // 设置编辑器内容 - 使用详情数据或列表数据
+        const description = modalForm.value.description || options.row?.description
+        if (description) {
+          setTimeout(() => {
+            if (addEditRef.value) {
+              addEditRef.value.setEditorContent(description)
+            }
+          }, 200)
+        }
       }
 
       setTimeout(() => {
@@ -312,71 +385,64 @@ async function handleOpen(options) {
 
 // 取消操作
 function handleCancel() {
+  // 重置地图实例
+  if (map) {
+    map.destroy()
+    map = null
+    marker = null
+  }
   showAddEditForm.value = false
   currentAction.value = ''
+  // 取消时也刷新列表,确保数据最新
+  nextTick(() => {
+    $table.value?.handleSearch()
+  })
 }
 
 // 确定操作
-async function handleConfirm() {
-  try {
-    // 设置图片URL
-    if (imgList.cover.length > 0) {
-      modalForm.value.imageUrl = imgList.cover[0].remoteUrl
-    }
-    if (imgList.logo.length > 0) {
-      modalForm.value.logoUrl = imgList.logo[0].remoteUrl
-    }
-
-    // 设置富文本内容
-    if (addEditRef.value) {
-      modalForm.value.description = addEditRef.value.getEditorContent()
-    }
-
-    // 记录当前操作类型
-    const actionType = currentAction.value
-
-    // 执行保存操作
-    if (actionType === 'add') {
-      await originalDoCreate(modalForm.value)
-    }
-    else if (actionType === 'edit') {
-      await originalDoUpdate(modalForm.value)
-    }
-
-    // 保存成功后隐藏表单
-    showAddEditForm.value = false
-    currentAction.value = ''
-
-    // 刷新列表
-    $table.value?.handleSearch()
-
-    $message.success(actionType === 'add' ? '新增成功' : '编辑成功')
-  }
-  catch (error) {
-    console.error('保存失败:', error)
-    $message.error('保存失败')
+function handleConfirm() {
+  // 重置地图实例
+  if (map) {
+    map.destroy()
+    map = null
+    marker = null
   }
+  // 保存成功后隐藏表单
+  showAddEditForm.value = false
+  currentAction.value = ''
+  // 使用nextTick确保DOM更新后再刷新列表
+  nextTick(() => {
+    $table.value?.handleSearch()
+  })
 }
 
 // 获取展会列表
 async function getExhibitionList() {
   try {
-    // 这里需要调用获取展会列表的API
-    // const response = await api.getExhibitionList()
-    // exhibitionOptions.value = response.data.map(item => ({
-    //   label: item.name,
-    //   value: item.id
-    // }))
-
-    // 临时模拟数据
-    exhibitionOptions.value = [
-      { label: '展会1', value: 1 },
-      { label: '展会2', value: 2 },
-      { label: '展会3', value: 3 },
-    ]
+    // 调用获取展会列表的API
+    const params = {
+      name: '',
+      pageNo: 1,
+      pageSize: 20, // 获取所有展会数据
+      online: '', //不传值表示获取全部
+    }
+    
+    const response = await api.getExhibitionList(params)
+    
+    if (response.code === 0 && response.data && response.data.pageData) {
+      exhibitionOptions.value = response.data.pageData.map(item => ({
+        label: item.name,
+        value: item.id,
+        ...item
+      }))
+    } else {
+      exhibitionOptions.value = []
+      console.warn('获取展会列表响应格式异常:', response)
+    }
   }
   catch (error) {
     console.error('获取展会列表失败:', error)
+    exhibitionOptions.value = []
   }
 }
 
@@ -478,56 +544,95 @@ function initMap() {
       return
     }
 
-    if (!map) {
+    // 如果地图实例已存在,先销毁
+    if (map) {
       try {
-        map = new TMap.Map(mapContainer, {
-          center: new TMap.LatLng(39.916527, 116.397128),
-          zoom: 13,
-        })
-
-        marker = new TMap.MultiMarker({
-          map,
-          styles: {
-            marker: new TMap.MarkerStyle({
-              width: 25,
-              height: 35,
-              anchor: { x: 16, y: 32 },
-            }),
-          },
-          geometries: [{
-            id: 'marker1',
-            styleId: 'marker',
-            position: new TMap.LatLng(39.916527, 116.397128),
-          }],
-        })
-
-        console.log('地图初始化成功')
-      }
-      catch (error) {
-        console.error('地图初始化失败:', error)
+        map.destroy()
+      } catch (error) {
+        console.warn('销毁旧地图实例失败:', error)
       }
+      map = null
+      marker = null
+    }
+
+    try {
+      map = new TMap.Map(mapContainer, {
+        center: new TMap.LatLng(39.916527, 116.397128),
+        zoom: 13,
+      })
+
+      marker = new TMap.MultiMarker({
+        map,
+        styles: {
+          marker: new TMap.MarkerStyle({
+            width: 25,
+            height: 35,
+            anchor: { x: 16, y: 32 },
+          }),
+        },
+        geometries: [{
+          id: 'marker1',
+          styleId: 'marker',
+          position: new TMap.LatLng(39.916527, 116.397128),
+        }],
+      })
+
+      console.log('地图初始化成功')
+    }
+    catch (error) {
+      console.error('地图初始化失败:', error)
     }
   }, 300)
 }
 
 // 搜索地址
-function searchLocation() {
-  if (!modalForm.value.address) {
-    $message.warning('请输入地址')
-    return
-  }
-
+function searchLocation(locationData) {
   if (typeof TMap === 'undefined') {
     $message.warning('地图服务未加载')
     return
   }
 
   try {
+    // 如果传入了位置数据(来自自动完成选择),直接使用
+    if (locationData && locationData.location) {
+      const { address, location } = locationData
+      
+      if (location.lat && location.lng) {
+        const latLng = new TMap.LatLng(location.lat, location.lng)
+
+        // 更新地图中心和缩放级别
+        if (map) {
+          map.setCenter(latLng)
+          map.setZoom(16) // 设置合适的缩放级别
+        }
+
+        // 更新标记位置
+        if (marker) {
+          marker.updateGeometries([{
+            id: 'marker1',
+            styleId: 'marker',
+            position: latLng,
+          }])
+        }
+
+        $message.success('地址定位成功')
+      } else {
+        $message.warning('位置信息无效')
+      }
+      return
+    }
+
+    // 兼容旧的搜索方式(如果没有传入位置数据)
+    if (!modalForm.value.address) {
+      $message.warning('请输入地址')
+      return
+    }
+
     $message.loading('搜索中...')
 
-    // 使用JSONP方式调用腾讯地图WebService API进行城市/区域搜索
+    // 使用JSONP方式调用腾讯地图WebService API进行地址建议搜索
     const callbackName = `jsonp_callback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
-    const url = `https://apis.map.qq.com/ws/place/v1/search?keyword=${encodeURIComponent(modalForm.value.address)}&boundary=region()&key=YCABZ-AFPRX-VD54O-TL3VN-TL7A3-KPBQJ&output=jsonp&callback=${callbackName}&page_size=10`
+    const url = `https://apis.map.qq.com/ws/place/v1/suggestion?keyword=${encodeURIComponent(modalForm.value.address)}&region=北京&key=YCABZ-AFPRX-VD54O-TL3VN-TL7A3-KPBQJ&output=jsonp&callback=${callbackName}&page_size=10`
 
     // 创建script标签进行JSONP请求
     const script = document.createElement('script')
@@ -567,7 +672,7 @@ function searchLocation() {
               modalForm.value.address = firstResult.title
             }
 
-            $message.success(`找到 ${data.count} 个相关地点,已定位到: ${firstResult.title}`)
+            // $message.success(`找到 ${data.count} 个相关地点,已定位到: ${firstResult.title}`)
 
             // 如果有多个结果,在控制台显示其他选项
             if (data.data.length > 1) {

+ 9 - 7
src/views/kzhanManage/OfflineExhibitionNewsMgt/api.js

@@ -1,11 +1,13 @@
 import { request } from '@/utils'
 
 export default {
-  create: data => request.post('/user', data),
-  read: (params = {}) => request.get('/user', { params }),
-  update: data => request.patch(`/user/${data.id}`, data),
-  delete: id => request.delete(`/user/${id}`),
-  resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
-
-  getAllRoles: () => request.get('/role?enable=1'),
+  create: data => request.post('/exhibition/add', data),
+  getById: id => request.get(`/exhibition/${id}`),
+  read: (params = {}) => {
+    // 添加online=1参数,表示线上展会
+    const queryParams = { ...params, online: 0 }
+    return request.get('/exhibition/page', { params: queryParams })
+  },
+  update: data => request.patch(`/exhibition/${data.id}`, data),
+  delete: id => request.delete(`/exhibition/${id}`),
 }

+ 618 - 0
src/views/kzhanManage/OfflineExhibitionNewsMgt/components/addAndEditOffline.vue

@@ -0,0 +1,618 @@
+<template>
+  <div class="add-edit-offline bg-card rounded-lg p-6 shadow-lg">
+    <div class="mb-6">
+      <h2 class="text-xl text-gray-800 font-semibold">
+        {{ isEdit ? '编辑线下展会' : '新增线下展会' }}
+      </h2>
+    </div>
+
+    <n-form
+      ref="formRef"
+      label-placement="left"
+      label-align="left"
+      :label-width="100"
+      :model="formData"
+      :disabled="modalAction === 'view'"
+    >
+      <n-form-item
+        label="展会名称"
+        path="name"
+        :rule="{
+          required: true,
+          message: '请输入展会名称',
+          trigger: ['input', 'blur'],
+        }"
+      >
+        <n-input v-model:value="formData.name" placeholder="请输入展会名称" />
+      </n-form-item>
+      
+      <n-form-item
+        label="展览日期"
+        path="dateRange"
+      >
+        <n-date-picker v-model:value="formData.dateRange" type="daterange" clearable placeholder="请选择展览日期" />
+      </n-form-item>
+      
+      <n-form-item
+        label="展馆"
+        path="pavilionId"
+        :rule="{
+          required: true,
+          type: 'number',
+          message: '请选择展馆',
+          trigger: ['change', 'blur'],
+        }"
+      >
+        <n-select
+          v-model:value="formData.pavilionId"
+          :options="pavilionOptions"
+          :loading="pavilionLoading"
+          filterable
+          remote
+          clearable
+          placeholder="请选择展馆"
+          :on-search="handlePavilionSearch"
+          label-field="name"
+          value-field="id"
+        />
+      </n-form-item>
+
+      <n-form-item
+        label="展览时间"
+        path="openTimeDetail"
+      >
+        <n-input v-model:value="formData.openTimeDetail" placeholder="请输入展览时间" />
+      </n-form-item>
+
+      <n-form-item label="展会封面图" path="coverImageUrl">
+        <div class="flex items-start gap-4">
+          <NUpload
+            class="w-200px text-center"
+            :custom-request="(options) => handleUpload(options, 'cover')"
+            :show-file-list="false"
+            accept=".png,.jpg,.jpeg"
+            @before-upload="onBeforeUpload"
+          >
+            <NUploadDragger>
+              <div class="h-60 f-c-c flex-col">
+                <i class="i-mdi:upload mb-4 text-32 color-primary" />
+                <NText class="text-12 color-gray">
+                  点击或拖拽上传封面图
+                </NText>
+              </div>
+            </NUploadDragger>
+          </NUpload>
+          <div v-if="imgList.cover && imgList.cover.length" class="ml-20">
+            <NImage width="100" :src="imgList.cover[0].url" />
+          </div>
+        </div>
+      </n-form-item>
+
+
+
+      <n-form-item
+        label="展会状态"
+        path="statusText"
+        :rule="{
+          required: true,
+          message: '请输入展会状态',
+          trigger: ['input', 'blur'],
+        }"
+      >
+        <n-input v-model:value="formData.statusText" placeholder="请输入展会状态" />
+      </n-form-item>
+      
+
+      
+      <n-form-item label="置顶" path="setTop">
+        <NSwitch v-model:value="formData.setTop">
+          <template #checked>
+            是
+          </template>
+          <template #unchecked>
+            否
+          </template>
+        </NSwitch>
+      </n-form-item>
+      
+      <n-form-item label="热门" path="hot">
+        <NSwitch v-model:value="formData.hot">
+          <template #checked>
+            是
+          </template>
+          <template #unchecked>
+            否
+          </template>
+        </NSwitch>
+      </n-form-item>
+      
+
+      
+      <n-form-item
+        label="相关艺术家"
+        path="artistId"
+      >
+        <n-select
+          v-model:value="formData.artistId"
+          :options="artistOptions"
+          :loading="artistLoading"
+          filterable
+          remote
+          clearable
+          multiple
+          placeholder="请选择相关艺术家"
+          :on-search="handleArtistSearch"
+          label-field="name"
+          value-field="id"
+        />
+      </n-form-item>
+
+      <n-form-item
+        label="地址"
+        path="address"
+        :rule="{
+          required: true,
+          message: '请输入地址',
+          trigger: ['input', 'blur', 'change'],
+        }"
+      >
+        <div class="w-full">
+          <div class="mb-2">
+            <NSelect
+              v-model:value="formData.address"
+              filterable
+              placeholder="请输入地址进行搜索"
+              :options="addressOptions"
+              :loading="addressLoading"
+              :remote="true"
+              :clear-filter-after-select="false"
+              @search="handleAddressSearch"
+              @update:value="handleAddressSelect"
+              class="w-full"
+            />
+          </div>
+          <div id="mapContainer" class="h-300px mt-10 w-full border border-gray-300 rounded" />
+        </div>
+      </n-form-item>
+
+      <n-form-item
+        label="详情"
+        path="description"
+      >
+        <div class="w-full">
+          <WangEditor
+            ref="editorRef"
+            v-model="formData.description"
+            :height="'300px'"
+            placeholder="请输入展会详情描述..."
+            :toolbar-config="toolbarConfig"
+            :editor-config="editorConfig"
+          />
+        </div>
+      </n-form-item>
+      
+
+    </n-form>
+
+    <!-- 操作按钮 -->
+    <div class="mt-6 flex justify-end gap-4 pt-4">
+      <NButton @click="handleCancel">
+        取消
+      </NButton>
+      <NButton type="primary" @click="handleConfirm">
+        确定
+      </NButton>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, watch } from 'vue'
+import { WangEditor } from '@/components'
+import { NButton, NImage, NSelect, NSwitch, NText, NTimePicker, NUpload, NUploadDragger, NDatePicker, NRadio, NRadioGroup, NInputNumber } from 'naive-ui'
+import { request } from '@/utils'
+import pavilionApi from '../../GalleryMgt/api'
+import artistApi from '../../ArtistMgt/api'
+import api from '../api'
+
+defineOptions({ name: 'AddAndEditOffline' })
+
+const props = defineProps({
+  isEdit: {
+    type: Boolean,
+    default: false,
+  },
+  formData: {
+    type: Object,
+    required: true,
+  },
+  imgList: {
+    type: Object,
+    required: true,
+  },
+  modalAction: {
+    type: String,
+    default: 'add'
+  },
+  pavilionOptions: {
+    type: Array,
+    default: () => [],
+  },
+  artistOptions: {
+    type: Array,
+    default: () => [],
+  },
+})
+
+const emit = defineEmits(['cancel', 'confirm', 'upload', 'search-location'])
+
+const formRef = ref(null)
+const editorRef = ref(null)
+
+// 地址搜索相关状态
+const addressOptions = ref([])
+const addressLoading = ref(false)
+let searchTimeout = null
+
+// 展馆和艺术家加载状态
+const pavilionLoading = ref(false)
+const artistLoading = ref(false)
+
+// 工具栏配置 - 移除表格和待办功能
+const toolbarConfig = {
+  excludeKeys: [
+    'insertTable', // 表格
+    'deleteTable',
+    'insertTableRow',
+    'deleteTableRow',
+    'insertTableCol',
+    'deleteTableCol',
+    'tableHeader',
+    'tableFullWidth',
+    'todo', // 待办
+  ]
+}
+
+// 编辑器配置 - 配置图片和视频上传
+const editorConfig = {
+  MENU_CONF: {
+    uploadImage: {
+      // 自定义上传
+      async customUpload(file, insertFn) {
+        try {
+          const formData = new FormData()
+          formData.append('file', file)
+          
+          const response = await request.post('/file/upload', formData, {
+            headers: {
+              'Content-Type': 'multipart/form-data'
+            }
+          })
+          console.log(response)
+          if (response.data) {
+            // 插入图片到编辑器
+            insertFn(response.data, file.name, response.data)
+            $message.success('图片上传成功')
+          } else {
+            throw new Error('上传响应格式错误')
+          }
+        } catch (error) {
+          console.error('图片上传失败', error)
+          $message.error('图片上传失败:' + (error.message || '未知错误'))
+        }
+      },
+      // 上传错误的回调函数
+      onError(file, err, res) {
+        console.error('图片上传失败', err, res)
+        $message.error('图片上传失败')
+      },
+      // 上传成功的回调函数
+      onSuccess(file, res) {
+        console.log('图片上传成功', res)
+      },
+      // 上传进度的回调函数
+      onProgress(progress) {
+        console.log('上传进度', progress)
+      },
+      // 限制文件大小,默认为 5M
+      maxFileSize: 5 * 1024 * 1024, // 5M
+      // 限制文件类型
+      allowedFileTypes: ['image/*'],
+    },
+    uploadVideo: {
+      // 自定义视频上传
+      async customUpload(file, insertFn) {
+        try {
+          const formData = new FormData()
+          formData.append('file', file)
+          
+          const response = await request.post('/file/upload', formData, {
+            headers: {
+              'Content-Type': 'multipart/form-data'
+            }
+          })
+          console.log(response)
+          if (response.data) {
+            // 插入视频到编辑器
+            insertFn(response.data)
+            $message.success('视频上传成功')
+          } else {
+            throw new Error('上传响应格式错误')
+          }
+        } catch (error) {
+          console.error('视频上传失败', error)
+          $message.error('视频上传失败:' + (error.message || '未知错误'))
+        }
+      },
+      // 上传错误的回调函数
+      onError(file, err, res) {
+        console.error('视频上传失败', err, res)
+        $message.error('视频上传失败')
+      },
+      // 上传成功的回调函数
+      onSuccess(file, res) {
+        console.log('视频上传成功', res)
+      },
+      // 上传进度的回调函数
+      onProgress(progress) {
+        console.log('视频上传进度', progress)
+      },
+      // 限制文件大小,默认为 50M
+      maxFileSize: 50 * 1024 * 1024, // 50M
+      // 限制文件类型
+      allowedFileTypes: ['video/*'],
+    }
+  }
+}
+
+// 上传前验证
+function onBeforeUpload({ file }) {
+  if (!file.file?.type.startsWith('image/')) {
+    $message.error('只能上传图片')
+    return false
+  }
+  return true
+}
+
+// 处理文件上传
+function handleUpload(options, type) {
+  emit('upload', options, type)
+}
+
+// 处理地址搜索
+function handleAddressSearch(query) {
+  if (!query || query.length < 2) {
+    addressOptions.value = []
+    return
+  }
+
+  // 防抖处理
+  if (searchTimeout) {
+    clearTimeout(searchTimeout)
+  }
+
+  searchTimeout = setTimeout(() => {
+    searchAddressSuggestions(query)
+  }, 300)
+}
+
+// 调用腾讯位置服务suggestion API
+function searchAddressSuggestions(keyword) {
+  addressLoading.value = true
+  
+  try {
+    // 使用JSONP方式调用腾讯地图WebService API进行地址建议搜索
+    const callbackName = `jsonp_callback_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
+    const url = `https://apis.map.qq.com/ws/place/v1/suggestion?keyword=${encodeURIComponent(keyword)}&region=北京&key=YCABZ-AFPRX-VD54O-TL3VN-TL7A3-KPBQJ&output=jsonp&callback=${callbackName}&page_size=10`
+
+    // 创建script标签进行JSONP请求
+    const script = document.createElement('script')
+    script.src = url
+
+    // 定义回调函数
+    window[callbackName] = function (data) {
+      try {
+        console.log('地址搜索响应:', data)
+        if (data.status === 0 && data.data && data.data.length > 0) {
+          // 转换为NSelect需要的格式
+          addressOptions.value = data.data.map(item => ({
+            label: item.title + (item.address ? ` - ${item.address}` : ''),
+            value: item.title + (item.address ? ` - ${item.address}` : ''),
+            location: item.location,
+            city: item.city,
+            province: item.province,
+            district: item.district,
+            adcode: item.adcode,
+            id: item.id,
+            category: item.category
+          }))
+        } else {
+          addressOptions.value = []
+        }
+      } catch (error) {
+        console.error('地址搜索处理失败:', error)
+        addressOptions.value = []
+      } finally {
+        addressLoading.value = false
+        // 清理:移除script标签和回调函数
+        document.head.removeChild(script)
+        delete window[callbackName]
+      }
+    }
+
+    // 处理加载错误
+    script.onerror = function () {
+      console.error('地址搜索JSONP请求失败')
+      addressOptions.value = []
+      addressLoading.value = false
+      // 清理
+      document.head.removeChild(script)
+      delete window[callbackName]
+    }
+
+    // 添加script标签到head中开始请求
+    document.head.appendChild(script)
+  } catch (error) {
+    console.error('地址搜索失败:', error)
+    addressLoading.value = false
+    addressOptions.value = []
+  }
+}
+
+// 处理地址选择
+function handleAddressSelect(value) {
+  if (value) {
+    // 找到选中的地址选项
+    const selectedOption = addressOptions.value.find(option => option.value === value)
+    if (selectedOption && selectedOption.location) {
+      // 触发地图更新
+      emit('search-location', {
+        address: value,
+        location: selectedOption.location,
+        city: selectedOption.city,
+        province: selectedOption.province,
+        district: selectedOption.district,
+        adcode: selectedOption.adcode,
+        id: selectedOption.id,
+        category: selectedOption.category
+      })
+    }
+  }
+}
+
+// 展馆搜索处理
+function handlePavilionSearch(query) {
+  emit('pavilion-search', query)
+}
+
+// 艺术家搜索处理
+function handleArtistSearch(query) {
+  emit('artist-search', query)
+}
+
+// 监听地址变化,在编辑模式下自动搜索和定位
+watch(() => props.formData.address, (newAddress) => {
+  if (newAddress && props.isEdit) {
+    // 延迟执行,确保地图已经初始化
+    setTimeout(() => {
+      // 直接触发地址搜索和定位,传递已有的经纬度信息
+      emit('search-location', {
+        address: newAddress,
+        latitude: props.formData.latitude,
+        longitude: props.formData.longitude,
+        city: props.formData.city
+      })
+    }, 1000)
+  }
+}, { immediate: true })
+
+// 取消操作
+function handleCancel() {
+  emit('cancel')
+}
+
+// 确定操作
+async function handleConfirm() {
+  formRef.value?.validate(async (errors) => {
+    if (!errors) {
+      try {
+        // 获取富文本内容
+        const description = getEditorContent()
+        
+        // 格式化展览日期
+        let openTime = ''
+        if (props.formData.dateRange && props.formData.dateRange.length === 2) {
+          // 使用本地日期格式化,避免时区转换问题
+          const startDate = new Date(props.formData.dateRange[0])
+          const endDate = new Date(props.formData.dateRange[1])
+          const formatDate = (date) => {
+            const year = date.getFullYear()
+            const month = String(date.getMonth() + 1).padStart(2, '0')
+            const day = String(date.getDate()).padStart(2, '0')
+            return `${year}-${month}-${day}`
+          }
+          openTime = `${formatDate(startDate)} - ${formatDate(endDate)}`
+        }
+        
+        // 获取图片URL(从imgList中获取)
+        let imageUrl = props.formData.coverImageUrl || ''
+        if (props.imgList.cover && props.imgList.cover.length > 0) {
+          imageUrl = props.imgList.cover[0].remoteUrl || props.imgList.cover[0].url
+        }
+        
+        // 构建请求参数
+        const requestData = {
+          address: props.formData.address || '',
+          artistIdList: Array.isArray(props.formData.artistId) ? props.formData.artistId : (props.formData.artistId ? [props.formData.artistId] : []),
+          city: props.formData.city || '',
+          description: description || '',
+          imageUrl: imageUrl,
+          latitude: props.formData.latitude || 0,
+          longitude: props.formData.longitude || 0,
+          name: props.formData.name || '',
+          online: 0,
+          openTime: openTime,
+          openTimeDetail: props.formData.openTimeDetail || '',
+          pavilionId: props.formData.pavilionId || 0,
+          hot: props.formData.hot ? 1 : 0,
+          setTop: props.formData.setTop ? 'A' : 'I',
+          statusText: props.formData.statusText || ''
+        }
+        
+        console.log(requestData, 'requestData')
+        
+        // 根据编辑状态调用不同接口
+        let response
+        if (props.isEdit) {
+          // 编辑模式:添加ID参数并调用更新接口
+          requestData.id = props.formData.id
+          response = await api.update(requestData)
+        } else {
+          // 新增模式:调用新增接口
+          response = await api.create(requestData)
+        }
+        
+        if (response.code === 0) {
+          $message.success(props.isEdit ? '更新成功' : '新增成功')
+          // 添加小延迟确保消息显示后再触发父组件处理
+          setTimeout(() => {
+            emit('confirm')
+          }, 100)
+        } else {
+          $message.error(response.message || (props.isEdit ? '更新失败' : '新增失败'))
+        }
+      } catch (error) {
+        console.error(props.isEdit ? '更新失败:' : '新增失败:', error)
+        $message.error(props.isEdit ? '更新失败,请重试' : '新增失败,请重试')
+      }
+    }
+  })
+}
+
+// 获取编辑器内容
+function getEditorContent() {
+  return editorRef.value?.getHtml() || ''
+}
+
+// 设置编辑器内容
+function setEditorContent(content) {
+  if (editorRef.value) {
+    editorRef.value.setHtml(content || '')
+  }
+}
+
+// 暴露表单引用和编辑器方法给父组件
+defineExpose({
+  formRef,
+  editorRef,
+  getEditorContent,
+  setEditorContent,
+})
+</script>
+
+<style lang="scss" scoped>
+.add-edit-offline {
+  max-width: 1200px;
+  margin: 0 auto;
+}
+</style>

+ 649 - 131
src/views/kzhanManage/OfflineExhibitionNewsMgt/index.vue

@@ -1,118 +1,46 @@
 <template>
   <CommonPage>
     <template #action>
-      <NButton v-permission="'addOnline'" type="primary" @click="handleAdd()">
+      <NButton v-if="!showAddEditForm" type="primary" @click="handleAdd()">
         <i class="i-material-symbols:add mr-4 text-18" />
         新增
       </NButton>
     </template>
 
     <MeCrud
+      v-if="!showAddEditForm"
       ref="$table"
       v-model:query-items="queryItems"
       :scroll-x="1400"
       :columns="columns"
       :get-data="api.read"
     >
-      <MeQueryItem label="展名称" :label-width="70">
+      <MeQueryItem label="展名称" :label-width="70">
         <n-input
           v-model:value="queryItems.name"
           type="text"
-          placeholder="请输入展名称"
+          placeholder="请输入展名称"
           clearable
         />
       </MeQueryItem>
-
-      <MeQueryItem label="展览状态" :label-width="70">
-        <n-select
-          v-model:value="queryItems.status"
-          clearable
-          :options="[
-            { label: '待审核', value: '待审核' },
-            { label: '已上架', value: '已上架' },
-          ]"
-        />
-      </MeQueryItem>
-
-      <MeQueryItem label="展馆" :label-width="50">
-        <n-input
-          v-model:value="queryItems.museum"
-          type="text"
-          placeholder="请输入展馆"
-          clearable
-        />
-      </MeQueryItem>
-
-      <div class="flex justify-end">
-        <NButton v-permission="'findOnline'" type="primary" @click="$table?.handleSearch()">
-          <template #icon>
-            <i class="i-material-symbols:search mr-2 text-16" />
-          </template>
-          查找
-        </NButton>
-      </div>
     </MeCrud>
 
-    <MeModal ref="modalRef" width="520px">
-      <n-form
-        ref="modalFormRef"
-        label-placement="left"
-        label-align="left"
-        :label-width="80"
-        :model="modalForm"
-        :disabled="modalAction === 'view'"
-      >
-        <n-form-item
-          label="展览名称"
-          path="name"
-          :rule="{
-            required: true,
-            message: '请输入展览名称',
-            trigger: ['input', 'blur'],
-          }"
-        >
-          <n-input v-model:value="modalForm.name" />
-        </n-form-item>
-        <n-form-item
-          label="展览状态"
-          path="status"
-          :rule="{
-            required: true,
-            message: '请选择展览状态',
-            trigger: ['change', 'blur'],
-          }"
-        >
-          <n-select
-            v-model:value="modalForm.status"
-            :options="[
-              { label: '已上架', value: '已上架' },
-              { label: '待审核', value: '待审核' },
-            ]"
-          />
-        </n-form-item>
-        <n-form-item
-          label="展览时间"
-          path="exhibitionTime"
-        >
-          <n-date-picker
-            v-model:value="modalForm.exhibitionTime"
-            type="daterange"
-            clearable
-          />
-        </n-form-item>
-        <n-form-item
-          label="展馆位置"
-          path="museum"
-          :rule="{
-            required: true,
-            message: '请输入展馆位置',
-            trigger: ['input', 'blur'],
-          }"
-        >
-          <n-input v-model:value="modalForm.museum" />
-        </n-form-item>
-      </n-form>
-    </MeModal>
+    <AddAndEditOffline
+        v-if="showAddEditForm"
+        ref="addEditRef"
+        :is-edit="modalAction === 'edit'"
+        :form-data="modalForm"
+        :img-list="imgList"
+        :modal-action="modalAction"
+        :pavilion-options="pavilionOptions"
+        :artist-options="artistOptions"
+        @cancel="handleFormCancel"
+        @confirm="handleComponentConfirm"
+        @upload="handleUpload"
+        @search-location="handleAddressSelect"
+        @pavilion-search="handlePavilionSearch"
+        @artist-search="handleArtistSearch"
+      />
   </CommonPage>
 </template>
 
@@ -120,18 +48,367 @@
 import { MeCrud, MeModal, MeQueryItem } from '@/components'
 import { useCrud } from '@/composables'
 import { withPermission } from '@/directives'
-import { formatDateTime } from '@/utils'
-import { NButton, NImage, NSwitch, NTag, NDatePicker, NSelect } from 'naive-ui'
+import { formatDateTime, request } from '@/utils'
+import { NButton, NImage, NSwitch, NTag, NSelect, NUpload, NUploadDragger, NText, NRadio, NRadioGroup, NInputNumber, NDatePicker, NTimePicker } from 'naive-ui'
+import WangEditor from '@/components/common/WangEditor.vue'
+import AddAndEditOffline from './components/addAndEditOffline.vue'
 import api from './api'
+import pavilionApi from '../GalleryMgt/api'
+import artistApi from '../ArtistMgt/api'
 
-defineOptions({ name: 'OfflineExhibitionNewsMgt' })
+defineOptions({ name: 'OnlineExhibition' })
 
 const $table = ref(null)
+const addEditRef = ref(null)
 /** QueryBar筛选参数(可选) */
 const queryItems = ref({})
 
-onMounted(() => {
+// 控制显示状态:false 显示表格,true 显示表单
+const showAddEditForm = ref(false)
+
+// 展馆相关状态
+const pavilionOptions = ref([])
+const pavilionLoading = ref(false)
+
+// 艺术家相关状态
+const artistOptions = ref([])
+const artistLoading = ref(false)
+
+// 地址相关状态
+const addressOptions = ref([])
+const addressLoading = ref(false)
+
+// 富文本编辑器相关 - 移到组件内部
+// const editorRef = ref(null)
+// const toolbarConfig = ref({})
+// const editorConfig = ref({})
+
+// 图片上传相关状态
+const imgList = ref({
+  cover: [],
+  share: []
+})
+
+// 防抖搜索展馆
+let searchTimer = null
+const handlePavilionSearch = (query) => {
+  if (searchTimer) {
+    clearTimeout(searchTimer)
+  }
+  searchTimer = setTimeout(() => {
+    loadPavilionOptions(query)
+  }, 300)
+}
+
+// 防抖搜索艺术家
+let artistSearchTimer = null
+const handleArtistSearch = (query) => {
+  if (artistSearchTimer) {
+    clearTimeout(artistSearchTimer)
+  }
+  artistSearchTimer = setTimeout(() => {
+    loadArtistOptions(query)
+  }, 300)
+}
+
+// 防抖搜索地址
+let addressSearchTimer = null
+const handleAddressSearch = (query) => {
+  if (addressSearchTimer) {
+    clearTimeout(addressSearchTimer)
+  }
+  addressSearchTimer = setTimeout(() => {
+    loadAddressOptions(query)
+  }, 300)
+}
+
+// 加载展馆选项
+const loadPavilionOptions = async (name = '') => {
+  pavilionLoading.value = true
+  try {
+    const params = {
+      pageSize: 20,
+      pageNo: 1
+    }
+    if (name) {
+      params.name = name,
+      params.pageSize = 100
+    }
+    const { data } = await pavilionApi.read(params)
+    pavilionOptions.value = data.pageData || []
+  } catch (error) {
+    console.error('加载展馆列表失败:', error)
+    pavilionOptions.value = []
+  } finally {
+    pavilionLoading.value = false
+  }
+}
+
+// 加载艺术家选项
+const loadArtistOptions = async (name = '') => {
+  artistLoading.value = true
+  try {
+    const params = {
+      pageSize: 20,
+      pageNo: 1
+    }
+    if (name) {
+      params.name = name
+      params.pageSize = 100
+    }
+    const { data } = await artistApi.read(params)
+    artistOptions.value = data.pageData || []
+  } catch (error) {
+    console.error('加载艺术家列表失败:', error)
+    artistOptions.value = []
+  } finally {
+    artistLoading.value = false
+  }
+}
+
+// 地图相关变量
+let map = null
+let marker = null
+
+// 初始化地图
+function initMap() {
+  if (typeof TMap === 'undefined') {
+    console.error('腾讯地图SDK未加载')
+    return
+  }
+  
+  // 等待DOM完全渲染后再初始化地图
+  setTimeout(() => {
+    const mapContainer = document.getElementById('mapContainer')
+    if (!mapContainer) {
+      console.warn('地图容器未找到')
+      return
+    }
+
+    // 检查容器是否可见
+    const rect = mapContainer.getBoundingClientRect()
+    if (rect.width === 0 || rect.height === 0) {
+      console.warn('地图容器尺寸为0,延迟初始化')
+      setTimeout(initMap, 500)
+      return
+    }
+
+    // 如果地图实例已存在,先销毁
+    if (map) {
+      try {
+        map.destroy()
+      } catch (error) {
+        console.warn('销毁旧地图实例失败:', error)
+      }
+      map = null
+      marker = null
+    }
+
+    try {
+      map = new TMap.Map(mapContainer, {
+        center: new TMap.LatLng(39.908823, 116.397470),
+        zoom: 13,
+      })
+
+      marker = new TMap.MultiMarker({
+        map,
+        styles: {
+          marker: new TMap.MarkerStyle({
+            width: 25,
+            height: 35,
+            anchor: { x: 16, y: 32 },
+          }),
+        },
+        geometries: [{
+          id: 'marker1',
+          styleId: 'marker',
+          position: new TMap.LatLng(39.908823, 116.397470),
+        }],
+      })
+
+      console.log('地图初始化成功')
+    }
+    catch (error) {
+      console.error('地图初始化失败:', error)
+    }
+  }, 300)
+}
+
+// 在地图上标记位置
+function markLocationOnMap(location, address, locationData) {
+  if (!map || !location) return
+  
+  try {
+    // 更新标记位置
+    const position = new TMap.LatLng(location.lat, location.lng)
+    
+    // 更新地图中心和缩放级别
+    if (map) {
+      map.setCenter(position)
+      map.setZoom(16) // 设置合适的缩放级别
+    }
+
+    // 更新标记位置
+    if (marker) {
+      marker.updateGeometries([{
+        id: 'marker1',
+        styleId: 'marker',
+        position: position,
+      }])
+    }
+    
+    // 存储经纬度信息到modalForm
+    modalForm.value.latitude = location.lat
+    modalForm.value.longitude = location.lng
+    modalForm.value.city = locationData.city
+    
+    console.log('地图标记成功:', { lat: location.lat, lng: location.lng, city: modalForm.value.city })
+  } catch (error) {
+    console.error('地图标记失败:', error)
+  }
+}
+
+// 处理地址选择 - 地图定位逻辑
+const handleAddressSelect = (locationData) => {
+  console.log('选择的地址:', locationData)
+  
+  if (locationData.location) {
+    // 在地图上标记位置
+    markLocationOnMap(locationData.location, locationData.address, locationData)
+  } else if (locationData.address) {
+    // 检查是否有传递过来的经纬度信息(编辑模式回显)
+    if (locationData.latitude && locationData.longitude) {
+      // 使用传递过来的经纬度进行地图定位
+      const savedLocation = {
+        lat: locationData.latitude,
+        lng: locationData.longitude
+      }
+      markLocationOnMap(savedLocation, locationData.address, {
+        city: locationData.city,
+        address: locationData.address
+      })
+    } else if (modalForm.value.latitude && modalForm.value.longitude) {
+      // 使用表单中已保存的经纬度进行地图定位
+      const savedLocation = {
+        lat: modalForm.value.latitude,
+        lng: modalForm.value.longitude
+      }
+      markLocationOnMap(savedLocation, locationData.address, {
+        city: modalForm.value.city,
+        address: locationData.address
+      })
+    } else {
+      // 如果没有坐标,可以进行地址解析
+      console.log('需要进行地址解析:', locationData.address)
+    }
+  }
+}
+
+// 确定操作
+function handleComponentConfirm() {
+  // 重置地图实例
+  if (map) {
+    map.destroy()
+    map = null
+    marker = null
+  }
+  // 保存成功后隐藏表单
+  showAddEditForm.value = false
+  // 使用nextTick确保DOM更新后再刷新列表
+  nextTick(() => {
+    $table.value?.handleSearch()
+  })
+}
+
+// 加载腾讯地图SDK
+function loadTencentMapSDK() {
+  return new Promise((resolve, reject) => {
+    // 检查是否已经加载
+    if (typeof TMap !== 'undefined') {
+      resolve()
+      return
+    }
+    
+    // 创建script标签加载SDK
+    const script = document.createElement('script')
+    script.src = 'https://map.qq.com/api/gljs?v=1.exp&key=YCABZ-AFPRX-VD54O-TL3VN-TL7A3-KPBQJ'
+    script.onload = () => {
+      console.log('腾讯地图SDK加载成功')
+      resolve()
+    }
+    script.onerror = () => {
+      console.error('腾讯地图SDK加载失败')
+      reject(new Error('腾讯地图SDK加载失败'))
+    }
+    document.head.appendChild(script)
+  })
+}
+
+// 上传前验证
+function onBeforeUpload({ file }) {
+  if (!file.file?.type.startsWith('image/')) {
+    $message.error('只能上传图片')
+    return false
+  }
+  return true
+}
+
+// 处理文件上传
+async function handleUpload(options, type) {
+  const { file } = options
+  try {
+    const formData = new FormData()
+    formData.append('file', file.file)
+    
+    const response = await request.post('/file/upload', formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    })
+    
+    if (response.data) {
+      // 更新对应类型的图片列表
+      imgList.value[type] = [{
+        url: response.data,
+        remoteUrl: response.data
+      }]
+      
+      // 更新表单数据
+      if (type === 'cover') {
+        modalForm.value.coverImageUrl = response.data
+      } else if (type === 'share') {
+        modalForm.value.shareImageUrl = response.data
+      }
+      
+      $message.success('图片上传成功')
+      options.onFinish()
+    } else {
+      throw new Error('上传响应格式错误')
+    }
+  } catch (error) {
+    console.error('图片上传失败', error)
+    $message.error('图片上传失败:' + (error.message || '未知错误'))
+    options.onError()
+  }
+}
+
+onMounted(async () => {
   $table.value?.handleSearch()
+  // 初始加载展馆选项
+  loadPavilionOptions()
+  // 初始加载艺术家选项
+  loadArtistOptions()
+  
+  // 加载腾讯地图SDK并初始化地图
+  try {
+    await loadTencentMapSDK()
+    // 延迟初始化地图,确保DOM已渲染
+    // setTimeout(() => {
+    //   initMap()
+    // }, 500)
+  } catch (error) {
+    console.error('地图SDK加载失败:', error)
+  }
 })
 
 const {
@@ -139,19 +416,252 @@ const {
   modalFormRef,
   modalForm,
   modalAction,
-  handleAdd,
   handleDelete,
-  handleOpen,
+  handleOpen: originalHandleOpen,
   handleSave,
 } = useCrud({
-  name: '线下展览',
-  initForm: { status: '待审核' },
-  doCreate: api.create,
+  name: '线上展会',
+  initForm: { 
+    display: true, 
+    setTop: false, 
+    isBomb: false,
+    hot: false,
+    dateRange: null,
+    startTime: null,
+    endTime: null,
+    statusText: '',
+    artistId: [],
+    address: '',
+    description: '',
+    coverImageUrl: '',
+    shareImageUrl: '',
+    latitude: 0,
+    longitude: 0,
+    city: ''
+  },
+  doCreate: (formData) => {
+    // 格式化展览日期
+    let openTime = ''
+    if (formData.dateRange && formData.dateRange.length === 2) {
+      const startDate = new Date(formData.dateRange[0]).toISOString().split('T')[0]
+      const endDate = new Date(formData.dateRange[1]).toISOString().split('T')[0]
+      openTime = `${startDate} - ${endDate}`
+    }
+    
+    const apiData = {
+      address: formData.address || '',
+      artistIdList: Array.isArray(formData.artistId) ? formData.artistId : (formData.artistId ? [formData.artistId] : []),
+      city: formData.city || '',
+      description: formData.description || '',
+      imageUrl: formData.coverImageUrl || '',
+      latitude: formData.latitude || 0,
+      longitude: formData.longitude || 0,
+      name: formData.name || '',
+      online: 0,
+      openTime: openTime,
+      openTimeDetail: formData.openTimeDetail || '',
+      pavilionId: formData.pavilionId || 0,
+      setHot: formData.hot ? 1 : 0,
+      setTop: formData.setTop ? 'A' : 'I',
+      statusText: formData.statusText || ''
+    }
+    return api.create(apiData)
+  },
   doDelete: api.delete,
-  doUpdate: api.update,
+  doUpdate: (formData) => {
+    // 格式化展览日期
+    let openTime = ''
+    if (formData.dateRange && formData.dateRange.length === 2) {
+      const startDate = new Date(formData.dateRange[0]).toISOString().split('T')[0]
+      const endDate = new Date(formData.dateRange[1]).toISOString().split('T')[0]
+      openTime = `${startDate} - ${endDate}`
+    }
+    
+    const apiData = {
+      id: formData.id,
+      address: formData.address || '',
+      artistIdList: Array.isArray(formData.artistId) ? formData.artistId : (formData.artistId ? [formData.artistId] : []),
+      city: formData.city || '',
+      description: formData.description || '',
+      imageUrl: formData.coverImageUrl || '',
+      latitude: formData.latitude || 0,
+      longitude: formData.longitude || 0,
+      name: formData.name || '',
+      online: 0,
+      openTime: openTime,
+      openTimeDetail: formData.openTimeDetail || '',
+      pavilionId: formData.pavilionId || 0,
+      setHot: formData.hot ? 1 : 0,
+      setTop: formData.setTop ? 'A' : 'I',
+      statusText: formData.statusText || ''
+    }
+    return api.update(apiData)
+  },
   refresh: () => $table.value?.handleSearch(),
 })
 
+// 重写handleAdd方法,切换到表单视图
+function handleAdd() {
+  // 重置表单数据
+  Object.assign(modalForm, {
+    display: true, 
+    setTop: false, 
+    isBomb: false,
+    hot: false,
+    dateRange: null,
+    startTime: null,
+    endTime: null,
+    statusText: '',
+    artistId: [],
+    address: '',
+    description: '',
+    coverImageUrl: '',
+    shareImageUrl: '',
+    name: '',
+    pavilionId: null,
+    latitude: 0,
+    longitude: 0,
+    city: ''
+  })
+  
+  // 设置为新增模式
+  modalAction.value = 'add'
+  
+  // 清空图片列表
+  imgList.value = {
+    cover: [],
+    share: []
+  }
+  
+  // 重新加载选项
+  loadPavilionOptions()
+  loadArtistOptions()
+  
+  // 切换到表单视图
+  showAddEditForm.value = true
+  
+  // 延迟初始化地图,确保DOM已渲染
+  setTimeout(() => {
+    initMap()
+  }, 800)
+}
+
+// 处理表单取消,返回表格视图
+function handleFormCancel() {
+  showAddEditForm.value = false
+  // 刷新列表数据
+  nextTick(() => {
+    $table.value?.handleSearch()
+  })
+}
+
+// 处理编辑操作
+function handleEdit(row) {
+  handleOpen({ action: 'edit', title: '编辑展会', row })
+}
+
+// 重写handleOpen方法,处理编辑时的数据获取和回显
+async function handleOpen(options) {
+  // 重新加载选项
+  loadPavilionOptions()
+  loadArtistOptions()
+  
+  // 切换到表单视图
+  showAddEditForm.value = true
+  
+  // 延迟初始化地图,确保弹窗DOM已渲染
+  setTimeout(() => {
+    initMap()
+  }, 800)
+  
+  // 清空图片列表
+  imgList.value = {
+    cover: [],
+    share: []
+  }
+  
+  // 如果是编辑模式,调用接口获取展会详情
+  if (options.action === 'edit' && options.row && options.row.id) {
+    try {
+      const { data: detailData } = await api.getById(options.row.id)
+      if (detailData) {
+        // 回显封面图
+        if (detailData.imageUrl) {
+          imgList.value.cover = [{
+            url: detailData.imageUrl,
+            remoteUrl: detailData.imageUrl
+          }]
+        }
+        
+        // 如果详情中有pavilion信息,将其合并到展馆选项列表中
+        let pavilionId = ''
+        if (detailData.pavilion) {
+          const existingPavilion = pavilionOptions.value.find(p => p.id === detailData.pavilion.id)
+          if (!existingPavilion) {
+            pavilionOptions.value.unshift(detailData.pavilion)
+          }
+          pavilionId = detailData.pavilion.id
+        }
+        
+        // 如果详情中有artistList信息,将其合并到艺术家选项列表中
+        let artistId = []
+        if (detailData.artistList && Array.isArray(detailData.artistList)) {
+          detailData.artistList.forEach(artist => {
+            const existingArtist = artistOptions.value.find(a => a.id === artist.id)
+            if (!existingArtist) {
+              artistOptions.value.unshift(artist)
+            }
+          })
+          artistId = detailData.artistList.map(artist => artist.id)
+        }
+        
+        // 处理日期范围回显
+        let dateRange = null
+        if (detailData.openTime && typeof detailData.openTime === 'string') {
+          const dateRangeParts = detailData.openTime.split(' - ')
+          if (dateRangeParts.length === 2) {
+            const startDate = new Date(dateRangeParts[0])
+            const endDate = new Date(dateRangeParts[1])
+            if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
+              dateRange = [startDate.getTime(), endDate.getTime()]
+            }
+          }
+        }
+        
+        // 更新options中的row数据为接口返回的完整数据
+        options.row = {
+          ...options.row,
+          id: detailData.id,
+          name: detailData.name || '',
+          dateRange: dateRange,
+          startTime: detailData.startTime || null,
+          endTime: detailData.endTime || null,
+          statusText: detailData.statusText || '',
+          pavilionId: pavilionId,
+          artistId: artistId,
+          address: detailData.address || '',
+          description: detailData.description || '',
+          coverImageUrl: detailData.imageUrl || '',
+          shareImageUrl: detailData.relayUrl || '',
+          display: detailData.display === 1,
+          setTop: detailData.setTop === 'A',
+          isBomb: detailData.isBomb === 1,
+          hot: detailData.hot === 1,
+          latitude: detailData.latitude || 0,
+          longitude: detailData.longitude || 0,
+          city: detailData.city || ''
+        }
+      }
+    } catch (error) {
+      console.error('获取展会详情失败:', error)
+      $message.error('获取展会详情失败')
+    }
+  }
+  
+  // 调用原始的handleOpen方法
+  originalHandleOpen(options)
+}
+
 const columns = [
   {
     title: 'ID',
@@ -159,44 +669,52 @@ const columns = [
     width: 80,
   },
   {
-    title: '展览名称',
+    title: '图片',
+    key: 'imageUrl',
+    width: 120,
+    align: 'center',
+    render: (row) => {
+      const imageUrl = row.imageUrl && row.imageUrl.startsWith('http')
+        ? row.imageUrl
+        : `${import.meta.env.VITE_COS_BASE_URL}/${row.imageUrl}`
+      return h(NImage, {
+        width: 80,
+        height: 60,
+        src: imageUrl,
+        style: 'object-fit: cover;',
+      })
+    },
+  },
+  {
+    title: '展会名称',
     key: 'name',
-    width: 150,
+    width: 270,
     ellipsis: { tooltip: true },
   },
   {
-    title: '展览状态',
-    key: 'status',
-    width: 100,
-    render: ({ status }) => {
-      return h(
-        NTag,
-        { type: status === '已上架' ? 'success' : 'warning' },
-        { default: () => status },
-      )
-    },
+    title: '展会状态',
+    key: 'statusText',
+    width: 100
   },
   {
     title: '展览时间',
-    key: 'exhibitionTime',
+    key: 'openTime',
     width: 200,
-    render(row) {
-      if (row.exhibitionTime && row.exhibitionTime.length === 2) {
-        return `${formatDateTime(row.exhibitionTime[0], 'YYYY.MM.DD')} - ${formatDateTime(row.exhibitionTime[1], 'YYYY.MM.DD')}`
-      }
-      return '-'
-    }
+    ellipsis: { tooltip: true },
   },
   {
     title: '展馆',
-    key: 'museum',
+    key: 'pavilionName',
     width: 200,
     ellipsis: { tooltip: true },
   },
   {
-    title: '展览资源',
-    key: 'resource',
-    width: 100,
+    title: '是否置顶',
+    key: 'setTop',
+    width: 120,
+    render: ({ setTop }) => {
+      return setTop == 'I' ? '是' : '否'
+    },
   },
   {
     title: '操作',
@@ -212,7 +730,7 @@ const columns = [
           {
             size: 'small',
             type: 'primary',
-            onClick: () => handleOpen({ action: 'edit', title: '编辑展览', row }),
+            onClick: () => handleEdit(row),
           },
           {
             default: () => '编辑',
@@ -237,17 +755,17 @@ const columns = [
   },
 ]
 
-async function handleEnable(row) {
-  row.enableLoading = true
+async function handleOnline(row) {
+  row.onlineLoading = true
   try {
-    await api.update({ id: row.id, status: row.status === '已上架' ? '待审核' : '已上架' })
-    row.enableLoading = false
+    await api.update({ id: row.id})
+    row.onlineLoading = false
     $message.success('操作成功')
     $table.value?.handleSearch()
   }
   catch (error) {
     console.error(error)
-    row.enableLoading = false
+    row.onlineLoading = false
   }
 }
 </script>

+ 19 - 7
src/views/kzhanManage/OnlineExhibition/api.js

@@ -1,11 +1,23 @@
 import { request } from '@/utils'
 
 export default {
-  create: data => request.post('/user', data),
-  read: (params = {}) => request.get('/user', { params }),
-  update: data => request.patch(`/user/${data.id}`, data),
-  delete: id => request.delete(`/user/${id}`),
-  resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
-
-  getAllRoles: () => request.get('/role?enable=1'),
+  create: data => request.post('/exhibition/add', data),
+  getById: id => request.get(`/exhibition/${id}`),
+  read: (params = {}) => {
+    // 添加online=1参数,表示线上展会
+    const queryParams = { ...params, online: 1 }
+    return request.get('/exhibition/page', { params: queryParams })
+  },
+  update: data => request.patch(`/exhibition/${data.id}`, data),
+  delete: id => request.delete(`/exhibition/${id}`),
+  // 获取OSS上传配置
+  getOssInfo: (fileNum = 1) => request.get('/file/oss/info', { params: { fileNum } }),
+  // 上传文件到OSS
+  uploadToOss: async (actionUrl, file) => {
+    const response = await fetch(actionUrl, {
+      method: 'PUT',
+      body: file,
+    })
+    return response
+  },
 }

+ 355 - 76
src/views/kzhanManage/OnlineExhibition/index.vue

@@ -14,87 +14,152 @@
       :columns="columns"
       :get-data="api.read"
     >
-      <MeQueryItem label="展名称" :label-width="70">
+      <MeQueryItem label="展名称" :label-width="70">
         <n-input
           v-model:value="queryItems.name"
           type="text"
-          placeholder="请输入展名称"
+          placeholder="请输入展名称"
           clearable
         />
       </MeQueryItem>
-
-      <MeQueryItem label="展览状态" :label-width="70">
-        <n-select
-          v-model:value="queryItems.enable"
-          clearable
-          :options="[
-            { label: '已下架', value: 0 },
-            { label: '已上架', value: 1 },
-          ]"
-        />
-      </MeQueryItem>
-
-      <MeQueryItem label="展馆" :label-width="50">
-        <n-input
-          v-model:value="queryItems.museum"
-          type="text"
-          placeholder="请输入展馆"
-          clearable
-        />
-      </MeQueryItem>
-
-      <div class="flex justify-end">
-        <NButton v-permission="'findOnline'" type="primary" @click="$table?.handleSearch()">
-          <template #icon>
-            <i class="i-material-symbols:search mr-2 text-16" />
-          </template>
-          查找
-        </NButton>
-      </div>
     </MeCrud>
 
-    <MeModal ref="modalRef" width="520px">
+    <MeModal ref="modalRef" width="720px">
       <n-form
         ref="modalFormRef"
         label-placement="left"
         label-align="left"
-        :label-width="80"
+        :label-width="100"
         :model="modalForm"
         :disabled="modalAction === 'view'"
       >
         <n-form-item
-          label="展名称"
+          label="展会名称"
           path="name"
           :rule="{
             required: true,
-            message: '请输入展名称',
+            message: '请输入展会名称',
             trigger: ['input', 'blur'],
           }"
         >
-          <n-input v-model:value="modalForm.name" />
+          <n-input v-model:value="modalForm.name" placeholder="请输入展会名称" />
         </n-form-item>
+        
         <n-form-item
-          label="展馆位置"
-          path="museum"
+          label="场景连接"
+          path="sceneUrl"
           :rule="{
             required: true,
-            message: '请输入展馆位置',
+            message: '请输入展会名称',
             trigger: ['input', 'blur'],
           }"
         >
-          <n-input v-model:value="modalForm.museum" />
+          <n-input v-model:value="modalForm.sceneUrl" placeholder="请输入场景连接" />
+        </n-form-item>
+        
+        <n-form-item
+          label="展馆"
+          path="pavilionId"
+          :rule="{
+            required: true,
+            type: 'number',
+            message: '请选择展馆',
+            trigger: ['change', 'blur'],
+          }"
+        >
+          <n-select
+            v-model:value="modalForm.pavilionId"
+            :options="pavilionOptions"
+            :loading="pavilionLoading"
+            filterable
+            remote
+            clearable
+            placeholder="请选择展馆"
+            :on-search="handlePavilionSearch"
+            label-field="name"
+            value-field="id"
+          />
+        </n-form-item>
+
+        <n-form-item label="展会封面图" path="coverImageUrl">
+          <div class="flex items-start gap-4">
+            <NUpload
+              class="w-200px text-center"
+              :custom-request="(options) => handleUpload(options, 'cover')"
+              :show-file-list="false"
+              accept=".png,.jpg,.jpeg"
+              @before-upload="onBeforeUpload"
+            >
+              <NUploadDragger>
+                <div class="h-60 f-c-c flex-col">
+                  <i class="i-mdi:upload mb-4 text-32 color-primary" />
+                  <NText class="text-12 color-gray">
+                    点击或拖拽上传封面图
+                  </NText>
+                </div>
+              </NUploadDragger>
+            </NUpload>
+            <div v-if="imgList.cover && imgList.cover.length" class="ml-20">
+              <NImage width="100" :src="imgList.cover[0].url" />
+            </div>
+          </div>
         </n-form-item>
 
-        <n-form-item label="状态" path="enable">
-          <NSwitch v-model:value="modalForm.enable">
+        <n-form-item label="转发图" path="shareImageUrl">
+          <div class="flex items-start gap-4">
+            <NUpload
+              class="w-200px text-center"
+              :custom-request="(options) => handleUpload(options, 'share')"
+              :show-file-list="false"
+              accept=".png,.jpg,.jpeg"
+              @before-upload="onBeforeUpload"
+            >
+              <NUploadDragger>
+                <div class="h-60 f-c-c flex-col">
+                  <i class="i-mdi:upload mb-4 text-32 color-primary" />
+                  <NText class="text-12 color-gray">
+                    点击或拖拽上传转发图
+                  </NText>
+                </div>
+              </NUploadDragger>
+            </NUpload>
+            <div v-if="imgList.share && imgList.share.length" class="ml-20">
+              <NImage width="100" :src="imgList.share[0].url" />
+            </div>
+          </div>
+        </n-form-item>
+
+        <n-form-item label="状态" path="display">
+          <n-radio-group v-model:value="modalForm.display">
+            <n-radio :value="true">上架</n-radio>
+            <n-radio :value="false">下架</n-radio>
+          </n-radio-group>
+        </n-form-item>
+        
+        <n-form-item label="置顶" path="setTop">
+          <NSwitch v-model:value="modalForm.setTop">
             <template #checked>
-              上架
+              
             </template>
             <template #unchecked>
-              下架
+              
             </template>
           </NSwitch>
         </n-form-item>
+        <n-form-item label="弹窗" path="isBomb">
+          <NSwitch v-model:value="modalForm.isBomb">
+            <template #checked>
+              是
+            </template>
+            <template #unchecked>
+              否
+            </template>
+          </NSwitch>
+        </n-form-item>
+        
+        <n-form-item label="排序" path="sort">
+          <n-input-number v-model:value="modalForm.sort" placeholder="请输入排序" :min="0" />
+        </n-form-item>
       </n-form>
     </MeModal>
   </CommonPage>
@@ -104,9 +169,10 @@
 import { MeCrud, MeModal, MeQueryItem } from '@/components'
 import { useCrud } from '@/composables'
 import { withPermission } from '@/directives'
-import { formatDateTime } from '@/utils'
-import { NButton, NImage, NSwitch, NTag } from 'naive-ui'
+import { formatDateTime, request } from '@/utils'
+import { NButton, NImage, NSwitch, NTag, NSelect, NUpload, NUploadDragger, NText, NRadio, NRadioGroup, NInputNumber } from 'naive-ui'
 import api from './api'
+import pavilionApi from '../GalleryMgt/api'
 
 defineOptions({ name: 'OnlineExhibition' })
 
@@ -114,8 +180,110 @@ const $table = ref(null)
 /** QueryBar筛选参数(可选) */
 const queryItems = ref({})
 
+// 展馆相关状态
+const pavilionOptions = ref([])
+const pavilionLoading = ref(false)
+
+// 图片上传相关状态
+const imgList = ref({
+  cover: [],
+  share: []
+})
+
+// 防抖搜索展馆
+let searchTimer = null
+const handlePavilionSearch = (query) => {
+  if (searchTimer) {
+    clearTimeout(searchTimer)
+  }
+  searchTimer = setTimeout(() => {
+    loadPavilionOptions(query)
+  }, 300)
+}
+
+// 加载展馆选项
+const loadPavilionOptions = async (name = '') => {
+  pavilionLoading.value = true
+  try {
+    const params = {
+      pageSize: 20,
+      pageNo: 1
+    }
+    if (name) {
+      params.name = name,
+      params.pageSize = 100
+    }
+    const { data } = await pavilionApi.read(params)
+    pavilionOptions.value = data.pageData || []
+  } catch (error) {
+    console.error('加载展馆列表失败:', error)
+    pavilionOptions.value = []
+  } finally {
+    pavilionLoading.value = false
+  }
+}
+
+// 上传前验证
+function onBeforeUpload({ file }) {
+  if (!file.file?.type.startsWith('image/')) {
+    $message.error('只能上传图片')
+    return false
+  }
+  return true
+}
+
+// 处理文件上传
+async function handleUpload(options, type) {
+  const { file } = options
+  try {
+    // 1. 获取OSS上传配置
+    const ossInfoResponse = await api.getOssInfo(1)
+    if (!ossInfoResponse.data || !ossInfoResponse.data.length) {
+      throw new Error('获取OSS配置失败')
+    }
+    
+    const ossInfo = ossInfoResponse.data[0]
+    const { actionUrl, fileName } = ossInfo
+    
+    // 2. 上传文件到OSS
+    const uploadResponse = await api.uploadToOss(actionUrl, file.file)
+    
+    if (uploadResponse.ok) {
+       // 3. 构建文件URL
+       const dir = ossInfo.dir
+       const fileName = ossInfo.fileName
+       const remoteUrl = dir + fileName
+       const localUrl = URL.createObjectURL(file.file)
+       
+       // 更新对应类型的图片列表
+       imgList.value[type] = [{
+         url: localUrl,
+         remoteUrl: remoteUrl
+       }]
+       
+       // 更新表单数据
+       if (type === 'cover') {
+         modalForm.value.coverImageUrl = remoteUrl
+       } else if (type === 'share') {
+         modalForm.value.shareImageUrl = remoteUrl
+       }
+      
+      $message.success('图片上传成功')
+      options.onFinish()
+    } else {
+      throw new Error('OSS上传失败')
+    }
+  } catch (error) {
+    console.error('图片上传失败', error)
+    $message.error('图片上传失败:' + (error.message || '未知错误'))
+    options.onError()
+  }
+}
+
 onMounted(() => {
   $table.value?.handleSearch()
+  // 初始加载展馆选项
+  loadPavilionOptions()
 })
 
 const {
@@ -125,17 +293,127 @@ const {
   modalAction,
   handleAdd,
   handleDelete,
-  handleOpen,
+  handleOpen: originalHandleOpen,
   handleSave,
 } = useCrud({
-  name: '在线展览',
-  initForm: { enable: true },
-  doCreate: api.create,
+  name: '线上展会',
+  initForm: { 
+    display: true, 
+    setTop: false, 
+    isBomb: false,
+    sort: 0,
+    sceneUrl: '',
+    coverImageUrl: '',
+    shareImageUrl: ''
+  },
+  doCreate: (formData) => {
+    const apiData = {
+      display: formData.display ? 1 : 0,
+      fee: "",
+      imageUrl: formData.coverImageUrl || "",
+      isBomb: formData.isBomb ? 1 : 0,
+      name: formData.name || "",
+      online: 1,
+      pavilionId: formData.pavilionId || 0,
+      relayUrl: formData.shareImageUrl || "",
+      sceneLink: formData.sceneUrl || "",
+      setTop: formData.setTop ? "A" : "I",
+      sort: formData.sort || 0
+    }
+    return api.create(apiData)
+  },
   doDelete: api.delete,
-  doUpdate: api.update,
+  doUpdate: (formData) => {
+    const apiData = {
+      id: formData.id,
+      display: formData.display ? 1 : 0,
+      fee: "",
+      imageUrl: formData.coverImageUrl || "",
+      isBomb: formData.isBomb ? 1 : 0,
+      name: formData.name || "",
+      online: 1,
+      pavilionId: formData.pavilionId || 0,
+      relayUrl: formData.shareImageUrl || "",
+      sceneLink: formData.sceneUrl || "",
+      setTop: formData.setTop ? "A" : "I",
+      sort: formData.sort || 0
+    }
+    return api.update(apiData)
+  },
   refresh: () => $table.value?.handleSearch(),
 })
 
+// 重写handleOpen方法,处理编辑时的数据获取和回显
+async function handleOpen(options) {
+  // 重新加载展馆选项
+  loadPavilionOptions()
+  
+  // 清空图片列表
+  imgList.value = {
+    cover: [],
+    share: []
+  }
+  
+  // 如果是编辑模式,调用接口获取展会详情
+  if (options.action === 'edit' && options.row && options.row.id) {
+    try {
+      const { data: detailData } = await api.getById(options.row.id)
+      if (detailData) {
+        // 回显封面图
+        if (detailData.imageUrl) {
+          imgList.value.cover = [{
+            url: detailData.imageUrl.startsWith('http')
+              ? detailData.imageUrl
+              : `${import.meta.env.VITE_COS_BASE_URL}/${detailData.imageUrl}`,
+            remoteUrl: detailData.imageUrl
+          }]
+        }
+        
+        // 回显转发图
+        if (detailData.relayUrl) {
+          imgList.value.share = [{
+            url: detailData.relayUrl.startsWith('http')
+              ? detailData.relayUrl
+              : `${import.meta.env.VITE_COS_BASE_URL}/${detailData.relayUrl}`,
+            remoteUrl: detailData.relayUrl
+          }]
+        }
+        
+        // 如果详情中有pavilion信息,将其合并到展馆选项列表中
+        let pavilionId = ''
+        if (detailData.pavilion) {
+          const existingPavilion = pavilionOptions.value.find(p => p.id === detailData.pavilion.id)
+          if (!existingPavilion) {
+            pavilionOptions.value.unshift(detailData.pavilion)
+          }
+          pavilionId = detailData.pavilion.id
+        }
+        
+        // 更新options中的row数据为接口返回的完整数据
+        options.row = {
+          ...options.row,
+          id: detailData.id,
+          name: detailData.name || '',
+          sceneUrl: detailData.sceneLink || '',
+          pavilionId: pavilionId,
+          coverImageUrl: detailData.imageUrl || '',
+          shareImageUrl: detailData.relayUrl || '',
+          display: detailData.display === 1,
+          setTop: detailData.setTop === 'I',
+          isBomb: detailData.isBomb === 1,
+          sort: detailData.sort || 0
+        }
+      }
+    } catch (error) {
+      console.error('获取展会详情失败:', error)
+      $message.error('获取展会详情失败')
+    }
+  }
+  
+  // 调用原始的handleOpen方法
+  originalHandleOpen(options)
+}
+
 const columns = [
   {
     title: 'ID',
@@ -143,43 +421,44 @@ const columns = [
     width: 80,
   },
   {
-    title: '展名称',
+    title: '展名称',
     key: 'name',
-    width: 150,
+    width: 270,
     ellipsis: { tooltip: true },
   },
   {
-    title: '展状态',
-    key: 'status',
+    title: '展状态',
+    key: 'display',
     width: 100,
-    render: ({ status }) => {
+    render: ({ display }) => {
       return h(
         NTag,
-        { type: status === '已下架' ? 'warning' : 'success' },
-        { default: () => status },
+        { type: display ? 'success' : 'warning' },
+        { default: () => display ? '上架中' : '已下架' },
       )
     },
   },
   {
     title: '展馆',
-    key: 'museum',
+    key: 'pavilionName',
     width: 200,
     ellipsis: { tooltip: true },
   },
   {
-    title: '展览收费',
-    key: 'fee',
-    width: 100,
-  },
-  {
-    title: '展览资源',
-    key: 'resource',
-    width: 100,
+    title: '是否置顶',
+    key: 'setTop',
+    width: 60,
+    render: ({ setTop }) => {
+      return setTop == 'I' ? '是' : '否'
+    },
   },
   {
-    title: '展览评价',
-    key: 'rating',
-    width: 100,
+    title: '是否弹窗',
+    key: 'isBomb',
+    width: 60,
+    render: ({ isBomb }) => {
+      return isBomb ? '是' : '否'
+    },
   },
   {
     title: '操作',
@@ -195,7 +474,7 @@ const columns = [
           {
             size: 'small',
             type: 'primary',
-            onClick: () => handleOpen({ action: 'edit', title: '编辑展', row }),
+            onClick: () => handleOpen({ action: 'edit', title: '编辑展', row }),
           },
           {
             default: () => '编辑',
@@ -220,17 +499,17 @@ const columns = [
   },
 ]
 
-async function handleEnable(row) {
-  row.enableLoading = true
+async function handleOnline(row) {
+  row.onlineLoading = true
   try {
-    await api.update({ id: row.id, enable: !row.enable })
-    row.enableLoading = false
+    await api.update({ id: row.id})
+    row.onlineLoading = false
     $message.success('操作成功')
     $table.value?.handleSearch()
   }
   catch (error) {
     console.error(error)
-    row.enableLoading = false
+    row.onlineLoading = false
   }
 }
 </script>

+ 22 - 7
src/views/kzhanManage/OrderMgt/api.js

@@ -1,11 +1,26 @@
 import { request } from '@/utils'
 
 export default {
-  create: data => request.post('/user', data),
-  read: (params = {}) => request.get('/user', { params }),
-  update: data => request.patch(`/user/${data.id}`, data),
-  delete: id => request.delete(`/user/${id}`),
-  resetPwd: (id, data) => request.patch(`/user/password/reset/${id}`, data),
-
-  getAllRoles: () => request.get('/role?enable=1'),
+  read: (params = {}) => {
+    const requestParams = {
+      pageNo: params.pageNo || 1,
+      pageSize: params.pageSize || 10,
+      orderTimeStart: params.orderTimeStart || '',
+      orderTimeEnd: params.orderTimeEnd || '',
+      paymentStatus: params.paymentStatus !== undefined ? params.paymentStatus : 0,
+      searchText: params.searchText || '',
+      searchType: params.searchType || ''
+    }
+    return request.get('/order/page', { params: requestParams })
+  },
+  // 获取OSS上传配置
+  getOssInfo: (fileNum = 1) => request.get('/file/oss/info', { params: { fileNum } }),
+  // 上传文件到OSS
+  uploadToOss: async (actionUrl, file) => {
+    const response = await fetch(actionUrl, {
+      method: 'PUT',
+      body: file,
+    })
+    return response
+  },
 }

+ 78 - 163
src/views/kzhanManage/OrderMgt/index.vue

@@ -1,12 +1,5 @@
 <template>
   <CommonPage>
-    <template #action>
-      <NButton v-permission="'addOnline'" type="primary" @click="handleAdd()">
-        <i class="i-material-symbols:add mr-4 text-18" />
-        新增
-      </NButton>
-    </template>
-
     <MeCrud
       ref="$table"
       v-model:query-items="queryItems"
@@ -14,127 +7,62 @@
       :columns="columns"
       :get-data="api.read"
     >
-      <MeQueryItem label="展览名称" :label-width="70">
-        <n-input
-          v-model:value="queryItems.name"
-          type="text"
-          placeholder="请输入展览名称"
-          clearable
-        />
+      <MeQueryItem label="选择日期" :label-width="70" style="width: 340px;">
+        <n-date-picker v-model:value="range" type="daterange" clearable style="width: 280px; min-width: 280px;" />
       </MeQueryItem>
 
-      <MeQueryItem label="展览状态" :label-width="70">
+      <MeQueryItem label="支付状态" :label-width="70">
         <n-select
-          v-model:value="queryItems.enable"
+          v-model:value="queryItems.paymentStatus"
           clearable
+          placeholder="请选择支付状态"
           :options="[
-            { label: '已下架', value: 0 },
-            { label: '已上架', value: 1 },
+            { label: '全部', value: null },
+            { label: '未支付', value: 0 },
+            { label: '已支付', value: 1 },
+            { label: '已取消', value: 2 },
           ]"
         />
       </MeQueryItem>
 
-      <MeQueryItem label="展馆" :label-width="50">
+      <MeQueryItem label="搜索" :label-width="50">
         <n-input
-          v-model:value="queryItems.museum"
+          v-model:value="queryItems.searchText"
           type="text"
-          placeholder="请输入展馆"
+          placeholder="请输入搜索内容"
+          style="width: 200px;"
           clearable
         />
       </MeQueryItem>
-
-      <div class="flex justify-end">
-        <NButton v-permission="'findOnline'" type="primary" @click="$table?.handleSearch()">
-          <template #icon>
-            <i class="i-material-symbols:search mr-2 text-16" />
-          </template>
-          查找
-        </NButton>
-      </div>
     </MeCrud>
-
-    <MeModal ref="modalRef" width="520px">
-      <n-form
-        ref="modalFormRef"
-        label-placement="left"
-        label-align="left"
-        :label-width="80"
-        :model="modalForm"
-        :disabled="modalAction === 'view'"
-      >
-        <n-form-item
-          label="展览名称"
-          path="name"
-          :rule="{
-            required: true,
-            message: '请输入展览名称',
-            trigger: ['input', 'blur'],
-          }"
-        >
-          <n-input v-model:value="modalForm.name" />
-        </n-form-item>
-        <n-form-item
-          label="展馆位置"
-          path="museum"
-          :rule="{
-            required: true,
-            message: '请输入展馆位置',
-            trigger: ['input', 'blur'],
-          }"
-        >
-          <n-input v-model:value="modalForm.museum" />
-        </n-form-item>
-
-        <n-form-item label="状态" path="enable">
-          <NSwitch v-model:value="modalForm.enable">
-            <template #checked>
-              上架
-            </template>
-            <template #unchecked>
-              下架
-            </template>
-          </NSwitch>
-        </n-form-item>
-      </n-form>
-    </MeModal>
   </CommonPage>
 </template>
 
 <script setup>
-import { MeCrud, MeModal, MeQueryItem } from '@/components'
-import { useCrud } from '@/composables'
-import { withPermission } from '@/directives'
+import { MeCrud, MeQueryItem } from '@/components'
 import { formatDateTime } from '@/utils'
-import { NButton, NImage, NSwitch, NTag } from 'naive-ui'
+import { NButton, NTag, NAvatar } from 'naive-ui'
 import api from './api'
 
-defineOptions({ name: 'OnlineExhibition' })
+defineOptions({ name: 'OrderManagement' })
 
 const $table = ref(null)
-/** QueryBar筛选参数(可选) */
-const queryItems = ref({})
+const range = ref(null)
+
+/** QueryBar筛选参数 */
+const queryItems = ref({
+  paymentStatus: null,
+  searchText: '',
+  searchType: '',
+  orderTimeStart: '',
+  orderTimeEnd: ''
+})
 
 onMounted(() => {
   $table.value?.handleSearch()
 })
 
-const {
-  modalRef,
-  modalFormRef,
-  modalForm,
-  modalAction,
-  handleAdd,
-  handleDelete,
-  handleOpen,
-  handleSave,
-} = useCrud({
-  name: '在线展览',
-  initForm: { enable: true },
-  doCreate: api.create,
-  doDelete: api.delete,
-  doUpdate: api.update,
-  refresh: () => $table.value?.handleSearch(),
-})
+
 
 const columns = [
   {
@@ -143,94 +71,81 @@ const columns = [
     width: 80,
   },
   {
-    title: '展览名称',
-    key: 'name',
-    width: 150,
+    title: '订单编号',
+    key: 'orderSn',
+    width: 180,
     ellipsis: { tooltip: true },
   },
   {
-    title: '展览状态',
-    key: 'status',
+    title: '订单状态',
+    key: 'paymentStatus',
     width: 100,
-    render: ({ status }) => {
+    render: ({ paymentStatus }) => {
+      const statusMap = {
+        0: { text: '未支付', type: 'warning' },
+        1: { text: '已支付', type: 'success' },
+        2: { text: '已取消', type: 'error' }
+      }
+      const status = statusMap[paymentStatus] || { text: '未知', type: 'default' }
       return h(
         NTag,
-        { type: status === '已下架' ? 'warning' : 'success' },
-        { default: () => status },
+        { type: status.type },
+        { default: () => status.text },
       )
     },
   },
   {
-    title: '展馆',
-    key: 'museum',
+    title: '商品',
+    key: 'productName',
     width: 200,
     ellipsis: { tooltip: true },
   },
   {
-    title: '展览收费',
-    key: 'fee',
-    width: 100,
+    title: '下单时间',
+    key: 'orderTime',
+    width: 160,
+    render: ({ orderTime }) => {
+      return orderTime ? formatDateTime(orderTime) : '-'
+    },
   },
   {
-    title: '展览资源',
-    key: 'resource',
+    title: '单价',
+    key: 'price',
     width: 100,
+    render: ({ price }) => {
+      return price
+    },
   },
   {
-    title: '展览评价',
-    key: 'rating',
-    width: 100,
+    title: '数量',
+    key: 'quantity',
+    width: 80,
   },
   {
-    title: '操作',
-    key: 'actions',
-    width: 200,
-    align: 'right',
-    fixed: 'right',
-    hideInExcel: true,
-    render(row) {
-      return [
-        h(
-          NButton,
-          {
-            size: 'small',
-            type: 'primary',
-            onClick: () => handleOpen({ action: 'edit', title: '编辑展览', row }),
-          },
-          {
-            default: () => '编辑',
-            icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
-          },
-        ),
-        h(
-          NButton,
-          {
-            size: 'small',
-            type: 'error',
-            style: 'margin-left: 12px;',
-            onClick: () => handleDelete(row.id),
-          },
-          {
-            default: () => '删除',
-            icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
-          },
-        ),
-      ]
+    title: '购买用户',
+    key: 'nickName',
+    width: 150,
+    render: ({ nickName, avatarUrl }) => {
+      return h('div', { class: 'flex items-center gap-2' }, [
+        h(NAvatar, {
+          size: 'small',
+          src: avatarUrl || '/src/assets/images/avatar.jpg',
+          fallbackSrc: '/src/assets/images/avatar.jpg'
+        }),
+        h('span', nickName || '微信用户')
+      ])
     },
   },
 ]
 
-async function handleEnable(row) {
-  row.enableLoading = true
-  try {
-    await api.update({ id: row.id, enable: !row.enable })
-    row.enableLoading = false
-    $message.success('操作成功')
-    $table.value?.handleSearch()
-  }
-  catch (error) {
-    console.error(error)
-    row.enableLoading = false
+// 监听日期范围变化,更新查询参数但不自动搜索
+watch(range, (newRange) => {
+  if (newRange && newRange.length === 2) {
+    queryItems.value.orderTimeStart = new Date(newRange[0]).toISOString().split('T')[0]
+    queryItems.value.orderTimeEnd = new Date(newRange[1]).toISOString().split('T')[0]
+  } else {
+    queryItems.value.orderTimeStart = ''
+    queryItems.value.orderTimeEnd = ''
   }
-}
+}, { deep: true })
 </script>

+ 28 - 0
src/views/modelManage/api.js

@@ -0,0 +1,28 @@
+import { request } from '@/utils'
+
+export default {
+  create: data => request.post('/antique/add', data),
+  getById: id => request.get(`/antique/${id}`),
+  read: (params = {}) => {
+    const queryParams = {
+      name: params.name || '',
+      uploadTimeStart: params.uploadTimeStart || '',
+      uploadTimeEnd: params.uploadTimeEnd || '',
+      pageNo: params.pageNo || 1,
+      pageSize: params.pageSize || 10
+    }
+    return request.get('/antique/page', { params: queryParams })
+  },
+  update: data => request.patch(`/antique/${data.id}`, data),
+  delete: id => request.delete(`/antique/${id}`),
+  // 获取OSS上传配置
+  getOssInfo: (fileNum = 1) => request.get('/file/oss/info', { params: { fileNum } }),
+  // 上传文件到OSS
+  uploadToOss: async (actionUrl, file) => {
+    const response = await fetch(actionUrl, {
+      method: 'PUT',
+      body: file,
+    })
+    return response
+  },
+}

+ 450 - 0
src/views/modelManage/index.vue

@@ -0,0 +1,450 @@
+<template>
+  <CommonPage>
+    <MeCrud
+      ref="$table"
+      v-model:query-items="queryItems"
+      :scroll-x="1400"
+      :columns="columns"
+      :get-data="api.read"
+    >
+      <MeQueryItem label="文物名称" :label-width="70">
+        <n-input
+          v-model:value="queryItems.name"
+          type="text"
+          placeholder="请输入文物名称"
+          clearable
+        />
+      </MeQueryItem>
+      <MeQueryItem label="上传日期" :label-width="70">
+        <n-date-picker
+          v-model:value="range"
+          type="daterange"
+          clearable
+          placeholder="选择上传日期范围"
+          style="width: 280px;"
+        />
+      </MeQueryItem>
+    </MeCrud>
+
+    <MeModal ref="modalRef" width="600px">
+      <n-form
+        ref="modalFormRef"
+        label-placement="left"
+        label-align="left"
+        :label-width="100"
+        :model="modalForm"
+        :disabled="modalAction === 'view'"
+      >
+        <n-form-item
+          label="文物名称"
+          path="name"
+          :rule="{
+            required: true,
+            message: '请输入文物名称',
+            trigger: ['input', 'blur'],
+          }"
+        >
+          <n-input v-model:value="modalForm.name" placeholder="请输入文物名称" />
+        </n-form-item>
+        
+        <n-form-item label="文物图片" path="coverImgUrl">
+          <div class="flex items-start gap-4">
+            <NUpload
+              class="w-200px text-center"
+              :custom-request="handleUpload"
+              :show-file-list="false"
+              accept=".png,.jpg,.jpeg"
+              @before-upload="onBeforeUpload"
+            >
+              <NUploadDragger>
+                <div class="h-60 f-c-c flex-col">
+                  <i class="i-mdi:upload mb-4 text-32 color-primary" />
+                  <NText class="text-12 color-gray">
+                    点击或拖拽上传文物图片
+                  </NText>
+                </div>
+              </NUploadDragger>
+            </NUpload>
+            <div v-if="imgList.cover && imgList.cover.length" class="ml-20">
+              <NImage width="100" :src="imgList.cover[0].url" />
+            </div>
+          </div>
+        </n-form-item>
+
+        <n-form-item label="审核状态" path="audit">
+          <n-select
+            v-model:value="modalForm.audit"
+            :options="[
+              { label: '待审核', value: 0 },
+              { label: '通过', value: 1 },
+              { label: '不通过', value: 2 }
+            ]"
+            placeholder="请选择审核状态"
+          />
+        </n-form-item>
+
+        <n-form-item label="是否热门" path="hot">
+          <n-switch 
+            v-model:value="modalForm.hot" 
+            :checked-value="1" 
+            :unchecked-value="0"
+            checked-text="是" 
+            unchecked-text="否"
+          />
+        </n-form-item>
+
+        <n-form-item label="显示状态" path="display">
+          <n-switch 
+            v-model:value="modalForm.display" 
+            :checked-value="1" 
+            :unchecked-value="0"
+            checked-text="显示" 
+            unchecked-text="隐藏"
+          />
+        </n-form-item>
+        
+        <n-form-item label="排序" path="sort">
+          <n-input-number v-model:value="modalForm.sort" placeholder="请输入排序" :min="0" />
+        </n-form-item>
+      </n-form>
+    </MeModal>
+  </CommonPage>
+</template>
+
+<script setup>
+import { MeCrud, MeModal, MeQueryItem } from '@/components'
+import { useCrud } from '@/composables'
+import { withPermission } from '@/directives'
+import { formatDateTime, useOssUpload } from '@/utils'
+import { NButton, NImage, NSwitch, NTag, NSelect, NUpload, NUploadDragger, NText, NRadio, NRadioGroup, NInputNumber, NDatePicker, NAvatar } from 'naive-ui'
+import api from './api'
+
+defineOptions({ name: 'AntiqueManage' })
+
+const $table = ref(null)
+/** QueryBar筛选参数(可选) */
+const queryItems = ref({
+  uploadTimeStart: '',
+  uploadTimeEnd: ''
+})
+
+// 日期范围选择
+const range = ref(null)
+
+// 图片列表状态
+const imgList = ref({
+  cover: []
+})
+
+// 使用OSS上传Hook
+const { handleNUpload, initOssConfig } = useOssUpload()
+
+// 监听日期范围变化
+watch(range, (newRange) => {
+  if (newRange && newRange.length === 2) {
+    // 使用本地时间格式化日期,避免时区问题
+    const startDate = new Date(newRange[0])
+    const endDate = new Date(newRange[1])
+    
+    queryItems.value.uploadTimeStart = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')}`
+    queryItems.value.uploadTimeEnd = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')}`
+  } else {
+    queryItems.value.uploadTimeStart = ''
+    queryItems.value.uploadTimeEnd = ''
+  }
+})
+
+// 上传前验证
+function onBeforeUpload({ file }) {
+  if (!file.file?.type.startsWith('image/')) {
+    $message.error('只能上传图片')
+    return false
+  }
+  return true
+}
+
+// 处理文件上传
+async function handleUpload(uploadOptions) {
+  try {
+    const result = await handleNUpload(uploadOptions, 'image/*')
+    
+    if (result) {
+      // 更新图片列表
+      imgList.value.cover.splice(0, imgList.value.cover.length)
+      imgList.value.cover.push({
+        id: uploadOptions.file.id,
+        name: uploadOptions.file.name,
+        url: result.localUrl,
+        remoteUrl: result.remoteUrl,
+      })
+      
+      // 更新表单数据
+      modalForm.value.coverImgUrl = result.remoteUrl
+    }
+  } catch (error) {
+    console.error('上传失败:', error)
+  }
+}
+
+
+
+onMounted(() => {
+  $table.value?.handleSearch()
+})
+
+// 审核功能
+function handleAudit(row) {
+  $dialog.warning({
+    title: '审核确认',
+    content: '确定将此3D模型审核通过?',
+    positiveText: '确定',
+    negativeText: '取消',
+    onPositiveClick: async () => {
+      try {
+        const auditData = {
+          id: row.id,
+          address: row.address || '',
+          audit: 1,
+          description: row.description || '',
+          display: row.display || 0,
+          name: row.name || ''
+        }
+        await api.update(auditData)
+        $message.success('审核通过成功')
+        $table.value?.handleSearch()
+      } catch (error) {
+        $message.error('审核失败')
+        console.error('审核失败:', error)
+      }
+    }
+  })
+}
+
+const {
+  modalRef,
+  modalFormRef,
+  modalForm,
+  modalAction,
+  handleAdd,
+  handleDelete,
+  handleOpen: originalHandleOpen,
+  handleSave,
+} = useCrud({
+  name: '文物',
+  initForm: { 
+    name: '',
+    coverImgUrl: '',
+    audit: 0,
+    hot: 0,
+    display: 1,
+    sort: 0
+  },
+  doCreate: api.create,
+  doDelete: api.delete,
+  doUpdate: api.update,
+  refresh: () => $table.value?.handleSearch(),
+})
+
+// 重写handleOpen以处理图片回显
+async function handleOpen(options) {
+  const {action, title, row} = options
+  
+  // 初始化OSS配置
+  await initOssConfig()
+  
+  // 重置图片列表
+  imgList.value.cover = []
+  
+  // 如果是编辑模式,调用API获取详细数据
+  if (action === 'edit' && row && row.id) {
+    try {
+      const response = await api.getById(row.id)
+      if (response.data) {
+        const data = response.data
+        
+        // 更新表单数据
+        Object.assign(modalForm.value, {
+          name: data.name || '',
+          coverImgUrl: data.coverImgUrl || '',
+          audit: data.audit || 0,
+          hot: data.hot || 0,
+          display: data.display || 1,
+          sort: data.sort || 0
+        })
+        
+        // 回显图片
+        if (data.coverImgUrl) {
+          // 处理图片URL,如果不是完整URL则添加基础路径
+          const imageUrl = data.coverImgUrl.startsWith('http') 
+            ? data.coverImgUrl 
+            : `${import.meta.env.VITE_COS_BASE_URL}/${data.coverImgUrl}`
+          imgList.value.cover = [{
+            url: imageUrl,
+            remoteUrl: data.coverImgUrl
+          }]
+        }
+      }
+    } catch (error) {
+      console.error('获取文物详情失败:', error)
+      $message.error('获取文物详情失败')
+    }
+  }
+  
+  // 调用原始的handleOpen
+  originalHandleOpen(options)
+}
+
+
+
+const columns = [
+  {
+    title: 'ID',
+    key: 'id',
+    width: 80,
+  },
+  {
+    title: '图片',
+    key: 'coverImgUrl',
+    width: 100,
+    render: (row) => {
+      const coverImgUrl = row.coverImgUrl && row.coverImgUrl.startsWith('http')
+        ? row.coverImgUrl
+        : `${import.meta.env.VITE_COS_BASE_URL}/${row.coverImgUrl}`
+      return h(NImage, {
+        width: 80,
+        height: 60,
+        src: coverImgUrl,
+        style: 'object-fit: cover;',
+      })
+    },
+  },
+  {
+    title: '文物名称',
+    key: 'name',
+    width: 200,
+    ellipsis: { tooltip: true },
+  },
+  {
+    title: '文物状态',
+    key: 'audit',
+    width: 100,
+    render: ({ audit }) => {
+      const statusMap = {
+        0: { text: '待审核', type: 'warning' },
+        1: { text: '通过', type: 'success' },
+        2: { text: '不通过', type: 'error' }
+      }
+      const status = statusMap[audit] || { text: '未知', type: 'default' }
+      return h(
+        NTag,
+        { type: status.type },
+        { default: () => status.text }
+      )
+    },
+  },
+  {
+    title: '是否热门',
+    key: 'hot',
+    width: 100,
+    render: ({ hot }) => {
+      return h(
+        NTag,
+        { type: hot === 1 ? 'success' : 'default' },
+        { default: () => hot === 1 ? '是' : '否' }
+      )
+    },
+  },
+  {
+    title: '显示状态',
+    key: 'display',
+    width: 100,
+    render: ({ display }) => {
+      return h(
+        NTag,
+        { type: display === 1 ? 'success' : 'warning' },
+        { default: () => display === 1 ? '显示' : '隐藏' }
+      )
+    },
+  },
+  {
+    title: '排序',
+    key: 'sort',
+    width: 80,
+  },
+  {
+    title: '上传日期',
+    key: 'createTime',
+    width: 150,
+    render: ({ createTime }) => {
+      return createTime ? formatDateTime(createTime) : '-'
+    },
+  },
+  {
+    title: '操作',
+    key: 'actions',
+    width: 250,
+    align: 'right',
+    fixed: 'right',
+    hideInExcel: true,
+    render(row) {
+      const buttons = []
+      
+      // 只有当audit不等于1时才显示审核按钮
+      if (row.audit !== 1) {
+        buttons.push(
+          h(
+            NButton,
+            {
+              size: 'small',
+              type: 'info',
+              onClick: () => handleAudit(row),
+            },
+            {
+              default: () => '审核',
+              icon: () => h('i', { class: 'i-material-symbols:check-circle-outline text-14' }),
+            },
+          )
+        )
+      }
+      
+      // 编辑按钮
+      buttons.push(
+        h(
+          NButton,
+          {
+            size: 'small',
+            type: 'primary',
+            style: 'margin-left: 8px;',
+            onClick: () => handleOpen({ action: 'edit', title: '编辑文物', row}),
+          },
+          {
+            default: () => '编辑',
+            icon: () => h('i', { class: 'i-material-symbols:edit-outline text-14' }),
+          },
+        )
+      )
+      
+      // 删除按钮
+      buttons.push(
+        h(
+          NButton,
+          {
+            size: 'small',
+            type: 'error',
+            style: 'margin-left: 8px;',
+            onClick: () => handleDelete(row.id),
+          },
+          {
+            default: () => '删除',
+            icon: () => h('i', { class: 'i-material-symbols:delete-outline text-14' }),
+          },
+        )
+      )
+      
+      return buttons
+    },
+  },
+]
+
+
+</script>