Explorar o código

fix: frontend testing fixes - TaskManager unicode + DB auto-migration

- TaskManager.vue: decode 219 unicode escape sequences to Chinese
- database.py: add _migrate_existing_tables() auto-migration for SQLite
  - ALTER TABLE adds 8 new SimulationResult columns if missing
  - Handles model additions without requiring DB reset
- task_manager.py: fix import (SessionLocal from database, Task from models.task)

Verified:
- All 10 frontend pages load with Chinese text
- Project detail page no longer 500 error
- Plan detail page shows 3 results all OK
- Plans list API returns 200 with 1 plan, 3 results
carlin hai 1 semana
pai
achega
17f81b561f

+ 33 - 1
web/backend/app/database.py

@@ -32,6 +32,38 @@ def get_db():
 
 
 def init_db():
-    """Create all database tables."""
+    """Create all database tables and migrate existing tables."""
     from . import models  # noqa: F401 - ensure models are imported
     Base.metadata.create_all(bind=engine)
+    _migrate_existing_tables()
+
+
+def _migrate_existing_tables():
+    """Add missing columns to existing tables (SQLite ALTER TABLE).
+
+    SQLite doesn't support DROP COLUMN or complex migrations, but ADD COLUMN
+    is supported. This handles model additions without requiring DB reset.
+    """
+    from sqlalchemy import inspect, text
+
+    inspector = inspect(engine)
+    if "simulation_results" not in inspector.get_table_names():
+        return
+
+    existing_cols = {c["name"] for c in inspector.get_columns("simulation_results")}
+    new_cols = {
+        "fidelity_level": "VARCHAR(30) DEFAULT 'L1_motorcad_emag'",
+        "confidence_grade": "VARCHAR(2) DEFAULT 'C'",
+        "model_template_version": "VARCHAR(100) DEFAULT ''",
+        "solver_settings_hash": "VARCHAR(64) DEFAULT ''",
+        "constraint_margins_json": "TEXT DEFAULT '{}'",
+        "surrogate_prediction_json": "TEXT DEFAULT '{}'",
+        "cross_validation_json": "TEXT DEFAULT '{}'",
+        "convergence_status_json": "TEXT DEFAULT '{}'",
+    }
+
+    with engine.connect() as conn:
+        for col_name, col_def in new_cols.items():
+            if col_name not in existing_cols:
+                conn.execute(text(f"ALTER TABLE simulation_results ADD COLUMN {col_name} {col_def}"))
+        conn.commit()

+ 2 - 1
web/backend/app/services/task_manager.py

@@ -10,7 +10,8 @@ from datetime import datetime
 from typing import Dict, List, Optional, Any
 from pathlib import Path
 
-from ..database import SessionLocal, Task
+from ..database import SessionLocal
+from ..models.task import Task
 
 
 class TaskManager:

+ 52 - 52
web/frontend/src/views/TaskManager.vue

@@ -3,13 +3,13 @@
     <el-card shadow="never">
       <template #header>
         <div class="card-header">
-          <span class="title">\u4efb\u52a1\u7ba1\u7406</span>
+          <span class="title">任务管理</span>
           <div>
             <el-button size="small" @click="loadTasks" :loading="loading">
-              <el-icon><Refresh /></el-icon> \u5237\u65b0
+              <el-icon><Refresh /></el-icon> 刷新
             </el-button>
             <el-button size="small" type="primary" @click="showCreate = true">
-              <el-icon><Plus /></el-icon> \u521b\u5efa\u4efb\u52a1
+              <el-icon><Plus /></el-icon> 创建任务
             </el-button>
           </div>
         </div>
@@ -17,89 +17,89 @@
 
       <el-table :data="tasks" border v-loading="loading" size="small">
         <el-table-column prop="task_id" label="ID" width="100" />
-        <el-table-column prop="task_name" label="\u4efb\u52a1\u540d\u79f0" min-width="150" />
-        <el-table-column label="\u72b6\u6001" width="100">
+        <el-table-column prop="task_name" label="任务名称" min-width="150" />
+        <el-table-column label="状态" width="100">
           <template #default="{ row }">
             <el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="\u8fdb\u5ea6" width="180">
+        <el-table-column label="进度" width="180">
           <template #default="{ row }">
             <el-progress :percentage="row.progress" :status="row.status === 'completed' ? 'success' : ''" :stroke-width="8" />
             <span style="font-size: 12px; color: #909399;">{{ row.completed_points }}/{{ row.total_points }}</span>
           </template>
         </el-table-column>
