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,403 @@
<!-- 细胞样本类型管理 -->
<!-- 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"
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="typeCode"
label="类型编码"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="typeName"
label="类型名称"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
prop="storageYears"
label="存储年限(年)"
width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="storagePrice"
label="存储价格(元)"
width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ row.storagePrice != null ? row.storagePrice : '—' }}
</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="dialogVisible"
:title="dialogTitle"
width="640px"
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
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="类型编码" prop="typeCode">
<el-input v-model="formData.typeCode" placeholder="租户内唯一" :disabled="isEdit" />
</el-form-item>
<el-form-item label="类型名称" prop="typeName">
<el-input v-model="formData.typeName" placeholder="必填" />
</el-form-item>
<el-form-item label="存储年限(年)">
<el-input
type="number"
step="any"
v-model="formData.storageYears"
:min="0"
:max="200"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="存储价格(元)">
<el-input
type="number"
step="any"
v-model="formData.storagePrice"
:min="0"
style="width: 100%"
/>
</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?.typeName || '类型详情'"
:icon="Collection"
:meta="`类型编码:${viewRow?.typeCode || ''}${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, Collection } from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchCellSampleTypeList,
fetchCreateCellSampleType,
fetchUpdateCellSampleType,
fetchDeleteCellSampleType
} from '@/api/admin'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
defineOptions({ name: 'BizCellSampleType' })
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.BizCellSampleType.CellSampleTypeItem[]>([])
const selectedRows = ref<Api.BizCellSampleType.CellSampleTypeItem[]>([])
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.BizCellSampleType.CellSampleTypeItem | null>(null)
const viewSections = computed(() => {
const row = viewRow.value
if (!row) return []
return [
{
title: '基本信息',
fields: [
{ label: '类型编码', value: row.typeCode },
{ label: '类型名称', value: row.typeName },
{ label: '存储年限(年)', value: row.storageYears },
{ label: '存储价格(元)', value: row.storagePrice },
{ label: '创建时间', value: row.createTime },
{ label: '备注', value: row.remark, span: true }
]
}
]
})
function handleView(row: Api.BizCellSampleType.CellSampleTypeItem) {
viewRow.value = row
viewVisible.value = true
}
const formData = reactive({
typeCode: '',
typeName: '',
storageYears: null as number | null,
storagePrice: null as number | null,
orderNum: 0,
remark: ''
})
const formRules = {
typeCode: [{ required: true, message: '请输入类型编码', trigger: 'blur' }],
typeName: [{ required: true, message: '请输入类型名称', trigger: 'blur' }]
}
const handleSelectionChange = (rows: Api.BizCellSampleType.CellSampleTypeItem[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchCellSampleTypeList({
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.typeCode = ''
formData.typeName = ''
formData.storageYears = null
formData.storagePrice = null
formData.orderNum = 0
formData.remark = ''
editingId.value = null
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增类型'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleEdit(row: Api.BizCellSampleType.CellSampleTypeItem) {
isEdit.value = true
dialogTitle.value = '编辑类型'
editingId.value = row.id
formData.typeCode = row.typeCode || ''
formData.typeName = row.typeName || ''
formData.storageYears = row.storageYears ?? null
formData.storagePrice = row.storagePrice ?? null
formData.orderNum = row.orderNum ?? 0
formData.remark = row.remark || ''
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
async function handleSubmit() {
await formRef.value?.validate?.()
submitLoading.value = true
try {
const payload = {
typeCode: formData.typeCode.trim(),
typeName: formData.typeName.trim(),
storageYears: formData.storageYears,
storagePrice: formData.storagePrice,
orderNum: formData.orderNum,
remark: formData.remark?.trim() || undefined
}
if (isEdit.value && editingId.value) {
await fetchUpdateCellSampleType(editingId.value, {
typeName: payload.typeName,
storageYears: payload.storageYears,
storagePrice: payload.storagePrice,
orderNum: payload.orderNum,
remark: payload.remark
})
} else {
await fetchCreateCellSampleType(payload)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
/* 全局 */
} finally {
submitLoading.value = false
}
}
async function handleDelete(row: Api.BizCellSampleType.CellSampleTypeItem) {
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
await fetchDeleteCellSampleType(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 fetchDeleteCellSampleType(id)
}
ElMessage.success('批量删除成功')
selectedRows.value = []
await loadData()
}, fullscreenLoadingText.batchDelete)
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -0,0 +1,684 @@
<!-- 采集协议号管理 -->
<!-- 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="protocolNo"
label="采集协议号"
min-width="160"
align="center"
header-align="center"
/>
<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="prefix">
<el-input v-model="batchGenForm.prefix" placeholder="如 ZKZX" />
</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="protocolNo">
<el-input
v-model="formData.protocolNo"
placeholder="租户内唯一,最多32位"
maxlength="32"
/>
</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?.protocolNo || '协议号详情'"
:icon="Tickets"
:meta="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, Tickets } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchCollectionProtocolNoList,
fetchCreateCollectionProtocolNo,
fetchUpdateCollectionProtocolNo,
fetchDeleteCollectionProtocolNo,
fetchUserOptions,
fetchBatchGenerateProtocolNos,
fetchBatchIssueProtocolNos,
fetchBatchRevokeProtocolNos
} from '@/api/admin'
defineOptions({ name: 'BizCollectionProtocolNo' })
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.BizCollectionProtocolNo.ProtocolNoItem[]>([])
const selectedRows = ref<Api.BizCollectionProtocolNo.ProtocolNoItem[]>([])
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.BizCollectionProtocolNo.ProtocolNoItem | null>(null)
const viewSections = computed(() => {
const row = viewRow.value
if (!row) return []
return [
{
title: '基本信息',
fields: [
{ label: '采集协议号', value: row.protocolNo },
{ 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.BizCollectionProtocolNo.ProtocolNoItem) {
viewRow.value = row
viewVisible.value = true
}
// --- 批量生成 ---
const batchGenVisible = ref(false)
const batchGenLoading = ref(false)
const batchGenFormRef = ref()
const batchGenForm = reactive({
prefix: 'ZKZX',
middle: '34',
seqStart: 1,
seqEnd: 100,
digitCount: 6,
})
const batchGenRules = {
prefix: [{ required: true, message: '请输入固定前缀', trigger: 'blur' }],
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 batchGenPreview = computed(() => {
const p = batchGenForm.prefix || ''
const m = batchGenForm.middle || ''
const s = batchGenForm.seqStart ?? 0
const e = batchGenForm.seqEnd ?? 0
const d = batchGenForm.digitCount ?? 6
if (!p || e < s) return '—'
const first = `${p}${m}${String(s).padStart(d, '0')}`
const last = `${p}${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({
protocolNo: '',
issued: 0,
issuedUserId: '',
issuedUserName: '',
issuedTime: '',
orderNum: 0,
remark: ''
})
const formRules = {
protocolNo: [{ required: true, message: '请输入采集协议号', trigger: 'blur' }]
}
const handleSelectionChange = (rows: Api.BizCollectionProtocolNo.ProtocolNoItem[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchCollectionProtocolNoList({
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.protocolNo = ''
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.BizCollectionProtocolNo.ProtocolNoItem) {
isEdit.value = true
dialogTitle.value = '编辑协议号'
editingId.value = row.id
formData.protocolNo = row.protocolNo || ''
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 = {
protocolNo: formData.protocolNo.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 fetchUpdateCollectionProtocolNo(editingId.value, payload)
} else {
await fetchCreateCollectionProtocolNo(payload)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
/* */
} finally {
submitLoading.value = false
}
}
async function handleDelete(row: Api.BizCollectionProtocolNo.ProtocolNoItem) {
await ElMessageBox.confirm('确定要删除该协议号吗?', '提示', { type: 'warning' })
await fetchDeleteCollectionProtocolNo(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 fetchDeleteCollectionProtocolNo(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 fetchBatchRevokeProtocolNos(ids)
selectedRows.value = []
await loadData()
}, fullscreenLoadingText.batchDelete)
}
// --- 批量生成 ---
function openBatchGenerate() {
batchGenForm.prefix = 'ZKZX'
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 fetchBatchGenerateProtocolNos({
prefix: batchGenForm.prefix.trim(),
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
// load user options
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 fetchBatchIssueProtocolNos({
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>
@@ -0,0 +1,665 @@
<!-- 采集地区及医院管理 -->
<!-- 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"
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="regionName"
label="地区名称"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
prop="createTime"
label="创建时间"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="医院信息"
min-width="240"
align="center"
header-align="center"
>
<template #default="{ row }">
<template v-if="row.hospitalNames && row.hospitalNames.length">
<div
v-for="(name, idx) in row.hospitalNames"
:key="idx"
class="hospital-cell-line"
>
{{ name }}
</div>
</template>
<span v-else class="muted">—</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="400"
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="openHospitalDialog(row)"
>地区医院配置</el-button
>
<el-button class="btn-op-item" @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="regionDialogVisible"
:title="regionDialogTitle"
width="560px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="regionFormRef"
:model="regionForm"
:rules="regionFormRules"
label-position="top"
class="biz-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="地区名称" prop="regionName" class="form-col-span-2">
<el-input v-model="regionForm.regionName" placeholder="必填" />
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="regionForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="regionDialogVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
class="btn-main"
type="primary"
@click="submitRegion"
:loading="regionSubmitLoading"
>
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<el-dialog
v-model="hospitalDialogVisible"
:title="hospitalDialogTitle"
width="800px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
@closed="onHospitalDialogClosed"
>
<div class="hospital-dialog-head">
<span class="muted">当前地区:{{ currentRegionName }}</span>
</div>
<div class="list-toolbar hospital-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ hospitalSelected.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAddHospital">
<el-icon><Plus /></el-icon>
新增医院
</el-button>
<el-button
class="btn-ghost"
type="danger"
:disabled="hospitalSelected.length === 0"
@click="handleBatchDeleteHospital"
>
批量删除
</el-button>
</div>
</div>
<div class="list-body list-wrap hospital-table-wrap">
<el-table
class="list-table"
:data="hospitalTable"
v-loading="hospitalLoading"
stripe
row-key="id"
max-height="360"
@selection-change="onHospitalSelectionChange"
>
<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"
/>
<el-table-column
prop="hospitalName"
label="医院名称"
min-width="200"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEditHospital(row)"
>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDeleteHospital(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="hospitalPagination.page"
:page-size="hospitalPagination.pageSize"
:total="hospitalPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="
(val) => (
(hospitalPagination.page = 1),
(hospitalPagination.pageSize = val),
loadHospitals()
)
"
@current-change="(val) => ((hospitalPagination.page = val), loadHospitals())"
/>
</div>
</div>
<el-dialog
v-model="hospitalFormVisible"
:title="hospitalFormTitle"
width="520px"
class="dialog-form-layout"
align-center
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="hospitalFormRef"
:model="hospitalForm"
:rules="hospitalFormRules"
label-position="top"
>
<el-form-item label="医院名称" prop="hospitalName">
<el-input v-model="hospitalForm.hospitalName" placeholder="必填" />
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="hospitalForm.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="hospitalFormVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
class="btn-main"
type="primary"
@click="submitHospital"
:loading="hospitalSubmitLoading"
>
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<template #footer>
<el-button class="btn-ghost" @click="hospitalDialogVisible = false">
<el-icon><Close /></el-icon>
关闭
</el-button>
</template>
</el-dialog>
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.regionName || '地区详情'"
:icon="Location"
:meta="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, Location } from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import {
fetchCollectionRegionList,
fetchCreateCollectionRegion,
fetchUpdateCollectionRegion,
fetchDeleteCollectionRegion,
fetchHospitalList,
fetchCreateHospital,
fetchUpdateHospital,
fetchDeleteHospital
} from '@/api/admin'
defineOptions({ name: 'BizCollectionRegion' })
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.BizCollectionRegion.RegionItem[]>([])
const selectedRows = ref<Api.BizCollectionRegion.RegionItem[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const regionDialogVisible = ref(false)
const regionDialogTitle = ref('新增地区')
const regionIsEdit = ref(false)
const regionSubmitLoading = ref(false)
const regionFormRef = ref()
const editingRegionId = ref<string | null>(null)
const regionForm = reactive({ regionName: '', orderNum: 0, remark: '' })
const regionFormRules = {
regionName: [{ required: true, message: '请输入地区名称', trigger: 'blur' }]
}
const hospitalDialogVisible = ref(false)
const hospitalDialogTitle = ref('地区医院配置')
const currentRegionId = ref('')
const currentRegionName = ref('')
const hospitalTable = ref<Api.BizCollectionHospital.HospitalItem[]>([])
const hospitalSelected = ref<Api.BizCollectionHospital.HospitalItem[]>([])
const hospitalLoading = ref(false)
const hospitalPagination = reactive({ page: 1, pageSize: 10, total: 0 })
const hospitalFormVisible = ref(false)
const hospitalFormTitle = ref('新增医院')
const hospitalIsEdit = ref(false)
const hospitalSubmitLoading = ref(false)
const hospitalFormRef = ref()
const editingHospitalId = ref<string | null>(null)
// ---- 查看弹窗 ----
const viewVisible = ref(false)
const viewRow = ref<Api.BizCollectionRegion.RegionItem | null>(null)
const viewSections = computed(() => {
const row = viewRow.value
if (!row) return []
const hospitals =
row.hospitalNames && row.hospitalNames.length > 0
? row.hospitalNames.join('、')
: '无'
return [
{
title: '基本信息',
fields: [
{ label: '地区名称', value: row.regionName },
{ label: '创建时间', value: row.createTime },
{ label: '关联医院', value: hospitals, span: true },
{ label: '备注', value: row.remark, span: true }
]
}
]
})
function handleView(row: Api.BizCollectionRegion.RegionItem) {
viewRow.value = row
viewVisible.value = true
}
const hospitalForm = reactive({ hospitalName: '', orderNum: 0, remark: '' })
const hospitalFormRules = {
hospitalName: [{ required: true, message: '请输入医院名称', trigger: 'blur' }]
}
function onHospitalSelectionChange(rows: Api.BizCollectionHospital.HospitalItem[]) {
hospitalSelected.value = rows
}
const handleSelectionChange = (rows: Api.BizCollectionRegion.RegionItem[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchCollectionRegionList({
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 handleAdd() {
regionIsEdit.value = false
regionDialogTitle.value = '新增地区'
editingRegionId.value = null
regionForm.regionName = ''
regionForm.orderNum = 0
regionForm.remark = ''
regionDialogVisible.value = true
regionFormRef.value?.clearValidate?.()
}
function handleEdit(row: Api.BizCollectionRegion.RegionItem) {
regionIsEdit.value = true
regionDialogTitle.value = '编辑地区'
editingRegionId.value = row.id
regionForm.regionName = row.regionName || ''
regionForm.orderNum = row.orderNum ?? 0
regionForm.remark = row.remark || ''
regionDialogVisible.value = true
regionFormRef.value?.clearValidate?.()
}
async function submitRegion() {
await regionFormRef.value?.validate?.()
regionSubmitLoading.value = true
try {
const payload = {
regionName: regionForm.regionName.trim(),
orderNum: regionForm.orderNum,
remark: regionForm.remark?.trim() || undefined
}
if (regionIsEdit.value && editingRegionId.value) {
await fetchUpdateCollectionRegion(editingRegionId.value, payload)
} else {
await fetchCreateCollectionRegion(payload)
}
ElMessage.success(regionIsEdit.value ? '更新成功' : '创建成功')
regionDialogVisible.value = false
await loadData()
} catch {
/* */
} finally {
regionSubmitLoading.value = false
}
}
async function handleDelete(row: Api.BizCollectionRegion.RegionItem) {
await ElMessageBox.confirm('删除地区将同时软删除其下属全部医院,确定继续?', '提示', {
type: 'warning'
})
await fetchDeleteCollectionRegion(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 fetchDeleteCollectionRegion(id)
}
ElMessage.success('批量删除成功')
selectedRows.value = []
await loadData()
}, fullscreenLoadingText.batchDelete)
}
function openHospitalDialog(row: Api.BizCollectionRegion.RegionItem) {
currentRegionId.value = row.id
currentRegionName.value = row.regionName || ''
hospitalPagination.page = 1
hospitalSelected.value = []
hospitalDialogTitle.value = `地区医院配置 — ${row.regionName}`
hospitalDialogVisible.value = true
loadHospitals()
}
async function loadHospitals() {
if (!currentRegionId.value) return
hospitalLoading.value = true
try {
const res = await fetchHospitalList(currentRegionId.value, {
page: hospitalPagination.page,
pageSize: hospitalPagination.pageSize
})
hospitalTable.value = res.list || []
hospitalPagination.total = res.total ?? 0
} finally {
hospitalLoading.value = false
}
}
function onHospitalDialogClosed() {
currentRegionId.value = ''
currentRegionName.value = ''
hospitalTable.value = []
}
function handleAddHospital() {
hospitalIsEdit.value = false
hospitalFormTitle.value = '新增医院'
editingHospitalId.value = null
hospitalForm.hospitalName = ''
hospitalForm.orderNum = 0
hospitalForm.remark = ''
hospitalFormVisible.value = true
hospitalFormRef.value?.clearValidate?.()
}
function handleEditHospital(row: Api.BizCollectionHospital.HospitalItem) {
hospitalIsEdit.value = true
hospitalFormTitle.value = '编辑医院'
editingHospitalId.value = row.id
hospitalForm.hospitalName = row.hospitalName || ''
hospitalForm.orderNum = row.orderNum ?? 0
hospitalForm.remark = row.remark || ''
hospitalFormVisible.value = true
hospitalFormRef.value?.clearValidate?.()
}
async function submitHospital() {
await hospitalFormRef.value?.validate?.()
hospitalSubmitLoading.value = true
try {
const rid = currentRegionId.value
const payload = {
hospitalName: hospitalForm.hospitalName.trim(),
orderNum: hospitalForm.orderNum,
remark: hospitalForm.remark?.trim() || undefined
}
if (hospitalIsEdit.value && editingHospitalId.value) {
await fetchUpdateHospital(rid, editingHospitalId.value, payload)
} else {
await fetchCreateHospital(rid, payload)
}
ElMessage.success(hospitalIsEdit.value ? '更新成功' : '创建成功')
hospitalFormVisible.value = false
await loadHospitals()
} catch {
/* */
} finally {
hospitalSubmitLoading.value = false
}
}
async function handleDeleteHospital(row: Api.BizCollectionHospital.HospitalItem) {
await ElMessageBox.confirm('确定删除该医院吗?', '提示', { type: 'warning' })
await fetchDeleteHospital(currentRegionId.value, row.id)
ElMessage.success('删除成功')
await loadHospitals()
}
async function handleBatchDeleteHospital() {
const ids = hospitalSelected.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 家医院吗?`, '提示', {
type: 'warning'
})
const rid = currentRegionId.value
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteHospital(rid, id)
}
ElMessage.success('批量删除成功')
hospitalSelected.value = []
await loadHospitals()
}, fullscreenLoadingText.batchDelete)
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.hospital-dialog-head {
margin-bottom: 12px;
}
.hospital-dialog-head .muted {
font-size: 13px;
color: var(--mint-text-muted);
}
.hospital-toolbar {
margin-bottom: 0;
}
.hospital-table-wrap {
padding-top: 0;
}
.hospital-cell-line {
line-height: 1.8;
text-align: left;
padding-left: 8px;
}
.muted {
color: var(--mint-text-muted);
}
</style>
@@ -0,0 +1,431 @@
<!-- 客户管理 -->
<!-- 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"
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="customerName"
label="客户姓名"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column prop="sex" label="性别" width="72" align="center" header-align="center">
<template #default="{ row }">{{ sexLabel(row.sex) }}</template>
</el-table-column>
<el-table-column
prop="idCardNo"
label="身份证号"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="age"
label="年龄"
width="72"
align="center"
header-align="center"
/>
<el-table-column
prop="phoneNo"
label="联系电话"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="customerTags"
label="客户标签"
min-width="140"
show-overflow-tooltip
/>
<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="dialogVisible"
:title="dialogTitle"
width="720px"
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
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="客户姓名" prop="customerName">
<el-input v-model="formData.customerName" placeholder="必填" />
</el-form-item>
<el-form-item label="性别" prop="sex">
<el-select v-model="formData.sex" placeholder="选择" style="width: 100%">
<el-option label="男" value="0" />
<el-option label="女" value="1" />
<el-option label="未知" value="2" />
</el-select>
</el-form-item>
<el-form-item label="身份证号" prop="idCardNo">
<el-input
v-model="formData.idCardNo"
placeholder="可选;填写后租户内不可重复"
clearable
/>
</el-form-item>
<el-form-item label="年龄">
<el-input
type="number"
step="any"
v-model="formData.age"
:min="0"
:max="150"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="formData.phoneNo" placeholder="可选" clearable />
</el-form-item>
<el-form-item label="客户标签" class="form-col-span-2">
<el-input v-model="formData.customerTags" placeholder="多个标签用英文逗号分隔" />
</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?.customerName || '客户详情'"
:icon="User"
:meta="`身份证:${viewRow?.idCardNo || '—'}${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, User } from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchCustomerList,
fetchCreateCustomer,
fetchUpdateCustomer,
fetchDeleteCustomer
} from '@/api/admin'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
defineOptions({ name: 'BizCustomer' })
function sexLabel(s: string) {
if (s === '0') return '男'
if (s === '1') return '女'
return '未知'
}
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
const tableData = ref<Api.BizCustomer.CustomerItem[]>([])
const selectedRows = ref<Api.BizCustomer.CustomerItem[]>([])
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.BizCustomer.CustomerItem | null>(null)
const viewSections = computed(() => {
const row = viewRow.value
if (!row) return []
return [
{
title: '基本信息',
fields: [
{ label: '客户姓名', value: row.customerName },
{ label: '性别', value: sexLabel(row.sex) },
{ label: '身份证号', value: row.idCardNo },
{ label: '年龄', value: row.age },
{ label: '联系电话', value: row.phoneNo },
{ label: '客户标签', value: row.customerTags, span: true },
{ label: '创建时间', value: row.createTime },
{ label: '备注', value: row.remark, span: true }
]
}
]
})
function handleView(row: Api.BizCustomer.CustomerItem) {
viewRow.value = row
viewVisible.value = true
}
const formData = reactive({
customerName: '',
sex: '2',
idCardNo: '',
age: null as number | null,
phoneNo: '',
customerTags: '',
orderNum: 0,
remark: ''
})
const formRules = {
customerName: [{ required: true, message: '请输入客户姓名', trigger: 'blur' }]
}
const handleSelectionChange = (rows: Api.BizCustomer.CustomerItem[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchCustomerList({
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.customerName = ''
formData.sex = '2'
formData.idCardNo = ''
formData.age = null
formData.phoneNo = ''
formData.customerTags = ''
formData.orderNum = 0
formData.remark = ''
editingId.value = null
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增客户'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleEdit(row: Api.BizCustomer.CustomerItem) {
isEdit.value = true
dialogTitle.value = '编辑客户'
editingId.value = row.id
formData.customerName = row.customerName || ''
formData.sex = row.sex || '2'
formData.idCardNo = row.idCardNo || ''
formData.age = row.age ?? null
formData.phoneNo = row.phoneNo || ''
formData.customerTags = row.customerTags || ''
formData.orderNum = row.orderNum ?? 0
formData.remark = row.remark || ''
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
async function handleSubmit() {
await formRef.value?.validate?.()
submitLoading.value = true
try {
const payload = {
customerName: formData.customerName.trim(),
sex: formData.sex,
idCardNo: formData.idCardNo?.trim() || undefined,
age: formData.age,
phoneNo: formData.phoneNo?.trim() || undefined,
customerTags: formData.customerTags?.trim() || undefined,
orderNum: formData.orderNum,
remark: formData.remark?.trim() || undefined
}
if (isEdit.value && editingId.value) {
await fetchUpdateCustomer(editingId.value, payload)
} else {
await fetchCreateCustomer(payload)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
/* 全局处理 */
} finally {
submitLoading.value = false
}
}
async function handleDelete(row: Api.BizCustomer.CustomerItem) {
await ElMessageBox.confirm('确定要删除该客户吗?', '提示', { type: 'warning' })
await fetchDeleteCustomer(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 fetchDeleteCustomer(id)
}
ElMessage.success('批量删除成功')
selectedRows.value = []
await loadData()
}, fullscreenLoadingText.batchDelete)
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -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>