From 32e5dfa9730276c1d7fe5d89e3aa95de2419061f Mon Sep 17 00:00:00 2001 From: lubimu-647 Date: Fri, 5 Jun 2026 09:27:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E6=9E=B6=E6=96=97=E6=AE=B4=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/server/main.py | 8 + .../services/action_detection_service.py | 265 ++++++++++++++ apps/server/services/model_service.py | 34 +- apps/web/package-lock.json | 48 +-- apps/web/src/api/detection.js | 13 + apps/web/src/components/ImageDetection.vue | 337 +++++++++++++++--- apps/web/vite.config.js | 14 +- 7 files changed, 632 insertions(+), 87 deletions(-) create mode 100644 apps/server/services/action_detection_service.py diff --git a/apps/server/main.py b/apps/server/main.py index ee6bd46..574fd1d 100644 --- a/apps/server/main.py +++ b/apps/server/main.py @@ -87,6 +87,14 @@ app.add_middleware( ) app.mount("/static", StaticFiles(directory="static"), name="static") + +docker_output_dir = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + "external", "video-recognition-system", "PaddlePaddle", "PaddleDetection", "output" +) +os.makedirs(docker_output_dir, exist_ok=True) +app.mount("/docker-output", StaticFiles(directory=docker_output_dir), name="docker-output") + app.include_router(detection.router, prefix="/api") app.include_router(models.router, prefix="/api") diff --git a/apps/server/services/action_detection_service.py b/apps/server/services/action_detection_service.py new file mode 100644 index 0000000..424811f --- /dev/null +++ b/apps/server/services/action_detection_service.py @@ -0,0 +1,265 @@ +""" +PaddleVideo 行为识别服务适配器(Docker API 调用方式) +通过 HTTP 请求调用运行在 Docker 中的 ppTSM 行为识别模型 +""" + +import os +import cv2 +import numpy as np +import logging +import httpx +import base64 +import json +from typing import Dict, List, Optional + +logger = logging.getLogger(__name__) + +# 获取外部项目路径 +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +EXTERNAL_PADDLE_PATH = os.path.join(BASE_DIR, 'external', 'video-recognition-system', 'PaddlePaddle') + + +class ActionDetectionService: + """行为识别服务(调用外部 Docker 服务)""" + + def __init__(self): + # 从环境变量获取 Docker 服务地址 + self.api_base_url = os.environ.get( + 'ACTION_DETECTION_API_URL', + 'http://localhost:8081' # 统一使用 8081 端口 + ) + self.timeout = int(os.environ.get('ACTION_DETECTION_TIMEOUT', '30')) + + # 类别定义(根据你的 ppTSM 模型) + self.classes = ['fight', 'normal'] + self.labels = {0: 'fight', 1: 'normal'} + + # 外部项目路径 + self.external_paddle_path = EXTERNAL_PADDLE_PATH + self.model_path = os.path.join(self.external_paddle_path, 'PaddleVideo', 'inference', 'ppTSM') + + # 健康检查 + self.available = self._check_service_health() + + logger.info(f"行为识别服务初始化完成") + logger.info(f"API地址: {self.api_base_url}") + logger.info(f"外部项目路径: {self.external_paddle_path}") + logger.info(f"服务可用: {self.available}") + + def _check_service_health(self) -> bool: + """检查 Docker 服务是否可用""" + try: + with httpx.Client(timeout=5) as client: + response = client.get(f"{self.api_base_url}/health") + return response.status_code == 200 + except Exception as e: + logger.warning(f"服务健康检查失败: {e}") + return False + + def detect_image(self, image: np.ndarray, threshold: float = 0.5) -> Dict: + """ + 调用 Docker 服务进行行为识别 + + Args: + image: OpenCV 图片 (BGR格式) + threshold: 置信度阈值 + + Returns: + 检测结果字典 + """ + if not self.available: + return { + 'success': False, + 'message': '行为识别服务不可用', + 'detections': [], + 'stats': None + } + + try: + # 将图片转换为 base64 + _, img_encoded = cv2.imencode('.jpg', image) + img_base64 = base64.b64encode(img_encoded).decode('utf-8') + + # 构建请求数据 + payload = { + 'image': img_base64, + 'threshold': threshold + } + + # 调用 Docker 服务 + with httpx.Client(timeout=self.timeout) as client: + response = client.post( + f"{self.api_base_url}/api/detect", + json=payload + ) + + if response.status_code != 200: + return { + 'success': False, + 'message': f"API调用失败: {response.status_code}", + 'detections': [], + 'stats': None + } + + # 解析响应 + result = response.json() + + return { + 'success': True, + 'message': '检测完成', + 'detections': result.get('detections', []), + 'stats': result.get('stats', None) + } + + except Exception as e: + logger.error(f"检测失败: {e}") + return { + 'success': False, + 'message': f'检测失败: {e}', + 'detections': [], + 'stats': None + } + + +class ActionDetectionModel: + """行为识别模型包装器,兼容 YOLO 接口""" + + def __init__(self): + self.service = ActionDetectionService() + self.names = {0: 'fight', 1: 'normal'} + + def __call__(self, image, conf=0.5, iou=0.45, verbose=False): + """ + 模拟 YOLO 模型的调用接口 + + Args: + image: OpenCV 图片 + conf: 置信度阈值 + iou: IoU 阈值 + verbose: 是否输出详细信息 + + Returns: + 模拟 YOLO 结果的对象 + """ + result = self.service.detect_image(image, threshold=conf) + return [ActionDetectionResult(result, self.names)] + + +class ActionDetectionResult: + """模拟 YOLO 检测结果对象""" + + def __init__(self, detection_result: Dict, names: Dict): + self.detection_result = detection_result + self.names = names + self.boxes = self._create_boxes() + + def _create_boxes(self): + """创建模拟的 boxes 对象""" + detections = self.detection_result.get('detections', []) + + if not detections: + return MockBoxes([]) + + xyxy = [] + conf = [] + cls = [] + + for det in detections: + if 'bbox' in det: + xyxy.append(det['bbox']) + conf.append(det.get('confidence', 0.0)) + cls.append(det.get('class_id', 0)) + + return MockBoxes(xyxy, conf, cls) + + +class MockBoxes: + """模拟 YOLO boxes 对象""" + + def __init__(self, xyxy_list, conf_list=None, cls_list=None): + try: + import torch + use_torch = True + except ImportError: + use_torch = False + + if xyxy_list and len(xyxy_list) > 0: + if use_torch: + self.xyxy = torch.tensor(xyxy_list, dtype=torch.float32) + self.conf = torch.tensor(conf_list, dtype=torch.float32).reshape(-1, 1) + self.cls = torch.tensor(cls_list, dtype=torch.int64).reshape(-1, 1) + else: + self.xyxy = np.array(xyxy_list, dtype=np.float32) + self.conf = np.array(conf_list, dtype=np.float32).reshape(-1, 1) + self.cls = np.array(cls_list, dtype=np.int64).reshape(-1, 1) + else: + if use_torch: + self.xyxy = torch.empty((0, 4), dtype=torch.float32) + self.conf = torch.empty((0, 1), dtype=torch.float32) + self.cls = torch.empty((0, 1), dtype=torch.int64) + else: + self.xyxy = np.array([]).reshape(0, 4) + self.conf = np.array([]).reshape(0, 1) + self.cls = np.array([]).reshape(0, 1) + + self._use_torch = use_torch + + def __iter__(self): + for i in range(len(self.xyxy)): + yield MockBox( + self.xyxy[i], + self.conf[i][0] if len(self.conf) > i else 0.0, + self.cls[i][0] if len(self.cls) > i else 0 + ) + + def __len__(self): + return len(self.xyxy) + + def cpu(self): + return self + + def numpy(self): + if self._use_torch: + if len(self.xyxy) > 0: + return ( + self.xyxy.numpy(), + self.conf.numpy(), + self.cls.numpy() + ) + else: + return ( + np.array([]).reshape(0, 4), + np.array([]).reshape(0, 1), + np.array([], dtype=np.int64).reshape(0, 1) + ) + else: + return ( + self.xyxy, + self.conf, + self.cls + ) + + +class MockBox: + """模拟单个 YOLO box 对象""" + + def __init__(self, xyxy, conf, cls): + try: + import torch + use_torch = True + except ImportError: + use_torch = False + + if use_torch: + if isinstance(xyxy, torch.Tensor): + self.xyxy = xyxy + else: + self.xyxy = torch.tensor(xyxy, dtype=torch.float32) + else: + if isinstance(xyxy, np.ndarray): + self.xyxy = xyxy + else: + self.xyxy = np.array(xyxy, dtype=np.float32) + + self.conf = conf + self.cls = cls diff --git a/apps/server/services/model_service.py b/apps/server/services/model_service.py index cc05f34..2189f9e 100644 --- a/apps/server/services/model_service.py +++ b/apps/server/services/model_service.py @@ -109,6 +109,15 @@ class ModelService: 'size': '200MB', 'description': '基于PaddlePaddle PP-YOLOE-l的违停检测模型,支持车牌识别', 'name': '违停检测 (Paddle)' + }, + 'action_detection': { + 'path': 'docker_api', + 'type': 'docker_api', + 'classes': ['fight', 'normal'], + 'labels': {'fight': '打架', 'normal': '正常'}, + 'size': 'Docker', + 'description': '基于PaddleVideo ppTSM的打架检测模型(通过Docker API调用)', + 'name': '打架检测 (Docker)' } } @@ -117,9 +126,12 @@ class ModelService: for model_id, config in self.model_configs.items(): model_path = config['path'] - # 检查模型是否存在(Paddle模型检查目录,YOLO模型检查文件) + # 检查模型是否存在 model_exists = False - if config['type'] == 'paddle': + if config['type'] == 'docker_api': + # Docker API 类型的模型不需要检查文件,总是可用 + model_exists = True + elif config['type'] == 'paddle': model_dir = os.path.dirname(model_path) required_files = ['model.pdmodel', 'model.pdiparams', 'infer_cfg.yml'] model_exists = all( @@ -153,6 +165,24 @@ class ModelService: config = self.model_configs[model_id] + # 处理 Docker API 模型 + if config['type'] == 'docker_api': + try: + if model_id == 'action_detection': + from .action_detection_service import ActionDetectionModel + logger.info(f"正在加载 Docker API 行为识别服务: {model_id}") + model = ActionDetectionModel() + else: + logger.error(f"未知的 Docker API 模型类型: {model_id}") + return None + + self.models[model_id] = model + logger.info(f"Docker API 服务加载成功: {model_id}") + return model + except Exception as e: + logger.error(f"Docker API 服务加载失败: {model_id}, 错误: {e}") + return None + # 处理 PaddleDetection 模型 if config['type'] == 'paddle': try: diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index c11ce23..1815d99 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -609,9 +609,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -626,9 +623,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -643,9 +637,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -660,9 +651,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -677,9 +665,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -694,9 +679,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -711,9 +693,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -728,9 +707,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -745,9 +721,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -762,9 +735,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -779,9 +749,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -796,9 +763,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -813,9 +777,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -924,6 +885,7 @@ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/lodash": "*" } @@ -1498,13 +1460,15 @@ "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash-es": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash-unified": { "version": "1.0.3", @@ -1717,6 +1681,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -1776,6 +1741,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.34.tgz", "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.34", "@vue/compiler-sfc": "3.5.34", diff --git a/apps/web/src/api/detection.js b/apps/web/src/api/detection.js index b935bfb..7653011 100644 --- a/apps/web/src/api/detection.js +++ b/apps/web/src/api/detection.js @@ -5,6 +5,11 @@ const api = axios.create({ timeout: 30000 }) +const dockerApi = axios.create({ + baseURL: '/docker-api', + timeout: 120000 +}) + export const detectionApi = { getModels() { return api.get('/models') @@ -26,5 +31,13 @@ export const detectionApi = { }, params }) + }, + + detectVideo(formData) { + return dockerApi.post('/api/fight_detect', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) } } diff --git a/apps/web/src/components/ImageDetection.vue b/apps/web/src/components/ImageDetection.vue index 2338c62..3bab4c3 100644 --- a/apps/web/src/components/ImageDetection.vue +++ b/apps/web/src/components/ImageDetection.vue @@ -36,25 +36,51 @@ - + - {{ resultImage ? '上传新图片' : '上传图片' }} + {{ resultImage || resultVideo ? '上传新文件' : (isActionDetection ? '上传视频' : '上传图片') }}
+ 检测结果 + +
+
+