-        <el-table-column prop="priority" label="\u4f18\u5148\u7ea7" width="80" />
-        <el-table-column prop="created_at" label="\u521b\u5efa\u65f6\u95f4" width="170">
+        <el-table-column prop="priority" label="优先级" width="80" />
+        <el-table-column prop="created_at" label="创建时间" width="170">
           <template #default="{ row }">{{ formatTime(row.created_at) }}</template>
         </el-table-column>
-        <el-table-column label="\u64cd\u4f5c" width="200" fixed="right">
+        <el-table-column label="操作" width="200" fixed="right">
           <template #default="{ row }">
-            <el-button size="small" link type="primary" @click="viewTask(row)">\u8be6\u60c5</el-button>
-            <el-button v-if="row.status === 'pending'" size="small" link type="success" @click="dispatchTask(row)">\u4e0b\u53d1</el-button>
-            <el-button v-if="['pending','dispatched','running'].includes(row.status)" size="small" link type="danger" @click="cancelTask(row)">\u53d6\u6d88</el-button>
-            <el-button v-if="row.status === 'completed'" size="small" link @click="downloadResults(row)">\u4e0b\u8f7d\u7ed3\u679c</el-button>
+            <el-button size="small" link type="primary" @click="viewTask(row)">详情</el-button>
+            <el-button v-if="row.status === 'pending'" size="small" link type="success" @click="dispatchTask(row)">下发</el-button>
+            <el-button v-if="['pending','dispatched','running'].includes(row.status)" size="small" link type="danger" @click="cancelTask(row)">取消</el-button>
+            <el-button v-if="row.status === 'completed'" size="small" link @click="downloadResults(row)">下载结果</el-button>
           </template>
         </el-table-column>
       </el-table>
     </el-card>
 
-    <el-dialog v-model="showCreate" title="\u521b\u5efa\u4eff\u771f\u4efb\u52a1" width="600px">
+    <el-dialog v-model="showCreate" title="创建仿真任务" width="600px">
       <el-form :model="createForm" label-width="120px" size="default">
-        <el-form-item label="\u4efb\u52a1\u540d\u79f0">
-          <el-input v-model="createForm.task_name" placeholder="\u53ef\u9009\uff0c\u9ed8\u8ba4\u81ea\u52a8\u751f\u6210" />
+        <el-form-item label="任务名称">
+          <el-input v-model="createForm.task_name" placeholder="可选,默认自动生成" />
         </el-form-item>
-        <el-form-item label="\u5173\u8054\u65b9\u6848ID">
+        <el-form-item label="关联方案ID">
           <el-input-number v-model="createForm.plan_id" :min="1" controls-position="right" />
         </el-form-item>
-        <el-form-item label="\u4f18\u5148\u7ea7">
+        <el-form-item label="优先级">
           <el-slider v-model="createForm.priority" :min="1" :max="10" show-input />
         </el-form-item>
-        <el-form-item label="\u6a21\u578b\u6570\u636e">
-          <el-input v-model="createForm.plan_data_json" type="textarea" :rows="4" placeholder='JSON\u683c\u5f0f\u7684\u65b9\u6848\u6570\u636e' />
+        <el-form-item label="模型数据">
+          <el-input v-model="createForm.plan_data_json" type="textarea" :rows="4" placeholder='JSON格式的方案数据' />
         </el-form-item>
-        <el-form-item label="\u53c2\u6570\u5217\u8868">
-          <el-input v-model="createForm.parameters_json" type="textarea" :rows="4" placeholder='JSON\u6570\u7ec4\uff0c\u5982 [{"airgap_mm":1.0},...]' />
+        <el-form-item label="参数列表">
+          <el-input v-model="createForm.parameters_json" type="textarea" :rows="4" placeholder='JSON数组,如 [{"airgap_mm":1.0},...]' />
         </el-form-item>
       </el-form>
       <template #footer>
-        <el-button @click="showCreate = false">\u53d6\u6d88</el-button>
-        <el-button type="primary" :loading="creating" @click="doCreate">\u521b\u5efa</el-button>
+        <el-button @click="showCreate = false">取消</el-button>
+        <el-button type="primary" :loading="creating" @click="doCreate">创建</el-button>
       </template>
     </el-dialog>
 
