完成SCA:蚕茧模块演示效果

This commit is contained in:
BBIT-Kai
2025-06-10 15:32:22 +08:00
parent 89adaf02b9
commit 617cc3162e
19 changed files with 620 additions and 44 deletions
@@ -15,7 +15,8 @@ import io.ktor.server.application.*
import io.ktor.server.tomcat.jakarta.* import io.ktor.server.tomcat.jakarta.*
const val VIDEO_INPUT_PATH = "/tmp/" //const val VIDEO_INPUT_PATH = "/tmp/"
const val VIDEO_INPUT_PATH = "C:/tmp/"
/** /**
* 服务器地址 * 服务器地址
@@ -16,7 +16,6 @@ object ImageDao {
fun insertImageAnalyticsData(request: ImageAnalyticsRequest) { fun insertImageAnalyticsData(request: ImageAnalyticsRequest) {
return transaction { return transaction {
ImageTable.insert { ImageTable.insert {
it[object_name] = request.object_name
it[upload_datetime] = Timestamp.valueOf(request.upload_datetime) it[upload_datetime] = Timestamp.valueOf(request.upload_datetime)
.toLocalDateTime().toKotlinLocalDateTime() .toLocalDateTime().toKotlinLocalDateTime()
it[file_name] = request.file_name it[file_name] = request.file_name
@@ -34,15 +33,19 @@ object ImageDao {
} }
fun getVideoList(): List<ImageAnalyticsRequest> { fun getImageList(name: String): List<ImageAnalyticsRequest> {
return transaction { return transaction {
ImageTable.selectAll() ImageTable.selectAll()
.where { ImageTable.name like "%$name%" }
.orderBy(ImageTable.upload_datetime, SortOrder.DESC) .orderBy(ImageTable.upload_datetime, SortOrder.DESC)
.map { .map {
ImageAnalyticsRequest( ImageAnalyticsRequest(
object_name = "http://${SERVER_PATH_OSS}:9000/image/" + it[ImageTable.object_name], id = it[ImageTable.id].value,
name = it[ImageTable.name],
upload_datetime = formatLocalDateTimeToString(it[ImageTable.upload_datetime]), upload_datetime = formatLocalDateTimeToString(it[ImageTable.upload_datetime]),
file_name = it[ImageTable.file_name], file_name = it[ImageTable.file_name],
image_pre = "http://${SERVER_PATH_OSS}:9000/image-sca/" + it[ImageTable.image_pre],
image_after = "http://${SERVER_PATH_OSS}:9000/image-sca/" + it[ImageTable.image_after],
resolution = it[ImageTable.resolution], resolution = it[ImageTable.resolution],
size = it[ImageTable.size], size = it[ImageTable.size],
cocoon_count = it[ImageTable.cocoon_count], cocoon_count = it[ImageTable.cocoon_count],
@@ -72,7 +72,6 @@ object VideoDao {
} }
} }
fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? { fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? {
return transaction { return transaction {
VideoTable.selectAll().where { VideoTable.id eq vId }.singleOrNull() VideoTable.selectAll().where { VideoTable.id eq vId }.singleOrNull()
@@ -6,10 +6,12 @@ import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
import org.jetbrains.exposed.v1.datetime.datetime import org.jetbrains.exposed.v1.datetime.datetime
import org.jetbrains.exposed.v1.json.json import org.jetbrains.exposed.v1.json.json
object ImageTable : IntIdTable("image") { object ImageTable : IntIdTable("image_sca") {
val object_name = varchar("object_name", 255) val name = varchar("name", 255)
val upload_datetime = datetime("upload_datetime") val upload_datetime = datetime("upload_datetime")
val file_name = varchar("file_name", 255) val file_name = varchar("file_name", 255)
val image_pre = varchar("image_pre", 255)
val image_after = varchar("image_after", 255)
val resolution = varchar("resolution", 255) val resolution = varchar("resolution", 255)
val size = float("size") val size = float("size")
val cocoon_count = float("cocoon_count") val cocoon_count = float("cocoon_count")
@@ -4,10 +4,13 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ImageAnalyticsRequest( data class ImageAnalyticsRequest(
val object_name: String, // Minio存储名 val id : Int,
val name : String,
val upload_datetime: String, // 上传时间 val upload_datetime: String, // 上传时间
val file_name: String, // 文件名 val file_name: String, // 文件名
val resolution: String, // 图片分辨率 val resolution: String, // 图片分辨率
val image_after: String,
val image_pre: String,
val size: Float, // 文件大小,单位MB val size: Float, // 文件大小,单位MB
val cocoon_count: Float, // 识别出的茧数量 val cocoon_count: Float, // 识别出的茧数量
val max_confidence: Float, // 最大置信度 val max_confidence: Float, // 最大置信度
@@ -9,9 +9,9 @@ import io.ktor.server.routing.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
fun Application.ImageAnalytics() { fun Application.ImageAnalytics() {
routing { routing {
route("/api") { route("/api/sca") {
// 上传分析结果 // 上传分析结果
post("/saveImageAnalyticsData") { post("/saveImageAnalyticsData") {
val request = call.receive<ImageAnalyticsRequest>() val request = call.receive<ImageAnalyticsRequest>()
@@ -28,9 +28,10 @@ fun Application.ImageAnalytics() {
stopHLSStream(camera) stopHLSStream(camera)
call.respond(BaseResponse(message = "摄像头流已停止", data = null)) call.respond(BaseResponse(message = "摄像头流已停止", data = null))
} }
// 获取已分析视频列表 // 获取已分析图片列表
get("/getImageList") { get("/getImageList") {
val res = ImageDao.getVideoList() val name = call.parameters["name"]
val res = ImageDao.getImageList(name ?: "")
call.respond(BaseResponse(data = res)) call.respond(BaseResponse(data = res))
} }
} }
@@ -2,6 +2,7 @@ package ink.snowflake.server.route
import ink.snowflake.server.SERVER_PATH_OSS import ink.snowflake.server.SERVER_PATH_OSS
import ink.snowflake.server.VIDEO_INPUT_PATH import ink.snowflake.server.VIDEO_INPUT_PATH
import ink.snowflake.server.database.VideoDao
import ink.snowflake.server.database.table.VideoTable import ink.snowflake.server.database.table.VideoTable
import ink.snowflake.server.database.table.VideoTable.vAAverageMaskedRatio import ink.snowflake.server.database.table.VideoTable.vAAverageMaskedRatio
import ink.snowflake.server.database.table.VideoTable.vACountPeople import ink.snowflake.server.database.table.VideoTable.vACountPeople
@@ -12,17 +13,19 @@ import ink.snowflake.server.database.table.VideoTable.vStartDateTime
import ink.snowflake.server.model.request.VideoAnalyticsRequest import ink.snowflake.server.model.request.VideoAnalyticsRequest
import ink.snowflake.server.model.response.* import ink.snowflake.server.model.response.*
import ink.snowflake.server.utils.WebSocketManager.broadcastMessage import ink.snowflake.server.utils.WebSocketManager.broadcastMessage
import ink.snowflake.server.database.VideoDao
import ink.snowflake.server.utils.runCommand import ink.snowflake.server.utils.runCommand
import io.ktor.http.content.* import io.ktor.http.content.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.auth.* import io.ktor.server.auth.*
import io.ktor.server.routing.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
import io.ktor.websocket.* import io.ktor.websocket.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toJavaLocalDateTime
import java.io.File import java.io.File
@@ -67,8 +70,7 @@ fun Application.VideoAnalytics() {
clients.remove(this) // 确保在连接关闭时移除客户端 clients.remove(this) // 确保在连接关闭时移除客户端
} }
} }
route("/api") { route("/api/iva") {
route("/iva") {
// 上传分析结果 // 上传分析结果
post("/saveVideoAnalyticsData") { post("/saveVideoAnalyticsData") {
val request = call.receive<VideoAnalyticsRequest>() val request = call.receive<VideoAnalyticsRequest>()
@@ -76,7 +78,7 @@ fun Application.VideoAnalytics() {
call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request))) call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request)))
} }
authenticate { authenticate {
post("/upload") { post("/createVideoTask") {
val multipart = call.receiveMultipart() //1G val multipart = call.receiveMultipart() //1G
// 确保 uploads 目录存在 // 确保 uploads 目录存在
val uploadDir = File(VIDEO_INPUT_PATH) val uploadDir = File(VIDEO_INPUT_PATH)
@@ -90,32 +92,35 @@ fun Application.VideoAnalytics() {
var name = "" var name = ""
var datetime = "" var datetime = ""
broadcastMessage("正在上传数据") broadcastMessage("正在上传数据")
multipart.forEachPart { part ->
when (part) { withContext(Dispatchers.IO) {
is PartData.FileItem -> { multipart.forEachPart { part ->
fileName = part.originalFileName ?: "unknown" when (part) {
val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径 is PartData.FileItem -> {
//ktor3 fileName = part.originalFileName ?: "unknown"
val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径
//ktor3
// file.outputStream().use { outputStream -> // file.outputStream().use { outputStream ->
// val writableChannel = Channels.newChannel(outputStream) // val writableChannel = Channels.newChannel(outputStream)
// part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel // part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel
// } // }
//ktor2 //ktor2
part.streamProvider().use { inputStream -> part.streamProvider().use { inputStream ->
file.outputStream().buffered().use { outputStream -> file.outputStream().buffered().use { outputStream ->
inputStream.copyTo(outputStream) inputStream.copyTo(outputStream)
}
} }
} }
}
is PartData.FormItem -> { is PartData.FormItem -> {
when (part.name) { when (part.name) {
"name" -> name = part.value "projectName" -> name = part.value
"datetime" -> datetime = part.value "projectDatetime" -> datetime = part.value
}
} }
}
else -> part.dispose() else -> part.dispose()
}
} }
} }
call.respond(BaseResponse(message = "上传成功", data = null)) call.respond(BaseResponse(message = "上传成功", data = null))
@@ -267,7 +272,6 @@ fun Application.VideoAnalytics() {
) )
} }
} }
}
} }
} }
} }
+1 -1
View File
@@ -13,7 +13,7 @@
}, },
{ {
"type": "chrome", "type": "chrome",
"name": "智能控制中心", "name": "主干AI实验室",
"request": "launch", "request": "launch",
"url": "http://localhost:5666", "url": "http://localhost:5666",
"env": { "NODE_ENV": "development" }, "env": { "NODE_ENV": "development" },
+1 -1
View File
@@ -1,5 +1,5 @@
# 应用标题 # 应用标题
VITE_APP_TITLE=智能控制中心 VITE_APP_TITLE=主干AI实验室
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离
VITE_APP_NAMESPACE=vben-web-antd VITE_APP_NAMESPACE=vben-web-antd
+1
View File
@@ -2,4 +2,5 @@ export * from './auth';
export * from './iva'; export * from './iva';
export * from './menu'; export * from './menu';
export * from './remote'; export * from './remote';
export * from './sca';
export * from './user'; export * from './user';
+12 -1
View File
@@ -8,10 +8,21 @@ export async function refreshVideoList(name = '') {
} }
/** /**
* 获取已分析的视频列表 * 获取已分析的视频
*/ */
export async function refreshVideoDetail(vId = '') { export async function refreshVideoDetail(vId = '') {
return requestClient.get('/iva/getAnalyticsDetailByVideoId', { return requestClient.get('/iva/getAnalyticsDetailByVideoId', {
params: { vId }, params: { vId },
}); });
} }
/**
* 上传视频分析任务
*/
export async function createVideoTask(formData: FormData) {
return requestClient.post('/iva/createVideoTask', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
+19
View File
@@ -0,0 +1,19 @@
import { requestClient } from '#/api/request';
/**
* 获取已分析的图片列表
*/
export async function refreshImageList(name = '') {
return requestClient.get('/sca/getImageList', { params: { name } });
}
/**
* 上传图片分析任务
*/
export async function createImageTask(formData: FormData) {
return requestClient.post('/sca/createImageTask', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
@@ -1,5 +1,6 @@
{ {
"title": "人工智能", "title": "人工智能",
"intelligence_video_analysis": "视频智能分析", "intelligence_video_analysis": "视频智能分析",
"silkworm_cocoon_analysis": "蚕茧仪评分析",
"young_silkworm_analysis": "催青阶段分析" "young_silkworm_analysis": "催青阶段分析"
} }
@@ -8,7 +8,7 @@ const routes: RouteRecordRaw[] = [
{ {
meta: { meta: {
icon: 'ic:baseline-view-in-ar', icon: 'ic:baseline-view-in-ar',
keepAlive: true, keepAlive: false,
order: 2, order: 2,
title: $t('ai.title'), title: $t('ai.title'),
}, },
@@ -22,9 +22,20 @@ const routes: RouteRecordRaw[] = [
authority: ['iva'], authority: ['iva'],
icon: 'mdi:video', icon: 'mdi:video',
title: $t('ai.intelligence_video_analysis'), title: $t('ai.intelligence_video_analysis'),
keepAlive: false,
}, },
component: () => import('#/views/ai/iva/index.vue'), component: () => import('#/views/ai/iva/index.vue'),
}, },
{
name: 'SCA',
path: '/ai/sca',
meta: {
authority: ['sca'],
icon: 'mdi:ice-cream',
title: $t('ai.silkworm_cocoon_analysis'),
},
component: () => import('#/views/ai/sca/index.vue'),
},
{ {
name: 'YSA', name: 'YSA',
path: '/ai/ysa', path: '/ai/ysa',
@@ -0,0 +1,159 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, Input, InputNumber, message } from 'ant-design-vue';
import { createVideoTask } from '#/api/core/iva';
const projectName = ref('');
const year = ref<null | number>(null);
const month = ref<null | number>(null);
const day = ref<null | number>(null);
const hours = ref<null | number>(null);
const minutes = ref<null | number>(null);
const seconds = ref<null | number>(null);
const fileName = ref('');
const selectedFile = ref<File | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const [Modal, modalApi] = useVbenModal({
title: '新建视频分析任务',
class: 'w-[600px]',
onCancel() {
modalApi.close();
},
onConfirm() {
if (!projectName.value || !selectedFile.value) {
message.warning('请填写项目名并选择视频文件');
}
uploadFile();
},
});
async function uploadFile() {
const formData = new FormData();
formData.append('file', selectedFile.value);
formData.append('projectName', projectName.value);
formData.append(
'projectDatetime',
`${year.value ? year.value.toString() : '2025'}-${
month.value ? month.value.toString().padStart(2, '0') : '01'
}-${day.value ? day.value.toString().padStart(2, '0') : '01'} ${
hours.value ? hours.value.toString().padStart(2, '0') : '00'
}:${minutes.value ? minutes.value.toString().padStart(2, '0') : '00'}:${
seconds.value ? seconds.value.toString().padStart(2, '0') : '00'
}`,
);
await createVideoTask(formData).then(() => {
message.success('任务创建成功,正在处理视频,请稍后刷新列表查看');
modalApi.close();
// 清空表单
projectName.value = '';
year.value = null;
month.value = null;
day.value = null;
hours.value = null;
minutes.value = null;
seconds.value = null;
fileName.value = '';
selectedFile.value = null;
});
}
function selectFile() {
fileInputRef.value?.click();
}
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
selectedFile.value = files[0];
fileName.value = files[0].name;
}
}
</script>
<template>
<Modal>
<Form layout="vertical">
<Form.Item label="项目名*" required>
<Input v-model:value="projectName" />
</Form.Item>
<Form.Item label="日期">
<div style="display: flex; gap: 8px">
<InputNumber
v-model:value="year"
:min="1900"
:max="2100"
placeholder="年"
style="flex: 1"
/>
<InputNumber
v-model:value="month"
:min="1"
:max="12"
placeholder="月"
style="flex: 1"
/>
<InputNumber
v-model:value="day"
:min="1"
:max="31"
placeholder="日"
style="flex: 1"
/>
</div>
</Form.Item>
<Form.Item label="时间">
<div style="display: flex; gap: 8px">
<InputNumber
v-model:value="hours"
:min="0"
:max="23"
placeholder="小时"
style="flex: 1"
/>
<InputNumber
v-model:value="minutes"
:min="0"
:max="59"
placeholder="分钟"
style="flex: 1"
/>
<InputNumber
v-model:value="seconds"
:min="0"
:max="59"
placeholder="秒"
style="flex: 1"
/>
</div>
</Form.Item>
<Form.Item label="上传视频*" required>
<div
@click="selectFile"
style="
padding: 16px;
text-align: center;
cursor: pointer;
border: 1px dashed #d9d9d9;
"
>
{{ fileName || '点击选择文件' }}
<input
type="file"
accept="video/*"
ref="fileInputRef"
@change="handleFileChange"
style="display: none"
/>
</div>
</Form.Item>
</Form>
</Modal>
</template>
+13 -4
View File
@@ -6,7 +6,7 @@ import type { EchartsUIType } from '@vben/plugins/echarts';
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { AnalysisOverview } from '@vben/common-ui'; import { AnalysisOverview, useVbenModal } from '@vben/common-ui';
import { import {
SvgBellIcon, SvgBellIcon,
SvgCakeIcon, SvgCakeIcon,
@@ -20,6 +20,8 @@ import videojs from 'video.js';
import * as api from '#/api'; import * as api from '#/api';
import CreateVideoTaskModal from './CreateVideoTaskModal.vue';
import 'video.js/dist/video-js.css'; import 'video.js/dist/video-js.css';
const list = ref<any[]>([]); const list = ref<any[]>([]);
@@ -40,8 +42,14 @@ function refreshList() {
loadList(); loadList();
message.success('视频列表加载完成'); message.success('视频列表加载完成');
} }
const [BaseModal, baseModalApi] = useVbenModal({
// 连接抽离的组件
connectedComponent: CreateVideoTaskModal,
});
const createTask = () => {}; function createTask() {
baseModalApi.open();
}
async function selectItem(item: any) { async function selectItem(item: any) {
const res = await api.refreshVideoDetail(item.v_id); const res = await api.refreshVideoDetail(item.v_id);
@@ -314,7 +322,6 @@ function refreshLineChart() {
refreshVideoPlayer(); refreshVideoPlayer();
} }
} }
function onListItemClick(video: any) { function onListItemClick(video: any) {
// 视频跳转到指定时间点 // 视频跳转到指定时间点
const vStartTime = const vStartTime =
@@ -338,6 +345,8 @@ function onListItemClick(video: any) {
</script> </script>
<template> <template>
<BaseModal />
<CreateVideoTaskModal />
<div class="flex h-full w-full bg-gray-50"> <div class="flex h-full w-full bg-gray-50">
<!-- 左侧筛选 + 列表 --> <!-- 左侧筛选 + 列表 -->
<div class="flex w-64 flex-col border-r bg-white p-4"> <div class="flex w-64 flex-col border-r bg-white p-4">
@@ -407,7 +416,7 @@ function onListItemClick(video: any) {
<div class="flex w-72 flex-col gap-4"> <div class="flex w-72 flex-col gap-4">
<!-- 视频基础信息展示 --> <!-- 视频基础信息展示 -->
<div <div
class="mt-6 w-full rounded border bg-white p-4" class="w-full rounded border bg-white p-4"
id="video_base_info" id="video_base_info"
> >
<div <div
@@ -0,0 +1,97 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { Form, Input, message } from 'ant-design-vue';
import { createVideoTask } from '#/api/core/iva';
const projectName = ref('');
const year = ref<null | number>(null);
const month = ref<null | number>(null);
const day = ref<null | number>(null);
const hours = ref<null | number>(null);
const minutes = ref<null | number>(null);
const seconds = ref<null | number>(null);
const fileName = ref('');
const selectedFile = ref<File | null>(null);
const fileInputRef = ref<HTMLInputElement | null>(null);
const [Modal, modalApi] = useVbenModal({
title: '新建蚕茧分析任务',
class: 'w-[600px]',
onCancel() {
modalApi.close();
},
onConfirm() {
if (!projectName.value || !selectedFile.value) {
message.warning('请填写项目名并选择蚕茧图片');
}
uploadFile();
},
});
async function uploadFile() {
const formData = new FormData();
formData.append('file', selectedFile.value!);
formData.append('projectName', projectName.value);
await createVideoTask(formData).then(() => {
message.success('任务创建成功,正在处理图像,请稍后刷新列表查看');
modalApi.close();
// 清空表单
projectName.value = '';
year.value = null;
month.value = null;
day.value = null;
hours.value = null;
minutes.value = null;
seconds.value = null;
fileName.value = '';
selectedFile.value = null;
});
}
function selectFile() {
fileInputRef.value?.click();
}
function handleFileChange(event: Event) {
const files = (event.target as HTMLInputElement).files;
if (files && files.length > 0) {
selectedFile.value = files[0];
fileName.value = files[0]!.name;
}
}
</script>
<template>
<Modal>
<Form layout="vertical">
<Form.Item label="项目名*" required>
<Input v-model:value="projectName" />
</Form.Item>
<Form.Item label="上传图片*" required>
<div
@click="selectFile"
style="
padding: 16px;
text-align: center;
cursor: pointer;
border: 1px dashed #d9d9d9;
"
>
{{ fileName || '点击选择文件' }}
<input
type="file"
accept="image/*"
ref="fileInputRef"
@change="handleFileChange"
style="display: none"
/>
</div>
</Form.Item>
</Form>
</Modal>
</template>
@@ -0,0 +1,255 @@
<script setup lang="ts">
import type { EchartsUIType } from '@vben/plugins/echarts';
import { onMounted, ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
import { Button, message } from 'ant-design-vue';
import * as api from '#/api';
import CreateYSATaskModal from './CreateYSATaskModal.vue';
const list = ref<any[]>([]);
const error = ref<null | string>(null);
const filterKeyword = ref('');
const selectedItem = ref<any>(null);
async function loadList() {
error.value = null;
const res = await api.refreshImageList(filterKeyword.value);
list.value = res || [];
}
function refreshList() {
filterKeyword.value = '';
loadList();
message.success('列表加载完成');
}
const [BaseModal, baseModalApi] = useVbenModal({
// 连接抽离的组件
connectedComponent: CreateYSATaskModal,
});
function createTask() {
baseModalApi.open();
}
async function selectItem(item: any) {
selectedItem.value = item;
refreshLineChart();
}
// 监听关键词变化,调用防抖接口
watch(filterKeyword, () => {
loadList();
});
watch(selectedItem, () => {});
onMounted(() => {
loadList();
});
const showInfoStr = ref<Record<string, number | string>>({});
const chartRef1 = ref<EchartsUIType>();
const { renderEcharts: renderEcharts1 } = useEcharts(chartRef1);
const chartRef2 = ref<EchartsUIType>();
const { renderEcharts: renderEcharts2 } = useEcharts(chartRef2);
function refreshLineChart() {
const data = selectedItem.value;
showInfoStr.value = {
项目名: data.name,
项目上传时间: data.upload_datetime,
文件名: data.file_name,
文件大小: `${data.size} MB`,
分辨率: data.resolution,
最高置信度: data.max_confidence,
最低置信度: data.min_confidence,
平均置信度: data.average_confidence,
分析时长: data.processing_time,
};
renderEcharts1({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true,
},
xAxis: [
{
type: 'category',
data: Object.keys(data.other_info),
axisTick: {
alignWithLabel: true,
},
},
],
yAxis: [
{
type: 'value',
},
],
series: [
{
name: '数量',
type: 'bar',
barWidth: '30%',
data: Object.values(data.other_info).map(Number),
},
],
});
renderEcharts2({
legend: { top: '5%', left: 'center' },
series: [
{
animationDelay() {
return Math.random() * 100;
},
animationEasing: 'exponentialInOut',
animationType: 'scale',
avoidLabelOverlap: false,
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
data: Object.entries(data.other_info).map(([key, val]) => ({
name: key,
value: Number(val),
})),
itemStyle: {
borderRadius: 10,
borderWidth: 3,
},
label: {
position: 'center',
show: false,
},
labelLine: {
show: false,
},
padAngle: 5,
name: '蚕茧分类',
radius: ['10%', '70%'],
type: 'pie',
},
],
tooltip: {
trigger: 'item',
},
});
}
</script>
<template>
<BaseModal />
<CreateYSATaskModal />
<div class="flex h-full w-full bg-gray-50">
<!-- 左侧筛选 + 列表 -->
<div class="flex w-64 flex-col border-r bg-white p-4">
<!-- 按钮组 -->
<div class="mb-4 flex justify-between space-x-2">
<Button type="primary" @click="createTask" class="flex-1">
新建任务
</Button>
<Button @click="refreshList" class="flex-1"> 刷新列表 </Button>
</div>
<!-- 筛选框 -->
<input
v-model="filterKeyword"
placeholder="筛选分析任务"
class="focus:ring-primary mb-4 rounded-md border px-3 py-2 focus:outline-none focus:ring-2"
/>
<!-- 列表 -->
<div class="flex-1 space-y-2 overflow-auto">
<div
v-for="item in list"
:key="item.v_id"
@click="selectItem(item)"
class="cursor-pointer rounded border p-3 hover:bg-gray-100"
:class="{ 'bg-gray-100': item.id === selectedItem?.id }"
>
<div class="text-base font-medium">{{ item.name }}</div>
<div class="text-sm text-gray-400">{{ item.upload_datetime }}</div>
</div>
</div>
</div>
<!-- 右侧Tab 内容区 -->
<div class="flex flex-1 flex-col overflow-hidden p-6">
<div
v-if="!selectedItem"
class="flex h-full items-center justify-center text-gray-400"
>
请先选择左侧列表中的分析任务
</div>
<template v-else>
<div class="flex h-full flex-col gap-4">
<!-- 主内容区域左右结构 -->
<div class="flex flex-1 gap-4">
<!-- 左侧 -->
<div class="flex w-72 flex-col gap-4">
<!-- 视频基础信息展示 -->
<div
class="w-full rounded border bg-white p-4"
id="video_base_info"
>
<div
v-for="(value, key) in showInfoStr"
:key="key"
class="mb-2 flex text-sm text-gray-700"
>
<div class="w-32 font-medium text-gray-900">{{ key }}</div>
<div class="flex-1 break-all text-gray-600">
{{ value || '—' }}
</div>
</div>
</div>
<!-- 空白卡片 -->
<div class="flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef2" />
</div>
</div>
<!-- 右侧 -->
<div class="flex flex-1 flex-col gap-4">
<!-- 左右两个图片显示 -->
<div class="flex flex-1 gap-4">
<!-- 左图 -->
<div
class="flex flex-1 items-center justify-center rounded border bg-white p-4"
>
<img
:src="selectedItem?.image_pre"
alt="左图"
class="object-contain"
/>
</div>
<!-- 右图 -->
<div
class="flex flex-1 items-center justify-center rounded border bg-white p-4"
>
<img
:src="selectedItem?.image_after"
alt="右图"
class="object-contain"
/>
</div>
</div>
<!-- 柱状图区域 -->
<div class="flex-1 rounded border bg-white p-4">
<EchartsUI ref="chartRef1" />
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
@@ -240,7 +240,7 @@ function getGreeting() {
{{ getGreeting() }}, {{ userStore.userInfo?.username }}, {{ getGreeting() }}, {{ userStore.userInfo?.username }},
开始您一天的工作吧 开始您一天的工作吧
</template> </template>
<template #description> 欢迎使用智能控制中心 </template> <template #description> 欢迎使用主干AI实验室 </template>
</WorkbenchHeader> </WorkbenchHeader>
</div> </div>
</template> </template>