Files
zkxw/web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue
T
2026-06-02 09:39:50 +08:00

614 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 采集协议单管理 · 协议单 Tab -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="protocol-audit-tab">
<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>
<el-form-item label="协议状态">
<el-select
v-model="searchForm.protocolStatus"
clearable
placeholder="全部"
style="width: 100%"
>
<el-option label="待生效" value="dsx" />
<el-option label="生效中" value="sxz" />
<el-option label="已过期" value="ygq" />
<el-option label="已作废" value="yzf" />
<el-option label="转科研" value="zky" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="样本采集时间">
<el-date-picker
v-model="sampleCollectTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint"
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
>
<el-button class="btn-main" type="primary" @click="handleSearch"
><el-icon><Search /></el-icon>搜索</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 type="primary" class="btn-main" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openAuditDialog()">
<el-icon><CircleCheck /></el-icon>
审核
</el-button>
<el-button
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openStatusChangeDialog('activate')"
>
协议生效
</el-button>
<el-button
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openStatusChangeDialog('expire')"
>
协议过期
</el-button>
<el-button
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openStatusChangeDialog('void')"
>
协议作废
</el-button>
<el-button
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openStatusChangeDialog('toResearch')"
>
协议转科研
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchDelete">
<el-icon><Delete /></el-icon>
批量删除
</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" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="protocolNo"
label="协议号"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="protocolStatus"
label="状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column
prop="expectedDeliveryDate"
label="预产期"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="sampleCollectSummary"
label="是否已采集"
min-width="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="registerDate"
label="登记日期"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="motherName"
label="母亲姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="motherPhone"
label="母亲电话"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="motherIdCard"
label="母亲身份证号"
min-width="190"
align="center"
header-align="center"
/>
<el-table-column
prop="fatherName"
label="父亲姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="newbornSex"
label="新生儿性别"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">{{
newbornSexLabel(String(row.newbornSex || ''))
}}</template>
</el-table-column>
<el-table-column
prop="deliveryMode"
label="分娩方式"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{
deliveryModeLabel(String(row.deliveryMode || ''))
}}</template>
</el-table-column>
<el-table-column
prop="newbornWeightGram"
label="新生儿体重(克)"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="sampleTypeNames"
label="样本类型"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="sampleCodes"
label="样本编码"
min-width="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="regionName"
label="采集地区"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="hospitalName"
label="采集医院"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="collectorUserName"
label="采集专员"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="salesUserName"
label="销售人员"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="sampleCollectTime"
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="160"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<div style="display: flex; justify-content: center; gap: 8px">
<el-button class="btn-op-item" @click="openAuditDialog(row)">
<el-icon><CircleCheck /></el-icon>审核
</el-button>
<el-dropdown trigger="hover" @command="(cmd: string) => handleDropdownCommand(cmd, row)">
<el-button class="btn-op-item">
<el-icon><More /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="view">
<el-icon><View /></el-icon>查看
</el-dropdown-item>
<el-dropdown-item command="edit">
<el-icon><Edit /></el-icon>编辑
</el-dropdown-item>
<el-dropdown-item command="delete" divided>
<el-icon><Delete /></el-icon>删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</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>
<!-- 新增/编辑弹窗 -->
<ProtocolFormDialog
v-model="formDialogVisible"
:edit-id="formDialogEditId"
:list-row="formDialogRow"
@saved="onFormSaved"
/>
<!-- 审核弹窗 -->
<ProtocolAuditDialog
v-model="auditDialogVisible"
:targets="auditTargets"
@submitted="onAuditSubmitted"
/>
<!-- 状态变更弹窗(生效/过期/作废/转科研) -->
<ProtocolStatusChangeDialog
v-model="statusChangeDialogVisible"
:action-type="statusChangeAction"
:targets="statusChangeTargets"
@submitted="onStatusChangeSubmitted"
/>
<!-- 查看弹窗 -->
<ProtocolViewDialog
v-model="viewVisible"
:protocol-id="viewProtocolId"
:list-row="viewRow"
show-audit-history
show-storage-cert
show-fullmoon-insurance
/>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
View,
Edit,
Delete,
Search,
RefreshLeft,
CircleCheck,
More
} from '@element-plus/icons-vue'
import ProtocolFormDialog from '../protocol/ProtocolFormDialog.vue'
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
import ProtocolAuditDialog, {
type ProtocolAuditTarget
} from '../protocol/ProtocolAuditDialog.vue'
import ProtocolStatusChangeDialog, {
type StatusChangeAction,
type StatusChangeTarget
} from '../protocol/ProtocolStatusChangeDialog.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchCollectionProtocolList,
fetchDeleteCollectionProtocol,
type CollectionProtocolRow
} from '@/api/collection-protocol'
import {
protocolStatusLabel,
protocolStatusTagType,
newbornSexLabel,
deliveryModeLabel
} from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageProtocolAuditTab' })
function formatLocalDate(d: Date): string {
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
function getDefaultCreateTimeRange(): [string, string] {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 30)
return [formatLocalDate(start), formatLocalDate(end)]
}
/* ====== 列表 ====== */
const searchForm = reactive<{ keyword: string; protocolStatus: string }>({
keyword: '',
protocolStatus: ''
})
const createTimeRange = ref<[string, string] | null>(getDefaultCreateTimeRange())
const sampleCollectTimeRange = ref<[string, string] | null>(null)
const tableData = ref<CollectionProtocolRow[]>([])
const selectedRows = ref<CollectionProtocolRow[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const handleSelectionChange = (rows: CollectionProtocolRow[]) => {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchCollectionProtocolList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
protocolStatus: searchForm.protocolStatus || undefined,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined,
sampleCollectTimeStart: sampleCollectTimeRange.value?.[0] || undefined,
sampleCollectTimeEnd: sampleCollectTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.protocolStatus = ''
createTimeRange.value = getDefaultCreateTimeRange()
sampleCollectTimeRange.value = null
pagination.page = 1
loadData()
}
/* ====== 查看弹窗 ====== */
const viewVisible = ref(false)
const viewProtocolId = ref<string | null>(null)
const viewRow = ref<CollectionProtocolRow | null>(null)
function handleView(row: CollectionProtocolRow) {
viewProtocolId.value = String(row.id)
viewRow.value = row
viewVisible.value = true
}
/* ====== 新增/编辑弹窗 ====== */
const formDialogVisible = ref(false)
const formDialogEditId = ref<string | null>(null)
const formDialogRow = ref<CollectionProtocolRow | null>(null)
function handleAdd() {
formDialogEditId.value = null
formDialogRow.value = null
formDialogVisible.value = true
}
function handleEdit(row: CollectionProtocolRow) {
formDialogEditId.value = row.id
formDialogRow.value = row
formDialogVisible.value = true
}
function onFormSaved() {
loadData()
}
/* ====== 审核 ====== */
const auditDialogVisible = ref(false)
const auditTargets = ref<ProtocolAuditTarget[]>([])
function openAuditDialog(row?: CollectionProtocolRow) {
const rows = row ? [row] : selectedRows.value
if (!rows.length) {
ElMessage.warning('请先选择要审核的协议单')
return
}
auditTargets.value = rows.map((r) => ({
id: String(r.id),
protocolNo: r.protocolNo != null ? String(r.protocolNo) : undefined
}))
auditDialogVisible.value = true
}
function onAuditSubmitted() {
selectedRows.value = []
loadData()
}
/* ====== 状态变更(生效/过期/作废/转科研) ====== */
const statusChangeDialogVisible = ref(false)
const statusChangeAction = ref<StatusChangeAction>('activate')
const statusChangeTargets = ref<StatusChangeTarget[]>([])
function openStatusChangeDialog(action: StatusChangeAction, row?: CollectionProtocolRow) {
const rows = row ? [row] : selectedRows.value
if (!rows.length) {
const labels: Record<StatusChangeAction, string> = {
activate: '协议生效',
expire: '协议过期',
void: '协议作废',
toResearch: '协议转科研'
}
ElMessage.warning(`请先选择要操作的协议单`)
return
}
statusChangeAction.value = action
statusChangeTargets.value = rows.map((r) => ({
id: String(r.id),
protocolNo: r.protocolNo != null ? String(r.protocolNo) : undefined
}))
statusChangeDialogVisible.value = true
}
function onStatusChangeSubmitted() {
selectedRows.value = []
loadData()
}
function handleDropdownCommand(command: string, row: any) {
switch (command) {
case 'view': handleView(row); break
case 'edit': handleEdit(row); break
case 'delete': handleDelete(row); break
}
}
/* ====== 删除 ====== */
function handleDelete(row: CollectionProtocolRow) {
ElMessageBox.confirm(`确认删除协议号「${row.protocolNo}」?`, '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
await fetchDeleteCollectionProtocol(String(row.id))
loadData()
})
.catch(() => {})
}
async function handleBatchDelete() {
if (!selectedRows.value.length) return
await ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录?`, '批量删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const rows = [...selectedRows.value]
await withFullscreenLoading(async () => {
for (const row of rows) {
await fetchDeleteCollectionProtocol(String(row.id))
}
ElMessage.success('批量删除成功')
selectedRows.value = []
loadData()
}, fullscreenLoadingText.batchDelete)
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
[searchForm, createTimeRange, sampleCollectTimeRange],
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
onMounted(() => {
loadData()
})
</script>