-    <el-drawer v-model="showDetail" title="\u4efb\u52a1\u8be6\u60c5" size="50%">
+    <el-drawer v-model="showDetail" title="任务详情" size="50%">
       <div v-if="currentTask">
         <el-descriptions :column="2" border size="small">
-          <el-descriptions-item label="\u4efb\u52a1ID">{{ currentTask.task_id }}</el-descriptions-item>
-          <el-descriptions-item label="\u540d\u79f0">{{ currentTask.task_name }}</el-descriptions-item>
-          <el-descriptions-item label="\u72b6\u6001">
+          <el-descriptions-item label="任务ID">{{ currentTask.task_id }}</el-descriptions-item>
+          <el-descriptions-item label="名称">{{ currentTask.task_name }}</el-descriptions-item>
+          <el-descriptions-item label="状态">
             <el-tag :type="statusType(currentTask.status)" size="small">{{ statusLabel(currentTask.status) }}</el-tag>
           </el-descriptions-item>
-          <el-descriptions-item label="\u4f18\u5148\u7ea7">{{ currentTask.priority }}</el-descriptions-item>
-          <el-descriptions-item label="\u603b\u70b9\u6570">{{ currentTask.total_points }}</el-descriptions-item>
-          <el-descriptions-item label="\u5df2\u5b8c\u6210">{{ currentTask.completed_points }}</el-descriptions-item>
-          <el-descriptions-item label="\u521b\u5efa\u65f6\u95f4" :span="2">{{ formatTime(currentTask.created_at) }}</el-descriptions-item>
+          <el-descriptions-item label="优先级">{{ currentTask.priority }}</el-descriptions-item>
+          <el-descriptions-item label="总点数">{{ currentTask.total_points }}</el-descriptions-item>
+          <el-descriptions-item label="已完成">{{ currentTask.completed_points }}</el-descriptions-item>
+          <el-descriptions-item label="创建时间" :span="2">{{ formatTime(currentTask.created_at) }}</el-descriptions-item>
         </el-descriptions>
 
         <el-progress :percentage="currentTask.progress" style="margin: 20px 0;" :stroke-width="12" />
 
-        <el-divider v-if="currentTask.progress_data" content-position="left">\u5b9e\u65f6\u8fdb\u5ea6</el-divider>
+        <el-divider v-if="currentTask.progress_data" content-position="left">实时进度</el-divider>
         <el-descriptions v-if="currentTask.progress_data" :column="2" border size="small">
-          <el-descriptions-item label="\u5f53\u524d\u70b9">{{ currentTask.progress_data.current_point }}</el-descriptions-item>
-          <el-descriptions-item label="\u5df2\u7528\u65f6\u95f4">{{ currentTask.progress_data.elapsed_time?.toFixed(1) }}s</el-descriptions-item>
+          <el-descriptions-item label="当前点">{{ currentTask.progress_data.current_point }}</el-descriptions-item>
+          <el-descriptions-item label="已用时间">{{ currentTask.progress_data.elapsed_time?.toFixed(1) }}s</el-descriptions-item>
         </el-descriptions>
 
-        <el-divider v-if="currentTask.result_metrics" content-position="left">\u7ed3\u679c\u7edf\u8ba1</el-divider>
+        <el-divider v-if="currentTask.result_metrics" content-position="left">结果统计</el-divider>
         <el-descriptions v-if="currentTask.result_metrics" :column="2" border size="small">
           <el-descriptions-item v-for="(v, k) in currentTask.result_metrics" :key="k" :label="k">{{ v }}</el-descriptions-item>
         </el-descriptions>
 
-        <el-divider content-position="left">\u64cd\u4f5c</el-divider>
+        <el-divider content-position="left">操作</el-divider>
         <div class="detail-actions">
-          <el-button v-if="currentTask.status === 'pending'" type="success" @click="dispatchTask(currentTask)">\u4e0b\u53d1\u4efb\u52a1</el-button>
-          <el-button v-if="currentTask.status === 'completed'" type="primary" @click="downloadResults(currentTask)">\u4e0b\u8f7d\u7ed3\u679cJSON</el-button>
-          <el-button @click="loadTasks">\u5237\u65b0\u72b6\u6001</el-button>
+          <el-button v-if="currentTask.status === 'pending'" type="success" @click="dispatchTask(currentTask)">下发任务</el-button>
+          <el-button v-if="currentTask.status === 'completed'" type="primary" @click="downloadResults(currentTask)">下载结果JSON</el-button>
+          <el-button @click="loadTasks">刷新状态</el-button>
         </div>
       </div>
     </el-drawer>
