This commit is contained in:
zlwu committed 2026-06-02 09:39:50 +08:00
1 parent 2e66e03792
commit 09afdb1d65
93 files changed
+22307 -15075

No files matched your search

@@ -155,6 +155,21 @@
>
</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="登记日期"
@@ -219,6 +234,22 @@
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="采集地区"
@@ -0,0 +1,364 @@
<!-- 样本信息管理 · 样本质检信息 Tab -->
<template>
<div class="sample-qc-info-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.qcStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="待质检" value="dzj" />
<el-option label="质检中" value="zjz" />
<el-option label="通过" value="tg" />
<el-option label="不通过" value="btg" />
<el-option label="已作废" value="yzf" />
</el-select>
</el-form-item>
<el-form-item label="通知状态">
<el-select
v-model="searchForm.smsNotified"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="已通知" value="1" />
<el-option label="未通知" value="0" />
</el-select>
</el-form-item>
<el-form-item label="样本时间">
<el-date-picker
v-model="sampleTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
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-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleView"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 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="motherName"
label="母亲姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="newbornName"
label="新生儿姓名"
min-width="110"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="sampleCode"
label="样本编码"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="sampleTypeName"
label="样本类型"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column
prop="qcCompleteTime"
label="质检完成时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
prop="qcStatusLabel"
label="质检状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="qcStatusTagType(String(row.qcStatus || ''))" size="small">
{{ row.qcStatusLabel || '待质检' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="smsNotified"
label="通知状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.smsNotified == 1 ? 'success' : 'info'" size="small">
{{ row.smsNotified == 1 ? '已通知' : '未通知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="360"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button class="btn-op-item" @click="handleExtractSms(row)">
<el-icon><ChatDotRound /></el-icon>提取短信
</el-button>
<el-button class="btn-op-item" @click="handleToggleNotified(row)">
<el-icon><Bell /></el-icon>{{ row.smsNotified == 1 ? '标记未通知' : '标记已通知' }}
</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>
<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 { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, RefreshLeft, View, ChatDotRound, Bell } from '@element-plus/icons-vue'
import { useClipboard } from '@vueuse/core'
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
import {
fetchQcSmsList,
fetchToggleQcSmsNotified,
fetchGenerateQcSms
} from '@/api/qc-sms-notification'
import { type CollectionProtocolRow } from '@/api/collection-protocol'
import {
protocolStatusLabel,
protocolStatusTagType
} from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageSampleQcInfoTab' })
function qcStatusTagType(status: string) {
const map: Record<string, 'success' | 'warning' | 'danger' | 'info'> = {
dzj: 'info',
zjz: 'warning',
tg: 'success',
btg: 'danger',
yzf: 'danger'
}
return map[status] || 'info'
}
function isQcCompleted(row: Record<string, unknown>) {
return row.qcStatus === 'tg' || row.qcStatus === 'btg'
}
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 getDefaultSampleTimeRange(): [string, string] {
const end = new Date()
const start = new Date()
start.setDate(start.getDate() - 30)
return [formatLocalDate(start), formatLocalDate(end)]
}
const searchForm = reactive({ keyword: '', qcStatus: '', smsNotified: '' })
const sampleTimeRange = ref<[string, string] | null>(getDefaultSampleTimeRange())
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const viewVisible = ref(false)
const viewProtocolId = ref<string | null>(null)
const viewRow = ref<CollectionProtocolRow | null>(null)
const { copy } = useClipboard()
async function loadData() {
loading.value = true
try {
const result = await fetchQcSmsList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
qcStatus: searchForm.qcStatus || undefined,
smsNotified: searchForm.smsNotified || undefined,
qcCompleteTimeStart: sampleTimeRange.value?.[0] || undefined,
qcCompleteTimeEnd: sampleTimeRange.value?.[1] || undefined
})
const res = (result as any) || {}
tableData.value = res.list || []
pagination.total = res.total || 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
Object.assign(searchForm, { keyword: '', qcStatus: '', smsNotified: '' })
sampleTimeRange.value = getDefaultSampleTimeRange()
handleSearch()
}
function handleView(row: Record<string, unknown>) {
const pid = String(row.collectionProtocolId || row.protocolId || '')
if (!pid) {
ElMessage.warning('缺少关联协议单,无法查看')
return
}
viewProtocolId.value = pid
viewRow.value = {
id: pid,
protocolNo: row.protocolNo,
motherName: row.motherName,
newbornName: row.newbornName
}
viewVisible.value = true
}
async function handleExtractSms(row: any) {
try {
const result = await fetchGenerateQcSms(row.id, {
qcWorkOrderId: row.qcWorkOrderId,
protocolSampleId: row.protocolSampleId
})
const content = (result as any)?.smsContent || ''
if (!content || content.startsWith('暂无短信内容')) {
ElMessage.warning(content || '短信内容为空')
return
}
await copy(content)
ElMessage.success('短信内容已复制到剪贴板')
} catch {
ElMessage.error('提取短信失败')
}
}
async function handleToggleNotified(row: any) {
if (!isQcCompleted(row)) {
ElMessage.warning('质检尚未完成,暂不可标记通知状态')
return
}
const newVal = row.smsNotified == 1 ? 0 : 1
try {
await fetchToggleQcSmsNotified(row.id, {
smsNotified: newVal,
qcWorkOrderId: row.qcWorkOrderId,
protocolSampleId: row.protocolSampleId
})
ElMessage.success(newVal === 1 ? '已标记为已通知' : '已标记为未通知')
loadData()
} catch {
ElMessage.error('操作失败')
}
}
watchDebounced(
[searchForm, sampleTimeRange],
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
onMounted(() => {
loadData()
})
</script>
@@ -1,4 +1,4 @@
<!-- 采集协议单管理 · 样本接收信息 Tab -->
<!-- 样本信息管理 · 样本接收信息 Tab -->
<template>
<div class="sample-reception-info-tab">
<div class="filter-block">
@@ -1,4 +1,4 @@
<!-- 采集协议单管理 · 样本入库信息 Tab -->
<!-- 样本信息管理 · 样本入库信息 Tab -->
<template>
<div class="storage-entry-info-tab">
<div class="filter-block">
@@ -1,4 +1,4 @@
<!-- 采集协议单管理(协议单 / 缴费 / 样本接收 / 入库 / 储存证 / 满月待办) -->
<!-- 采集协议单管理(协议单 / 缴费 / 储存证 / 满月待办) -->
<template>
<div class="page-container">
<section class="panel">
@@ -29,8 +29,6 @@
<ProtocolAuditTab v-if="activeTab === 'protocol'" />
<PaymentInfoTab v-else-if="activeTab === 'payment-info'" />
<SampleReceptionInfoTab v-else-if="activeTab === 'sample-reception'" />
<StorageEntryInfoTab v-else-if="activeTab === 'storage-entry'" />
<StorageCertTab v-else-if="activeTab === 'storage-cert'" />
<FullmoonTodoTab v-else-if="activeTab === 'fullmoon-todo'" />
</section>
@@ -39,24 +37,17 @@
<script setup lang="ts">
import type { Component } from 'vue'
import { Document, Wallet, Box, FolderOpened, Medal, Calendar } from '@element-plus/icons-vue'
import { Document, Wallet, Medal, Calendar } from '@element-plus/icons-vue'
import ProtocolAuditTab from './ProtocolAuditTab.vue'
import PaymentInfoTab from './PaymentInfoTab.vue'
import SampleReceptionInfoTab from './SampleReceptionInfoTab.vue'
import StorageEntryInfoTab from './StorageEntryInfoTab.vue'
import StorageCertTab from './StorageCertTab.vue'
import FullmoonTodoTab from './FullmoonTodoTab.vue'
defineOptions({ name: 'CellStorageProtocolAudit' })
const VALID_TABS = [
'protocol',
'payment-info',
'sample-reception',
'storage-entry',
'storage-cert',
'fullmoon-todo'
] as const
const LEGACY_SAMPLE_TABS = ['sample-reception', 'storage-entry'] as const
const VALID_TABS = ['protocol', 'payment-info', 'storage-cert', 'fullmoon-todo'] as const
type EntityTabKey = (typeof VALID_TABS)[number]
@@ -78,18 +69,6 @@
desc: '缴费计划与到账查询',
icon: Wallet
},
{
key: 'sample-reception',
title: '样本接收信息',
desc: '接收结论与短信通知',
icon: Box
},
{
key: 'storage-entry',
title: '样本入库信息',
desc: '样本入库记录查询',
icon: FolderOpened
},
{
key: 'storage-cert',
title: '储存证管理',
@@ -105,10 +84,18 @@
]
const route = useRoute()
const router = useRouter()
const activeTab = ref<EntityTabKey>('protocol')
onMounted(() => {
const tab = String(route.query.tab || '')
if (LEGACY_SAMPLE_TABS.includes(tab as (typeof LEGACY_SAMPLE_TABS)[number])) {
router.replace({
name: 'CellStorageSampleInfo',
query: { ...route.query, tab }
})
return
}
if (VALID_TABS.includes(tab as EntityTabKey)) {
activeTab.value = tab as EntityTabKey
}
@@ -285,7 +285,17 @@
<el-radio label="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="新生儿分娩时间" class="form-col-span-2">
<el-form-item label="预产期">
<el-date-picker
v-model="formData.expectedDeliveryDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择预产期"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="新生儿分娩时间">
<el-date-picker
v-model="formData.newbornDeliveryTime"
type="datetime"
@@ -469,11 +479,11 @@
:disabled="!selectedSamples.length"
@click="openBatchTransportDialog"
>
应用到选中样本
批量配置运输信息
</el-button>
</div>
<p v-if="formData.samples.length" class="sample-transport-hint"
>勾选样本后,点击「应用到选中样本」可批量填写运输信息。点击展开可编辑单个运输信息。</p
>勾选样本后,点击「批量配置运输信息」可统一填写运输信息。点击展开可编辑单个运输信息。</p
>
<!-- Sample Cards -->
@@ -1162,7 +1172,10 @@
} from '@/api/admin'
import { fetchEthnicList, type EthnicOption } from '@/api/ethnic'
import { useUserStore } from '@/store/modules/user'
import { syncFqPaymentDetailRowCount } from '@/utils/protocol-payment-installment'
import {
calcFqInstallmentDueDate,
syncFqPaymentDetailRowCount
} from '@/utils/protocol-payment-installment'
import AmountInput from '@/components/common/amount-input/index.vue'
import { formatMoneyDisplay } from '@/utils/money-format'
import {
@@ -1434,6 +1447,7 @@
fatherIdCard: '',
newbornName: '',
newbornIdCard: '',
expectedDeliveryDate: '',
newbornDeliveryTime: '',
newbornSex: '',
deliveryMode: '',
@@ -1487,6 +1501,7 @@
fatherIdCard: '',
newbornName: '',
newbornIdCard: '',
expectedDeliveryDate: '',
newbornDeliveryTime: '',
newbornSex: '',
deliveryMode: '',
@@ -1634,17 +1649,46 @@
syncPaymentDueAmounts()
}
/** 日期加 N 个月,返回 YYYYMMDD 格式(入参支持 YYYYMMDD 或 YYYY-MM-DD) */
function addMonths(dateStr: string, months: number): string {
const compact = dateStr.replace(/-/g, '')
const y = parseInt(compact.slice(0, 4), 10)
const m = parseInt(compact.slice(4, 6), 10) - 1
const d = parseInt(compact.slice(6, 8), 10)
const date = new Date(y, m + months, d)
const yy = date.getFullYear()
const mm = String(date.getMonth() + 1).padStart(2, '0')
const dd = String(date.getDate()).padStart(2, '0')
return `${yy}${mm}${dd}`
/** 分期开始时间默认值:新生儿分娩时间 > 预产期 > 当前日期(YYYYMMDD) */
function defaultInstallmentStartDateYmd(): string {
const deliveryYmd = datetimeToYmd(formData.newbornDeliveryTime)
if (deliveryYmd) return deliveryYmd
const expectedYmd = datetimeToYmd(formData.expectedDeliveryDate)
if (expectedYmd) return expectedYmd
const now = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
}
function datetimeToYmd(val: string): string {
const compact = String(val || '')
.trim()
.replace(/[-:\sT]/g, '')
.slice(0, 8)
return /^\d{8}$/.test(compact) ? compact : ''
}
function isFqNumericFieldEmpty(val: unknown): boolean {
if (val == null || val === '') return true
const n = Number(val)
return !Number.isFinite(n) || n < 1
}
/**
* 分期默认:期数、每期间隔均为 6;开始时间为空时按分娩/预产期/今日填充。
* resetCountInterval:切换到分期时强制重置期数与间隔(与新增一致)。
*/
function applyFqPaymentDefaults(options?: { resetCountInterval?: boolean }) {
const reset = options?.resetCountInterval ?? false
if (reset || isFqNumericFieldEmpty(formData.installmentCount)) {
formData.installmentCount = 6
}
if (reset || isFqNumericFieldEmpty(formData.installmentIntervalMonths)) {
formData.installmentIntervalMonths = 6
}
if (!formData.installmentStartDate) {
formData.installmentStartDate = defaultInstallmentStartDateYmd()
}
}
function toIntOrUndefined(val: unknown): number | undefined {
@@ -1676,7 +1720,7 @@
syncPaymentDueAmounts()
formData.paymentDetails.forEach((d, i) => {
d.installmentNo = i + 1
d.installmentDueDate = addMonths(startDate, i * interval)
d.installmentDueDate = calcFqInstallmentDueDate(startDate, i, interval)
})
return
}
@@ -1691,7 +1735,7 @@
transferTime: '',
payStatus: 'dfj',
installmentNo: i + 1,
installmentDueDate: addMonths(startDate, i * interval),
installmentDueDate: calcFqInstallmentDueDate(startDate, i, interval),
attachments: [] as Array<{
id?: string
fileName: string
@@ -1710,6 +1754,7 @@
/** 缴费模式切换回调 */
function onPayModeChange(val: string) {
if (val === 'fq') {
applyFqPaymentDefaults({ resetCountInterval: true })
tryGenerateInstallmentDetails()
} else {
// 趸交模式:将趸交截止时间同步到所有明细的应缴费时间
@@ -1798,6 +1843,7 @@
formData.fatherIdCard = String(pc.fatherIdCard || '')
formData.newbornName = String(pc.newbornName || '')
formData.newbornIdCard = String(pc.newbornIdCard || '')
formData.expectedDeliveryDate = String(pc.expectedDeliveryDate || '')
formData.newbornDeliveryTime = String(pc.newbornDeliveryTime || '')
formData.newbornSex = String(pc.newbornSex ?? '')
formData.deliveryMode = String(pc.deliveryMode || '')
@@ -1831,6 +1877,9 @@
formData.installmentCount = pay.installmentCount ?? null
formData.installmentIntervalMonths = pay.installmentIntervalMonths ?? null
formData.installmentStartDate = String(pay.installmentStartDate || '')
if (formData.payMode === 'fq') {
applyFqPaymentDefaults()
}
}
const pd = bundle.paymentDetails as Array<Record<string, any>> | null
@@ -1971,6 +2020,7 @@
fatherIdCard: formData.fatherIdCard || undefined,
newbornName: formData.newbornName || undefined,
newbornIdCard: formData.newbornIdCard || undefined,
expectedDeliveryDate: formData.expectedDeliveryDate || undefined,
newbornDeliveryTime: formData.newbornDeliveryTime || undefined,
newbornSex: formData.newbornSex || undefined,
deliveryMode: formData.deliveryMode || undefined,
@@ -2531,8 +2581,8 @@
sc: Api.BizSampleCode.SampleCodeItem,
row: Record<string, any>
) {
if (!sc.usedInProtocol) return false
return String(row.sampleCodeId || '') !== sc.id
if (sc.usedInProtocol && String(row.sampleCodeId || '') !== sc.id) return true
return formData.samples.some((s) => s !== row && String(s.sampleCodeId || '') === sc.id)
}
function onSampleCodeChange(row: Record<string, any>) {
@@ -352,6 +352,12 @@
><div class="data-label">新生儿身份证</div
><div class="data-value">{{ dash(partyCustomer?.newbornIdCard) }}</div></div
>
<div class="data-item"
><div class="data-label">预产期</div
><div class="data-value">{{
dash(partyCustomer?.expectedDeliveryDate)
}}</div></div
>
<div class="data-item"
><div class="data-label">新生儿分娩时间</div
><div class="data-value">{{
@@ -0,0 +1,188 @@
<!-- 样本信息管理(样本接收 / 样本质检 / 样本入库) -->
<template>
<div class="page-container">
<section class="panel">
<div
class="panel-entity-switch panel-entity-switch--triple"
role="tablist"
aria-label="视图类型"
>
<button
v-for="item in entityTabs"
:key="item.key"
type="button"
class="entity-switch-card"
:class="{ 'is-active': activeTab === item.key }"
role="tab"
:aria-selected="activeTab === item.key"
@click="activeTab = item.key"
>
<span class="entity-switch-card__icon"
><el-icon><component :is="item.icon" /></el-icon
></span>
<span class="entity-switch-card__main">
<span class="entity-switch-card__title">{{ item.title }}</span>
<span class="entity-switch-card__desc">{{ item.desc }}</span>
</span>
</button>
</div>
<SampleReceptionInfoTab v-if="activeTab === 'sample-reception'" />
<SampleQcInfoTab v-else-if="activeTab === 'sample-qc'" />
<StorageEntryInfoTab v-else-if="activeTab === 'storage-entry'" />
</section>
</div>
</template>
<script setup lang="ts">
import type { Component } from 'vue'
import { Box, DocumentChecked, FolderOpened } from '@element-plus/icons-vue'
import SampleReceptionInfoTab from '../protocol-audit/SampleReceptionInfoTab.vue'
import SampleQcInfoTab from '../protocol-audit/SampleQcInfoTab.vue'
import StorageEntryInfoTab from '../protocol-audit/StorageEntryInfoTab.vue'
defineOptions({ name: 'CellStorageSampleInfo' })
const VALID_TABS = ['sample-reception', 'sample-qc', 'storage-entry'] as const
type EntityTabKey = (typeof VALID_TABS)[number]
const entityTabs: Array<{
key: EntityTabKey
title: string
desc: string
icon: Component
}> = [
{
key: 'sample-reception',
title: '样本接收信息',
desc: '接收结论与短信通知',
icon: Box
},
{
key: 'sample-qc',
title: '样本质检信息',
desc: '质检结论与短信通知',
icon: DocumentChecked
},
{
key: 'storage-entry',
title: '样本入库信息',
desc: '样本入库记录查询',
icon: FolderOpened
}
]
const route = useRoute()
const activeTab = ref<EntityTabKey>('sample-reception')
onMounted(() => {
const tab = String(route.query.tab || '')
if (VALID_TABS.includes(tab as EntityTabKey)) {
activeTab.value = tab as EntityTabKey
}
})
</script>
<style scoped lang="scss">
.panel-entity-switch {
display: flex;
justify-content: center;
gap: 12px;
padding: 16px 16px 12px;
}
.panel-entity-switch--triple {
flex-wrap: wrap;
.entity-switch-card {
flex: 0 1 200px;
min-width: 180px;
max-width: 240px;
padding: 12px 14px;
}
.entity-switch-card__icon {
width: 36px;
height: 36px;
font-size: 18px;
}
.entity-switch-card__title {
font-size: 13px;
}
.entity-switch-card__desc {
font-size: 11px;
}
}
.entity-switch-card {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
border: 1px solid var(--mint-border, #d9e8e2);
border-radius: 10px;
background: var(--surface, #fff);
cursor: pointer;
text-align: left;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
box-shadow 0.2s ease;
&:hover:not(.is-active) {
border-color: #a7f3d0;
background: #f8fafc;
}
&.is-active {
border-color: var(--primary, #10b981);
background: var(--primary-bg, #ecfdf5);
box-shadow: 0 0 0 1px rgb(16 185 129 / 12%);
}
}
.entity-switch-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 8px;
background: #f1f5f9;
color: #64748b;
font-size: 20px;
.entity-switch-card.is-active & {
background: #fff;
color: var(--primary-dark, #059669);
}
}
.entity-switch-card__main {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.entity-switch-card__title {
font-size: 14px;
font-weight: 600;
color: var(--text, #1e293b);
line-height: 1.4;
.entity-switch-card.is-active & {
color: var(--primary-dark, #059669);
}
}
.entity-switch-card__desc {
font-size: 12px;
color: var(--mint-text-muted, #64748b);
line-height: 1.4;
}
</style>
@@ -1,4 +1,4 @@
<!-- 旧「样本接收信息」菜单入口:重定向至采集协议单管理 · 样本接收 Tab -->
<!-- 旧「样本接收信息」菜单入口:重定向至样本信息管理 · 样本接收 Tab -->
<template>
<div />
</template>
@@ -11,7 +11,7 @@
onMounted(() => {
router.replace({
name: 'CellStorageProtocolAudit',
name: 'CellStorageSampleInfo',
query: { ...route.query, tab: 'sample-reception' }
})
})
@@ -1,4 +1,4 @@
<!-- 旧「样本入库信息」菜单入口:重定向至采集协议单管理 · 样本入库 Tab -->
<!-- 旧「样本入库信息」菜单入口:重定向至样本信息管理 · 样本入库 Tab -->
<template>
<div />
</template>
@@ -11,7 +11,7 @@
onMounted(() => {
router.replace({
name: 'CellStorageProtocolAudit',
name: 'CellStorageSampleInfo',
query: { ...route.query, tab: 'storage-entry' }
})
})
@@ -2,7 +2,7 @@
<template>
<el-dialog
:model-value="modelValue"
:title="(isEditMode ? '编辑体检信息' : '填写体检信息') + ' — ' + patientName"
:title="(isEditMode ? '编辑预约信息' : '填写体检信息') + ' — ' + patientName"
width="1080px"
class="dialog-form-layout checkup-fill-dialog"
align-center
@@ -2,7 +2,7 @@
<template>
<el-dialog
:model-value="modelValue"
:title="'标记已体检 — ' + (patientName || '')"
:title="(editMode ? '编辑体检信息' : '标记已体检') + ' — ' + (patientName || '')"
width="1080px"
class="dialog-form-layout checkup-mark-checked-dialog"
align-center
@@ -158,7 +158,11 @@
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><CircleCheck /></el-icon>确认标记已体检
<el-icon>
<Check v-if="editMode" />
<CircleCheck v-else />
</el-icon>
{{ editMode ? '确定' : '确认标记已体检' }}
</el-button>
</template>
</el-dialog>
@@ -480,6 +484,8 @@
modelValue: boolean
checkupId: string
patientName?: string
/** 编辑模式:仅保存体检登记信息,不变更体检状态 */
editMode?: boolean
}>()
const emit = defineEmits(['update:modelValue', 'success'])
@@ -530,7 +536,7 @@
const requireInstitution = () => {
if (!checkupInstitutionId.value) {
ElMessage.warning('该记录未关联体检机构,请先在「编辑体检信息」中选择机构')
ElMessage.warning('该记录未关联体检机构,请先在「编辑预约信息」中选择机构')
return false
}
return true
@@ -814,8 +820,10 @@
checkupRemark: checkupRemark.value || undefined,
actualCheckupFee: actualTotal.value || undefined
})
await markPilotCheckupChecked({ id: props.checkupId })
ElMessage.success('已标记为已体检')
if (!props.editMode) {
await markPilotCheckupChecked({ id: props.checkupId })
}
ElMessage.success(props.editMode ? '保存成功' : '已标记为已体检')
emit('update:modelValue', false)
emit('success')
} catch (e: any) {
@@ -221,8 +221,11 @@
<el-dropdown-item v-if="hasPrimaryCheckupActions(row)" command="view">
<el-icon><View /></el-icon>查看
</el-dropdown-item>
<el-dropdown-item v-if="canEditCheckup(row)" command="edit">
<el-icon><EditPen /></el-icon>编辑
<el-dropdown-item v-if="canEditReserve(row)" command="editReserve">
<el-icon><EditPen /></el-icon>编辑预约信息
</el-dropdown-item>
<el-dropdown-item v-if="canEditActualCheckupInfo(row)" command="editActual">
<el-icon><Edit /></el-icon>编辑体检信息
</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -251,7 +254,7 @@
@success="onBatchReserveSuccess"
/>
<!-- 填写 / 编辑体检信息弹窗 -->
<!-- 填写 / 编辑预约信息弹窗 -->
<CheckupFillDialog
v-model="fillDialogVisible"
:checkup-id="currentCheckupId"
@@ -263,11 +266,12 @@
<!-- 查看弹窗 -->
<CheckupViewDialog v-model="viewDialogVisible" :checkup-row="viewCheckupRow" />
<!-- 标记已体检弹窗 -->
<!-- 标记已体检 / 编辑体检登记信息弹窗 -->
<CheckupMarkCheckedDialog
v-model="markCheckedVisible"
:checkup-id="currentCheckupId"
:patient-name="currentPatientName"
:edit-mode="markCheckedEditMode"
@success="loadData"
/>
</div>
@@ -330,6 +334,7 @@
const viewDialogVisible = ref(false)
const viewCheckupRow = ref<any>(null)
const markCheckedVisible = ref(false)
const markCheckedEditMode = ref(false)
const currentCheckupId = ref('')
const currentPatientName = ref('')
@@ -387,8 +392,11 @@
/** 待体检且已填写机构信息时可标记已体检 */
const canMarkChecked = (row: any) => row.checkupStatus === 'dtj' && !!row.institutionName
/** 已填写机构信息时可编辑 */
const canEditCheckup = (row: any) => !!row.institutionName
/** 已填写机构信息时可编辑预约 */
const canEditReserve = (row: any) => !!row.institutionName
/** 已体检时可编辑体检登记信息 */
const canEditActualCheckupInfo = (row: any) => row.checkupStatus === 'ytj'
/** 是否存在填写体检信息 / 标记已体检主操作 */
const hasPrimaryCheckupActions = (row: any) =>
@@ -397,9 +405,9 @@
/** 更多菜单是否展示 */
const hasMoreCheckupActions = (row: any) => {
if (hasPrimaryCheckupActions(row)) {
return canEditCheckup(row) || canMarkChecked(row)
return canEditReserve(row) || canMarkChecked(row)
}
return canEditCheckup(row)
return canEditReserve(row) || canEditActualCheckupInfo(row)
}
const handleDropdownCommand = (command: string, row: any) => {
@@ -407,8 +415,11 @@
case 'view':
handleView(row)
break
case 'edit':
handleEdit(row)
case 'editReserve':
handleEditReserve(row)
break
case 'editActual':
handleEditActual(row)
break
}
}
@@ -420,13 +431,20 @@
fillDialogVisible.value = true
}
const handleEdit = (row: any) => {
const handleEditReserve = (row: any) => {
currentCheckupId.value = row.id
currentPatientName.value = row.patientName || ''
fillEditRow.value = { ...row }
fillDialogVisible.value = true
}
const handleEditActual = (row: any) => {
currentCheckupId.value = row.id
currentPatientName.value = row.patientName || ''
markCheckedEditMode.value = true
markCheckedVisible.value = true
}
const onFillSuccess = () => {
fillEditRow.value = null
loadData()
@@ -461,6 +479,7 @@
const handleMarkChecked = (row: any) => {
currentCheckupId.value = row.id
currentPatientName.value = row.patientName || ''
markCheckedEditMode.value = false
markCheckedVisible.value = true
}
@@ -24,8 +24,7 @@
</el-button>
<el-button
v-if="editor.selectedStep.value.stepStatus === 'jxz'"
class="btn-op-item"
size="small"
class="btn-main task-exec-toolbar-btn"
type="success"
:loading="editor.stepActionLoading.value"
:disabled="editor.manualSaveLoading.value"
@@ -331,7 +331,7 @@
:is-completed-edit="completeConclusionIsEdit"
:initial-conclusion="completeConclusionInitial.conclusion"
:initial-remark="completeConclusionInitial.remark"
@submit="submitCompleteWithConclusion"
@submit="handleCompleteSubmit"
/>
</div>
</template>
@@ -386,10 +386,16 @@
)
function handleClose() {
dialogVisible.value = false
emit('success')
}
async function handleCompleteSubmit(payload: { conclusion: string; remark?: string }) {
await submitCompleteWithConclusion(payload)
if (taskDetail.value?.taskStatus === 'ywc') {
dialogVisible.value = false
}
}
function stepSeqClass(status: string) {
if (status === 'ywc') return 'seq-done'
if (status === 'jxz') return 'seq-running'
@@ -156,6 +156,21 @@
>
</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="donationDate"
label="捐赠日期"
@@ -220,6 +235,22 @@
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="采集地区"
@@ -212,7 +212,17 @@
<el-radio label="0">女</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="新生儿分娩时间" class="form-col-span-2">
<el-form-item label="预产期">
<el-date-picker
v-model="formData.expectedDeliveryDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择预产期"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="新生儿分娩时间">
<el-date-picker
v-model="formData.newbornDeliveryTime"
type="datetime"
@@ -396,11 +406,11 @@
:disabled="!selectedSamples.length"
@click="openBatchTransportDialog"
>
应用到选中样本
批量配置运输信息
</el-button>
</div>
<p v-if="formData.samples.length" class="sample-transport-hint"
>勾选样本后,点击「应用到选中样本」可批量填写运输信息。点击展开可编辑单个运输信息。</p
>勾选样本后,点击「批量配置运输信息」可统一填写运输信息。点击展开可编辑单个运输信息。</p
>
<!-- Sample Cards -->
@@ -1121,6 +1131,7 @@
fatherIdCard: '',
newbornName: '',
newbornIdCard: '',
expectedDeliveryDate: '',
newbornDeliveryTime: '',
newbornSex: '',
deliveryMode: '',
@@ -1166,6 +1177,7 @@
fatherIdCard: '',
newbornName: '',
newbornIdCard: '',
expectedDeliveryDate: '',
newbornDeliveryTime: '',
newbornSex: '',
deliveryMode: '',
@@ -1343,6 +1355,7 @@
formData.fatherIdCard = String(pc.fatherIdCard || '')
formData.newbornName = String(pc.newbornName || '')
formData.newbornIdCard = String(pc.newbornIdCard || '')
formData.expectedDeliveryDate = String(pc.expectedDeliveryDate || '')
formData.newbornDeliveryTime = String(pc.newbornDeliveryTime || '')
formData.newbornSex = String(pc.newbornSex ?? '')
formData.deliveryMode = String(pc.deliveryMode || '')
@@ -1496,6 +1509,7 @@
fatherIdCard: formData.fatherIdCard || undefined,
newbornName: formData.newbornName || undefined,
newbornIdCard: formData.newbornIdCard || undefined,
expectedDeliveryDate: formData.expectedDeliveryDate || undefined,
newbornDeliveryTime: formData.newbornDeliveryTime || undefined,
newbornSex: formData.newbornSex || undefined,
deliveryMode: formData.deliveryMode || undefined,
@@ -1965,8 +1979,8 @@
sc: Api.BizSampleCode.SampleCodeItem,
row: Record<string, any>
) {
if (!sc.usedInProtocol) return false
return String(row.sampleCodeId || '') !== sc.id
if (sc.usedInProtocol && String(row.sampleCodeId || '') !== sc.id) return true
return formData.samples.some((s) => s !== row && String(s.sampleCodeId || '') === sc.id)
}
function onSampleCodeChange(row: Record<string, any>) {
@@ -276,6 +276,10 @@
><div class="data-label">新生儿身份证</div
><div class="data-value">{{ dash(party?.newbornIdCard) }}</div></div
>
<div class="data-item"
><div class="data-label">预产期</div
><div class="data-value">{{ dash(party?.expectedDeliveryDate) }}</div></div
>
<div class="data-item"
><div class="data-label">新生儿分娩时间</div
><div class="data-value">{{ dash(party?.newbornDeliveryTime) }}</div></div