# 生产任务 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 ``` - [ ] **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 ``` --- ## 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 ``` --- ## 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** 在 `