Files
zkxw/web-admin/src/views/cell-storage/protocol-audit/ProtocolAuditTab.vue
T
2026-06-16 13:01:17 +08:00

1033 lines
31 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">
<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>