refactor(pipeline): 路径直接传输 — 统一 ctx 字段名/panel key/step 形参名
This commit is contained in:
24
.qwen/settings.json
Normal file
24
.qwen/settings.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(\"c:\\users\\duxin\\appdata\\local\\programs\\python\\python311\\python.exe\" *)",
|
||||
"Bash(get-childitem *)",
|
||||
"Bash(select-object *)",
|
||||
"Bash(python *)",
|
||||
"Bash(where *)",
|
||||
"Bash(conda *)",
|
||||
"Bash(dir *)",
|
||||
"Bash(cmd *)",
|
||||
"Bash(del *)",
|
||||
"Bash(powershell *)",
|
||||
"Bash(git *)",
|
||||
"Bash(type *)",
|
||||
"Bash(.\\venv\\scripts\\python.exe *)",
|
||||
"Bash(\"d:\\111\\office\\zhlduijie\\1.wq\\wq_gui\\venv\\scripts\\python.exe\" *)",
|
||||
"Bash(c:\\users\\duxin\\appdata\\local\\programs\\python\\python311\\python.exe *)",
|
||||
"Bash(venv\\scripts\\python.exe *)",
|
||||
"Bash(findstr *)"
|
||||
]
|
||||
},
|
||||
"$version": 4
|
||||
}
|
||||
23
.qwen/settings.json.orig
Normal file
23
.qwen/settings.json.orig
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(\"c:\\users\\duxin\\appdata\\local\\programs\\python\\python311\\python.exe\" *)",
|
||||
"Bash(get-childitem *)",
|
||||
"Bash(select-object *)",
|
||||
"Bash(python *)",
|
||||
"Bash(where *)",
|
||||
"Bash(conda *)",
|
||||
"Bash(dir *)",
|
||||
"Bash(cmd *)",
|
||||
"Bash(del *)",
|
||||
"Bash(powershell *)",
|
||||
"Bash(git *)",
|
||||
"Bash(type *)",
|
||||
"Bash(.\\venv\\scripts\\python.exe *)",
|
||||
"Bash(\"d:\\111\\office\\zhlduijie\\1.wq\\wq_gui\\venv\\scripts\\python.exe\" *)",
|
||||
"Bash(c:\\users\\duxin\\appdata\\local\\programs\\python\\python311\\python.exe *)",
|
||||
"Bash(venv\\scripts\\python.exe *)"
|
||||
]
|
||||
},
|
||||
"$version": 4
|
||||
}
|
||||
141
.qwen/skills/code_replacement_state_audit/SKILL.md
Normal file
141
.qwen/skills/code_replacement_state_audit/SKILL.md
Normal file
@ -0,0 +1,141 @@
|
||||
---
|
||||
name: 代码替换请求的现状审计
|
||||
description: 处理用户"代码替换/新增"指令时,先审计磁盘真实状态再用 ask_user_question 确认——避免覆盖已落盘的高版本代码
|
||||
source: auto-skill
|
||||
extracted_at: '2026-06-03T05:36:58.746Z'
|
||||
---
|
||||
|
||||
# 代码替换请求的现状审计
|
||||
|
||||
## 适用场景
|
||||
|
||||
用户给出"代码替换"或"按某版本代码新增"指令,但**没有提供与磁盘当前状态对比信息**时。典型触发:
|
||||
|
||||
- 用户贴了一段代码说"请帮我写/替换这个"
|
||||
- 用户引用某个文档/旧版本/旧 chat 说"按这个来"
|
||||
- 之前的 `state_snapshot` / `memory` / `git log` 描述可能与磁盘现状不一致
|
||||
|
||||
## 核心原则
|
||||
|
||||
**永远不要盲信"用户给的代码是最新版本"**——磁盘上的代码可能已经是更完善的版本(用户或其他 agent 已迭代过)。覆盖 = 丢功能。
|
||||
|
||||
直接覆盖的代价不一定是显式 bug,也可能是"丢失用户已批准的设计决策"(如 duck-type 探测 / ctx 抽象 / 信号协议 / 二次确认窗 / 错误定位)。
|
||||
|
||||
## 5 步标准操作
|
||||
|
||||
### 1. 确认文件存在
|
||||
|
||||
`glob` 或 `list_directory` 看目标文件是否已存在:
|
||||
|
||||
- 不存在 → 新建
|
||||
- 存在 → 进入第 2 步审计
|
||||
|
||||
### 2. grep 关键符号 + 读关键段
|
||||
|
||||
- 找"用户贴的代码"里的 3-5 个关键符号(函数名 / 类名 / 关键常量 / import)
|
||||
- 在磁盘文件里 grep 同样的符号
|
||||
- `read_file` 关键段(行号从 grep 结果直接拿)
|
||||
|
||||
### 3. 构造差异对照表
|
||||
|
||||
列出:
|
||||
|
||||
```
|
||||
| 目标文件 | 用户贴的版本 | 磁盘现有版本 | 直接覆盖会丢失 |
|
||||
```
|
||||
|
||||
**关键列**:"直接覆盖会丢失什么"——让用户判断成本。具体粒度到"功能模块 / 设计决策 / 防御层 / 入口协议",不要写"代码差异"这种空话。
|
||||
|
||||
### 4. ask_user_question 让用户拍板
|
||||
|
||||
3 个标准选项(措辞可调,但**必须给出现状 + 三选一**):
|
||||
|
||||
- **A. 保留现状**(推荐,磁盘已是更新版)—— 直接进 Smoke Test
|
||||
- **B. 强制覆盖到旧版** —— 写明丢什么 + 备份建议(git stash / 复制到 `_old.py`)
|
||||
- **C. 混合:只取某段增量** —— 见第 5 步
|
||||
|
||||
**不要在第 1 次 ask 时就列具体的"哪段增量"**——先让用户在 A/B/C 之间选。如果选 C,再做第 5 步。
|
||||
|
||||
### 5. 若用户选 C,识别"真正增量"
|
||||
|
||||
对比 1.0 vs 2.0,识别 1.0 真正独有的部分(2.0 没有的):
|
||||
|
||||
- ❌ 排除 1.0 比 2.0 简单的(2.0 是超集 / 工厂分层 / 多了 CLI)
|
||||
- ❌ 排除 1.0 整体被 2.0 工厂分层超越的(_make_objective vs _build_model + _get_search_space)
|
||||
- ✅ 关注 1.0 独有的功能层(即使 2.0 不"明显"需要)
|
||||
|
||||
对每个候选增量,再问一次"采纳哪段",让用户具体选(multiSelect=false,一次只选 1 段最稳)。
|
||||
|
||||
## 落地原则
|
||||
|
||||
执行"采纳 1.0 某段增量到 2.0"时:
|
||||
|
||||
- **最小化外科手术式编辑**:只动需要动的文件,只改需要改的段
|
||||
- **保留 2.0 的设计决策**(duck-type 探测 / ctx 抽象 / 信号协议 / 二次确认窗 / 错误定位)
|
||||
- **顶部 import 增量用 `replace_all=False` 单点插入**,避免破坏其他 import 顺序
|
||||
- **同名变量全链路替换**(如 `self.config` → `clean_config`)要贯穿 ctx 构造 / v2 调用 / v1 fallback,避免双源差异
|
||||
- **单步模式不一定要清洗**(不走 panel 完整 config,与清洗器无关)
|
||||
- **清洗器这种"防患于未然"的代码要给日志**(`self.log_message.emit(f"[清洗器] 已删除 N 个未知 key")`)让运行时可见
|
||||
|
||||
## 验证三件套
|
||||
|
||||
落地后必跑:
|
||||
|
||||
1. **AST 语法检查**:`ast.parse(open(p, encoding='utf-8-sig').read())` 对 5 个核心文件
|
||||
- 必加 `utf-8-sig`:WQ_GUI 的 water_quality_gui.py line 1 是 BOM,plain `utf-8` 必挂
|
||||
2. **关键符号 grep**:确认新代码的关键符号(import / 关键函数调用)都命中,hit 数符合预期
|
||||
3. **顶层导入测试**:用 mock PyQt5 + `sys.path.insert(0, 'src/gui/core')`,验证模块整体可加载
|
||||
- PyQt5 mock 模板见下方"参考代码"
|
||||
- Windows 环境调 Python:用 conda env 的 `python.exe` 全路径,不要靠 PATH
|
||||
|
||||
## 反例(不要做)
|
||||
|
||||
- ❌ "按用户贴的代码原封不动写入"——1.0 简化版的覆盖陷阱
|
||||
- ❌ "保留 state_snapshot 描述"——state snapshot 可能不准确(写的是意图,磁盘才是事实)
|
||||
- ❌ "用 git log 反推当前状态"——git log 不能反映工作区未提交改动
|
||||
- ❌ "靠 memory 推断当前状态"——memory 可能是 22 天前的(已确认过期)
|
||||
- ❌ "磁盘和用户给的代码看起来一样就不审计"——一行之差可能就是"防弹层"丢失
|
||||
|
||||
## 参考代码
|
||||
|
||||
### PyQt5 mock 模板(worker_thread.py 顶层导入测试)
|
||||
|
||||
```python
|
||||
import os, sys
|
||||
os.environ['GDAL_FILENAME_IS_UTF8'] = 'YES'
|
||||
os.environ['SHAPE_ENCODING'] = 'UTF-8'
|
||||
sys.path.insert(0, 'src/gui/core')
|
||||
|
||||
import types
|
||||
pyqt5 = types.ModuleType("PyQt5")
|
||||
qtc = types.ModuleType("PyQt5.QtCore")
|
||||
class _QThread:
|
||||
def __init__(self, *a, **kw): pass
|
||||
class _Signal:
|
||||
def __init__(self, *a, **kw): pass
|
||||
qtc.QThread = _QThread
|
||||
qtc.pyqtSignal = _Signal
|
||||
qtc.Qt = type("Qt", (), {"QueuedConnection": 1, "UserRole": 0})()
|
||||
sys.modules["PyQt5"] = pyqt5
|
||||
sys.modules["PyQt5.QtCore"] = qtc
|
||||
|
||||
import worker_thread
|
||||
# 副作用: check_pipeline_dependencies() 会打印依赖检查日志(可忽略)
|
||||
```
|
||||
|
||||
### Windows 上跑 conda env python
|
||||
|
||||
```bat
|
||||
cmd /c "D:\xxx\anconda\envs\XXX\python.exe D:\path\to\script.py"
|
||||
```
|
||||
|
||||
PowerShell 单行 `python -c "..."` 在中文路径 / 双引号 / 单引号嵌套时易翻车,**写临时 .py 文件再用 `cmd /c` 调**最稳。
|
||||
|
||||
## 案例来源(2026-06-03 WQ_GUI 路线 B MVP)
|
||||
|
||||
- 用户贴 1.0 简化版:300 行 automl_trainer / 简化 worker_thread.run() / 简化 on_run_all_clicked
|
||||
- 磁盘上 2.0 落盘版:545 行 automl_trainer(_build_model + _get_search_space 工厂 / argparse CLI)/ duck-type 探测 v2 + PipelineContext 抽象 / 完整二次确认窗 / 失败步骤 _focus_step 定位 / [DEPRECATED] stop 保留
|
||||
- 1.0 唯一真增量 = **"防弹级参数清洗器"**(method_map 14 项 + inspect.signature 过滤未知 key + has_kwargs 豁免 + 未知 key 数量日志)
|
||||
- 落地:worker_thread.py:run() 内 set_callback 之后插入 53 行清洗器,self.config 6 处替换为 clean_config
|
||||
- 验证:5 文件 AST 全通过 + 关键符号 7 项命中 + PyQt5 mock 下 import 成功
|
||||
- 净增行数:407 → 457(+50 行)
|
||||
206
.qwen/skills/wq_gui_data_flow/SKILL.md
Normal file
206
.qwen/skills/wq_gui_data_flow/SKILL.md
Normal file
@ -0,0 +1,206 @@
|
||||
---
|
||||
name: WQ_GUI 数据流转架构
|
||||
description: WQ_GUI ProjectSession 事件总线驱动的步骤间数据传递机制(完整重构版)
|
||||
source: auto-skill
|
||||
extracted_at: '2026-05-28T09:07:34.967Z'
|
||||
---
|
||||
|
||||
# WQ_GUI 数据流转架构
|
||||
|
||||
## 核心结论
|
||||
|
||||
整个系统是**基于文件路径驱动**的管道,所有数据存储在本地磁盘。重构后通过 `ProjectSession` 事件总线实现 Panel 间完全解耦。
|
||||
|
||||
---
|
||||
|
||||
## 1. 旧架构(旧代码中已删除)
|
||||
|
||||
主窗口通过 `self.step_outputs` 字典 + `step_dependencies` 配置 + `auto_populate_*` 系列方法管理步骤间路径填充。存在高度耦合问题:
|
||||
|
||||
```python
|
||||
# 已废弃并删除
|
||||
self.step_outputs = {}
|
||||
self._init_step_dependencies()
|
||||
self.update_step_outputs(step_name, work_path)
|
||||
self.auto_populate_dependent_steps(completed_step)
|
||||
self.auto_populate_step_inputs(step_id)
|
||||
self.find_step_output(work_path, step_id, output_type)
|
||||
self.add_auto_fill_buttons_to_panels()
|
||||
self.scan_work_directory_for_files(work_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 新架构:ProjectSession 事件总线
|
||||
|
||||
### Session 核心 API(`src/core/project_session.py`)
|
||||
|
||||
```python
|
||||
class ProjectSession(QObject):
|
||||
path_updated = pyqtSignal(str, str, str) # step, out_type, path
|
||||
step_outputs_ready = pyqtSignal(str, str) # step, out_type
|
||||
|
||||
def update_output(step, out_type, path):
|
||||
"""Panel 完成后广播输出路径"""
|
||||
|
||||
def update_outputs(step, {out_type: path, ...}):
|
||||
"""Panel 完成后批量广播多个输出路径"""
|
||||
|
||||
def get_output(step, out_type):
|
||||
"""Panel 可主动查询上游路径(用于自动填充)"""
|
||||
|
||||
def get_step_outputs(step):
|
||||
"""返回该 step 的全部输出字典"""
|
||||
|
||||
def scan_work_directory():
|
||||
"""主窗口 on_step_completed 末尾调用,扫描并广播所有已知路径"""
|
||||
```
|
||||
|
||||
### Panel 重构模板
|
||||
|
||||
```python
|
||||
class StepXPanel(QWidget):
|
||||
def __init__(self, session=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.session = session
|
||||
self.work_dir = None
|
||||
self.init_ui()
|
||||
self._bind_session_signals()
|
||||
|
||||
def _bind_session_signals(self):
|
||||
if not self.session:
|
||||
return
|
||||
self.session.path_updated.connect(
|
||||
self._on_session_path_updated, Qt.QueuedConnection
|
||||
)
|
||||
|
||||
@pyqtSlot(str, str, str)
|
||||
def _on_session_path_updated(self, step_name, output_type, path):
|
||||
print(f"[StepX Debug] 收到广播: step={step_name}, type={output_type}, path={path}")
|
||||
if step_name == 'step1':
|
||||
if output_type == 'reference_img':
|
||||
if not self.img_file.get_path().strip():
|
||||
self.img_file.set_path(path)
|
||||
print(f"[StepX] 自动填充参考影像: {path}")
|
||||
elif output_type == 'water_mask':
|
||||
if not self.water_mask_file.get_path().strip():
|
||||
self.water_mask_file.set_path(path)
|
||||
print(f"[StepX] 自动填充水域掩膜: {path}")
|
||||
# ...
|
||||
|
||||
def on_step_finished(self, success, message):
|
||||
"""由主窗口 on_step_completed 通过 getattr 动态调用"""
|
||||
if not success:
|
||||
return
|
||||
if self.session:
|
||||
outputs = {}
|
||||
path = self.output_widget.get_path().strip()
|
||||
if path:
|
||||
outputs['output_type'] = path
|
||||
if outputs:
|
||||
self.session.update_outputs('stepX', outputs)
|
||||
```
|
||||
|
||||
### 主窗口两处改动
|
||||
|
||||
```python
|
||||
# 1. __init__ 中注入 session(所有 Panel 统一注入)
|
||||
self.step1_panel = Step1Panel(session=self.session)
|
||||
self.step2_panel = Step2Panel(session=self.session)
|
||||
self.step3_panel = Step3Panel(session=self.session)
|
||||
self.step4_panel = Step4Panel(session=self.session)
|
||||
self.step5_panel = Step5Panel(session=self.session)
|
||||
self.step5_5_panel = Step5_5Panel(session=self.session)
|
||||
self.step6_panel = Step6Panel(session=self.session)
|
||||
self.step6_5_panel = Step6_5Panel(session=self.session)
|
||||
self.step6_75_panel = Step6_75Panel(session=self.session)
|
||||
self.step7_panel = Step7Panel(session=self.session)
|
||||
self.step8_panel = Step8Panel(session=self.session)
|
||||
self.step8_5_panel = Step8_5Panel(session=self.session)
|
||||
self.step8_75_panel = Step8_75Panel(session=self.session)
|
||||
self.step9_panel = Step9Panel(session=self.session)
|
||||
|
||||
# 2. on_step_completed(通用动态获取,无需维护字典)
|
||||
def on_step_completed(self, step_name, success, message):
|
||||
if not success:
|
||||
return
|
||||
if hasattr(self, 'session') and self.session:
|
||||
self.session.scan_work_directory()
|
||||
|
||||
panel = getattr(self, f"{step_name}_panel", None)
|
||||
if panel and hasattr(panel, 'on_step_finished'):
|
||||
panel.on_step_finished(success, message)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 全链路事件流
|
||||
|
||||
### step1 → step2 / step3 路径(通过 Shapefile 栅格化产物)
|
||||
|
||||
| 场景 | 广播的 water_mask 路径 |
|
||||
|------|----------------------|
|
||||
| NDWI 模式 | `output_file` 用户指定路径 |
|
||||
| Shapefile 模式 | `{work_dir}/1_water_mask/water_mask_from_shp.dat`(优先)<br>若文件不存在则 fallback 回 `mask_file.get_path()` |
|
||||
|
||||
```
|
||||
step1 完成
|
||||
→ step1_panel.on_step_finished()
|
||||
→ session.update_outputs('step1', {
|
||||
'reference_img': img_path,
|
||||
'water_mask': mask_path # 可能是 .dat 或 .shp(见上表)
|
||||
})
|
||||
→ step2_panel._on_session_path_updated()
|
||||
→ step3_panel._on_session_path_updated()
|
||||
```
|
||||
|
||||
### step3 → step5 / step7;step5 → 下游训练
|
||||
|
||||
```
|
||||
step3.deglint_image ──┬─→ step5.deglint_image(填充 img_file)
|
||||
└─→ step7.deglint_image(填充 img_file)
|
||||
|
||||
step5.training_spectra ──┬─→ step5_5.index_features
|
||||
├─→ step6.models_dir ──→ step8.predictions
|
||||
├─→ step6_5.models_dir ──→ step8_5.predictions
|
||||
└─→ step6_75.models_dir ──→ step8_75.predictions
|
||||
|
||||
step7.sampling_points ──┬─→ step8
|
||||
├─→ step8_5
|
||||
└─→ step8_75
|
||||
|
||||
step8/8_5/8_75.predictions ──→ step9.distribution_map
|
||||
```
|
||||
|
||||
### 各 Panel 监听/发布对照表(完整版)
|
||||
|
||||
| Panel | 监听 | 发布 |
|
||||
|-------|------|------|
|
||||
| step1 | — | `reference_img`, `water_mask` |
|
||||
| step2 | `step1.reference_img`, `step1.water_mask` | `glint_mask` |
|
||||
| step3 | `step1.reference_img`, `step1.water_mask`, `step2.glint_mask` | `deglint_image` |
|
||||
| step4 | — | `processed_data` |
|
||||
| step5 | `step3.deglint_image`, `step4.processed_data`, `step2.glint_mask` | `training_spectra` |
|
||||
| step5_5 | `step5.training_spectra` | `index_features` |
|
||||
| step6 | `step5.training_spectra` | `models_dir` |
|
||||
| step6_5 | `step5.training_spectra` | `models_dir` |
|
||||
| step6_75 | `step5.training_spectra` | `models_dir` |
|
||||
| step7 | `step3.deglint_image`, `step1.water_mask`, `step2.glint_mask` | `sampling_points` |
|
||||
| step8 | `step7.sampling_points`, `step6.models_dir` | `predictions` |
|
||||
| step8_5 | `step7.sampling_points`, `step6_5.models_dir` | `predictions` |
|
||||
| step8_75 | `step7.sampling_points`, `step6_75.models_dir` | `predictions` |
|
||||
| step9 | `step8.predictions`, `step8_5.predictions`, `step8_75.predictions` | `distribution_map` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键约束
|
||||
|
||||
- `__init__` 参数 `session=None`(向后兼容,主窗口可继续不传)
|
||||
- 所有 Panel 的 `init_ui / get_config / set_config / update_from_config` 完整保留
|
||||
- 删除所有 `self.window().stepX_panel` 跨界访问
|
||||
- 使用 `self.session.get_output()` 替代直接读取其他 panel 的 widget
|
||||
- 监听使用 `Qt.QueuedConnection` 确保跨线程安全
|
||||
- 仅在 field 为空时自动填充(`not widget.get_path().strip()`)
|
||||
- `update_from_config` 中优先从 Session 获取路径,再用 Session 广播
|
||||
- 主窗口 `on_step_completed` 中使用 `getattr(self, f"{step_name}_panel", None)` 实现通用动态获取,无需维护硬编码字典
|
||||
- `step1` Shapefile 模式下,**不能**直接广播 `.shp` 输入文件,必须拼接 `{work_dir}/1_water_mask/water_mask_from_shp.dat` 作为产物路径
|
||||
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