Merge branch 'devlopment' of http://pms.dtyx.net:3000/wuxueyu/Industrial-image-management-system---web into devlopment
This commit is contained in:
commit
f3fa274e18
|
@ -1199,46 +1199,141 @@ export const systemRoutes: RouteRecordRaw[] = [
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/chat-platform',
|
path: '/image-detection',
|
||||||
name: 'ChatPlatform',
|
name: 'ImageDetection',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/chat-platform/options',
|
redirect: '/Image-detection/tower-monitoring/clearance-monitoring',
|
||||||
meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
|
meta: {
|
||||||
|
title: '图像检测',
|
||||||
|
icon: 'monitor',
|
||||||
|
hidden: false,
|
||||||
|
sort: 6.5,
|
||||||
|
},
|
||||||
|
|
||||||
children: [
|
children: [
|
||||||
// {
|
{
|
||||||
// path: '/chat-platform/options',
|
path: '/image-detection/image-analysis',
|
||||||
// name: 'ChatOptions',
|
name: 'ImageAnalysis',
|
||||||
// component: () => import('@/views/default/redirect/index.vue'), // 临时使用一个组件,实际开发中需要替换
|
component: () => import('@/components/ParentView/index.vue'),
|
||||||
// meta: {
|
meta: {
|
||||||
// title: '二级选项1',
|
title: '检查图像分析',
|
||||||
// icon: 'setting',
|
icon: 'line-chart',
|
||||||
// hidden: false
|
hidden: false,
|
||||||
// }
|
},
|
||||||
// }
|
redirect: '/image-detection/image-analysis/defect-detection',
|
||||||
],
|
children: [
|
||||||
|
{path: '/image-detection/image-analysis/defect-detection',
|
||||||
|
name: 'DefectDetection',
|
||||||
|
component: () => import('@/views/construction-operation-platform/implementation-workflow/data-processing/intelligent-inspection/defect-algorithm/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '缺陷检测',
|
||||||
|
icon: 'line-chart',
|
||||||
|
hidden: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{path: '/image-detection/image-analysis/defect-edit',
|
||||||
|
name: 'DefectEdit',
|
||||||
|
component: () => import('@/components/ParentView/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '缺陷编辑',
|
||||||
|
icon: 'line-chart',
|
||||||
|
hidden: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{path: '/image-detection/image-analysis/generate-reports',
|
||||||
|
name: 'GenerateReports',
|
||||||
|
component: () => import('@/views/project-operation-platform/data-processing/report-generation/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '生成报告',
|
||||||
|
icon: 'line-chart',
|
||||||
|
hidden: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{path: '/image-detection/image-analysis/defect-base',
|
||||||
|
name: 'DefectBase',
|
||||||
|
component: () => import('@/views/project-operation-platform/data-processing/standard-info/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '缺陷标准数据信息库',
|
||||||
|
icon: 'line-chart',
|
||||||
|
hidden: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tower-monitoring',
|
||||||
|
name: 'TowerMonitoring',
|
||||||
|
component: () => import('@/components/ParentView/index.vue'),
|
||||||
|
redirect: '/tower-monitoring/clearance-monitoring',
|
||||||
|
meta: {
|
||||||
|
title: '音视频检测',
|
||||||
|
icon: 'monitor',
|
||||||
|
hidden: false,
|
||||||
|
sort: 6.5,
|
||||||
},
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/tower-monitoring/clearance-monitoring',
|
||||||
|
name: 'ClearanceMonitoring',
|
||||||
|
component: () => import('@/views/tower-monitoring/deformation.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '净空监测',
|
||||||
|
icon: 'fullscreen',
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tower-monitoring/deformation-monitoring',
|
||||||
|
name: 'DeformationMonitoring',
|
||||||
|
component: () => import('@/views/tower-monitoring/clearance.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '形变监测',
|
||||||
|
icon: 'line-chart',
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tower-monitoring/whistle-monitoring',
|
||||||
|
name: 'WhistleMonitoring',
|
||||||
|
component: () => import('@/views/tower-monitoring/whistle.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '哨声监测',
|
||||||
|
icon: 'sound',
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/tower-monitoring/vibration-monitoring',
|
||||||
|
name: 'VibrationMonitoring',
|
||||||
|
component: () => import('@/views/tower-monitoring/vibration.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '振动监测',
|
||||||
|
icon: 'shake',
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/image-detection/reporting-center',
|
||||||
|
name: 'ReportingCenter',
|
||||||
|
component: () => import('@/views/tower-monitoring/vibration.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '报告中心',
|
||||||
|
icon: 'shake',
|
||||||
|
hidden: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// path: '/user/profile',
|
// path: '/chat-platform',
|
||||||
// name: 'UserProfile',
|
// name: 'ChatPlatform',
|
||||||
// component: Layout,
|
// component: Layout,
|
||||||
// redirect: '/user/profile',
|
// redirect: '/chat-platform/options',
|
||||||
// meta: {
|
// meta: { title: '聊天平台', icon: 'message', hidden: false, sort: 6 },
|
||||||
// title: '个人中心',
|
|
||||||
// icon: 'user',
|
|
||||||
// hidden: false,
|
|
||||||
// sort: 100,
|
|
||||||
// },
|
|
||||||
// children: [
|
// children: [
|
||||||
// {
|
|
||||||
// path: '/user/profile',
|
|
||||||
// name: 'UsersProfile',
|
|
||||||
// component: () => import('@/views/user/profile/index.vue'),
|
|
||||||
// meta: {
|
|
||||||
// title: '个人中心',
|
|
||||||
// icon: 'user',
|
|
||||||
// hidden: false,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// ],
|
// ],
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
|
|
|
@ -73,18 +73,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="props" tap="形变" title="形变原数据">
|
|
||||||
<div class="tab-content">
|
|
||||||
<raw-data>
|
|
||||||
|
|
||||||
</raw-data>
|
|
||||||
</div>
|
|
||||||
</a-tab-pane>
|
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 文件预览模态框(待重新设计) -->
|
|
||||||
<!-- <PreviewModal ref="previewModal" /> -->
|
|
||||||
</GiPageLayout>
|
</GiPageLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -107,8 +97,6 @@ import {
|
||||||
} from '@/apis/industrial-image'
|
} from '@/apis/industrial-image'
|
||||||
import DeformationTap from './components/DeformationTap.vue'
|
import DeformationTap from './components/DeformationTap.vue'
|
||||||
|
|
||||||
// 预览弹窗引用(待重新设计)
|
|
||||||
// const previewModal = ref()
|
|
||||||
// 活动选项卡
|
// 活动选项卡
|
||||||
const activeTab = ref('image')
|
const activeTab = ref('image')
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,498 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,665 @@
|
||||||
|
<template>
|
||||||
|
<GiPageLayout>
|
||||||
|
<div class="raw-data-container">
|
||||||
|
<!-- 顶部页签 -->
|
||||||
|
<a-tabs v-model:active-key="topTab" type="rounded" destroy-on-hide>
|
||||||
|
<!-- 原始数据 -->
|
||||||
|
<a-tab-pane key="data" title="原始数据">
|
||||||
|
<!-- 顶部按钮 -->
|
||||||
|
<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 />
|
||||||
|
</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-tab-pane>
|
||||||
|
|
||||||
|
<!-- 净空报告 -->
|
||||||
|
<a-tab-pane key="report" title="净空报告">
|
||||||
|
<!-- 筛选 -->
|
||||||
|
<a-form :model="reportFilterForm" layout="inline" style="margin-bottom: 16px">
|
||||||
|
<a-form-item label="项目">
|
||||||
|
<a-select v-model="reportFilterForm.projectId" placeholder="请选择项目" :options="projectOptions"
|
||||||
|
allow-clear />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="机组">
|
||||||
|
<a-select v-model="reportFilterForm.turbineId" placeholder="请选择机组"
|
||||||
|
:options="turbineReportOptions" allow-clear :disabled="!reportFilterForm.projectId" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button type="primary" @click="loadReportGrid">查询</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<!-- 网格 -->
|
||||||
|
<a-row :gutter="[24, 24]">
|
||||||
|
<a-col v-for="item in reportGrid" :key="item.videoId" :xs="24" :sm="12" :md="8" :lg="6">
|
||||||
|
<!-- 卡片内部 -->
|
||||||
|
<a-card hoverable>
|
||||||
|
<div @click="openReportDetail(item)" style="cursor: pointer">
|
||||||
|
<video :src="item.videoUrl"
|
||||||
|
style="width: 100%; height: 120px; object-fit: cover; border-radius: 4px" />
|
||||||
|
<div class="video-name">{{ item.videoName }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-row :gutter="8" style="margin-top: 8px">
|
||||||
|
<a-col :span="8">
|
||||||
|
<div class="stat-label">最小</div>
|
||||||
|
<div class="stat-value">{{ item.min }} m</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<div class="stat-label">最大</div>
|
||||||
|
<div class="stat-value">{{ item.max }} m</div>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="8">
|
||||||
|
<div class="stat-label">平均</div>
|
||||||
|
<div class="stat-value">{{ item.avg }} m</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<!-- 查看详情按钮 -->
|
||||||
|
<a-button size="mini" style="margin-top: 8px; width: 100%"
|
||||||
|
@click="openReportDetail(item)">
|
||||||
|
查看详情
|
||||||
|
</a-button>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 新增:报告详情模态框 -->
|
||||||
|
<a-modal v-model:visible="reportDetailVisible" title="净空报告详情" width="900px" :footer="false"
|
||||||
|
@cancel="reportDetailVisible = false">
|
||||||
|
<a-tabs size="small">
|
||||||
|
<a-tab-pane key="trend" title="趋势数据">
|
||||||
|
<a-table :columns="trendColumns" :data="currentReportDetail?.trend" size="small"
|
||||||
|
:scroll="{ y: 400 }" row-key="frame" />
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="json" title="原始 JSON">
|
||||||
|
<pre style="max-height: 400px; overflow: auto">{{
|
||||||
|
JSON.stringify(currentReportDetail?.rawJson, null, 2)
|
||||||
|
}}</pre>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
</a-modal>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
|
||||||
|
<!-- 上传弹窗(已去掉处理结果) -->
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 视频预览弹窗(仅保留原始视频) -->
|
||||||
|
<a-modal v-model:visible="previewVisible" title="视频预览" width="800px" :footer="false"
|
||||||
|
@cancel="previewVisible = false">
|
||||||
|
<a-tabs v-model:active-key="activePreviewTab">
|
||||||
|
<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-tabs>
|
||||||
|
</a-modal>
|
||||||
|
</GiPageLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, watch, computed } 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 reportDetailVisible = ref(false)
|
||||||
|
const currentReportDetail = ref<ReportItem | null>(null)
|
||||||
|
|
||||||
|
function openReportDetail(item: ReportItem) {
|
||||||
|
currentReportDetail.value = item
|
||||||
|
reportDetailVisible.value = true
|
||||||
|
}
|
||||||
|
/* ---------------- 列表 ---------------- */
|
||||||
|
const columns: TableColumnData[] = [
|
||||||
|
{ title: '文件名', dataIndex: 'videoName', ellipsis: true, width: 220 },
|
||||||
|
{ 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 })
|
||||||
|
|
||||||
|
/* ---------------- 顶部 Tab ---------------- */
|
||||||
|
const topTab = ref<'data' | 'report'>('data')
|
||||||
|
|
||||||
|
/* ---------------- 报告筛选 ---------------- */
|
||||||
|
const reportFilterForm = reactive({
|
||||||
|
projectId: '',
|
||||||
|
turbineId: ''
|
||||||
|
})
|
||||||
|
const turbineReportOptions = ref<{ label: string; value: string }[]>([])
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => reportFilterForm.projectId,
|
||||||
|
async (val) => {
|
||||||
|
reportFilterForm.turbineId = ''
|
||||||
|
turbineReportOptions.value = []
|
||||||
|
if (!val) return
|
||||||
|
const { data } = await getTurbineList({ projectId: val })
|
||||||
|
turbineReportOptions.value = data.map((t: any) => ({
|
||||||
|
label: t.turbineName,
|
||||||
|
value: t.turbineId
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ---------------- 报告网格 ---------------- */
|
||||||
|
interface ReportItem {
|
||||||
|
videoId: string
|
||||||
|
videoName: string
|
||||||
|
videoUrl: string
|
||||||
|
min: string
|
||||||
|
max: string
|
||||||
|
avg: string
|
||||||
|
trend: any[]
|
||||||
|
rawJson: any
|
||||||
|
_activeKey?: string[]
|
||||||
|
}
|
||||||
|
const reportGrid = ref<ReportItem[]>([])
|
||||||
|
|
||||||
|
async function loadReportGrid() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 1000,
|
||||||
|
projectId: reportFilterForm.projectId,
|
||||||
|
turbineId: reportFilterForm.turbineId || undefined
|
||||||
|
}
|
||||||
|
const { data } = await getVideoPage(params)
|
||||||
|
const rows = data.filter(
|
||||||
|
(r: any) => r.type === 'clearance' && r.preTreatment
|
||||||
|
)
|
||||||
|
const base = import.meta.env.VITE_API_BASE_URL.replace(/\/+$/, '')
|
||||||
|
|
||||||
|
reportGrid.value = await Promise.all(
|
||||||
|
rows.map(async (r: any) => {
|
||||||
|
const jsonUrl = `${base}${r.preImagePath}/results.json`
|
||||||
|
const json = await (await fetch(jsonUrl)).json()
|
||||||
|
|
||||||
|
const all = Object.values(json.clearance_history || {})
|
||||||
|
.flat()
|
||||||
|
.filter((v: any) => typeof v === 'number')
|
||||||
|
const min = Math.min(...all)
|
||||||
|
const max = Math.max(...all)
|
||||||
|
const avg = all.reduce((a: number, b: number) => a + b, 0) / all.length
|
||||||
|
|
||||||
|
const { tip0, tip1, tip2 } = json.clearance_history || {}
|
||||||
|
const len = Math.max(tip0?.length ?? 0, tip1?.length ?? 0, tip2?.length ?? 0)
|
||||||
|
const trend = Array.from({ length: len }, (_, i) => ({
|
||||||
|
frame: i + 1,
|
||||||
|
tip0: tip0?.[i]?.toFixed(3) ?? '-',
|
||||||
|
tip1: tip1?.[i]?.toFixed(3) ?? '-',
|
||||||
|
tip2: tip2?.[i]?.toFixed(3) ?? '-'
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
videoId: r.videoId,
|
||||||
|
videoName: r.videoName,
|
||||||
|
videoUrl: new URL(r.videoPath.replace(/^\/+/, ''), base).href,
|
||||||
|
min: min.toFixed(2),
|
||||||
|
max: max.toFixed(2),
|
||||||
|
avg: avg.toFixed(2),
|
||||||
|
trend,
|
||||||
|
rawJson: json,
|
||||||
|
_activeKey: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDetail(item: ReportItem) {
|
||||||
|
item._activeKey = item._activeKey?.length ? [] : ['detail']
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 原列表逻辑 ---------------- */
|
||||||
|
onMounted(async () => {
|
||||||
|
const { data } = await getProjectList({ page: 1, pageSize: 1000 })
|
||||||
|
projectOptions.value = data.map((p: any) => ({
|
||||||
|
label: p.projectName,
|
||||||
|
value: p.projectId
|
||||||
|
}))
|
||||||
|
handleQuery()
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ---------------- 列表查询 ---------------- */
|
||||||
|
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)
|
||||||
|
tableData.value = data
|
||||||
|
pagination.total = data.length
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 上传 ---------------- */
|
||||||
|
const showUploadModal = ref(false)
|
||||||
|
const uploading = ref(false)
|
||||||
|
|
||||||
|
function openUploadModal() {
|
||||||
|
uploadForm.projectId = ''
|
||||||
|
uploadForm.turbineId = ''
|
||||||
|
uploadForm.type = ''
|
||||||
|
uploadForm.fileList = []
|
||||||
|
showUploadModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
// 如果当前在报告页,顺带刷新
|
||||||
|
if (topTab.value === 'report') await loadReportGrid()
|
||||||
|
} 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()
|
||||||
|
if (topTab.value === 'report') await loadReportGrid()
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 预览 ---------------- */
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
const previewUrl = ref('')
|
||||||
|
const activePreviewTab = ref<'video' | 'result'>('video')
|
||||||
|
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- 趋势表格列 ---------------- */
|
||||||
|
const trendColumns = [
|
||||||
|
{ title: '帧', dataIndex: 'frame', width: 70 },
|
||||||
|
{ title: 'tip0 (m)', dataIndex: 'tip0' },
|
||||||
|
{ title: 'tip1 (m)', dataIndex: 'tip1' },
|
||||||
|
{ title: 'tip2 (m)', dataIndex: 'tip2' }
|
||||||
|
]
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-name {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,497 @@
|
||||||
|
<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)
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
// 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>
|
|
@ -0,0 +1,497 @@
|
||||||
|
<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)
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
// 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>
|
Loading…
Reference in New Issue