style(admin): unify dialog layouts and dense readonly presentation

This commit is contained in:
2026-10-20 10:11:31 +08:00
parent 7ff7c9914f
commit 3267679758
214 changed files with 10931 additions and 2654 deletions
@@ -16,13 +16,16 @@ const loading = ref(false)
const previewUrl = ref('')
const printBlob = ref<Blob | null>(null)
const error = ref<ApiErrorDetails | null>(null)
let previewRequest = 0
const options: Array<{ value: OceanBookingPrintType; label: string; description: string }> = [
{ value: 'SHIPPING_INSTRUCTION', label: '出口货物托运单', description: 'Sea Freight Shipping Instructions · PDF' },
{ value: 'BILL_OF_LADING_CONFIRMATION', label: '提单确认件', description: '提单内容确认及打印 · PDF' },
]
const selectedOption = computed(() => options.find((item) => item.value === selectedType.value) || null)
const previewTitle = computed(() => `${selectedOption.value?.label || '打印单据'} · ${props.bookingNo || props.bookingId}`)
const bookingCode = computed(() => props.bookingNo?.trim() || '')
const selectionTitle = computed(() => bookingCode.value ? `选择打印单据 · ${bookingCode.value}` : '选择打印单据')
const previewTitle = computed(() => { const title = selectedOption.value?.label || '打印单据'; return bookingCode.value ? `${title} · ${bookingCode.value}` : title })
function releasePreview() {
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
@@ -30,6 +33,7 @@ function releasePreview() {
printBlob.value = null
}
function reset() {
previewRequest += 1
releasePreview()
selectedType.value = ''
error.value = null
@@ -37,17 +41,19 @@ function reset() {
}
async function preview() {
if (!props.allowed || !selectedType.value || loading.value) return
const request = ++previewRequest
loading.value = true
error.value = null
releasePreview()
try {
const blob = await downloadOceanBookingPrint(props.bookingId, selectedType.value)
if (request !== previewRequest || !open.value) return
printBlob.value = blob
previewUrl.value = URL.createObjectURL(blob)
} catch (cause) {
error.value = getApiErrorDetails(cause, '打印单据加载失败,请稍后重试')
if (request === previewRequest) error.value = getApiErrorDetails(cause, '打印单据加载失败,请稍后重试')
} finally {
loading.value = false
if (request === previewRequest) loading.value = false
}
}
function backToSelection() {
@@ -59,19 +65,20 @@ function download() {
const url = URL.createObjectURL(printBlob.value)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `${props.bookingNo || props.bookingId}-${selectedOption.value.label}.pdf`
anchor.download = `${bookingCode.value || '订舱单据'}-${selectedOption.value.label}.pdf`
anchor.click()
URL.revokeObjectURL(url)
}
watch(open, (value) => { if (!value) reset() })
onBeforeUnmount(releasePreview)
onBeforeUnmount(() => { previewRequest += 1; releasePreview() })
</script>
<template>
<AppDialog class="admin-density-dialog"
<AppDialog class="booking-print-dialog admin-density-dialog"
v-model="open"
:title="previewUrl ? previewTitle : '选择打印单据'"
:title="previewUrl ? previewTitle : selectionTitle"
:fullscreenable="Boolean(previewUrl)"
width="min(1120px, calc(100vw - 48px))"
:loading="loading"
:show-confirm="false"
@@ -79,18 +86,17 @@ onBeforeUnmount(releasePreview)
destroy-on-close
>
<el-alert v-if="!allowed" title="暂无订舱单据打印权限" type="warning" :closable="false" show-icon />
<template v-else-if="!previewUrl">
<p class="booking-print-current">当前订舱:<strong>{{ bookingNo || bookingId }}</strong></p>
<div class="booking-print-options" role="radiogroup" aria-label="打印单据类型">
<label v-for="item in options" :key="item.value" class="booking-print-option" :class="{ 'is-selected': selectedType === item.value }">
<el-radio v-model="selectedType" :value="item.value"><span class="sr-only">{{ item.label }}</span></el-radio>
<FileText :size="22" />
<span><strong>{{ item.label }}</strong><small>{{ item.description }}</small></span>
</label>
</div>
<section v-else-if="!previewUrl" class="density-dialog-section" data-tone="source">
<p class="booking-print-current">当前订舱:<strong>{{ bookingCode || '—' }}</strong></p>
<el-radio-group v-model="selectedType" class="booking-print-options" aria-label="打印单据类型">
<el-radio v-for="item in options" :key="item.value" :value="item.value" class="booking-print-option">
<FileText :size="18" />
<span class="booking-print-label"><strong>{{ item.label }}</strong><small>{{ item.description }}</small></span>
</el-radio>
</el-radio-group>
<LoadErrorState v-if="error" title="打印单据加载失败" :message="error.message" :show-retry="false"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
</template>
<div v-else class="booking-print-preview" :aria-label="previewTitle">
</section>
<div v-else class="density-dialog-section booking-print-preview" data-tone="source" :aria-label="previewTitle">
<iframe :src="previewUrl" :title="`${previewTitle} PDF 预览`" />
</div>
<template #footer="{ close }">
@@ -105,5 +111,21 @@ onBeforeUnmount(releasePreview)
</template>
<style scoped>
.booking-print-current{margin:0 0 var(--admin-space-5);color:var(--cc-muted);font-size:var(--admin-font-body)}.booking-print-current strong{color:var(--cc-text)}.booking-print-options{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--admin-space-5)}.booking-print-option{display:grid;grid-template-columns:auto auto minmax(0,1fr);align-items:center;gap:var(--admin-space-5);border:1px solid var(--cc-border);border-radius:var(--admin-radius);background:var(--admin-surface);cursor:pointer;min-height:var(--admin-empty-height, 64px);padding:var(--admin-space-3) var(--admin-space-5)}.booking-print-option.is-selected{border-color:var(--cc-primary);background:var(--cc-primary-soft)}.booking-print-option span{display:grid;min-width:0;gap:var(--admin-space-1)}.booking-print-option strong{color:var(--cc-text);font-size:var(--admin-font-section)}.booking-print-option small{color:var(--cc-muted);font-size:var(--admin-font-caption)}.booking-print-preview{min-height:0;overflow:hidden;border:1px solid var(--cc-border);border-radius:var(--admin-radius);background:#f3f5f8}.booking-print-preview iframe{display:block;width:100%;height:min(72vh,760px);border:0;background:var(--admin-surface)}.booking-print-footer{width:100%}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(max-width:720px){.booking-print-options{grid-template-columns:1fr}.booking-print-preview iframe{height:68vh}}
.booking-print-current { margin: 0 0 var(--admin-space-5); color: var(--cc-muted); }
.booking-print-current strong { color: var(--cc-text); }
.booking-print-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--admin-space-5); width: 100%; }
.booking-print-option { min-width: 0; height: auto; min-height: var(--admin-control-height); margin: 0; white-space: normal; }
.booking-print-option :deep(.el-radio__label) { display: flex; align-items: center; gap: var(--admin-space-3); min-width: 0; white-space: normal; }
.booking-print-option :deep(.el-radio__label) > svg { flex: 0 0 auto; }
.booking-print-label { display: grid; min-width: 0; gap: var(--admin-space-1); }
.booking-print-label strong { color: var(--cc-text); }
.booking-print-label small { color: var(--cc-muted); font-size: var(--admin-font-caption); }
.booking-print-preview { min-height: 0; overflow: hidden; }
.booking-print-preview iframe { display: block; width: 100%; height: min(72vh, 760px); border: 0; background: var(--admin-surface); }
.booking-print-footer { width: 100%; }
:global(.booking-print-dialog.el-dialog.is-fullscreen .el-dialog__body),
:global(.booking-print-dialog.el-dialog.is-fullscreen .app-dialog__body) { display: flex; flex: 1; }
:global(.booking-print-dialog.el-dialog.is-fullscreen .booking-print-preview) { display: flex; flex: 1; }
:global(.booking-print-dialog.el-dialog.is-fullscreen .booking-print-preview iframe) { height: 100%; flex: 1; min-height: 0; }
@media (max-width: 720px) { .booking-print-options { grid-template-columns: 1fr; } .booking-print-preview iframe { height: 68vh; } }
</style>