Pārlūkot izejas kodu

fix:新增图片字段

null 1 mēnesi atpakaļ
vecāks
revīzija
71684abad7

+ 290 - 25
src/views/subSystem/basic/Equipment.vue

@@ -14,6 +14,11 @@
         <el-form-item label="制造商">
           <el-input v-model="searchForm.manufacturer" placeholder="请输入制造商" clearable style="width:160px" />
         </el-form-item>
+        <el-form-item label="所属片区">
+          <el-select v-model="searchForm.district" placeholder="请选择所属片区" clearable style="width:120px">
+            <el-option v-for="opt in districtOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+          </el-select>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" @click="handleSearch">查询</el-button>
           <el-button @click="handleReset">重置</el-button>
@@ -25,6 +30,26 @@
     <el-card class="table-card">
       <el-table :data="tableData" border stripe v-loading="loading" style="width: 100%">
         <el-table-column prop="equipmentCode" label="设备编码" min-width="130" />
+        <el-table-column prop="district" label="所属片区" min-width="100">
+          <template #default="{ row }">
+            <span>{{ row.district || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="设备图片" width="100">
+          <template #default="{ row }">
+            <div v-if="row.equipmentImage" class="table-image-cell">
+              <el-image
+                :src="row.equipmentImage"
+                fit="cover"
+                :preview-src-list="[row.equipmentImage]"
+                preview-teleported
+                hide-on-click-pdf
+                class="equipment-table-image"
+              />
+            </div>
+            <el-tag v-else type="info" size="small">无图</el-tag>
+          </template>
+        </el-table-column>
         <el-table-column prop="equipmentName" label="设备名称" min-width="160" />
         <el-table-column prop="equipmentModel" label="设备型号" width="110" />
         <el-table-column prop="manufacturer" label="制造商" width="110" />
@@ -84,6 +109,7 @@
         <el-descriptions-item label="设备编码" :span="1">{{ detailData.equipmentCode }}</el-descriptions-item>
         <el-descriptions-item label="设备名称" :span="1">{{ detailData.equipmentName }}</el-descriptions-item>
         <el-descriptions-item label="设备型号">{{ detailData.equipmentModel }}</el-descriptions-item>
+        <el-descriptions-item label="设备类别">{{ detailData.typeName || '-' }}</el-descriptions-item>
         <el-descriptions-item label="规格参数">{{ detailData.equipmentSpec }}</el-descriptions-item>
         <el-descriptions-item label="制造商">{{ detailData.manufacturer }}</el-descriptions-item>
         <el-descriptions-item label="生产日期">{{ detailData.productionDate }}</el-descriptions-item>
@@ -113,6 +139,20 @@
         </el-descriptions-item>
         <el-descriptions-item label="维护人员">{{ detailData.maintainer || '-' }}</el-descriptions-item>
         <el-descriptions-item label="联系方式">{{ detailData.maintainerPhone || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="所属片区">{{ detailData.district || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="经度">{{ detailData.longitude || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="纬度">{{ detailData.latitude || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="设备图片" :span="2">
+          <el-image
+            v-if="detailData.equipmentImage"
+            :src="detailData.equipmentImage"
+            fit="cover"
+            :preview-src-list="[detailData.equipmentImage]"
+            preview-teleported
+            class="detail-equipment-image"
+          />
+          <span v-else>-</span>
+        </el-descriptions-item>
         <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
       </el-descriptions>
     </el-dialog>
@@ -120,28 +160,42 @@
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="1100px" destroy-on-close>
       <el-form :model="formData" :rules="rules" ref="formRef" label-width="110px" size="default">
         <el-row :gutter="28">
+          <el-col :span="12">
+            <el-form-item label="所属片区" prop="district">
+              <el-select v-model="formData.district" placeholder="请选择所属片区" style="width:100%" @change="onDistrictChange">
+                <el-option v-for="opt in districtOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
           <el-col :span="12">
             <el-form-item label="设备编码" prop="equipmentCode">
-              <el-input v-model="formData.equipmentCode" placeholder="请输入设备编码" />
+              <el-input v-model="formData.equipmentCode" placeholder="保存时自动生成" readonly />
             </el-form-item>
           </el-col>
+        </el-row>
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="设备名称" prop="equipmentName">
               <el-input v-model="formData.equipmentName" placeholder="请输入设备名称" />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="设备型号">
               <el-input v-model="formData.equipmentModel" />
             </el-form-item>
           </el-col>
+        </el-row>
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="规格参数">
               <el-input v-model="formData.equipmentSpec" />
             </el-form-item>
           </el-col>
+          <el-col :span="12">
+            <el-form-item label="制造商">
+              <el-input v-model="formData.manufacturer" />
+            </el-form-item>
+          </el-col>
         </el-row>
         <el-row :gutter="28">
           <el-col :span="12">
@@ -151,25 +205,18 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="制造商">
-              <el-input v-model="formData.manufacturer" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="安装位置">
               <el-input v-model="formData.equipmentLocation" />
             </el-form-item>
           </el-col>
+        </el-row>
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="维护人员">
               <el-input v-model="formData.maintainer" />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="所属管线">
               <el-select v-model="formData.networkId" placeholder="请选择管线" clearable style="width:100%" @change="onNetworkChange">
@@ -177,6 +224,8 @@
               </el-select>
             </el-form-item>
           </el-col>
+        </el-row>
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="关联管点">
               <el-select v-model="formData.pointId" placeholder="请先选择管线" clearable style="width:100%">
@@ -184,20 +233,18 @@
               </el-select>
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="经度">
               <el-input-number v-model="formData.longitude" :precision="10" style="width:100%" />
             </el-form-item>
           </el-col>
+        </el-row>
+        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="纬度">
               <el-input-number v-model="formData.latitude" :precision="10" style="width:100%" />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row :gutter="28">
           <el-col :span="12">
             <el-form-item label="联系方式">
               <el-input v-model="formData.maintainerPhone" />
@@ -207,6 +254,29 @@
         <el-form-item label="备注">
           <el-input v-model="formData.remark" type="textarea" :rows="3" />
         </el-form-item>
+        <el-form-item label="设备图片">
+          <el-upload
+            class="image-uploader"
+            action="#"
+            :auto-upload="false"
+            :show-file-list="false"
+            :http-request="handleUploadRequest"
+            :on-change="handleImageChange"
+            :on-remove="handleImageRemove"
+            accept="image/jpeg,image/png,image/jpg"
+          >
+            <div v-if="formData.equipmentImage || equipmentImagePreview">
+              <img :src="equipmentImagePreview || formData.equipmentImage" alt="设备图片" class="preview-image" />
+              <div class="image-uploader-cover">
+                <el-icon><Plus /></el-icon>
+              </div>
+            </div>
+            <div v-else class="image-uploader-placeholder">
+              <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+              <div>点击上传图片</div>
+            </div>
+          </el-upload>
+        </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="dialogVisible = false">取消</el-button>
@@ -219,32 +289,62 @@
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, UploadFilled } from '@element-plus/icons-vue'
+import request from '@/utils/request'
 import { getEquipmentFullPage, addEquipment, updateEquipment, deleteEquipment, getEquipmentTypeList, getPipeNetworkOptions, getGasPipePointPage } from '@/api/pipeNetwork/basic'
 
+const baseApi = import.meta.env.VITE_APP_BASE_API
+
 const loading = ref(false)
 const tableData = ref([])
 const pageNum = ref(1)
 const pageSize = ref(10)
 const total = ref(0)
-const searchForm = reactive({ equipmentCode: '', equipmentName: '', equipmentModel: '', manufacturer: '' })
+const searchForm = reactive({ equipmentCode: '', equipmentName: '', equipmentModel: '', manufacturer: '', district: '' })
 const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
 const formRef = ref(null)
+
 const rules = {
-  equipmentCode: [{ required: true, message: '请输入设备编码', trigger: 'blur' }],
   equipmentName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
-  equipmentTypeId: [{ required: true, message: '请选择设备类别', trigger: 'change' }]
+  equipmentTypeId: [{ required: true, message: '请选择设备类别', trigger: 'change' }],
+  district: [{ required: true, message: '请选择所属片区', trigger: 'change' }]
 }
+
 const detailVisible = ref(false)
 const detailData = ref(null)
 const equipmentTypeOptions = ref([])
 const networkOptions = ref([])
 const pointOptions = ref([])
+const equipmentImagePreview = ref('')
+let equipmentImageFile = null
+
+const districtOptions = [
+  { label: '城南', value: '城南', prefix: 'CN' },
+  { label: '城北', value: '城北', prefix: 'CB' },
+  { label: '太常', value: '太常', prefix: 'TC' }
+]
+
+const generateEquipmentCode = () => {
+  if (!formData.district) return ''
+  const opt = districtOptions.find(o => o.value === formData.district)
+  if (opt && opt.prefix) {
+    const now = new Date()
+    const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`
+    const timeStr = `${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`
+    return `${opt.prefix}-GAS-${dateStr}${timeStr}`
+  }
+  return ''
+}
+
 const formData = reactive({
-  equipmentCode: '', equipmentName: '', equipmentModel: '',
+  district: '',
+  equipmentCode: '',
+  equipmentName: '', equipmentModel: '',
   equipmentSpec: '', manufacturer: '', equipmentLocation: '', equipmentTypeId: '',
   networkId: '', pointId: '',
-  longitude: null, latitude: null, maintainer: '', maintainerPhone: '', remark: ''
+  longitude: null, latitude: null, maintainer: '', maintainerPhone: '', remark: '',
+  equipmentImage: ''
 })
 
 async function loadData() {
@@ -257,7 +357,7 @@ async function loadData() {
 }
 function handleSearch() { pageNum.value = 1; loadData() }
 function handleReset() {
-  Object.assign(searchForm, { equipmentCode: '', equipmentName: '', equipmentModel: '', manufacturer: '' })
+  Object.assign(searchForm, { equipmentCode: '', equipmentName: '', equipmentModel: '', manufacturer: '', district: '' })
   handleSearch()
 }
 async function loadEquipmentTypes() {
@@ -283,17 +383,82 @@ function onNetworkChange(val) {
   formData.pointId = ''
   if (val) { loadPointOptions(val) } else { pointOptions.value = [] }
 }
+function onDistrictChange() {
+  // 当片区变更时,如果正在新增模式下,自动生成设备编码
+  if (!formData.equipmentId && formData.district) {
+    formData.equipmentCode = generateEquipmentCode()
+  }
+}
+/** 覆盖 el-upload 默认上传行为,不做任何请求 */
+function handleUploadRequest() {
+  // 空函数,阻止 el-upload 默认的 action 请求
+}
+
+/** 图片选择后处理(验证 + 预览) */
+function handleImageChange(file) {
+  if (!file.raw) return
+  const isImage = file.raw.type === 'image/jpeg' || file.raw.type === 'image/png' || file.raw.type === 'image/jpg'
+  if (!isImage) {
+    ElMessage.error('只能上传图片文件 (JPG/PNG)!')
+    return
+  }
+  const isLt5M = file.raw.size / 1024 / 1024 < 5
+  if (!isLt5M) {
+    ElMessage.error('图片大小不能超过 5MB!')
+    return
+  }
+  equipmentImageFile = file.raw
+  const reader = new FileReader()
+  reader.readAsDataURL(file.raw)
+  reader.onload = () => {
+    equipmentImagePreview.value = reader.result
+  }
+}
+
+/** 图片删除 */
+function handleImageRemove() {
+  equipmentImageFile = null
+  equipmentImagePreview.value = ''
+  formData.equipmentImage = ''
+}
+
 function handleAdd() {
   dialogTitle.value = '新增'
-  const template = { equipmentCode: '', equipmentName: '', equipmentModel: '', equipmentSpec: '', manufacturer: '', equipmentLocation: '', equipmentTypeId: '', networkId: '', pointId: '', longitude: null, latitude: null, maintainer: '', maintainerPhone: '', remark: '' }
+  const template = { 
+    equipmentCode: '', 
+    equipmentName: '', 
+    equipmentModel: '', 
+    equipmentSpec: '', 
+    manufacturer: '', 
+    equipmentLocation: '', 
+    equipmentTypeId: '', 
+    networkId: '', 
+    pointId: '', 
+    longitude: null, 
+    latitude: null, 
+    maintainer: '', 
+    maintainerPhone: '', 
+    remark: '',
+    district: '',
+    equipmentImage: ''
+  }
   if (formData.equipmentId) delete formData.equipmentId
   Object.assign(formData, template)
+  equipmentImageFile = null
+  equipmentImagePreview.value = ''
   dialogVisible.value = true
   loadEquipmentTypes(); loadNetworkOptions()
 }
 function handleEdit(row) {
   dialogTitle.value = '编辑'
   Object.assign(formData, row)
+  // 如果有已有图片,直接使用返回的 URL 生成预览
+  if (row.equipmentImage) {
+    equipmentImagePreview.value = row.equipmentImage
+  } else {
+    equipmentImagePreview.value = ''
+  }
+  equipmentImageFile = null
   dialogVisible.value = true
   loadEquipmentTypes(); loadNetworkOptions()
   if (row.networkId) loadPointOptions(row.networkId)
@@ -301,7 +466,23 @@ function handleEdit(row) {
 async function handleSave() {
   const valid = await formRef.value.validate().catch(() => false)
   if (!valid) return
+  
   try {
+    // 如果有新图片,先通过 MinIO 通用接口上传到 MinIO
+    if (equipmentImageFile) {
+      const uploadFormData = new FormData()
+      uploadFormData.append('file', equipmentImageFile)
+      uploadFormData.append('dir', 'upload') // 指定存储目录(可选)
+      const uploadResponse = await request({
+        url: '/minio/upload',
+        method: 'post',
+        data: uploadFormData
+      })
+      formData.equipmentImage = uploadResponse.url || uploadResponse.fileName
+      // 上传完成后清空文件,避免重复上传
+      equipmentImageFile = null
+    }
+    
     const isEdit = !!formData.equipmentId
     if (isEdit) {
       await updateEquipment(formData)
@@ -309,12 +490,13 @@ async function handleSave() {
       const res = await addEquipment(formData)
       formData.equipmentId = res.data // 后端 saveWithCheck 返回实体 UUID
       if (!formData.equipmentId) {
-        ElMessage.error('新增失败,未获取到设备ID')
+        ElMessage.error('新增失败,未获取到设备 ID')
         return
       }
     }
     ElMessage.success(isEdit ? '修改成功' : '新增成功')
-    dialogVisible.value = false; loadData()
+    dialogVisible.value = false
+    loadData()
   } catch (e) {
     const msg = e?.response?.data?.msg || e?.message || '操作失败'
     ElMessage.error(msg)
@@ -337,4 +519,87 @@ onMounted(() => { loadData() })
 .equipment-container { padding: 16px; }
 .search-card { margin-bottom: 16px; }
 .search-form { display: flex; flex-wrap: wrap; }
+
+/* 表格图片样式 */
+.table-image-cell {
+  width: 80px;
+}
+
+.equipment-table-image {
+  width: 64px;
+  height: 64px;
+  border-radius: 4px;
+  cursor: pointer;
+  object-fit: cover;
+}
+
+/* 详情图片样式 */
+.detail-equipment-image {
+  width: 200px;
+  height: 200px;
+  border-radius: 6px;
+  cursor: pointer;
+  object-fit: cover;
+}
+
+/* 图片上传样式 */
+.image-uploader {
+  position: relative;
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  width: 200px;
+  height: 200px;
+}
+
+.image-uploader:hover {
+  border-color: #409eff;
+}
+
+.image-uploader .preview-image {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
+  border-radius: 6px;
+}
+
+.image-uploader-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  color: #909399;
+  font-size: 14px;
+}
+
+.image-uploader-placeholder .el-icon--upload {
+  font-size: 28px;
+  margin-bottom: 8px;
+}
+
+.image-uploader-cover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0;
+  transition: opacity 0.3s;
+  border-radius: 6px;
+}
+
+.image-uploader:hover .image-uploader-cover {
+  opacity: 1;
+}
+
+.image-uploader-cover .el-icon {
+  color: #fff;
+  font-size: 28px;
+}
 </style>

+ 176 - 3
src/views/subSystem/basic/RepairRecord.vue

@@ -71,6 +71,20 @@
         <el-table-column prop="repairCompany" label="维修单位" min-width="160" show-overflow-tooltip />
         <el-table-column prop="repairCost" label="费用(元)" width="110" align="right" />
         <el-table-column prop="repairPerson" label="负责人" width="100" />
+        <el-table-column label="图片" width="80">
+          <template #default="{ row }">
+            <template v-if="parseImageUrls(row.repairImages).length">
+              <el-image
+                :src="getImageUrl(parseImageUrls(row.repairImages)[0])"
+                fit="cover"
+                :preview-src-list="parseImageUrls(row.repairImages).map(getImageUrl)"
+                preview-teleported
+                class="table-image"
+              />
+            </template>
+            <el-tag v-else type="info" size="small">无</el-tag>
+          </template>
+        </el-table-column>
         <el-table-column prop="createTime" label="创建时间" width="170" />
         <el-table-column label="操作" width="150" fixed="right">
           <template #default="{ row }">
@@ -108,12 +122,43 @@
         <el-form-item label="费用(元)"><el-input-number v-model="formData.repairCost" :precision="2" :step="100" style="width:100%" /></el-form-item>
         <el-form-item label="负责人"><el-input v-model="formData.repairPerson" /></el-form-item>
         <el-form-item label="备注"><el-input v-model="formData.remark" type="textarea" :rows="2" /></el-form-item>
+        <el-form-item label="维修图片">
+          <el-upload
+            action="#"
+            :auto-upload="false"
+            :show-file-list="false"
+            :http-request="() => {}"
+            :on-change="handleImageChange"
+            accept="image/jpeg,image/png,image/jpg"
+            multiple
+          >
+            <el-button type="primary" plain>选择图片</el-button>
+            <template #tip>
+              <div class="el-upload__tip">支持 JPG/PNG 格式,单张不超过 5MB</div>
+            </template>
+          </el-upload>
+          <div v-if="imageList.length" class="image-list">
+            <div v-for="(img, index) in imageList" :key="index" class="image-item">
+              <img :src="img" class="image-thumb" @click="previewImage(index)" />
+              <el-icon class="image-remove" @click="removeImage(index)"><Close /></el-icon>
+            </div>
+          </div>
+        </el-form-item>
       </el-form>
       <template #footer>
         <el-button @click="dialogVisible = false">取消</el-button>
         <el-button type="primary" @click="handleSubmit">确定</el-button>
       </template>
     </el-dialog>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="previewVisible"
+      :url-list="previewList"
+      :initial-index="previewIndex"
+      hide-on-click-modal
+      @close="previewVisible = false"
+    />
   </div>
 </template>
 
@@ -121,9 +166,13 @@
 import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
 import * as echarts from 'echarts'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { Close } from '@element-plus/icons-vue'
+import request from '@/utils/request'
 import { convertCoord } from '@/utils/coordTransform'
 import { getPipeNetworkOptions, getRepairRecordPage, getRepairStats, addRepairRecord, updateRepairRecord, deleteRepairRecord } from '@/api/pipeNetwork/basic'
 
+const baseApi = import.meta.env.VITE_APP_BASE_API
+
 // ==================== 数据 ====================
 const loading = ref(false)
 const pageNum = ref(1)
@@ -133,7 +182,7 @@ const searchForm = reactive({ networkId: '', repairType: '' })
 const dialogVisible = ref(false)
 const dialogTitle = ref('新增')
 const formRef = ref(null)
-const formData = reactive({ repairId: '', networkId: '', repairDate: '', repairType: '', repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: '' })
+const formData = reactive({ repairId: '', networkId: '', repairDate: '', repairType: '', repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: '', repairImages: '' })
 
 const repairTypeDict = ref([
   { dictValue: '1', dictLabel: '抢修' },
@@ -184,6 +233,66 @@ const pipePointData = [
 // ==================== 维修记录数据 ====================
 const tableData = ref([])
 
+// 图片相关
+const imageList = ref([])        // 展示用预览列表(URL)
+const imageUrls = ref([])         // 已上传的图片URL列表
+const imageUploading = ref(false) // 上传中状态
+const previewVisible = ref(false)
+const previewList = ref([])
+const previewIndex = ref(0)
+
+/** 获取图片展示 URL(直接使用上传接口返回的完整 URL) */
+function getImageUrl(url) {
+  if (!url) return ''
+  return url.trim()
+}
+
+/** 将存储的 repairImages 字符串转为有效 URL 数组 */
+function parseImageUrls(repairImages) {
+  if (!repairImages) return []
+  return repairImages.split(',').map(s => s.trim()).filter(Boolean)
+}
+
+/** 图片选择后立即上传到 MinIO */
+async function handleImageChange(file) {
+  if (!file.raw) return
+  const isImage = file.raw.type === 'image/jpeg' || file.raw.type === 'image/png' || file.raw.type === 'image/jpg'
+  if (!isImage) { ElMessage.error('只能上传图片文件 (JPG/PNG)!'); return }
+  if (file.raw.size / 1024 / 1024 >= 5) { ElMessage.error('图片大小不能超过 5MB!'); return }
+
+  imageUploading.value = true
+  try {
+    const uploadFormData = new FormData()
+    uploadFormData.append('file', file.raw)
+    uploadFormData.append('dir', 'upload') // 指定存储目录(可选)
+    const uploadResponse = await request({
+      url: '/minio/upload',
+      method: 'post',
+      data: uploadFormData
+    })
+    const url = uploadResponse.url || uploadResponse.fileName
+    imageUrls.value.push(url)
+    imageList.value.push(getImageUrl(url))
+  } catch (e) {
+    ElMessage.error('图片上传失败')
+  } finally {
+    imageUploading.value = false
+  }
+}
+
+/** 删除图片 */
+function removeImage(index) {
+  imageUrls.value.splice(index, 1)
+  imageList.value.splice(index, 1)
+}
+
+/** 预览大图 */
+function previewImage(index) {
+  previewList.value = [...imageList.value]
+  previewIndex.value = index
+  previewVisible.value = true
+}
+
 const pressureColors = { high: '#e63946', medium: '#f77f00', low: '#2a9d8f' }
 const repairedNetworkIds = computed(() => { const s = new Set(); tableData.value.forEach(r => s.add(r.networkId)); return s })
 
@@ -419,12 +528,25 @@ function handleReset() {
 }
 function handleAdd() {
   dialogTitle.value = '新增'; dialogVisible.value = true
-  Object.assign(formData, { repairId: '', networkId: '', repairDate: '', repairType: '', repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: '' })
+  Object.assign(formData, { repairId: '', networkId: '', repairDate: '', repairType: '', repairContent: '', repairCompany: '', repairCost: null, repairPerson: '', remark: '', repairImages: '' })
+  imageList.value = []
+  imageUrls.value = []
+  loadNetworkOptions()
+}
+function handleEdit(row) {
+  dialogTitle.value = '编辑'; dialogVisible.value = true
+  Object.assign(formData, { ...row })
+  // 回显已有图片
+  const urls = parseImageUrls(row.repairImages)
+  imageUrls.value = [...urls]
+  imageList.value = urls.map(url => getImageUrl(url))
   loadNetworkOptions()
 }
-function handleEdit(row) { dialogTitle.value = '编辑'; dialogVisible.value = true; Object.assign(formData, { ...row }); loadNetworkOptions() }
 async function handleSubmit() {
   try {
+    if (imageUploading.value) { ElMessage.warning('图片正在上传,请稍候'); return }
+    // 将图片URL列表拼接为逗号分隔字符串
+    formData.repairImages = imageUrls.value.join(',')
     if (formData.repairId) { await updateRepairRecord(formData); ElMessage.success('修改成功') }
     else { await addRepairRecord(formData); ElMessage.success('新增成功') }
     dialogVisible.value = false; loadData(); nextTick(() => renderMapScene())
@@ -465,6 +587,57 @@ onBeforeUnmount(() => {
 <style scoped>
 .repair-container { padding: 16px; display: flex; flex-direction: column; gap: 16px; background: #f0f2f6; min-height: calc(100vh - 84px); }
 
+/* 多图上传样式 */
+.table-image {
+  width: 40px;
+  height: 40px;
+  border-radius: 4px;
+  cursor: pointer;
+}
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-top: 8px;
+}
+.image-item {
+  position: relative;
+  width: 80px;
+  height: 80px;
+  border-radius: 4px;
+  overflow: visible;
+}
+.image-thumb {
+  width: 80px;
+  height: 80px;
+  object-fit: cover;
+  border-radius: 4px;
+  border: 1px solid #dcdfe6;
+  cursor: pointer;
+}
+.image-thumb:hover {
+  border-color: #409eff;
+}
+.image-remove {
+  position: absolute;
+  top: -6px;
+  right: -6px;
+  width: 18px;
+  height: 18px;
+  background: #f56c6c;
+  color: #fff;
+  border-radius: 50%;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  z-index: 1;
+}
+.image-remove:hover {
+  background: #e64a4a;
+}
+
 /* ========== 上半部分 ========== */
 .top-section { display: flex; gap: 16px; align-items: stretch; }
 .topo-card { flex: 1; min-width: 0; display: flex; flex-direction: column; }

+ 264 - 6
src/views/subSystem/hidden/HazardList.vue

@@ -61,6 +61,21 @@
             <el-tag :type="statusTag(row.hazardStatus)">{{ getDictLabel(hazardStatusDict, row.hazardStatus) }}</el-tag>
           </template>
         </el-table-column>
+        <el-table-column label="隐患图片" width="90">
+          <template #default="{ row }">
+            <template v-if="parseImageUrls(row.hazardImages).length">
+              <el-image
+                :src="parseImageUrls(row.hazardImages)[0]"
+                fit="cover"
+                :preview-src-list="parseImageUrls(row.hazardImages)"
+                preview-teleported
+                hide-on-click-modal
+                class="table-image"
+              />
+            </template>
+            <el-tag v-else type="info" size="small">无</el-tag>
+          </template>
+        </el-table-column>
         <el-table-column label="关联管网" width="140">
           <template #default="{ row }">{{ getNetworkName(row.networkId) }}</template>
         </el-table-column>
@@ -69,9 +84,10 @@
         </el-table-column>
         <el-table-column prop="reportUnit" label="上报单位" width="120" />
         <el-table-column prop="reportTime" label="上报时间" width="160" />
-        <el-table-column label="操作" width="260" fixed="right">
+        <el-table-column label="操作" width="320" fixed="right">
           <template #default="{ row }">
             <el-button type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
+            <el-button type="info" size="small" @click="handleDetail(row)">详情</el-button>
             <el-button type="warning" size="small" @click="handleSubmitHazard(row)" :disabled="row.isSubmitted === 1">
               {{ row.isSubmitted === 1 ? '已报送' : '报送' }}
             </el-button>
@@ -86,12 +102,13 @@
       />
     </el-card>
 
+    <!-- 新增/编辑对话框 -->
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="750px" destroy-on-close>
       <el-form :model="formData" ref="formRef" label-width="100px">
         <el-row :gutter="16">
           <el-col :span="12">
             <el-form-item label="隐患编号" prop="hazardNo">
-              <el-input v-model="formData.hazardNo" placeholder="请输入隐患编号" />
+              <el-input v-model="formData.hazardNo" placeholder="保存时自动生成" readonly />
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -169,6 +186,32 @@
               <el-input-number v-model="formData.latitude" :precision="10" style="width: 100%" />
             </el-form-item>
           </el-col>
+          <el-col :span="24">
+            <el-form-item label="隐患图片">
+              <el-upload
+                action="#"
+                :auto-upload="false"
+                :show-file-list="false"
+                :http-request="() => {}"
+                :on-change="handleImageChange"
+                accept="image/jpeg,image/png,image/jpg"
+                multiple
+              >
+                <el-button type="primary" plain :loading="imageUploading">
+                  {{ imageUploading ? '上传中...' : '选择图片' }}
+                </el-button>
+                <template #tip>
+                  <div class="el-upload__tip">支持 JPG/PNG 格式,单张不超过 5MB,可多选</div>
+                </template>
+              </el-upload>
+              <div v-if="imageList.length" class="image-list">
+                <div v-for="(img, index) in imageList" :key="index" class="image-item">
+                  <img :src="img" class="image-thumb" @click="previewImage(index)" />
+                  <el-icon class="image-remove" @click="removeImage(index)"><Close /></el-icon>
+                </div>
+              </div>
+            </el-form-item>
+          </el-col>
         </el-row>
       </el-form>
       <template #footer>
@@ -176,13 +219,69 @@
         <el-button type="primary" @click="handleSave">确定</el-button>
       </template>
     </el-dialog>
+
+    <!-- 详情对话框 -->
+    <el-dialog title="隐患详情" v-model="detailVisible" width="900px">
+      <el-descriptions v-if="detailData" :column="2" border>
+        <el-descriptions-item label="隐患编号">{{ detailData.hazardNo }}</el-descriptions-item>
+        <el-descriptions-item label="隐患类型">{{ getDictLabel(hazardTypeDict, detailData.hazardType) }}</el-descriptions-item>
+        <el-descriptions-item label="隐患等级">
+          <el-tag :type="levelTag(detailData.hazardLevel)">{{ getDictLabel(hazardLevelDict, detailData.hazardLevel) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="隐患来源">{{ getDictLabel(hazardSourceDict, detailData.hazardSource) }}</el-descriptions-item>
+        <el-descriptions-item label="状态">
+          <el-tag :type="statusTag(detailData.hazardStatus)">{{ getDictLabel(hazardStatusDict, detailData.hazardStatus) }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="报送状态">
+          <el-tag :type="detailData.isSubmitted === 1 ? 'success' : 'info'">{{ detailData.isSubmitted === 1 ? '已报送' : '未报送' }}</el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="关联管网">{{ getNetworkName(detailData.networkId) }}</el-descriptions-item>
+        <el-descriptions-item label="关联设备">{{ getEquipmentName(detailData.equipmentId) }}</el-descriptions-item>
+        <el-descriptions-item label="上报单位">{{ detailData.reportUnit || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="上报时间">{{ detailData.reportTime || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="隐患描述" :span="2">{{ detailData.hazardDesc || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="整改方案" :span="2">{{ detailData.rectifyPlan || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="整改期限">{{ detailData.rectifyDeadline || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="整改责任人">{{ detailData.rectifyPerson || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="经度">{{ detailData.longitude || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="纬度">{{ detailData.latitude || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="隐患图片" :span="2">
+          <div v-if="parseImageUrls(detailData.hazardImages).length" class="detail-image-list">
+            <el-image
+              v-for="(img, index) in parseImageUrls(detailData.hazardImages)"
+              :key="index"
+              :src="img"
+              fit="cover"
+              :preview-src-list="parseImageUrls(detailData.hazardImages)"
+              :initial-index="index"
+              preview-teleported
+              hide-on-click-modal
+              class="detail-image"
+            />
+          </div>
+          <span v-else>-</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
+      </el-descriptions>
+    </el-dialog>
+
+    <!-- 图片预览 -->
+    <el-image-viewer
+      v-if="previewVisible"
+      :url-list="previewList"
+      :initial-index="previewIndex"
+      hide-on-click-modal
+      @close="previewVisible = false"
+    />
   </div>
 </template>
 
 <script setup>
 import { ref, reactive, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
-import { getHazardPage, addHazard, updateHazard, deleteHazard, submitHazard } from '@/api/pipeNetwork/hazard'
+import { Close } from '@element-plus/icons-vue'
+import request from '@/utils/request'
+import { getHazardPage, getHazardById, addHazard, updateHazard, deleteHazard, submitHazard } from '@/api/pipeNetwork/hazard'
 import { getDicts } from '@/api/system/dict/data'
 import { getPipeNetworkOptions, getEquipmentByTopLevelType } from '@/api/pipeNetwork/basic'
 
@@ -199,7 +298,8 @@ const formData = reactive({
   hazardId: '', hazardNo: '', hazardType: '', hazardLevel: '', hazardDesc: '',
   hazardSource: '', hazardStatus: '2', reportUnit: '', reportTime: '',
   rectifyPlan: '', rectifyDeadline: '', rectifyPerson: '',
-  networkId: '', equipmentId: '', longitude: null, latitude: null
+  networkId: '', equipmentId: '', longitude: null, latitude: null,
+  hazardImages: ''
 })
 
 const hazardTypeDict = ref([])
@@ -209,6 +309,18 @@ const hazardSourceDict = ref([])
 const networkOptions = ref([])
 const equipmentOptions = ref([])
 
+// 详情相关
+const detailVisible = ref(false)
+const detailData = ref(null)
+
+// 图片相关
+const imageList = ref([])
+const imageUrls = ref([])
+const imageUploading = ref(false)
+const previewVisible = ref(false)
+const previewList = ref([])
+const previewIndex = ref(0)
+
 function getDictLabel(dict, value) {
   const item = dict.find(d => d.dictValue === value)
   return item ? item.dictLabel : value
@@ -223,6 +335,52 @@ function statusTag(value) {
   return map[value] || 'info'
 }
 
+/** 将存储的 hazardImages 字符串转为 URL 数组 */
+function parseImageUrls(hazardImages) {
+  if (!hazardImages) return []
+  return hazardImages.split(',').map(s => s.trim()).filter(Boolean)
+}
+
+/** 图片选择后立即上传到 MinIO */
+async function handleImageChange(file) {
+  if (!file.raw) return
+  const isImage = file.raw.type === 'image/jpeg' || file.raw.type === 'image/png' || file.raw.type === 'image/jpg'
+  if (!isImage) { ElMessage.error('只能上传图片文件 (JPG/PNG)!'); return }
+  if (file.raw.size / 1024 / 1024 >= 5) { ElMessage.error('图片大小不能超过 5MB!'); return }
+
+  imageUploading.value = true
+  try {
+    const uploadFormData = new FormData()
+    uploadFormData.append('file', file.raw)
+    uploadFormData.append('dir', 'upload')
+    const uploadResponse = await request({
+      url: '/minio/upload',
+      method: 'post',
+      data: uploadFormData
+    })
+    const url = uploadResponse.url || uploadResponse.fileName
+    imageUrls.value.push(url)
+    imageList.value.push(url)
+  } catch (e) {
+    ElMessage.error('图片上传失败')
+  } finally {
+    imageUploading.value = false
+  }
+}
+
+/** 删除图片 */
+function removeImage(index) {
+  imageUrls.value.splice(index, 1)
+  imageList.value.splice(index, 1)
+}
+
+/** 预览大图 */
+function previewImage(index) {
+  previewList.value = [...imageList.value]
+  previewIndex.value = index
+  previewVisible.value = true
+}
+
 async function loadDicts() {
   const [typeRes, levelRes, statusRes, sourceRes] = await Promise.all([
     getDicts('hazard_type'),
@@ -265,15 +423,37 @@ function getEquipmentName(id) {
   const found = equipmentOptions.value.find(e => e.equipmentId === id)
   return found ? found.equipmentName : id || '-'
 }
+/** 自动生成隐患编号:YH-YYYYMMDDHHmmss */
+function generateHazardNo() {
+  const now = new Date()
+  const pad = (n) => String(n).padStart(2, '0')
+  const dateStr = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
+  const timeStr = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
+  return `YH-${dateStr}${timeStr}`
+}
 function handleAdd() {
   dialogTitle.value = '新增'
-  Object.assign(formData, { hazardId: '', hazardNo: '', hazardType: '', hazardLevel: '', hazardDesc: '', hazardSource: '', hazardStatus: '2', reportUnit: '', reportTime: '', rectifyPlan: '', rectifyDeadline: '', rectifyPerson: '', networkId: '', equipmentId: '', longitude: null, latitude: null })
+  Object.assign(formData, { hazardId: '', hazardNo: generateHazardNo(), hazardType: '', hazardLevel: '', hazardDesc: '', hazardSource: '', hazardStatus: '2', reportUnit: '', reportTime: '', rectifyPlan: '', rectifyDeadline: '', rectifyPerson: '', networkId: '', equipmentId: '', longitude: null, latitude: null, hazardImages: '' })
+  imageList.value = []
+  imageUrls.value = []
+  dialogVisible.value = true
+  loadNetworkOptions(); loadEquipmentOptions()
+}
+function handleEdit(row) {
+  dialogTitle.value = '编辑'
+  Object.assign(formData, row)
+  // 回显已有图片
+  const urls = parseImageUrls(row.hazardImages)
+  imageUrls.value = [...urls]
+  imageList.value = [...urls]
   dialogVisible.value = true
   loadNetworkOptions(); loadEquipmentOptions()
 }
-function handleEdit(row) { dialogTitle.value = '编辑'; Object.assign(formData, row); dialogVisible.value = true; loadNetworkOptions(); loadEquipmentOptions() }
 async function handleSave() {
   try {
+    if (imageUploading.value) { ElMessage.warning('图片正在上传,请稍候'); return }
+    // 将图片URL列表拼接为逗号分隔字符串
+    formData.hazardImages = imageUrls.value.join(',')
     if (formData.hazardId) { await updateHazard(formData); ElMessage.success('修改成功') }
     else { await addHazard(formData); ElMessage.success('新增成功') }
     dialogVisible.value = false; loadData()
@@ -292,6 +472,17 @@ async function handleSubmitHazard(row) {
     await submitHazard(row.hazardId); ElMessage.success('报送成功'); loadData()
   }).catch(() => {})
 }
+async function handleDetail(row) {
+  try {
+    const res = await getHazardById(row.hazardId)
+    detailData.value = res.data || row
+    detailVisible.value = true
+  } catch (e) {
+    // 接口失败时降级使用表格行数据
+    detailData.value = row
+    detailVisible.value = true
+  }
+}
 
 onMounted(async () => { await loadDicts(); loadData(); loadNetworkOptions(); loadEquipmentOptions() })
 </script>
@@ -299,4 +490,71 @@ onMounted(async () => { await loadDicts(); loadData(); loadNetworkOptions(); loa
 <style scoped>
 .hazard-container { padding: 16px; }
 .search-card { margin-bottom: 16px; }
+
+/* 表格图片样式 */
+.table-image {
+  width: 40px;
+  height: 40px;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+/* 多图上传列表样式 */
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-top: 8px;
+}
+.image-item {
+  position: relative;
+  width: 80px;
+  height: 80px;
+  border-radius: 4px;
+  overflow: visible;
+}
+.image-thumb {
+  width: 80px;
+  height: 80px;
+  object-fit: cover;
+  border-radius: 4px;
+  border: 1px solid #dcdfe6;
+  cursor: pointer;
+}
+.image-thumb:hover {
+  border-color: #409eff;
+}
+.image-remove {
+  position: absolute;
+  top: -6px;
+  right: -6px;
+  width: 18px;
+  height: 18px;
+  background: #f56c6c;
+  color: #fff;
+  border-radius: 50%;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  z-index: 1;
+}
+.image-remove:hover {
+  background: #e64a4a;
+}
+
+/* 详情图片样式 */
+.detail-image-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+.detail-image {
+  width: 120px;
+  height: 120px;
+  border-radius: 6px;
+  cursor: pointer;
+  object-fit: cover;
+}
 </style>