完成SCA:蚕茧模块演示效果
This commit is contained in:
@@ -15,7 +15,8 @@ import io.ktor.server.application.*
|
||||
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) {
|
||||
return transaction {
|
||||
ImageTable.insert {
|
||||
it[object_name] = request.object_name
|
||||
it[upload_datetime] = Timestamp.valueOf(request.upload_datetime)
|
||||
.toLocalDateTime().toKotlinLocalDateTime()
|
||||
it[file_name] = request.file_name
|
||||
@@ -34,15 +33,19 @@ object ImageDao {
|
||||
}
|
||||
|
||||
|
||||
fun getVideoList(): List<ImageAnalyticsRequest> {
|
||||
fun getImageList(name: String): List<ImageAnalyticsRequest> {
|
||||
return transaction {
|
||||
ImageTable.selectAll()
|
||||
.where { ImageTable.name like "%$name%" }
|
||||
.orderBy(ImageTable.upload_datetime, SortOrder.DESC)
|
||||
.map {
|
||||
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]),
|
||||
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],
|
||||
size = it[ImageTable.size],
|
||||
cocoon_count = it[ImageTable.cocoon_count],
|
||||
|
||||
@@ -72,7 +72,6 @@ object VideoDao {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? {
|
||||
return transaction {
|
||||
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.json.json
|
||||
|
||||
object ImageTable : IntIdTable("image") {
|
||||
val object_name = varchar("object_name", 255)
|
||||
object ImageTable : IntIdTable("image_sca") {
|
||||
val name = varchar("name", 255)
|
||||
val upload_datetime = datetime("upload_datetime")
|
||||
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 size = float("size")
|
||||
val cocoon_count = float("cocoon_count")
|
||||
|
||||
@@ -4,10 +4,13 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ImageAnalyticsRequest(
|
||||
val object_name: String, // Minio存储名
|
||||
val id : Int,
|
||||
val name : String,
|
||||
val upload_datetime: String, // 上传时间
|
||||
val file_name: String, // 文件名
|
||||
val resolution: String, // 图片分辨率
|
||||
val image_after: String,
|
||||
val image_pre: String,
|
||||
val size: Float, // 文件大小,单位MB
|
||||
val cocoon_count: Float, // 识别出的茧数量
|
||||
val max_confidence: Float, // 最大置信度
|
||||
|
||||
@@ -11,7 +11,7 @@ import io.ktor.server.response.*
|
||||
|
||||
fun Application.ImageAnalytics() {
|
||||
routing {
|
||||
route("/api") {
|
||||
route("/api/sca") {
|
||||
// 上传分析结果
|
||||
post("/saveImageAnalyticsData") {
|
||||
val request = call.receive<ImageAnalyticsRequest>()
|
||||
@@ -28,9 +28,10 @@ fun Application.ImageAnalytics() {
|
||||
stopHLSStream(camera)
|
||||
call.respond(BaseResponse(message = "摄像头流已停止", data = null))
|
||||
}
|
||||
// 获取已分析视频列表
|
||||
// 获取已分析图片列表
|
||||
get("/getImageList") {
|
||||
val res = ImageDao.getVideoList()
|
||||
val name = call.parameters["name"]
|
||||
val res = ImageDao.getImageList(name ?: "")
|
||||
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.VIDEO_INPUT_PATH
|
||||
import ink.snowflake.server.database.VideoDao
|
||||
import ink.snowflake.server.database.table.VideoTable
|
||||
import ink.snowflake.server.database.table.VideoTable.vAAverageMaskedRatio
|
||||
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.response.*
|
||||
import ink.snowflake.server.utils.WebSocketManager.broadcastMessage
|
||||
import ink.snowflake.server.database.VideoDao
|
||||
import ink.snowflake.server.utils.runCommand
|
||||
import io.ktor.http.content.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.server.websocket.*
|
||||
import io.ktor.websocket.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.toJavaLocalDateTime
|
||||
import java.io.File
|
||||
@@ -67,8 +70,7 @@ fun Application.VideoAnalytics() {
|
||||
clients.remove(this) // 确保在连接关闭时移除客户端
|
||||
}
|
||||
}
|
||||
route("/api") {
|
||||
route("/iva") {
|
||||
route("/api/iva") {
|
||||
// 上传分析结果
|
||||
post("/saveVideoAnalyticsData") {
|
||||
val request = call.receive<VideoAnalyticsRequest>()
|
||||
@@ -76,7 +78,7 @@ fun Application.VideoAnalytics() {
|
||||
call.respond(BaseResponse(data = VideoDao.insertVideoAnalyticsData(request)))
|
||||
}
|
||||
authenticate {
|
||||
post("/upload") {
|
||||
post("/createVideoTask") {
|
||||
val multipart = call.receiveMultipart() //1G
|
||||
// 确保 uploads 目录存在
|
||||
val uploadDir = File(VIDEO_INPUT_PATH)
|
||||
@@ -90,6 +92,8 @@ fun Application.VideoAnalytics() {
|
||||
var name = ""
|
||||
var datetime = ""
|
||||
broadcastMessage("正在上传数据")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
multipart.forEachPart { part ->
|
||||
when (part) {
|
||||
is PartData.FileItem -> {
|
||||
@@ -110,14 +114,15 @@ fun Application.VideoAnalytics() {
|
||||
|
||||
is PartData.FormItem -> {
|
||||
when (part.name) {
|
||||
"name" -> name = part.value
|
||||
"datetime" -> datetime = part.value
|
||||
"projectName" -> name = part.value
|
||||
"projectDatetime" -> datetime = part.value
|
||||
}
|
||||
}
|
||||
|
||||
else -> part.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
call.respond(BaseResponse(message = "上传成功", data = null))
|
||||
broadcastMessage("上传完成,开始启动AI引擎")
|
||||
val command = listOf(
|
||||
@@ -271,7 +276,6 @@ fun Application.VideoAnalytics() {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun broadcastMessage(message: String) { // 封装的广播消息方法
|
||||
|
||||
Vendored
+1
-1
@@ -13,7 +13,7 @@
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"name": "智能控制中心",
|
||||
"name": "主干AI实验室",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:5666",
|
||||
"env": { "NODE_ENV": "development" },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 应用标题
|
||||
VITE_APP_TITLE=智能控制中心
|
||||
VITE_APP_TITLE=主干AI实验室
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-antd
|
||||
|
||||
@@ -2,4 +2,5 @@ export * from './auth';
|
||||
export * from './iva';
|
||||
export * from './menu';
|
||||
export * from './remote';
|
||||
export * from './sca';
|
||||
export * from './user';
|
||||
|
||||
@@ -8,10 +8,21 @@ export async function refreshVideoList(name = '') {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已分析的视频列表
|
||||
* 获取已分析的视频
|
||||
*/
|
||||
export async function refreshVideoDetail(vId = '') {
|
||||
return requestClient.get('/iva/getAnalyticsDetailByVideoId', {
|
||||
params: { vId },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传视频分析任务
|
||||
*/
|
||||
export async function createVideoTask(formData: FormData) {
|
||||
return requestClient.post('/iva/createVideoTask', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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": "人工智能",
|
||||
"intelligence_video_analysis": "视频智能分析",
|
||||
"silkworm_cocoon_analysis": "蚕茧仪评分析",
|
||||
"young_silkworm_analysis": "催青阶段分析"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'ic:baseline-view-in-ar',
|
||||
keepAlive: true,
|
||||
keepAlive: false,
|
||||
order: 2,
|
||||
title: $t('ai.title'),
|
||||
},
|
||||
@@ -22,9 +22,20 @@ const routes: RouteRecordRaw[] = [
|
||||
authority: ['iva'],
|
||||
icon: 'mdi:video',
|
||||
title: $t('ai.intelligence_video_analysis'),
|
||||
keepAlive: false,
|
||||
},
|
||||
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',
|
||||
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>
|
||||
@@ -6,7 +6,7 @@ import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { AnalysisOverview } from '@vben/common-ui';
|
||||
import { AnalysisOverview, useVbenModal } from '@vben/common-ui';
|
||||
import {
|
||||
SvgBellIcon,
|
||||
SvgCakeIcon,
|
||||
@@ -20,6 +20,8 @@ import videojs from 'video.js';
|
||||
|
||||
import * as api from '#/api';
|
||||
|
||||
import CreateVideoTaskModal from './CreateVideoTaskModal.vue';
|
||||
|
||||
import 'video.js/dist/video-js.css';
|
||||
|
||||
const list = ref<any[]>([]);
|
||||
@@ -40,8 +42,14 @@ function refreshList() {
|
||||
loadList();
|
||||
message.success('视频列表加载完成');
|
||||
}
|
||||
const [BaseModal, baseModalApi] = useVbenModal({
|
||||
// 连接抽离的组件
|
||||
connectedComponent: CreateVideoTaskModal,
|
||||
});
|
||||
|
||||
const createTask = () => {};
|
||||
function createTask() {
|
||||
baseModalApi.open();
|
||||
}
|
||||
|
||||
async function selectItem(item: any) {
|
||||
const res = await api.refreshVideoDetail(item.v_id);
|
||||
@@ -314,7 +322,6 @@ function refreshLineChart() {
|
||||
refreshVideoPlayer();
|
||||
}
|
||||
}
|
||||
|
||||
function onListItemClick(video: any) {
|
||||
// 视频跳转到指定时间点
|
||||
const vStartTime =
|
||||
@@ -338,6 +345,8 @@ function onListItemClick(video: any) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseModal />
|
||||
<CreateVideoTaskModal />
|
||||
<div class="flex h-full w-full bg-gray-50">
|
||||
<!-- 左侧:筛选 + 列表 -->
|
||||
<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="mt-6 w-full rounded border bg-white p-4"
|
||||
class="w-full rounded border bg-white p-4"
|
||||
id="video_base_info"
|
||||
>
|
||||
<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 }},
|
||||
开始您一天的工作吧!
|
||||
</template>
|
||||
<template #description> 欢迎使用智能控制中心 </template>
|
||||
<template #description> 欢迎使用主干AI实验室 </template>
|
||||
</WorkbenchHeader>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user