This commit is contained in:
cjw
2026-02-17 00:25:18 +08:00
parent 64acdb0e8c
commit 9cdb46eec3
2 changed files with 590 additions and 9 deletions
+79 -7
View File
@@ -1,6 +1,6 @@
# api/routes.py # api/routes.py
from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Request, Depends from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Request, Depends
from typing import Optional from typing import Optional, Dict, Any
import uuid import uuid
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@@ -118,14 +118,86 @@ async def upload_stp(
@router.get("/status/{task_id}") @router.get("/status/{task_id}")
@router.post("/status/{task_id}") @router.post("/status/{task_id}")
async def get_status(task_id: str): async def get_status(task_id: str, db_session: AsyncSession = Depends(get_db_session)):
"""获取任务状态""" """
if task_id not in tasks: 获取任务状态
优先返回内存中的任务信息;
如果内存中不存在,则从 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, "任务不存在") raise HTTPException(404, "任务不存在")
task = tasks[task_id] processing_task, stp_file = row
logger.info(f"返回任务状态: {task_id} - {task['status']}")
return task # 从 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") @router.get("/debug/tasks")
+511 -2
View File
@@ -7,11 +7,13 @@
body { body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 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; min-height: 100vh;
padding: 20px; padding: 16px;
color: #111827;
} }
/* 旧版容器保留,兼容可能的使用 */
.container { .container {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
@@ -458,3 +460,510 @@ body {
grid-template-columns: 1fr; 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));
}
}