Industrial-image-management.../src/views/operation-platform/data-processing/data-storage/components/raw-data.vue

591 lines
17 KiB
Vue
Raw Normal View History

2025-08-10 20:10:47 +08:00
<template>
<GiPageLayout>
<div class="raw-data-container">
<!-- <div class="page-header">
<div class="page-title">原始数据管理</div>
<div class="page-subtitle">管理和分析原始视频数据</div>
</div> -->
<div class="action-bar">
<div class="action-buttons">
<a-button type="primary" @click="showUploadModal = true">
<template #icon>
<IconUpload />
</template>
上传视频
</a-button>
<a-button type="primary" @click="handleBatchAnalysis">
<template #icon>
<IconPlayCircle />
</template>
批量分析
</a-button>
<a-button type="primary" @click="handleExportData">
<template #icon>
<IconDownload />
</template>
导出数据
</a-button>
</div>
<div class="filter-section">
<a-form :model="filterForm" layout="inline">
<a-form-item label="项目">
<a-select v-model="filterForm.projectId" placeholder="请选择项目">
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item>
<a-form-item label="机组号">
<a-input v-model="filterForm.unitNumber" placeholder="请输入机组号" />
</a-form-item>
<a-form-item label="状态">
<a-select v-model="filterForm.status" placeholder="请选择状态">
<a-option value="completed">已完成</a-option>
<a-option value="pending">待分析</a-option>
<a-option value="analyzing">分析中</a-option>
<a-option value="failed">失败</a-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleFilterChange">查询</a-button>
</a-form-item>
</a-form>
</div>
</div>
<div class="project-sections">
<div v-for="project in filteredProjects" :key="project.id" class="project-section">
<div class="project-header">
<div class="project-title">{{ project.name }}</div>
<div class="project-stats">
<div class="stat-item">
<IconVideoCamera />
{{ project.totalVideos }} 个视频
</div>
<div class="stat-item">
<IconCheckCircle />
{{ project.completedCount }} 个已完成
</div>
<div class="stat-item">
<IconClockCircle />
{{ project.pendingCount }} 个待分析
</div>
</div>
</div>
<div class="units-grid">
<div v-for="unit in project.units" :key="unit.id" class="unit-card">
<div class="unit-header">
<div class="unit-title">{{ unit.number }}</div>
<div class="unit-actions">
<a-button type="primary" size="small" @click="handleViewUnitVideos(unit)">查看全部</a-button>
<a-button type="primary" size="small" @click="handleAnalyzeUnit(unit)">{{
getAnalysisButtonText(unit.status)
}}</a-button>
</div>
</div>
<div class="videos-list">
<div v-for="video in unit.videos" :key="video.id" class="video-item">
<div class="video-thumbnail">
<img :src="video.thumbnail" alt="Video Thumbnail" />
<div class="video-overlay" @click="handlePlayVideo(video)">
<IconPlayArrowFill style="font-size: 24px; color: #fff;" />
</div>
</div>
<div class="video-info">
<div class="video-name">{{ video.name }}</div>
<div class="video-meta">
<span>{{ video.duration }}</span>
<span>{{ video.angle }}°</span>
</div>
<div class="video-status">
<a-tag :color="getStatusColor(video.status)">{{ getStatusText(video.status) }}</a-tag>
</div>
</div>
</div>
</div>
<div class="analysis-progress">
<div class="progress-info">
<span>分析进度</span>
<span>{{ unit.progress }}%</span>
</div>
<a-progress :percent="unit.progress" :show-text="false" status="active" />
</div>
</div>
</div>
</div>
</div>
<!-- 视频播放模态框 -->
<a-modal v-model:visible="videoModalVisible" title="原始视频播放" width="900px" @ok="videoModalVisible = false"
@cancel="videoModalVisible = false">
<video v-if="selectedVideo" :src="selectedVideo.url" controls
style="width: 100%; height: 480px; border-radius: 8px; background: #000;"></video>
<div v-if="selectedVideo" class="video-meta-info">
<p>项目{{ selectedVideo.projectName }}</p>
<p>机组号{{ selectedVideo.unitNumber }}</p>
<p>采集人{{ selectedVideo.collector }}</p>
<p>风速{{ selectedVideo.windSpeed }} m/s</p>
<p>转速{{ selectedVideo.rpm }} rpm</p>
<p>采集时间{{ selectedVideo.time }}</p>
<p>角度{{ selectedVideo.angle }}°</p>
</div>
</a-modal>
<!-- 上传视频模态框 -->
<a-modal v-model:visible="showUploadModal" title="上传原始视频" width="600px" @ok="handleUpload"
@cancel="showUploadModal = false">
<a-form :model="uploadForm" layout="vertical">
<a-form-item label="项目" required>
<a-select v-model="uploadForm.projectId" placeholder="请选择项目">
<a-option value="project-1">风电场A区</a-option>
<a-option value="project-2">风电场B区</a-option>
<a-option value="project-3">风电场C区</a-option>
</a-select>
</a-form-item>
<a-form-item label="机组号" required>
<a-input v-model="uploadForm.unitNumber" placeholder="请输入机组号" />
</a-form-item>
<a-form-item label="采集人" required>
<a-input v-model="uploadForm.collector" placeholder="请输入采集人姓名" />
</a-form-item>
<a-form-item label="风速 (m/s)">
<a-input-number v-model="uploadForm.windSpeed" :min="0" />
</a-form-item>
<a-form-item label="转速 (rpm)">
<a-input-number v-model="uploadForm.rpm" :min="0" />
</a-form-item>
<a-form-item label="采集时间" required>
<a-date-picker v-model="uploadForm.time" show-time format="YYYY-MM-DD HH:mm" style="width: 100%;" />
</a-form-item>
<a-form-item label="视频文件可多选建议3个角度" required>
<a-upload v-model:file-list="uploadForm.fileList" :multiple="true" :limit="3" accept="video/*"
:auto-upload="false" list-type="picture-card">
<template #upload-button>
<a-button>选择视频</a-button>
</template>
</a-upload>
</a-form-item>
</a-form>
</a-modal>
</div>
</GiPageLayout>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue'
import { Message } from '@arco-design/web-vue'
import {
IconUpload,
IconPlayCircle,
IconDownload,
IconVideoCamera,
IconCheckCircle,
IconClockCircle,
IconPlayArrowFill
} from '@arco-design/web-vue/es/icon'
const showUploadModal = ref(false)
const videoModalVisible = ref(false)
const selectedVideo = ref<any>(null)
const filterForm = reactive({
projectId: '',
unitNumber: '',
status: ''
})
const uploadForm = reactive({
projectId: '',
unitNumber: '',
collector: '',
windSpeed: null,
rpm: null,
time: '',
fileList: []
})
// 示例数据结构
const projects = ref([
{
id: 'project-1',
name: '风电场A区',
totalVideos: 6,
completedCount: 4,
pendingCount: 2,
units: [
{
id: 'A-001',
number: 'A-001',
status: 'completed',
progress: 100,
videos: [
{
id: 'v1',
name: 'A-001-正面',
url: '/videos/A-001-front.mp4',
thumbnail: '/images/A-001-front.jpg',
angle: 0,
duration: '00:30',
status: 'completed',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
},
{
id: 'v2',
name: 'A-001-侧面',
url: '/videos/A-001-side.mp4',
thumbnail: '/images/A-001-side.jpg',
angle: 90,
duration: '00:30',
status: 'completed',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
},
{
id: 'v3',
name: 'A-001-背面',
url: '/videos/A-001-back.mp4',
thumbnail: '/images/A-001-back.jpg',
angle: 180,
duration: '00:30',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-001',
collector: '张三',
windSpeed: 8.2,
rpm: 15,
time: '2023-11-05 08:00'
}
]
},
{
id: 'A-002',
number: 'A-002',
status: 'analyzing',
progress: 60,
videos: [
{
id: 'v4',
name: 'A-002-正面',
url: '/videos/A-002-front.mp4',
thumbnail: '/images/A-002-front.jpg',
angle: 0,
duration: '00:28',
status: 'analyzing',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
},
{
id: 'v5',
name: 'A-002-侧面',
url: '/videos/A-002-side.mp4',
thumbnail: '/images/A-002-side.jpg',
angle: 90,
duration: '00:28',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
},
{
id: 'v6',
name: 'A-002-背面',
url: '/videos/A-002-back.mp4',
thumbnail: '/images/A-002-back.jpg',
angle: 180,
duration: '00:28',
status: 'pending',
projectName: '风电场A区',
unitNumber: 'A-002',
collector: '李四',
windSpeed: 7.9,
rpm: 14,
time: '2023-11-05 12:00'
}
]
}
]
}
// ... 其他项目
])
const filteredProjects = computed(() => {
// 按筛选条件过滤
return projects.value
.filter(p => !filterForm.projectId || p.id === filterForm.projectId)
.map(project => ({
...project,
units: project.units
.filter(u => !filterForm.unitNumber || u.number === filterForm.unitNumber)
.map(unit => ({
...unit,
videos: unit.videos.filter(v => !filterForm.status || v.status === filterForm.status)
}))
.filter(u => u.videos.length > 0)
}))
.filter(p => p.units.length > 0)
})
function handleFilterChange() {
// 这里可以加API请求
}
function handlePlayVideo(video: any) {
selectedVideo.value = video
videoModalVisible.value = true
}
function handleViewUnitVideos(unit: any) {
// 可扩展为弹窗或跳转页面,展示该机组所有视频
Message.info(`查看机组 ${unit.number} 的所有视频`)
}
function handleAnalyzeUnit(unit: any) {
// 触发分析流程
Message.success(`已提交机组 ${unit.number} 的分析任务`)
// 实际应调用API并刷新状态
}
function getStatusColor(status: string) {
switch (status) {
case 'completed': return 'green'
case 'pending': return 'gray'
case 'analyzing': return 'blue'
case 'failed': return 'red'
default: return 'gray'
}
}
function getStatusText(status: string) {
switch (status) {
case 'completed': return '已完成'
case 'pending': return '待分析'
case 'analyzing': return '分析中'
case 'failed': return '失败'
default: return '未知'
}
}
function getAnalysisButtonText(status: string) {
switch (status) {
case 'completed': return '重新分析'
case 'pending': return '分析'
case 'analyzing': return '分析中...'
case 'failed': return '重新分析'
default: return '分析'
}
}
function handleBatchAnalysis() {
Message.success('批量分析任务已提交')
}
function handleExportData() {
Message.success('数据导出成功')
}
function handleUpload() {
Message.success('上传成功')
showUploadModal.value = false
}
</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>