提交
This commit is contained in:
1 parent
93cd506a90
commit
3b875ec370
93 files changed
+11514
-2304
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<!-- 财务确认弹窗:附件上传 + 财务备注(对齐采集协议单附件 UI) -->
|
||||
<template>
|
||||
<div class="protocol-finance-confirm-fields">
|
||||
<el-form-item :label="remarkLabel" class="form-col-span-2">
|
||||
<el-form-item v-if="showRemark" :label="remarkLabel" class="form-col-span-2">
|
||||
<el-input
|
||||
:model-value="financeRemark"
|
||||
type="textarea"
|
||||
@@ -97,11 +97,13 @@
|
||||
uploadFolder?: string
|
||||
remarkLabel?: string
|
||||
attachmentLabel?: string
|
||||
showRemark?: boolean
|
||||
}>(),
|
||||
{
|
||||
uploadFolder: 'protocol-finance-confirm',
|
||||
remarkLabel: '财务备注',
|
||||
attachmentLabel: '财务确认附件'
|
||||
attachmentLabel: '财务确认附件',
|
||||
showRemark: true
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
<!-- 质检任务 — 只读查看(参数、结论、附件) -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<SystemViewDialog
|
||||
v-model:visible="innerVisible"
|
||||
title="质检任务详情"
|
||||
width="960px"
|
||||
:grid-columns="3"
|
||||
:icon="Tickets"
|
||||
:meta="viewDetail?.taskNo || ''"
|
||||
:sections="viewSections"
|
||||
append-to-body
|
||||
compact
|
||||
modal-class="qc-task-view-dialog-mask"
|
||||
>
|
||||
<template #reception-extra>
|
||||
<div class="view-reception-attachments">
|
||||
<div class="view-kv view-kv-span-2">
|
||||
<dt>接收附件</dt>
|
||||
<dd>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="viewReceptionAttachmentFiles"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #append>
|
||||
<div
|
||||
v-if="viewDetail?.attachmentFiles?.length"
|
||||
class="view-section qc-task-view-attachments"
|
||||
>
|
||||
<h3>{{ viewDetail?.taskType === 'dsfjc' ? '检测材料' : '检测附件' }}</h3>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="viewDetail.attachmentFiles"
|
||||
@preview="handleAttachmentPreview"
|
||||
@download="handleAttachmentDownload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Tickets } from '@element-plus/icons-vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { fetchQcTaskDetail, type QcTaskDetail } from '@/api/qc-task'
|
||||
import { fetchQcTaskResults } from '@/api/qc-task-result'
|
||||
import {
|
||||
taskTypeLabel,
|
||||
taskStatusLabel,
|
||||
qcBizTypeLabel,
|
||||
receptionSourceLabel,
|
||||
acceptConclusionLabel,
|
||||
transportTypeLabel,
|
||||
workOrderStatusLabel,
|
||||
qcTaskConclusionLabel
|
||||
} from '@/utils/qc-labels'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
taskId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const viewDetail = ref<QcTaskDetail | null>(null)
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.taskId] as const,
|
||||
async ([vis, id]) => {
|
||||
if (!vis || !id) {
|
||||
viewDetail.value = null
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const detail = await fetchQcTaskDetail(id)
|
||||
try {
|
||||
const results = await fetchQcTaskResults(id)
|
||||
;(detail as QcTaskDetail).results = results || []
|
||||
} catch {
|
||||
;(detail as QcTaskDetail).results = []
|
||||
}
|
||||
viewDetail.value = detail
|
||||
} catch {
|
||||
viewDetail.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function formatMetric(v: unknown) {
|
||||
if (v === null || v === undefined || v === '') return '—'
|
||||
return String(v)
|
||||
}
|
||||
|
||||
function formatCheckResults(items?: Array<{ itemName?: string; checked?: boolean }>) {
|
||||
if (!items?.length) return '—'
|
||||
return items.map((x) => `${x.itemName || ''}${x.checked ? '(✓)' : '(×)'}`).join('、')
|
||||
}
|
||||
|
||||
const viewReceptionAttachmentFiles = computed(() => {
|
||||
const r = viewDetail.value?.reception
|
||||
if (!r) return []
|
||||
if (Array.isArray(r.receiveAttachmentFiles) && r.receiveAttachmentFiles.length) {
|
||||
return r.receiveAttachmentFiles as Array<Record<string, unknown>>
|
||||
}
|
||||
const urls = String(r.receiveAttachments || '')
|
||||
if (!urls.trim()) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
.map((fileUrl, i) => ({ id: `rec_${i}`, fileUrl, fileName: fileUrl.split('/').pop() || '' }))
|
||||
})
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
const r = d.reception || {}
|
||||
const transportFields: ViewSection['fields'] = [
|
||||
{ label: '运输方式', value: transportTypeLabel(String(r.transportType || '')) }
|
||||
]
|
||||
if (r.transportType === 'kdys') {
|
||||
transportFields.push(
|
||||
{ label: '快递公司', value: r.expressCompanyName || '—' },
|
||||
{ label: '快递单号', value: r.waybillNo || '—' }
|
||||
)
|
||||
} else if (r.transportType === 'ryxd') {
|
||||
transportFields.push(
|
||||
{ label: '携带人', value: r.carrierName || '—' },
|
||||
{ label: '携带人电话', value: r.carrierPhone || '—' }
|
||||
)
|
||||
}
|
||||
if (r.etaTime) {
|
||||
transportFields.push({ label: '预计到达', value: r.etaTime })
|
||||
}
|
||||
const resultSection =
|
||||
d.results && d.results.length > 0
|
||||
? [
|
||||
{
|
||||
title: '检测参数',
|
||||
fields: d.results.map((item) => ({
|
||||
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
|
||||
value: item.paramValue || '—'
|
||||
}))
|
||||
}
|
||||
]
|
||||
: []
|
||||
return [
|
||||
{
|
||||
title: '任务信息',
|
||||
fields: [
|
||||
{ label: '任务号', value: d.taskNo },
|
||||
{ label: '业务类型', value: d.qcBizTypeLabel || qcBizTypeLabel(d.qcBizType || ''), tagType: 'success' },
|
||||
{ label: '工单号', value: d.workOrderNo || '—' },
|
||||
{
|
||||
label: '工单状态',
|
||||
value: d.workOrderStatus ? workOrderStatusLabel(d.workOrderStatus) : '—',
|
||||
tagType: d.workOrderStatus === 'ywc' ? 'success' : d.workOrderStatus === 'ypz' ? 'warning' : 'info'
|
||||
},
|
||||
{ label: '检测类型', value: taskTypeLabel(d.taskType), tagType: d.taskType === 'sysz' ? 'success' : 'info' },
|
||||
{ label: '检测项目', value: d.itemName },
|
||||
{ label: '项目编码', value: d.itemCode || '—' },
|
||||
{ label: '第三方公司', value: d.companyName || '—' },
|
||||
{
|
||||
label: '任务状态',
|
||||
value: taskStatusLabel(d.taskStatus),
|
||||
tagType: d.taskStatus === 'ywc' ? 'success' : d.taskStatus === 'jxz' ? 'warning' : 'info'
|
||||
},
|
||||
{
|
||||
label: '检测结论',
|
||||
value: d.conclusionLabel || qcTaskConclusionLabel(d.conclusion || '') || '—',
|
||||
tagType: d.conclusion === 'tg' ? 'success' : d.conclusion === 'btg' ? 'danger' : 'info'
|
||||
},
|
||||
{ label: '完成时间', value: d.completedTime || '—' },
|
||||
{ label: '创建时间', value: d.createTime || '—' },
|
||||
{ label: '备注', value: d.remark || '—', span: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '样本信息',
|
||||
fields: [
|
||||
{ label: '协议号', value: r.protocolNo || d.protocolNo || '—' },
|
||||
{ label: '样本编码', value: r.sampleCode || d.sampleCode || '—' },
|
||||
{
|
||||
label: '样本来源',
|
||||
value: r.sourceTypeLabel || d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''),
|
||||
tagType: 'primary'
|
||||
},
|
||||
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
|
||||
]
|
||||
},
|
||||
...(transportFields.length > 1 || r.transportType
|
||||
? [{ title: '运输信息', fields: transportFields }]
|
||||
: []),
|
||||
{
|
||||
title: '样本接收',
|
||||
slot: 'reception-extra',
|
||||
fields: [
|
||||
{ label: '接收时间', value: r.receptionTime || d.receptionTime || '—' },
|
||||
{ label: '接收经手人', value: r.handlerName || d.handlerName || '—' },
|
||||
{
|
||||
label: '接收温度(℃)',
|
||||
value: r.receiveTemperature ? `${r.receiveTemperature}℃` : '—'
|
||||
},
|
||||
{ label: '体积(ml)', value: formatMetric(r.volumeMl) },
|
||||
{ label: '净重(g)', value: formatMetric(r.netWeightG) },
|
||||
{
|
||||
label: '结论',
|
||||
value:
|
||||
r.acceptConclusionLabel ||
|
||||
acceptConclusionLabel(String(r.acceptConclusion || ''))
|
||||
},
|
||||
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
|
||||
{ label: '备注', value: r.remark || '—', span: true }
|
||||
]
|
||||
},
|
||||
...resultSection
|
||||
]
|
||||
})
|
||||
|
||||
function attachmentFullUrl(url: string) {
|
||||
const u = String(url || '').trim()
|
||||
if (!u) return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
|
||||
}
|
||||
|
||||
function handleAttachmentPreview(file: Record<string, unknown>) {
|
||||
const url = attachmentFullUrl(String(file.fileUrl || ''))
|
||||
if (url) window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function handleAttachmentDownload(file: Record<string, unknown>) {
|
||||
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 叠在协议/捐赠查看弹窗之上 */
|
||||
.qc-task-view-dialog-mask {
|
||||
z-index: 4000 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.view-reception-attachments {
|
||||
margin-top: 4px;
|
||||
|
||||
.view-kv {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 8px 12px;
|
||||
font-size: 13px;
|
||||
align-items: start;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.qc-task-view-attachments {
|
||||
padding: 12px 18px;
|
||||
border-bottom: none;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 10px;
|
||||
padding-left: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
border-left: 3px solid #10b981;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,299 @@
|
||||
<!-- 样本查看 — 质检信息卡片(协议单/捐赠单查看弹窗复用) -->
|
||||
<template>
|
||||
<div class="sample-card__section sample-card__section--qc">
|
||||
<div class="sample-card__section-label">质检信息</div>
|
||||
|
||||
<div v-if="!qcInfo" class="sample-qc-empty">{{ emptyHint }}</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="qc-work-order-row">
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">工单号</span>
|
||||
<span class="qc-kv__value qc-kv__value--code">{{ qcInfo.workOrderNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">业务类型</span>
|
||||
<span class="qc-kv__value">{{
|
||||
qcInfo.qcBizTypeLabel || qcBizTypeLabel(qcInfo.qcBizType || '')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">工单状态</span>
|
||||
<span class="qc-kv__value">
|
||||
<el-tag :type="workOrderStatusTagType(qcInfo.workOrderStatus || '')" size="small">
|
||||
{{ qcInfo.workOrderStatusLabel || workOrderStatusLabel(qcInfo.workOrderStatus || '') }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="qcInfo.workOrderStatus === 'dpz' && !qcInfo.tasks?.length" class="sample-qc-hint">
|
||||
工单待配置检测项目
|
||||
</div>
|
||||
|
||||
<div v-if="qcInfo.tasks?.length" class="qc-detail-cards qc-detail-cards--tasks">
|
||||
<div
|
||||
v-for="task in qcInfo.tasks"
|
||||
:key="task.id"
|
||||
class="qc-detail-card qc-detail-card--task"
|
||||
>
|
||||
<div class="qc-detail-card__header">
|
||||
<span class="qc-detail-card__name">{{ task.itemName || '—' }}</span>
|
||||
<el-tag type="info" size="small">{{
|
||||
task.taskTypeLabel || taskTypeLabel(task.taskType || '')
|
||||
}}</el-tag>
|
||||
</div>
|
||||
<div class="qc-detail-card__body">
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">任务号</span>
|
||||
<span class="qc-kv__value">{{ task.taskNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">任务状态</span>
|
||||
<span class="qc-kv__value">
|
||||
<el-tag :type="taskStatusTagType(task.taskStatus)" size="small">
|
||||
{{ task.taskStatusLabel || taskStatusLabel(task.taskStatus || '') }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="qc-kv">
|
||||
<span class="qc-kv__label">检测结论</span>
|
||||
<span class="qc-kv__value">
|
||||
<el-tag
|
||||
v-if="task.conclusion"
|
||||
:type="qcTaskConclusionTagType(task.conclusion)"
|
||||
size="small"
|
||||
>
|
||||
{{ task.conclusionLabel || qcTaskConclusionLabel(task.conclusion) }}
|
||||
</el-tag>
|
||||
<span v-else class="qc-kv__muted">—</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qc-detail-card__foot">
|
||||
<button type="button" class="qc-detail-card__link" @click.stop="openTaskView(task)">
|
||||
点击查看详情
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="qcInfo.workOrderStatus !== 'dpz'"
|
||||
class="sample-qc-hint"
|
||||
>
|
||||
暂无检测任务
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Teleport to="body">
|
||||
<QcTaskViewDialog v-model:visible="taskViewVisible" :task-id="taskViewId" />
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
qcBizTypeLabel,
|
||||
workOrderStatusLabel,
|
||||
taskTypeLabel,
|
||||
taskStatusLabel,
|
||||
qcTaskConclusionLabel,
|
||||
qcTaskConclusionTagType
|
||||
} from '@/utils/qc-labels'
|
||||
import { workOrderStatusTagType, taskStatusTagType } from '@/utils/status-tag-registry'
|
||||
import QcTaskViewDialog from '@/components/business/qc-task-view-dialog/index.vue'
|
||||
|
||||
export type SampleQcTask = {
|
||||
id?: string
|
||||
taskNo?: string
|
||||
taskType?: string
|
||||
taskTypeLabel?: string
|
||||
itemName?: string
|
||||
itemCode?: string
|
||||
companyName?: string
|
||||
taskStatus?: string
|
||||
taskStatusLabel?: string
|
||||
conclusion?: string
|
||||
conclusionLabel?: string
|
||||
completedTime?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export type SampleQcInfo = {
|
||||
workOrderNo?: string
|
||||
workOrderStatus?: string
|
||||
workOrderStatusLabel?: string
|
||||
qcBizType?: string
|
||||
qcBizTypeLabel?: string
|
||||
configuredTime?: string
|
||||
hasLab?: boolean
|
||||
hasThirdParty?: boolean
|
||||
labItems?: Array<{ id?: string; itemCode?: string; itemName?: string }>
|
||||
thirdPartyItems?: Array<{
|
||||
id?: string
|
||||
itemCode?: string
|
||||
itemName?: string
|
||||
companyName?: string
|
||||
}>
|
||||
tasks?: SampleQcTask[]
|
||||
}
|
||||
|
||||
export type SampleReceptionBrief = {
|
||||
acceptConclusion?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
qcInfo?: SampleQcInfo | null
|
||||
sampleReception?: SampleReceptionBrief | null
|
||||
}>()
|
||||
|
||||
const taskViewVisible = ref(false)
|
||||
const taskViewId = ref('')
|
||||
|
||||
const emptyHint = computed(() => {
|
||||
const rec = props.sampleReception
|
||||
if (!rec) {
|
||||
return '暂未接收样本,接收结论为「接收」后将自动生成质检工单'
|
||||
}
|
||||
if (rec.acceptConclusion === 'bj') {
|
||||
return '样本拒收,不生成质检工单'
|
||||
}
|
||||
if (rec.acceptConclusion === 'js') {
|
||||
return '已接收,质检工单生成中或待配置'
|
||||
}
|
||||
return '暂无质检信息'
|
||||
})
|
||||
|
||||
function openTaskView(task: SampleQcTask) {
|
||||
if (!task.id) return
|
||||
taskViewId.value = task.id
|
||||
taskViewVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.sample-card__section--qc {
|
||||
margin-top: 4px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.sample-qc-empty,
|
||||
.sample-qc-hint {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.qc-work-order-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
gap: 12px 24px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.qc-work-order-row .qc-kv {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.qc-detail-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 10px;
|
||||
|
||||
&--tasks {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-detail-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
|
||||
&--task {
|
||||
border-left: 3px solid #0ea5e9;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-detail-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.qc-detail-card__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.qc-detail-card__body {
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.qc-detail-card__foot {
|
||||
padding: 2px 12px 8px;
|
||||
text-align: right;
|
||||
border-top: 1px dashed #f1f5f9;
|
||||
}
|
||||
|
||||
.qc-detail-card__link {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: #10b981;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #059669;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-kv__muted {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.qc-kv {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.qc-kv__label {
|
||||
color: #94a3b8;
|
||||
flex-shrink: 0;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.qc-kv__value {
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
|
||||
&--code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,11 +3,14 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
width="720px"
|
||||
:width="width"
|
||||
class="system-view-dialog"
|
||||
:class="[dialogClass, { 'system-view-dialog--compact': compact }]"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:show-close="false"
|
||||
:append-to-body="appendToBody"
|
||||
:modal-class="modalClass"
|
||||
style="backdrop-filter: none"
|
||||
>
|
||||
<div class="dialog-scroll">
|
||||
@@ -25,7 +28,7 @@
|
||||
<!-- sections -->
|
||||
<div v-for="section in sections" :key="section.title" class="view-section">
|
||||
<h3>{{ section.title }}</h3>
|
||||
<dl class="view-kv-grid">
|
||||
<dl class="view-kv-grid" :class="`view-kv-grid--cols-${gridColumns}`">
|
||||
<div
|
||||
v-for="field in section.fields"
|
||||
:key="field.label"
|
||||
@@ -33,9 +36,13 @@
|
||||
:class="{ 'view-kv-span-2': field.span }"
|
||||
>
|
||||
<dt>{{ field.label }}</dt>
|
||||
<dd>{{ field.value ?? '—' }}</dd>
|
||||
<dd>
|
||||
<el-tag v-if="field.tagType" :type="field.tagType" size="small">{{ field.value ?? '—' }}</el-tag>
|
||||
<template v-else>{{ field.value ?? '—' }}</template>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<slot v-if="section.slot" :name="section.slot" />
|
||||
</div>
|
||||
<slot name="append" />
|
||||
</div>
|
||||
@@ -52,24 +59,52 @@
|
||||
import { computed, type Component } from 'vue'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
|
||||
export type TagType = 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
|
||||
|
||||
export interface ViewField {
|
||||
label: string
|
||||
value: string | number | boolean | null | undefined
|
||||
span?: boolean
|
||||
/** 传入后以 el-tag 渲染,值为 el-tag 的 type 属性 */
|
||||
tagType?: TagType
|
||||
}
|
||||
|
||||
export interface ViewSection {
|
||||
title: string
|
||||
fields: ViewField[]
|
||||
/** 区块内追加内容(如附件列表),对应具名插槽名 */
|
||||
slot?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
title: string
|
||||
icon?: Component
|
||||
meta?: string
|
||||
sections: ViewSection[]
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
title: string
|
||||
icon?: Component
|
||||
meta?: string
|
||||
sections: ViewSection[]
|
||||
/** 弹窗宽度,默认 720px */
|
||||
width?: string
|
||||
/** 字段栅格列数,默认 2 */
|
||||
gridColumns?: 2 | 3
|
||||
/** 挂到 body,用于嵌套在其它弹窗内时叠层展示 */
|
||||
appendToBody?: boolean
|
||||
/** 遮罩类名(可配合全局样式提高 z-index) */
|
||||
modalClass?: string
|
||||
/** 附加 dialog class */
|
||||
dialogClass?: string
|
||||
/** 紧凑布局:缩小头部/区块间距与内容区最大高度 */
|
||||
compact?: boolean
|
||||
}>(),
|
||||
{
|
||||
width: '720px',
|
||||
gridColumns: 2,
|
||||
appendToBody: false,
|
||||
modalClass: '',
|
||||
dialogClass: '',
|
||||
compact: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void
|
||||
@@ -153,7 +188,11 @@
|
||||
.view-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 28px;
|
||||
gap: 12px 20px;
|
||||
|
||||
&--cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.view-kv {
|
||||
@@ -180,8 +219,15 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.view-kv-grid--cols-3 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.view-kv-grid {
|
||||
.view-kv-grid,
|
||||
.view-kv-grid--cols-3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -190,6 +236,60 @@
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.system-view-dialog--compact {
|
||||
.dialog-scroll {
|
||||
max-height: min(68vh, 560px);
|
||||
}
|
||||
|
||||
.view-hero {
|
||||
gap: 12px;
|
||||
padding: 12px 18px;
|
||||
}
|
||||
|
||||
.view-hero-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.view-hero-main h2 {
|
||||
margin-bottom: 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.view-hero-meta {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.view-section {
|
||||
padding: 12px 18px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.view-kv-grid {
|
||||
gap: 8px 16px;
|
||||
}
|
||||
|
||||
.view-kv {
|
||||
font-size: 12px;
|
||||
grid-template-columns: 84px 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.system-view-dialog--compact.el-dialog .el-dialog__footer {
|
||||
padding: 8px 18px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
Reference in new issue
Block a user