2026-09-08 22:03:03 +08:00
< script setup lang = "ts" >
2026-09-14 18:34:34 +08:00
import '@/styles/adminDensity.css'
2026-09-09 15:26:43 +08:00
import { computed , onMounted , onBeforeUnmount , reactive , ref , watch } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { getFinancePaymentVoucher , downloadFinancePaymentVoucher } from '@/modules/finance'
2026-09-08 22:03:03 +08:00
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { getApiErrorDetails , getApiErrorMessage , type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
2026-09-09 00:49:13 +08:00
import { listContainerFeeSupplierOptions , uploadContainerFeePaymentVoucher , type ContainerFeePaymentMethod , type ContainerFeeSettlementMode , type ContainerFeeSupplierOption } from '@/modules/loading-lists'
2026-09-08 22:03:03 +08:00
import { adjustOverseasContainerFee , createOverseasContainerFee , listOverseasContainerCustomers , listOverseasContainerFees , reviewOverseasContainerFee , submitOverseasContainerFee , voidOverseasContainerFee , type OverseasContainerCustomer , type OverseasContainerFeeDirection , type OverseasContainerFeeItem , type OverseasContainerFeeSaveRequest , type OverseasContainerFeeSource } from '../api/overseasContainerFees'
2026-09-09 10:18:12 +08:00
const props = withDefaults ( defineProps < { title ?: string ; containerBookingId : string ; containerNo : string | null ; readonly ?: boolean } > (), { title : '目的港及海外段费用(尾程)' , readonly : false })
2026-09-08 22:03:03 +08:00
const access = useAccessPreviewStore ()
const rows = ref < OverseasContainerFeeSource [] >([])
const customers = ref < OverseasContainerCustomer [] >([])
const suppliers = ref < ContainerFeeSupplierOption [] >([])
const loading = ref ( false )
const actionLoading = ref ( false )
const voucherUploading = ref ( false )
2026-09-09 15:26:43 +08:00
const voucherInput = ref < HTMLInputElement | null >( null )
const voucherNames = reactive < Record < string , string > > ({})
let uploadSequence = 0
let previewSequence = 0
const previewOpen = ref ( false )
const previewLoading = ref ( false )
const previewError = ref < ApiErrorDetails | null >( null )
const previewUrl = ref ( '' )
const previewName = ref ( '付款凭证' )
const previewIsImage = ref ( false )
function clearPreview () {
previewSequence ++
if ( previewUrl . value ) URL . revokeObjectURL ( previewUrl . value )
previewUrl . value = ''
}
async function previewVoucher ( customerId : string , fileId : string ) {
clearPreview ()
const sequence = previewSequence
previewOpen . value = true ; previewLoading . value = true ; previewError . value = null
previewName . value = voucherNames [ fileId ] || '付款凭证'
try {
const metadata = await getFinancePaymentVoucher ( customerId , fileId )
const blob = await downloadFinancePaymentVoucher ( customerId , fileId )
if ( sequence !== previewSequence ) return
previewName . value = metadata . fileName || '付款凭证'
voucherNames [ fileId ] = previewName . value
previewIsImage . value = [ 'image/jpeg' , 'image/png' , 'image/gif' , 'image/webp' , 'image/bmp' ]. includes ( metadata . contentType )
previewUrl . value = URL . createObjectURL ( new Blob ([ blob ], { type : previewIsImage . value ? metadata . contentType : 'application/octet-stream' }))
} catch ( cause ) {
if ( sequence === previewSequence ) previewError . value = getApiErrorDetails ( cause , '付款凭证读取失败' )
} finally {
if ( sequence === previewSequence ) previewLoading . value = false
}
}
watch ( previewOpen , ( open ) => { if ( ! open ) clearPreview () })
onBeforeUnmount (() => { uploadSequence ++ ; clearPreview () })
2026-09-08 22:03:03 +08:00
const error = ref < ApiErrorDetails | null >( null )
const dialogOpen = ref ( false )
const editing = ref < OverseasContainerFeeSource | null >( null )
const canView = computed (() => access . can ( 'platform:finance:customer-arap:list' ))
2026-09-09 10:18:12 +08:00
const canEdit = computed (() => ! props . readonly && access . can ( 'platform:finance:fee-source:edit' ))
const canReview = computed (() => ! props . readonly && access . can ( 'platform:finance:fee-source:review' ))
2026-09-08 22:03:03 +08:00
const feeItems : Array < { value : OverseasContainerFeeItem ; label : string } > = [
{ value : 'PORT_FEE' , label : '港口费' }, { value : 'STORAGE_FEE' , label : '堆存费' },
{ value : 'CONTAINER_REPAIR_FEE' , label : '修箱费' }, { value : 'CRANE_FEE' , label : '吊机费' },
{ value : 'TERMINAL_DRAYAGE_FEE' , label : '码头拖车费' }, { value : 'OTHER' , label : '其他' },
]
const feeItemLabel = ( value : string ) => feeItems . find (( item ) => item . value === value ) ? . label || value
const reviewLabels : Record < string , string > = { DRAFT : '草稿' , PENDING : '待审核' , APPROVED : '已审核' , REJECTED : '已驳回' }
const form = reactive < { customerId : string ; feeItemCode : OverseasContainerFeeItem ; direction : OverseasContainerFeeDirection ; businessDate : string ; amount : number | null ; supplierId : string ; settlementMode : ContainerFeeSettlementMode ; paidAt : string ; paymentMethod : ContainerFeePaymentMethod ; paymentReferenceNo : string ; paymentVoucherFileIds : string [] } > ({ customerId : '' , feeItemCode : 'PORT_FEE' , direction : 'PAYABLE' , businessDate : new Date (). toISOString (). slice ( 0 , 10 ), amount : null , supplierId : '' , settlementMode : 'ON_ACCOUNT' , paidAt : '' , paymentMethod : 'BANK_TRANSFER' , paymentReferenceNo : '' , paymentVoucherFileIds : [] })
2026-09-09 15:26:43 +08:00
watch (() => form . customerId , () => { uploadSequence ++ ; voucherUploading . value = false ; form . paymentVoucherFileIds = [] }, { flush : 'sync' })
watch ( dialogOpen , ( open ) => { if ( ! open ) { uploadSequence ++ ; voucherUploading . value = false } }, { flush : 'sync' })
2026-09-08 22:03:03 +08:00
function key ( scope : string ) { return ` ${ scope } - ${ Date . now () } - ${ Math . random (). toString ( 36 ). slice ( 2 , 10 ) } ` }
function customerName ( customerId : string ) { return customers . value . find (( item ) => item . customerId === customerId ) ? . customerName || null }
function selectedSupplier () { return suppliers . value . find (( item ) => item . id === form . supplierId ) || null }
function saveRequest ( row ?: OverseasContainerFeeSource ) : OverseasContainerFeeSaveRequest {
const item = feeItems . find (( option ) => option . value === form . feeItemCode )
return {
customerId : form . customerId , customerName : customerName ( form . customerId ), sourceType : 'OVERSEAS_CONTAINER_FEE' , businessType : 'OVERSEAS_CONTAINER' ,
businessId : props . containerBookingId , businessNo : props . containerNo || '' , orderId : null , inboundId : null , inboundCode : null , containerId : null ,
containerBookingId : props . containerBookingId , containerNo : props . containerNo || '' , warehouseId : null ,
feeItemCode : form . feeItemCode , feeItemName : item ? . label || row ? . feeItemName || form . feeItemCode , direction : form . direction ,
supplierId : form . direction === 'PAYABLE' ? selectedSupplier () ? . id || null : null ,
supplierName : form . direction === 'PAYABLE' ? selectedSupplier () ? . supplierName || null : null ,
supplierCategory : form . direction === 'PAYABLE' ? selectedSupplier () ? . category || null : null ,
settlementMode : form . direction === 'PAYABLE' ? form . settlementMode : null ,
paidAt : form . direction === 'PAYABLE' && form . settlementMode === 'DIRECT_PAID' ? form . paidAt || null : null ,
paymentMethod : form . direction === 'PAYABLE' && form . settlementMode === 'DIRECT_PAID' ? form . paymentMethod : null ,
paymentReferenceNo : form . direction === 'PAYABLE' && form . settlementMode === 'DIRECT_PAID' ? form . paymentReferenceNo . trim () || null : null ,
paymentVoucherFileIds : form . direction === 'PAYABLE' && form . settlementMode === 'DIRECT_PAID' && form . paymentVoucherFileIds . length ? form . paymentVoucherFileIds : null ,
businessDate : form . businessDate , currency : 'CNY' , amount : Number ( form . amount ),
}
}
async function load () {
if ( ! canView . value ) return
loading . value = true ; error . value = null
2026-09-09 11:55:10 +08:00
try { [ rows . value , customers . value , suppliers . value ] = await Promise . all ([ listOverseasContainerFees ( props . containerBookingId ), listOverseasContainerCustomers ( props . containerBookingId ), canEdit . value && access . can ( 'platform:supplier:list' ) ? listContainerFeeSupplierOptions ( null ) : Promise . resolve ([])]) }
2026-09-08 22:03:03 +08:00
catch ( cause ) { error . value = getApiErrorDetails ( cause , '尾程计费加载失败' ) }
finally { loading . value = false }
}
function openCreate () {
editing . value = null ; form . customerId = customers . value [ 0 ] ? . customerId || '' ; form . feeItemCode = 'PORT_FEE' ; form . direction = 'PAYABLE' ; form . businessDate = new Date (). toISOString (). slice ( 0 , 10 ); form . amount = null ; form . supplierId = '' ; form . settlementMode = 'ON_ACCOUNT' ; form . paidAt = '' ; form . paymentMethod = 'BANK_TRANSFER' ; form . paymentReferenceNo = '' ; form . paymentVoucherFileIds = []; dialogOpen . value = true
}
2026-09-09 15:26:43 +08:00
function openAdjust ( row : OverseasContainerFeeSource ) {
2026-09-08 22:03:03 +08:00
editing . value = row ; form . customerId = row . customerId ; form . feeItemCode = row . feeItemCode ; form . direction = row . direction ; form . businessDate = row . businessDate ; form . amount = row . amount ; form . supplierId = row . supplierId || '' ; form . settlementMode = row . settlementMode === 'DIRECT_PAID' ? 'DIRECT_PAID' : 'ON_ACCOUNT' ; form . paidAt = row . paidAt || '' ; form . paymentMethod = ( row . paymentMethod as ContainerFeePaymentMethod ) || 'BANK_TRANSFER' ; form . paymentReferenceNo = row . paymentReferenceNo || '' ; form . paymentVoucherFileIds = [... row . paymentVoucherFileIds ]; dialogOpen . value = true
}
async function mutate ( task : () => Promise < unknown >, success : string ) {
actionLoading . value = true
try { await task (); ElMessage . success ( success ); dialogOpen . value = false ; await load () }
catch ( cause ) { ElMessage . error ( getApiErrorMessage ( cause , '尾程计费操作失败' )) }
finally { actionLoading . value = false }
}
async function save () {
2026-09-09 15:26:43 +08:00
if ( voucherUploading . value || actionLoading . value ) return
2026-09-08 22:03:03 +08:00
if ( ! form . customerId || ! form . businessDate || ! form . amount || form . amount <= 0 ) return ElMessage . warning ( '请完整填写客户、业务日期和大于 0 的金额' )
if ( ! props . containerNo ) return ElMessage . warning ( '请先补录真实集装箱号后再维护尾程费用' )
if ( form . direction === 'PAYABLE' && ! selectedSupplier ()) return ElMessage . warning ( '应付费用必须选择供应商' )
if ( form . direction === 'PAYABLE' && form . settlementMode === 'DIRECT_PAID' && ( ! form . paidAt || ! form . paymentMethod || ( ! form . paymentReferenceNo . trim () && form . paymentVoucherFileIds . length === 0 ))) return ElMessage . warning ( '直接已付必须填写付款时间、方式,并提供流水号或付款凭证' )
const payload = saveRequest ( editing . value || undefined )
if ( editing . value ) await mutate (() => adjustOverseasContainerFee ( editing . value ! . sourceFeeId , editing . value ! . sourceVersion , payload , key ( 'container-fee-adjust' )), '费用调整已提交审核' )
else await mutate (() => createOverseasContainerFee ( payload , key ( 'container-fee-create' )), '费用草稿已创建' )
}
2026-09-09 15:26:43 +08:00
async function uploadVoucher ( event : Event ) {
const input = event . target as HTMLInputElement
const file = input . files ? .[ 0 ]
input . value = ''
if ( ! file || voucherUploading . value ) return
if ( ! form . customerId ) { ElMessage . warning ( '请先选择费用所属客户' ); return }
const customerId = form . customerId
const sequence = ++ uploadSequence
2026-09-08 22:03:03 +08:00
voucherUploading . value = true
2026-09-09 15:26:43 +08:00
try {
const uploaded = await uploadContainerFeePaymentVoucher ( customerId , file )
if ( sequence !== uploadSequence ) return
form . paymentVoucherFileIds = [ uploaded . fileId ]
voucherNames [ uploaded . fileId ] = uploaded . fileName || file . name
ElMessage . success ( '付款凭证已上传' )
} catch ( cause ) {
if ( sequence === uploadSequence ) ElMessage . error ( getApiErrorMessage ( cause , '付款凭证上传失败' ))
} finally {
if ( sequence === uploadSequence ) voucherUploading . value = false
}
2026-09-08 22:03:03 +08:00
}
2026-09-09 15:26:43 +08:00
async function submit ( row : OverseasContainerFeeSource ) {
2026-09-14 18:34:34 +08:00
await ElMessageBox . confirm ( `确认提交“ ${ row . feeItemName } ”审核?` , '提交费用' , { customClass : 'admin-density-confirm' , type : 'warning' })
2026-09-08 22:03:03 +08:00
await mutate (() => submitOverseasContainerFee ( row . sourceFeeId , key ( 'container-fee-submit' )), '费用已提交审核' )
}
2026-09-09 15:26:43 +08:00
async function review ( row : OverseasContainerFeeSource , decision : 'APPROVE' | 'REJECT' ) {
2026-09-08 22:03:03 +08:00
let reason : string | null = null
2026-09-14 18:34:34 +08:00
if ( decision === 'REJECT' ) { const result = await ElMessageBox . prompt ( '请输入驳回原因' , '驳回费用' , { customClass : 'admin-density-confirm' , inputPattern : /\S+/ , inputErrorMessage : '请输入驳回原因' , type : 'warning' }); reason = result . value . trim () }
else await ElMessageBox . confirm ( `确认审核通过“ ${ row . feeItemName } ”?` , '审核费用' , { customClass : 'admin-density-confirm' , type : 'warning' })
2026-09-08 22:03:03 +08:00
await mutate (() => reviewOverseasContainerFee ( row . sourceFeeId , decision , reason , key ( 'container-fee-review' )), decision === 'APPROVE' ? '费用已审核通过' : '费用已驳回' )
}
2026-09-09 15:26:43 +08:00
async function voidFee ( row : OverseasContainerFeeSource ) {
2026-09-14 18:34:34 +08:00
const result = await ElMessageBox . prompt ( '请输入作废原因' , '作废费用' , { customClass : 'admin-density-confirm' , inputPattern : /\S+/ , inputErrorMessage : '请输入作废原因' , type : 'warning' })
2026-09-08 22:03:03 +08:00
await mutate (() => voidOverseasContainerFee ( row . sourceFeeId , row . sourceVersion , result . value . trim (), key ( 'container-fee-void' )), '费用已作废' )
}
onMounted ( load )
</ script >
< template >
2026-09-14 18:34:34 +08:00
< div class = "last-leg-fees admin-density" >
2026-09-09 10:18:12 +08:00
< header class = "last-leg-fees__header" >< div >< h2 >{{ title }}</ h2 >< p > 按真实货柜展示港口 、 堆存 、 修箱 、 吊机和码头拖车费用 , 金额按币种分别列示 。 </ p ></ div >< el-button v-if = "canEdit" type="primary" :disabled="!containerNo || customers.length === 0" @click="openCreate" > 新增费用 </ el -button ></ header >
2026-09-09 11:55:10 +08:00
< el-alert v-if = "canView && !canEdit && !canReview" type="info" :closable="false" title="当前账号仅有海外段费用查看权限。" />
2026-09-08 22:03:03 +08:00
< el-alert v-if = "!containerNo" type="warning" :closable="false" title="当前货柜尚未补录真实集装箱号,暂不能新增尾程费用。" />
< el-alert v-if = "!canView" type="warning" :closable="false" title="当前账号无权查看尾程计费。" />
< LoadErrorState v-else-if = "error" title="尾程计费加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></ template ></ LoadErrorState >
< el-table v-else v-loading = "loading" :data="rows" empty-text="当前货柜暂无尾程费用" >
2026-09-14 18:34:34 +08:00
< el-table-column label = "操作" fixed = "left" width = "240" >< template # default = "{ row }" >< div class = "row-actions density-actions" >< el-button data -tone =" primary " class = "density-action" v-if = "canEdit && row.sourceStatus === 'ACTIVE' && ['DRAFT', 'REJECTED'].includes(row.reviewStatus)" type="primary" @click="submit(row as OverseasContainerFeeSource)">提交</el-button><el-button data-tone="warning" class="density-action" v-if="canReview && row.sourceStatus === 'ACTIVE' && row.reviewStatus === 'PENDING'" type="success" @click="review(row as OverseasContainerFeeSource, 'APPROVE')">通过</el-button><el-button data-tone="danger" class="density-action" v-if="canReview && row.sourceStatus === 'ACTIVE' && row.reviewStatus === 'PENDING'" type="danger" @click="review(row as OverseasContainerFeeSource, 'REJECT')">驳回</el-button><el-button class="density-action" v-if="canEdit && row.sourceStatus === 'ACTIVE' && row.reviewStatus === 'APPROVED'" type="primary" @click="openAdjust(row as OverseasContainerFeeSource)">调整</el-button><el-button data-tone="danger" class="density-action" v-if="canReview && row.sourceStatus === 'ACTIVE' && row.reviewStatus === 'APPROVED'" type="danger" @click="voidFee(row as OverseasContainerFeeSource)">作废</el-button><span v-if="row.sourceStatus !== 'ACTIVE' || (!canEdit && !canReview)" > — </ span ></ div ></ template > </ el -table-column >
2026-09-08 22:03:03 +08:00
< el-table-column prop = "customerName" label = "客户" min -width =" 160 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . customerName || '—' }}</ template ></ el-table-column >
< el-table-column prop = "feeItemCode" label = "费用项目" min -width =" 140 " show -overflow-tooltip >< template # default = "{ row }" >{{ feeItemLabel ( row . feeItemCode ) }}</ template ></ el-table-column >
< el-table-column prop = "direction" label = "方向" width = "100" >< template # default = "{ row }" >{{ row . direction === 'RECEIVABLE' ? '应收' : '应付' }}</ template ></ el-table-column >
< el-table-column prop = "supplierName" label = "供应商" min -width =" 150 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . supplierName || '—' }}</ template ></ el-table-column >
< el-table-column prop = "amount" label = "金额" min -width =" 130 " align = "right" >< template # default = "{ row }" > ¥ {{ row . amount . toFixed ( 2 ) }}</ template ></ el-table-column >
2026-09-09 15:26:43 +08:00
< el-table-column label = "付款凭证" min -width =" 180 " >< template # default = "{ row }" >< div v-if = "row.paymentVoucherFileIds.length" class="voucher-list"><el-button v-for="(fileId, index) in (row.paymentVoucherFileIds as string[])" :key="fileId" link type="primary" @click="previewVoucher(row.customerId, fileId)" >{{ voucherNames [ fileId ] || ` 付款凭证 $ { index + 1 } ` }} </ el -button ></ div >< span v-else > — </ span ></ template > </ el -table-column >
2026-09-08 22:03:03 +08:00
< el-table-column prop = "businessDate" label = "业务日期" width = "120" />
< el-table-column prop = "reviewStatus" label = "审核状态" width = "110" >< template # default = "{ row }" >< el-tag >{{ reviewLabels [ row . reviewStatus ] || row . reviewStatus }}</ el-tag ></ template ></ el-table-column >
< el-table-column prop = "reviewReason" label = "审核说明" min -width =" 180 " show -overflow-tooltip >< template # default = "{ row }" >{{ row . reviewReason || '—' }}</ template ></ el-table-column >
</ el-table >
2026-09-14 18:34:34 +08:00
< AppDialog class = "admin-density-dialog" v-model = "dialogOpen" :title="editing ? '调整尾程费用' : '新增尾程费用'" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="actionLoading" :confirm-disabled="voucherUploading" @confirm="save" >
< el-form class = "density-form" label -position =" top " >< div class = "fee-form-grid" >< el-form-item label = "客户" required >< el-select v-model = "form.customerId" :disabled="Boolean(editing) || voucherUploading" placeholder="请选择柜内客户"><el-option v-for="customer in customers" :key="customer.customerId" :label="customer.customerName || '未命名客户'" :value="customer.customerId" /></el-select></el-form-item><el-form-item label="费用项目" required><el-select v-model="form.feeItemCode"><el-option v-for="item in feeItems" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item><el-form-item label="费用方向" required><el-select v-model="form.direction"><el-option label="应收" value="RECEIVABLE" /><el-option label="应付" value="PAYABLE" /></el-select></el-form-item><el-form-item label="业务日期" required><el-date-picker v-model="form.businessDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="金额(CNY) " required><el-input-number v-model="form.amount" :min="0.01" :precision="2" :step="10" placeholder="请输入费用金额" /></el-form-item><template v-if="form.direction === 'PAYABLE'"><el-form-item label="供应商" required><el-select v-model="form.supplierId" filterable placeholder="请选择有效供应商"><el-option v-for="supplier in suppliers" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" /></el-select></el-form-item><el-form-item label="结算方式" required><el-select v-model="form.settlementMode"><el-option label="账期结算" value="ON_ACCOUNT" /><el-option label="直接已付" value="DIRECT_PAID" /></el-select></el-form-item><template v-if="form.settlementMode === 'DIRECT_PAID'"><el-form-item label="付款时间" required><el-date-picker v-model="form.paidAt" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" /></el-form-item><el-form-item label="付款方式" required><el-select v-model="form.paymentMethod"><el-option label="线上支付" value="ONLINE" /><el-option label="银行转账" value="BANK_TRANSFER" /><el-option label="现金" value="CASH" /><el-option label="其他" value="OTHER" /></el-select></el-form-item><el-form-item label="付款流水号"><el-input v-model="form.paymentReferenceNo" maxlength="128" placeholder="请输入付款流水号(可选)" /></el-form-item><el-form-item label="付款凭证"><div class="voucher-list"><input ref="voucherInput" type="file" accept="image/*" hidden @change="uploadVoucher" /><el-button :loading="voucherUploading" :disabled="!form.customerId || actionLoading" @click="voucherInput?.click()">{{ form.paymentVoucherFileIds.length ? '重新上传凭证' : '上传付款凭证' }}</el-button><div v-for="(fileId, index) in form.paymentVoucherFileIds" :key="fileId" class="voucher-item"><el-button link type="primary" @click="previewVoucher(form.customerId, fileId)">{{ voucherNames[fileId] || `付款凭证 ${index + 1}` }}</el-button><el-button link type="danger" :disabled="voucherUploading || actionLoading" @click="form.paymentVoucherFileIds = form.paymentVoucherFileIds.filter((id) => id !== fileId)" > 移除 </ el -button ></ div ></ div ></ el-form-item ></ template ></ template ></ div ></ el-form >
2026-09-09 15:26:43 +08:00
</ AppDialog >
2026-09-14 18:34:34 +08:00
< AppDialog class = "admin-density-dialog" v-model = "previewOpen" :title="previewName" width="min(900px, calc(100vw - 28px))" :show-confirm="false" cancel-text="关闭" append -to-body >
2026-09-09 15:26:43 +08:00
< p v-if = "previewLoading" > 正在读取付款凭证 … </ p >
< div v-else-if = "previewError"><el-alert type="error" :closable="false" :title="previewError.message" /><ApiErrorMeta :details="previewError" /></ div >
< div v-else-if = "previewUrl" class="voucher-preview"><img v-if="previewIsImage" :src="previewUrl" :alt="previewName" /><p v-else>此文件暂不支持预览,请下载后查看。</p><a :href="previewUrl" :download="previewName" > 下载付款凭证 </ a ></ div >
2026-09-08 22:03:03 +08:00
</ AppDialog >
</ div >
</ template >
< style scoped >
2026-09-14 18:34:34 +08:00
. last - leg - fees { display : grid ; gap : var ( -- admin - space - 5 ); min - width : 0 ; }
. last - leg - fees__header { display : flex ; align - items : center ; justify - content : space - between ; gap : var ( -- admin - space - 6 ); }
. last - leg - fees__header h2 { margin : 0 ; color : # 253553 ; font - size : var ( -- admin - font - title ); }
. last - leg - fees__header p { margin : var ( -- admin - space - 2 ) 0 0 ; color : # 77839 b ; }
. voucher - list { display : flex ; flex - direction : column ; align - items : flex - start ; gap : var ( -- admin - space - 3 ); min - width : 0 ; }
. voucher - item { display : flex ; gap : var ( -- admin - space - 3 ); max - width : 100 % ; }
2026-09-09 15:26:43 +08:00
. voucher - list : deep (. el - button ) { white - space : normal ; overflow - wrap : anywhere ; margin - left : 0 ; }
2026-09-14 18:34:34 +08:00
. voucher - preview { display : grid ; gap : var ( -- admin - space - 5 ); }
2026-09-09 15:26:43 +08:00
. voucher - preview img { max - width : 100 % ; max - height : 65 vh ; object - fit : contain ; margin : auto ; }
2026-09-14 18:34:34 +08:00
. row - actions { white - space : nowrap ; }
. fee - form - grid { display : grid ; grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr )); gap : 0 var ( -- admin - space - 5 ); }
2026-09-08 22:03:03 +08:00
. fee - form - grid : deep (. el - select ), . fee - form - grid : deep (. el - date - editor ), . fee - form - grid : deep (. el - input - number ) { width : 100 % ; }
@ media ( max - width : 760 px ) { . last - leg - fees__header { align - items : flex - start ; flex - direction : column ; } . fee - form - grid { grid - template - columns : 1 fr ; } }
</ style >