365 lines
12 KiB
Vue
365 lines
12 KiB
Vue
<!-- 样本信息管理 · 样本质检信息 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>
|