109 lines
5.8 KiB
Vue
109 lines
5.8 KiB
Vue
<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>
|