# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
This commit is contained in:
wuweihua
2026-11-03 18:16:08 +08:00
27 changed files with 1962 additions and 11 deletions
+19
View File
@@ -0,0 +1,19 @@
import request from '@/utils/request'
/** Air bill list */
export function listAirBill(query) {
return request({
url: '/price/airBill/list',
method: 'get',
params: query
})
}
/** Air bill detail by bill code */
export function getAirBill(billCode) {
return request({
url: '/price/airBill/detail',
method: 'get',
params: { billCode }
})
}
@@ -0,0 +1,205 @@
<template>
<div class="app-container air-bill-page">
<basic-table
ref="tableRef"
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
row-key="billCode"
:search-per-row="4"
:page-sizes="[50, 100, 200, 300]"
:page-size="100"
:height="tableHeight"
virtual
>
<template #search-billCodes="{ model }">
<el-input
v-model="model.billCodes"
type="textarea"
:rows="1"
resize="none"
placeholder="提单号,多个用逗号/换行分隔"
clearable
/>
</template>
<template #search-shippingDateRange="{ model }">
<el-date-picker
v-model="model.shippingDateRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="-"
start-placeholder="开始"
end-placeholder="结束"
style="width: 100%"
/>
</template>
<template #search-createTimeRange="{ model }">
<el-date-picker
v-model="model.createTimeRange"
type="datetimerange"
value-format="YYYY-MM-DD HH:mm:ss"
range-separator="-"
start-placeholder="开始"
end-placeholder="结束"
style="width: 100%"
/>
</template>
<template #col-billCode="{ row }">
<el-button link type="primary" @click="openDetail(row)">{{ row.billCode || '-' }}</el-button>
</template>
<template #col-action="{ row }">
<el-button link type="primary" v-hasPermi="['price:airBill:query']" @click="openDetail(row)">详情</el-button>
</template>
</basic-table>
<air-bill-detail v-model="open" :title="title" :loading="loading" :detail="detail" />
</div>
</template>
<script setup name="AirBill">
import { listAirBill, getAirBill } from '@/api/price/airBill'
import AirBillDetail from './components/AirBillDetail.vue'
const tableRef = ref()
const tableHeight = ref(480)
let tableResizeObserver = null
const open = ref(false)
const title = ref('提单详情')
const loading = ref(false)
const detail = ref(null)
const columns = [
{ label: '提单号', prop: 'billCode', minWidth: 150, fixed: 'left', slot: 'billCode', showOverflowTooltip: true },
{ label: '首程落地时间', prop: 'firstLandingTime', width: 170, parseTime: true },
{ label: '航班号', prop: 'airNo', minWidth: 120, showOverflowTooltip: true },
{ label: '实际航班号', prop: 'actualAirNo', minWidth: 120, showOverflowTooltip: true },
{ label: '航空公司代理', prop: 'airCompanyAgent', minWidth: 160, showOverflowTooltip: true },
{ label: '目的港代理编码', prop: 'desPortAgentCode', width: 130 },
{ label: '报关公司编码', prop: 'declareCode', width: 120 },
{ label: '清关编码', prop: 'clearanceCode', width: 110 },
{ label: '清关代理', prop: 'qingguanAgent', minWidth: 120, showOverflowTooltip: true },
{ label: '出仓仓库地址', prop: 'stockAddr', minWidth: 140, showOverflowTooltip: true },
{ label: '提单类型', prop: 'billType', width: 100, align: 'center', dict: 'srv_air_bill_type' },
{ label: '始发机场', prop: 'startAirport', width: 100 },
{ label: '目的机场', prop: 'endAirport', width: 100 },
{ label: '目的港', prop: 'desPort', width: 100 },
{ label: '航线名称', prop: 'routeName', minWidth: 120, showOverflowTooltip: true },
{ label: '航空公司', prop: 'airCompany', minWidth: 110, showOverflowTooltip: true },
{ label: '报关费', prop: 'baoguanFee', width: 100, align: 'right' },
{ label: '报关其他费用', prop: 'baoguanOtherFee', width: 120, align: 'right' },
{ label: '仓储费', prop: 'stockFee', width: 100, align: 'right' },
{ label: '目的港代理费', prop: 'desPortAgentFee', width: 120, align: 'right' },
{ label: '空运其他费用', prop: 'airOtherFee', width: 120, align: 'right' },
{ label: '其他费用', prop: 'otherFee', width: 100, align: 'right' },
{ label: '总件数', prop: 'pieceNumber', width: 90, align: 'center' },
{ label: '总体积', prop: 'volume', width: 100, align: 'right' },
{ label: '实际重量', prop: 'actualWeight', width: 100, align: 'right' },
{ label: '实际体积', prop: 'actualVolume', width: 100, align: 'right' },
{ label: '体积重', prop: 'volumeWeight', width: 100, align: 'right' },
{ label: '发货日期', prop: 'shippingDate', width: 170, parseTime: true },
{ label: '起飞时间', prop: 'startTime', width: 170, parseTime: true },
{ label: '到达时间', prop: 'arriveTime', width: 170, parseTime: true },
{ label: '到仓时间', prop: 'arriveStockTime', width: 170, parseTime: true },
{ label: '清关时间', prop: 'clearanceTime', width: 170, parseTime: true },
{ label: '操作', prop: 'action', align: 'center', width: 90, fixed: 'right', slot: 'action', showInToolbar: false, showOverflowTooltip: false }
]
const searchColumns = [
{ label: '提单号', prop: 'billCodes', span: 6, slot: 'billCodes' },
{ label: '始发机场', prop: 'startAirport', span: 6 },
{ label: '目的机场', prop: 'endAirport', span: 6 },
{ label: '目的港', prop: 'desPort', span: 6 },
{ label: '航线名称', prop: 'routeName', span: 6 },
{ label: '航空公司', prop: 'airCompany', span: 6 },
{ label: '航班号', prop: 'airNo', span: 6 },
{ label: '提单类型', prop: 'billType', span: 6, component: 'select', dict: 'srv_air_bill_type', placeholder: '请选择' },
{ label: '发货日期', prop: 'shippingDateRange', span: 12, slot: 'shippingDateRange' },
{ label: '创建时间', prop: 'createTimeRange', span: 12, slot: 'createTimeRange' }
]
function parseBillCodes(raw) {
const values = Array.isArray(raw) ? raw : [raw]
return [...new Set(
values
.filter(v => v !== null && v !== undefined)
.flatMap(v => String(v).split(/[\s,,;;]+/))
.map(v => v.trim())
.filter(Boolean)
)]
}
function normalizeQuery(params = {}) {
const query = { ...params }
const codes = parseBillCodes(query.billCodes)
if (codes.length === 1) {
query.billCode = codes[0]
delete query.billCodes
} else if (codes.length > 1) {
query.billCodes = codes
} else {
delete query.billCodes
delete query.billCode
}
const ship = query.shippingDateRange
if (Array.isArray(ship) && ship.length === 2) {
query.shippingDateBegin = ship[0]
query.shippingDateEnd = ship[1]
}
delete query.shippingDateRange
const create = query.createTimeRange
if (Array.isArray(create) && create.length === 2) {
query.createTimeBegin = create[0]
query.createTimeEnd = create[1]
}
delete query.createTimeRange
if (query.billType === '' || query.billType === null || query.billType === undefined) {
delete query.billType
}
return query
}
function queryList(params) {
return listAirBill(normalizeQuery(params))
}
function openDetail(row) {
if (!row?.billCode) return
open.value = true
title.value = '提单详情 - ' + row.billCode
loading.value = true
detail.value = null
getAirBill(row.billCode).then(res => {
detail.value = res.data || null
}).finally(() => {
loading.value = false
})
}
function updateTableHeight() {
const tableEl = document.querySelector('.air-bill-page > .basic-table .el-table, .air-bill-page > .basic-table .basic-table-virtual')
if (!tableEl) return
const top = tableEl.getBoundingClientRect().top
tableHeight.value = Math.max(240, Math.floor(window.innerHeight - top - 70))
}
onMounted(() => {
nextTick(() => {
updateTableHeight()
const form = document.querySelector('.air-bill-page > .basic-table .basic-form')
if (form && typeof ResizeObserver !== 'undefined') {
tableResizeObserver = new ResizeObserver(() => updateTableHeight())
tableResizeObserver.observe(form)
}
})
window.addEventListener('resize', updateTableHeight)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateTableHeight)
if (tableResizeObserver) {
tableResizeObserver.disconnect()
tableResizeObserver = null
}
})
</script>
@@ -0,0 +1,116 @@
<template>
<el-drawer
:model-value="modelValue"
:title="title"
size="840px"
append-to-body
destroy-on-close
class="air-bill-detail-drawer"
@update:model-value="$emit('update:modelValue', $event)"
>
<div v-loading="loading" class="air-bill-detail-wrap">
<el-empty v-if="!detail" description="暂无详情" />
<div v-else class="air-bill-detail">
<section class="detail-block">
<div class="sec-title">基本信息</div>
<el-descriptions class="detail-desc" :column="2" border size="small">
<el-descriptions-item label="提单号">{{ detail.billCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="提单类型"><dict-tag :options="srv_air_bill_type" :value="detail.billType" /></el-descriptions-item>
<el-descriptions-item label="始发机场">{{ detail.startAirport || '-' }}</el-descriptions-item>
<el-descriptions-item label="目的机场">{{ detail.endAirport || '-' }}</el-descriptions-item>
<el-descriptions-item label="目的港">{{ detail.desPort || '-' }}</el-descriptions-item>
<el-descriptions-item label="航线名称">{{ detail.routeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="航空公司">{{ detail.airCompany || '-' }}</el-descriptions-item>
<el-descriptions-item label="总件数">{{ numOrDash(detail.pieceNumber) }}</el-descriptions-item>
<el-descriptions-item label="总体积">{{ numOrDash(detail.volume) }}</el-descriptions-item>
<el-descriptions-item label="实际重量">{{ numOrDash(detail.actualWeight) }}</el-descriptions-item>
<el-descriptions-item label="实际体积">{{ numOrDash(detail.actualVolume) }}</el-descriptions-item>
<el-descriptions-item label="体积重">{{ numOrDash(detail.volumeWeight) }}</el-descriptions-item>
</el-descriptions>
</section>
<section class="detail-block">
<div class="sec-title">费用信息</div>
<el-descriptions class="detail-desc" :column="2" border size="small">
<el-descriptions-item label="报关费">{{ money(detail.baoguanFee) }}</el-descriptions-item>
<el-descriptions-item label="报关其他费用">{{ money(detail.baoguanOtherFee) }}</el-descriptions-item>
<el-descriptions-item label="仓储费">{{ money(detail.stockFee) }}</el-descriptions-item>
<el-descriptions-item label="目的港代理费">{{ money(detail.desPortAgentFee) }}</el-descriptions-item>
<el-descriptions-item label="空运其他费用">{{ money(detail.airOtherFee) }}</el-descriptions-item>
<el-descriptions-item label="其他费用">{{ money(detail.otherFee) }}</el-descriptions-item>
</el-descriptions>
</section>
<section class="detail-block">
<div class="sec-title">航班与代理</div>
<el-descriptions class="detail-desc" :column="2" border size="small">
<el-descriptions-item label="航班号">{{ detail.airNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="实际航班号">{{ detail.actualAirNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="航空公司代理">{{ detail.airCompanyAgent || '-' }}</el-descriptions-item>
<el-descriptions-item label="目的港代理编码">{{ detail.desPortAgentCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="报关公司编码">{{ detail.declareCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="清关编码">{{ detail.clearanceCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="清关代理">{{ detail.qingguanAgent || '-' }}</el-descriptions-item>
<el-descriptions-item label="出仓仓库地址">{{ detail.stockAddr || '-' }}</el-descriptions-item>
</el-descriptions>
</section>
<section class="detail-block">
<div class="sec-title">时效信息</div>
<el-descriptions class="detail-desc" :column="2" border size="small">
<el-descriptions-item label="首程落地时间">{{ formatDateTime(detail.firstLandingTime) }}</el-descriptions-item>
<el-descriptions-item label="发货日期">{{ formatDateTime(detail.shippingDate) }}</el-descriptions-item>
<el-descriptions-item label="起飞时间">{{ formatDateTime(detail.startTime) }}</el-descriptions-item>
<el-descriptions-item label="到达时间">{{ formatDateTime(detail.arriveTime) }}</el-descriptions-item>
<el-descriptions-item label="到仓时间">{{ formatDateTime(detail.arriveStockTime) }}</el-descriptions-item>
<el-descriptions-item label="清关时间">{{ formatDateTime(detail.clearanceTime) }}</el-descriptions-item>
</el-descriptions>
</section>
</div>
</div>
</el-drawer>
</template>
<script setup name="AirBillDetail">
import { parseTime } from '@/utils/bms'
const { srv_air_bill_type } = useDict('srv_air_bill_type')
defineProps({
modelValue: { type: Boolean, default: false },
title: { type: String, default: '提单详情' },
loading: { type: Boolean, default: false },
detail: { type: Object, default: null }
})
defineEmits(['update:modelValue'])
function numOrDash(v) {
return v === null || v === undefined || v === '' ? '-' : v
}
function money(v) {
if (v === null || v === undefined || v === '') return '-'
const n = Number(v)
if (Number.isNaN(n)) return String(v)
return n.toFixed(4)
}
function formatDateTime(v) {
return v ? parseTime(v) : '-'
}
</script>
<style scoped>
.air-bill-detail-wrap {
min-height: 200px;
}
.detail-block + .detail-block {
margin-top: 16px;
}
.sec-title {
font-weight: 600;
margin-bottom: 8px;
color: var(--el-text-color-primary);
}
</style>