refactor(pipeline): 路径直接传输 — 统一 ctx 字段名/panel key/step 形参名
This commit is contained in:
229
.qwen/skills/wq_gui_frontend_scaffold/SKILL.md
Normal file
229
.qwen/skills/wq_gui_frontend_scaffold/SKILL.md
Normal file
@ -0,0 +1,229 @@
|
||||
---
|
||||
name: WQ_GUI 前端 Vue3 + Element Plus 脚手架
|
||||
description: WQ_GUI 项目 frontend/ 目录的 Vite + Vue 3 + TS + Element Plus 最小可运行脚手架,以及 useTaskPoller 与 Element Plus UI 的接线模式
|
||||
source: auto-skill
|
||||
extracted_at: '2026-06-02T08:17:33.116Z'
|
||||
---
|
||||
|
||||
# WQ_GUI 前端脚手架 (Vue 3 + Element Plus)
|
||||
|
||||
## 适用场景
|
||||
|
||||
为 WQ_GUI FastAPI 后端 (`127.0.0.1:8000`) 搭建一个**最小可联调**的浏览器控制台。
|
||||
后端已暴露:
|
||||
|
||||
- `POST /api/modeling/train` → `{ task_id, status, kind }`
|
||||
- `POST /api/modeling/predict` → `{ task_id, status, kind }`
|
||||
- `GET /api/tasks/{task_id}` → `TaskRecord`(含 PENDING/PROCESSING/SUCCESS/FAILED + 模型指标 / 输出路径)
|
||||
- `GET /api/algorithms` → 算法清单
|
||||
|
||||
前端已有 (`frontend/src/`):
|
||||
|
||||
- `api/request.ts`:axios 单例 + 响应拦截器自动 unwrap,baseURL 走 `VITE_API_BASE_URL` 缺省 `http://127.0.0.1:8000`
|
||||
- `api/tasks.ts`:所有提交 / 查询函数 + 完整 `TaskRecord` / `TaskStatus` / `TaskKind` 类型
|
||||
- `composables/useTaskPoller.ts`:完整轮询 composable,支持 3 种用法(静态 / 响应式 taskId / 手动)
|
||||
|
||||
## 1. 一次性补齐的脚手架文件
|
||||
|
||||
`frontend/` 初始状态**只有 `src/api` 和 `src/composables`**,缺整个 Vite 骨架。直接照下面这 7 个文件铺一遍:
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── .env.development # VITE_API_BASE_URL=http://127.0.0.1:8000
|
||||
├── .gitignore # node_modules / dist / .vite
|
||||
├── env.d.ts # vite/client + ImportMeta + *.vue shim
|
||||
├── index.html # 挂载 #app
|
||||
├── package.json
|
||||
├── tsconfig.json # 严格模式 + @ → src + bundler resolution
|
||||
├── tsconfig.node.json # 给 vite.config.ts 用
|
||||
├── vite.config.ts # @ alias + 0.0.0.0:5173
|
||||
└── src/
|
||||
├── main.ts
|
||||
└── App.vue
|
||||
```
|
||||
|
||||
### 锁定版本(2026-06 联调通过)
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"vue": "^3.4.27",
|
||||
"element-plus": "^2.7.5",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vue-tsc": "^2.0.19"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`@types/node` 必加**——`vite.config.ts` 用了 `import { fileURLToPath, URL } from 'node:url'`,否则 `npm run build` 类型检查必挂。
|
||||
|
||||
### `tsconfig.json` 关键字段
|
||||
|
||||
- `"moduleResolution": "bundler"`
|
||||
- `"allowImportingTsExtensions": true`(配合 `vue-tsc --noEmit`)
|
||||
- `"paths": { "@/*": ["src/*"] }` + `"baseUrl": "."`
|
||||
- `"include": ["src/**/*.vue"]`(`vue-tsc` 才会处理 SFC)
|
||||
- `"references": [{ "path": "./tsconfig.node.json" }]`
|
||||
|
||||
### `vite.config.ts` 关键字段
|
||||
|
||||
```ts
|
||||
resolve: {
|
||||
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
|
||||
},
|
||||
server: { host: '0.0.0.0', port: 5173 },
|
||||
```
|
||||
|
||||
`0.0.0.0` 方便局域网真机调试;端口冲突时 `strictPort: false` 允许 Vite 自动 +1。
|
||||
|
||||
---
|
||||
|
||||
## 2. main.ts 模板(全量注册 Element Plus)
|
||||
|
||||
```ts
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ElementPlus)
|
||||
|
||||
// 全量注册图标 (<el-icon><Cpu /></el-icon>)
|
||||
for (const [name, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(name, component)
|
||||
}
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
联调期**全量注册最省事**;后期打包体积大再换 `unplugin-vue-components` 按需。
|
||||
|
||||
---
|
||||
|
||||
## 3. useTaskPoller 接线模式(双实例)
|
||||
|
||||
训练 / 推断是**两条独立流水线**,各起一个 `useTaskPoller` 实例。核心套路:把 `task_id` 包成 `ref<string | null>(null)`,composable 内部 `watch` 会**自动 start()**,无需手动调:
|
||||
|
||||
```ts
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { submitTrain, submitPredict, type TaskRecord } from './api/tasks'
|
||||
import { useTaskPoller } from './composables/useTaskPoller'
|
||||
|
||||
// —— 训练 ——
|
||||
const trainTaskId = ref<string | null>(null)
|
||||
const trainPoller = useTaskPoller(trainTaskId) // 传 ref 进去, 自动 watch
|
||||
|
||||
async function onStartTrain() {
|
||||
const { task_id } = await submitTrain({ ... })
|
||||
trainTaskId.value = task_id // 赋值后 watch 触发 start()
|
||||
}
|
||||
|
||||
// —— 推断 ——
|
||||
const predictTaskId = ref<string | null>(null)
|
||||
const predictPoller = useTaskPoller(predictTaskId)
|
||||
const modelId = ref('')
|
||||
|
||||
// 训练一成功, model_id 自动填入推断输入框
|
||||
watch(
|
||||
() => trainPoller.result.value?.model_id,
|
||||
(newId) => { if (newId) modelId.value = newId },
|
||||
)
|
||||
|
||||
async function onStartPredict() {
|
||||
const { task_id } = await submitPredict({ model_id: modelId.value, ... })
|
||||
predictTaskId.value = task_id
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
|
||||
- `trainPoller.result.value` 才是 SUCCESS 后的完整 `TaskRecord`;`record.value` 是任意时刻(含中间态)的最新记录。模板里同时展示用 `trainPoller.record.value ?? trainPoller.result.value`。
|
||||
- `poller.isPolling.value` / `poller.status.value` / `poller.error.value` / `poller.taskId.value` 都是 `Ref`,模板里必须用 `.value`(它们是嵌套 ref,**Vue 模板不会自动 unwrap**)。
|
||||
|
||||
---
|
||||
|
||||
## 4. el-progress 状态映射
|
||||
|
||||
`PollerStatus = 'idle' | 'PENDING' | 'PROCESSING' | 'SUCCESS' | 'FAILED'`
|
||||
`el-progress` 的 `status` 接受 `'' | 'success' | 'warning' | 'exception'`。
|
||||
|
||||
```ts
|
||||
function progressOf(status: string): number {
|
||||
switch (status) {
|
||||
case 'idle':
|
||||
case 'PENDING': return 10
|
||||
case 'PROCESSING':return 60
|
||||
case 'SUCCESS':
|
||||
case 'FAILED': return 100
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
function progressStatusOf(s: string): '' | 'success' | 'exception' {
|
||||
if (s === 'SUCCESS') return 'success'
|
||||
if (s === 'FAILED') return 'exception'
|
||||
return ''
|
||||
}
|
||||
```
|
||||
|
||||
模板里 `v-if="poller.isPolling.value || poller.status.value === 'SUCCESS' || poller.status.value === 'FAILED'"` 控制展示。
|
||||
|
||||
---
|
||||
|
||||
## 5. CSS:深色控制台风(slate 渐变 + 卡片玻璃态)
|
||||
|
||||
```css
|
||||
.app-root {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.panel {
|
||||
background: rgba(30, 41, 59, 0.7) !important;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18) !important;
|
||||
}
|
||||
.app-main {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr; /* 左训练 / 右推断 */
|
||||
gap: 20px;
|
||||
}
|
||||
@media (max-width: 960px) { .app-main { grid-template-columns: 1fr; } }
|
||||
```
|
||||
|
||||
深色背景下 Element Plus 的 `el-form-item__label` / `el-descriptions__label` 默认是黑色文字,必须 `:deep()` 覆盖成浅色。
|
||||
|
||||
---
|
||||
|
||||
## 6. 启动与验证
|
||||
|
||||
```bat
|
||||
cd /d D:\111\office\ZHLduijie\1.WQ\WQ_GUI\frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
打开 `http://127.0.0.1:5173/`,联调期望路径:
|
||||
|
||||
1. 左侧「开始训练」→ 立即拿到 `task_id` + 黄色 `轮询中` + 进度条 60%
|
||||
2. 后端 SUCCESS → 进度条变绿,下面出现 `model_id` 标签 + R²/RMSE/MAE
|
||||
3. 右侧 `model_id` 被自动填入 → 「开始推断」→ 走 `output_zarr_path` 展示
|
||||
4. 任何一步 FAILED → 进度条变红 + 后端 `error` 字段
|
||||
|
||||
---
|
||||
|
||||
## 7. 已知 caveat
|
||||
|
||||
- **第一次 `npm install` 约 150MB**,要耐心等。
|
||||
- `useTaskPoller` 已有 `onUnmounted` 自动清理,**不要再手写 `clearInterval`**。
|
||||
- `request.ts` 注释里写明 FastAPI dev 期 `allow_origins=["*"]`,**不需要配 Vite proxy**;如果未来后端收紧 CORS,再在 `vite.config.ts` 加 `server.proxy['/api']`。
|
||||
- `feature_start` 后端接受 `number | string`;el-input v-model 出来是 string,**直接传给 API 即可**,后端会自己判别。
|
||||
- `v-model` 绑 `ref<number | string>(4)` 类型注解是必须的,否则 TS 会推断成 `Ref<number>`,输入框失焦报错。
|
||||
- `@element-plus/icons-vue` 全量注册后用 `<el-icon><Cpu /></el-icon>` 调,本期 App.vue 没用到但留着扩展位。
|
||||
Reference in New Issue
Block a user