790 lines
22 KiB
Markdown
790 lines
22 KiB
Markdown
# 工艺设备关联设备管理 Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 将工艺库、工艺路线步骤、生产任务步骤中的「设备名称」自由文本改为关联 `biz_equipment` 主数据(`equipment_id` + `equipment_name` 快照),三处 UI 改为设备下拉选择,步骤级可覆盖。
|
||
|
||
**Architecture:** 三张业务表新增 `equipment_id` 字段,保留 `equipment_name` 快照;后端统一 `resolve_equipment_snapshot()` 校验租户并写入名称;复制链路(工艺→路线→任务)同步复制 ID+名称;设备删除前检查工艺/路线引用。前端三页复用 `fetchAllEquipments()` 下拉。
|
||
|
||
**Tech Stack:** FastAPI + SQLAlchemy + Pydantic v2 (backend) | Vue 3 + Element Plus (web-admin) | MySQL 5.7
|
||
|
||
**设计规格文档:** `docs/superpowers/specs/2026-06-15-process-equipment-link-design.md`
|
||
|
||
---
|
||
|
||
## 文件清单
|
||
|
||
| 操作 | 文件 |
|
||
|------|------|
|
||
| 新建 | `docs/sql/2026-06-15-process-equipment-id.sql` |
|
||
| 新建 | `backend/scripts/migrate_process_equipment_id.py` |
|
||
| 修改 | `docs/sql/zkxw.sql` |
|
||
| 修改 | `backend/app/models/models.py` |
|
||
| 修改 | `backend/app/crud/equipment.py` |
|
||
| 修改 | `backend/app/crud/process.py` |
|
||
| 修改 | `backend/app/crud/process_route.py` |
|
||
| 修改 | `backend/app/crud/production_task.py` |
|
||
| 修改 | `backend/app/schemas/process_route.py` |
|
||
| 修改 | `backend/app/schemas/production_task.py` |
|
||
| 修改 | `backend/app/api/production.py` |
|
||
| 修改 | `web-admin/src/views/production/process/index.vue` |
|
||
| 修改 | `web-admin/src/views/production/process/ProcessRouteTab.vue` |
|
||
| 修改 | `web-admin/src/views/production/production-work-order/StepDetailPanel.vue` |
|
||
| 修改 | `web-admin/src/views/production/production-work-order/useTaskStepEditor.ts` |
|
||
| 可选新建 | `web-admin/src/utils/equipment-select.ts`(下拉 label/历史数据提示 helper,三页复用) |
|
||
|
||
---
|
||
|
||
### Task 1: SQL 迁移 — 三表新增 equipment_id
|
||
|
||
**Files:**
|
||
- Create: `docs/sql/2026-06-15-process-equipment-id.sql`
|
||
- Modify: `docs/sql/zkxw.sql`(三表 CREATE TABLE 段落同步加字段)
|
||
|
||
- [ ] **Step 1: 创建增量 SQL**
|
||
|
||
```sql
|
||
-- 工艺设备关联:三表新增 equipment_id
|
||
ALTER TABLE biz_process
|
||
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
|
||
|
||
ALTER TABLE biz_process_route_step
|
||
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
|
||
|
||
ALTER TABLE biz_production_task_step
|
||
ADD COLUMN equipment_id varchar(64) DEFAULT NULL COMMENT '关联设备ID(biz_equipment.id)' AFTER description;
|
||
|
||
ALTER TABLE biz_process
|
||
ADD KEY idx_process_equipment (equipment_id, del_flag);
|
||
```
|
||
|
||
- [ ] **Step 2: 在开发/测试库执行**
|
||
|
||
Run(按实际连接串调整):
|
||
```bash
|
||
mysql -u root -p zkxw < docs/sql/2026-06-15-process-equipment-id.sql
|
||
```
|
||
Expected: 3 条 `ALTER TABLE` 成功,无报错。
|
||
|
||
- [ ] **Step 3: 同步 zkxw.sql 基线**
|
||
|
||
在 `docs/sql/zkxw.sql` 的 `biz_process`、`biz_process_route_step`、`biz_production_task_step` 建表语句中,`description` 后插入同样的 `equipment_id` 列定义;`biz_process` 增加 `idx_process_equipment` 索引。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add docs/sql/2026-06-15-process-equipment-id.sql docs/sql/zkxw.sql
|
||
git commit -m "feat: 三表新增 equipment_id 字段以关联设备管理"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: Model — 三实体加 equipment_id
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/models/models.py`
|
||
|
||
- [ ] **Step 1: BizProcess 增加字段**
|
||
|
||
在 `BizProcess.description` 之后添加:
|
||
|
||
```python
|
||
equipment_id = Column(String(64), nullable=True, comment="关联设备ID")
|
||
```
|
||
|
||
- [ ] **Step 2: BizProcessRouteStep 增加字段**
|
||
|
||
在 `BizProcessRouteStep.description` 之后添加同样一行。
|
||
|
||
- [ ] **Step 3: BizProductionTaskStep 增加字段**
|
||
|
||
在 `BizProductionTaskStep.description` 之后添加同样一行。
|
||
|
||
- [ ] **Step 4: 重启后端确认 ORM 无报错**
|
||
|
||
Run:
|
||
```bash
|
||
cd backend && source venv/bin/activate && python -c "from app.models.models import BizProcess, BizProcessRouteStep, BizProductionTaskStep; print('ok')"
|
||
```
|
||
Expected: `ok`
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add backend/app/models/models.py
|
||
git commit -m "feat: Model 增加 equipment_id 字段"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: 设备快照 Helper + 删除引用检查
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/crud/equipment.py`
|
||
|
||
- [ ] **Step 1: 添加 resolve_equipment_snapshot**
|
||
|
||
在文件顶部 import 区追加 `Tuple`(若尚未导入),并在 `get_equipment` 之后添加:
|
||
|
||
```python
|
||
def resolve_equipment_snapshot(
|
||
db: Session,
|
||
equipment_id: Optional[str],
|
||
tenant_no: str,
|
||
) -> Tuple[Optional[str], Optional[str]]:
|
||
"""校验设备并返回 (equipment_id, equipment_name) 快照。空 ID 则返回 (None, None)。"""
|
||
eid = (equipment_id or "").strip()
|
||
if not eid:
|
||
return None, None
|
||
obj = (
|
||
db.query(BizEquipment)
|
||
.filter(
|
||
BizEquipment.id == eid,
|
||
BizEquipment.tenant_no == tenant_no,
|
||
BizEquipment.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
if not obj:
|
||
raise ValueError("所选设备不存在或已删除")
|
||
return obj.id, (obj.equipment_name or "").strip() or obj.equipment_code
|
||
|
||
|
||
def equipment_is_referenced(db: Session, equipment_id: str) -> bool:
|
||
from app.models.models import BizProcess, BizProcessRouteStep
|
||
|
||
eid = (equipment_id or "").strip()
|
||
if not eid:
|
||
return False
|
||
proc_hit = (
|
||
db.query(BizProcess.id)
|
||
.filter(BizProcess.equipment_id == eid, BizProcess.del_flag == 0)
|
||
.first()
|
||
)
|
||
if proc_hit:
|
||
return True
|
||
step_hit = (
|
||
db.query(BizProcessRouteStep.id)
|
||
.filter(BizProcessRouteStep.equipment_id == eid, BizProcessRouteStep.del_flag == 0)
|
||
.first()
|
||
)
|
||
return step_hit is not None
|
||
```
|
||
|
||
- [ ] **Step 2: 修改 soft_delete_equipment**
|
||
|
||
```python
|
||
def soft_delete_equipment(db: Session, equipment_id: str) -> None:
|
||
obj = get_equipment(db, equipment_id)
|
||
if not obj:
|
||
raise ValueError("设备不存在")
|
||
if equipment_is_referenced(db, equipment_id):
|
||
raise ValueError("该设备已被工艺或工艺路线引用,无法删除")
|
||
obj.del_flag = 1
|
||
obj.update_time = datetime.now()
|
||
db.add(obj)
|
||
db.commit()
|
||
```
|
||
|
||
- [ ] **Step 3: 手动验证**
|
||
|
||
Run:
|
||
```bash
|
||
cd backend && source venv/bin/activate && python -c "
|
||
from app.crud.equipment import resolve_equipment_snapshot, equipment_is_referenced
|
||
print('import ok')
|
||
"
|
||
```
|
||
Expected: `import ok`
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add backend/app/crud/equipment.py
|
||
git commit -m "feat: 设备快照解析与删除引用检查"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: 工艺 CRUD — equipment_id 读写
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/crud/process.py`
|
||
|
||
- [ ] **Step 1: _row_to_dict 增加 equipmentId**
|
||
|
||
```python
|
||
def _row_to_dict(p: BizProcess) -> Dict[str, Any]:
|
||
return {
|
||
"id": p.id,
|
||
"processCode": p.process_code,
|
||
"categoryId": p.category_id or "",
|
||
"processName": p.process_name,
|
||
"description": p.description or "",
|
||
"equipmentId": p.equipment_id or "",
|
||
"equipmentName": p.equipment_name or "",
|
||
# ... 其余不变
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: create_process 改签名与写入**
|
||
|
||
函数参数 `equipment_name` 改为 `equipment_id: Optional[str] = None`,并在 `obj = BizProcess(...)` 前调用:
|
||
|
||
```python
|
||
from app.crud.equipment import resolve_equipment_snapshot
|
||
|
||
eid, ename = resolve_equipment_snapshot(db, equipment_id, tenant_no)
|
||
# BizProcess(...) 内:
|
||
equipment_id=eid,
|
||
equipment_name=ename,
|
||
```
|
||
|
||
- [ ] **Step 3: update_process 同样处理**
|
||
|
||
当 `equipment_id is not None`(含显式传空字符串清空)时:
|
||
|
||
```python
|
||
if equipment_id is not None:
|
||
eid, ename = resolve_equipment_snapshot(db, equipment_id, obj.tenant_no)
|
||
obj.equipment_id = eid
|
||
obj.equipment_name = ename
|
||
```
|
||
|
||
移除对 `equipment_name` 参数的直接 setattr 逻辑。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add backend/app/crud/process.py
|
||
git commit -m "feat: 工艺 CRUD 支持 equipment_id 并写入名称快照"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: 工艺 API Schema + 路由入参
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/api/production.py`
|
||
|
||
- [ ] **Step 1: ProcessCreateBody / ProcessUpdateBody**
|
||
|
||
将 `equipment_name` 字段替换为:
|
||
|
||
```python
|
||
equipment_id: Optional[str] = Field(None, alias="equipmentId")
|
||
```
|
||
|
||
Create/Update 路由调用 `create_process` / `update_process` 时传 `equipment_id=body.equipment_id`(Update 从 payload 取 `equipment_id` key)。
|
||
|
||
- [ ] **Step 2: 确认 create/update 路由**
|
||
|
||
`create_process` 调用处示例:
|
||
|
||
```python
|
||
process_svc.create_process(
|
||
db,
|
||
process_code=body.process_code,
|
||
process_name=body.process_name,
|
||
category_id=body.category_id,
|
||
description=body.description,
|
||
equipment_id=body.equipment_id,
|
||
# ...
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 3: 启动后端 smoke test**
|
||
|
||
Run:
|
||
```bash
|
||
cd backend && source venv/bin/activate && python -c "from app.api.production import ProcessCreateBody; print(ProcessCreateBody.model_fields.keys())"
|
||
```
|
||
Expected: 输出含 `equipment_id`,不含可写 `equipment_name`。
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add backend/app/api/production.py
|
||
git commit -m "feat: 工艺 API 改为接收 equipmentId"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 6: 工艺路线步骤 — 复制与更新
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/crud/process_route.py`
|
||
- Modify: `backend/app/schemas/process_route.py`
|
||
|
||
- [ ] **Step 1: _step_row_to_dict 增加 equipmentId**
|
||
|
||
```python
|
||
"equipmentId": s.equipment_id or "",
|
||
"equipmentName": s.equipment_name or "",
|
||
```
|
||
|
||
- [ ] **Step 2: add_step 复制 equipment_id**
|
||
|
||
`BizProcessRouteStep(...)` 构造处增加:
|
||
|
||
```python
|
||
equipment_id=proc.equipment_id,
|
||
equipment_name=proc.equipment_name,
|
||
```
|
||
|
||
- [ ] **Step 3: copy_step 复制 equipment_id**
|
||
|
||
```python
|
||
equipment_id=src.equipment_id,
|
||
equipment_name=src.equipment_name,
|
||
```
|
||
|
||
- [ ] **Step 4: update_step 支持 equipment_id**
|
||
|
||
在 `update_step` 中,若 `kwargs.get("equipment_id")` 不为 None(用 sentinel 区分未传 vs 清空,或直接检查 key in kwargs):
|
||
|
||
```python
|
||
from app.crud.equipment import resolve_equipment_snapshot
|
||
|
||
if "equipment_id" in kwargs:
|
||
eid, ename = resolve_equipment_snapshot(db, kwargs.get("equipment_id"), step.tenant_no)
|
||
step.equipment_id = eid
|
||
step.equipment_name = ename
|
||
```
|
||
|
||
从 `for field in [... "equipment_name" ...]` 循环中**移除** `equipment_name`(不再接受纯文本写入)。
|
||
|
||
- [ ] **Step 5: StepUpdateBody schema**
|
||
|
||
`backend/app/schemas/process_route.py`:
|
||
|
||
```python
|
||
equipment_id: Optional[str] = Field(None, alias="equipmentId")
|
||
# 删除 equipment_name 字段
|
||
```
|
||
|
||
- [ ] **Step 6: production.py 中 step update 路由传 equipment_id**
|
||
|
||
找到 `update_route_step` / `StepUpdateBody` 使用处,确保 payload 映射 `equipment_id`。
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add backend/app/crud/process_route.py backend/app/schemas/process_route.py backend/app/api/production.py
|
||
git commit -m "feat: 工艺路线步骤支持 equipmentId 复制与更新"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 7: 生产任务步骤 — 复制与更新
|
||
|
||
**Files:**
|
||
- Modify: `backend/app/crud/production_task.py`
|
||
- Modify: `backend/app/schemas/production_task.py`
|
||
|
||
- [ ] **Step 1: _step_to_dict 增加 equipmentId**
|
||
|
||
```python
|
||
"equipmentId": s.equipment_id or "",
|
||
```
|
||
|
||
- [ ] **Step 2: _clone_route_to_task 复制 equipment_id**
|
||
|
||
```python
|
||
equipment_id=rs.equipment_id,
|
||
equipment_name=rs.equipment_name,
|
||
```
|
||
|
||
- [ ] **Step 3: add_task_step(process_id=...) 复制 equipment_id**
|
||
|
||
`BizProductionTaskStep(...)` 从工艺复制处增加 `equipment_id=proc.equipment_id`。
|
||
|
||
- [ ] **Step 4: add_task_step 无 process_id 分支**
|
||
|
||
改为接受 `equipment_id`,经 `resolve_equipment_snapshot` 写入;移除 `equipment_name` 直写。
|
||
|
||
- [ ] **Step 5: update_task_step**
|
||
|
||
```python
|
||
if "equipment_id" in data:
|
||
eid, ename = resolve_equipment_snapshot(db, data.get("equipment_id"), tenant_no)
|
||
step.equipment_id = eid
|
||
step.equipment_name = ename
|
||
```
|
||
|
||
从通用 field 循环中移除 `equipment_name`。
|
||
|
||
- [ ] **Step 6: copy_task_step 复制 equipment_id**
|
||
|
||
```python
|
||
equipment_id=src.equipment_id,
|
||
equipment_name=src.equipment_name,
|
||
```
|
||
|
||
- [ ] **Step 7: TaskStepAdd / TaskStepUpdate schema**
|
||
|
||
```python
|
||
# TaskStepAdd
|
||
equipment_id: Optional[str] = Field(None, alias="equipmentId")
|
||
|
||
# TaskStepUpdate
|
||
equipment_id: Optional[str] = Field(None, alias="equipmentId")
|
||
```
|
||
|
||
删除两个 schema 中的 `equipment_name` 可写字段。
|
||
|
||
- [ ] **Step 8: Commit**
|
||
|
||
```bash
|
||
git add backend/app/crud/production_task.py backend/app/schemas/production_task.py backend/app/api/production.py
|
||
git commit -m "feat: 生产任务步骤支持 equipmentId 复制与更新"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 8: 历史数据迁移脚本
|
||
|
||
**Files:**
|
||
- Create: `backend/scripts/migrate_process_equipment_id.py`
|
||
|
||
- [ ] **Step 1: 编写迁移脚本**
|
||
|
||
```python
|
||
#!/usr/bin/env python3
|
||
"""按 equipment_name 精确匹配回填 equipment_id。用法: python scripts/migrate_process_equipment_id.py [--dry-run]"""
|
||
import argparse
|
||
from sqlalchemy import and_
|
||
|
||
from app.core.database import SessionLocal
|
||
from app.models.models import BizProcess, BizProcessRouteStep, BizProductionTaskStep, BizEquipment
|
||
|
||
|
||
TABLES = [
|
||
("biz_process", BizProcess),
|
||
("biz_process_route_step", BizProcessRouteStep),
|
||
("biz_production_task_step", BizProductionTaskStep),
|
||
]
|
||
|
||
|
||
def migrate_table(db, model, dry_run: bool) -> int:
|
||
updated = 0
|
||
rows = db.query(model).filter(model.del_flag == 0, model.equipment_id.is_(None)).all()
|
||
for row in rows:
|
||
name = (row.equipment_name or "").strip()
|
||
if not name:
|
||
continue
|
||
eq = (
|
||
db.query(BizEquipment)
|
||
.filter(
|
||
BizEquipment.tenant_no == row.tenant_no,
|
||
BizEquipment.del_flag == 0,
|
||
BizEquipment.equipment_name == name,
|
||
)
|
||
.first()
|
||
)
|
||
if not eq:
|
||
continue
|
||
if not dry_run:
|
||
row.equipment_id = eq.id
|
||
updated += 1
|
||
return updated
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--dry-run", action="store_true")
|
||
args = parser.parse_args()
|
||
db = SessionLocal()
|
||
try:
|
||
for label, model in TABLES:
|
||
n = migrate_table(db, model, args.dry_run)
|
||
print(f"{label}: matched {n}")
|
||
if not args.dry_run:
|
||
db.commit()
|
||
print("committed")
|
||
else:
|
||
print("dry-run, no commit")
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
```
|
||
|
||
- [ ] **Step 2: dry-run 执行**
|
||
|
||
Run:
|
||
```bash
|
||
cd backend && source venv/bin/activate && python scripts/migrate_process_equipment_id.py --dry-run
|
||
```
|
||
Expected: 打印各表匹配数量,无异常。
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add backend/scripts/migrate_process_equipment_id.py
|
||
git commit -m "feat: 历史工艺设备名称回填 equipment_id 脚本"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 9: 前端 — 工艺列表/表单设备下拉
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/production/process/index.vue`
|
||
- 可选: `web-admin/src/utils/equipment-select.ts`
|
||
|
||
- [ ] **Step 1: 引入 fetchAllEquipments**
|
||
|
||
```typescript
|
||
import { fetchAllEquipments, /* 现有 imports */ } from '@/api/production'
|
||
```
|
||
|
||
- [ ] **Step 2: 增加设备选项与 label helper**
|
||
|
||
```typescript
|
||
const equipmentOptions = ref<any[]>([])
|
||
|
||
function equipmentOptionLabel(row: { equipmentCode?: string; equipmentName?: string }) {
|
||
const code = row.equipmentCode || ''
|
||
const name = row.equipmentName || ''
|
||
return code && name ? `${code} / ${name}` : name || code || '-'
|
||
}
|
||
|
||
async function loadEquipmentOptions() {
|
||
equipmentOptions.value = await fetchAllEquipments()
|
||
}
|
||
```
|
||
|
||
在 `loadReferenceData()` 或 `handleAdd`/`handleEdit` 的 `Promise.all` 中加入 `loadEquipmentOptions()`。
|
||
|
||
- [ ] **Step 3: formData 改 equipmentId**
|
||
|
||
```typescript
|
||
const formData = reactive({
|
||
// ...
|
||
equipmentId: '',
|
||
// 移除 equipmentName 作为表单字段(展示用 detail 快照即可)
|
||
})
|
||
```
|
||
|
||
`resetForm`、`handleEdit`、`handleSubmit` 中:
|
||
- 读取 `detail.equipmentId`
|
||
- 提交 `{ equipmentId: formData.equipmentId || undefined }`,**不再传 equipmentName**
|
||
|
||
- [ ] **Step 4: 模板替换 el-input → el-select**
|
||
|
||
```vue
|
||
<el-form-item label="设备名称">
|
||
<el-select
|
||
v-model="formData.equipmentId"
|
||
placeholder="请选择设备"
|
||
clearable
|
||
filterable
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="eq in equipmentOptions"
|
||
:key="eq.id"
|
||
:label="equipmentOptionLabel(eq)"
|
||
:value="eq.id"
|
||
:disabled="eq.status === '0'"
|
||
/>
|
||
</el-select>
|
||
<p v-if="!formData.equipmentId && formDataLegacyEquipmentName" class="form-tip">
|
||
历史文本:{{ formDataLegacyEquipmentName }},请重新选择设备
|
||
</p>
|
||
</el-form-item>
|
||
```
|
||
|
||
列表列仍展示 `equipmentName`(快照),无需改 prop。
|
||
|
||
- [ ] **Step 5: 本地 dev 冒烟**
|
||
|
||
Run: `cd web-admin && pnpm dev`,打开 `/#/production/process`(或实际菜单路径),新增/编辑工艺选设备并保存。
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/views/production/process/index.vue
|
||
git commit -m "feat: 工艺表单设备改为下拉关联设备管理"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 10: 前端 — 工艺路线步骤设备下拉
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/production/process/ProcessRouteTab.vue`
|
||
|
||
- [ ] **Step 1: 加载 equipmentOptions(同 Task 9)**
|
||
|
||
在 `loadReferenceData` 或组件 mount 时调用 `fetchAllEquipments()`。
|
||
|
||
- [ ] **Step 2: 模板替换设备输入框**
|
||
|
||
将 `selectedStep.equipmentName` 的 `el-input` 改为 `el-select`,`v-model="selectedStep.equipmentId"`。
|
||
|
||
- [ ] **Step 3: saveStepBasic 改 payload**
|
||
|
||
```typescript
|
||
async function saveStepBasic() {
|
||
// ...
|
||
await fetchUpdateRouteStep({
|
||
stepId: selectedStep.value.id,
|
||
// ...
|
||
equipmentId: selectedStep.value.equipmentId || null
|
||
})
|
||
// 本地同步展示名(可选,便于 UI 即时反馈)
|
||
const eq = equipmentOptions.value.find((e) => e.id === selectedStep.value.equipmentId)
|
||
if (eq) selectedStep.value.equipmentName = eq.equipmentName
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: 历史数据提示**
|
||
|
||
当 `!selectedStep.equipmentId && selectedStep.equipmentName` 时显示「历史文本:xxx」。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/views/production/process/ProcessRouteTab.vue
|
||
git commit -m "feat: 工艺路线步骤设备改为下拉选择"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 11: 前端 — 生产任务步骤设备下拉
|
||
|
||
**Files:**
|
||
- Modify: `web-admin/src/views/production/production-work-order/StepDetailPanel.vue`
|
||
- Modify: `web-admin/src/views/production/production-work-order/useTaskStepEditor.ts`
|
||
|
||
- [ ] **Step 1: useTaskStepEditor 加载 equipmentOptions**
|
||
|
||
在 editor 初始化或 `loadTaskDetail` 后加载 `fetchAllEquipments()`,export `equipmentOptions` 与 `equipmentOptionLabel`。
|
||
|
||
- [ ] **Step 2: saveStepBasic 改 payload**
|
||
|
||
```typescript
|
||
await fetchUpdateTaskStep(taskId.value, selectedStep.value.id, {
|
||
process_name: selectedStep.value.processName,
|
||
category_id: selectedStep.value.categoryId,
|
||
description: selectedStep.value.description,
|
||
equipment_id: selectedStep.value.equipmentId || null,
|
||
estimated_duration: selectedStep.value.estimatedDuration,
|
||
sop_ids: selectedStep.value.sopIds
|
||
}, operateScope.value)
|
||
```
|
||
|
||
- [ ] **Step 3: StepDetailPanel.vue 模板**
|
||
|
||
```vue
|
||
<el-form-item label="设备名称">
|
||
<el-select
|
||
v-model="editor.selectedStep.value.equipmentId"
|
||
placeholder="请选择设备"
|
||
clearable
|
||
filterable
|
||
:disabled="readonly"
|
||
style="width: 100%"
|
||
@change="editor.saveStepBasic()"
|
||
>
|
||
<el-option
|
||
v-for="eq in editor.equipmentOptions.value"
|
||
:key="eq.id"
|
||
:label="editor.equipmentOptionLabel(eq)"
|
||
:value="eq.id"
|
||
:disabled="eq.status === '0'"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
```
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add web-admin/src/views/production/production-work-order/StepDetailPanel.vue \
|
||
web-admin/src/views/production/production-work-order/useTaskStepEditor.ts
|
||
git commit -m "feat: 生产任务步骤设备改为下拉选择"
|
||
```
|
||
|
||
---
|
||
|
||
### Task 12: 联调验证 + 字段一致性 + 知识图谱
|
||
|
||
**Files:**
|
||
- 无新文件
|
||
|
||
- [ ] **Step 1: 字段一致性检查**
|
||
|
||
Run:
|
||
```bash
|
||
cd backend && source venv/bin/activate && python scripts/check_field_consistency.py
|
||
```
|
||
Expected: process 相关无新增不一致告警(若有,按 `_row_to_dict` 补全 Create/Update schema 与前端 formData)。
|
||
|
||
- [ ] **Step 2: 端到端手工测试清单**
|
||
|
||
| # | 操作 | 预期 |
|
||
|---|------|------|
|
||
| 1 | 设备管理新建设备 A | 成功 |
|
||
| 2 | 工艺库新建工艺,选设备 A | 列表显示 A 名称;DB 有 equipment_id |
|
||
| 3 | 工艺路线拖入该工艺 | 步骤 equipmentId = A |
|
||
| 4 | 路线步骤改选设备 B | 仅步骤变更,工艺仍为 A |
|
||
| 5 | 生产任务选该路线 | 任务步骤 equipmentId = B |
|
||
| 6 | 任务步骤改选设备 C | 保存成功 |
|
||
| 7 | 删除设备 A(工艺仍引用) | 报错「已被引用」 |
|
||
| 8 | 清空工艺设备 | equipment_id/name 均为 NULL |
|
||
|
||
- [ ] **Step 3: 更新知识图谱**
|
||
|
||
Run:
|
||
```bash
|
||
./scripts/update-graph.sh all
|
||
```
|
||
|
||
- [ ] **Step 4: 更新设计文档状态**
|
||
|
||
将 `docs/superpowers/specs/2026-06-15-process-equipment-link-design.md` 头部 `状态: 待确认` 改为 `状态: 已实现`。
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add docs/superpowers/specs/2026-06-15-process-equipment-link-design.md backend/graphify-out web-admin/graphify-out
|
||
git commit -m "docs: 工艺设备关联功能完成并更新设计状态"
|
||
```
|
||
|
||
---
|
||
|
||
## Spec 覆盖自检
|
||
|
||
| Spec 章节 | 对应 Task |
|
||
|-----------|-----------|
|
||
| 2 需求共识(步骤级覆盖) | Task 6–7, 10–11 |
|
||
| 3 数据流复制点 | Task 6–7 |
|
||
| 4 数据库变更 | Task 1–2 |
|
||
| 4.4 历史迁移 | Task 8 |
|
||
| 5.2 CRUD helper | Task 3 |
|
||
| 5.3 API Schema | Task 5–7 |
|
||
| 5.4 复制逻辑 | Task 6–7 |
|
||
| 6 前端三页 | Task 9–11 |
|
||
| 7 设备删除约束 | Task 3 |
|
||
| 8 测试要点 | Task 12 |
|
||
|
||
无遗漏项。
|
||
|
||
---
|
||
|
||
## 执行选项
|
||
|
||
Plan complete and saved to `docs/superpowers/plans/2026-06-15-process-equipment-link.md`.
|
||
|
||
**两种执行方式:**
|
||
|
||
1. **Subagent-Driven(推荐)** — 按 Task 1→12 逐任务派发子 agent,每任务完成后 review
|
||
2. **Inline Execution** — 在本会话中按 Task 批量实现,每 2–3 个 Task 设检查点
|
||
|
||
你希望用哪种方式开始实现?
|