diff --git a/src/api/routes.py b/src/api/routes.py index 98a0d68..762b4c5 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -1,6 +1,6 @@ # api/routes.py from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Request, Depends -from typing import Optional +from typing import Optional, Dict, Any import uuid from datetime import datetime from pathlib import Path @@ -118,14 +118,86 @@ async def upload_stp( @router.get("/status/{task_id}") @router.post("/status/{task_id}") -async def get_status(task_id: str): - """获取任务状态""" - if task_id not in tasks: +async def get_status(task_id: str, db_session: AsyncSession = Depends(get_db_session)): + """ + 获取任务状态 + + 优先返回内存中的任务信息; + 如果内存中不存在,则从 PostgreSQL + RustFS 组装一个持久化的任务视图, + 结构与内存任务保持尽量一致,便于前端集中展示总结性信息。 + """ + # 1. 内存任务(进行中的任务) + if task_id in tasks: + task = tasks[task_id] + logger.info(f"返回内存任务状态: {task_id} - {task['status']}") + return task + + # 2. 持久化任务(已完成/失败,或服务重启后的任务) + from sqlalchemy import select + from models.database import ProcessingTask, STPFile, GeometryData, MeshData, MoldCavityData + storage_service = StorageIntegrationService() + + # 查询任务和文件元数据 + result = await db_session.execute( + select(ProcessingTask, STPFile) + .join(STPFile, ProcessingTask.stp_file_id == STPFile.id) + .where(ProcessingTask.task_id == task_id) + ) + row = result.first() + if not row: raise HTTPException(404, "任务不存在") - task = tasks[task_id] - logger.info(f"返回任务状态: {task_id} - {task['status']}") - return task + processing_task, stp_file = row + + # 从 RustFS 取几何 / 型腔 / 网格详细 JSON(小量数据,便于前端展示汇总) + file_with_data = await storage_service.get_stp_file_with_data( + db_session, stp_file_id=stp_file.id + ) + + geometry_json: Optional[Dict[str, Any]] = None + if file_with_data.get("geometry_data"): + # save_geometry_data 支持两种格式,这里直接拿回来的就是几何字典 + geometry_json = file_with_data["geometry_data"] + + cavity_json: Optional[Dict[str, Any]] = file_with_data.get("mold_cavity_data") + + # 组装网格摘要:从 MeshData 表中读出摘要字段,避免把完整网格 JSON 丢给前端 + mesh_summary = None + mesh_record = await db_session.execute( + select(MeshData).where(MeshData.stp_file_id == stp_file.id) + ) + mesh_record = mesh_record.scalar_one_or_none() + if mesh_record: + mesh_summary = { + "vertex_count": mesh_record.vertex_count, + "face_count": mesh_record.face_count, + "point_count": mesh_record.point_count, + "quality": mesh_record.quality, + } + + # 构造与内存任务兼容的任务视图 + task_view = { + "task_id": processing_task.task_id, + "status": processing_task.status, + "filename": stp_file.original_filename if stp_file else "", + "file_path": stp_file.file_path or "", + "file_size": stp_file.file_size if stp_file else 0, + "upload_time": processing_task.created_time.isoformat() + if processing_task.created_time + else "", + "completed_at": processing_task.completed_time.isoformat() + if processing_task.completed_time + else "", + "geometry_data": geometry_json, + # key_info 沿用之前的结构,直接使用详细型腔 JSON,前端已按该结构解析 + "key_info": cavity_json, + "mesh_summary": mesh_summary, + "analysis_result": None, + "error": processing_task.error_message or stp_file.error_message or None, + } + + logger.info(f"返回持久化任务状态: {task_id} - {processing_task.status}") + return task_view @router.get("/debug/tasks") diff --git a/static/style.css b/static/style.css index 4a5a14d..1942f1b 100644 --- a/static/style.css +++ b/static/style.css @@ -7,11 +7,13 @@ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #1f2933 0%, #111827 100%); min-height: 100vh; - padding: 20px; + padding: 16px; + color: #111827; } +/* 旧版容器保留,兼容可能的使用 */ .container { max-width: 1200px; margin: 0 auto; @@ -457,4 +459,511 @@ body { .analysis-info { grid-template-columns: 1fr; } +} + +/* ===== 新版 Vue SPA 布局样式 ===== */ + +.app-shell { + max-width: 1280px; + margin: 0 auto; + min-height: calc(100vh - 32px); + background: #0b1120; + border-radius: 18px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.app-header { + padding: 18px 24px; + background: radial-gradient(circle at top left, #1d4ed8, #020617 60%); + border-bottom: 1px solid rgba(148, 163, 184, 0.4); + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.app-title { + display: flex; + align-items: center; + gap: 14px; + color: #e5e7eb; +} + +.app-title h1 { + font-size: 1.5rem; + margin-bottom: 2px; +} + +.app-title p { + font-size: 0.9rem; + color: #9ca3af; +} + +.logo { + width: 42px; + height: 42px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + background: radial-gradient(circle at 30% 20%, #f59e0b, #ef4444 60%); + font-size: 1.4rem; +} + +.app-nav { + display: flex; + gap: 10px; + align-items: center; +} + +.nav-link { + padding: 8px 16px; + border-radius: 999px; + text-decoration: none; + color: #e5e7eb; + font-size: 0.9rem; + border: 1px solid transparent; + transition: all 0.2s ease; +} + +.nav-link:hover { + border-color: rgba(148, 163, 184, 0.7); + background: rgba(15, 23, 42, 0.7); +} + +.nav-link.active { + background: linear-gradient(135deg, #3b82f6, #6366f1); + color: white; + border-color: transparent; +} + +.app-main { + flex: 1; + padding: 18px 22px 16px; + background: radial-gradient(circle at top, #020617, #020617 50%, #020617 100%); + color: #e5e7eb; + overflow-y: auto; +} + +.app-footer { + padding: 10px 18px; + font-size: 0.8rem; + color: #9ca3af; + background: #020617; + border-top: 1px solid rgba(148, 163, 184, 0.3); + display: flex; + justify-content: space-between; + align-items: center; +} + +.footer-left span + span { + margin-left: 6px; +} + +.dashboard, +.history-view, +.result-view { + display: flex; + flex-direction: column; + gap: 18px; +} + +.dashboard h2, +.history-view h2, +.result-view h2 { + font-size: 1.3rem; + margin-bottom: 4px; +} + +.card-subtitle { + color: #9ca3af; + font-size: 0.9rem; +} + +.grid-2 { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); + gap: 16px; +} + +.grid-3 { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + margin-top: 12px; +} + +.card { + background: radial-gradient(circle at top left, #020617, #020617 60%); + border-radius: 16px; + padding: 16px 18px; + border: 1px solid rgba(148, 163, 184, 0.25); + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.7); +} + +.card h2, +.card h3 { + font-size: 1.05rem; + margin-bottom: 8px; + color: #e5e7eb; +} + +.upload-panel { + margin-top: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.upload-dropzone { + border-radius: 14px; + border: 1px dashed rgba(148, 163, 184, 0.6); + padding: 26px 18px; + text-align: center; + background: linear-gradient(135deg, rgba(15, 23, 42, 0.9), rgba(30, 64, 175, 0.4)); + cursor: pointer; + transition: all 0.2s ease; + color: #e5e7eb; +} + +.upload-dropzone:hover { + border-color: #60a5fa; + background: linear-gradient(135deg, rgba(15, 23, 42, 1), rgba(37, 99, 235, 0.6)); +} + +.upload-icon { + font-size: 2.3rem; + margin-bottom: 6px; +} + +.upload-btn { + background: linear-gradient(135deg, #3b82f6, #6366f1); + color: white; + border: none; + padding: 10px 18px; + font-size: 0.95rem; + border-radius: 999px; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; +} + +.upload-btn:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 10px 25px rgba(37, 99, 235, 0.4); +} + +.upload-btn:disabled { + background: #4b5563; + cursor: not-allowed; + box-shadow: none; +} + +.error-text { + color: #fecaca; + font-size: 0.85rem; +} + +.current-task { + margin-top: 10px; + border-radius: 14px; + border: 1px solid rgba(148, 163, 184, 0.4); + background: radial-gradient(circle at top left, #020617, #020617 70%); +} + +.info-row { + display: flex; + justify-content: space-between; + font-size: 0.9rem; + color: #e5e7eb; +} + +.info-row + .info-row { + margin-top: 4px; +} + +.small-hint { + margin-top: 6px; + font-size: 0.8rem; + color: #9ca3af; +} + +.summary-card { + display: flex; + flex-direction: column; + gap: 10px; +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin-top: 4px; +} + +.summary-item { + background: rgba(15, 23, 42, 0.9); + border-radius: 12px; + padding: 8px 10px; + border: 1px solid rgba(148, 163, 184, 0.4); +} + +.summary-label { + font-size: 0.85rem; + color: #9ca3af; + margin-bottom: 2px; +} + +.summary-value { + font-size: 1.2rem; + font-weight: 600; + color: #e5e7eb; +} + +.summary-value.text-left { + font-size: 0.9rem; +} + +.summary-value .filename { + font-weight: 600; +} + +.summary-value .meta { + color: #9ca3af; + margin-top: 2px; +} + +.summary-empty { + grid-column: 1 / -1; + font-size: 0.9rem; + color: #9ca3af; +} + +.link-btn { + margin-top: 4px; + font-size: 0.9rem; + color: #93c5fd; + text-decoration: none; +} + +.link-btn:hover { + text-decoration: underline; +} + +.center-block { + text-align: center; + padding: 24px 8px; + color: #9ca3af; +} + +.spinner { + border: 3px solid rgba(148, 163, 184, 0.3); + border-top: 3px solid #60a5fa; + border-radius: 50%; + width: 32px; + height: 32px; + animation: spin 0.9s linear infinite; + margin: 0 auto 10px; +} + +.spinner.small { + width: 22px; + height: 22px; + margin-bottom: 6px; +} + +.status-pill { + padding: 2px 10px; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + text-transform: none; +} + +.status-processing { + background: rgba(250, 204, 21, 0.15); + color: #facc15; +} + +.status-completed { + background: rgba(52, 211, 153, 0.18); + color: #6ee7b7; +} + +.status-failed { + background: rgba(248, 113, 113, 0.18); + color: #fb7185; +} + +.file-list { + display: flex; + flex-direction: column; + gap: 10px; + margin-top: 8px; +} + +.file-card { + border-radius: 14px; + border: 1px solid rgba(148, 163, 184, 0.4); + background: rgba(15, 23, 42, 0.9); + padding: 10px 12px; +} + +.file-card.expanded { + border-color: #60a5fa; +} + +.file-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.file-name { + font-size: 0.95rem; + color: #e5e7eb; +} + +.file-meta { + font-size: 0.8rem; + color: #9ca3af; +} + +.expand-btn { + border: none; + background: transparent; + color: #9ca3af; + font-size: 0.8rem; + cursor: pointer; +} + +.record-list { + margin-top: 8px; + border-top: 1px dashed rgba(55, 65, 81, 0.8); + padding-top: 6px; +} + +.record-item { + border-radius: 12px; + border: 1px solid rgba(55, 65, 81, 0.9); + background: rgba(15, 23, 42, 0.95); + padding: 8px 10px; + margin-top: 6px; + cursor: pointer; + transition: background 0.2s ease, transform 0.1s ease; +} + +.record-item:hover { + background: rgba(17, 24, 39, 1); + transform: translateY(-1px); +} + +.record-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.82rem; + color: #e5e7eb; +} + +.record-meta { + margin-top: 4px; + font-size: 0.78rem; + color: #9ca3af; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.result-layout { + display: flex; + flex-direction: column; + gap: 14px; +} + +.task-card .info-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + font-size: 0.88rem; +} + +.info-item { + display: flex; + flex-direction: column; + gap: 2px; +} + +.info-label { + font-size: 0.8rem; + color: #9ca3af; +} + +.info-value { + font-size: 0.92rem; + color: #e5e7eb; +} + +.data-grid { + display: flex; + flex-direction: column; + gap: 6px; +} + +.data-item { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.88rem; + color: #e5e7eb; +} + +.data-label { + color: #9ca3af; +} + +.data-value { + font-weight: 500; +} + +.placeholder { + font-size: 0.85rem; + color: #6b7280; +} + +.error-box { + margin-top: 8px; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid rgba(248, 113, 113, 0.5); + background: rgba(127, 29, 29, 0.4); + font-size: 0.85rem; + color: #fecaca; +} + +@media (max-width: 900px) { + .app-shell { + border-radius: 14px; + } + .app-header { + flex-direction: column; + align-items: flex-start; + } + .grid-2 { + grid-template-columns: minmax(0, 1fr); + } + .grid-3 { + grid-template-columns: minmax(0, 1fr); + } + .task-card .info-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } \ No newline at end of file