|
|
@@ -0,0 +1,426 @@
|
|
|
+# 生产接收:接种量文本化、冻存时间仅日期 — 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:** 将生产样本接收的「接种量」改为最长 255 的自由文本,将「冻存时间」改为仅日期(选择/展示 `YYYY-MM-DD`)。
|
|
|
+
|
|
|
+**Architecture:** 库表 `inoculation_amount` 由 `decimal(18,4)` 改为 `VARCHAR(255)`;前后端按字符串读写。冻存时间列保持 `datetime`,前端 `kind: 'date'` + `el-date-picker type="date"`,展示用 `formatDateOnly`;后端继续 `parse_datetime` 兼容仅日期字符串。弹窗改动集中在共用 `SampleReceptionDialog.vue`。
|
|
|
+
|
|
|
+**Tech Stack:** Vue 3 + Element Plus(web-admin)、FastAPI + SQLAlchemy(backend)、MySQL 5.7、node:test(前端纯函数单测)
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-08-31-production-reception-inoculation-cryo-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File map
|
|
|
+
|
|
|
+| File | Responsibility |
|
|
|
+|------|----------------|
|
|
|
+| `docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql` | 线上/本地 `ALTER` 迁移 |
|
|
|
+| `docs/sql/zkxw.sql` | 基线 CREATE 中列类型同步 |
|
|
|
+| `backend/app/models/models.py` | ORM 列类型 |
|
|
|
+| `backend/app/api/sample_production_reception.py` | Pydantic body 类型 |
|
|
|
+| `backend/app/crud/sample_production_reception.py` | 读写作字符串 |
|
|
|
+| `web-admin/src/utils/sample-production-reception-fields.ts` | 字段 kind / 类型 / 展示 |
|
|
|
+| `web-admin/src/utils/sample-production-reception-fields.test.ts` | 字段定义与展示单测 |
|
|
|
+| `web-admin/src/api/sample-production-reception.ts` | Payload TS 类型 |
|
|
|
+| `web-admin/src/views/production/production-work-order/SampleReceptionDialog.vue` | 表单控件与回填 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 前端字段定义 + 展示(TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `web-admin/src/utils/sample-production-reception-fields.test.ts`
|
|
|
+- Modify: `web-admin/src/utils/sample-production-reception-fields.ts`
|
|
|
+
|
|
|
+- [ ] **Step 1: Write the failing test**
|
|
|
+
|
|
|
+```ts
|
|
|
+import { describe, it } from 'node:test'
|
|
|
+import assert from 'node:assert/strict'
|
|
|
+import {
|
|
|
+ resolveProductionReceptionFields,
|
|
|
+ formatProductionFieldValue,
|
|
|
+ type ProductionReceptionRecord
|
|
|
+} from './sample-production-reception-fields'
|
|
|
+
|
|
|
+describe('inoculationAmount / cryopreservationTime field kinds', () => {
|
|
|
+ it('marks inoculationAmount as text and cryopreservationTime as date for UC-MSC profiles', () => {
|
|
|
+ for (const source of ['cell_storage', 'research_donation'] as const) {
|
|
|
+ const fields = resolveProductionReceptionFields(source, '脐带间充质干细胞')
|
|
|
+ const inoc = fields.find((f) => f.key === 'inoculationAmount')
|
|
|
+ const cryo = fields.find((f) => f.key === 'cryopreservationTime')
|
|
|
+ assert.ok(inoc)
|
|
|
+ assert.equal(inoc.kind, 'text')
|
|
|
+ assert.equal(inoc.maxLength, 255)
|
|
|
+ assert.ok(cryo)
|
|
|
+ assert.equal(cryo.kind, 'date')
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ it('formats free-text inoculation and date-only cryopreservation', () => {
|
|
|
+ const fields = resolveProductionReceptionFields('cell_storage', '脐带间充质干细胞')
|
|
|
+ const inoc = fields.find((f) => f.key === 'inoculationAmount')!
|
|
|
+ const cryo = fields.find((f) => f.key === 'cryopreservationTime')!
|
|
|
+ const record: ProductionReceptionRecord = {
|
|
|
+ inoculationAmount: '约 5×10⁶ cells,接种体积 2ml',
|
|
|
+ cryopreservationTime: '2026-07-14 15:30:00'
|
|
|
+ }
|
|
|
+ assert.equal(formatProductionFieldValue(inoc, record), '约 5×10⁶ cells,接种体积 2ml')
|
|
|
+ assert.equal(formatProductionFieldValue(cryo, record), '2026-07-14')
|
|
|
+ })
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Run test to verify it fails**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && node --import tsx --test src/utils/sample-production-reception-fields.test.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(`kind` 仍为 `number`/`datetime`,或 `maxLength` / `date` 分支不存在)
|
|
|
+
|
|
|
+- [ ] **Step 3: Update field definitions and formatter**
|
|
|
+
|
|
|
+In `web-admin/src/utils/sample-production-reception-fields.ts`:
|
|
|
+
|
|
|
+1. Extend kind union:
|
|
|
+
|
|
|
+```ts
|
|
|
+kind: 'number' | 'text' | 'datetime' | 'date' | 'select' | 'integer'
|
|
|
+maxLength?: number
|
|
|
+```
|
|
|
+
|
|
|
+2. Change defs:
|
|
|
+
|
|
|
+```ts
|
|
|
+inoculationAmount: {
|
|
|
+ key: 'inoculationAmount',
|
|
|
+ label: '接种量',
|
|
|
+ kind: 'text',
|
|
|
+ maxLength: 255
|
|
|
+},
|
|
|
+cryopreservationTime: {
|
|
|
+ key: 'cryopreservationTime',
|
|
|
+ label: '冻存时间',
|
|
|
+ kind: 'date'
|
|
|
+},
|
|
|
+```
|
|
|
+
|
|
|
+3. Change record type:
|
|
|
+
|
|
|
+```ts
|
|
|
+inoculationAmount?: string
|
|
|
+```
|
|
|
+
|
|
|
+4. In `formatProductionFieldValue`, after infectious / before or with datetime:
|
|
|
+
|
|
|
+```ts
|
|
|
+if (def.kind === 'datetime') {
|
|
|
+ return formatDateTimeDisplay(String(raw)) || '—'
|
|
|
+}
|
|
|
+if (def.kind === 'date') {
|
|
|
+ return formatDateOnly(String(raw)) || '—'
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+(`formatDateOnly` 已在本文件顶部从 `@/utils/datetime-display` 导入。)
|
|
|
+
|
|
|
+- [ ] **Step 4: Run test to verify it passes**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && node --import tsx --test src/utils/sample-production-reception-fields.test.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/utils/sample-production-reception-fields.ts \
|
|
|
+ web-admin/src/utils/sample-production-reception-fields.test.ts
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): 生产接收接种量改文本、冻存时间改日期展示
|
|
|
+
|
|
|
+字段 kind 与只读格式对齐产品:自由文本接种量、冻存仅 YYYY-MM-DD。
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 弹窗表单与 API 类型
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/api/sample-production-reception.ts`
|
|
|
+- Modify: `web-admin/src/views/production/production-work-order/SampleReceptionDialog.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: Update API payload type**
|
|
|
+
|
|
|
+In `web-admin/src/api/sample-production-reception.ts`:
|
|
|
+
|
|
|
+```ts
|
|
|
+inoculationAmount?: string
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Update SampleReceptionDialog template**
|
|
|
+
|
|
|
+Replace the text / datetime branches so text supports `maxLength`,并增加 `date`:
|
|
|
+
|
|
|
+```vue
|
|
|
+<el-input
|
|
|
+ v-if="field.kind === 'text'"
|
|
|
+ v-model="prodForm[field.key]"
|
|
|
+ clearable
|
|
|
+ :maxlength="field.maxLength"
|
|
|
+ :show-word-limit="!!field.maxLength"
|
|
|
+/>
|
|
|
+<DecimalInput
|
|
|
+ v-else-if="field.kind === 'number' || field.kind === 'integer'"
|
|
|
+ v-model="prodForm[field.key]"
|
|
|
+ :min="0"
|
|
|
+ :precision="field.kind === 'integer' ? 0 : 4"
|
|
|
+ style="width: 100%"
|
|
|
+/>
|
|
|
+<el-date-picker
|
|
|
+ v-else-if="field.kind === 'date'"
|
|
|
+ v-model="prodForm[field.key]"
|
|
|
+ type="date"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ placeholder="选择日期"
|
|
|
+ style="width: 100%"
|
|
|
+/>
|
|
|
+<el-date-picker
|
|
|
+ v-else-if="field.kind === 'datetime'"
|
|
|
+ v-model="prodForm[field.key]"
|
|
|
+ type="datetime"
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
+ placeholder="选择时间"
|
|
|
+ style="width: 100%"
|
|
|
+/>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Fix populateFromProduction for date fields**
|
|
|
+
|
|
|
+In `populateFromProduction`,对 `date`(及处理时间同类)回填截日期:
|
|
|
+
|
|
|
+```ts
|
|
|
+function populateFromProduction(rec: ProductionReceptionRecord) {
|
|
|
+ prodForm.processTime = formatProcessTimeDate(rec.processTime) || todayDate()
|
|
|
+ prodForm.handlerUserId = rec.handlerUserId || ''
|
|
|
+ prodForm.handlerName = rec.handlerName || ''
|
|
|
+ prodForm.remark = rec.remark || ''
|
|
|
+ for (const field of productionFieldDefs.value) {
|
|
|
+ const raw = rec[field.key as keyof ProductionReceptionRecord]
|
|
|
+ if (field.kind === 'date') {
|
|
|
+ prodForm[field.key] = formatProcessTimeDate(raw != null ? String(raw) : '') || ''
|
|
|
+ } else {
|
|
|
+ prodForm[field.key] = raw as never
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+(`formatProcessTimeDate` 即 `formatDateOnly` 封装,已导入本文件。)
|
|
|
+
|
|
|
+`resetProdForm` 中 `date` 与 `text` 一样置 `''`(现有 `integer || number ? undefined : ''` 已覆盖)。
|
|
|
+
|
|
|
+- [ ] **Step 4: Smoke-check TypeScript on touched files(optional local)**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -n 40
|
|
|
+```
|
|
|
+
|
|
|
+若环境慢可跳过;至少确认无新增明显类型错误。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/api/sample-production-reception.ts \
|
|
|
+ web-admin/src/views/production/production-work-order/SampleReceptionDialog.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): 样本接收弹窗接种量文本输入、冻存仅选日期
|
|
|
+
|
|
|
+共用 SampleReceptionDialog,生产工单与我的生产任务一并生效。
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 后端模型 / API / CRUD + SQL
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql`
|
|
|
+- Modify: `docs/sql/zkxw.sql`(CREATE 中 `inoculation_amount` 一行)
|
|
|
+- Modify: `backend/app/models/models.py`
|
|
|
+- Modify: `backend/app/api/sample_production_reception.py`
|
|
|
+- Modify: `backend/app/crud/sample_production_reception.py`
|
|
|
+
|
|
|
+- [ ] **Step 1: Add migration SQL**
|
|
|
+
|
|
|
+Create `docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql`:
|
|
|
+
|
|
|
+```sql
|
|
|
+-- 生产接收接种量:数值 → 自由文本(最长 255)
|
|
|
+-- 执行前请备份;MySQL 会将既有 decimal 值转为对应数字字符串。
|
|
|
+
|
|
|
+ALTER TABLE biz_sample_production_reception
|
|
|
+ MODIFY COLUMN inoculation_amount VARCHAR(255) NULL COMMENT '接种量';
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Sync baseline zkxw.sql CREATE column**
|
|
|
+
|
|
|
+In `docs/sql/zkxw.sql`,将:
|
|
|
+
|
|
|
+```sql
|
|
|
+`inoculation_amount` decimal(18,4) DEFAULT NULL COMMENT '接种量',
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+
|
|
|
+```sql
|
|
|
+`inoculation_amount` varchar(255) DEFAULT NULL COMMENT '接种量',
|
|
|
+```
|
|
|
+
|
|
|
+(仅改 CREATE 定义行;不必改历史 INSERT 中的 NULL/数值字面量。)
|
|
|
+
|
|
|
+- [ ] **Step 3: Update SQLAlchemy model**
|
|
|
+
|
|
|
+In `backend/app/models/models.py` `BizSampleProductionReception`:
|
|
|
+
|
|
|
+```python
|
|
|
+inoculation_amount = Column(String(255), nullable=True, comment="接种量")
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Update API body type**
|
|
|
+
|
|
|
+In `backend/app/api/sample_production_reception.py` `ProductionReceptionBody`:
|
|
|
+
|
|
|
+```python
|
|
|
+inoculation_amount: Optional[str] = Field(None, alias="inoculationAmount")
|
|
|
+```
|
|
|
+
|
|
|
+`_body_to_payload` 保持:
|
|
|
+
|
|
|
+```python
|
|
|
+"inoculationAmount": body.inoculation_amount,
|
|
|
+```
|
|
|
+
|
|
|
+(可选:写入前 `strip`;若在 CRUD 统一 strip 亦可。)
|
|
|
+
|
|
|
+`cryopreservation_time` 仍 `parse_datetime`——`YYYY-MM-DD` 经 `fromisoformat` 可得当日 0 点,无需改解析。
|
|
|
+
|
|
|
+- [ ] **Step 5: Update CRUD read/write**
|
|
|
+
|
|
|
+In `backend/app/crud/sample_production_reception.py` `_row_to_dict`:
|
|
|
+
|
|
|
+```python
|
|
|
+"inoculationAmount": row.inoculation_amount or "",
|
|
|
+```
|
|
|
+
|
|
|
+In `_apply_payload_to_row`:
|
|
|
+
|
|
|
+```python
|
|
|
+row.inoculation_amount = (
|
|
|
+ (str(data.get("inoculationAmount") or "").strip() or None)
|
|
|
+ if "inoculationAmount" in allowed
|
|
|
+ else None
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 6: Apply migration on local DB(if available)**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 按本地习惯执行 ALTER;示例(需正确库连接):
|
|
|
+# mysql ... < docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql
|
|
|
+```
|
|
|
+
|
|
|
+若本机无库,跳过并在验收说明中注明「需执行迁移」。
|
|
|
+
|
|
|
+- [ ] **Step 7: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql \
|
|
|
+ docs/sql/zkxw.sql \
|
|
|
+ backend/app/models/models.py \
|
|
|
+ backend/app/api/sample_production_reception.py \
|
|
|
+ backend/app/crud/sample_production_reception.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(backend): 生产接收接种量改为 VARCHAR(255) 自由文本
|
|
|
+
|
|
|
+同步模型、API、CRUD 与 SQL 迁移/基线,支持数字与说明文案组合录入。
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 图谱更新与验收
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- 可能更新:`backend/graphify-out/*`、`web-admin/graphify-out/*`(由脚本生成)
|
|
|
+
|
|
|
+- [ ] **Step 1: Update knowledge graph**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh all
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Re-run frontend unit test**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd web-admin && node --import tsx --test src/utils/sample-production-reception-fields.test.ts
|
|
|
+```
|
|
|
+
|
|
|
+Expected: PASS
|
|
|
+
|
|
|
+- [ ] **Step 3: Manual acceptance checklist**
|
|
|
+
|
|
|
+1. 脐带间充质干细胞:接种量可输入含中文/符号的长文案(≤255),保存回显一致。
|
|
|
+2. 冻存时间选择器无时分秒;保存后只读与再编辑均为 `YYYY-MM-DD`。
|
|
|
+3. 历史冻存带时分秒数据打开弹窗可回填日期部分。
|
|
|
+4. 生产工单与我的生产任务行为一致。
|
|
|
+5. 围产期造血干细胞 profile 数值字段仍为 `DecimalInput`。
|
|
|
+
|
|
|
+- [ ] **Step 4: Mark design implemented**
|
|
|
+
|
|
|
+Update header in `docs/superpowers/specs/2026-08-31-production-reception-inoculation-cryo-design.md`:
|
|
|
+
|
|
|
+```markdown
|
|
|
+> 状态:已实现
|
|
|
+> 完成日期:2026-08-31
|
|
|
+> 实现计划:`docs/superpowers/plans/2026-08-31-production-reception-inoculation-cryo.md`
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit remaining docs/graph if dirty**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add docs/superpowers/specs/2026-08-31-production-reception-inoculation-cryo-design.md \
|
|
|
+ docs/superpowers/plans/2026-08-31-production-reception-inoculation-cryo.md \
|
|
|
+ backend/graphify-out web-admin/graphify-out 2>/dev/null || true
|
|
|
+git status
|
|
|
+# 若有变更:
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+docs: 标记接种量/冻存时间改造已实现并更新图谱
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage check
|
|
|
+
|
|
|
+| Spec 项 | Task |
|
|
|
+|---------|------|
|
|
|
+| 接种量 VARCHAR(255) + 文本框 | Task 1–3 |
|
|
|
+| 冻存仅日期、列仍 datetime | Task 1–2(展示/表单)、Task 3(解析不变) |
|
|
|
+| 共用弹窗两页生效 | Task 2 |
|
|
|
+| SQL 迁移 + zkxw 基线 | Task 3 |
|
|
|
+| 验收场景 | Task 4 |
|
|
|
+
|
|
|
+## Placeholder scan
|
|
|
+
|
|
|
+无 TBD /「类似 Task N」占位;类型名与 `inoculationAmount` / `cryopreservationTime` 全计划一致。
|