docs: add temporary QC external target code implementation plan
Break the approved design into SQL, backend validation, API, and web-admin UI tasks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e527b15221
commit
19b0346f2d
1 file changed
+698
@@ -0,0 +1,698 @@
|
||||
# 临时质检 · 外部质检对象编号 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:** 临时质检工单支持「系统细胞产品 / 外部科研编号」二选一;外部路径可手写编号并在列表/详情展示。
|
||||
|
||||
**Architecture:** 在 `biz_qc_work_order` 增加可空字段 `external_target_code`;手动创建/更新临时工单时用纯函数归一化互斥目标(有产品则清空外部编号,有外部编号则清空产品 ID);列表/详情透出字段;前端弹窗用来源切换(方案 A);卡片标题与详情按来源展示。生产派发链路不改。
|
||||
|
||||
**Tech Stack:** FastAPI / SQLAlchemy / Pydantic v2 / Vue 3 / Element Plus;SQL MySQL 5.7。
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-20-temporary-qc-external-target-code-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| File | Responsibility |
|
||||
|------|----------------|
|
||||
| Create: `docs/sql/2026-08-20-qc-work-order-external-target-code.sql` | 增量 ALTER |
|
||||
| Modify: `backend/app/models/models.py` | `BizQcWorkOrder.external_target_code` |
|
||||
| Modify: `backend/app/schemas/qc_work_order.py` | Create/Update Body 增加 `externalTargetCode` |
|
||||
| Modify: `backend/app/crud/qc_work_order.py` | 归一化校验、create/update、`_wo_to_dict`、keyword |
|
||||
| Create: `backend/tests/test_temp_qc_target_resolve.py` | 互斥归一化纯函数单测 |
|
||||
| Modify: `backend/app/api/qc.py` | create/update 传入 `external_target_code` |
|
||||
| Modify: `web-admin/src/api/qc-work-order.ts` | 类型与 create/update 参数 |
|
||||
| Modify: `web-admin/src/views/quality-inspection/qc-work-order/index.vue` | 方案 A 弹窗 |
|
||||
| Modify: `web-admin/src/utils/qc-work-order-card-meta.ts` | 卡片标题 + temporary meta |
|
||||
| Modify: `web-admin/src/views/quality-inspection/qc-work-order/composables/inbound-view-sections.ts` | 详情分区 |
|
||||
| Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue` | 任务侧「质检对象」兜底 |
|
||||
| Modify: `web-admin/src/utils/qc-work-order-tabs.ts` | temporary keyword placeholder |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: SQL 增量字段
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/sql/2026-08-20-qc-work-order-external-target-code.sql`
|
||||
|
||||
- [ ] **Step 1: Write SQL**
|
||||
|
||||
```sql
|
||||
-- 临时质检:外部质检对象编号(手写,可空)
|
||||
-- MySQL 5.7
|
||||
|
||||
ALTER TABLE `biz_qc_work_order`
|
||||
ADD COLUMN `external_target_code` varchar(64) DEFAULT NULL COMMENT '外部质检对象编号(手写)' AFTER `cell_product_id`;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/sql/2026-08-20-qc-work-order-external-target-code.sql
|
||||
git commit -m "docs(sql): add external_target_code on biz_qc_work_order"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 后端模型 + 互斥归一化纯函数(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/models/models.py`(`BizQcWorkOrder`,约 `cell_product_id` 后)
|
||||
- Modify: `backend/app/crud/qc_work_order.py`
|
||||
- Create: `backend/tests/test_temp_qc_target_resolve.py`
|
||||
|
||||
- [ ] **Step 1: Write failing tests**
|
||||
|
||||
```python
|
||||
"""临时质检对象:系统产品 / 外部编号互斥归一化。"""
|
||||
|
||||
import unittest
|
||||
|
||||
from app.crud.qc_work_order import resolve_temp_qc_target
|
||||
|
||||
|
||||
class TestResolveTempQcTarget(unittest.TestCase):
|
||||
def test_cell_product_only(self):
|
||||
cp, ext = resolve_temp_qc_target(
|
||||
cell_product_id=" pid-1 ", external_target_code=" "
|
||||
)
|
||||
self.assertEqual(cp, "pid-1")
|
||||
self.assertIsNone(ext)
|
||||
|
||||
def test_external_only(self):
|
||||
cp, ext = resolve_temp_qc_target(
|
||||
cell_product_id="", external_target_code=" EXT-1 "
|
||||
)
|
||||
self.assertIsNone(cp)
|
||||
self.assertEqual(ext, "EXT-1")
|
||||
|
||||
def test_both_raises(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
resolve_temp_qc_target(
|
||||
cell_product_id="pid", external_target_code="EXT"
|
||||
)
|
||||
self.assertIn("不能同时", str(ctx.exception))
|
||||
|
||||
def test_neither_raises(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
resolve_temp_qc_target(cell_product_id="", external_target_code="")
|
||||
self.assertIn("质检对象", str(ctx.exception))
|
||||
|
||||
def test_external_too_long(self):
|
||||
with self.assertRaises(ValueError) as ctx:
|
||||
resolve_temp_qc_target(
|
||||
cell_product_id="", external_target_code="x" * 65
|
||||
)
|
||||
self.assertIn("64", str(ctx.exception))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests — expect FAIL (import / missing function)**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && python -m pytest tests/test_temp_qc_target_resolve.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(`resolve_temp_qc_target` 未定义)
|
||||
|
||||
- [ ] **Step 3: Add model column**
|
||||
|
||||
In `BizQcWorkOrder`, immediately after `cell_product_id`:
|
||||
|
||||
```python
|
||||
cell_product_id = Column(String(64), nullable=True, comment="质检对象-细胞产品ID")
|
||||
external_target_code = Column(String(64), nullable=True, comment="外部质检对象编号(手写)")
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Implement `resolve_temp_qc_target` near `_validate_cell_product_id`**
|
||||
|
||||
```python
|
||||
def resolve_temp_qc_target(
|
||||
*,
|
||||
cell_product_id: Optional[str] = None,
|
||||
external_target_code: Optional[str] = None,
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""临时质检对象互斥归一化。返回 (cell_product_id|None, external_target_code|None)。"""
|
||||
cp = (cell_product_id or "").strip() or None
|
||||
ext = (external_target_code or "").strip() or None
|
||||
if cp and ext:
|
||||
raise ValueError("质检对象不能同时选择系统产品并填写外部编号")
|
||||
if not cp and not ext:
|
||||
raise ValueError("请选择质检对象或填写外部编号")
|
||||
if ext and len(ext) > 64:
|
||||
raise ValueError("外部编号最长 64 个字符")
|
||||
if ext:
|
||||
return None, ext
|
||||
return cp, None
|
||||
```
|
||||
|
||||
Ensure `Tuple` / `Optional` imports exist at top of `qc_work_order.py`.
|
||||
|
||||
- [ ] **Step 5: Run tests — expect PASS**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && python -m pytest tests/test_temp_qc_target_resolve.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/models/models.py backend/app/crud/qc_work_order.py backend/tests/test_temp_qc_target_resolve.py
|
||||
git commit -m "feat(backend): resolve temporary QC target cell product vs external code"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Schema + create/update + dict + keyword + API
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/schemas/qc_work_order.py`
|
||||
- Modify: `backend/app/crud/qc_work_order.py`(`create_temp_qc_work_order`、`update_qc_work_order`、`_wo_to_dict`、列表 keyword)
|
||||
- Modify: `backend/app/api/qc.py`
|
||||
|
||||
- [ ] **Step 1: Extend Pydantic bodies**
|
||||
|
||||
In `QcWorkOrderCreateBody` add:
|
||||
|
||||
```python
|
||||
external_target_code: Optional[str] = Field(
|
||||
None, alias="externalTargetCode", description="外部质检对象编号(手写)"
|
||||
)
|
||||
```
|
||||
|
||||
In `QcWorkOrderUpdateBody` add the same field.
|
||||
|
||||
- [ ] **Step 2: Update `create_temp_qc_work_order`**
|
||||
|
||||
Change signature to accept `external_target_code: str = ""` and replace the old forced `_validate_cell_product_id` block with:
|
||||
|
||||
```python
|
||||
cp_id, ext_code = resolve_temp_qc_target(
|
||||
cell_product_id=cell_product_id,
|
||||
external_target_code=external_target_code,
|
||||
)
|
||||
if cp_id:
|
||||
cp_id = _validate_cell_product_id(db, cp_id, tenant_no)
|
||||
wo = BizQcWorkOrder(
|
||||
...
|
||||
cell_product_id=cp_id,
|
||||
external_target_code=ext_code,
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
Keep other fields unchanged.
|
||||
|
||||
- [ ] **Step 3: Update `update_qc_work_order`**
|
||||
|
||||
Add param `external_target_code: Optional[str] = None`. For `wo_type == "temporary"`, when **either** `cell_product_id is not None` **or** `external_target_code is not None`(编辑弹窗总会同时提交两者时用显式传参):
|
||||
|
||||
约定:编辑临时工单时 API **始终同时传入** `cellProductId` 与 `externalTargetCode`(一侧为空串)。实现:
|
||||
|
||||
```python
|
||||
if wo_type == "temporary":
|
||||
if cell_product_id is not None or external_target_code is not None:
|
||||
cp_id, ext_code = resolve_temp_qc_target(
|
||||
cell_product_id=cell_product_id if cell_product_id is not None else (wo.cell_product_id or ""),
|
||||
external_target_code=external_target_code
|
||||
if external_target_code is not None
|
||||
else (getattr(wo, "external_target_code", None) or ""),
|
||||
)
|
||||
# 前端编辑会同时提交两侧;为避免「只改备注」误走上面分支,
|
||||
# API 层:仅当 body 显式带了 cellProductId 或 externalTargetCode 字段时才更新对象。
|
||||
if cp_id:
|
||||
cp_id = _validate_cell_product_id(db, cp_id, tenant_no)
|
||||
wo.cell_product_id = cp_id
|
||||
wo.external_target_code = ext_code
|
||||
```
|
||||
|
||||
更稳妥的约定(推荐在本 Task 落实):
|
||||
|
||||
- `update` 增加显式参数;API 用 `body.model_fields_set` / `body.model_dump(exclude_unset=True)` 判断是否更新质检对象。
|
||||
- 若 `cellProductId` 或 `externalTargetCode` **任一在 unset 之外**:用 body 值(缺省当 `""`)调用 `resolve_temp_qc_target`,再写回。
|
||||
- 若两者都 unset:不改质检对象(备注/状态路径不变)。
|
||||
|
||||
Example in API:
|
||||
|
||||
```python
|
||||
data = body.model_dump(by_alias=False, exclude_unset=True)
|
||||
touch_target = "cell_product_id" in data or "external_target_code" in data
|
||||
wo = wo_svc.update_qc_work_order(
|
||||
...,
|
||||
cell_product_id=data.get("cell_product_id") if touch_target else None,
|
||||
external_target_code=data.get("external_target_code") if touch_target else None,
|
||||
# 当 touch_target 时,传入两侧(缺省 ""),crud 内若两者参数均 not None 则 resolve
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
Simplest crud rule that matches frontend(编辑弹窗总是带两侧):
|
||||
|
||||
```python
|
||||
if wo_type == "temporary" and cell_product_id is not None and external_target_code is not None:
|
||||
cp_id, ext_code = resolve_temp_qc_target(
|
||||
cell_product_id=cell_product_id,
|
||||
external_target_code=external_target_code,
|
||||
)
|
||||
if cp_id:
|
||||
cp_id = _validate_cell_product_id(db, cp_id, tenant_no)
|
||||
wo.cell_product_id = cp_id
|
||||
wo.external_target_code = ext_code
|
||||
```
|
||||
|
||||
备注-only 更新(`WorkOrderRemarkDialog`)不传这两个字段 → 两者保持 `None` → 不改对象。前端临时编辑弹窗必须 **同时传** `cellProductId` 与 `externalTargetCode`(一侧 `""`)。
|
||||
|
||||
- [ ] **Step 4: `_wo_to_dict` 增加字段**
|
||||
|
||||
Next to `cellProductId`:
|
||||
|
||||
```python
|
||||
"cellProductId": getattr(wo, "cell_product_id", None) or "",
|
||||
"externalTargetCode": getattr(wo, "external_target_code", None) or "",
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Keyword — general list**
|
||||
|
||||
In `list_*` keyword block(约 `keyword_conditions = [...]`),追加:
|
||||
|
||||
```python
|
||||
BizQcWorkOrder.external_target_code.like(like),
|
||||
```
|
||||
|
||||
Also in `list_temp_qc_work_orders` if still used:
|
||||
|
||||
```python
|
||||
q0 = q0.filter(
|
||||
or_(
|
||||
BizQcWorkOrder.work_order_no.like(like),
|
||||
BizQcWorkOrder.external_target_code.like(like),
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
(需 `or_` 已导入。)
|
||||
|
||||
- [ ] **Step 6: Wire API create/update**
|
||||
|
||||
`create_qc_work_order` temporary branch:
|
||||
|
||||
```python
|
||||
wo = wo_svc.create_temp_qc_work_order(
|
||||
db,
|
||||
tenant_no=tenant_no,
|
||||
user_id=user.get("user_id"),
|
||||
create_dept_id=user.get("dept_id", ""),
|
||||
cell_product_id=body.cell_product_id or "",
|
||||
external_target_code=body.external_target_code or "",
|
||||
remark=body.remark or "",
|
||||
)
|
||||
```
|
||||
|
||||
`update_qc_work_order` API: pass `external_target_code=body.external_target_code`(与 `cell_product_id` 同样,可为 None)。
|
||||
|
||||
- [ ] **Step 7: Smoke import**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && python -c "from app.crud.qc_work_order import resolve_temp_qc_target, create_temp_qc_work_order; from app.schemas.qc_work_order import QcWorkOrderCreateBody; print('OK')"
|
||||
```
|
||||
|
||||
Expected: `OK`
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/schemas/qc_work_order.py backend/app/crud/qc_work_order.py backend/app/api/qc.py
|
||||
git commit -m "feat(backend): persist external target code on temporary QC work orders"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 前端 API 类型
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/api/qc-work-order.ts`
|
||||
|
||||
- [ ] **Step 1: Extend `QcWorkOrder`**
|
||||
|
||||
After `cellProductId?: string` add:
|
||||
|
||||
```typescript
|
||||
externalTargetCode?: string
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Extend create/update payloads**
|
||||
|
||||
```typescript
|
||||
export function fetchCreateQcWorkOrder(data: {
|
||||
workOrderType?: string
|
||||
protocolNo?: string
|
||||
sampleCode?: string
|
||||
receptionSource?: string
|
||||
qcBizType?: string
|
||||
cellProductId?: string
|
||||
externalTargetCode?: string
|
||||
remark?: string
|
||||
}) { ... }
|
||||
|
||||
export function fetchUpdateQcWorkOrder(
|
||||
id: string,
|
||||
data: {
|
||||
cellProductId?: string
|
||||
externalTargetCode?: string
|
||||
workOrderStatus?: string
|
||||
qcConclusion?: string
|
||||
remark?: string
|
||||
}
|
||||
) { ... }
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/api/qc-work-order.ts
|
||||
git commit -m "feat(web-admin): type externalTargetCode on QC work order API"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 新增/编辑弹窗(方案 A)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/quality-inspection/qc-work-order/index.vue`
|
||||
|
||||
- [ ] **Step 1: Template — temporary form block**
|
||||
|
||||
Replace the temporary branch of the add dialog form so it includes来源切换. Structure:
|
||||
|
||||
```vue
|
||||
<div v-if="addMode === 'temporary'" class="form-grid-2">
|
||||
<el-form-item v-if="editingId" label="工单号" class="form-col-span-2">
|
||||
<el-input :model-value="editingWorkOrderNo" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="来源类型" prop="targetSource" class="form-col-span-2">
|
||||
<el-radio-group v-model="addForm.targetSource" @change="onTargetSourceChange">
|
||||
<el-radio-button value="cell_product">系统细胞产品</el-radio-button>
|
||||
<el-radio-button value="external">外部科研编号</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="addForm.targetSource === 'cell_product'"
|
||||
label="质检对象"
|
||||
prop="cellProductId"
|
||||
class="form-col-span-2"
|
||||
>
|
||||
<!-- existing el-select remote unchanged -->
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-else
|
||||
label="质检对象"
|
||||
prop="externalTargetCode"
|
||||
class="form-col-span-2"
|
||||
>
|
||||
<el-input
|
||||
v-model="addForm.externalTargetCode"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
placeholder="请输入外部编号"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- remark unchanged -->
|
||||
</div>
|
||||
```
|
||||
|
||||
(若项目 Element Plus 版本 `el-radio-button` 用 `label` 而非 `value`,与仓库其它页保持一致。)
|
||||
|
||||
- [ ] **Step 2: Script — form state & rules**
|
||||
|
||||
```typescript
|
||||
const addForm = reactive({
|
||||
sampleCode: '',
|
||||
receptionSource: 'cell_storage',
|
||||
qcBizType: 'inbound',
|
||||
targetSource: 'cell_product' as 'cell_product' | 'external',
|
||||
cellProductId: '',
|
||||
externalTargetCode: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const addRules = computed(() => {
|
||||
if (addMode.value !== 'temporary') {
|
||||
return { /* existing inbound rules */ }
|
||||
}
|
||||
if (addForm.targetSource === 'external') {
|
||||
return {
|
||||
externalTargetCode: [
|
||||
{ required: true, message: '请填写外部编号', trigger: 'blur' },
|
||||
{
|
||||
validator: (_: unknown, v: string, cb: (e?: Error) => void) => {
|
||||
if ((v || '').trim().length > 64) cb(new Error('外部编号最长 64 个字符'))
|
||||
else cb()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return {
|
||||
cellProductId: [
|
||||
{ required: true, message: '请选择质检对象(细胞产品)', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Bind `:rules="addRules"` on `el-form`(若原为静态 `addRules`,改为 computed)。
|
||||
|
||||
```typescript
|
||||
function onTargetSourceChange() {
|
||||
addForm.cellProductId = ''
|
||||
addForm.externalTargetCode = ''
|
||||
cellProductOptions.value = []
|
||||
nextTick(() => addFormRef.value?.clearValidate())
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Open handlers**
|
||||
|
||||
`handleAddTemporary`: set `targetSource = 'cell_product'`, clear both target fields, then `searchCellProductOptions('')`.
|
||||
|
||||
`handleEdit`:
|
||||
|
||||
```typescript
|
||||
const hasExternal = !!(row.externalTargetCode || '').trim()
|
||||
addForm.targetSource = hasExternal ? 'external' : 'cell_product'
|
||||
addForm.cellProductId = row.cellProductId || ''
|
||||
addForm.externalTargetCode = row.externalTargetCode || ''
|
||||
addForm.remark = row.remark || ''
|
||||
cellProductOptions.value = []
|
||||
if (addForm.targetSource === 'cell_product') {
|
||||
ensureCellProductOption(row)
|
||||
searchCellProductOptions(row.cellProductCode || row.cellProductName || '')
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: `submitAdd` temporary branches**
|
||||
|
||||
```typescript
|
||||
if (editingId.value) {
|
||||
const payload =
|
||||
addMode.value === 'temporary'
|
||||
? {
|
||||
cellProductId:
|
||||
addForm.targetSource === 'cell_product' ? addForm.cellProductId : '',
|
||||
externalTargetCode:
|
||||
addForm.targetSource === 'external'
|
||||
? addForm.externalTargetCode.trim()
|
||||
: '',
|
||||
remark: addForm.remark
|
||||
}
|
||||
: { remark: addForm.remark /* + status fields if any */ }
|
||||
await fetchUpdateQcWorkOrder(editingId.value, payload)
|
||||
...
|
||||
} else if (addMode.value === 'temporary') {
|
||||
await fetchCreateQcWorkOrder({
|
||||
workOrderType: 'temporary',
|
||||
cellProductId:
|
||||
addForm.targetSource === 'cell_product' ? addForm.cellProductId : '',
|
||||
externalTargetCode:
|
||||
addForm.targetSource === 'external' ? addForm.externalTargetCode.trim() : '',
|
||||
remark: addForm.remark
|
||||
})
|
||||
```
|
||||
|
||||
注意:备注编辑弹窗 `submitRemarkEdit` **不要**带 `cellProductId`/`externalTargetCode`。
|
||||
|
||||
- [ ] **Step 5: Manual UI check**(dev server)
|
||||
|
||||
- 临时 Tab → 新增 → 切换外部 → 填编号 → 创建成功
|
||||
- 列表能看到编号(Task 6 完成后)
|
||||
- 编辑切回来源系统 → 选产品 → 保存
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/quality-inspection/qc-work-order/index.vue
|
||||
git commit -m "feat(web-admin): temporary QC dialog source toggle for external target code"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 列表卡片 + 详情 + 任务子列表 + placeholder
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/utils/qc-work-order-card-meta.ts`
|
||||
- Modify: `web-admin/src/views/quality-inspection/qc-work-order/composables/inbound-view-sections.ts`
|
||||
- Modify: `web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue`
|
||||
- Modify: `web-admin/src/utils/qc-work-order-tabs.ts`
|
||||
|
||||
- [ ] **Step 1: Card title**
|
||||
|
||||
```typescript
|
||||
export function resolveTemporaryWorkOrderCardTitle(row: QcWorkOrder): string {
|
||||
const external = String(row.externalTargetCode || '').trim()
|
||||
if (external) return external
|
||||
return formatTempCellProductField(row, row.cellProductCode)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: `temporaryFields` — 质检对象**
|
||||
|
||||
在 `temporaryFields` 数组靠前插入(产品名称之前):
|
||||
|
||||
```typescript
|
||||
{
|
||||
label: '质检对象',
|
||||
getValue: (row) => String(row.externalTargetCode || '').trim() || '—',
|
||||
visible: (row) => !!(row.externalTargetCode || '').trim()
|
||||
},
|
||||
```
|
||||
|
||||
保留产品名称/类型字段(外部单会显示 `—`,可接受)。
|
||||
|
||||
- [ ] **Step 3: `buildTemporaryViewSections`**
|
||||
|
||||
After 工单来源 fields,分支:
|
||||
|
||||
```typescript
|
||||
const external = String(detail.externalTargetCode || '').trim()
|
||||
if (external) {
|
||||
basicFields.push(
|
||||
{ label: '质检对象', value: external },
|
||||
{ label: '对象来源', value: '外部', tagType: 'warning' }
|
||||
)
|
||||
} else {
|
||||
basicFields.push(
|
||||
{ label: '产品编码', value: detail.cellProductCode || '—' },
|
||||
{ label: '产品名称', value: detail.cellProductName || '—' },
|
||||
{ label: '产品类型', value: detail.cellProductTypeName || '—' },
|
||||
{ label: '代次', value: detail.cellProductPassage || '—' },
|
||||
{ label: '规格', value: resolveCellProductSpecDisplay(detail) || '—' }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
(其余状态/备注字段保持原顺序接在后面。)
|
||||
|
||||
- [ ] **Step 4: Task sublist 质检对象 section**
|
||||
|
||||
In `QcWorkOrderTaskSubList.vue` where `cellProductSection` is built for temp QC:
|
||||
|
||||
```typescript
|
||||
const external = String(d.externalTargetCode || '').trim()
|
||||
const cellProductSection: ViewSection[] = isTempQc
|
||||
? [
|
||||
{
|
||||
title: '质检对象',
|
||||
fields: external
|
||||
? [
|
||||
{ label: '外部编号', value: external },
|
||||
{ label: '对象来源', value: '外部', tagType: 'warning' as const }
|
||||
]
|
||||
: [
|
||||
{ label: '产品编码', value: d.cellProductCode || '—' },
|
||||
{ label: '产品名称', value: d.cellProductName || '—' },
|
||||
{ label: '产品类型', value: d.cellProductTypeName || '—' },
|
||||
{ label: '代次', value: d.cellProductPassage || '—' },
|
||||
{ label: '规格', value: resolveCellProductSpecDisplay(d) || '—' }
|
||||
]
|
||||
}
|
||||
]
|
||||
: []
|
||||
```
|
||||
|
||||
(确认 `ViewField` 支持 `tagType`;若该处 fields 不支持 tag,则对象来源用纯文案。)
|
||||
|
||||
- [ ] **Step 5: Keyword placeholder**
|
||||
|
||||
```typescript
|
||||
if (tab.key === 'temporary') return '工单号 / 外部编号'
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/utils/qc-work-order-card-meta.ts \
|
||||
web-admin/src/views/quality-inspection/qc-work-order/composables/inbound-view-sections.ts \
|
||||
web-admin/src/views/quality-inspection/qc-work-order/QcWorkOrderTaskSubList.vue \
|
||||
web-admin/src/utils/qc-work-order-tabs.ts
|
||||
git commit -m "feat(web-admin): show external target code on temporary QC list and detail"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 图谱更新 + 验收
|
||||
|
||||
- [ ] **Step 1: 执行库表变更**(本地/测试库)
|
||||
|
||||
```bash
|
||||
# 使用你们现有方式执行 docs/sql/2026-08-20-qc-work-order-external-target-code.sql
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 手工验收清单**
|
||||
|
||||
1. 系统产品路径:新建临时工单 → 列表标题为产品编码 → 详情产品字段正常
|
||||
2. 外部编号路径:新建 → 列表标题为外部编号 → 详情显示质检对象 +「外部」
|
||||
3. 同一外部编号建两单均成功
|
||||
4. 编辑:外部 ↔ 系统切换后保存,对侧字段清空
|
||||
5. 备注编辑不丢质检对象
|
||||
6. keyword 搜外部编号能命中
|
||||
7. 生产派发临时质检(若有现成数据)行为不变
|
||||
|
||||
- [ ] **Step 3: Update graph**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh all
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit graph if changed**
|
||||
|
||||
```bash
|
||||
git add backend/graphify-out web-admin/graphify-out
|
||||
git commit -m "chore: update graphify after temporary QC external target code"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage self-check
|
||||
|
||||
| Spec 要求 | Task |
|
||||
|-----------|------|
|
||||
| `external_target_code` 字段 | 1, 2 |
|
||||
| 互斥校验 / 不唯一 / ≤64 | 2, 3, 5 |
|
||||
| Create/Update API | 3, 4, 5 |
|
||||
| 方案 A 弹窗 | 5 |
|
||||
| 列表标题 / meta / 详情 / 任务侧 | 6 |
|
||||
| keyword | 3, 6 |
|
||||
| 生产派发不动 | (无 Task 改派发代码) |
|
||||
| 不做伪细胞产品 / 不借 sample_code | (未引入) |
|
||||
|
||||
## Placeholder scan
|
||||
|
||||
无 TBD/TODO;函数名统一 `resolve_temp_qc_target` / `externalTargetCode` / `external_target_code`。
|
||||
Reference in new issue
Block a user