# 生产任务 I/O 与细胞产品库联动 — 实施计划
> **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:** 生产任务的产出和原材料面板与细胞产品库、库位系统联动,实现产出即入库、消耗即释放。
**Architecture:** 后端事务内 diff 计算新增/删除/修改,每个操作联动细胞产品 CRUD 和库位占用。前端产出面板改为子弹窗+列表模式,原材料面板增强展示。
**Tech Stack:** FastAPI + SQLAlchemy (sync Session), Vue 3 + Element Plus + TypeScript
**Design Spec:** `docs/superpowers/specs/2026-05-22-task-io-cell-product-integration-design.md`
---
## File Structure
### 后端
| 文件 | 操作 | 职责 |
|------|------|------|
| `backend/app/schemas/production_task.py` | 修改 | `TaskOutputItem` 扩展完整细胞产品字段 |
| `backend/app/crud/production_task.py` | 修改 | `set_task_outputs` 增加 diff + 细胞产品 CRUD + 库位联动;`set_task_inputs` 增加细胞产品状态联动 |
| `backend/app/crud/cell_product.py` | 修改 | 提取 `_create_cell_product_obj` 和 `_update_cell_product_fields` 供任务模块复用 |
| `backend/app/api/production.py` | 修改 | 路由适配新 schema(如果参数名有变化) |
| `backend/app/crud/production_task.py` | 修改 | `_task_output_to_dict` 增加细胞产品完整信息 |
### 前端
| 文件 | 操作 | 职责 |
|------|------|------|
| `web-admin/src/api/production-task.ts` | 修改 | `fetchSetTaskOutputs` 参数类型适配 |
| `web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue` | **新建** | 产出细胞产品新增/编辑子弹窗 |
| `web-admin/src/views/production/production-work-order/TaskOutputList.vue` | **新建** | 产出列表组件 |
| `web-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vue` | **新建** | 增强版原材料表格 |
| `web-admin/src/views/production/production-work-order/useTaskStepEditor.ts` | 修改 | 产出/原材料操作逻辑重构 |
| `web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue` | 修改 | 替换产出/原材料面板组件 |
---
## Task 1: 后端 Schema — 扩展 TaskOutputItem
**Files:**
- Modify: `backend/app/schemas/production_task.py` (第 92-101 行)
- [ ] **Step 1: 扩展 TaskOutputItem 添加完整细胞产品字段**
在 `backend/app/schemas/production_task.py` 中,将 `TaskOutputItem` 从简单字段扩展为包含完整细胞产品属性:
```python
class TaskOutputItem(BaseModel):
model_config = ConfigDict(populate_by_name=True)
# 关联字段
cell_product_id: Optional[str] = Field(None, alias="cellProductId")
# 细胞产品完整字段
product_code: Optional[str] = Field(None, alias="productCode")
product_name: Optional[str] = Field(None, alias="productName")
product_type_id: Optional[str] = Field(None, alias="productTypeId")
spec_id: Optional[str] = Field(None, alias="specId")
passage: Optional[str] = None
batch_no: Optional[str] = Field(None, alias="batchNo")
storage_slot_id: Optional[str] = Field(None, alias="storageSlotId")
expected_cell_count: Optional[float] = Field(None, alias="expectedCellCount")
viability: Optional[float] = None
freeze_date: Optional[str] = Field(None, alias="freezeDate")
quantity: Optional[float] = None
unit: Optional[str] = None
expiry_date: Optional[str] = Field(None, alias="expiryDate")
product_images: Optional[str] = Field(None, alias="productImages")
product_status: Optional[str] = Field(None, alias="productStatus")
source_type: Optional[str] = Field(None, alias="sourceType")
# 兼容旧字段
output_name: Optional[str] = Field(None, alias="outputName")
output_barcode: Optional[str] = Field(None, alias="outputBarcode")
remark: Optional[str] = None
attachments: Optional[str] = None
```
- [ ] **Step 2: 确认文件无语法错误**
Run: `cd backend && source venv/bin/activate && python -c "from app.schemas.production_task import TaskOutputItem; print('OK')"`
Expected: `OK`
---
## Task 2: 后端 CRUD — 改造 set_task_outputs
**Files:**
- Modify: `backend/app/crud/production_task.py` (第 163-176 行 `_task_output_to_dict`,第 870-891 行 `set_task_outputs`)
- Modify: `backend/app/crud/cell_product.py` (提取内部辅助函数)
- [ ] **Step 1: 增强 `_task_output_to_dict` 返回细胞产品信息**
在 `backend/app/crud/production_task.py` 中修改 `_task_output_to_dict` 函数,增加细胞产品的完整信息:
```python
def _task_output_to_dict(o: BizProductionTaskOutput, *, product_dict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
result = {
"id": o.id,
"taskId": o.task_id,
"cellProductId": o.cell_product_id or "",
"storageSlotId": o.storage_slot_id or "",
"outputName": o.output_name or "",
"outputBarcode": o.output_barcode or "",
"quantity": float(o.quantity) if o.quantity is not None else None,
"unit": o.unit or "",
"remark": o.remark or "",
"attachments": o.attachments or "",
}
if product_dict:
result["productCode"] = product_dict.get("productCode", "")
result["productName"] = product_dict.get("productName", "")
result["productTypeId"] = product_dict.get("productTypeId", "")
result["productTypeName"] = product_dict.get("productTypeName", "")
result["specId"] = product_dict.get("specId", "")
result["specName"] = product_dict.get("specName", "")
result["passage"] = product_dict.get("passage", "")
result["batchNo"] = product_dict.get("batchNo", "")
result["storageSlotName"] = product_dict.get("storageSlotName", "")
result["expectedCellCount"] = product_dict.get("expectedCellCount")
result["viability"] = product_dict.get("viability")
result["freezeDate"] = product_dict.get("freezeDate", "")
result["expiryDate"] = product_dict.get("expiryDate", "")
result["productStatus"] = product_dict.get("productStatus", "")
result["productImages"] = product_dict.get("productImages", "")
return result
```
- [ ] **Step 2: 改造 `set_task_outputs` 增加 diff + 细胞产品联动**
在 `backend/app/crud/production_task.py` 中,完全重写 `set_task_outputs` 函数。在文件顶部增加 import:
```python
from app.models.models import (
BizCellProduct,
BizStorageSlotOccupancy,
)
from app.crud import storage_occupancy as occupancy_svc
from app.crud import cell_product as cell_product_svc
```
替换 `set_task_outputs` 函数为:
```python
def set_task_outputs(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# --- 1. 加载旧 outputs(未软删的) ---
old_rows = db.query(BizProductionTaskOutput).filter(
and_(
BizProductionTaskOutput.task_id == task_id,
BizProductionTaskOutput.del_flag == 0,
)
).all()
old_cp_map: Dict[str, BizProductionTaskOutput] = {}
for o in old_rows:
if o.cell_product_id:
old_cp_map[o.cell_product_id] = o
# --- 2. 构建新 outputs 的 cp_id 集合 ---
new_cp_ids: set = set()
for it in items:
cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
if cp_id:
new_cp_ids.add(cp_id)
# --- 3. 处理删除(旧有新无) ---
for cp_id, old_row in old_cp_map.items():
if cp_id not in new_cp_ids:
# 作废细胞产品
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
).first()
if product:
product.product_status = "voided"
product.update_by = user_id
product.update_time = now
db.add(product)
# 释放库位
if product.storage_slot_id:
occupancy_svc.remove_occupancy_by_item(db, "cell_product", cp_id, user_id=user_id)
# 软删关联行
old_row.del_flag = 1
old_row.update_by = user_id
old_row.update_time = now
db.add(old_row)
# --- 4. 处理新增和修改 ---
for it in items:
cp_id = (it.get("cell_product_id") or it.get("cellProductId") or "").strip()
if cp_id and cp_id in old_cp_map:
# === 修改已有 ===
product = db.query(BizCellProduct).filter(
BizCellProduct.id == cp_id, BizCellProduct.del_flag == 0
).first()
if product:
old_slot_id = product.storage_slot_id
# 更新产品字段
_update_product_from_output(product, it, user_id, now)
new_slot_id = product.storage_slot_id
# 库位变更处理
if old_slot_id != new_slot_id:
if old_slot_id:
occupancy_svc.remove_occupancy_by_item(db, "cell_product", cp_id, user_id=user_id)
if new_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
# 更新关联行
old_row = old_cp_map[cp_id]
old_row.output_name = product.product_name if product else (it.get("output_name") or it.get("outputName") or "")
old_row.output_barcode = product.product_code if product else (it.get("output_barcode") or it.get("outputBarcode") or "")
old_row.storage_slot_id = product.storage_slot_id if product else None
old_row.quantity = it.get("quantity")
old_row.unit = (it.get("unit") or "").strip() or None
old_row.remark = (it.get("remark") or "").strip() or None
old_row.update_by = user_id
old_row.update_time = now
db.add(old_row)
else:
# === 新增 ===
product = BizCellProduct(
id=generate_uuid(),
product_code=(it.get("product_code") or it.get("productCode") or "").strip(),
product_name=(it.get("product_name") or it.get("productName") or "").strip(),
product_type_id=(it.get("product_type_id") or it.get("productTypeId") or "").strip() or None,
spec_id=(it.get("spec_id") or it.get("specId") or "").strip() or None,
passage=(it.get("passage") or "").strip() or None,
batch_no=(it.get("batch_no") or it.get("batchNo") or "").strip() or None,
storage_slot_id=(it.get("storage_slot_id") or it.get("storageSlotId") or "").strip() or None,
expected_cell_count=it.get("expected_cell_count") if it.get("expected_cell_count") is not None else (it.get("expectedCellCount") if it.get("expectedCellCount") is not None else None),
viability=it.get("viability") if it.get("viability") is not None else None,
freeze_date=_parse_optional_date(it.get("freeze_date") or it.get("freezeDate")),
product_status=it.get("product_status") or it.get("productStatus") or "frozen",
source_type="production",
production_task_id=task_id,
quantity=it.get("quantity"),
unit=(it.get("unit") or "").strip() or None,
expiry_date=_parse_optional_date(it.get("expiry_date") or it.get("expiryDate")),
product_images=(it.get("product_images") or it.get("productImages") or "").strip() or None,
tenant_no=tenant_no,
order_num=0,
remark=(it.get("remark") or "").strip() or None,
create_by=user_id,
create_dept_id="",
create_time=now,
update_by=user_id,
update_time=now,
del_flag=0,
)
db.add(product)
db.flush()
# 创建库位占用
if product.storage_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
# 创建关联行
output_row = BizProductionTaskOutput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
cell_product_id=product.id,
storage_slot_id=product.storage_slot_id,
output_name=product.product_name or "",
output_barcode=product.product_code or "",
quantity=product.quantity,
unit=product.unit,
remark=product.remark,
attachments=(it.get("attachments") or "").strip() or None,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
)
db.add(output_row)
db.flush()
db.commit()
```
- [ ] **Step 3: 添加辅助函数**
在 `set_task_outputs` 函数之前,添加两个辅助函数:
```python
def _parse_optional_date(val):
"""解析可选日期字符串"""
if not val:
return None
from dateutil.parser import parse as _parse_dt
try:
return _parse_dt(str(val))
except Exception:
return None
def _update_product_from_output(product: BizCellProduct, it: Dict[str, Any], user_id, now):
"""从 output item 更新细胞产品字段"""
for field_map in [
("product_name", "product_name", "productName"),
("product_type_id", "product_type_id", "productTypeId"),
("spec_id", "spec_id", "specId"),
("batch_no", "batch_no", "batchNo"),
("storage_slot_id", "storage_slot_id", "storageSlotId"),
("unit", "unit", "unit"),
("product_images", "product_images", "productImages"),
("remark", "remark", "remark"),
]:
val = it.get(field_map[1]) or it.get(field_map[2])
if val is not None:
setattr(product, field_map[0], (val or "").strip() or None)
for num_field in [("expected_cell_count", "expected_cell_count", "expectedCellCount"), ("viability", "viability", "viability")]:
val = it.get(num_field[1])
if val is None:
val = it.get(num_field[2])
if val is not None:
setattr(product, num_field[0], val)
for date_field in [("freeze_date", "freeze_date", "freezeDate"), ("expiry_date", "expiry_date", "expiryDate")]:
val = it.get(date_field[1]) or it.get(date_field[2])
if val is not None:
setattr(product, date_field[0], _parse_optional_date(val))
passage = it.get("passage")
if passage is not None:
product.passage = (passage or "").strip() or None
quantity = it.get("quantity")
if quantity is not None:
product.quantity = quantity
product.update_by = user_id
product.update_time = now
def _create_occupancy_for_product(db: Session, product: BizCellProduct, tenant_no: str, user_id: Optional[str]):
"""为细胞产品创建库位占用记录"""
if not product.storage_slot_id:
return
occ = BizStorageSlotOccupancy(
id=generate_uuid(),
slot_id=product.storage_slot_id,
item_type="cell_product",
item_id=product.id,
item_code=product.product_code or "",
item_name=product.product_name or "",
item_source="production",
stored_at=datetime.now(),
occupancy_status="stored",
tenant_no=tenant_no,
order_num=0,
create_by=user_id,
create_dept_id="",
create_time=datetime.now(),
update_by=user_id,
update_time=datetime.now(),
del_flag=0,
)
db.add(occ)
db.flush()
occupancy_svc._refresh_slot_cache(db, product.storage_slot_id)
```
- [ ] **Step 4: 在 `storage_occupancy.py` 中确认 `remove_occupancy_by_item` 存在**
Run: `cd backend && source venv/bin/activate && python -c "from app.crud.storage_occupancy import remove_occupancy_by_item; print('OK')"`
Expected: `OK`(如果函数名不同,查阅 `backend/app/crud/storage_occupancy.py` 找到正确的按 item_type+item_id 删除的函数名并修正 Step 3 中的调用)
- [ ] **Step 5: 验证后端模块导入无误**
Run: `cd backend && source venv/bin/activate && python -c "from app.crud.production_task import set_task_outputs; print('OK')"`
Expected: `OK`
---
## Task 3: 后端 CRUD — 改造 set_task_inputs
**Files:**
- Modify: `backend/app/crud/production_task.py` (第 850-867 行 `set_task_inputs`)
- [ ] **Step 1: 重写 `set_task_inputs` 增加细胞产品状态联动**
替换 `set_task_inputs` 函数:
```python
def set_task_inputs(
db: Session, task_id: str, tenant_no: str,
user_id: Optional[str], items: List[Dict[str, Any]],
) -> None:
now = datetime.now()
# --- 1. 加载旧 inputs ---
old_rows = db.query(BizProductionTaskInput).filter(
and_(
BizProductionTaskInput.task_id == task_id,
BizProductionTaskInput.del_flag == 0,
)
).all()
old_cp_ids: set = set()
for o in old_rows:
if o.input_type == "cell_product" and o.input_id:
old_cp_ids.add(o.input_id.strip())
# --- 2. 构建新的细胞产品 ID 集合 ---
new_cp_ids: set = set()
for it in items:
input_type = (it.get("input_type") or it.get("inputType") or "").strip()
input_id = (it.get("input_id") or it.get("inputId") or "").strip()
if input_type == "cell_product" and input_id:
new_cp_ids.add(input_id)
# --- 3. 被移除的细胞产品 → 恢复 frozen + 恢复库位 ---
for pid in old_cp_ids - new_cp_ids:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == pid, BizCellProduct.del_flag == 0
).first()
if product and product.product_status == "used":
product.product_status = "frozen"
product.update_by = user_id
product.update_time = now
db.add(product)
# 恢复库位占用
if product.storage_slot_id:
_create_occupancy_for_product(db, product, tenant_no, user_id)
# --- 4. 新增的细胞产品 → 标记 used + 释放库位 ---
for pid in new_cp_ids - old_cp_ids:
product = db.query(BizCellProduct).filter(
BizCellProduct.id == pid, BizCellProduct.del_flag == 0
).first()
if product and product.product_status != "used":
product.product_status = "used"
product.update_by = user_id
product.update_time = now
db.add(product)
# 释放库位
if product.storage_slot_id:
occupancy_svc.remove_occupancy_by_item(db, "cell_product", pid, user_id=user_id)
# --- 5. 软删旧行 + 创建新行 ---
for o in old_rows:
o.del_flag = 1
o.update_by = user_id
o.update_time = now
db.add(o)
for it in items:
db.add(BizProductionTaskInput(
id=generate_uuid(), task_id=task_id, tenant_no=tenant_no,
input_type=(it.get("input_type") or it.get("inputType") or "").strip() or None,
input_id=(it.get("input_id") or it.get("inputId") or "").strip() or None,
input_code=(it.get("input_code") or it.get("inputCode") or "").strip() or None,
input_name=(it.get("input_name") or it.get("inputName") or "").strip() or None,
remark=(it.get("remark") or "").strip() or None,
order_num=0, create_by=user_id, create_dept_id="",
create_time=now, update_by=user_id, update_time=now, del_flag=0,
))
db.flush()
db.commit()
```
- [ ] **Step 2: 验证**
Run: `cd backend && source venv/bin/activate && python -c "from app.crud.production_task import set_task_inputs; print('OK')"`
Expected: `OK`
---
## Task 4: 后端 — 增强 get_production_task_detail 产出信息
**Files:**
- Modify: `backend/app/crud/production_task.py` (`get_production_task_detail` 中 outputs 部分约第 499-505 行)
- [ ] **Step 1: 在 `get_production_task_detail` 中为 outputs 填充细胞产品信息**
找到 `get_production_task_detail` 函数中处理 outputs 的部分(约第 499-505 行),将:
```python
result["outputs"] = [_task_output_to_dict(o) for o in outputs]
```
替换为:
```python
# 产出列表:填充细胞产品详情
output_dicts = []
cp_ids = [o.cell_product_id for o in outputs if o.cell_product_id]
cp_map: Dict[str, Dict] = {}
if cp_ids:
cp_rows = db.query(BizCellProduct).filter(
BizCellProduct.id.in_(cp_ids), BizCellProduct.del_flag == 0
).all()
cp_dicts = cell_product_svc._enrich_product_dicts(
db, tenant_no, [cell_product_svc._row_to_dict(p) for p in cp_rows]
)
for d in cp_dicts:
cp_map[d["id"]] = d
for o in outputs:
pdict = cp_map.get(o.cell_product_id) if o.cell_product_id else None
output_dicts.append(_task_output_to_dict(o, product_dict=pdict))
result["outputs"] = output_dicts
```
注意:需要在文件顶部添加 `from app.crud import cell_product as cell_product_svc`(如果 Task 2 中尚未添加)。
- [ ] **Step 2: 验证**
Run: `cd backend && source venv/bin/activate && python -c "from app.crud.production_task import get_production_task_detail; print('OK')"`
Expected: `OK`
---
## Task 5: 前端 API 层 — 更新 fetchSetTaskOutputs 参数
**Files:**
- Modify: `web-admin/src/api/production-task.ts` (第 72-74 行)
- [ ] **Step 1: 更新 `fetchSetTaskOutputs` 参数类型**
将 `web-admin/src/api/production-task.ts` 中的 `fetchSetTaskOutputs` 添加 TypeScript 类型注释:
```typescript
export interface TaskOutputItem {
cellProductId?: string
productCode?: string
productName?: string
productTypeId?: string
specId?: string
passage?: string
batchNo?: string
storageSlotId?: string
expectedCellCount?: number
viability?: number
freezeDate?: string
quantity?: number
unit?: string
expiryDate?: string
productImages?: string
productStatus?: string
sourceType?: string
outputName?: string
outputBarcode?: string
remark?: string
attachments?: string
}
export function fetchSetTaskOutputs(taskId: string, data: TaskOutputItem[]) {
return request.post({ url: `/web/admin/production-tasks/${taskId}/outputs`, params: data })
}
```
- [ ] **Step 2: 验证 TS 无报错**
Run: `cd web-admin && npx vue-tsc --noEmit 2>&1 | head -20`
Expected: 无与 `production-task.ts` 相关的错误(其他 pre-existing 错误可忽略)
---
## Task 6: 前端 — 新建 TaskOutputProductDialog.vue
**Files:**
- Create: `web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue`
**参考**: `web-admin/src/views/production/cell-product/index.vue` 第 249-559 行的新增/编辑弹窗表单
- [ ] **Step 1: 创建组件文件**
创建 `web-admin/src/views/production/production-work-order/TaskOutputProductDialog.vue`,内容为:
```vue
vis && searchUnits('')">
取消
确定
```
- [ ] **Step 2: 验证无语法错误**
Run: `cd web-admin && npx vue-tsc --noEmit 2>&1 | grep -i "TaskOutputProductDialog" || echo "No errors for this file"`
Expected: 无该文件相关错误
---
## Task 7: 前端 — 新建 TaskOutputList.vue
**Files:**
- Create: `web-admin/src/views/production/production-work-order/TaskOutputList.vue`
- [ ] **Step 1: 创建产出列表组件**
创建 `web-admin/src/views/production/production-work-order/TaskOutputList.vue`:
```vue
{{ row.productTypeName || '—' }}
{{ row.passage || '—' }}
{{ row.storageSlotName || '—' }}
{{ row.quantity ?? '—' }} {{ row.unit || '' }}
{{ productStatusTag(row.productStatus).label }}
—
编辑
删除
```
---
## Task 8: 前端 — 新建 TaskInputEnhancedTable.vue
**Files:**
- Create: `web-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vue`
- [ ] **Step 1: 创建增强版原材料表格组件**
创建 `web-admin/src/views/production/production-work-order/TaskInputEnhancedTable.vue`:
```vue
{{ row.inputType === 'sample' ? '样本' : row.inputType === 'cell_product' ? '细胞产品' : row.inputType }}
{ row.inputId = ''; row.inputCode = ''; row.inputName = '' }">
onCellProductSelect(row, val)"
>
{{ row.inputCode || '—' }}
{{ row.inputCode || '—' }}
{{ row.inputName || '—' }}
{{ row.productTypeName || '—' }}
—
{{ row.passage || '—' }}
—
{{ row.storageSlotName || '—' }}
—
{{ productStatusTag(row.productStatus).label }}
—
{{ row.remark || '—' }}
删除
```
---
## Task 9: 前端 — 重构 useTaskStepEditor.ts
**Files:**
- Modify: `web-admin/src/views/production/production-work-order/useTaskStepEditor.ts`
这是最复杂的改造任务。主要变更:
1. 产出操作改为子弹窗模式
2. 原材料选择细胞产品时填充完整信息
3. 保存时提交增强后的数据
- [ ] **Step 1: 增加产出子弹窗状态**
在 `useTaskStepEditor` composable 中(约第 72-74 行 `selectedVirtualStep` 之后),增加产出子弹窗状态:
```typescript
// ── 产出子弹窗状态 ────────────────────────────────────────
const outputDialogVisible = ref(false)
const outputDialogMode = ref<'add' | 'edit'>('add')
const editingOutputIndex = ref(-1)
const editingOutputData = ref | null>(null)
// ── 产出下拉选项 ─────────────────────────────────────────
const outputDropdownOptions = reactive({
productTypes: [] as Array<{ id: string; productTypeName: string }>,
specs: [] as Array<{ id: string; specName: string }>,
storageSlots: [] as Array<{ id: string; name: string }>,
})
```
- [ ] **Step 2: 增加下拉选项加载函数**
在 composable 中增加:
```typescript
async function loadOutputDropdownData() {
try {
const [typeRes, specRes] = await Promise.all([
(await import('@/api/admin')).fetchCellProductTypeList({ page: 1, pageSize: 100 }),
(await import('@/api/admin')).fetchCellProductSpecList({ page: 1, pageSize: 100 }),
])
outputDropdownOptions.productTypes = typeRes?.list || typeRes || []
outputDropdownOptions.specs = specRes?.list || specRes || []
// 储物格加载(复用细胞产品库的逻辑)
const facilityRes = await (await import('@/api/admin')).fetchStorageFacilityList()
const facilities = facilityRes?.list || facilityRes || []
const { buildTreePathLabelFromFlat } = await import('@/utils/tree-path')
const { fetchStorageSlotList } = await import('@/api/admin')
const leafNodes = facilities.filter((n: any) => n.isLeaf === 1)
const slotOptions: Array<{ id: string; name: string }> = []
for (const node of leafNodes) {
const slotRes: any = await fetchStorageSlotList({ facilityNodeId: node.id })
const slots = slotRes?.list || slotRes || []
const nodePath = buildTreePathLabelFromFlat(node.id, facilities, { nameKey: 'nodeName' })
for (const slot of slots) {
const code = slot.slotCode || ''
const name = nodePath && code ? `${nodePath} / ${code}` : code || nodePath
slotOptions.push({ id: slot.id, name })
}
}
outputDropdownOptions.storageSlots = slotOptions
} catch {
/* 下拉数据加载失败不阻塞 */
}
}
```
- [ ] **Step 3: 修改产出操作函数**
替换原有的 `addTaskOutputRow`、`removeTaskOutputRow`、`saveTaskOutputs`:
```typescript
function addTaskOutputRow() {
outputDialogMode.value = 'add'
editingOutputIndex.value = -1
editingOutputData.value = null
outputDialogVisible.value = true
}
function editTaskOutputRow(row: Record, index: number) {
outputDialogMode.value = 'edit'
editingOutputIndex.value = index
editingOutputData.value = { ...row }
outputDialogVisible.value = true
}
function handleOutputDialogSave(formData: Record) {
if (outputDialogMode.value === 'add') {
taskOutputs.value.push({
cellProductId: '',
...formData,
outputName: formData.productName,
outputBarcode: formData.productCode,
productStatus: 'frozen',
sourceType: 'production',
})
} else if (outputDialogMode.value === 'edit' && editingOutputIndex.value >= 0) {
const existing = taskOutputs.value[editingOutputIndex.value]
taskOutputs.value[editingOutputIndex.value] = {
...existing,
...formData,
outputName: formData.productName || existing.outputName,
outputBarcode: formData.productCode || existing.outputBarcode,
}
}
}
function removeTaskOutputRow(i: number) {
taskOutputs.value.splice(i, 1)
}
async function saveTaskOutputs() {
if (!taskDetail.value?.id) return
if (!taskOutputs.value.length) {
ElMessage.warning('请至少添加一项产出(细胞产品)')
return
}
const invalid = taskOutputs.value.find(
(row) => !(row.productCode || row.outputBarcode || '').trim() || !(row.productName || row.outputName || '').trim()
)
if (invalid) {
ElMessage.warning('请补全每行产出的产品编码与名称')
return
}
outputSaving.value = true
try {
await fetchSetTaskOutputs(taskDetail.value.id, taskOutputs.value)
ElMessage.success('产出已保存')
await loadTaskDetail(taskDetail.value.id)
} catch { /* global error handling */ } finally { outputSaving.value = false }
}
```
- [ ] **Step 4: 修改原材料选择联动**
增强 `onCellProductInputSelect` 函数,填充完整产品信息:
```typescript
function onCellProductInputSelect(row: Record, productId: string) {
if (!productId) { row.inputId = ''; return }
const master = cellProductMasterById.value[productId]
if (!master) return
row.inputId = master.id
row.inputCode = master.product_code || master.productCode || ''
row.inputName = master.product_name || master.productName || ''
// 增强信息填充
row.productTypeName = master.product_type_name || master.productTypeName || ''
row.passage = master.passage || ''
row.storageSlotName = master.storage_slot_name || master.storageSlotName || ''
row.productStatus = master.product_status || master.productStatus || ''
}
```
- [ ] **Step 5: 在 initEditor 中加载下拉选项**
在 `initEditor` 函数末尾(约第 755 行 `configSuppressSave.value = false` 之前),增加:
```typescript
if (mode.value === 'execution') {
void loadOutputDropdownData()
}
```
- [ ] **Step 6: 在 return 对象中导出新状态和方法**
在 return 对象中增加:
```typescript
outputDialogVisible, outputDialogMode, editingOutputIndex, editingOutputData,
outputDropdownOptions, loadOutputDropdownData,
editTaskOutputRow, handleOutputDialogSave,
```
- [ ] **Step 7: 验证**
Run: `cd web-admin && npx vue-tsc --noEmit 2>&1 | grep -i "useTaskStepEditor" || echo "No errors"`
Expected: 无该文件相关错误
---
## Task 10: 前端 — 改造 TaskExecutionDialog.vue
**Files:**
- Modify: `web-admin/src/views/production/production-work-order/TaskExecutionDialog.vue`
- [ ] **Step 1: 添加新组件 import**
在 `