请上传图片进行检测

@@ -97,8 +123,21 @@ + + + +
+
{{ formattedJson }}
+
+
+ - + @@ -131,9 +204,13 @@ import { ElMessage } from 'element-plus' import { UploadFilled, Picture, + VideoCamera, View, DataLine, - List + List, + Files, + Warning, + Loading } from '@element-plus/icons-vue' import { detectionApi } from '@/api/detection' import DetectionConfig from './DetectionConfig.vue' @@ -192,16 +269,29 @@ const stopResize = () => { const originalImage = ref('') const resultImage = ref('') +const uploadedVideoUrl = ref('') +const resultVideo = ref('') const detections = ref([]) const stats = ref(null) +const alerts = ref([]) +const isDetecting = ref(false) +const dockerResult = ref(null) +const actionResult = ref(null) + +const isActionDetection = computed(() => { + return config.value.model === 'action_detection' +}) + const uploadUrl = computed(() => { + if (isActionDetection.value) { + return null + } const params = new URLSearchParams({ model_id: config.value.model, confidence: config.value.confidence, iou: config.value.iou }) - // 添加算法配置 if (config.value.algorithmConfig && Object.keys(config.value.algorithmConfig).length > 0) { params.append('algorithm_config', JSON.stringify(config.value.algorithmConfig)) } @@ -209,35 +299,61 @@ const uploadUrl = computed(() => { return `/api/detect/image?${params.toString()}` }) -const beforeUpload = (file) => { - const isImage = file.type.startsWith('image/') - if (!isImage) { - ElMessage.error('只能上传图片文件') +const formattedJson = computed(() => { + if (!dockerResult.value) return '' + return JSON.stringify(dockerResult.value, null, 2) +}) + +const beforeUpload = async (file) => { + isDetecting.value = true + + if (isActionDetection.value) { + const isVideo = file.type.startsWith('video/') + if (!isVideo) { + ElMessage.error('只能上传视频文件') + isDetecting.value = false + return false + } + + const formData = new FormData() + formData.append('video', file) + formData.append('confidence', config.value.confidence) + + try { + const response = await detectionApi.detectVideo(formData) + handleActionDetectionSuccess(response.data) + } catch (error) { + handleUploadError(error) + } + return false + } else { + const isImage = file.type.startsWith('image/') + if (!isImage) { + ElMessage.error('只能上传图片文件') + isDetecting.value = false + return false + } + originalImage.value = URL.createObjectURL(file) + return true } - originalImage.value = URL.createObjectURL(file) - return true } const handleUploadSuccess = (response) => { + isDetecting.value = false console.log('Upload success response:', response) + if (response.success) { - // 使用 base64 图片数据 if (response.data.image_base64) { resultImage.value = `data:image/jpeg;base64,${response.data.image_base64}` - console.log('Result image set, length:', response.data.image_base64.length) } else { console.error('No image_base64 in response:', response.data) } detections.value = response.data.detections || [] stats.value = response.data.stats - // 处理告警信息 if (response.data.alerts && response.data.alerts.length > 0) { alerts.value = response.data.alerts - console.log('收到告警:', response.data.alerts) - - // 显示告警通知 response.data.alerts.forEach(alert => { ElMessage({ message: `行为告警: ${alert.type} - ${alert.message}`, @@ -253,6 +369,38 @@ const handleUploadSuccess = (response) => { } } +const handleActionDetectionSuccess = (response) => { + isDetecting.value = false + console.log('Action detection response:', response) + + if (response.code === 200) { + dockerResult.value = response + actionResult.value = response.data + + if (response.data.output_video) { + const videoPath = response.data.output_video.replace(/^output\//, '') + resultVideo.value = `/docker-output/${videoPath}` + } + + ElMessage.success(response.message) + } else { + ElMessage.error(response.message || '检测失败') + } +} + +const handleUploadError = (error) => { + isDetecting.value = false + console.error('Upload error:', error) + + if (error.response) { + ElMessage.error(`上传失败: ${error.response.data.message || error.response.status}`) + } else if (error.request) { + ElMessage.error('请求超时或服务器未响应,请检查Docker服务是否正常运行') + } else { + ElMessage.error(`上传失败: ${error.message}`) + } +} + const getConfidenceType = (confidence) => { if (!confidence && confidence !== 0) return 'info' if (confidence >= 0.8) return 'success' @@ -285,7 +433,6 @@ const modelName = computed(() => { overflow-y: auto; } -/* 拖拽调整条 */ .resize-handle { width: 8px; flex-shrink: 0; @@ -326,7 +473,6 @@ const modelName = computed(() => { overflow-x: hidden; } -/* 卡片通用样式 */ :deep(.el-card) { border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.1); @@ -348,7 +494,6 @@ const modelName = computed(() => { border-radius: 16px 16px 0 0; } -/* 卡片头部样式 */ .card-header { display: flex; align-items: center; @@ -374,7 +519,6 @@ const modelName = computed(() => { display: block; } -/* 配置卡片样式 */ .config-card { height: auto; max-height: calc(100vh - 100px); @@ -401,14 +545,12 @@ const modelName = computed(() => { width: 100%; } -/* 模型选择器 */ .model-size { float: right; color: #64748B; font-size: 12px; } -/* 滑块样式 */ .slider-value { text-align: center; font-size: 14px; @@ -424,7 +566,6 @@ const modelName = computed(() => { font-family: 'Fira Code', monospace; } -/* 空状态占位区域 */ .empty-placeholder { display: flex; flex-direction: column; @@ -444,8 +585,53 @@ const modelName = computed(() => { .empty-text { font-size: 16px; color: #94A3B8; +} + +.detection-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + color: #64748B; +} + +.loading-spinner { + display: flex; + align-items: center; + justify-content: center; + width: 80px; + height: 80px; + margin-bottom: 16px; +} + +.loading-icon { + font-size: 48px; + color: #22C55E; + animation: spin 1s linear infinite; +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.loading-text { + font-size: 16px; + color: #F8FAFC; + font-weight: 500; margin: 0 0 8px 0; - font-family: 'Fira Sans', sans-serif; +} + +.loading-hint { + font-size: 13px; + color: #64748B; + margin: 0; } .empty-hint { @@ -454,7 +640,6 @@ const modelName = computed(() => { margin: 0; } -/* 图片展示区域 */ .image-card { margin-bottom: 20px; } @@ -488,6 +673,21 @@ const modelName = computed(() => { background: #000; } +.video-result-container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.display-video { + width: 100%; + height: 100%; + object-fit: contain; + background: #000; +} + .placeholder { text-align: center; color: #64748B; @@ -505,7 +705,6 @@ const modelName = computed(() => { margin: 0; } -/* 统计卡片 */ .stats-card { margin-bottom: 20px; height: calc(100% - 20px); @@ -553,7 +752,6 @@ const modelName = computed(() => { border: 1px solid rgba(255, 255, 255, 0.1); } -/* 详情卡片 */ .details-card { margin-bottom: 20px; } @@ -627,7 +825,6 @@ const modelName = computed(() => { border: 1px solid rgba(255, 255, 255, 0.1); } -/* 帮助图标样式 */ .help-icon { margin-left: 6px; font-size: 14px; @@ -640,7 +837,6 @@ const modelName = computed(() => { color: #22C55E; } -/* 告警卡片 */ .alerts-card { margin-bottom: 20px; border: 2px solid #EF4444; @@ -722,7 +918,70 @@ const modelName = computed(() => { font-family: 'Fira Code', monospace; } -/* 响应式布局 */ +.json-result-card { + margin-bottom: 20px; +} + +.json-result-card :deep(.el-card__body) { + padding: 20px; +} + +.json-content { + background: #0F172A; + border-radius: 12px; + padding: 16px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.json-pre { + color: #22C55E; + font-family: 'Fira Code', monospace; + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + word-break: break-all; + margin: 0; + max-height: 400px; + overflow-y: auto; +} + +.action-result-card { + margin-bottom: 20px; +} + +.action-result-card :deep(.el-card__body) { + padding: 20px; +} + +.action-result-content { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; +} + +.result-item { + display: flex; + flex-direction: column; + gap: 8px; + padding: 16px; + background: rgba(15, 23, 42, 0.6); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 12px; + text-align: center; +} + +.result-label { + font-size: 13px; + color: #94A3B8; + font-weight: 500; +} + +.result-value { + font-size: 16px; + color: #F8FAFC; + font-weight: 600; +} + @media (max-width: 768px) { .image-detection-container { flex-direction: column; @@ -751,14 +1010,8 @@ const modelName = computed(() => { min-height: 200px; } - .stats-descriptions :deep(.el-descriptions__body) { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 8px; - } - - .stats-descriptions :deep(.el-descriptions__cell) { - padding: 12px; + .action-result-content { + grid-template-columns: 1fr; } } @@ -768,4 +1021,4 @@ const modelName = computed(() => { min-height: 180px; } } - + \ No newline at end of file diff --git a/apps/web/vite.config.js b/apps/web/vite.config.js index 3c0980e..bec0642 100644 --- a/apps/web/vite.config.js +++ b/apps/web/vite.config.js @@ -10,19 +10,29 @@ export default defineConfig({ } }, server: { + host: '0.0.0.0', port: 3001, proxy: { '/api': { - target: 'http://localhost:8000', + target: 'http://localhost:8001', changeOrigin: true }, '/static': { - target: 'http://localhost:8000', + target: 'http://localhost:8001', changeOrigin: true }, '/ws': { target: 'ws://localhost:8000', ws: true + }, + '/docker-api': { + target: 'http://localhost:8081', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/docker-api/, '') + }, + '/docker-output': { + target: 'http://localhost:8001', + changeOrigin: true } } }