1033 lines
31 KiB
Vue
1033 lines
31 KiB
Vue
<!-- 采集协议单管理 · 协议单 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">
|
||
<div v-loading="loading" class="protocol-card-section">
|
||
<div v-if="tableData.length" class="protocol-card-list">
|
||
<article
|
||
v-for="(row, index) in tableData"
|
||
:key="String(row.id)"
|
||
class="protocol-card"
|
||
:class="{ 'is-selected': isRowSelected(String(row.id)) }"
|
||
@dblclick="handleView(row)"
|
||
>
|
||
<div class="protocol-card__select" @click.stop>
|
||
<el-checkbox
|
||
:model-value="isRowSelected(String(row.id))"
|
||
@change="(val) => toggleRowSelection(row, !!val)"
|
||
/>
|
||
</div>
|
||
<div class="protocol-card__aside">
|
||
<div class="protocol-card__name-row">
|
||
<span class="protocol-card__index">{{
|
||
(pagination.page - 1) * pagination.pageSize + index + 1
|
||
}}</span>
|
||
<span class="protocol-card__name">{{ row.motherName || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__contact">
|
||
<div class="protocol-card__sub">{{ row.motherPhone || '—' }}</div>
|
||
<div class="protocol-card__sub protocol-card__sub--id">
|
||
{{ row.motherIdCard ? maskIdCard(String(row.motherIdCard)) : '—' }}
|
||
</div>
|
||
</div>
|
||
<el-tag
|
||
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
|
||
size="small"
|
||
class="protocol-card__status"
|
||
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
|
||
>
|
||
<div v-if="row.createTime" class="protocol-card__create-time">
|
||
创建 {{ row.createTime }}
|
||
</div>
|
||
</div>
|
||
<div class="protocol-card__sections">
|
||
<section class="protocol-card__block protocol-card__block--basic">
|
||
<div class="protocol-card__block-title">基础信息</div>
|
||
<div class="protocol-card__kv-grid">
|
||
<div class="protocol-card__kv">
|
||
<label>协议号</label><span>{{ row.protocolNo || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>登记日期</label><span>{{ row.registerDate || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>采集地区</label><span>{{ row.regionName || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>采集医院</label
|
||
><span :title="String(row.hospitalName || '')">{{
|
||
row.hospitalName || '—'
|
||
}}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>采集专员</label><span>{{ row.collectorUserName || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>样本采集</label><span>{{ row.sampleCollectSummary || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>采集时间</label><span>{{ row.sampleCollectTime || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="protocol-card__block protocol-card__block--newborn">
|
||
<div class="protocol-card__block-title">新生儿</div>
|
||
<div class="protocol-card__kv-grid">
|
||
<div class="protocol-card__kv">
|
||
<label>姓名</label><span>{{ row.newbornName || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>性别</label
|
||
><span>{{ newbornSexLabel(String(row.newbornSex || '')) || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>预产期</label><span>{{ row.expectedDeliveryDate || '—' }}</span>
|
||
</div>
|
||
<div class="protocol-card__kv">
|
||
<label>分娩时间</label><span>{{ row.newbornDeliveryTime || '—' }}</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="protocol-card__block protocol-card__block--samples">
|
||
<div class="protocol-card__block-title">
|
||
样本信息
|
||
<span v-if="getSampleItems(row).length" class="protocol-card__block-count">
|
||
{{ getSampleItems(row).length }} 项
|
||
</span>
|
||
</div>
|
||
<div v-if="getSampleItems(row).length" class="protocol-card__sample-list">
|
||
<div
|
||
v-for="(item, sIdx) in getSampleItems(row)"
|
||
:key="sIdx"
|
||
class="protocol-card__sample-item"
|
||
>
|
||
<span class="protocol-card__sample-seq">{{ sIdx + 1 }}</span>
|
||
<div class="protocol-card__sample-main">
|
||
<div class="protocol-card__sample-line">
|
||
<span class="protocol-card__sample-label">类型</span>
|
||
<span class="protocol-card__sample-value">{{
|
||
item.sampleTypeName || '—'
|
||
}}</span>
|
||
<el-tag
|
||
v-if="item.sampleStatus"
|
||
:type="sampleStatusTagType(item.sampleStatus)"
|
||
size="small"
|
||
class="protocol-card__sample-status"
|
||
>
|
||
{{ sampleStatusLabel(item.sampleStatus) }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="protocol-card__sample-line">
|
||
<span class="protocol-card__sample-label">编码</span>
|
||
<span class="protocol-card__sample-value protocol-card__sample-value--code">
|
||
{{ item.sampleCode || '—' }}
|
||
</span>
|
||
</div>
|
||
<div class="protocol-card__sample-line">
|
||
<span class="protocol-card__sample-label">运输</span>
|
||
<span class="protocol-card__sample-value">{{
|
||
formatTransportSummary(item.transport)
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="protocol-card__empty">暂无样本</div>
|
||
</section>
|
||
|
||
<section v-if="getPaymentSummary(row)" class="protocol-card__block protocol-card__block--payment">
|
||
<div class="protocol-card__block-title">缴费信息</div>
|
||
<QcWorkOrderProtocolPaymentPanel
|
||
:summary="getPaymentSummary(row)"
|
||
compact
|
||
embedded
|
||
hide-overall-pay-status
|
||
hide-installment-detail-amounts
|
||
/>
|
||
</section>
|
||
<section v-else class="protocol-card__block protocol-card__block--payment">
|
||
<div class="protocol-card__block-title">缴费信息</div>
|
||
<div class="protocol-card__empty">暂无缴费信息</div>
|
||
</section>
|
||
</div>
|
||
<div class="protocol-card__foot">
|
||
<div class="protocol-card__actions">
|
||
<el-button class="btn-op-item" @click.stop="openAuditDialog(row)">
|
||
<el-icon><CircleCheck /></el-icon>审核
|
||
</el-button>
|
||
<el-button class="btn-op-item" @click.stop="handleView(row)">
|
||
<el-icon><View /></el-icon>查看
|
||
</el-button>
|
||
<el-button class="btn-op-item" @click.stop="handleEdit(row)">
|
||
<el-icon><Edit /></el-icon>编辑
|
||
</el-button>
|
||
<el-button class="btn-op-item" @click.stop="handleDelete(row)">
|
||
<el-icon><Delete /></el-icon>删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
<el-empty
|
||
v-else-if="!loading"
|
||
class="protocol-card-empty"
|
||
description="暂无协议单"
|
||
:image-size="80"
|
||
/>
|
||
</div>
|
||
<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
|
||
} 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 QcWorkOrderProtocolPaymentPanel from '@/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue'
|
||
import type { QcProtocolPaymentSummary } from '@/api/qc-work-order'
|
||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||
import {
|
||
fetchCollectionProtocolList,
|
||
fetchDeleteCollectionProtocol,
|
||
type CollectionProtocolRow
|
||
} from '@/api/collection-protocol'
|
||
import {
|
||
formatProtocolDocDeleteConfirm,
|
||
formatProtocolDocBatchDeleteConfirm
|
||
} from '@/utils/protocol-doc-label'
|
||
import {
|
||
protocolStatusLabel,
|
||
protocolStatusTagType,
|
||
newbornSexLabel,
|
||
sampleStatusLabel,
|
||
sampleStatusTagType,
|
||
transportTypeLabel
|
||
} from '@/utils/status-tag-registry'
|
||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||
|
||
defineOptions({ name: 'CellStorageProtocolAuditTab' })
|
||
|
||
type ProtocolSampleTransport = {
|
||
transportType?: string
|
||
destSiteName?: string
|
||
expressCompanyName?: string
|
||
waybillNo?: string
|
||
carrierName?: string
|
||
carrierPhone?: string
|
||
etaTime?: string
|
||
}
|
||
|
||
type ProtocolSampleItem = {
|
||
sampleTypeName: string
|
||
sampleCode: string
|
||
sampleStatus: string
|
||
labStorageCode: string
|
||
transport?: ProtocolSampleTransport | null
|
||
}
|
||
|
||
function getPaymentSummary(row: CollectionProtocolRow): QcProtocolPaymentSummary | null {
|
||
const summary = row.paymentSummary
|
||
if (!summary || typeof summary !== 'object') return null
|
||
return summary as QcProtocolPaymentSummary
|
||
}
|
||
|
||
function formatTransportSummary(transport?: ProtocolSampleTransport | null): string {
|
||
if (!transport?.transportType) return '—'
|
||
const parts: string[] = [transportTypeLabel(String(transport.transportType))]
|
||
const dest = String(transport.destSiteName || '').trim()
|
||
if (dest) parts.push(dest)
|
||
if (transport.transportType === 'kdys') {
|
||
const expressName = String(transport.expressCompanyName || '').trim()
|
||
if (expressName) parts.push(expressName)
|
||
const waybill = String(transport.waybillNo || '').trim()
|
||
if (waybill) parts.push(waybill)
|
||
} else if (transport.transportType === 'ryxd') {
|
||
const carrier = String(transport.carrierName || '').trim()
|
||
if (carrier) parts.push(carrier)
|
||
const phone = String(transport.carrierPhone || '').trim()
|
||
if (phone) parts.push(phone)
|
||
}
|
||
const eta = String(transport.etaTime || '').trim()
|
||
if (eta) parts.push(`预计 ${eta}`)
|
||
return parts.join(' · ')
|
||
}
|
||
|
||
function getSampleItems(row: CollectionProtocolRow): ProtocolSampleItem[] {
|
||
const rawItems = row.sampleItems
|
||
if (Array.isArray(rawItems) && rawItems.length) {
|
||
return rawItems.map((item) => {
|
||
const rec = item as Record<string, unknown>
|
||
const transportRaw = rec.transport
|
||
const transport =
|
||
transportRaw && typeof transportRaw === 'object'
|
||
? (transportRaw as ProtocolSampleTransport)
|
||
: null
|
||
return {
|
||
sampleTypeName: String(rec.sampleTypeName || '').trim(),
|
||
sampleCode: String(rec.sampleCode || '').trim(),
|
||
sampleStatus: String(rec.sampleStatus || '').trim(),
|
||
labStorageCode: String(rec.labStorageCode || '').trim(),
|
||
transport
|
||
}
|
||
})
|
||
}
|
||
const types = String(row.sampleTypeNames || '')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
const codes = String(row.sampleCodes || '')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
const labCodes = String(row.labStorageCodes || '')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
const len = Math.max(types.length, codes.length)
|
||
if (!len) return []
|
||
return Array.from({ length: len }, (_, i) => ({
|
||
sampleTypeName: types[i] || '',
|
||
sampleCode: codes[i] || '',
|
||
sampleStatus: '',
|
||
labStorageCode: labCodes[i] || '',
|
||
transport: null
|
||
}))
|
||
}
|
||
|
||
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 isRowSelected = (id: string) => selectedRows.value.some((r) => String(r.id) === id)
|
||
|
||
const toggleRowSelection = (row: CollectionProtocolRow, checked: boolean) => {
|
||
const id = String(row.id)
|
||
if (checked) {
|
||
if (!isRowSelected(id)) selectedRows.value = [...selectedRows.value, row]
|
||
return
|
||
}
|
||
selectedRows.value = selectedRows.value.filter((r) => String(r.id) !== id)
|
||
}
|
||
|
||
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
|
||
const validIds = new Set(tableData.value.map((r) => String(r.id)))
|
||
selectedRows.value = selectedRows.value.filter((r) => validIds.has(String(r.id)))
|
||
} 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),
|
||
motherName: r.motherName != null ? String(r.motherName) : 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),
|
||
motherName: r.motherName != null ? String(r.motherName) : undefined
|
||
}))
|
||
statusChangeDialogVisible.value = true
|
||
}
|
||
|
||
function onStatusChangeSubmitted() {
|
||
selectedRows.value = []
|
||
loadData()
|
||
}
|
||
|
||
/* ====== 删除 ====== */
|
||
function handleDelete(row: CollectionProtocolRow) {
|
||
ElMessageBox.confirm(formatProtocolDocDeleteConfirm('cell_storage', row), '删除确认', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(async () => {
|
||
await fetchDeleteCollectionProtocol(String(row.id))
|
||
loadData()
|
||
})
|
||
.catch(() => {})
|
||
}
|
||
|
||
async function handleBatchDelete() {
|
||
if (!selectedRows.value.length) return
|
||
await ElMessageBox.confirm(
|
||
formatProtocolDocBatchDeleteConfirm('cell_storage', 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>
|
||
|
||
<style scoped lang="scss">
|
||
.protocol-card-section {
|
||
min-height: 120px;
|
||
}
|
||
|
||
.protocol-card-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.protocol-card {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: stretch;
|
||
gap: 12px;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
border: 1px solid var(--mint-border, #e2e8f0);
|
||
border-radius: 10px;
|
||
background: #fff;
|
||
padding: 12px 14px 12px 40px;
|
||
transition:
|
||
border-color 0.15s,
|
||
box-shadow 0.15s;
|
||
|
||
&:hover {
|
||
border-color: #c5e0d4;
|
||
box-shadow: 0 2px 8px rgb(15 23 42 / 5%);
|
||
}
|
||
|
||
&.is-selected {
|
||
border-color: var(--primary, #10b981);
|
||
box-shadow: 0 0 0 1px rgb(16 185 129 / 25%);
|
||
}
|
||
}
|
||
|
||
.protocol-card__select {
|
||
position: absolute;
|
||
top: 12px;
|
||
left: 12px;
|
||
z-index: 1;
|
||
}
|
||
|
||
.protocol-card__aside {
|
||
flex: 0 0 148px;
|
||
min-width: 132px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding-right: 12px;
|
||
border-right: 1px solid #eef2f6;
|
||
}
|
||
|
||
.protocol-card__status {
|
||
align-self: flex-start;
|
||
}
|
||
|
||
.protocol-card__create-time {
|
||
margin-top: auto;
|
||
font-size: 11px;
|
||
color: var(--mint-text-muted, #64748b);
|
||
line-height: 1.35;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.protocol-card__name-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.protocol-card__index {
|
||
flex-shrink: 0;
|
||
min-width: 22px;
|
||
height: 22px;
|
||
padding: 0 6px;
|
||
border-radius: 6px;
|
||
background: var(--primary-bg, #ecfdf5);
|
||
color: var(--primary-dark, #059669);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
line-height: 22px;
|
||
text-align: center;
|
||
}
|
||
|
||
.protocol-card__name {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--text, #1e293b);
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.protocol-card__contact {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.protocol-card__sub {
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #64748b);
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
|
||
&--id {
|
||
white-space: normal;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.protocol-card__sections {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: stretch;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
.protocol-card__block {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
background: #f8fafc;
|
||
border: 1px solid #eef2f6;
|
||
|
||
&--basic {
|
||
flex: 1.1;
|
||
min-width: 180px;
|
||
}
|
||
|
||
&--newborn {
|
||
flex: 0.85;
|
||
min-width: 140px;
|
||
}
|
||
|
||
&--samples {
|
||
flex: 1.35;
|
||
min-width: 220px;
|
||
}
|
||
|
||
&--payment {
|
||
flex: 1.2;
|
||
min-width: 200px;
|
||
padding-bottom: 4px;
|
||
|
||
:deep(.qc-protocol-payment--compact) {
|
||
padding: 0;
|
||
background: transparent;
|
||
}
|
||
|
||
:deep(.qc-protocol-payment__compact-details) {
|
||
border-top-color: #e2e8f0;
|
||
}
|
||
|
||
:deep(.qc-protocol-payment__detail-chip) {
|
||
background: #fff;
|
||
border-color: #e2e8f0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.protocol-card__block-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--primary-dark, #059669);
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.protocol-card__block-count {
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
color: var(--mint-text-muted, #64748b);
|
||
}
|
||
|
||
.protocol-card__kv-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.protocol-card__kv {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
font-size: 12px;
|
||
line-height: 1.35;
|
||
|
||
label {
|
||
flex-shrink: 0;
|
||
color: var(--mint-text-muted, #334155);
|
||
}
|
||
|
||
span {
|
||
min-width: 0;
|
||
color: var(--text, #1e293b);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
}
|
||
|
||
.protocol-card__empty {
|
||
font-size: 12px;
|
||
color: var(--mint-text-muted, #334155);
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.protocol-card__sample-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.protocol-card__sample-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 6px 8px;
|
||
border-radius: 6px;
|
||
background: #fff;
|
||
border: 1px solid var(--mint-border, #e2e8f0);
|
||
}
|
||
|
||
.protocol-card__sample-seq {
|
||
flex-shrink: 0;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 4px;
|
||
background: var(--primary-bg, #ecfdf5);
|
||
color: var(--primary-dark, #059669);
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
line-height: 18px;
|
||
text-align: center;
|
||
}
|
||
|
||
.protocol-card__sample-main {
|
||
min-width: 0;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.protocol-card__sample-line {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
font-size: 12px;
|
||
line-height: 1.35;
|
||
}
|
||
|
||
.protocol-card__sample-label {
|
||
flex-shrink: 0;
|
||
width: 28px;
|
||
color: var(--mint-text-muted, #334155);
|
||
}
|
||
|
||
.protocol-card__sample-value {
|
||
min-width: 0;
|
||
flex: 1;
|
||
color: var(--text, #1e293b);
|
||
word-break: break-all;
|
||
|
||
&--code {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
|
||
.protocol-card__sample-status {
|
||
flex-shrink: 0;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.protocol-card__foot {
|
||
flex: 0 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding-left: 12px;
|
||
border-left: 1px solid #eef2f6;
|
||
}
|
||
|
||
.protocol-card__actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
align-items: stretch;
|
||
gap: 6px;
|
||
min-width: 72px;
|
||
|
||
:deep(.el-button) {
|
||
width: 100%;
|
||
margin: 0;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
.protocol-card-empty {
|
||
padding: 32px 16px;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.protocol-card {
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.protocol-card__sections {
|
||
flex-basis: 100%;
|
||
order: 3;
|
||
}
|
||
|
||
.protocol-card__foot {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
border-left: none;
|
||
border-top: 1px solid #eef2f6;
|
||
padding-left: 0;
|
||
padding-top: 10px;
|
||
margin-left: 0;
|
||
order: 4;
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.protocol-card__sections {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.protocol-card__block {
|
||
min-width: 0 !important;
|
||
}
|
||
}
|
||
</style>
|