This commit is contained in:
zlwu committed 2026-05-21 13:20:15 +08:00
1 parent 14f1a84bb3
commit d387ba4017
34 files changed
+6155 -3865

No files matched your search

+5 -2
View File
@@ -32,6 +32,9 @@ export function fetchDeleteCellProduct(id: string) {
return request.del({ url: `/web/admin/cell-products/${id}` })
}
export function fetchNextCellProductCode() {
return request.get<{ code: string }>({ url: '/web/admin/cell-products/next-code' })
export function fetchNextCellProductCode(count = 1) {
return request.get<{ code: string; codes?: string[] }>({
url: '/web/admin/cell-products/next-code',
params: count > 1 ? { count } : undefined
})
}
+17 -2
View File
@@ -1,13 +1,28 @@
import request from '@/utils/http'
export interface ProductionWorkOrderItem {
id: string
workOrderNo: string
protocolNo?: string
sampleCode?: string
receptionSource?: string
receptionSourceLabel?: string
sampleTypeName?: string
handlerName?: string
receptionTime?: string
workOrderStatus: string
createTime?: string
remark?: string
}
export function fetchProductionWorkOrderList(params?: {
page?: number
pageSize?: number
keyword?: string
workOrderNo?: string
workOrderStatus?: string
}) {
return request.get<{
list: any[]
list: ProductionWorkOrderItem[]
total: number
page: number
pageSize: number
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product_ts", "label": "cell-product.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L1"}, {"id": "api_cell_product_fetchcellproductlist", "label": "fetchCellProductList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L3"}, {"id": "api_cell_product_fetchcellproductdetail", "label": "fetchCellProductDetail()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L19"}, {"id": "api_cell_product_fetchcreatecellproduct", "label": "fetchCreateCellProduct()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L23"}, {"id": "api_cell_product_fetchupdatecellproduct", "label": "fetchUpdateCellProduct()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L27"}, {"id": "api_cell_product_fetchdeletecellproduct", "label": "fetchDeleteCellProduct()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L31"}, {"id": "api_cell_product_fetchnextcellproductcode", "label": "fetchNextCellProductCode()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L35"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_http", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L1", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product_ts", "target": "api_cell_product_fetchcellproductlist", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L3", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product_ts", "target": "api_cell_product_fetchcellproductdetail", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L19", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product_ts", "target": "api_cell_product_fetchcreatecellproduct", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L23", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product_ts", "target": "api_cell_product_fetchupdatecellproduct", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L27", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product_ts", "target": "api_cell_product_fetchdeletecellproduct", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L31", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_cell_product_ts", "target": "api_cell_product_fetchnextcellproductcode", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L35", "weight": 1.0}], "raw_calls": [{"caller_nid": "api_cell_product_fetchcellproductlist", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L10"}, {"caller_nid": "api_cell_product_fetchcellproductdetail", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L20"}, {"caller_nid": "api_cell_product_fetchcreatecellproduct", "callee": "post", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L24"}, {"caller_nid": "api_cell_product_fetchupdatecellproduct", "callee": "put", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L28"}, {"caller_nid": "api_cell_product_fetchdeletecellproduct", "callee": "del", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L32"}, {"caller_nid": "api_cell_product_fetchnextcellproductcode", "callee": "get", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/cell-product.ts", "source_location": "L36"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_cell_product_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_cell_product_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L1"}, {"id": "cell_product_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L828"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_cell_product_index_vue", "target": "cell_product_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/cell-product/index.vue", "source_location": "L828", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1"}, {"id": "process_route_index_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1049"}, {"id": "process_route_index_get", "label": "get()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1203"}, {"id": "process_route_index_set", "label": "set()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1213"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1049", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_if", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1138", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_get", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1203", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_production_process_route_index_vue", "target": "process_route_index_set", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1213", "weight": 1.0}], "raw_calls": [{"caller_nid": "process_route_index_if", "callee": "mapReagentOptionLabel", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1051"}, {"caller_nid": "process_route_index_if", "callee": "mapConsumableOptionLabel", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1140"}, {"caller_nid": "process_route_index_get", "callee": "isArray", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1207"}, {"caller_nid": "process_route_index_get", "callee": "filter", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1207"}, {"caller_nid": "process_route_index_get", "callee": "filter", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1208"}, {"caller_nid": "process_route_index_get", "callee": "map", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1208"}, {"caller_nid": "process_route_index_get", "callee": "split", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1208"}, {"caller_nid": "process_route_index_get", "callee": "String", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1208"}, {"caller_nid": "process_route_index_set", "callee": "join", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/production/process-route/index.vue", "source_location": "L1215"}]}
@@ -269,6 +269,7 @@
v-model="formData.productCode"
placeholder="请输入产品编码"
:readonly="isEdit"
@input="onBaseProductCodeInput"
/>
</el-form-item>
<el-form-item label="产品名称" prop="productName">
@@ -322,7 +323,7 @@
<el-form-item label="批次号" prop="batchNo">
<el-input v-model="formData.batchNo" placeholder="请输入批次号" />
</el-form-item>
<el-form-item label="储物格位置" prop="storageSlotId">
<el-form-item v-if="isEdit" label="储物格位置" prop="storageSlotId">
<el-select
v-model="formData.storageSlotId"
placeholder="请选择储物格"
@@ -365,13 +366,18 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item label="数量" prop="quantity">
<el-form-item
:label="isEdit ? '数量' : '入库份数'"
prop="quantity"
>
<el-input-number
v-model="formData.quantity"
:min="0"
:min="isEdit ? 0 : 1"
:max="isEdit ? undefined : 50"
:precision="0"
controls-position="right"
style="width: 100%"
@change="onCreateCountChange"
/>
</el-form-item>
<el-form-item label="单位">
@@ -489,6 +495,49 @@
placeholder="可选"
/>
</el-form-item>
<div v-if="!isEdit" class="product-lines-section">
<div class="product-lines-head">
<span class="product-lines-title">入库明细</span>
<span class="form-label-tip"
>每份填写编码与储物格;多份时编码为「产品编码-01、-02…」</span
>
</div>
<el-table :data="productLines" size="small" class="product-lines-table">
<el-table-column
type="index"
label="序号"
width="64"
align="center"
/>
<el-table-column label="产品编码" min-width="200">
<template #default="{ row }">
<el-input
v-model="row.productCode"
placeholder="产品编码"
@input="onLineProductCodeInput"
/>
</template>
</el-table-column>
<el-table-column label="储物格位置" min-width="280">
<template #default="{ row }">
<el-select
v-model="row.storageSlotId"
placeholder="请选择储物格"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="item in storageSlotOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</template>
</el-table-column>
</el-table>
</div>
</div>
</el-form>
<template #footer>
@@ -605,6 +654,8 @@
defineOptions({ name: 'ProductionCellProduct' })
type ProductLine = { productCode: string; storageSlotId: string }
type ProductImageFile = {
id?: string
fileName: string
@@ -728,12 +779,101 @@
remark: '',
})
const productLines = ref<ProductLine[]>([{ productCode: '', storageSlotId: '' }])
const formRules = {
productCode: [{ required: true, message: '请输入产品编码', trigger: 'blur' }],
productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
batchNo: [{ required: true, message: '请输入批次号', trigger: 'blur' }],
}
function createQuantityValue() {
const n = Math.floor(Number(formData.quantity) || 0)
if (n < 1) return 1
return Math.min(n, 50)
}
function stripProductCodeSuffix(code: string) {
const c = (code || '').trim()
const m = c.match(/^(.+)-(\d{2})$/)
return m ? m[1].trim() : c
}
function buildLineProductCode(base: string, index: number, total: number) {
const b = stripProductCodeSuffix(base)
if (!b) return ''
if (total <= 1) return b
return `${b}-${String(index + 1).padStart(2, '0')}`
}
function syncLineCodesFromBase() {
const total = createQuantityValue()
const base = formData.productCode
productLines.value.forEach((line, index) => {
line.productCode = buildLineProductCode(base, index, total)
})
}
function syncProductLines() {
const count = createQuantityValue()
const prev = productLines.value.map((l) => ({ ...l }))
const lines: ProductLine[] = []
for (let i = 0; i < count; i++) {
lines.push({
productCode:
count > 1
? buildLineProductCode(formData.productCode, i, count)
: prev[i]?.productCode || formData.productCode || '',
storageSlotId: prev[i]?.storageSlotId || '',
})
}
productLines.value = lines.length ? lines : [{ productCode: '', storageSlotId: '' }]
if (count <= 1 && productLines.value[0]) {
formData.productCode = productLines.value[0].productCode
}
}
function onCreateCountChange() {
if (!isEdit.value) {
syncProductLines()
if (createQuantityValue() > 1) syncLineCodesFromBase()
}
}
function onBaseProductCodeInput() {
if (!isEdit.value && createQuantityValue() > 1) syncLineCodesFromBase()
else if (!isEdit.value && createQuantityValue() <= 1 && productLines.value[0]) {
productLines.value[0].productCode = formData.productCode
}
}
function onLineProductCodeInput() {
if (!isEdit.value && createQuantityValue() <= 1) {
formData.productCode = productLines.value[0]?.productCode || ''
}
}
async function validateProductLines(): Promise<boolean> {
if (isEdit.value) return true
const count = createQuantityValue()
for (let i = 0; i < productLines.value.length; i++) {
const line = productLines.value[i]
if (!line.productCode?.trim()) {
ElMessage.warning(`请填写第 ${i + 1} 份产品编码`)
return false
}
if (!line.storageSlotId) {
ElMessage.warning(`请选择第 ${i + 1} 份产品的储物格位置`)
return false
}
}
if (productLines.value.length !== count) {
ElMessage.warning('入库明细行数与入库份数不一致,请调整入库份数')
return false
}
return true
}
/* ---- 选择 ---- */
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows
@@ -985,6 +1125,7 @@
formData.productImages = ''
formData.remark = ''
productImageFiles.value = []
productLines.value = [{ productCode: '', storageSlotId: '' }]
editingId.value = null
}
@@ -993,6 +1134,7 @@
isEdit.value = false
dialogTitle.value = '新增产品'
resetForm()
formData.quantity = 1
dialogVisible.value = true
void searchUnitOptions('')
formRef.value?.clearValidate?.()
@@ -1004,6 +1146,10 @@
} catch {
/* 自动生成失败时用户可手动输入 */
}
syncProductLines()
if (productLines.value[0]) {
productLines.value[0].productCode = formData.productCode
}
}
/* ---- 编辑 ---- */
@@ -1036,22 +1182,20 @@
/* ---- 提交 ---- */
async function handleSubmit() {
if (!(await validateProductLines())) return
await formRef.value?.validate?.()
submitLoading.value = true
try {
syncImagesToForm()
const payload = {
productCode: formData.productCode.trim(),
const shared = {
productName: formData.productName.trim(),
productTypeId: formData.productTypeId || undefined,
specId: formData.specId || undefined,
passage: formData.passage || undefined,
batchNo: formData.batchNo.trim(),
storageSlotId: formData.storageSlotId || undefined,
expectedCellCount: formData.expectedCellCount ?? undefined,
viability: formData.viability ?? undefined,
freezeDate: formData.freezeDate || undefined,
quantity: formData.quantity,
unit: formData.unit?.trim() || undefined,
expiryDate: formData.expiryDate || undefined,
productStatus: formData.productStatus || undefined,
@@ -1059,11 +1203,22 @@
remark: formData.remark?.trim() || undefined,
}
if (isEdit.value && editingId.value) {
await fetchUpdateCellProduct(editingId.value, payload)
await fetchUpdateCellProduct(editingId.value, {
...shared,
storageSlotId: formData.storageSlotId || undefined,
quantity: formData.quantity,
})
ElMessage.success('更新成功')
} else {
await fetchCreateCellProduct(payload)
const qty = createQuantityValue()
const items = productLines.value.map((line) => ({
productCode: line.productCode.trim(),
storageSlotId: line.storageSlotId,
}))
const res = await fetchCreateCellProduct({ ...shared, items })
const n = (res as { count?: number })?.count ?? qty
ElMessage.success(n > 1 ? `成功创建 ${n} 份细胞产品` : '创建成功')
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
@@ -1121,6 +1276,35 @@
}
}
.product-lines-section {
grid-column: 1 / -1;
margin-top: 4px;
}
.product-lines-head {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.product-lines-title {
font-size: 13px;
font-weight: 600;
color: var(--mint-text, #334155);
}
.product-lines-table {
width: 100%;
}
.product-line-code {
font-size: 14px;
color: #334155;
line-height: 32px;
}
.attachment-upload {
width: 100%;
}
@@ -16,7 +16,7 @@
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="工单号">
<el-input v-model="searchForm.keyword" placeholder="请输入工单号" clearable />
<el-input v-model="searchForm.workOrderNo" placeholder="请输入工单号" clearable />
</el-form-item>
<el-form-item label="工单状态">
<el-select v-model="searchForm.workOrderStatus" placeholder="全部" clearable style="width: 100%">
@@ -47,6 +47,15 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center">
<template #default="{ row }">{{ row.sampleTypeName || '—' }}</template>
</el-table-column>
<el-table-column prop="handlerName" label="接收人" min-width="100" align="center">
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
</el-table-column>
<el-table-column prop="receptionTime" label="接收时间" min-width="170" align="center">
<template #default="{ row }">{{ row.receptionTime || '—' }}</template>
</el-table-column>
<el-table-column prop="workOrderStatus" label="工单状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="woStatusTag(row.workOrderStatus).type" size="small">
@@ -123,7 +132,7 @@
}
/* ---- 搜索 & 列表 ---- */
const searchForm = reactive({ keyword: '', workOrderStatus: '' })
const searchForm = reactive({ workOrderNo: '', workOrderStatus: '' })
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -134,7 +143,7 @@
const res = await fetchProductionWorkOrderList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
workOrderNo: searchForm.workOrderNo || undefined,
workOrderStatus: searchForm.workOrderStatus || undefined
})
tableData.value = res.list || []
@@ -150,7 +159,7 @@
}
function handleReset() {
searchForm.keyword = ''
searchForm.workOrderNo = ''
searchForm.workOrderStatus = ''
pagination.page = 1
loadData()
@@ -175,6 +184,9 @@
value: sourceTag(d.receptionSource).label,
tagType: sourceTag(d.receptionSource).type as any
},
{ label: '样本类型', value: d.sampleTypeName || '—' },
{ label: '接收人', value: d.handlerName || '—' },
{ label: '接收时间', value: d.receptionTime || '—' },
{
label: '工单状态',
value: woStatusTag(d.workOrderStatus).label,