132 lines
6.8 KiB
Vue
132 lines
6.8 KiB
Vue
<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>
|