完善细节
This commit is contained in:
+3
-2
@@ -61,7 +61,7 @@ async def main():
|
|||||||
# MCP服务-ailab
|
# MCP服务-ailab
|
||||||
endpoint_url_ai_lab = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D"
|
endpoint_url_ai_lab = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D"
|
||||||
# endpoint_url_ai_lab = "ws://ce_bot_mcp:8004/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D"
|
# endpoint_url_ai_lab = "ws://ce_bot_mcp:8004/mcp_endpoint/mcp/?token=TsSP9lBq6Oa1WMkachHoS2TtNt4GKV/Gli24pk5Rjpk%3D"
|
||||||
task_mcp1 = asyncio.create_task(init_mcp_server(endpoint_url_ai_lab))
|
# task_mcp1 = asyncio.create_task(init_mcp_server(endpoint_url_ai_lab))
|
||||||
|
|
||||||
# MCP服务-ql
|
# MCP服务-ql
|
||||||
endpoint_url_ql = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=8ZmCzp7FzsbxwHOg2%2FvBQkxrC3QWJiI%2B4iTfouExinjcT8ZgLwQfFUtgcMInI7St"
|
endpoint_url_ql = "wss://ai.ronsunny.cn:8090/aimcp/mcp_endpoint/mcp/?token=8ZmCzp7FzsbxwHOg2%2FvBQkxrC3QWJiI%2B4iTfouExinjcT8ZgLwQfFUtgcMInI7St"
|
||||||
@@ -70,7 +70,8 @@ async def main():
|
|||||||
# RabbitMQ服务
|
# RabbitMQ服务
|
||||||
task_mq = asyncio.create_task(mq_pull_analysis_async())
|
task_mq = asyncio.create_task(mq_pull_analysis_async())
|
||||||
|
|
||||||
await asyncio.gather(task_api, task_mcp1, task_mcp2, task_mq)
|
# await asyncio.gather(task_api, task_mcp1, task_mcp2, task_mq)
|
||||||
|
await asyncio.gather(task_api, task_mcp2, task_mq)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ def push_file(bucket_name, object_name, file_bytes, contents, content_type):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_upload_token(bucket_name, object_name, xpires=timedelta(hours=1)):
|
||||||
|
upload_url = minio_client.presigned_put_object(
|
||||||
|
bucket_name=bucket_name, object_name=object_name, expires=xpires
|
||||||
|
)
|
||||||
|
return {"upload_url": upload_url, "object_name": object_name}
|
||||||
|
|
||||||
|
|
||||||
def get_temp_url(bucket_name, object_name):
|
def get_temp_url(bucket_name, object_name):
|
||||||
return minio_client.presigned_get_object(
|
return minio_client.presigned_get_object(
|
||||||
bucket_name, object_name, expires=timedelta(seconds=3600)
|
bucket_name, object_name, expires=timedelta(seconds=3600)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from utils.GlobalVariable import LOCAL_IP
|
from utils.GlobalVariable import LOCAL_IP
|
||||||
|
|
||||||
RABBIT_HOST = LOCAL_IP
|
RABBIT_HOST = LOCAL_IP
|
||||||
RABBIT_VHOST = "/bbit_ai"
|
RABBIT_VHOST = "bbit_ai"
|
||||||
RABBIT_USER = "bbit_ai"
|
RABBIT_USER = "ai_lab"
|
||||||
RABBIT_PASSWORD = "123456"
|
RABBIT_PASSWORD = "123456"
|
||||||
QUEUE_NAME = "analysis_queue"
|
QUEUE_NAME = "analysis_queue"
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ from uuid import UUID
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
import db.postgres as pg
|
import db.postgres as pg
|
||||||
|
import utils.MyUtils as utils
|
||||||
|
from agent.serviceAgent import get_service_agent_reply
|
||||||
from config.security import get_user_id_from_token
|
from config.security import get_user_id_from_token
|
||||||
|
from llm.memLLM import take_memory
|
||||||
|
from llm.titleChain import get_title
|
||||||
from models.BaseResponse import BaseResponse
|
from models.BaseResponse import BaseResponse
|
||||||
from models.ChatRequest import ChatRequest
|
from models.ChatRequest import ChatRequest
|
||||||
|
|
||||||
serviceRouter = APIRouter()
|
serviceRouter = APIRouter()
|
||||||
from llm.titleChain import get_title
|
|
||||||
from agent.serviceAgent import get_service_agent_reply
|
|
||||||
from llm.memLLM import take_memory
|
|
||||||
import utils.MyUtils as utils
|
|
||||||
|
|
||||||
|
|
||||||
# 对话列表
|
# 对话列表
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import uuid
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import APIRouter, File, Form, Depends, Query
|
from fastapi import APIRouter, File, Form, Depends, Query
|
||||||
|
|
||||||
import db.postgres as pg
|
import db.postgres as pg
|
||||||
|
from config.minIO import get_upload_token
|
||||||
from config.security import get_user_id_from_token
|
from config.security import get_user_id_from_token
|
||||||
from llm.ticketLLM import *
|
from llm.ticketLLM import *
|
||||||
from models.BaseResponse import BaseResponse
|
from models.BaseResponse import BaseResponse
|
||||||
@@ -149,3 +151,10 @@ def getSilkwormCocoonAnalysisTasks(
|
|||||||
"items": items,
|
"items": items,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@visionRouter.post("/getIVASCUploadToken")
|
||||||
|
def getIVASCUploadToken():
|
||||||
|
# 生成唯一文件名,避免覆盖
|
||||||
|
object_name = f"raw/{uuid.uuid4()}"
|
||||||
|
return get_upload_token("video-sca", object_name)
|
||||||
|
|||||||
Vendored
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"name": "主干AI实验室",
|
"name": "AI实验室",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"url": "http://localhost:5666",
|
"url": "http://localhost:5666",
|
||||||
"env": { "NODE_ENV": "development" },
|
"env": { "NODE_ENV": "development" },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 应用标题
|
# 应用标题
|
||||||
VITE_APP_TITLE=主干AI实验室
|
VITE_APP_TITLE=AI实验室
|
||||||
|
|
||||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||||
VITE_APP_NAMESPACE=vben-web-antd
|
VITE_APP_NAMESPACE=vben-web-antd
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'ic:round-remove-red-eye',
|
icon: 'ic:round-remove-red-eye',
|
||||||
authority: ['iva', 'sca', 'sca2', 'ysa', 'ticket', 'license'],
|
authority: ['iva', 'iva-sc', 'sca', 'sca2', 'ysa', 'ticket', 'license'],
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
order: 2,
|
order: 2,
|
||||||
title: $t('计算机视觉'),
|
title: $t('计算机视觉'),
|
||||||
@@ -21,11 +21,22 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: {
|
meta: {
|
||||||
authority: ['iva'],
|
authority: ['iva'],
|
||||||
icon: 'mdi:video',
|
icon: 'mdi:video',
|
||||||
title: $t('ai.intelligence_video_analysis'),
|
title: '工作视频分析',
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
component: () => import('#/views/cv/iva/index.vue'),
|
component: () => import('#/views/cv/iva/index.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'IVA-sc',
|
||||||
|
path: '/cv/iva-sc',
|
||||||
|
meta: {
|
||||||
|
authority: ['iva-sc'],
|
||||||
|
icon: 'mdi:video-image',
|
||||||
|
title: '蚕茧视频分析',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
component: () => import('#/views/cv/iva-sc/index.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'SCA',
|
name: 'SCA',
|
||||||
path: '/cv/sca',
|
path: '/cv/sca',
|
||||||
|
|||||||
@@ -97,6 +97,16 @@ const routes: RouteRecordRaw[] = [
|
|||||||
title: '数据可视化',
|
title: '数据可视化',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'VMManager-webui',
|
||||||
|
path: '/set/VMManager',
|
||||||
|
component: IFrameView,
|
||||||
|
meta: {
|
||||||
|
icon: 'mdi:monitor-dashboard',
|
||||||
|
link: `https://10.10.12.100/ui/#/host`,
|
||||||
|
title: '虚拟机管理',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,502 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type Player from 'video.js/dist/types/player';
|
||||||
|
|
||||||
|
import type { AnalysisOverviewItem } from '@vben/common-ui';
|
||||||
|
import type { EchartsUIType } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
nextTick,
|
||||||
|
onBeforeUnmount,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
import { AnalysisOverview, useVbenModal } from '@vben/common-ui';
|
||||||
|
import {
|
||||||
|
SvgBellIcon,
|
||||||
|
SvgCakeIcon,
|
||||||
|
SvgCardIcon,
|
||||||
|
SvgDownloadIcon,
|
||||||
|
} from '@vben/icons';
|
||||||
|
import { EchartsUI, useEcharts } from '@vben/plugins/echarts';
|
||||||
|
|
||||||
|
import { Button, Form, Input, message } from 'ant-design-vue';
|
||||||
|
import videojs from 'video.js';
|
||||||
|
|
||||||
|
import * as api from '#/api';
|
||||||
|
import { createImageTaskV2 } from '#/api';
|
||||||
|
|
||||||
|
import 'video.js/dist/video-js.css';
|
||||||
|
|
||||||
|
const list = ref<any[]>([]);
|
||||||
|
const error = ref<null | string>(null);
|
||||||
|
const filterKeyword = ref('');
|
||||||
|
const activeTab = ref<'detail' | 'video'>('detail');
|
||||||
|
const selectedItem = ref<any>(null);
|
||||||
|
const detailList = ref<any[]>([]);
|
||||||
|
const videoEl = ref<HTMLVideoElement | null>(null);
|
||||||
|
const player = ref<null | Player>(null);
|
||||||
|
async function loadList() {
|
||||||
|
error.value = null;
|
||||||
|
const res = await api.refreshVideoList(filterKeyword.value);
|
||||||
|
list.value = res || [];
|
||||||
|
}
|
||||||
|
function refreshList() {
|
||||||
|
filterKeyword.value = '';
|
||||||
|
loadList();
|
||||||
|
message.success('视频列表加载完成');
|
||||||
|
}
|
||||||
|
function createTask() {
|
||||||
|
modalApi.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectItem(item: any) {
|
||||||
|
const res = await api.refreshVideoDetail(item.v_id);
|
||||||
|
selectedItem.value = res;
|
||||||
|
refreshLineChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'detail', label: '分析详情' },
|
||||||
|
{ key: 'video', label: '分析视频' },
|
||||||
|
];
|
||||||
|
let overviewItems: AnalysisOverviewItem[];
|
||||||
|
|
||||||
|
// 监听关键词变化,调用防抖接口
|
||||||
|
watch(filterKeyword, () => {
|
||||||
|
loadList();
|
||||||
|
});
|
||||||
|
watch(selectedItem, () => {
|
||||||
|
overviewItems = [
|
||||||
|
{
|
||||||
|
icon: SvgDownloadIcon,
|
||||||
|
title: '出现的蚕茧数量',
|
||||||
|
totalTitle: '',
|
||||||
|
totalValue: 0,
|
||||||
|
value: selectedItem.value.v_a_count_people,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgCardIcon,
|
||||||
|
title: '出现最多的种类',
|
||||||
|
totalTitle: '',
|
||||||
|
totalValue: 0,
|
||||||
|
value: selectedItem.value.v_a_max_action,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgCakeIcon,
|
||||||
|
title: '最多同框蚕茧数量',
|
||||||
|
totalTitle: '',
|
||||||
|
totalValue: 0,
|
||||||
|
value: selectedItem.value.v_a_total_people,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: SvgBellIcon,
|
||||||
|
title: '时间',
|
||||||
|
totalTitle: '最长停留时间',
|
||||||
|
totalValue: 0,
|
||||||
|
value: selectedItem.value.v_a_max_stay_time,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
watch([activeTab, selectedItem], async ([tab]) => {
|
||||||
|
if (tab === 'video' && selectedItem.value?.v_video_play_path) {
|
||||||
|
refreshVideoPlayer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onMounted(() => {
|
||||||
|
loadList();
|
||||||
|
});
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
player.value?.dispose();
|
||||||
|
player.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ 切换视频项时销毁并重建
|
||||||
|
function refreshVideoPlayer() {
|
||||||
|
nextTick(() => {
|
||||||
|
if (player.value) {
|
||||||
|
player.value.src([
|
||||||
|
{
|
||||||
|
src: selectedItem.value.v_video_play_path,
|
||||||
|
type: 'video/mp4',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
if (!videoEl.value) return;
|
||||||
|
player.value = videojs(videoEl.value, {
|
||||||
|
controls: true,
|
||||||
|
autoplay: false,
|
||||||
|
preload: 'auto',
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
src: selectedItem.value.v_video_play_path,
|
||||||
|
type: 'video/mp4',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
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.v_name,
|
||||||
|
文件名: data.v_file_name,
|
||||||
|
文件大小: `${data.v_size} MB`,
|
||||||
|
总时长: `${data.v_duration} 秒`,
|
||||||
|
分辨率: data.v_resolution,
|
||||||
|
视频编码格式: data.v_video_codec,
|
||||||
|
音频编码格式: data.v_audio_codec,
|
||||||
|
总体比特率: data.v_overall_bit_rate,
|
||||||
|
};
|
||||||
|
|
||||||
|
detailList.value = data.v_details_list || [];
|
||||||
|
|
||||||
|
const detail = selectedItem.value.v_a_details;
|
||||||
|
let yTotalData = Array.isArray(detail.yTotalData)
|
||||||
|
? detail.yTotalData.map((item: any) => [item.first, item.second])
|
||||||
|
: []; // 默认值为空数组
|
||||||
|
let yMaskedData = Array.isArray(detail.yMaskedData)
|
||||||
|
? detail.yMaskedData.map((item: any) => [item.first, item.second])
|
||||||
|
: []; // 默认值为空数组
|
||||||
|
const areaData = Array.isArray(detail.areaData)
|
||||||
|
? detail.areaData.map((actionGroup: any) => {
|
||||||
|
return actionGroup.map((action: any) => {
|
||||||
|
return { xAxis: action.xAxis, itemStyle: action.itemStyle };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
: []; // 默认值为空数组
|
||||||
|
yTotalData = yTotalData.map((item: any) => [
|
||||||
|
new Date(item[0]).getTime(),
|
||||||
|
item[1],
|
||||||
|
]);
|
||||||
|
yMaskedData = yMaskedData.map((item: any) => [
|
||||||
|
new Date(item[0]).getTime(),
|
||||||
|
item[1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
renderEcharts1({
|
||||||
|
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '总人数',
|
||||||
|
type: 'line',
|
||||||
|
step: 'end',
|
||||||
|
data: yTotalData,
|
||||||
|
markArea: {
|
||||||
|
itemStyle: { color: 'rgba(255, 173, 177, 0.4)' },
|
||||||
|
data: areaData,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: '口罩佩戴人数', type: 'line', step: 'end', data: yMaskedData },
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
axisPointer: {
|
||||||
|
lineStyle: {
|
||||||
|
color: '#019680',
|
||||||
|
width: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trigger: 'axis',
|
||||||
|
},
|
||||||
|
xAxis: { type: 'time' },
|
||||||
|
yAxis: { type: 'value' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const maskedRatio = data.v_a_average_masked_ratio * 100;
|
||||||
|
const noMaskedRatio = 100 - maskedRatio;
|
||||||
|
renderEcharts2({
|
||||||
|
legend: { top: '5%', left: 'center' },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
animationDelay() {
|
||||||
|
return Math.random() * 100;
|
||||||
|
},
|
||||||
|
animationEasing: 'exponentialInOut',
|
||||||
|
animationType: 'scale',
|
||||||
|
avoidLabelOverlap: false,
|
||||||
|
color: ['#5ab1ef', '#b6a2de', '#67e0e3', '#2ec7c9'],
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
value: maskedRatio,
|
||||||
|
name: '非正茧(%)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: noMaskedRatio,
|
||||||
|
name: '正茧(%)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
fontSize: '12',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
borderRadius: 10,
|
||||||
|
borderWidth: 3,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
position: 'center',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
labelLine: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
padAngle: 5,
|
||||||
|
name: '正茧平均占比',
|
||||||
|
radius: ['40%', '70%'],
|
||||||
|
type: 'pie',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (activeTab.value === 'video' && player.value) {
|
||||||
|
// 如果当前是视频标签页,刷新播放器
|
||||||
|
refreshVideoPlayer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const projectName = ref('');
|
||||||
|
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 (!selectedFile.value) {
|
||||||
|
message.warning('请选择蚕茧图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uploadFile();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async function uploadFile() {
|
||||||
|
// 先关闭弹窗
|
||||||
|
modalApi.close();
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', selectedFile.value!);
|
||||||
|
formData.append('projectName', projectName.value);
|
||||||
|
await createImageTaskV2(formData).then(() => {
|
||||||
|
// 清空表单
|
||||||
|
projectName.value = '';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = ref(9);
|
||||||
|
const total = ref(0); // 总条数
|
||||||
|
// 计算总页数
|
||||||
|
const totalPages = computed(() => Math.ceil(total.value / pageSize.value));
|
||||||
|
|
||||||
|
function changePage(newPage) {
|
||||||
|
page.value = newPage;
|
||||||
|
loadList();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex h-full w-full flex-col">
|
||||||
|
<BaseModal />
|
||||||
|
<Modal>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="任务名称">
|
||||||
|
<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="video/*"
|
||||||
|
ref="fileInputRef"
|
||||||
|
@change="handleFileChange"
|
||||||
|
style="display: none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
<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.v_id === selectedItem?.v_id }"
|
||||||
|
>
|
||||||
|
<div class="text-base font-medium">{{ item.v_name }}</div>
|
||||||
|
<div class="text-sm text-gray-400">{{ item.v_a_time }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="mt-2 flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
:disabled="page === 1"
|
||||||
|
@click="changePage(page - 1)"
|
||||||
|
class="rounded border px-1"
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<span>第 {{ page }} 页 / 共 {{ totalPages }} 页</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
:disabled="page === totalPages"
|
||||||
|
@click="changePage(page + 1)"
|
||||||
|
class="rounded border px-1"
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧:Tab 内容区 -->
|
||||||
|
<div class="flex flex-1 flex-col overflow-hidden p-6">
|
||||||
|
<!-- Tab 标题 -->
|
||||||
|
<div class="mb-4 flex shrink-0 space-x-4 border-b">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
@click="activeTab = tab.key as 'detail' | 'video'"
|
||||||
|
class="px-4 py-2"
|
||||||
|
:class="[
|
||||||
|
activeTab === tab.key
|
||||||
|
? 'border-primary text-primary border-b-2'
|
||||||
|
: 'hover:text-primary text-gray-500',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab 内容(滚动区域) -->
|
||||||
|
<div class="flex-1 overflow-auto">
|
||||||
|
<div
|
||||||
|
v-if="!selectedItem"
|
||||||
|
class="flex h-full items-center justify-center text-gray-400"
|
||||||
|
>
|
||||||
|
请先选择左侧列表中的分析任务
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'detail'"
|
||||||
|
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="h-[300px] flex-1 rounded border bg-white p-4">
|
||||||
|
<EchartsUI ref="chartRef2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧 -->
|
||||||
|
<div class="flex flex-1 flex-col gap-4">
|
||||||
|
<!-- 上:四个统计卡片 -->
|
||||||
|
<AnalysisOverview
|
||||||
|
:items="overviewItems"
|
||||||
|
class="grid grid-cols-4 gap-4"
|
||||||
|
/>
|
||||||
|
<!-- 下:折线图区域 -->
|
||||||
|
<div class="flex-1 rounded border bg-white p-4">
|
||||||
|
<EchartsUI ref="chartRef1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="activeTab === 'video'" class="flex h-full space-x-4">
|
||||||
|
<video
|
||||||
|
ref="videoEl"
|
||||||
|
class="video-js vjs-default-skin h-full w-full"
|
||||||
|
preload="auto"
|
||||||
|
controls
|
||||||
|
></video>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -19,9 +19,16 @@ const cv: WorkbenchQuickNavItem[] = [
|
|||||||
color: '#3fb27f',
|
color: '#3fb27f',
|
||||||
authority: ['iva'],
|
authority: ['iva'],
|
||||||
icon: 'mdi:video',
|
icon: 'mdi:video',
|
||||||
title: '视频智能分析',
|
title: '工作视频分析',
|
||||||
url: '/cv/iva',
|
url: '/cv/iva',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
color: '#3fb27f',
|
||||||
|
authority: ['iva-sc'],
|
||||||
|
icon: 'mdi:video-image',
|
||||||
|
title: '蚕茧视频分析',
|
||||||
|
url: '/cv/iva-sc',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
color: '#3fb27f',
|
color: '#3fb27f',
|
||||||
authority: ['sca'],
|
authority: ['sca'],
|
||||||
@@ -139,7 +146,7 @@ function getGreeting() {
|
|||||||
{{ getGreeting() }}, {{ userStore.userInfo?.username }},
|
{{ getGreeting() }}, {{ userStore.userInfo?.username }},
|
||||||
开始您一天的工作吧!
|
开始您一天的工作吧!
|
||||||
</template>
|
</template>
|
||||||
<template #description> 欢迎使用主干AI实验室</template>
|
<template #description> 欢迎使用AI实验室</template>
|
||||||
</WorkbenchHeader>
|
</WorkbenchHeader>
|
||||||
|
|
||||||
<div class="mt-5 flex flex-col lg:flex-row">
|
<div class="mt-5 flex flex-col lg:flex-row">
|
||||||
|
|||||||
Reference in New Issue
Block a user