Files
admin-web/src/modules/ocean-bookings/components/OceanBookingPrintDialog.vue
T

132 lines
6.8 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.
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { Download, FileText } from '@lucide/vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { downloadOceanBookingPrint } from '../api/platformOceanBookings'
import type { OceanBookingPrintType } from '../types'
const props = defineProps<{ bookingId: string; bookingNo?: string | null; allowed: boolean }>()
const open = defineModel<boolean>({ default: false })
const selectedType = ref<OceanBookingPrintType | ''>('')
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 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)
previewUrl.value = ''
printBlob.value = null
}
function reset() {
previewRequest += 1
releasePreview()
selectedType.value = ''
error.value = null
loading.value = false
}
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) {
if (request === previewRequest) error.value = getApiErrorDetails(cause, '打印单据加载失败,请稍后重试')
} finally {
if (request === previewRequest) loading.value = false
}
}
function backToSelection() {
releasePreview()
error.value = null
}
function download() {
if (!printBlob.value || !selectedOption.value) return
const url = URL.createObjectURL(printBlob.value)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = `${bookingCode.value || '订舱单据'}-${selectedOption.value.label}.pdf`
anchor.click()
URL.revokeObjectURL(url)
}
watch(open, (value) => { if (!value) reset() })
onBeforeUnmount(() => { previewRequest += 1; releasePreview() })
</script>
<template>
<AppDialog class="booking-print-dialog admin-density-dialog"
v-model="open"
:title="previewUrl ? previewTitle : selectionTitle"
:fullscreenable="Boolean(previewUrl)"
width="min(1120px, calc(100vw - 48px))"
:loading="loading"
:show-confirm="false"
:cancel-text="previewUrl ? '关闭预览' : '取消'"
destroy-on-close
>
<el-alert v-if="!allowed" title="暂无订舱单据打印权限" type="warning" :closable="false" show-icon />
<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>
</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 }">
<div class="app-dialog__footer booking-print-footer">
<el-button v-if="previewUrl" :disabled="loading" @click="backToSelection">返回选择</el-button>
<el-button v-else :disabled="loading" @click="close">取消</el-button>
<el-button v-if="previewUrl" type="primary" :disabled="!printBlob" @click="download"><Download :size="16" />下载 PDF</el-button>
<el-button v-else type="primary" :loading="loading" :disabled="!allowed || !selectedType" @click="preview">预览 PDF</el-button>
</div>
</template>
</AppDialog>
</template>
<style scoped>
.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>