This commit is contained in:
zlwu committed 2026-05-17 08:36:37 +08:00
1 parent 246ad2228e
commit 8e2dd8b9f0
186 files changed
+38821 -2571

No files matched your search

@@ -0,0 +1,710 @@
<!-- 样本编码管理 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>样本编码</h1>
<p class="page-desc">维护样本编码池及是否下发标记。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">样本编码列表</h2>
</div>
<div class="filter-block">
<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-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增样本编码
</el-button>
<el-button class="btn-ghost" @click="openBatchGenerate">
批量生成
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length === 0"
@click="openBatchIssue"
>
批量下发
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length === 0"
@click="handleBatchRevoke"
>
批量收回
</el-button>
<el-button
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0"
@click="handleBatchDelete"
>
批量删除
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="sampleCode"
label="样本编码"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="sampleCodeType"
label="编码类型"
width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ sampleCodeTypeLabel(row.sampleCodeType) }}</template>
</el-table-column>
<el-table-column
prop="issued"
label="是否下发"
width="80"
align="center"
header-align="center"
>
<template #default="{ row }">
<span>{{ row.issued === 1 ? '是' : '否' }}</span>
</template>
</el-table-column>
<el-table-column
prop="issuedUserName"
label="下发对象"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.issuedUserName || '-' }}</template>
</el-table-column>
<el-table-column
prop="issuedTime"
label="下发时间"
min-width="180"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.issuedTime || '-' }}</template>
</el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<!-- 批量生成弹窗 -->
<el-dialog
v-model="batchGenVisible"
title="批量生成样本编码"
width="560px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="batchGenFormRef"
:model="batchGenForm"
:rules="batchGenRules"
label-position="top"
scroll-to-error
>
<div class="form-grid-2">
<el-form-item label="编码类型" prop="sampleCodeType">
<el-select v-model="batchGenForm.sampleCodeType" placeholder="请选择" style="width: 100%">
<el-option label="造血编码 (ZKZX)" value="zxbm" />
<el-option label="脐带编码 (ZKQD)" value="qdbm" />
</el-select>
</el-form-item>
<el-form-item label="中间段" prop="middle">
<el-input v-model="batchGenForm.middle" placeholder="如 34" />
</el-form-item>
<el-form-item label="开始序号" prop="seqStart">
<el-input
type="number"
step="any"
v-model="batchGenForm.seqStart"
:min="0"
:max="99999"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="结束序号" prop="seqEnd">
<el-input
type="number"
step="any"
v-model="batchGenForm.seqEnd"
:min="0"
:max="99999"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="序号位数" prop="digitCount">
<template #label
>序号位数<span class="form-label-tip">补零后的总位数</span></template
>
<el-input
type="number"
step="any"
v-model="batchGenForm.digitCount"
:min="1"
:max="10"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="预览" class="form-col-span-2">
<span class="text-sm text-gray-500">{{ batchGenPreview }}</span>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="batchGenVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="batchGenLoading" @click="handleBatchGenerate">
<el-icon><Check /></el-icon>
生成
</el-button>
</template>
</el-dialog>
<!-- 批量下发弹窗 -->
<el-dialog
v-model="batchIssueVisible"
title="批量下发样本编码"
width="560px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="batchIssueFormRef"
:model="batchIssueForm"
:rules="batchIssueRules"
label-position="top"
scroll-to-error
>
<el-form-item label="下发用户" prop="issuedUserId">
<el-select
v-model="batchIssueForm.issuedUserId"
placeholder="请选择用户"
filterable
style="width: 100%"
>
<el-option
v-for="opt in userOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="已选编码">
<span class="text-sm text-gray-500">
已选 {{ selectedRows.length }} 条,下发后"是否下发"将变为"是"
</span>
</el-form-item>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="batchIssueVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="batchIssueLoading" @click="handleBatchIssue">
<el-icon><Check /></el-icon>
确定下发
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="560px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="biz-dialog-form"
scroll-to-error
>
<div class="form-grid-2">
<el-form-item label="样本编码" prop="sampleCode">
<el-input
v-model="formData.sampleCode"
placeholder="租户内唯一,最多32位"
maxlength="32"
/>
</el-form-item>
<el-form-item v-if="isEdit" label="编码类型">
<span class="text-sm text-gray-500">{{ sampleCodeTypeLabel(formData.sampleCodeType) }}</span>
</el-form-item>
<el-form-item label="是否下发">
<el-switch
v-model="formData.issued"
:active-value="1"
:inactive-value="0"
inline-prompt
active-text="是"
inactive-text="否"
/>
</el-form-item>
<el-form-item v-if="formData.issued === 1" label="下发用户">
<el-select
v-model="formData.issuedUserId"
placeholder="请选择用户"
filterable
style="width: 100%"
>
<el-option
v-for="opt in userOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item v-if="formData.issued === 1 && formData.issuedTime" label="下发时间">
<span class="text-sm text-gray-500">{{ formData.issuedTime }}</span>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.sampleCode || '样本编码详情'"
:icon="Ticket"
:meta="`编码类型:${sampleCodeTypeLabel(viewRow?.sampleCodeType || '')}${viewRow?.createTime ? ' · ' + viewRow.createTime : ''}`"
:sections="viewSections"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Ticket } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchSampleCodeList,
fetchCreateSampleCode,
fetchUpdateSampleCode,
fetchDeleteSampleCode,
fetchUserOptions,
fetchBatchGenerateSampleCodes,
fetchBatchIssueSampleCodes,
fetchBatchRevokeSampleCodes
} from '@/api/admin'
defineOptions({ name: 'BizSampleCode' })
function sampleCodeTypeLabel(t: string) {
if (t === 'zxbm') return '造血编码'
if (t === 'qdbm') return '脐带编码'
return t || '-'
}
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.BizSampleCode.SampleCodeItem[]>([])
const selectedRows = ref<Api.BizSampleCode.SampleCodeItem[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const dialogVisible = ref(false)
const dialogTitle = ref('新增样本编码')
const isEdit = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const editingId = ref<string | null>(null)
// ---- 查看弹窗 ----
const viewVisible = ref(false)
const viewRow = ref<Api.BizSampleCode.SampleCodeItem | null>(null)
const viewSections = computed(() => {
const row = viewRow.value
if (!row) return []
return [
{
title: '基本信息',
fields: [
{ label: '样本编码', value: row.sampleCode },
{ label: '编码类型', value: sampleCodeTypeLabel(row.sampleCodeType) },
{ label: '是否下发', value: row.issued === 1 ? '是' : '否' },
{ label: '下发对象', value: row.issuedUserName || '-' },
{ label: '下发时间', value: row.issuedTime || '-' },
{ label: '创建时间', value: row.createTime },
{ label: '备注', value: row.remark, span: true }
]
}
]
})
function handleView(row: Api.BizSampleCode.SampleCodeItem) {
viewRow.value = row
viewVisible.value = true
}
// --- 批量生成 ---
const batchGenVisible = ref(false)
const batchGenLoading = ref(false)
const batchGenFormRef = ref()
const batchGenForm = reactive({
sampleCodeType: 'zxbm',
middle: '34',
seqStart: 1,
seqEnd: 100,
digitCount: 6,
})
const batchGenRules = {
sampleCodeType: [{ required: true, message: '请选择编码类型', trigger: 'change' }],
middle: [{ required: true, message: '请输入中间段', trigger: 'blur' }],
seqStart: [{ required: true, message: '请输入开始序号', trigger: 'blur' }],
seqEnd: [{ required: true, message: '请输入结束序号', trigger: 'blur' }],
digitCount: [{ required: true, message: '请输入序号位数', trigger: 'blur' }],
}
const typePrefixMap: Record<string, string> = { zxbm: 'ZKZX', qdbm: 'ZKQD' }
const batchGenPreview = computed(() => {
const t = batchGenForm.sampleCodeType || ''
const m = batchGenForm.middle || ''
const s = batchGenForm.seqStart ?? 0
const e = batchGenForm.seqEnd ?? 0
const d = batchGenForm.digitCount ?? 6
const prefix = typePrefixMap[t] || ''
if (!prefix || e < s) return '—'
const first = `${prefix}${m}${String(s).padStart(d, '0')}`
const last = `${prefix}${m}${String(e).padStart(d, '0')}`
return `${first} ~ ${last}(共 ${e - s + 1} 条)`
})
// --- 批量下发 ---
const batchIssueVisible = ref(false)
const batchIssueLoading = ref(false)
const batchIssueFormRef = ref()
const batchIssueForm = reactive({
issuedUserId: '',
})
const batchIssueRules = {
issuedUserId: [{ required: true, message: '请选择下发用户', trigger: 'change' }],
}
const userOptions = ref<Array<{ label: string; value: string }>>([])
const formData = reactive({
sampleCode: '',
sampleCodeType: '',
issued: 0,
issuedUserId: '',
issuedUserName: '',
issuedTime: '',
orderNum: 0,
remark: ''
})
const formRules = {
sampleCode: [{ required: true, message: '请输入样本编码', trigger: 'blur' }]
}
const handleSelectionChange = (rows: Api.BizSampleCode.SampleCodeItem[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchSampleCodeList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
pagination.page = 1
loadData()
}
function resetForm() {
formData.sampleCode = ''
formData.sampleCodeType = ''
formData.issued = 0
formData.issuedUserId = ''
formData.issuedUserName = ''
formData.issuedTime = ''
formData.orderNum = 0
formData.remark = ''
editingId.value = null
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增样本编码'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
async function handleEdit(row: Api.BizSampleCode.SampleCodeItem) {
isEdit.value = true
dialogTitle.value = '编辑样本编码'
editingId.value = row.id
formData.sampleCode = row.sampleCode || ''
formData.sampleCodeType = row.sampleCodeType || ''
formData.issued = row.issued === 1 ? 1 : 0
formData.issuedUserId = row.issuedUserId || ''
formData.issuedUserName = row.issuedUserName || ''
formData.issuedTime = row.issuedTime || ''
formData.orderNum = row.orderNum ?? 0
formData.remark = row.remark || ''
dialogVisible.value = true
formRef.value?.clearValidate?.()
if (!userOptions.value.length) {
try {
userOptions.value = await fetchUserOptions()
} catch { /* ignore */ }
}
}
async function handleSubmit() {
await formRef.value?.validate?.()
submitLoading.value = true
try {
const payload = {
sampleCode: formData.sampleCode.trim(),
issued: formData.issued,
issuedUserId: formData.issued === 1 ? formData.issuedUserId || undefined : null,
issuedUserName: formData.issued === 1 ? formData.issuedUserName || undefined : null,
issuedTime: formData.issuedTime || undefined,
orderNum: formData.orderNum,
remark: formData.remark?.trim() || undefined
}
if (isEdit.value && editingId.value) {
await fetchUpdateSampleCode(editingId.value, payload)
} else {
await fetchCreateSampleCode(payload)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
/* */
} finally {
submitLoading.value = false
}
}
async function handleDelete(row: Api.BizSampleCode.SampleCodeItem) {
await ElMessageBox.confirm('确定要删除该样本编码吗?', '提示', { type: 'warning' })
await fetchDeleteSampleCode(row.id)
ElMessage.success('删除成功')
await loadData()
}
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条吗?`, '提示', { type: 'warning' })
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteSampleCode(id)
}
ElMessage.success('批量删除成功')
selectedRows.value = []
await loadData()
}, fullscreenLoadingText.batchDelete)
}
async function handleBatchRevoke() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(
`确定收回选中的 ${ids.length} 条吗?收回后将清除下发信息。`,
'提示',
{ type: 'warning' }
)
await withFullscreenLoading(async () => {
await fetchBatchRevokeSampleCodes(ids)
selectedRows.value = []
await loadData()
}, fullscreenLoadingText.batchDelete)
}
// --- 批量生成 ---
function openBatchGenerate() {
batchGenForm.sampleCodeType = 'zxbm'
batchGenForm.middle = '34'
batchGenForm.seqStart = 1
batchGenForm.seqEnd = 100
batchGenForm.digitCount = 6
batchGenVisible.value = true
}
async function handleBatchGenerate() {
await batchGenFormRef.value?.validate?.()
if (batchGenForm.seqStart > batchGenForm.seqEnd) {
ElMessage.warning('开始序号不能大于结束序号')
return
}
batchGenLoading.value = true
try {
await fetchBatchGenerateSampleCodes({
sampleCodeType: batchGenForm.sampleCodeType,
middle: batchGenForm.middle.trim(),
seqStart: batchGenForm.seqStart,
seqEnd: batchGenForm.seqEnd,
digitCount: batchGenForm.digitCount,
})
batchGenVisible.value = false
await loadData()
} finally {
batchGenLoading.value = false
}
}
// --- 批量下发 ---
async function openBatchIssue() {
if (!selectedRows.value.length) return
batchIssueForm.issuedUserId = ''
batchIssueVisible.value = true
if (!userOptions.value.length) {
try {
userOptions.value = await fetchUserOptions()
} catch { /* ignore */ }
}
}
async function handleBatchIssue() {
await batchIssueFormRef.value?.validate?.()
batchIssueLoading.value = true
try {
const ids = selectedRows.value.map(r => r.id)
const userOpt = userOptions.value.find(u => u.value === batchIssueForm.issuedUserId)
await fetchBatchIssueSampleCodes({
ids,
issuedUserId: batchIssueForm.issuedUserId,
issuedUserName: userOpt?.label || undefined,
})
batchIssueVisible.value = false
selectedRows.value = []
await loadData()
} finally {
batchIssueLoading.value = false
}
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>