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: 后端在现有 Model/Schema/CRUD/API 层增量修改;前端全面重写 cell-product/index.vue 页面(表格列、搜索栏、新增/编辑弹窗、查看弹窗)。编码生成采用实时自增模式(参考 _generate_work_order_no)。
Tech Stack: Python 3.9 + FastAPI + SQLAlchemy + Pydantic v2(后端);Vue 3 + TypeScript + Element Plus + Tailwind CSS 4(前端)
Design Spec: docs/superpowers/specs/2026-05-21-cell-product-library-design.md
| 操作 | 文件路径 | 职责 |
|---|---|---|
| 新增 | docs/sql/2026-05-21-cell-product-alter.sql |
数据库迁移:新增 4 字段 + 更新状态默认值 |
| 修改 | backend/app/models/models.py (BizCellProduct 类, L2122-2149) |
新增 4 个 Column,更新 product_status 默认值 |
| 修改 | backend/app/schemas/cell_product.py |
Create/Update schema 新增字段 + 状态校验 |
| 修改 | backend/app/crud/cell_product.py |
_row_to_dict 新增映射 + create/update 新增字段 + 新增 get_next_code |
| 修改 | backend/app/api/production.py (L1550-1632) |
新增 next-code 端点 + update 端点增加状态校验 |
| 修改 | web-admin/src/api/cell-product.ts |
新增 fetchNextCellProductCode 接口 |
| 重写 | web-admin/src/views/production/cell-product/index.vue |
表格列 + 搜索 + 弹窗 + 查看弹窗 全面重写 |
Files:
Create: docs/sql/2026-05-21-cell-product-alter.sql
[ ] Step 1: 编写 ALTER TABLE SQL
-- 细胞产品库字段重设计迁移
-- 日期: 2026-05-21
-- 1. 新增 4 个业务字段(放在 storage_slot_id 之后,product_status 之前)
ALTER TABLE biz_cell_product
ADD COLUMN `passage` VARCHAR(8) DEFAULT NULL COMMENT '代次(P1~P8)' AFTER `storage_slot_id`,
ADD COLUMN `freeze_date` DATETIME DEFAULT NULL COMMENT '冻存日期' AFTER `passage`,
ADD COLUMN `expected_cell_count` DECIMAL(18,4) DEFAULT NULL COMMENT '预计细胞数量(单位:亿)' AFTER `freeze_date`,
ADD COLUMN `viability` DECIMAL(5,2) DEFAULT NULL COMMENT '活性(百分比,0~100)' AFTER `expected_cell_count`;
-- 2. 更新 product_status 默认值和注释
ALTER TABLE biz_cell_product
MODIFY COLUMN `product_status` VARCHAR(16) DEFAULT 'frozen' COMMENT '状态: frozen已冻存/sold已售卖/used已使用';
-- 3. 迁移旧状态数据(如有)
UPDATE biz_cell_product SET product_status = 'frozen' WHERE product_status = 'zk';
UPDATE biz_cell_product SET product_status = 'sold' WHERE product_status = 'yc';
UPDATE biz_cell_product SET product_status = 'used' WHERE product_status = 'ysy';
[ ] Step 2: 在数据库执行 SQL(由 DBA 或手动执行)
[ ] Step 3: 提交
git add docs/sql/2026-05-21-cell-product-alter.sql
git commit -m "feat(cell-product): add migration SQL for new fields and status enum"
Files:
Modify: backend/app/models/models.py (BizCellProduct 类, 约第 2122-2149 行)
[ ] Step 1: 在 BizCellProduct 类中新增 4 个 Column,更新 product_status 默认值
在 storage_slot_id 行之后、product_status 行之前插入新字段,同时更新 product_status 默认值:
# 在 storage_slot_id 行之后新增:
passage = Column(String(8), nullable=True, comment="代次(P1~P8)")
freeze_date = Column(DateTime, nullable=True, comment="冻存日期")
expected_cell_count = Column(Numeric(18, 4), nullable=True, comment="预计细胞数量(单位:亿)")
viability = Column(Numeric(5, 2), nullable=True, comment="活性(百分比,0~100)")
# 修改 product_status 行:
product_status = Column(String(16), nullable=True, default="frozen", comment="产品状态 frozen已冻存/sold已售卖/used已使用")
Run: cd backend && source venv/bin/activate && python -c "from app.models.models import BizCellProduct; print('OK')"
Expected: OK
git add backend/app/models/models.py
git commit -m "feat(cell-product): add passage, freeze_date, expected_cell_count, viability columns to model"
Files:
Modify: backend/app/schemas/cell_product.py
[ ] Step 1: 更新 CellProductCreate、CellProductUpdate、CellProductListQuery
完整替换 backend/app/schemas/cell_product.py:
from pydantic import BaseModel, field_validator
from typing import Optional
from datetime import datetime
class CellProductCreate(BaseModel):
product_code: str
product_name: str
product_type_id: Optional[str] = None
spec_id: Optional[str] = None
batch_no: Optional[str] = None
storage_slot_id: Optional[str] = None
passage: Optional[str] = None
freeze_date: Optional[datetime] = None
expected_cell_count: Optional[float] = None
viability: Optional[float] = None
product_status: Optional[str] = 'frozen'
source_type: Optional[str] = 'manual'
quantity: Optional[float] = None
unit: Optional[str] = None
expiry_date: Optional[datetime] = None
product_images: Optional[str] = None
remark: Optional[str] = None
@field_validator('viability')
@classmethod
def validate_viability(cls, v):
if v is not None and (v < 0 or v > 100):
raise ValueError('活性必须在 0~100 之间')
return v
@field_validator('passage')
@classmethod
def validate_passage(cls, v):
if v is not None:
valid = {f'P{i}' for i in range(1, 9)}
if v.upper() not in valid:
raise ValueError('代次必须为 P1~P8')
return v.upper()
return v
class CellProductUpdate(BaseModel):
product_name: Optional[str] = None
product_type_id: Optional[str] = None
spec_id: Optional[str] = None
batch_no: Optional[str] = None
storage_slot_id: Optional[str] = None
passage: Optional[str] = None
freeze_date: Optional[datetime] = None
expected_cell_count: Optional[float] = None
viability: Optional[float] = None
product_status: Optional[str] = None
quantity: Optional[float] = None
unit: Optional[str] = None
expiry_date: Optional[datetime] = None
product_images: Optional[str] = None
remark: Optional[str] = None
@field_validator('viability')
@classmethod
def validate_viability(cls, v):
if v is not None and (v < 0 or v > 100):
raise ValueError('活性必须在 0~100 之间')
return v
@field_validator('passage')
@classmethod
def validate_passage(cls, v):
if v is not None:
valid = {f'P{i}' for i in range(1, 9)}
if v.upper() not in valid:
raise ValueError('代次必须为 P1~P8')
return v.upper()
return v
@field_validator('product_status')
@classmethod
def validate_status(cls, v):
if v is not None and v not in ('frozen', 'sold', 'used'):
raise ValueError('状态必须为 frozen/sold/used')
return v
class CellProductListQuery(BaseModel):
page: int = 1
page_size: int = 20
product_code: Optional[str] = None
product_name: Optional[str] = None
product_type_id: Optional[str] = None
product_status: Optional[str] = None
passage: Optional[str] = None
Run: cd backend && source venv/bin/activate && python -c "from app.schemas.cell_product import CellProductCreate, CellProductUpdate; print('OK')"
Expected: OK
git add backend/app/schemas/cell_product.py
git commit -m "feat(cell-product): update schemas with new fields, status validation, and passage validator"
Files:
Modify: backend/app/crud/cell_product.py
[ ] Step 1: 更新 _row_to_dict 新增 4 个字段映射
在 _row_to_dict 函数中,在 "storageSlotId" 之后新增:
"passage": c.passage or "",
"freezeDate": c.freeze_date.strftime("%Y-%m-%d %H:%M:%S") if c.freeze_date else "",
"expectedCellCount": float(c.expected_cell_count) if c.expected_cell_count is not None else None,
"viability": float(c.viability) if c.viability is not None else None,
create_cell_product 新增字段写入在 create_cell_product 函数的 BizCellProduct(...) 构造中,在 storage_slot_id= 之后新增:
passage=(data.passage or "").strip() or None if hasattr(data, "passage") and data.passage else None,
freeze_date=data.freeze_date if hasattr(data, "freeze_date") else None,
expected_cell_count=data.expected_cell_count if hasattr(data, "expected_cell_count") else None,
viability=data.viability if hasattr(data, "viability") else None,
同时更新 product_status 默认值:
product_status=data.product_status or "frozen",
update_cell_product 新增字段处理 + 状态校验在 update_cell_product 函数中,在 storage_slot_id 处理之后新增:
if hasattr(data, "passage") and data.passage is not None:
obj.passage = (data.passage or "").strip() or None
if hasattr(data, "freeze_date") and data.freeze_date is not None:
obj.freeze_date = data.freeze_date
if hasattr(data, "expected_cell_count") and data.expected_cell_count is not None:
obj.expected_cell_count = data.expected_cell_count
if hasattr(data, "viability") and data.viability is not None:
obj.viability = data.viability
在 product_status 处理处增加状态流转校验:
if data.product_status is not None and data.product_status != obj.product_status:
# 状态流转校验:只允许 frozen → sold/used,不可回退,不可互转
allowed = {"frozen": {"sold", "used"}}
from_status = obj.product_status
to_status = data.product_status
if from_status not in allowed or to_status not in allowed[from_status]:
raise ValueError(f"不允许从状态 {from_status} 变更为 {to_status}")
obj.product_status = data.product_status
get_next_code 方法在文件末尾 total_pages 函数之前新增:
def get_next_code(db: Session, tenant_no: str) -> str:
"""生成下一个产品编码: CP + YYYYMMDD + 6位序列号"""
today_str = datetime.now().strftime("%Y%m%d")
prefix = f"CP{today_str}"
last = (
db.query(BizCellProduct)
.filter(
BizCellProduct.tenant_no == tenant_no,
BizCellProduct.product_code.like(f"{prefix}%"),
BizCellProduct.del_flag == 0,
)
.order_by(BizCellProduct.product_code.desc())
.first()
)
seq = 1
if last and last.product_code:
try:
seq = int(last.product_code[-6:]) + 1
except (ValueError, IndexError):
seq = 1
return f"{prefix}{seq:06d}"
list_cell_products 支持 passage 筛选在 list_cell_products 函数签名中新增 passage 参数,在筛选逻辑中新增:
passage: Optional[str] = None,
在 product_status 筛选之后新增:
if passage:
q0 = q0.filter(BizCellProduct.passage == passage)
Run: cd backend && source venv/bin/activate && python -c "from app.crud.cell_product import get_next_code, list_cell_products; print('OK')"
Expected: OK
git add backend/app/crud/cell_product.py
git commit -m "feat(cell-product): update CRUD with new fields, status validation, next-code generation"
Files:
Modify: backend/app/api/production.py (约第 1550-1632 行)
[ ] Step 1: 新增 GET /cell-products/next-code 端点
在 list_cell_products 端点(@router.get("/cell-products"))之前新增:
@router.get("/cell-products/next-code")
async def get_next_cell_product_code(
user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _get_tenant_no(db, user)
code = cell_product_svc.get_next_code(db, tenant_no)
return success_response({"code": code})
重要: 此端点必须放在 /cell-products 之前(FastAPI 路由匹配顺序),否则 {product_id} 会匹配到 next-code。
list_cell_products 端点新增 passage 参数在端点签名中新增:
passage: Optional[str] = None,
在调用 cell_product_svc.list_cell_products 时新增参数:
passage=passage,
Run: cd backend && source venv/bin/activate && python -c "from app.api.production import router; print('OK')"
Expected: OK
git add backend/app/api/production.py
git commit -m "feat(cell-product): add next-code endpoint and passage filter to list API"
Files:
Modify: web-admin/src/api/cell-product.ts
[ ] Step 1: 新增 fetchNextCellProductCode 接口
在文件末尾新增:
export function fetchNextCellProductCode() {
return request.get<{ code: string }>({ url: '/web/admin/cell-products/next-code' })
}
git add web-admin/src/api/cell-product.ts
git commit -m "feat(cell-product): add fetchNextCellProductCode API"
Files:
web-admin/src/views/production/cell-product/index.vue参考模板: src/views/system/order/index.vue(复杂列表模式)
参考组件: src/views/cell-storage/protocol/ProtocolFormDialog.vue(附件上传卡片样式)
下拉数据来源:
fetchCellProductTypeList from @/api/adminfetchCellProductSpecList from @/api/adminfetchStorageSlotList from @/api/admin代次: 前端硬编码 P1~P8
[ ] Step 1: 重写 <template> 部分
主要改动点:
搜索栏状态选项更新:
<el-option label="已冻存" value="frozen" />
<el-option label="已售卖" value="sold" />
<el-option label="已使用" value="used" />
新增代次搜索筛选项:
<el-form-item label="代次">
<el-select v-model="searchForm.passage" placeholder="全部" clearable style="width: 100%">
<el-option v-for="i in 8" :key="i" :label="`P${i}`" :value="`P${i}`" />
</el-select>
</el-form-item>
表格新增列(插在批次号之后、储物格位置之后):
<el-table-column prop="passage" label="代次" width="80" align="center" />
<el-table-column prop="expectedCellCount" label="预计细胞数量(亿)" min-width="140" align="center">
<template #default="{ row }">
{{ row.expectedCellCount ?? '—' }}
</template>
</el-table-column>
<el-table-column prop="viability" label="活性(%)" width="90" align="center">
<template #default="{ row }">
{{ row.viability != null ? row.viability : '—' }}
</template>
</el-table-column>
状态 Tag 映射更新:
function productStatusTag(status: string) {
const map: Record<string, { label: string; type: string }> = {
frozen: { label: '已冻存', type: 'success' },
sold: { label: '已售卖', type: 'warning' },
used: { label: '已使用', type: 'info' },
}
return map[status] || { label: status, type: 'info' }
}
表单新增字段(在储物格之后插入新行):
<el-form-item label="代次" prop="passage">
<el-select v-model="formData.passage" placeholder="请选择代次" clearable style="width: 100%">
<el-option v-for="i in 8" :key="i" :label="`P${i}`" :value="`P${i}`" />
</el-select>
</el-form-item>
<el-form-item label="预计细胞数量(亿)" prop="expectedCellCount">
<el-input-number v-model="formData.expectedCellCount" :min="0" :precision="4" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="活性(%)" prop="viability">
<el-input-number v-model="formData.viability" :min="0" :max="100" :precision="1" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="冻存日期" prop="freezeDate">
<el-date-picker v-model="formData.freezeDate" type="date" placeholder="请选择冻存日期" value-format="YYYY-MM-DD" style="width: 100%" />
</el-form-item>
状态字段(编辑时):
<el-form-item v-if="isEdit" label="状态" prop="productStatus">
<el-select v-model="formData.productStatus" :disabled="formData.productStatus !== 'frozen'" style="width: 100%">
<el-option label="已冻存" value="frozen" :disabled="true" />
<el-option label="已售卖" value="sold" />
<el-option label="已使用" value="used" />
</el-select>
</el-form-item>
产品编码字段(新增时可编辑,编辑时只读):
<el-form-item label="产品编码" prop="productCode">
<el-input v-model="formData.productCode" placeholder="自动生成,可修改" :readonly="isEdit" />
</el-form-item>
产品图片改为上传方式(参考 ProtocolFormDialog 附件上传卡片样式):
<el-form-item label="产品图片" class="form-col-span-2">
<div class="image-upload">
<div class="image-list">
<el-upload
ref="imageUploadRef"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*"
@change="handleImageChange"
>
<div class="upload-card">
<el-icon :size="28" color="#10b981"><Plus /></el-icon>
<span class="upload-card-text">上传图片</span>
</div>
</el-upload>
<div
v-for="(img, index) in imageList"
:key="img.id || index"
class="image-item"
>
<div class="image-preview" @click="handlePreviewImage(img)">
<el-image :src="img.fileUrl" fit="cover" class="preview-img" />
</div>
<div class="image-footer">
<span class="image-name">{{ img.fileName }}</span>
<div class="image-actions">
<el-icon class="action-icon" @click="handleDeleteImage(index)"><Close /></el-icon>
</div>
</div>
</div>
</div>
</div>
</el-form-item>
<script setup> 部分主要改动点:
fetchNextCellProductCode from @/api/cell-product;fetchCellProductTypeList, fetchCellProductSpecList, fetchStorageSlotList from @/api/admin;fetchUploadAttachment from @/api/businesspassage, freezeDate, expectedCellCount, viability, productStatuspassageonMounted 中调用三个下拉接口填充 productTypeOptions、specOptions、storageSlotOptionshandleAdd 中调用 fetchNextCellProductCode 填充imageList ref、handleImageChange、handleDeleteImage、handlePreviewImagehandleEdit 中填充 passage/freezeDate/expectedCellCount/viability/productStatus下拉数据加载:
async function loadOptions() {
try {
const [typeRes, specRes, slotRes] = await Promise.all([
fetchCellProductTypeList({ page: 1, pageSize: 999 }),
fetchCellProductSpecList({ page: 1, pageSize: 999 }),
fetchStorageSlotList(),
])
productTypeOptions.value = (typeRes as any)?.list || []
specOptions.value = (specRes as any)?.list || []
storageSlotOptions.value = Array.isArray(slotRes) ? slotRes : (slotRes as any)?.list || []
} catch { /* 静默 */ }
}
编码自动生成:
async function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增产品'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
// 自动生成编码
try {
const res = await fetchNextCellProductCode()
formData.productCode = res.code || ''
} catch { /* 静默 */ }
}
图片上传处理:
const imageList = ref<Array<{ id: string; fileName: string; fileUrl: string; _uploading?: boolean }>>([])
async function handleImageChange(uploadFile: any) {
const file = uploadFile.raw || uploadFile
if (!file) return
if (file.size / 1024 / 1024 > 10) {
ElMessage.error('图片大小不能超过 10MB')
return
}
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
imageList.value.push({ id: tempId, fileName: file.name, fileUrl: '', _uploading: true })
const idx = imageList.value.length - 1
try {
const fd = new FormData()
fd.append('folder', 'cell-product')
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
imageList.value[idx] = { id: res.id, fileName: res.fileName, fileUrl: res.fileUrl, _uploading: false }
} catch {
imageList.value.splice(idx, 1)
ElMessage.error('图片上传失败')
}
}
function handleDeleteImage(index: number) {
imageList.value.splice(index, 1)
}
function handlePreviewImage(img: any) {
if (img.fileUrl) window.open(img.fileUrl, '_blank')
}
// 同步图片 URL 到 formData
function syncImagesToForm() {
formData.productImages = imageList.value.map((img) => img.fileUrl).filter(Boolean).join(',')
}
// 从 formData.productImages 还原 imageList
function syncFormToImages(imagesStr: string) {
if (!imagesStr) { imageList.value = []; return }
imageList.value = imagesStr.split(',').filter(Boolean).map((url, i) => ({
id: `img_${i}`,
fileName: url.split('/').pop() || `图片${i + 1}`,
fileUrl: url,
}))
}
查看弹窗 sections 更新:
const viewSections = computed(() => {
if (!viewRow.value) return []
const row = viewRow.value
return [
{
title: '基本信息',
fields: [
{ label: '产品编码', value: row.productCode },
{ label: '产品名称', value: row.productName },
{ label: '产品类型', value: row.productTypeName || '—' },
{ label: '规格', value: row.specName || '—' },
{ label: '代次', value: row.passage || '—' },
{ label: '批次号', value: row.batchNo },
{ label: '储物格位置', value: row.storageSlotName || '—' },
{ label: '预计细胞数量(亿)', value: row.expectedCellCount ?? '—' },
{ label: '活性(%)', value: row.viability != null ? row.viability : '—' },
{ label: '数量', value: row.quantity ?? '—' },
{ label: '单位', value: row.unit || '—' },
{ label: '冻存日期', value: row.freezeDate || '—' },
{ label: '有效期', value: row.expiryDate || '—' },
{
label: '状态',
value: productStatusTag(row.productStatus).label,
tagType: productStatusTag(row.productStatus).type,
},
{ label: '备注', value: row.remark || '—', span: true },
],
},
]
})
提交时同步图片:
async function handleSubmit() {
await formRef.value?.validate?.()
submitLoading.value = true
try {
syncImagesToForm()
const payload = {
productCode: formData.productCode.trim(),
productName: formData.productName.trim(),
productTypeId: formData.productTypeId || undefined,
specId: formData.specId || undefined,
batchNo: formData.batchNo.trim(),
storageSlotId: formData.storageSlotId || undefined,
passage: formData.passage || undefined,
freezeDate: formData.freezeDate || undefined,
expectedCellCount: formData.expectedCellCount ?? undefined,
viability: formData.viability ?? undefined,
productStatus: isEdit.value ? formData.productStatus || undefined : 'frozen',
quantity: formData.quantity,
unit: formData.unit?.trim() || undefined,
expiryDate: formData.expiryDate || undefined,
productImages: formData.productImages?.trim() || undefined,
remark: formData.remark?.trim() || undefined,
}
if (isEdit.value && editingId.value) {
await fetchUpdateCellProduct(editingId.value, payload)
} else {
await fetchCreateCellProduct(payload)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
submitLoading.value = false
}
}
编辑时填充新字段(在 handleEdit 中):
formData.passage = row.passage || ''
formData.freezeDate = row.freezeDate || ''
formData.expectedCellCount = row.expectedCellCount ?? undefined
formData.viability = row.viability ?? undefined
formData.productStatus = row.productStatus || 'frozen'
syncFormToImages(row.productImages || '')
resetForm 新增字段重置:
function resetForm() {
// ...existing fields...
formData.passage = ''
formData.freezeDate = ''
formData.expectedCellCount = undefined
formData.viability = undefined
formData.productStatus = 'frozen'
imageList.value = []
editingId.value = null
}
搜索参数传递更新(在 loadData 中):
const res = await fetchCellProductList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
productStatus: searchForm.productStatus || undefined,
passage: searchForm.passage || undefined,
})
注意:需更新 fetchCellProductList 的 params 类型以支持 productStatus 和 passage。
<style> 部分(图片上传卡片样式)在现有样式基础上新增图片上传卡片样式(参考 ProtocolFormDialog):
.image-upload {
.image-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.upload-card {
width: 140px;
height: 96px;
border: 2px dashed #a7f3d0;
border-radius: 8px;
background: #f0fdf4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.2s;
&:hover {
border-color: #10b981;
}
.upload-card-text {
margin-top: 4px;
font-size: 12px;
color: #64748b;
}
}
.image-item {
width: 140px;
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
transition: border-color 0.2s;
&:hover {
border-color: #10b981;
}
}
.image-preview {
width: 140px;
height: 68px;
background: #f8fafc;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.preview-img {
width: 100%;
height: 100%;
}
}
.image-footer {
padding: 4px 8px;
display: flex;
align-items: center;
justify-content: space-between;
.image-name {
font-size: 11px;
color: #64748b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 90px;
}
.action-icon {
cursor: pointer;
color: #94a3b8;
&:hover { color: #ef4444; }
}
}
}
fetchCellProductList 参数类型修改 web-admin/src/api/cell-product.ts 中的参数类型:
export function fetchCellProductList(params?: {
page?: number
pageSize?: number
keyword?: string
productStatus?: string
passage?: string
}) {
Run: cd web-admin && pnpm build
Expected: 构建成功
git add web-admin/src/views/production/cell-product/index.vue web-admin/src/api/cell-product.ts
git commit -m "feat(cell-product): rewrite page with new fields, image upload, view dialog, and status lifecycle"
Files:
执行: ./scripts/update-graph.sh all
[ ] Step 1: 更新前后端知识图谱
Run: ./scripts/update-graph.sh all
Expected: 图谱更新成功
git add backend/graphify-out/ web-admin/graphify-out/
git commit -m "chore: update knowledge graphs after cell-product redesign"
| Spec 需求 | 对应 Task |
|---|---|
| 代次 P1~P8 下拉 | Task 2(Model) + Task 3(Schema) + Task 7(Frontend) |
| 冻存日期 | Task 2 + Task 3 + Task 7 |
| 预计细胞数量(亿) | Task 2 + Task 3 + Task 7 |
| 活性(%) 0~100 | Task 2 + Task 3(Schema validator) + Task 7 |
| 状态 frozen/sold/used | Task 1(SQL) + Task 2(Model) + Task 3(Schema) + Task 4(CRUD validation) + Task 7(Frontend) |
| 产品编码自动生成 CP+YYYYMMDD+6位 | Task 4(get_next_code) + Task 5(API) + Task 6(Frontend API) + Task 7(handleAdd) |
| 产品图片上传 | Task 7(Frontend image upload + SCSS) |
| 查看弹窗 | Task 7(viewSections) |
| 状态单向流转 | Task 4(update_cell_product status validation) |
| 下拉数据来源(类型/规格/储物格) | Task 7(loadOptions) |
| 搜索筛选(关键词+状态+代次) | Task 4(list_cell_products passage filter) + Task 5(API) + Task 7 |
passage: Model String(8) → Schema Optional[str] → CRUD str → Frontend stringfreeze_date: Model DateTime → Schema Optional[datetime] → Frontend string(YYYY-MM-DD)expected_cell_count: Model Numeric(18,4) → Schema Optional[float] → CRUD float() → Frontend numberviability: Model Numeric(5,2) → Schema Optional[float] (validator 0~100) → Frontend numberproduct_status: Model String(16) default='frozen' → Schema Optional[str] (validator) → Frontend string_row_to_dict 输出 camelCase key 与前端 formData 一致