@@ -136,8 +136,8 @@ const statusType = (s: string) => {
 }
 const statusLabel = (s: string) => {
   const map: Record<string, string> = {
-    pending: '\u5f85\u4e0b\u53d1', dispatched: '\u5df2\u4e0b\u53d1', running: '\u8fd0\u884c\u4e2d',
-    completed: '\u5df2\u5b8c\u6210', failed: '\u5931\u8d25', cancelled: '\u5df2\u53d6\u6d88',
+    pending: '待下发', dispatched: '已下发', running: '运行中',
+    completed: '已完成', failed: '失败', cancelled: '已取消',
   }
   return map[s] || s
 }
@@ -149,7 +149,7 @@ const loadTasks = async () => {
     const res: any = await api.get('/tasks', { params: { limit: 50 } })
     tasks.value = res.tasks || []
   } catch (e: any) {
-    ElMessage.error('\u52a0\u8f7d\u4efb\u52a1\u5217\u8868\u5931\u8d25: ' + (e.message || e))
+    ElMessage.error('加载任务列表失败: ' + (e.message || e))
   } finally {
     loading.value = false
   }
@@ -157,9 +157,9 @@ const loadTasks = async () => {
 
 const doCreate = async () => {
   let plan_data: any, parameters: any[]
-  try { plan_data = JSON.parse(createForm.plan_data_json) } catch { ElMessage.error('\u6a21\u578b\u6570\u636e\u4e0d\u662f\u6709\u6548JSON'); return }
-  try { parameters = JSON.parse(createForm.parameters_json) } catch { ElMessage.error('\u53c2\u6570\u5217\u8868\u4e0d\u662f\u6709\u6548JSON'); return }
-  if (!Array.isArray(parameters) || !parameters.length) { ElMessage.error('\u53c2\u6570\u5217\u8868\u4e0d\u80fd\u4e3a\u7a7a'); return }
+  try { plan_data = JSON.parse(createForm.plan_data_json) } catch { ElMessage.error('模型数据不是有效JSON'); return }
+  try { parameters = JSON.parse(createForm.parameters_json) } catch { ElMessage.error('参数列表不是有效JSON'); return }
+  if (!Array.isArray(parameters) || !parameters.length) { ElMessage.error('参数列表不能为空'); return }
 
   creating.value = true
   try {
@@ -169,11 +169,11 @@ const doCreate = async () => {
       priority: createForm.priority,
       plan_data, parameters,
     })
-    ElMessage.success('\u4efb\u52a1\u521b\u5efa\u6210\u529f')
+    ElMessage.success('任务创建成功')
     showCreate.value = false
     loadTasks()
   } catch (e: any) {
-    ElMessage.error('\u521b\u5efa\u5931\u8d25: ' + (e.message || e))
+    ElMessage.error('创建失败: ' + (e.message || e))
   } finally {
     creating.value = false
   }
@@ -182,18 +182,18 @@ const doCreate = async () => {
 const dispatchTask = async (row: any) => {
   try {
     await api.post(`/tasks/${row.task_id}/dispatch`)
-    ElMessage.success('\u4efb\u52a1\u5df2\u4e0b\u53d1')
+    ElMessage.success('任务已下发')
     loadTasks()
   } catch (e: any) {
-    ElMessage.error('\u4e0b\u53d1\u5931\u8d25: ' + (e.message || e))
+    ElMessage.error('下发失败: ' + (e.message || e))
   }
 }
 
 const cancelTask = async (row: any) => {
   try {
-    await ElMessageBox.confirm(`\u786e\u5b9a\u53d6\u6d88\u4efb\u52a1 "${row.task_name}"?`, '\u786e\u8ba4', { type: 'warning' })
+    await ElMessageBox.confirm(`确定取消任务 "${row.task_name}"?`, '确认', { type: 'warning' })
     await api.post(`/tasks/${row.task_id}/cancel`)
-    ElMessage.success('\u4efb\u52a1\u5df2\u53d6\u6d88')
+    ElMessage.success('任务已取消')
     loadTasks()
   } catch { /* cancelled */ }
 }