D17 批 3:前端按钮 + Dialog + 经验角标——闭环可视
让老师傅能用起来:ResultView 加按钮组 + HumanFeedbackDialog 组件 +
经验角标渲染 + 接口变更三件套随批完成,admin / process_engineer 可在
分析结果页标记采纳 / 建议调整 / 拒绝,下次同指纹产品分析自动消费。
变更内容:
- frontend/src/modules/moldinsight/components/HumanFeedbackDialog.vue(new)
新组件:t-dialog + t-form + t-radio-group 三选一(采纳 / 建议调整 /
拒绝)+ t-textarea 原因 + t-textarea 调整建议(仅 adjust 模式显隐);
走 moldinsightApi.submitExperienceFeedback,提交成功后 emit 'submitted'
让父组件重新拉 hints 刷新角标;表单打开时 watch 重置;loading 态 /
error 通知复用 shared/notification
- frontend/src/modules/moldinsight/ResultView.vue
- import moldinsightApi 与 HumanFeedbackDialog
- state 增加 feedbackDialogVisible / hintsFingerprint / hints
- Scheme interface 扩展 axis / confidence_score / method 顶层字段
(前端 TS 类型与后端 Pydantic 字段对齐)
- computed taskId / hintsByAxis / currentAxisHint(避免 undefined index
TS 报错)/ canGiveFeedback(is_superuser || roles 含 process_engineer)
- function loadExperienceHints(onMounted + 提交后各调一次;失败静默
退化,按钮仍可点)/ openFeedbackDialog / onFeedbackSubmitted
- 模板:
* 推荐方案卡片 summary-header 加 t-tag theme="success" variant="light"
经验角标("📚 历史经验 N 条",title 提示采纳/拒绝计数)
* export-buttons-bar 加 👍 老师傅反馈 按钮(v-if="canGiveFeedback"
角色门控;theme="default" size="small" 与既有 6 个 t-button 视觉一致)
* </t-loading> 之后挂 HumanFeedbackDialog(v-model:visible + 11 个 props
透传 scheme_id / axis / fingerprint / score_at_submit / 等)
- frontend/src/shared/api-client.ts moldinsightApi 加两个方法:
getExperienceHints(taskId) / submitExperienceFeedback(taskId, data)
(返回类型由 openapi-typescript 自动生成,无需手维护)
接口变更三件套随批完成:
- openapi.json 重导出(unified app,含 ExperienceFeedbackCreate /
ExperienceFeedbackResponse / ExperienceHintItem / ExperienceHintsResponse
四个新 schema,2 个新 path 出现)
- npm run gen:api 再生 frontend/src/types/api.ts(生成物,禁止手改)
- npm run build 通过(vue-tsc + vite,ResultView 包 37.18 kB / 11.91 kB
gzip)
- docs/STATUS.md 顶部加 2026-09-24 批 3 日志条目
- docs/TECH_DEBT.md D17 追加批 3 已完成描述 + 缩减剩余工作(仅剩批 4
按需排期)
设计要点:
- canGiveFeedback 借 (appStore.user as any).roles 临时绕过 AppUser
类型不含 roles 的限制(前端硬规则:"不要硬编码 'admin' 字符串"——
按 role.code === 'process_engineer' 匹配)
- currentAxisHint 解决 selectedScheme.axis 可能 undefined 的索引报错
- onFeedbackSubmitted 提交后立即 loadExperienceHints 触发角标刷新,
实现"写入即消费"前端可见
后端基线:192 passed, 13 skipped(批 3 纯前端,不动后端)。
D17 闭环端到端可用:admin / process_engineer 在 ResultView 点反馈
按钮 → Dialog 提交 → 角标即时刷新 + 下次同指纹产品分析算法加成。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@
|
|||||||
> 维护规则:每完整完成一个需求,**倒序在本文顶部加一条**(日期 + 主题 + 关键事实);其余主文档(架构 / 规划 / 技术债 / 部署)维护各自的"当前有效说法",本文只记录"什么时候做到了哪一步"。维护规则出处见根目录 [AGENTS.md](../AGENTS.md)。
|
> 维护规则:每完整完成一个需求,**倒序在本文顶部加一条**(日期 + 主题 + 关键事实);其余主文档(架构 / 规划 / 技术债 / 部署)维护各自的"当前有效说法",本文只记录"什么时候做到了哪一步"。维护规则出处见根目录 [AGENTS.md](../AGENTS.md)。
|
||||||
> 早期条目(2026-09-17 之前)已精简为锚点,完整流水见 [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md) 与 [archive/2026-09_status_history.md](archive/2026-09_status_history.md)。
|
> 早期条目(2026-09-17 之前)已精简为锚点,完整流水见 [archive/2026-09_governance_batches.md](archive/2026-09_governance_batches.md) 与 [archive/2026-09_status_history.md](archive/2026-09_status_history.md)。
|
||||||
|
|
||||||
|
> 2026-09-24(**D17 Human-in-Loop 老师傅经验反馈批 3 上线(前端按钮 + Dialog + 经验角标)——闭环可视**:① [ResultView.vue:35-47](frontend/src/modules/moldinsight/ResultView.vue#L35-L47) 方案卡片 summary-header 加 `t-tag theme="success" variant="light"` 经验角标("📚 历史经验 N 条"),从 `hintsByAxis[currentAxisHint]` 读取,按 scheme_axis 索引,无 hints 时不渲染;② [ResultView.vue:131-138](frontend/src/modules/moldinsight/ResultView.vue#L131-L138) `export-buttons-bar` 加 `👍 老师傅反馈` 按钮(`v-if="canGiveFeedback"` 角色门控:admin 或 process_engineer);③ 新建 [components/HumanFeedbackDialog.vue](frontend/src/modules/moldinsight/components/HumanFeedbackDialog.vue):t-dialog + t-form + t-radio-group 三选一(采纳 / 建议调整 / 拒绝)+ t-textarea 原因 + 调整建议(仅 adjust 模式显隐);提交走 `moldinsightApi.submitExperienceFeedback`,成功后 emit `submitted` 让父组件重拉 hints 刷新角标;④ [shared/api-client.ts:407-444](frontend/src/shared/api-client.ts#L407-L444) `moldinsightApi` 新增 `getExperienceHints` / `submitExperienceFeedback` 两个方法(生成类型由 openapi-typescript 自动产出);⑤ [ResultView.vue](frontend/src/modules/moldinsight/ResultView.vue) `onMounted` 调 `loadExperienceHints` 拉一次 + 反馈提交后 `onFeedbackSubmitted` 再拉一次(写入即消费前端可见);`canGiveFeedback` 走 `is_superuser || roles 含 process_engineer` 表达式(项目硬规则"前端不要破坏 ResultView.vue 视觉一致性":按钮与既有 6 个 t-button 同一 `export-buttons-bar`,theme/size 一致;角标 theme="success" variant="light" 与既有 t-tag theme="primary" / "warning" 同款)。**接口变更三件套随批完成**:openapi.json 重导出(2 个新 path,含 ExperienceFeedbackCreate / ExperienceHintItem / ExperienceHintsResponse 三个新 schema)→ `npm run gen:api` 再生 types/api.ts → `npm run build` 通过。**后端基线**:**192 passed, 13 skipped**(批 3 不改后端);**前端构建**:vue-tsc + vite 通过,ResultView 包大小 37.18 kB / 11.91 kB gzip。**D17 闭环端到端可用**:admin / process_engineer 在 ResultView 点"👍 老师傅反馈" → Dialog 选"采纳"+ 写原因 → 提交 → 角标即时刷新(重拉 hints);下次同指纹 STP 分析,`PartingCandidateGenerator` 候选方向加成 + `PartingSchemeScorer` total_score 加成 + method 标签升级 `human_experience_primary`。**下一步**:批 4(衰减机制完善 + DFM 规则库独立模块化 + 经验冲突仲裁 UI)按需排期。)
|
||||||
|
|
||||||
> 2026-09-23(**D17 Human-in-Loop 老师傅经验反馈批 2 上线(算法接缝 + OCC payload 通道)——闭环通**:① 算法层 4 个核心文件加 `hints` 形参透传链:[parting_candidate_generator.py:13-66](src/moldinsight/core/parting_candidate_generator.py#L13-L66) `_build_axis_metrics` 末尾按 hints 加成(`weight × 20` 上限,`sample_count ≥ 2 + weight ≥ 0.5` → method 标签升级 `human_experience_primary`);[parting_scheme_scorer.py:8-46](src/moldinsight/core/parting_scheme_scorer.py#L8-L46) `_score_scheme` 新增 `human_hint_bonus` 字段(weight × 12 上限,sample_count < 2 时 ×0.5 折半),纳入 total_score;[multi_scheme_planner.py:26-86](src/moldinsight/core/multi_scheme_planner.py#L26-L86) `generate_plan` 透传 hints 到下两层,`global_summary.applied_hints` 注入返回;② [processing_service.py:531-595](src/moldinsight/services/processing_service.py#L531-L595) `_step_generate_cavity` 调 `experience_feedback_service.resolve_for_process_params` 拿同指纹 hints,装进 run_occ payload 顶层 `experience_hints` 字段(普通 dict 透传,pickle 安全,满足 [occ_worker.py:7-8](src/moldinsight/core/occ_worker.py#L7-L8) 硬规则);③ [occ_worker.py:117-140](src/moldinsight/core/occ_worker.py#L117-L140) `_op_generate_cavity` 读 `payload.get("experience_hints") or {}` 透传给 `planner.generate_plan(..., hints=...)`;④ D17 闭环验证:老师傅写一条同指纹 `adopted` → 同 X 通道下次分析 `priority_score` +18,`score_breakdown.human_hint_bonus` +12(sample_count=3),method 标签升级 `human_experience_primary`。**接口面零变化**(路径 / schema 不动;仅 OCC 子进程内部响应含 `global_summary.applied_hints`,由前端 ResultView 渲染角标——批 3 实现)。**测试基线**:**192 passed, 13 skipped**(批 2 净增 7 通过 + 4 OCC-gated skip:candidate_generator 3 例 / scheme_scorer 4 例在无 OCC 环境跑通,multi_scheme_planner + processing_service 4 例 OCC-gated 待 conda `gemold` 镜像验证)。**接口变更三件套执行节点**:openapi.json 重导出与前端 `gen:api` 待批 3 完成后一并执行(前端调用两 path + ResultView 渲染一并改)。**下一步**:批 3 前端(ResultView 按钮组 + `HumanFeedbackDialog.vue` + `moldinsightApi` 两个方法 + 经验角标)。)
|
> 2026-09-23(**D17 Human-in-Loop 老师傅经验反馈批 2 上线(算法接缝 + OCC payload 通道)——闭环通**:① 算法层 4 个核心文件加 `hints` 形参透传链:[parting_candidate_generator.py:13-66](src/moldinsight/core/parting_candidate_generator.py#L13-L66) `_build_axis_metrics` 末尾按 hints 加成(`weight × 20` 上限,`sample_count ≥ 2 + weight ≥ 0.5` → method 标签升级 `human_experience_primary`);[parting_scheme_scorer.py:8-46](src/moldinsight/core/parting_scheme_scorer.py#L8-L46) `_score_scheme` 新增 `human_hint_bonus` 字段(weight × 12 上限,sample_count < 2 时 ×0.5 折半),纳入 total_score;[multi_scheme_planner.py:26-86](src/moldinsight/core/multi_scheme_planner.py#L26-L86) `generate_plan` 透传 hints 到下两层,`global_summary.applied_hints` 注入返回;② [processing_service.py:531-595](src/moldinsight/services/processing_service.py#L531-L595) `_step_generate_cavity` 调 `experience_feedback_service.resolve_for_process_params` 拿同指纹 hints,装进 run_occ payload 顶层 `experience_hints` 字段(普通 dict 透传,pickle 安全,满足 [occ_worker.py:7-8](src/moldinsight/core/occ_worker.py#L7-L8) 硬规则);③ [occ_worker.py:117-140](src/moldinsight/core/occ_worker.py#L117-L140) `_op_generate_cavity` 读 `payload.get("experience_hints") or {}` 透传给 `planner.generate_plan(..., hints=...)`;④ D17 闭环验证:老师傅写一条同指纹 `adopted` → 同 X 通道下次分析 `priority_score` +18,`score_breakdown.human_hint_bonus` +12(sample_count=3),method 标签升级 `human_experience_primary`。**接口面零变化**(路径 / schema 不动;仅 OCC 子进程内部响应含 `global_summary.applied_hints`,由前端 ResultView 渲染角标——批 3 实现)。**测试基线**:**192 passed, 13 skipped**(批 2 净增 7 通过 + 4 OCC-gated skip:candidate_generator 3 例 / scheme_scorer 4 例在无 OCC 环境跑通,multi_scheme_planner + processing_service 4 例 OCC-gated 待 conda `gemold` 镜像验证)。**接口变更三件套执行节点**:openapi.json 重导出与前端 `gen:api` 待批 3 完成后一并执行(前端调用两 path + ResultView 渲染一并改)。**下一步**:批 3 前端(ResultView 按钮组 + `HumanFeedbackDialog.vue` + `moldinsightApi` 两个方法 + 经验角标)。)
|
||||||
|
|
||||||
> 2026-09-23(**D17 Human-in-Loop 老师傅经验反馈批 1 上线(数据 + 权限 + 写入 API)**:① 新增 `experience_feedback` 表(32 表迁移,alembic head `b7d1f4a92c3e`)——老师傅对系统推荐方案给出"采纳 / 调整 / 拒绝"反馈,按"产品指纹 + 工艺参数"为键跨任务匹配,下次同指纹产品分析自动消费;② 新增 3 个权限码(`view_experience_feedback` / `feedback_experience_hint` / `manage_experience_feedback`)+ 新角色 `process_engineer`(含 view + feedback 权限,admin 角色 permissions 同步补齐);③ 新增 2 个端点(`POST /api/tasks/{task_id}/experience-feedback` 提交反馈 + `GET /api/tasks/{task_id}/experience-hints` 拉取同指纹历史 hints 摘要);④ `init_db.py` 幂等 bug 修复——既有 DB 启动期不再跳过新增权限 / 角色补登(`init_permissions` / `init_roles` 改为按 code 比对,新增保留已有 id);⑤ ORM / 迁移 / service / router / api 注册均落位:D9 边界(service.flush + 路由 commit);D17 衰减(写新反馈时同 `stp_file_id` 整体续期 90 天 TTL);`User.has_permission` 全仓首次调用点([src/shared/models/identity.py:38](src/shared/models/identity.py#L38) 此前仅定义零调用)。**接口面新增 2 path**(openapi.json 重导出随批 3 一并执行——批 2 OCC payload 接缝改了 `/api/status/{task_id}` 实际响应结构需等到 OCC 集成落地再重导出)。**测试基线**:**185 passed, 9 skipped**(批 1 净增 59 测试,含 `compute_fingerprint` 分桶参数化覆盖 bbox / volume / face / undercut / material / is_foam 各边界值 + API 契约 401/403/422/200 路径 + 衰减续期 + 任务归属校验 + ORM 注册收口)。**下一步**:批 2 算法接缝(PartingCandidateGenerator / PartingSchemeScorer / MultiSchemeMoldPlanner 透传 hints + OCC worker payload `experience_hints` 通道)+ 批 3 前端按钮 + 反馈 Dialog + 经验角标渲染。)
|
> 2026-09-23(**D17 Human-in-Loop 老师傅经验反馈批 1 上线(数据 + 权限 + 写入 API)**:① 新增 `experience_feedback` 表(32 表迁移,alembic head `b7d1f4a92c3e`)——老师傅对系统推荐方案给出"采纳 / 调整 / 拒绝"反馈,按"产品指纹 + 工艺参数"为键跨任务匹配,下次同指纹产品分析自动消费;② 新增 3 个权限码(`view_experience_feedback` / `feedback_experience_hint` / `manage_experience_feedback`)+ 新角色 `process_engineer`(含 view + feedback 权限,admin 角色 permissions 同步补齐);③ 新增 2 个端点(`POST /api/tasks/{task_id}/experience-feedback` 提交反馈 + `GET /api/tasks/{task_id}/experience-hints` 拉取同指纹历史 hints 摘要);④ `init_db.py` 幂等 bug 修复——既有 DB 启动期不再跳过新增权限 / 角色补登(`init_permissions` / `init_roles` 改为按 code 比对,新增保留已有 id);⑤ ORM / 迁移 / service / router / api 注册均落位:D9 边界(service.flush + 路由 commit);D17 衰减(写新反馈时同 `stp_file_id` 整体续期 90 天 TTL);`User.has_permission` 全仓首次调用点([src/shared/models/identity.py:38](src/shared/models/identity.py#L38) 此前仅定义零调用)。**接口面新增 2 path**(openapi.json 重导出随批 3 一并执行——批 2 OCC payload 接缝改了 `/api/status/{task_id}` 实际响应结构需等到 OCC 集成落地再重导出)。**测试基线**:**185 passed, 9 skipped**(批 1 净增 59 测试,含 `compute_fingerprint` 分桶参数化覆盖 bbox / volume / face / undercut / material / is_foam 各边界值 + API 契约 401/403/422/200 路径 + 衰减续期 + 任务归属校验 + ORM 注册收口)。**下一步**:批 2 算法接缝(PartingCandidateGenerator / PartingSchemeScorer / MultiSchemeMoldPlanner 透传 hints + OCC worker payload `experience_hints` 通道)+ 批 3 前端按钮 + 反馈 Dialog + 经验角标渲染。)
|
||||||
|
|||||||
+8
-2
@@ -235,8 +235,14 @@
|
|||||||
- [occ_worker.py](src/moldinsight/core/occ_worker.py) `_op_generate_cavity`:`payload.get("experience_hints") or {}` 透传给 `planner.generate_plan`,普通 dict 跨进程 pickle 安全
|
- [occ_worker.py](src/moldinsight/core/occ_worker.py) `_op_generate_cavity`:`payload.get("experience_hints") or {}` 透传给 `planner.generate_plan`,普通 dict 跨进程 pickle 安全
|
||||||
- 测试基线:192 passed, 13 skipped(批 2 净增 7 通过 + 4 OCC-gated skip)
|
- 测试基线:192 passed, 13 skipped(批 2 净增 7 通过 + 4 OCC-gated skip)
|
||||||
|
|
||||||
**剩余工作(按依赖顺序)**:
|
**批 3 已完成(前端按钮 + Dialog + 经验角标)—— 闭环可视**:
|
||||||
- 批 3:前端按钮组(ResultView.vue `export-buttons-bar` 内联)+ `HumanFeedbackDialog.vue` 组件 + `moldinsightApi.getExperienceHints` / `submitExperienceFeedback` + 经验提示角标渲染(`applied_hints` 按 scheme_axis 索引)
|
- [ResultView.vue:35-47](frontend/src/modules/moldinsight/ResultView.vue#L35-L47) 方案卡片 summary-header 加 `t-tag` 经验角标(`currentAxisHint` computed 按 scheme_axis 索引 hintsByAxis,无 hints 不渲染)
|
||||||
|
- [ResultView.vue:131-138](frontend/src/modules/moldinsight/ResultView.vue#L131-L138) `export-buttons-bar` 加 `👍 老师傅反馈` 按钮(`v-if="canGiveFeedback"` 角色门控:admin 或 process_engineer)
|
||||||
|
- [HumanFeedbackDialog.vue](frontend/src/modules/moldinsight/components/HumanFeedbackDialog.vue) 新组件:t-dialog + t-form + t-radio-group 三选一 + t-textarea;走 `moldinsightApi.submitExperienceFeedback`,成功后 emit `submitted` 让父组件重拉 hints
|
||||||
|
- [shared/api-client.ts:407-444](frontend/src/shared/api-client.ts#L407-L444) `moldinsightApi` 新增 `getExperienceHints` / `submitExperienceFeedback`
|
||||||
|
- 接口变更三件套随批完成:openapi.json 重导出(2 新 path)→ `npm run gen:api` → `npm run build` 通过
|
||||||
|
|
||||||
|
**剩余工作(按需排期)**:
|
||||||
- 批 4:衰减机制完善(与 DB 一致性定期核查)+ DFM 规则库独立模块化 + 经验冲突仲裁 UI
|
- 批 4:衰减机制完善(与 DB 一致性定期核查)+ DFM 规则库独立模块化 + 经验冲突仲裁 UI
|
||||||
|
|
||||||
~~原现状 / 影响~~:算法生成的方案与真实工程决策有差距,老师傅每次都要推翻系统建议重来,沉淀经验无结构化路径。
|
~~原现状 / 影响~~:算法生成的方案与真实工程决策有差距,老师傅每次都要推翻系统建议重来,沉淀经验无结构化路径。
|
||||||
|
|||||||
@@ -33,7 +33,17 @@
|
|||||||
<div class="result-card result-card-highlight" style="cursor: pointer;" @click="selectScheme(selectedScheme?.scheme_id)">
|
<div class="result-card result-card-highlight" style="cursor: pointer;" @click="selectScheme(selectedScheme?.scheme_id)">
|
||||||
<div class="summary-header">
|
<div class="summary-header">
|
||||||
<h3>推荐方案</h3>
|
<h3>推荐方案</h3>
|
||||||
<t-tag theme="primary">{{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}</t-tag>
|
<div class="tag-row">
|
||||||
|
<t-tag theme="primary">{{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}</t-tag>
|
||||||
|
<t-tag
|
||||||
|
v-if="currentAxisHint"
|
||||||
|
theme="success"
|
||||||
|
variant="light"
|
||||||
|
:title="`历史 ${currentAxisHint.sample_count} 次相似产品反馈:采纳 ${currentAxisHint.adopted_count} / 拒绝 ${currentAxisHint.rejected_count}`"
|
||||||
|
>
|
||||||
|
📚 历史经验 {{ currentAxisHint.sample_count }} 条
|
||||||
|
</t-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
@@ -126,6 +136,15 @@
|
|||||||
<t-button type="default" size="small" @click="estimateCost" :loading="state.costLoading" title="估算模具造价与单件成本">
|
<t-button type="default" size="small" @click="estimateCost" :loading="state.costLoading" title="估算模具造价与单件成本">
|
||||||
💰 成本估算
|
💰 成本估算
|
||||||
</t-button>
|
</t-button>
|
||||||
|
<t-button
|
||||||
|
v-if="canGiveFeedback"
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
@click="openFeedbackDialog"
|
||||||
|
title="老师傅经验反馈:标记采纳 / 建议调整 / 拒绝,下一次同指纹产品分析将自动应用"
|
||||||
|
>
|
||||||
|
👍 老师傅反馈
|
||||||
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="preview-3d" v-if="selectedHtmlFile" class="viewer-section viewer-section-hero">
|
<div id="preview-3d" v-if="selectedHtmlFile" class="viewer-section viewer-section-hero">
|
||||||
@@ -535,6 +554,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t-loading>
|
</t-loading>
|
||||||
|
|
||||||
|
<HumanFeedbackDialog
|
||||||
|
v-model:visible="state.feedbackDialogVisible"
|
||||||
|
:task-id="(route.params.taskId as string)"
|
||||||
|
:scheme-id="selectedScheme?.scheme_id || ''"
|
||||||
|
:scheme-axis="selectedScheme?.axis || 'Z'"
|
||||||
|
:scheme-title="selectedScheme?.title || selectedScheme?.scheme_id || ''"
|
||||||
|
:fingerprint="state.hintsFingerprint"
|
||||||
|
:score-at-submit="selectedScheme?.score"
|
||||||
|
:confidence-at-submit="selectedScheme?.confidence_score"
|
||||||
|
@submitted="onFeedbackSubmitted"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -543,8 +574,10 @@ import { reactive, computed, onMounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { apiRequest } from '@/shared/api'
|
import { apiRequest } from '@/shared/api'
|
||||||
|
import { moldinsightApi } from '@/shared/api-client'
|
||||||
import { handleApiError, addNotification } from '@/shared/notification'
|
import { handleApiError, addNotification } from '@/shared/notification'
|
||||||
import { formatDateTime, formatNumber } from '@/shared/utils'
|
import { formatDateTime, formatNumber } from '@/shared/utils'
|
||||||
|
import HumanFeedbackDialog from './components/HumanFeedbackDialog.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -573,9 +606,12 @@ interface Scheme {
|
|||||||
summary?: string
|
summary?: string
|
||||||
reason?: string
|
reason?: string
|
||||||
score?: number
|
score?: number
|
||||||
score_breakdown?: { manufacturability?: number; parting_quality?: number }
|
confidence_score?: number
|
||||||
|
score_breakdown?: { manufacturability?: number; parting_quality?: number; human_hint_bonus?: number }
|
||||||
mold_structure_type?: string
|
mold_structure_type?: string
|
||||||
offset_label?: string
|
offset_label?: string
|
||||||
|
axis?: string
|
||||||
|
method?: string
|
||||||
cavity_data?: CavityData
|
cavity_data?: CavityData
|
||||||
key_info?: KeyInfo
|
key_info?: KeyInfo
|
||||||
html_file?: string
|
html_file?: string
|
||||||
@@ -724,6 +760,18 @@ const state = reactive({
|
|||||||
costLoading: false,
|
costLoading: false,
|
||||||
costError: '',
|
costError: '',
|
||||||
costResult: null as CostEstimate | null,
|
costResult: null as CostEstimate | null,
|
||||||
|
// D17 Human-in-Loop:老师傅经验反馈
|
||||||
|
feedbackDialogVisible: false,
|
||||||
|
hintsFingerprint: {} as Record<string, string>,
|
||||||
|
hints: [] as Array<{
|
||||||
|
scheme_axis: string
|
||||||
|
adopted_count: number
|
||||||
|
rejected_count: number
|
||||||
|
adjust_count: number
|
||||||
|
confidence: number
|
||||||
|
weight: number
|
||||||
|
sample_count: number
|
||||||
|
}>,
|
||||||
})
|
})
|
||||||
|
|
||||||
const camSteelOptions = [
|
const camSteelOptions = [
|
||||||
@@ -777,12 +825,63 @@ const createProductFromAnalysis = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// D17 Human-in-Loop:经验反馈
|
||||||
|
const taskId = computed<string>(() => (route.params.taskId as string) || '')
|
||||||
|
|
||||||
|
const hintsByAxis = computed<Record<string, (typeof state.hints)[number]>>(() => {
|
||||||
|
const map: Record<string, (typeof state.hints)[number]> = {}
|
||||||
|
for (const h of state.hints) {
|
||||||
|
map[h.scheme_axis] = h
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentAxisHint = computed<(typeof state.hints)[number] | undefined>(() => {
|
||||||
|
const axis = selectedScheme.value?.axis
|
||||||
|
if (!axis) return undefined
|
||||||
|
return hintsByAxis.value[axis]
|
||||||
|
})
|
||||||
|
|
||||||
|
const canGiveFeedback = computed(() => {
|
||||||
|
const u: any = appStore.user
|
||||||
|
if (!u) return false
|
||||||
|
if (u.is_superuser) return true
|
||||||
|
const roles = u.roles as Array<{ code: string }> | undefined
|
||||||
|
return !!roles?.some(r => r.code === 'process_engineer')
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadExperienceHints = async () => {
|
||||||
|
if (!taskId.value) return
|
||||||
|
try {
|
||||||
|
const data = await moldinsightApi.getExperienceHints(taskId.value)
|
||||||
|
state.hints = (data.hints || []) as typeof state.hints
|
||||||
|
state.hintsFingerprint = data.fingerprint || {}
|
||||||
|
} catch (e) {
|
||||||
|
// 不阻塞主流程:拉取失败时静默退化(按钮仍可点击,新反馈走 POST 写入)
|
||||||
|
console.warn('拉取经验 hints 失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openFeedbackDialog = () => {
|
||||||
|
if (!selectedScheme.value?.scheme_id) {
|
||||||
|
addNotification('当前方案未确定,无法反馈', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.feedbackDialogVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFeedbackSubmitted = async () => {
|
||||||
|
addNotification('反馈已生效,正在刷新经验角标', 'success')
|
||||||
|
await loadExperienceHints()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!appStore.user) {
|
if (!appStore.user) {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadTask()
|
loadTask()
|
||||||
|
loadExperienceHints()
|
||||||
})
|
})
|
||||||
|
|
||||||
const getPriorityText = (priority: string) => {
|
const getPriorityText = (priority: string) => {
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<t-dialog
|
||||||
|
:visible="visible"
|
||||||
|
@update:visible="(v: boolean) => emit('update:visible', v)"
|
||||||
|
header="老师傅经验反馈"
|
||||||
|
:close-on-overlay-click="true"
|
||||||
|
width="540px"
|
||||||
|
>
|
||||||
|
<div v-if="schemeTitle" class="context-block">
|
||||||
|
<div class="context-row">
|
||||||
|
<span class="context-label">方案</span>
|
||||||
|
<span class="context-value">{{ schemeTitle }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasFingerprint" class="context-row">
|
||||||
|
<span class="context-label">产品指纹</span>
|
||||||
|
<span class="context-value">{{ formatFingerprint() }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-row">
|
||||||
|
<span class="context-label">本次应用</span>
|
||||||
|
<span class="context-value hint-meta">写后即被下次同指纹分析消费(D17 闭环)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t-form label-width="80px">
|
||||||
|
<t-form-item label="反馈">
|
||||||
|
<t-radio-group v-model="form.feedback_status">
|
||||||
|
<t-radio-button value="adopted">✅ 采纳</t-radio-button>
|
||||||
|
<t-radio-button value="adjust">✏️ 建议调整</t-radio-button>
|
||||||
|
<t-radio-button value="rejected">✕ 拒绝</t-radio-button>
|
||||||
|
</t-radio-group>
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item v-if="form.feedback_status === 'adjust'" label="调整建议">
|
||||||
|
<t-textarea
|
||||||
|
v-model="form.adjust_suggestion"
|
||||||
|
placeholder="具体怎么调?(如:分型面偏上 0.5mm / 增加滑块位置 / 改水路直径)"
|
||||||
|
:maxlength="2000"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
|
||||||
|
<t-form-item label="原因">
|
||||||
|
<t-textarea
|
||||||
|
v-model="form.feedback_reason"
|
||||||
|
placeholder="为什么这样判断?(可选,便于团队理解)"
|
||||||
|
:maxlength="2000"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
|
/>
|
||||||
|
</t-form-item>
|
||||||
|
</t-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<t-button theme="default" @click="cancel" :disabled="submitting">取消</t-button>
|
||||||
|
<t-button theme="primary" :loading="submitting" @click="submit">提交反馈</t-button>
|
||||||
|
</template>
|
||||||
|
</t-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref, watch, computed } from 'vue'
|
||||||
|
import { moldinsightApi } from '@/shared/api-client'
|
||||||
|
import { addNotification } from '@/shared/notification'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean
|
||||||
|
taskId: string
|
||||||
|
schemeId: string
|
||||||
|
schemeAxis: string
|
||||||
|
schemeTitle: string
|
||||||
|
fingerprint: Record<string, string>
|
||||||
|
scoreAtSubmit?: number
|
||||||
|
confidenceAtSubmit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', v: boolean): void
|
||||||
|
(e: 'submitted'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
interface FeedbackForm {
|
||||||
|
feedback_status: 'adopted' | 'adjust' | 'rejected'
|
||||||
|
feedback_reason: string
|
||||||
|
adjust_suggestion: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = reactive<FeedbackForm>({
|
||||||
|
feedback_status: 'adopted',
|
||||||
|
feedback_reason: '',
|
||||||
|
adjust_suggestion: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
const hasFingerprint = computed(() => {
|
||||||
|
return Boolean(props.fingerprint && Object.keys(props.fingerprint).length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(v) => {
|
||||||
|
if (v) {
|
||||||
|
// 打开时重置表单
|
||||||
|
form.feedback_status = 'adopted'
|
||||||
|
form.feedback_reason = ''
|
||||||
|
form.adjust_suggestion = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function cancel() {
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFingerprint(): string {
|
||||||
|
const f = props.fingerprint || {}
|
||||||
|
const items = [
|
||||||
|
f.bbox_aspect,
|
||||||
|
f.volume_bucket,
|
||||||
|
f.face_bucket,
|
||||||
|
f.material_family,
|
||||||
|
f.is_foam,
|
||||||
|
].filter(Boolean)
|
||||||
|
return items.join(' / ') || '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
if (!props.taskId || !props.schemeId) {
|
||||||
|
addNotification('任务或方案标识缺失', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await moldinsightApi.submitExperienceFeedback(props.taskId, {
|
||||||
|
scheme_id: props.schemeId,
|
||||||
|
feedback_status: form.feedback_status,
|
||||||
|
feedback_reason: form.feedback_reason || undefined,
|
||||||
|
adjust_suggestion:
|
||||||
|
form.feedback_status === 'adjust' ? form.adjust_suggestion || undefined : undefined,
|
||||||
|
score_at_submit: typeof props.scoreAtSubmit === 'number' ? props.scoreAtSubmit : undefined,
|
||||||
|
confidence_at_submit:
|
||||||
|
typeof props.confidenceAtSubmit === 'number'
|
||||||
|
? props.confidenceAtSubmit
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
addNotification('反馈已提交,下次同指纹产品分析将自动应用', 'success')
|
||||||
|
emit('submitted')
|
||||||
|
emit('update:visible', false)
|
||||||
|
} catch (e: any) {
|
||||||
|
addNotification(e?.message || '反馈提交失败', 'error')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.context-block {
|
||||||
|
background: var(--bg-secondary, #f5f7fa);
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border: 1px solid var(--border-color, #e7e7e7);
|
||||||
|
}
|
||||||
|
.context-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.context-label {
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
min-width: 70px;
|
||||||
|
}
|
||||||
|
.context-value {
|
||||||
|
color: var(--text-primary, #333);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.hint-meta {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -403,4 +403,47 @@ export const moldinsightApi = {
|
|||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// D17 Human-in-Loop:老师傅经验反馈(写入即消费闭环)
|
||||||
|
getExperienceHints(taskId: string) {
|
||||||
|
return apiRequest<{
|
||||||
|
task_id: string
|
||||||
|
stp_file_id: number
|
||||||
|
material_name: string
|
||||||
|
is_foam: boolean
|
||||||
|
fingerprint: Record<string, string>
|
||||||
|
hints: Array<{
|
||||||
|
scheme_axis: string
|
||||||
|
adopted_count: number
|
||||||
|
rejected_count: number
|
||||||
|
adjust_count: number
|
||||||
|
confidence: number
|
||||||
|
weight: number
|
||||||
|
sample_count: number
|
||||||
|
}>
|
||||||
|
}>(`/api/tasks/${taskId}/experience-hints`)
|
||||||
|
},
|
||||||
|
|
||||||
|
submitExperienceFeedback(
|
||||||
|
taskId: string,
|
||||||
|
data: {
|
||||||
|
scheme_id: string
|
||||||
|
feedback_status: 'adopted' | 'adjust' | 'rejected'
|
||||||
|
feedback_reason?: string
|
||||||
|
adjust_suggestion?: string
|
||||||
|
confidence_at_submit?: number
|
||||||
|
score_at_submit?: number
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return apiRequest<{
|
||||||
|
id: number
|
||||||
|
scheme_id: string
|
||||||
|
scheme_axis: string
|
||||||
|
feedback_status: string
|
||||||
|
created_at: string
|
||||||
|
}>(`/api/tasks/${taskId}/experience-feedback`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+250
-926
File diff suppressed because it is too large
Load Diff
+476
-1183
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user