From ce44d803b73a65b2cc31db5bcc662139029463d3 Mon Sep 17 00:00:00 2001
From: doum <doum>
Date: 星期五, 03 七月 2026 10:07:32 +0800
Subject: [PATCH] 海康电表维护
---
admin/src/views/business/collectionMedia.vue | 370 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 359 insertions(+), 11 deletions(-)
diff --git a/admin/src/views/business/collectionMedia.vue b/admin/src/views/business/collectionMedia.vue
index d6ca607..2745fb5 100644
--- a/admin/src/views/business/collectionMedia.vue
+++ b/admin/src/views/business/collectionMedia.vue
@@ -51,19 +51,38 @@
<el-table-column prop="endTime" label="缁撴潫鏃堕棿" min-width="160" />
<el-table-column prop="downloadStatus" label="涓嬭浇鐘舵��" width="90">
<template slot-scope="{row}">
- <span v-if="row.downloadStatus === 1">宸蹭笅杞�</span>
- <span v-else-if="row.downloadStatus === 2">澶辫触</span>
- <span v-else-if="row.downloadStatus === 3">涓嬭浇涓�</span>
- <span v-else>寰呬笅杞�</span>
+ <span :class="downloadStatusClass(row)">{{ downloadStatusLabel(row) }}</span>
</template>
</el-table-column>
- <el-table-column prop="filePathLocal" label="鏈湴璺緞" min-width="160" show-overflow-tooltip />
- <el-table-column label="鎿嶄綔" width="220" fixed="right">
+ <el-table-column prop="snapshotStatus" label="蹇収" width="90">
<template slot-scope="{row}">
- <el-button type="text" v-if="canPreview(row)" @click="handlePreview(row)">棰勮</el-button>
+ <span :class="snapshotStatusClass(row)">{{ snapshotStatusLabel(row) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="filePathLocal" label="鏈湴璺緞" min-width="120">
+ <template slot-scope="{row}">
+ <div v-if="isDownloadedVideo(row)" class="list-video-cell" @click="handlePreview(row)">
+ <video
+ :key="'thumb-' + row.id"
+ :src="buildListVideoSrc(row)"
+ class="list-video-thumb"
+ muted
+ preload="metadata"
+ playsinline
+ />
+ </div>
+ <span v-else class="path-text" :title="row.filePathLocal">{{ row.filePathLocal || '-' }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="240" fixed="right">
+ <template slot-scope="{row}">
<el-button type="text" v-if="canDownloadToStation(row)" v-permissions="['business:collectionMedia:download', 'business:collectionStation:sync']"
@click="handleDownload(row)">涓嬭浇</el-button>
<el-button type="text" v-if="row.downloadStatus === 1" @click="handleSaveLocal(row)">涓嬭浇鍒版湰鍦�</el-button>
+ <el-button type="text" v-if="row.downloadStatus === 1 && row.snapshotStatus !== 1"
+ @click="handleAnalyzeSnapshot(row)">鐢熸垚蹇収</el-button>
+ <el-button type="text" v-if="row.downloadStatus === 1" @click="openManualCorrect(row)">浜哄伐绾犳</el-button>
+ <el-button type="text" v-if="row.snapshotStatus === 2" @click="handleViewSnapshot(row)">鏌ョ湅蹇収</el-button>
</template>
</el-table-column>
</el-table>
@@ -82,6 +101,44 @@
<div v-else-if="!previewLoading" class="preview-empty">鏆備笉鏀寔璇ョ被鍨嬮瑙�</div>
</div>
</el-dialog>
+
+ <el-dialog :title="snapshotTitle" :visible.sync="snapshotVisible" width="720px" append-to-body>
+ <div v-loading="snapshotLoading" class="snapshot-wrap">
+ <div v-if="snapshotList.length" class="snapshot-grid">
+ <div v-for="item in snapshotList" :key="item.id + '-' + snapshotCacheVersion" class="snapshot-item">
+ <div class="snapshot-label">{{ snapshotTypeLabel(item.snapshotType) }}</div>
+ <el-image v-if="item.fileUrlFull" :src="buildSnapshotImageUrl(item.fileUrlFull)" fit="contain"
+ class="snapshot-image" :preview-src-list="[buildSnapshotImageUrl(item.fileUrlFull)]" />
+ <div class="snapshot-meta">
+ <span>鏃跺埢: {{ item.timestampSec }}s</span>
+ <span v-if="item.confidence">缃俊搴�: {{ item.confidence }}</span>
+ <span>鏉ユ簮: {{ item.source || '-' }}</span>
+ </div>
+ </div>
+ </div>
+ <div v-else-if="!snapshotLoading" class="preview-empty">鏆傛棤蹇収锛岃鍏堢偣鍑汇�岀敓鎴愬揩鐓с��</div>
+ <div v-if="snapshotMediaRow" class="snapshot-actions">
+ <el-button type="primary" plain @click="openManualCorrect(snapshotMediaRow)">浜哄伐绾犳鏃跺埢</el-button>
+ </div>
+ </div>
+ </el-dialog>
+
+ <el-dialog title="浜哄伐绾犳蹇収鏃跺埢" :visible.sync="manualCorrectVisible" width="860px" append-to-body
+ @close="closeManualCorrect">
+ <div v-loading="manualCorrectLoading" class="manual-correct-wrap">
+ <video v-if="manualPreviewSrc" ref="manualVideo" :src="manualPreviewSrc" controls preload="metadata"
+ playsinline class="preview-video" @loadedmetadata="onManualVideoLoaded" @timeupdate="onManualTimeUpdate" />
+ <div class="manual-slider">
+ <span>褰撳墠鏃跺埢: {{ manualTimestampSec.toFixed(1) }}s</span>
+ <el-slider v-model="manualTimestampSec" :min="0" :max="manualDurationSec" :step="0.5"
+ @input="seekManualVideo" />
+ </div>
+ <div class="manual-buttons">
+ <el-button type="primary" :loading="manualSaving" @click="saveManualSnapshot(1)">璁句负闂ㄥご鍥�</el-button>
+ <el-button type="success" :loading="manualSaving" @click="saveManualSnapshot(2)">璁句负浜や粯鍥�</el-button>
+ </div>
+ </div>
+ </el-dialog>
</TableLayout>
</template>
@@ -90,7 +147,16 @@
import TableLayout from '@/layouts/TableLayout'
import Pagination from '@/components/common/Pagination'
import { syncMedia, downloadMedia, batchDownloadMedia, list as fetchStationList } from '@/api/business/collectionStation'
-import { fetchPreviewText, fetchPreviewBlob, fetchMediaFile, ensureMp4Blob, buildPreviewUrl } from '@/api/business/collectionMedia'
+import {
+ fetchPreviewText,
+ fetchPreviewBlob,
+ fetchMediaFile,
+ ensureMp4Blob,
+ buildPreviewUrl,
+ analyzeMediaSnapshot,
+ fetchMediaSnapshots,
+ saveManualMediaSnapshot
+} from '@/api/business/collectionMedia'
export default {
name: 'CollectionMedia',
@@ -113,7 +179,23 @@
previewRow: null,
previewUseDirectUrl: false,
previewBlobUrl: '',
- downloadPollTimer: null
+ downloadPollTimer: null,
+ snapshotVisible: false,
+ snapshotLoading: false,
+ snapshotTitle: '閰嶉�佸揩鐓�',
+ snapshotList: [],
+ snapshotPollTimer: null,
+ snapshotMediaRow: null,
+ manualCorrectVisible: false,
+ manualCorrectLoading: false,
+ manualCorrectRow: null,
+ manualPreviewSrc: '',
+ manualBlobUrl: '',
+ manualTimestampSec: 0,
+ manualDurationSec: 600,
+ manualSaving: false,
+ manualSeekFromSlider: false,
+ snapshotCacheVersion: 0
}
},
created () {
@@ -131,7 +213,9 @@
},
beforeDestroy () {
this.revokePreviewUrl()
+ this.revokeManualPreviewUrl()
this.stopDownloadPoll()
+ this.stopSnapshotPoll()
},
methods: {
loadStations () {
@@ -151,8 +235,11 @@
if (row.mediaType === 2) return '闊抽'
return '瑙嗛'
},
- canPreview (row) {
- return row.downloadStatus === 1 && (row.fileUrlFull || row.filePathLocal)
+ isDownloadedVideo (row) {
+ return row.downloadStatus === 1 && row.filePathLocal && this.resolvePreviewMode(row) === 'video'
+ },
+ buildListVideoSrc (row) {
+ return row.fileUrlFull || buildPreviewUrl(row.id)
},
canDownloadToStation (row) {
return row.downloadStatus !== 1 && row.downloadStatus !== 3
@@ -199,6 +286,94 @@
}).catch(err => {
this.$message.error(err.message || '涓嬭浇鍒版湰鍦板け璐�')
})
+ },
+ snapshotTypeLabel (type) {
+ return type === 2 ? '璐у搧浜や粯鍥�' : '鍒板簵闂ㄥご鍥�'
+ },
+ downloadStatusLabel (row) {
+ const map = { 0: '寰呬笅杞�', 1: '宸蹭笅杞�', 2: '澶辫触', 3: '涓嬭浇涓�' }
+ return map[row.downloadStatus] != null ? map[row.downloadStatus] : '寰呬笅杞�'
+ },
+ downloadStatusClass (row) {
+ const map = { 0: 'status-info', 1: 'status-success', 2: 'status-danger', 3: 'status-primary' }
+ return map[row.downloadStatus] || 'status-info'
+ },
+ snapshotStatusLabel (row) {
+ if (row.snapshotStatus === 2) return '宸插畬鎴�'
+ if (row.snapshotStatus === 1) return '鍒嗘瀽涓�'
+ if (row.snapshotStatus === 3) return '澶辫触'
+ if (row.downloadStatus === 1) return '鏈垎鏋�'
+ return '-'
+ },
+ snapshotStatusClass (row) {
+ if (row.snapshotStatus === 2) return 'status-success'
+ if (row.snapshotStatus === 1) return 'status-warning'
+ if (row.snapshotStatus === 3) return 'status-danger'
+ if (row.downloadStatus === 1) return 'status-info'
+ return 'status-muted'
+ },
+ buildSnapshotImageUrl (url) {
+ if (!url) return ''
+ const sep = url.indexOf('?') >= 0 ? '&' : '?'
+ return `${url}${sep}_t=${this.snapshotCacheVersion}`
+ },
+ handleAnalyzeSnapshot (row) {
+ analyzeMediaSnapshot(row.id).then(res => {
+ this.$message.success(res || '宸叉彁浜ゅ揩鐓у垎鏋�')
+ this.search()
+ this.startSnapshotPoll(row.id)
+ })
+ },
+ handleViewSnapshot (row) {
+ this.snapshotMediaRow = row
+ this.snapshotTitle = (row.fileName || '濯掍綋') + ' - 閰嶉�佸揩鐓�'
+ this.snapshotVisible = true
+ this.loadSnapshots(row.id)
+ },
+ loadSnapshots (mediaId) {
+ this.snapshotLoading = true
+ this.snapshotList = []
+ this.snapshotCacheVersion = Date.now()
+ fetchMediaSnapshots(mediaId, this.snapshotCacheVersion).then(list => {
+ this.snapshotList = list || []
+ this.snapshotLoading = false
+ }).catch(err => {
+ this.snapshotLoading = false
+ this.$message.error(err.message || '鍔犺浇蹇収澶辫触')
+ })
+ },
+ startSnapshotPoll (mediaId) {
+ this.stopSnapshotPoll()
+ let count = 0
+ this.snapshotPollTimer = setInterval(() => {
+ count++
+ if (count > 40) {
+ this.stopSnapshotPoll()
+ return
+ }
+ this.api.fetchList({
+ page: this.tableData.pagination.pageIndex,
+ capacity: this.tableData.pagination.pageSize,
+ model: this.searchForm,
+ sorts: this.tableData.sorts
+ }).then(data => {
+ this.tableData.list = data.records
+ this.tableData.pagination.total = data.total
+ const row = (data.records || []).find(item => item.id === mediaId)
+ if (row && row.snapshotStatus !== 1) {
+ this.stopSnapshotPoll()
+ if (row.snapshotStatus === 2 && this.snapshotVisible) {
+ this.loadSnapshots(mediaId)
+ }
+ }
+ }).catch(() => {})
+ }, 3000)
+ },
+ stopSnapshotPoll () {
+ if (this.snapshotPollTimer) {
+ clearInterval(this.snapshotPollTimer)
+ this.snapshotPollTimer = null
+ }
},
startDownloadPoll () {
this.stopDownloadPoll()
@@ -333,6 +508,92 @@
URL.revokeObjectURL(this.previewBlobUrl)
this.previewBlobUrl = ''
}
+ },
+ openManualCorrect (row) {
+ if (row.downloadStatus !== 1) {
+ this.$message.warning('璇峰厛涓嬭浇濯掍綋鏂囦欢')
+ return
+ }
+ this.manualCorrectRow = row
+ this.manualCorrectVisible = true
+ this.manualCorrectLoading = true
+ this.manualTimestampSec = 0
+ this.manualDurationSec = 600
+ this.revokeManualPreviewUrl()
+ fetchPreviewBlob(row.id)
+ .then(blob => ensureMp4Blob(blob))
+ .then(blob => {
+ this.manualBlobUrl = URL.createObjectURL(blob)
+ this.manualPreviewSrc = this.manualBlobUrl
+ this.manualCorrectLoading = false
+ })
+ .catch(err => {
+ this.manualCorrectLoading = false
+ this.$message.error(err.message || '鍔犺浇瑙嗛澶辫触')
+ })
+ },
+ closeManualCorrect () {
+ this.revokeManualPreviewUrl()
+ this.manualPreviewSrc = ''
+ this.manualCorrectRow = null
+ },
+ revokeManualPreviewUrl () {
+ if (this.manualBlobUrl) {
+ URL.revokeObjectURL(this.manualBlobUrl)
+ this.manualBlobUrl = ''
+ }
+ },
+ onManualVideoLoaded () {
+ const video = this.$refs.manualVideo
+ if (video && video.duration && isFinite(video.duration)) {
+ this.manualDurationSec = Math.max(1, Math.floor(video.duration))
+ }
+ },
+ onManualTimeUpdate () {
+ if (this.manualSeekFromSlider) {
+ return
+ }
+ const video = this.$refs.manualVideo
+ if (video) {
+ this.manualTimestampSec = Math.round(video.currentTime * 2) / 2
+ }
+ },
+ seekManualVideo (val) {
+ const video = this.$refs.manualVideo
+ if (!video) {
+ return
+ }
+ this.manualSeekFromSlider = true
+ video.currentTime = val
+ setTimeout(() => {
+ this.manualSeekFromSlider = false
+ }, 200)
+ },
+ saveManualSnapshot (snapshotType) {
+ if (!this.manualCorrectRow) {
+ return
+ }
+ this.manualSaving = true
+ saveManualMediaSnapshot({
+ mediaId: this.manualCorrectRow.id,
+ snapshotType,
+ timestampSec: this.manualTimestampSec
+ }).then(res => {
+ this.manualSaving = false
+ this.$message.success(res || '淇濆瓨鎴愬姛')
+ const mediaId = this.manualCorrectRow.id
+ this.snapshotCacheVersion = Date.now()
+ this.search()
+ if (this.snapshotMediaRow && this.snapshotMediaRow.id === mediaId) {
+ this.snapshotMediaRow = { ...this.snapshotMediaRow, snapshotStatus: 2 }
+ }
+ if (this.snapshotVisible && this.snapshotMediaRow && this.snapshotMediaRow.id === mediaId) {
+ this.loadSnapshots(mediaId)
+ }
+ }).catch(err => {
+ this.manualSaving = false
+ this.$message.error(err.message || '淇濆瓨澶辫触')
+ })
}
}
}
@@ -369,4 +630,91 @@
color: #909399;
padding: 40px 0;
}
+.snapshot-wrap {
+ min-height: 120px;
+}
+.snapshot-grid {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+.snapshot-item {
+ flex: 1;
+ min-width: 280px;
+}
+.snapshot-label {
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+.snapshot-image {
+ width: 100%;
+ max-height: 280px;
+ background: #f5f7fa;
+}
+.snapshot-meta {
+ margin-top: 8px;
+ font-size: 12px;
+ color: #606266;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.snapshot-actions {
+ margin-top: 16px;
+ text-align: center;
+}
+.manual-correct-wrap {
+ min-height: 200px;
+}
+.manual-slider {
+ margin-top: 16px;
+}
+.manual-buttons {
+ margin-top: 16px;
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+}
+.list-video-cell {
+ display: inline-block;
+ cursor: pointer;
+ line-height: 0;
+}
+.list-video-thumb {
+ width: 70px;
+ height: 70px;
+ object-fit: cover;
+ background: #000;
+ border-radius: 4px;
+ vertical-align: middle;
+}
+.path-text {
+ display: inline-block;
+ max-width: 160px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.status-success {
+ color: #67c23a;
+ font-weight: 500;
+}
+.status-danger {
+ color: #f56c6c;
+ font-weight: 500;
+}
+.status-warning {
+ color: #e6a23c;
+ font-weight: 500;
+}
+.status-primary {
+ color: #409eff;
+ font-weight: 500;
+}
+.status-info {
+ color: #909399;
+}
+.status-muted {
+ color: #c0c4cc;
+}
</style>
--
Gitblit v1.9.3