| src/views/environmentAccess/components/PhotoCapture.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/intelligentInspectionManagement/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/remoteMonitor/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/vehicleInfoAudit/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/environmentAccess/vehicleInfoCollect/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/views/environmentAccess/components/PhotoCapture.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,147 @@ <template> <div class="photo-capture"> <div v-if="modelValue" class="photo-box"> <el-image :src="modelValue" :preview-src-list="[modelValue]" fit="cover" class="photo-img" /> <div class="photo-remove" @click.stop="remove"> <el-icon><Close /></el-icon> </div> </div> <div v-else class="photo-box photo-empty" @click="pick"> <el-icon class="photo-icon"><Camera /></el-icon> <span class="photo-text">{{ placeholder }}</span> </div> <input ref="inputRef" type="file" accept="image/*" capture="environment" class="photo-input" @change="onChange" /> </div> </template> <script setup> import { ref } from "vue"; import axios from "axios"; import { ElMessage } from "element-plus"; import { Camera, Close } from "@element-plus/icons-vue"; import { getToken } from "@/utils/auth"; defineOptions({ name: "PhotoCapture" }); const props = defineProps({ modelValue: { type: String, default: "" }, placeholder: { type: String, default: "æç § / ä¸ä¼ " }, }); const emit = defineEmits(["update:modelValue"]); const inputRef = ref(); const loading = ref(false); const MAX_SIZE = 10 * 1024 * 1024; const pick = () => { if (loading.value) return; inputRef.value && inputRef.value.click(); }; const remove = () => { emit("update:modelValue", ""); }; const onChange = async (e) => { const file = e.target.files && e.target.files[0]; e.target.value = ""; if (!file) return; if (!/^image\//.test(file.type)) { ElMessage.warning("è¯·éæ©å¾çæä»¶"); return; } if (file.size > MAX_SIZE) { ElMessage.warning("å¾çä¸è½è¶ è¿ 10MBï¼è¯·å缩åéè¯"); return; } loading.value = true; try { const fd = new FormData(); fd.append("file", file); const res = await axios.post(import.meta.env.VITE_APP_BASE_API + "/common/upload", fd, { headers: { Authorization: "Bearer " + getToken(), "Content-Type": "multipart/form-data", }, }); const body = res && res.data; if (body && body.code === 200) { emit("update:modelValue", body.url || body.fileName || ""); ElMessage.success("ä¸ä¼ æå"); } else { ElMessage.error((body && body.msg) || "ä¸ä¼ 失败"); } } catch (err) { ElMessage.error("ä¸ä¼ 失败ï¼è¯·æ£æ¥ç½ç»åéè¯"); } finally { loading.value = false; } }; </script> <style scoped lang="scss"> .photo-capture { width: 100%; } .photo-box { position: relative; width: 100%; height: 140px; border-radius: 6px; overflow: hidden; border: 1px solid #dcdfe6; background: #fafafa; box-sizing: border-box; } .photo-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; border-style: dashed; cursor: pointer; color: #909399; &:active { background: #f0f2f5; } } .photo-icon { font-size: 30px; margin-bottom: 6px; } .photo-text { font-size: 13px; } .photo-img { width: 100%; height: 140px; display: block; } .photo-remove { position: absolute; top: 4px; right: 4px; width: 24px; height: 24px; border-radius: 50%; background: rgba(0, 0, 0, 0.55); color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 13px; } .photo-input { display: none; } </style> src/views/environmentAccess/intelligentInspectionManagement/index.vue
@@ -61,8 +61,8 @@ <el-col :span="24"><el-form-item label="å·¡æ£ç»æ/è®°å½ï¼"><el-input v-model="form.inspectionResult" type="textarea" :rows="2" placeholder="请è¾å ¥å·¡æ£ç»æ" /></el-form-item></el-col> </el-row> <el-row :gutter="30"> <el-col :span="12"><el-form-item label="AIè§é¢åæç»æï¼"><el-input v-model="form.aiAnalysis" type="textarea" :rows="2" placeholder="é¢çï¼AIè§é¢åæç»æ" /></el-form-item></el-col> <el-col :span="12"><el-form-item label="æ 人æº/æºå¨äººæ°æ®ï¼"><el-input v-model="form.uavData" type="textarea" :rows="2" placeholder="é¢çï¼æ 人æº/æºå¨äººåä¼ æ°æ®" /></el-form-item></el-col> <el-col :span="12"><el-form-item label="AIè§é¢åæç»æï¼"><el-input v-model="form.aiAnalysis" type="textarea" :rows="2" placeholder="设å¤åä¼ åèªå¨åå ¥ï¼ä¹å¯æå·¥å½å ¥" /></el-form-item></el-col> <el-col :span="12"><el-form-item label="æ 人æº/æºå¨äººæ°æ®ï¼"><el-input v-model="form.uavData" type="textarea" :rows="2" placeholder="设å¤åä¼ åèªå¨åå ¥ï¼ä¹å¯æå·¥å½å ¥" /></el-form-item></el-col> </el-row> <el-row :gutter="30"> <el-col :span="24"><el-form-item label="å·¡æ£æ¥åï¼"><el-input v-model="form.reportPath" type="textarea" :rows="2" placeholder="èªå¨çæ/å¡«åå·¡æ£æ¥å" /></el-form-item></el-col> @@ -85,12 +85,78 @@ <el-button @click="reportVisible = false">å ³é</el-button> </template> </el-dialog> <!-- å·¡æ£åä¼ è¯¦æ ï¼AI è§é¢åæ + æ 人æº/æºå¨äººåä¼ æ°æ® --> <el-dialog v-model="traceVisible" :title="traceTitle" width="75%" top="6vh"> <div v-if="!traceHasData" class="trace-empty"> 该巡æ£ä»»å¡ææ 设å¤åä¼ æ°æ®ãæ äººæº / æºå¨äºº / AI æå头éè¿åä¼ æ¥å£ä¸æ¥åï¼è¿éä¼èªå¨å±ç¤ºè¯å«ç»æãç°åºç §çä¸è½¨è¿¹ã </div> <template v-else> <div class="trace-block"> <div class="trace-title">æ°æ®æ¥æº</div> <el-descriptions :column="4" border size="small"> <el-descriptions-item label="æ¥æºç±»å">{{ deviceTypeText(trace.device.deviceType) }}</el-descriptions-item> <el-descriptions-item label="设å¤ç¼å·">{{ trace.device.deviceCode || "-" }}</el-descriptions-item> <el-descriptions-item label="å·¡æ£åºå">{{ trace.device.area || "-" }}</el-descriptions-item> <el-descriptions-item label="设å¤çµé">{{ trace.device.battery != null ? trace.device.battery + "%" : "-" }}</el-descriptions-item> <el-descriptions-item label="ééæ¶é´">{{ trace.device.collectedAt || "-" }}</el-descriptions-item> <el-descriptions-item label="ç³»ç»æ¥æ¶æ¶é´">{{ trace.device.receivedAt || "-" }}</el-descriptions-item> <el-descriptions-item label="ééåæ ">{{ coordText }}</el-descriptions-item> <el-descriptions-item label="ä½ä¸é«åº¦">{{ trace.device.altitude != null ? trace.device.altitude + " m" : "-" }}</el-descriptions-item> </el-descriptions> </div> <div class="trace-block"> <div class="trace-title">AI è§é¢åæç»æ</div> <div class="trace-summary">{{ trace.ai.summary || "æ¬æ¬¡åä¼ æªé带 AI ç»è®º" }}</div> <el-table v-if="aiObjects.length" :data="aiObjects" border size="small"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="object" label="è¯å«å¯¹è±¡" min-width="140" show-overflow-tooltip /> <el-table-column prop="conclusion" label="è¯å«ç»è®º" min-width="160" show-overflow-tooltip /> <el-table-column label="置信度" width="110" align="center"> <template #default="{ row }">{{ row.confidence != null ? (Number(row.confidence) * 100).toFixed(1) + "%" : "-" }}</template> </el-table-column> <el-table-column prop="frameTime" label="æªå¸§æ¶é´" width="170" /> </el-table> <div v-else class="trace-sub-empty">ææ ç»æåè¯å«æ¡ç®</div> </div> <div v-if="tracePhotos.length" class="trace-block"> <div class="trace-title">ç°åºééç §ç</div> <div class="trace-photos"> <el-image v-for="(p, i) in tracePhotos" :key="i" :src="p" :preview-src-list="tracePhotos" :initial-index="i" fit="cover" class="trace-photo" /> </div> </div> <div v-if="traceTrack.length" class="trace-block"> <div class="trace-title">设å¤å·¡æ£è½¨è¿¹</div> <el-table :data="traceTrack" border size="small" max-height="200"> <el-table-column type="index" label="åºå·" width="60" align="center" /> <el-table-column prop="t" label="æ¶é´" width="140" /> <el-table-column prop="lng" label="ç»åº¦" width="140" /> <el-table-column prop="lat" label="纬度" width="140" /> <el-table-column prop="alt" label="é«åº¦(m)" width="110" /> </el-table> </div> </template> <template #footer> <el-button @click="traceVisible = false">å ³é</el-button> </template> </el-dialog> </div> </template> <script setup> import { Search } from "@element-plus/icons-vue"; import { ref, reactive, toRefs, onMounted, getCurrentInstance } from "vue"; import { ref, reactive, toRefs, onMounted, computed, getCurrentInstance } from "vue"; import { ElMessageBox } from "element-plus"; import { listInspection, addInspection, updateInspection, deleteInspection, generateInspectionReport } from "@/api/environmentAccess/inspection.js"; @@ -112,8 +178,9 @@ { label: "ç¶æ", prop: "status", width: 90, dataType: "tag", formatData: (p) => statusText(p), formatType: (p) => statusType(p) }, { label: "å·¡æ£ç»æ", prop: "inspectionResult", showOverflowTooltip: true }, { dataType: "action", label: "æä½", align: "center", fixed: "right", width: 100, dataType: "action", label: "æä½", align: "center", fixed: "right", width: 200, operation: [ { name: "åä¼ è¯¦æ ", type: "text", clickFun: (row) => openTrace(row) }, { name: "ç¼è¾", type: "text", clickFun: (row) => openForm("edit", row) }, { name: "çææ¥å", type: "text", clickFun: (row) => openReport(row) }, ], @@ -149,6 +216,47 @@ a.download = reportTitle.value + ".doc"; a.click(); URL.revokeObjectURL(a.href); }; /* ---------- å·¡æ£åä¼ è¯¦æ ï¼AI è§é¢åæ + æ 人æº/æºå¨äººåä¼ ï¼ ---------- */ const traceVisible = ref(false); const traceTitle = ref("å·¡æ£åä¼ è¯¦æ "); const trace = ref({ device: {}, ai: {}, photos: [], track: [] }); const deviceTypeText = (t) => ({ UAV: "æ 人æº", ROBOT: "å·¡æ£æºå¨äºº", CAMERA: "AI æå头" }[t] || t || "-"); const safeJson = (txt) => { if (!txt) return null; try { const v = JSON.parse(txt); return v && typeof v === "object" ? v : null; } catch (e) { return null; } }; const traceHasData = computed(() => { const d = trace.value.device || {}; const ai = trace.value.ai || {}; return !!(d.deviceCode || d.deviceType || ai.summary || (ai.objects && ai.objects.length)); }); const aiObjects = computed(() => (trace.value.ai && trace.value.ai.objects) || []); const tracePhotos = computed(() => (trace.value.device && trace.value.device.photos) || []); const traceTrack = computed(() => (trace.value.device && trace.value.device.track) || []); const coordText = computed(() => { const d = trace.value.device || {}; if (d.longitude == null || d.latitude == null) return "-"; return d.longitude + ", " + d.latitude; }); const openTrace = (row) => { const uav = safeJson(row.uavData); const aiParsed = safeJson(row.aiAnalysis); const ai = aiParsed || { summary: row.aiAnalysis || "", objects: [] }; trace.value = { device: uav || {}, ai: ai, photos: (uav && uav.photos) || [], track: (uav && uav.track) || [], }; traceTitle.value = (row.inspectionName || "æºè½å·¡æ£") + " - åä¼ è¯¦æ "; traceVisible.value = true; }; const handleQuery = () => { page.current = 1; getList(); }; @@ -192,4 +300,40 @@ <style scoped lang="scss"> .ml10 { margin-left: 10px; } .table_list { margin-top: unset; } .trace-block { margin-bottom: 18px; } .trace-title { font-size: 14px; font-weight: 600; color: #303133; border-left: 3px solid #409eff; padding-left: 8px; margin-bottom: 10px; } .trace-summary { font-size: 13px; color: #606266; background: #f5f7fa; border-radius: 4px; padding: 8px 12px; margin-bottom: 10px; line-height: 1.6; } .trace-sub-empty, .trace-empty { color: #909399; font-size: 13px; line-height: 1.8; } .trace-empty { padding: 24px 8px; text-align: center; } .trace-photos { display: flex; flex-wrap: wrap; gap: 10px; } .trace-photo { width: 132px; height: 96px; border-radius: 4px; border: 1px solid #ebeef5; cursor: pointer; } </style> src/views/environmentAccess/remoteMonitor/index.vue
@@ -162,7 +162,8 @@ const startAlarmPolling = () => { loadAlarms(false); alarmTimer = setInterval(() => loadAlarms(true), 60000); // 15 ç§è½®è¯¢ä¸æ¬¡ï¼æ°æ¥è¦ä¼ç«å³å¼¹åºç«å æé alarmTimer = setInterval(() => loadAlarms(true), 15000); }; onMounted(() => { src/views/environmentAccess/vehicleInfoAudit/index.vue
@@ -127,6 +127,44 @@ </el-form-item> </el-col> </el-row> <el-row :gutter="30"> <el-col :span="8"> <el-form-item label="车çç §ç"> <el-image v-if="form.platePhoto" :src="form.platePhoto" :preview-src-list="[form.platePhoto]" fit="cover" class="audit-photo" /> <span v-else class="audit-photo-empty">æªä¸ä¼ </span> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="è¡é©¶è¯ç §ç"> <el-image v-if="form.licensePhoto" :src="form.licensePhoto" :preview-src-list="[form.licensePhoto]" fit="cover" class="audit-photo" /> <span v-else class="audit-photo-empty">æªä¸ä¼ </span> </el-form-item> </el-col> <el-col :span="8"> <el-form-item label="车è¾å ¨æ¯ç §ç"> <el-image v-if="form.scenePhoto" :src="form.scenePhoto" :preview-src-list="[form.scenePhoto]" fit="cover" class="audit-photo" /> <span v-else class="audit-photo-empty">æªä¸ä¼ </span> </el-form-item> </el-col> </el-row> </el-form> <template #footer> <div class="dialog-footer"> @@ -144,6 +182,20 @@ @close="closeAuditDialog" > <el-form :model="auditForm" label-width="100px"> <el-form-item label="ééç §ç"> <div class="audit-photos"> <el-image v-for="(p, i) in auditPhotos" :key="i" :src="p" :preview-src-list="auditPhotos" :initial-index="i" fit="cover" class="audit-thumb" /> <span v-if="!auditPhotos.length" class="audit-photo-empty">è¯¥è®°å½æªä¸ä¼ ç §ç</span> </div> </el-form-item> <el-form-item label="å®¡æ ¸ç»æ" prop="status"> <el-radio-group v-model="auditForm.status"> <el-radio label="approved">éè¿</el-radio> @@ -170,7 +222,7 @@ </template> <script setup> import { ref, reactive } from "vue"; import { ref, reactive, computed } from "vue"; import { Search } from "@element-plus/icons-vue"; import { ElMessage, ElMessageBox } from "element-plus"; import PIMTable from "@/components/PIMTable/PIMTable.vue"; @@ -340,6 +392,9 @@ registeDate: "", natureOfUse: "", fuelType: "", platePhoto: "", licensePhoto: "", scenePhoto: "", reviewStatus: "pending", }; } @@ -355,6 +410,10 @@ const auditForm = reactive({ status: "approved", comment: "", }); const auditPhotos = computed(() => { const r = currentAuditRow.value || {}; return [r.platePhoto, r.licensePhoto, r.scenePhoto].filter(Boolean); }); // æ¥è¯¢ @@ -496,5 +555,29 @@ .table_list { margin-top: 20px; } .audit-photo { width: 140px; height: 100px; border-radius: 4px; border: 1px solid #ebeef5; cursor: pointer; } .audit-photo-empty { font-size: 13px; color: #909399; } .audit-photos { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; } .audit-thumb { width: 96px; height: 72px; border-radius: 4px; border: 1px solid #ebeef5; cursor: pointer; } </style> src/views/environmentAccess/vehicleInfoCollect/index.vue
@@ -71,6 +71,18 @@ </el-select> </el-form-item> <el-form-item label="车çç §ç" prop="platePhoto"> <PhotoCapture v-model="form.platePhoto" placeholder="ææè½¦ç" /> </el-form-item> <el-form-item label="è¡é©¶è¯ç §ç" prop="licensePhoto"> <PhotoCapture v-model="form.licensePhoto" placeholder="ææè¡é©¶è¯" /> </el-form-item> <el-form-item label="车è¾å ¨æ¯ç §ç" prop="scenePhoto"> <PhotoCapture v-model="form.scenePhoto" placeholder="ææè½¦è¾å ¨æ¯" /> </el-form-item> <div class="collect-actions"> <el-button type="primary" size="large" :loading="submitting" style="width: 100%" @click="submit"> æäº¤éé @@ -85,6 +97,7 @@ import { ref, reactive } from "vue"; import { ElMessage } from "element-plus"; import { addVehicleInfo } from "@/api/environmentAccess/vehicleInfo.js"; import PhotoCapture from "../components/PhotoCapture.vue"; defineOptions({ name: "车è¾ä¿¡æ¯éé" }); @@ -110,6 +123,9 @@ registeDate: "", natureOfUse: "", fuelType: "", platePhoto: "", licensePhoto: "", scenePhoto: "", }); const form = reactive(emptyForm()); @@ -186,4 +202,28 @@ .collect-actions { margin-top: 22px; } /* ææºç«¯éé */ @media (max-width: 768px) { .collect-page { padding: 12px 12px calc(48px + env(safe-area-inset-bottom)); min-height: auto; } .collect-header h2 { font-size: 18px; } .collect-form :deep(.el-form-item) { margin-bottom: 16px; } .collect-actions { position: sticky; bottom: 0; padding: 10px 0 calc(10px + env(safe-area-inset-bottom)); background: #fff; margin-top: 12px; } } </style>