Industrial-image-management.../src/views/tower-monitoring/clearance.vue

499 lines
16 KiB
Vue

<template>
<GiPageLayout>
<div class="raw-data-container">
<!-- 顶部按钮 -->
<div class="action-bar">
<div class="action-buttons">
<a-button type="primary" @click="openUploadModal">
<template #icon>
<IconUpload />
</template>
上传视频
</a-button>
</div>
</div>
<!-- 筛选 -->
<div class="filter-section">
<a-form :model="filterForm" layout="inline">
<a-form-item label="项目" required>
<a-select v-model="filterForm.projectId" placeholder="请选择项目" :options="projectOptions"
allow-clear @change="" />
</a-form-item>
<a-form-item label="机组">
<a-select v-model="filterForm.turbineId" placeholder="请选择机组" :options="turbineOptions"
allow-clear :disabled="!filterForm.projectId" />
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleQuery">查询</a-button>
</a-form-item>
</a-form>
</div>
<!-- 列表 -->
<a-table :columns="columns" :data="tableData" :pagination="pagination" :loading="loading"
:scroll="{ y: 'calc(100vh - 380px)' }">
<template #type="{ record }">
<a-tag>{{ record.type === 'clearance' ? '净空' : '形变' }}</a-tag>
</template>
<template #status="{ record }">
<a-tag :color="record.preTreatment ? 'green' : 'red'">
{{ record.preTreatment ? '已处理' : '未处理' }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a-button size="mini" @click="handlePreview(record)">预览</a-button>
<a-button size="mini" @click="handleDownload(record)">下载</a-button>
<a-popconfirm content="确认删除?" @ok="handleDelete(record)">
<a-button size="mini" status="danger">删除</a-button>
</a-popconfirm>
</a-space>
</template>
</a-table>
<!-- 上传弹窗 -->
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" :ok-loading="uploading"
@ok="handleUpload" @cancel="showUploadModal = false">
<a-form :model="uploadForm" layout="vertical">
<a-form-item label="项目" required>
<a-select v-model="uploadForm.projectId" placeholder="请选择项目" :options="projectOptions"
allow-clear @change="onProjectChangeUpload" />
</a-form-item>
<a-form-item label="机组">
<a-select v-model="uploadForm.turbineId" placeholder="请选择机组" :options="turbineOptionsUpload"
allow-clear :disabled="!uploadForm.projectId" />
</a-form-item>
<a-form-item label="类型" required>
<a-select v-model="uploadForm.type" placeholder="请选择类型" :options="typeOptions" />
</a-form-item>
<a-form-item label="视频文件" required>
<a-upload v-model:file-list="uploadForm.fileList" :multiple="uploadMode === 'batch'"
:limit="uploadMode === 'batch' ? 10 : 1" accept="video/*" :auto-upload="false"
list-type="picture-card" />
</a-form-item>
<a-form-item>
<a-radio-group v-model="uploadMode" type="button">
<a-radio value="single">单文件</a-radio>
<a-radio value="batch">批量</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</a-modal>
</div>
</GiPageLayout>
<!-- 视频预览弹窗 -->
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
@cancel="previewVisible = false">
<a-tabs v-model:active-key="activePreviewTab" @change="activePreviewTab = $event as any">
<!-- 原始视频 -->
<a-tab-pane key="video" title="原始视频">
<video v-if="previewUrl" :src="previewUrl" controls
style="width: 100%; max-height: 60vh; border-radius: 4px"></video>
</a-tab-pane>
<!-- 处理结果 -->
<a-tab-pane key="result" title="处理结果">
<a-spin :loading="loadingResult">
<a-space direction="vertical" size="medium" style="width: 100%">
<!-- 图片 -->
<img v-if="resultImgUrl" :src="resultImgUrl" style="max-width: 100%; border-radius: 4px"
alt="last frame" />
<!-- JSON 预览 -->
<a-card title="results.json" size="small">
<pre>{{ JSON.stringify(resultJson, null, 2) }}</pre>
</a-card>
</a-space>
</a-spin>
</a-tab-pane>
</a-tabs>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import { Message } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import { IconUpload } from '@arco-design/web-vue/es/icon'
import {
getProjectList,
getTurbineList
} from '@/apis/industrial-image'
import {
getVideoPage,
uploadBatchVideo,
uploadSingleVideo,
deleteVideo,
downloadVideo
} from '@/apis/video-monitor'
/* ---------------- 下拉 & 表单 ---------------- */
const projectOptions = ref<{ label: string; value: string }[]>([])
const turbineOptions = ref<{ label: string; value: string }[]>([]) // 筛选用
const turbineOptionsUpload = ref<{ label: string; value: string }[]>([]) // 上传弹窗用
const typeOptions = [
{ label: '净空', value: 'clearance' },
{ label: '形变', value: 'deformation' }
]
const filterForm = reactive({
projectId: '',
turbineId: ''
})
const uploadForm = reactive({
projectId: '',
turbineId: '',
type: '',
fileList: [] as any[]
})
const uploadMode = ref<'single' | 'batch'>('single')
/* ---------------- 列表 ---------------- */
const columns: TableColumnData[] = [
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
// { title: '项目', dataIndex: 'projectName' },
// { title: '机组', dataIndex: 'turbineName' },
{ title: '类型', slotName: 'type' },
{ title: '上传时间', dataIndex: 'uploadTime' },
{ title: '状态', slotName: 'status' },
{ title: '操作', slotName: 'action', width: 120, fixed: 'right' }
]
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ current: 1, pageSize: 20, total: 0 })
/* ---------------- 控制弹窗 ---------------- */
const showUploadModal = ref(false)
const uploading = ref(false)
/* ---------------- 初始化 ---------------- */
onMounted(async () => {
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
projectOptions.value = data.map((p: any) => ({ label: p.projectName, value: p.projectId }))
handleQuery()
})
const activePreviewTab = ref<'video' | 'result'>('video') // 当前标签页
const resultImgUrl = ref('')
const resultJson = ref<Record<string, any>>({})
const loadingResult = ref(false)
const resutlVideoUrl = ref('')
async function loadResultFiles(row: any) {
if (!row.preTreatment) return
loadingResult.value = true
try {
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
// 图片
resultImgUrl.value = `${base}${row.preImagePath}/last_frame.jpg`
resutlVideoUrl.value = `${base}${row.preImagePath}/annotated_video.mp4`
// JSON
const jsonUrl = `${base}${row.preImagePath}/results.json`
const res = await fetch(jsonUrl)
resultJson.value = await res.json()
} catch (e) {
console.error(e)
resultJson.value = {}
} finally {
loadingResult.value = false
}
console.log('result', resultImgUrl.value)
}
/* 项目 -> 机组(筛选) */
watch(
() => filterForm.projectId,
async (val) => {
filterForm.turbineId = ''
turbineOptions.value = []
if (!val) return
const { data } = await getTurbineList({ projectId: val })
turbineOptions.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
}
)
const previewVisible = ref(false)
const previewUrl = ref('')
function handlePreview(row: any) {
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
previewUrl.value = new URL(row.videoPath.replace(/^\/+/, ''), base).href
previewVisible.value = true
activePreviewTab.value = 'video' // 默认先显示视频
if (row.preTreatment) {
loadResultFiles(row) // 预加载结果
}
}
/* 项目 -> 机组(上传弹窗) */
async function onProjectChangeUpload(projectId: string) {
uploadForm.turbineId = ''
turbineOptionsUpload.value = []
if (!projectId) return
const { data } = await getTurbineList({ projectId })
turbineOptionsUpload.value = data.map((t: any) => ({ label: t.turbineName, value: t.turbineId }))
}
/* ---------------- 查询 ---------------- */
function handleQuery() {
pagination.current = 1
loadTable()
}
async function loadTable() {
loading.value = true
try {
const params = {
pageNo: pagination.current,
pageSize: pagination.pageSize,
projectId: filterForm.projectId,
turbineId: filterForm.turbineId || undefined
}
const { data } = await getVideoPage(params)
console.log(data)
tableData.value = data
pagination.total = data.length
} finally {
loading.value = false
}
}
/* ---------------- 上传 ---------------- */
function openUploadModal() {
uploadForm.projectId = ''
uploadForm.turbineId = ''
uploadForm.type = ''
uploadForm.fileList = []
showUploadModal.value = true
}
async function handleUpload() {
if (!uploadForm.projectId || !uploadForm.type || !uploadForm.fileList.length) {
Message.warning('请完整填写')
return
}
uploading.value = true
try {
const files = uploadForm.fileList.map((f: any) => f.file)
if (uploadMode.value === 'single') {
await uploadSingleVideo(
uploadForm.projectId,
uploadForm.turbineId || '',
uploadForm.type,
files[0]
)
} else {
await uploadBatchVideo(
uploadForm.projectId,
uploadForm.turbineId || '',
uploadForm.type,
files
)
}
Message.success('上传成功')
showUploadModal.value = false
loadTable()
} finally {
uploading.value = false
}
}
/* ---------------- 下载 / 删除 ---------------- */
async function handleDownload(row: any) {
const url = await downloadVideo(row.videoId)
window.open(url, '_blank')
}
async function handleDelete(row: any) {
await deleteVideo(row.videoId)
Message.success('删除成功')
loadTable()
}
</script>
<style scoped lang="scss">
.raw-data-container {
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-title {
font-size: 28px;
font-weight: 600;
margin: 0 0 8px 0;
color: #1d2129;
}
.page-subtitle {
font-size: 14px;
color: #86909c;
margin: 0;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
.filter-section {
margin-left: 24px;
}
}
.project-sections {
.project-section {
margin-bottom: 32px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px #f0f1f2;
padding: 20px;
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.project-title {
font-size: 20px;
font-weight: 600;
}
.project-stats {
display: flex;
gap: 16px;
.stat-item {
display: flex;
align-items: center;
gap: 4px;
color: #86909c;
}
}
}
.units-grid {
display: flex;
gap: 24px;
flex-wrap: wrap;
.unit-card {
background: #fafbfc;
border-radius: 8px;
padding: 16px;
width: 360px;
margin-bottom: 16px;
.unit-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.unit-title {
font-size: 16px;
font-weight: 600;
}
.unit-actions {
display: flex;
gap: 8px;
}
}
.videos-list {
display: flex;
gap: 12px;
margin-bottom: 8px;
.video-item {
width: 100px;
.video-thumbnail {
position: relative;
width: 100px;
height: 60px;
border-radius: 6px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.2);
opacity: 0;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
}
}
.video-info {
margin-top: 4px;
.video-name {
font-size: 12px;
font-weight: 500;
color: #1d2129;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.video-meta {
font-size: 11px;
color: #86909c;
display: flex;
gap: 4px;
}
.video-status {
margin-top: 2px;
}
}
}
}
.analysis-progress {
margin-top: 8px;
.progress-info {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #86909c;
margin-bottom: 2px;
}
}
}
}
}
}
.video-meta-info {
margin-top: 16px;
font-size: 13px;
color: #4e5969;
p {
margin: 2px 0;
}
}
</style>