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

109 lines
5.8 KiB
Vue
Raw Normal View History

<script setup lang="ts">
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)
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}`)
function releasePreview() {
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
previewUrl.value = ''
printBlob.value = null
}
function reset() {
releasePreview()
selectedType.value = ''
error.value = null
loading.value = false
}
async function preview() {
if (!props.allowed || !selectedType.value || loading.value) return
loading.value = true
error.value = null
releasePreview()
try {
const blob = await downloadOceanBookingPrint(props.bookingId, selectedType.value)
printBlob.value = blob
previewUrl.value = URL.createObjectURL(blob)
} catch (cause) {
error.value = getApiErrorDetails(cause, '打印单据加载失败,请稍后重试')
} finally {
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 = `${props.bookingNo || props.bookingId}-${selectedOption.value.label}.pdf`
anchor.click()
URL.revokeObjectURL(url)
}
watch(open, (value) => { if (!value) reset() })
onBeforeUnmount(releasePreview)
</script>
<template>
<AppDialog
v-model="open"
:title="previewUrl ? previewTitle : '选择打印单据'"
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 />
<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>
<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">
<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 16px;color:var(--cc-muted);font-size:13px}.booking-print-current strong{color:var(--cc-text)}.booking-print-options{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.booking-print-option{display:grid;grid-template-columns:auto auto minmax(0,1fr);align-items:center;gap:12px;min-height:92px;padding:16px;border:1px solid var(--cc-border);border-radius:10px;background:#fff;cursor:pointer}.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:4px}.booking-print-option strong{color:var(--cc-text);font-size:15px}.booking-print-option small{color:var(--cc-muted);font-size:12px}.booking-print-preview{min-height:0;overflow:hidden;border:1px solid var(--cc-border);border-radius:8px;background:#f3f5f8}.booking-print-preview iframe{display:block;width:100%;height:min(72vh,760px);border:0;background:#fff}.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}}
</style>