1112 lines
26 KiB
Vue
1112 lines
26 KiB
Vue
<template>
|
||
<div class="algorithm-showcase-container">
|
||
<!-- 左侧算法服务一览 -->
|
||
<div class="left-sidebar">
|
||
<div class="sidebar-header">
|
||
<h2>AI算法</h2>
|
||
<p>{{ algorithmStore.algorithms.length }}个算法</p>
|
||
</div>
|
||
|
||
<div class="algorithm-categories">
|
||
<div
|
||
v-for="(category, key) in categories"
|
||
:key="key"
|
||
class="category-item"
|
||
>
|
||
<div class="category-header">
|
||
<div class="category-icon" :style="{ backgroundColor: category.color }">
|
||
<component :is="category.icon" />
|
||
</div>
|
||
<h3>{{ category.name }}</h3>
|
||
<span class="algorithm-count">{{ getAlgorithmsByCategory(key).length }}个算法</span>
|
||
</div>
|
||
|
||
<div class="algorithm-list">
|
||
<div
|
||
v-for="algorithm in getAlgorithmsByCategory(key)"
|
||
:key="algorithm.id"
|
||
class="algorithm-item"
|
||
:class="{ active: selectedAlgorithm && selectedAlgorithm.id === algorithm.id }"
|
||
@click="selectAlgorithm(algorithm)"
|
||
>
|
||
<span class="algorithm-name">{{ algorithm.name }}</span>
|
||
<el-tag size="small" type="info">{{ getOutputTypeName(algorithm.output_type) }}</el-tag>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中间内容区域 -->
|
||
<div class="main-content">
|
||
<!-- 视频/图片展示 -->
|
||
<div class="media-display">
|
||
<div v-if="!uploadedFile" class="media-placeholder">
|
||
<el-icon class="play-icon"><VideoPlay /></el-icon>
|
||
<p>请上传或选择演示素材</p>
|
||
<p class="support-text">支持视频和图片格式</p>
|
||
</div>
|
||
|
||
<div v-else-if="isImageFile" class="image-preview">
|
||
<img :src="uploadedFile.url" alt="上传图片" />
|
||
</div>
|
||
|
||
<div v-else-if="isVideoFile" class="video-preview">
|
||
<video controls :src="uploadedFile.url">
|
||
您的浏览器不支持视频播放
|
||
</video>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 素材上传 -->
|
||
<div class="upload-section">
|
||
<div class="section-header">
|
||
<h3>素材上传</h3>
|
||
<el-tag v-if="!userStore.isLoggedIn" type="warning" size="small">
|
||
<el-icon><Warning /></el-icon>
|
||
需要登录
|
||
</el-tag>
|
||
</div>
|
||
<div class="upload-tabs">
|
||
<el-tabs v-model="activeUploadTab">
|
||
<el-tab-pane label="视频数据" name="video">
|
||
<div class="upload-area">
|
||
<el-upload
|
||
class="upload-demo"
|
||
drag
|
||
action="#"
|
||
:auto-upload="false"
|
||
:on-change="handleFileChange"
|
||
:accept="'.mp4,.avi,.mov,.wmv'"
|
||
>
|
||
<el-icon class="el-icon--upload"><Upload /></el-icon>
|
||
<div class="el-upload__text">点击或拖拽文件到此处上传</div>
|
||
<template #tip>
|
||
<div class="el-upload__tip">
|
||
支持 MP4、AVI、MOV 格式
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
|
||
<div class="demo-videos">
|
||
<h4>演示视频:</h4>
|
||
<div class="demo-video-list">
|
||
<div
|
||
v-for="(demo, index) in demoVideos"
|
||
:key="index"
|
||
class="demo-video-item"
|
||
@click="selectDemoVideo(demo)"
|
||
>
|
||
<div class="demo-video-thumbnail">
|
||
<el-icon class="play-icon"><VideoPlay /></el-icon>
|
||
</div>
|
||
<span>{{ demo.name }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="图片/图表" name="image">
|
||
<div class="upload-area">
|
||
<el-upload
|
||
class="upload-demo"
|
||
drag
|
||
action="#"
|
||
:auto-upload="false"
|
||
:on-change="handleFileChange"
|
||
:accept="'.jpg,.jpeg,.png,.gif'"
|
||
>
|
||
<el-icon class="el-icon--upload"><Upload /></el-icon>
|
||
<div class="el-upload__text">点击或拖拽文件到此处上传</div>
|
||
<template #tip>
|
||
<div class="el-upload__tip">
|
||
支持 JPG、PNG、GIF 格式
|
||
</div>
|
||
</template>
|
||
</el-upload>
|
||
|
||
<div class="demo-images">
|
||
<h4>演示图片:</h4>
|
||
<div class="demo-image-list">
|
||
<div
|
||
v-for="(demo, index) in demoImages"
|
||
:key="index"
|
||
class="demo-image-item"
|
||
@click="selectDemoImage(demo)"
|
||
>
|
||
<img :src="demo.url" :alt="demo.name" />
|
||
<span>{{ demo.name }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-tab-pane>
|
||
|
||
<el-tab-pane label="标注工具" name="annotation">
|
||
<div class="annotation-tools">
|
||
<p>标注工具功能开发中...</p>
|
||
</div>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 算法执行 -->
|
||
<div class="algorithm-execution">
|
||
<div class="section-header">
|
||
<h3>算法执行</h3>
|
||
<el-tag v-if="!userStore.isLoggedIn" type="warning" size="small">
|
||
<el-icon><Warning /></el-icon>
|
||
需要登录
|
||
</el-tag>
|
||
</div>
|
||
<div class="execution-form">
|
||
<el-form :inline="true" class="execution-form">
|
||
<el-form-item label="选择算法模型">
|
||
<el-select
|
||
v-model="selectedModel"
|
||
placeholder="请选择算法..."
|
||
style="width: 200px"
|
||
>
|
||
<el-option
|
||
v-for="algorithm in algorithmStore.algorithms"
|
||
:key="algorithm.id"
|
||
:label="algorithm.name"
|
||
:value="algorithm.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button
|
||
type="primary"
|
||
@click="executeAlgorithm"
|
||
:loading="executing"
|
||
:disabled="!selectedAlgorithm || !uploadedFile"
|
||
>
|
||
<el-icon><VideoPlay /></el-icon>
|
||
运行算法
|
||
</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<div v-if="executionProgress > 0" class="execution-progress">
|
||
<el-progress
|
||
:percentage="executionProgress"
|
||
:status="executionStatus"
|
||
/>
|
||
<p class="progress-text">{{ executionMessage }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧运行结果 -->
|
||
<div class="right-sidebar">
|
||
<div class="sidebar-header">
|
||
<h2>运行结果</h2>
|
||
</div>
|
||
|
||
<div v-if="!executionResult" class="result-placeholder">
|
||
<el-icon class="chart-icon"><DataAnalysis /></el-icon>
|
||
<p>等待算法执行</p>
|
||
<p class="placeholder-text">执行完成后将在此展示</p>
|
||
</div>
|
||
|
||
<div v-else class="result-content">
|
||
<div class="result-header">
|
||
<h3>{{ selectedAlgorithm && selectedAlgorithm.name || '算法' }} 执行结果</h3>
|
||
<span class="execution-time">
|
||
执行时间: {{ executionResult.response_time }}ms
|
||
</span>
|
||
</div>
|
||
|
||
<div class="result-details">
|
||
<el-descriptions :column="1" border>
|
||
<el-descriptions-item label="执行状态">
|
||
<el-tag :type="executionResult.status === 'success' ? 'success' : 'danger'">
|
||
{{ executionResult.status === 'success' ? '成功' : '失败' }}
|
||
</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="输出类型">
|
||
{{ getOutputTypeName(selectedAlgorithm && selectedAlgorithm.output_type || '') }}
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</div>
|
||
|
||
<div class="result-display">
|
||
<h4>输出结果</h4>
|
||
|
||
<div v-if="isImageResult" class="result-image">
|
||
<img :src="executionResult.output_data.result?.image || executionResult.output_data.image" alt="结果图片" />
|
||
</div>
|
||
|
||
<div v-else-if="isVideoResult" class="result-video">
|
||
<video
|
||
controls
|
||
autoplay
|
||
loop
|
||
:src="getVideoResultUrl()"
|
||
@error="handleVideoError"
|
||
>
|
||
您的浏览器不支持视频播放
|
||
</video>
|
||
</div>
|
||
|
||
<div v-else-if="isTextResult" class="result-text">
|
||
<pre>{{ executionResult.output_data.text }}</pre>
|
||
</div>
|
||
|
||
<div v-else class="result-json">
|
||
<pre>{{ JSON.stringify(executionResult.output_data, null, 2) }}</pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import axios from 'axios'
|
||
import { useAlgorithmStore } from '../stores/algorithm'
|
||
import { useUserStore } from '../stores/user'
|
||
import {
|
||
VideoPlay,
|
||
Upload,
|
||
DataAnalysis,
|
||
Grid,
|
||
MagicStick,
|
||
ArrowUp,
|
||
Camera,
|
||
Star,
|
||
Warning
|
||
} from '@element-plus/icons-vue'
|
||
|
||
// 获取路由和存储
|
||
const router = useRouter()
|
||
const algorithmStore = useAlgorithmStore()
|
||
const userStore = useUserStore()
|
||
|
||
// 响应式数据
|
||
const selectedAlgorithm = ref<any>(null)
|
||
const uploadedFile = ref<any>(null)
|
||
const selectedModel = ref('')
|
||
const executing = ref(false)
|
||
const executionProgress = ref(0)
|
||
const executionStatus = ref('')
|
||
const executionMessage = ref('')
|
||
const executionResult = ref<any>(null)
|
||
const activeUploadTab = ref('video')
|
||
|
||
// WebSocket 连接
|
||
let wsConnection: WebSocket | null = null
|
||
const realtimeDetections = ref<any[]>([])
|
||
const isRealtimeMode = ref(false)
|
||
|
||
// 连接到 WebSocket 服务器
|
||
const connectWebSocket = () => {
|
||
const wsUrl = `ws://localhost:8766`
|
||
|
||
try {
|
||
wsConnection = new WebSocket(wsUrl)
|
||
|
||
wsConnection.onopen = () => {
|
||
console.log('[WS] Connected to WebSocket server')
|
||
isRealtimeMode.value = true
|
||
}
|
||
|
||
wsConnection.onmessage = (event) => {
|
||
try {
|
||
const data = JSON.parse(event.data)
|
||
console.log('[WS] Received:', data)
|
||
|
||
if (data.msg_type === 'frame') {
|
||
realtimeDetections.value.push(data)
|
||
// 保持最近100帧
|
||
if (realtimeDetections.value.length > 100) {
|
||
realtimeDetections.value.shift()
|
||
}
|
||
} else if (data.msg_type === 'alert') {
|
||
ElMessage.warning(`检测到异常: ${data.event_type}`)
|
||
}
|
||
} catch (e) {
|
||
console.error('[WS] Parse error:', e)
|
||
}
|
||
}
|
||
|
||
wsConnection.onerror = (error) => {
|
||
console.error('[WS] Error:', error)
|
||
}
|
||
|
||
wsConnection.onclose = () => {
|
||
console.log('[WS] Disconnected')
|
||
isRealtimeMode.value = false
|
||
}
|
||
} catch (e) {
|
||
console.error('[WS] Connection failed:', e)
|
||
}
|
||
}
|
||
|
||
// 断开 WebSocket 连接
|
||
const disconnectWebSocket = () => {
|
||
if (wsConnection) {
|
||
wsConnection.close()
|
||
wsConnection = null
|
||
}
|
||
isRealtimeMode.value = false
|
||
realtimeDetections.value = []
|
||
}
|
||
|
||
// 分类配置
|
||
const categories = {
|
||
computer_vision: {
|
||
name: '计算机视觉',
|
||
icon: Camera,
|
||
color: '#409EFF'
|
||
},
|
||
video_processing: {
|
||
name: '视频处理',
|
||
icon: VideoPlay,
|
||
color: '#67C23A'
|
||
},
|
||
nlp: {
|
||
name: '自然语言处理',
|
||
icon: Star,
|
||
color: '#E6A23C'
|
||
},
|
||
ml: {
|
||
name: '机器学习',
|
||
icon: MagicStick,
|
||
color: '#909399'
|
||
},
|
||
edge_computing: {
|
||
name: '边缘计算',
|
||
icon: Grid,
|
||
color: '#F56C6C'
|
||
},
|
||
medical: {
|
||
name: '医疗算法',
|
||
icon: DataAnalysis,
|
||
color: '#722ED1'
|
||
},
|
||
autonomous_driving: {
|
||
name: '自动驾驶算法',
|
||
icon: ArrowUp,
|
||
color: '#FAAD14'
|
||
}
|
||
}
|
||
|
||
// 演示视频
|
||
const demoVideos = [
|
||
{ id: 1, name: '街道交通监控', url: '' },
|
||
{ id: 2, name: '人群行为分析', url: '' }
|
||
]
|
||
|
||
// 演示图片
|
||
const demoImages = [
|
||
{ id: 1, name: '城市街景', url: '' },
|
||
{ id: 2, name: '人物肖像', url: '' }
|
||
]
|
||
|
||
// 输出类型映射
|
||
const outputTypes = {
|
||
image: '图片',
|
||
video: '视频',
|
||
text: '文本',
|
||
json: 'JSON',
|
||
audio: '音频'
|
||
}
|
||
|
||
// 计算属性
|
||
const getOutputTypeName = (type: string) => {
|
||
return outputTypes[type as keyof typeof outputTypes] || type
|
||
}
|
||
|
||
const getAlgorithmsByCategory = (category: string) => {
|
||
return algorithmStore.algorithms.filter(algorithm => algorithm.tech_category === category)
|
||
}
|
||
|
||
const isImageFile = computed(() => {
|
||
if (!uploadedFile.value) return false
|
||
return uploadedFile.value.type.startsWith('image/')
|
||
})
|
||
|
||
const isVideoFile = computed(() => {
|
||
if (!uploadedFile.value) return false
|
||
return uploadedFile.value.type.startsWith('video/')
|
||
})
|
||
|
||
const isImageResult = computed(() => {
|
||
if (!executionResult.value || !selectedAlgorithm.value) return false
|
||
const outputData = executionResult.value.output_data || {}
|
||
const result = outputData.result || outputData
|
||
|
||
// 检查是否有图片数据
|
||
if (result.image || result.image_url || result.output_image) return true
|
||
return false
|
||
})
|
||
|
||
const isVideoResult = computed(() => {
|
||
if (!executionResult.value || !selectedAlgorithm.value) return false
|
||
const outputData = executionResult.value.output_data || {}
|
||
const result = outputData.result || outputData
|
||
|
||
// 检查是否有视频数据
|
||
if (result.video || result.video_url || result.output_video) return true
|
||
return false
|
||
})
|
||
|
||
const isTextResult = computed(() => {
|
||
if (!executionResult.value || !selectedAlgorithm.value) return false
|
||
return selectedAlgorithm.value.output_type === 'text'
|
||
})
|
||
|
||
const getVideoResultUrl = () => {
|
||
if (!executionResult.value || !executionResult.value.output_data) return ''
|
||
|
||
const outputData = executionResult.value.output_data || {}
|
||
const result = outputData.result || outputData
|
||
|
||
// 检查是否是base64编码的视频数据
|
||
if (result.video && typeof result.video === 'string' && result.video.startsWith('data:')) {
|
||
return result.video
|
||
}
|
||
|
||
// 检查是否有video_url
|
||
if (result.video_url) {
|
||
console.log('[DEBUG] video_url 原始值:', result.video_url)
|
||
const url = result.video_url
|
||
console.log('[DEBUG] video_url 返回值:', url)
|
||
return url
|
||
}
|
||
|
||
// 检查是否有output_video
|
||
if (result.output_video) {
|
||
return result.output_video
|
||
}
|
||
|
||
return ''
|
||
}
|
||
|
||
const handleVideoError = (event: Event) => {
|
||
console.error('视频加载失败:', event)
|
||
ElMessage.error('视频加载失败,请检查算法输出格式')
|
||
}
|
||
|
||
// 方法
|
||
const selectAlgorithm = (algorithm: any) => {
|
||
selectedAlgorithm.value = algorithm
|
||
selectedModel.value = algorithm.id
|
||
}
|
||
|
||
const handleFileChange = async (file: any) => {
|
||
// 检查用户是否登录
|
||
if (!userStore.isLoggedIn) {
|
||
ElMessage.warning('请先登录后再上传文件')
|
||
router.push('/login')
|
||
return
|
||
}
|
||
|
||
// 检查是否选择了算法
|
||
if (!selectedAlgorithm.value) {
|
||
ElMessage.warning('请先选择一个算法')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 创建临时URL用于预览
|
||
const url = URL.createObjectURL(file.raw)
|
||
|
||
// 上传文件到MinIO
|
||
const formData = new FormData()
|
||
formData.append('file', file.raw)
|
||
formData.append('algorithm_id', selectedAlgorithm.value.id)
|
||
|
||
// 调用后端API上传文件
|
||
const response = await axios.post('/api/v1/data/media/upload', formData, {
|
||
headers: { 'Content-Type': 'multipart/form-data' }
|
||
})
|
||
|
||
uploadedFile.value = {
|
||
name: file.name,
|
||
type: file.raw.type,
|
||
url: url,
|
||
file: file.raw,
|
||
uploaded: true,
|
||
filePath: response.data.file_path
|
||
}
|
||
|
||
ElMessage.success('文件上传成功')
|
||
} catch (error: any) {
|
||
console.error('文件上传失败:', error)
|
||
ElMessage.error(error.response?.data?.detail || '文件上传失败')
|
||
|
||
// 即使上传失败,也使用临时URL
|
||
const url = URL.createObjectURL(file.raw)
|
||
uploadedFile.value = {
|
||
name: file.name,
|
||
type: file.raw.type,
|
||
url: url,
|
||
file: file.raw,
|
||
uploaded: false
|
||
}
|
||
}
|
||
}
|
||
|
||
const selectDemoVideo = (demo: any) => {
|
||
// 这里可以设置演示视频的URL
|
||
uploadedFile.value = {
|
||
name: demo.name,
|
||
type: 'video/mp4',
|
||
url: demo.url || '',
|
||
demo: true
|
||
}
|
||
}
|
||
|
||
const selectDemoImage = (demo: any) => {
|
||
// 这里可以设置演示图片的URL
|
||
uploadedFile.value = {
|
||
name: demo.name,
|
||
type: 'image/jpeg',
|
||
url: demo.url || '',
|
||
demo: true
|
||
}
|
||
}
|
||
|
||
const executeAlgorithm = async () => {
|
||
if (!selectedAlgorithm.value || !uploadedFile.value) {
|
||
ElMessage.warning('请先选择算法和上传文件')
|
||
return
|
||
}
|
||
|
||
executing.value = true
|
||
executionProgress.value = 0
|
||
executionStatus.value = 'success'
|
||
executionMessage.value = '正在执行算法...'
|
||
|
||
try {
|
||
// 获取算法的默认版本
|
||
const defaultVersion = selectedAlgorithm.value.versions?.find((v: any) => v.is_default)
|
||
if (!defaultVersion) {
|
||
ElMessage.error('该算法没有可用的版本')
|
||
executing.value = false
|
||
return
|
||
}
|
||
|
||
// 准备调用参数 - 使用文件路径或MinIO路径
|
||
const callRequest = {
|
||
algorithm_id: selectedAlgorithm.value.id,
|
||
version_id: defaultVersion.id,
|
||
input_data: {
|
||
video: uploadedFile.value.filePath || uploadedFile.value.url
|
||
},
|
||
params: {
|
||
ws_url: 'ws://localhost:8766'
|
||
}
|
||
}
|
||
|
||
// 调用算法API
|
||
executionProgress.value = 30
|
||
executionMessage.value = '正在调用算法...'
|
||
console.log('Calling algorithm API with:', callRequest)
|
||
|
||
const response = await axios.post('/api/v1/algorithms/call/public', callRequest)
|
||
console.log('Algorithm response:', response.data)
|
||
|
||
executionProgress.value = 80
|
||
executionMessage.value = '正在处理结果...'
|
||
|
||
// 设置执行结果
|
||
executionResult.value = {
|
||
id: response.data.id,
|
||
status: response.data.status,
|
||
output_data: response.data.output_data,
|
||
response_time: response.data.response_time,
|
||
error_message: response.data.error_message
|
||
}
|
||
|
||
executionProgress.value = 100
|
||
executionMessage.value = '算法执行完成!'
|
||
|
||
ElMessage.success('算法执行成功')
|
||
} catch (error: any) {
|
||
console.error('算法执行失败:', error)
|
||
executionStatus.value = 'exception'
|
||
executionMessage.value = '算法执行失败'
|
||
|
||
executionResult.value = {
|
||
id: 'error-' + Date.now(),
|
||
status: 'error',
|
||
output_data: {},
|
||
response_time: 0,
|
||
error_message: error.response?.data?.detail || '执行失败'
|
||
}
|
||
|
||
ElMessage.error(error.response?.data?.detail || '算法执行失败')
|
||
} finally {
|
||
executing.value = false
|
||
}
|
||
}
|
||
|
||
// 初始化
|
||
onMounted(async () => {
|
||
await algorithmStore.fetchAlgorithms()
|
||
|
||
// 默认选择第一个算法
|
||
if (algorithmStore.algorithms.length > 0) {
|
||
selectAlgorithm(algorithmStore.algorithms[0])
|
||
}
|
||
|
||
// 连接 WebSocket 服务器
|
||
connectWebSocket()
|
||
})
|
||
|
||
// 组件卸载时断开 WebSocket
|
||
onUnmounted(() => {
|
||
disconnectWebSocket()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.algorithm-showcase-container {
|
||
display: flex;
|
||
height: 100vh;
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
/* 左侧算法服务一览 */
|
||
.left-sidebar {
|
||
width: 300px;
|
||
background-color: #fff;
|
||
border-right: 1px solid #e4e7ed;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 20px;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.sidebar-header h2 {
|
||
font-size: 18px;
|
||
margin: 0 0 5px 0;
|
||
color: #303133;
|
||
}
|
||
|
||
.sidebar-header p {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
margin: 0;
|
||
}
|
||
|
||
.algorithm-categories {
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.category-item {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.category-header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 20px 10px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.category-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 10px;
|
||
color: #fff;
|
||
}
|
||
|
||
.category-header h3 {
|
||
font-size: 14px;
|
||
margin: 0;
|
||
flex: 1;
|
||
color: #303133;
|
||
}
|
||
|
||
.algorithm-count {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.algorithm-list {
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.algorithm-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 10px 15px;
|
||
margin-bottom: 5px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.algorithm-item:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.algorithm-item.active {
|
||
background-color: #ecf5ff;
|
||
border-left: 3px solid #409EFF;
|
||
}
|
||
|
||
.algorithm-name {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
flex: 1;
|
||
}
|
||
|
||
/* 中间内容区域 */
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* 视频/图片展示 */
|
||
.media-display {
|
||
flex: 1;
|
||
background-color: #1f2937;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
min-height: 400px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.media-placeholder {
|
||
text-align: center;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.play-icon {
|
||
font-size: 64px;
|
||
margin-bottom: 20px;
|
||
color: #4b5563;
|
||
}
|
||
|
||
.media-placeholder p {
|
||
margin: 5px 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.support-text {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
|
||
.image-preview {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.image-preview img {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.video-preview {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.video-preview video {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
}
|
||
|
||
/* 素材上传 */
|
||
.upload-section {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.section-header h3 {
|
||
font-size: 16px;
|
||
margin: 0;
|
||
color: #303133;
|
||
}
|
||
|
||
.upload-area {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.demo-videos {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.demo-videos h4 {
|
||
font-size: 14px;
|
||
margin: 0 0 10px 0;
|
||
color: #606266;
|
||
}
|
||
|
||
.demo-video-list {
|
||
display: flex;
|
||
gap: 15px;
|
||
}
|
||
|
||
.demo-video-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.demo-video-item:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.demo-video-thumbnail {
|
||
width: 100%;
|
||
height: 100px;
|
||
background-color: #f5f7fa;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.demo-video-thumbnail .play-icon {
|
||
font-size: 24px;
|
||
color: #909399;
|
||
}
|
||
|
||
.demo-images {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.demo-images h4 {
|
||
font-size: 14px;
|
||
margin: 0 0 10px 0;
|
||
color: #606266;
|
||
}
|
||
|
||
.demo-image-list {
|
||
display: flex;
|
||
gap: 15px;
|
||
}
|
||
|
||
.demo-image-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.demo-image-item:hover {
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.demo-image-item img {
|
||
width: 100%;
|
||
height: 100px;
|
||
object-fit: cover;
|
||
border-radius: 4px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
/* 算法执行 */
|
||
.algorithm-execution {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.execution-form {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.execution-progress {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.progress-text {
|
||
font-size: 14px;
|
||
color: #606266;
|
||
margin-top: 5px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* 右侧运行结果 */
|
||
.right-sidebar {
|
||
width: 400px;
|
||
background-color: #fff;
|
||
border-left: 1px solid #e4e7ed;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.right-sidebar .sidebar-header {
|
||
padding: 20px;
|
||
border-bottom: 1px solid #e4e7ed;
|
||
}
|
||
|
||
.right-sidebar .sidebar-header h2 {
|
||
font-size: 18px;
|
||
margin: 0;
|
||
color: #303133;
|
||
}
|
||
|
||
.result-placeholder {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 400px;
|
||
text-align: center;
|
||
color: #909399;
|
||
}
|
||
|
||
.chart-icon {
|
||
font-size: 64px;
|
||
margin-bottom: 20px;
|
||
color: #dcdfe6;
|
||
}
|
||
|
||
.result-placeholder p {
|
||
margin: 5px 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.placeholder-text {
|
||
font-size: 14px;
|
||
color: #c0c4cc;
|
||
}
|
||
|
||
.result-content {
|
||
padding: 20px;
|
||
}
|
||
|
||
.result-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.result-header h3 {
|
||
font-size: 16px;
|
||
margin: 0;
|
||
color: #303133;
|
||
}
|
||
|
||
.execution-time {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.result-details {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.result-display {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.result-display h4 {
|
||
font-size: 14px;
|
||
margin: 0 0 15px 0;
|
||
color: #606266;
|
||
}
|
||
|
||
.result-image img,
|
||
.result-video video {
|
||
width: 100%;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.result-text pre,
|
||
.result-json pre {
|
||
background-color: #f5f7fa;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
border: 1px solid #e4e7ed;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
overflow-x: auto;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 1600px) {
|
||
.left-sidebar {
|
||
width: 250px;
|
||
}
|
||
|
||
.right-sidebar {
|
||
width: 350px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 1400px) {
|
||
.left-sidebar {
|
||
width: 200px;
|
||
}
|
||
|
||
.right-sidebar {
|
||
width: 300px;
|
||
}
|
||
}
|
||
|
||
/* 滚动条样式 */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
</style> |