Files
shanxia2.0/web-admin/src/views/pl/freight/components/UnsettledPlanPanel.vue
T
zlwuandCursor b1f734dbb0 feat(pl): add Excel export for freight unsettled plans and settlement tabs
Support filtered and selected export across freight settlement board panels.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-21 18:17:10 +08:00

477 lines
15 KiB
Vue

<template>
<div class="freight-tab-panel">
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="运输公司">
<el-select
v-model="searchForm.carrierId"
placeholder="全部"
clearable
filterable
remote
:remote-method="searchCarriers"
:loading="carrierSelectLoading"
@focus="onCarrierSelectFocus"
>
<el-option
v-for="c in carrierList"
:key="c.id"
:label="c.carrierName"
:value="c.id"
/>
</el-select>
</el-form-item>
<el-form-item label="计划编号">
<el-input
v-model="searchForm.q"
placeholder="搜索计划编号/客户"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="单据类型">
<el-select v-model="searchForm.planType" placeholder="全部" clearable>
<el-option label="直销单" value="direct" />
<el-option label="入库单" value="inbound" />
<el-option label="出库单" value="outbound" />
</el-select>
</el-form-item>
<el-form-item label="车牌号">
<el-input v-model="searchForm.licensePlate" placeholder="搜索车牌号" clearable />
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="计划编号/客户/车牌/承运商"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-main"
type="primary"
:disabled="!selectedRows.length"
:loading="createLoading"
@click="handleCreateSettlement"
>
<el-icon><DocumentAdd /></el-icon>生成结算单
</el-button>
<ListExportDropdown
:export-fn="freightApi.exportUnsettledPlans"
:get-filter-params="getUnsettledExportFilterParams"
:has-selection="true"
:selected-ids="selectedRows.map((r) => r.id)"
:visible-columns="unsettledExportColumns"
filename="待结算计划.xlsx"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<div ref="unsettledTableWrapRef" class="table-scroll bx-list-table-wrap">
<ListTable table-key="pl-freight-components-unsettled-plan-panel"
class="list-table"
:data="planList"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleViewPlan"
@header-dragend="() => unsettledStatsRowRef?.resync?.()"
>
<el-table-column type="selection" min-width="48" align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="planNo" label="计划编号" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="handleViewPlan(row)">{{ row.planNo }}</el-link>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('planDate')"
prop="planDate"
label="计划日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('planType')"
prop="planType"
label="单据类型"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
{{ getPlanTypeLabel(row.planType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('partnerName')"
prop="partnerName"
label="客户/供应商"
min-width="140"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.customerName || row.supplierName || '—' }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="160"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
{{ formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('transportCompany')"
prop="transportCompany"
label="承运商"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('freightAmount')"
prop="freightAmount"
label="运费(元)"
min-width="120"
align="right"
>
<template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('archiveTime')"
prop="archiveTime"
label="归档时间"
min-width="160"
align="center"
>
<template #default="{ row }">{{
row.archiveTime ? formatDateTime(row.archiveTime) : '—'
}}</template>
</el-table-column>
</ListTable>
<ListColumnStatsRow
ref="unsettledStatsRowRef"
storage-key="pl-freight-unsettled-plan-column-stats"
:columns="unsettledStatsRowColumns"
:data="planList"
:table-wrap-el="unsettledTableWrapRef"
:get-cell-value="getUnsettledStatCellValue"
/>
</div>
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 100, 500]"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
<PlPlanViewDialog v-model:visible="planViewVisible" :plan-id="viewPlanId" />
</div>
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
import type { StatsRowColumn } from '@/utils/list-column-stats'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { DocumentAdd, RefreshLeft, Search } from '@element-plus/icons-vue'
import { freightApi, vehicleApi, type UnsettledPlanItem, type Vehicle } from '@/api/pl'
import { usePlRemoteCarrierOptions } from '@/composables/usePlRemoteOptions'
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
import { pickBxPageResult } from '@/utils/http/bx'
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
import { getPlanTypeLabel, getPlanTypeTagType } from '@/views/pl/freight/freight-status'
import { buildVehicleTrailerLookup, formatPlVehiclePlateDisplay } from '@/utils/pl-display'
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatDateTime } from '@/utils/format'
const emit = defineEmits<{ created: [settlementId: string] }>()
const listColumnDefs = [
{ prop: 'planDate', label: '计划日期' },
{ prop: 'planType', label: '单据类型' },
{ prop: 'partnerName', label: '客户/供应商' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'transportCompany', label: '承运商' },
{ prop: 'freightAmount', label: '运费(元)' },
{ prop: 'archiveTime', label: '归档时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-freight-unsettled-plan-columns', listColumnDefs)
const unsettledTableWrapRef = ref<HTMLElement | null>(null)
const unsettledStatsRowRef = ref<{ resync?: () => void } | null>(null)
const unsettledStatsRowColumns = computed((): StatsRowColumn[] => {
const cols: StatsRowColumn[] = [
{ key: '__selection', interactive: false },
{ key: '__index', interactive: false },
{ key: 'planNo', interactive: true, prop: 'planNo' }
]
for (const def of listColumnDefs) {
if (!isColumnVisible(def.prop)) continue
cols.push({
key: def.prop,
interactive: true,
prop: def.prop,
money: def.prop === 'freightAmount'
})
}
return cols
})
function getUnsettledStatCellValue(row: UnsettledPlanItem, prop: string): unknown {
if (prop === 'planType') return getPlanTypeLabel(row.planType)
if (prop === 'partnerName') return row.customerName || row.supplierName || ''
if (prop === 'licensePlate') {
return formatPlVehiclePlateDisplay(row.licensePlate, undefined, vehicleTrailerLookup.value)
}
if (prop === 'archiveTime') {
return row.archiveTime ? formatDateTime(row.archiveTime) : ''
}
return (row as any)?.[prop]
}
const loading = ref(false)
const createLoading = ref(false)
const {
list: carrierList,
loading: carrierSelectLoading,
search: searchCarriers,
onFocus: onCarrierSelectFocus
} = usePlRemoteCarrierOptions({
getSelectedId: () => searchForm.carrierId,
listParams: { status: '1' }
})
const vehicleOptions = ref<Vehicle[]>([])
const vehicleTrailerLookup = computed(() => buildVehicleTrailerLookup(vehicleOptions.value))
const planList = ref<UnsettledPlanItem[]>([])
const selectedRows = ref<UnsettledPlanItem[]>([])
function getUnsettledExportFilterParams() {
return {
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
carrierId: searchForm.carrierId || undefined,
planType: searchForm.planType || undefined,
licensePlate: searchForm.licensePlate || undefined
}
}
const unsettledExportColumns = computed(() => {
const cols = [...visibleColumns.value]
if (!cols.includes('planNo')) cols.unshift('planNo')
return cols
})
const planViewVisible = ref(false)
const viewPlanId = ref('')
const quickSearchKeyword = ref('')
const searchForm = reactive({
carrierId: '',
q: '',
planType: '',
licensePlate: ''
})
const pagination = reactive({
page: 1,
pageSize: 20,
total: 0
})
function formatAmount(amount: number) {
if (!amount) return '¥0.00'
return formatMoneyDisplay(amount)
}
async function fetchData() {
loading.value = true
try {
const res = await freightApi.listUnsettledPlans({
carrierId: searchForm.carrierId || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
planType: searchForm.planType || undefined,
licensePlate: searchForm.licensePlate || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
const data = res.data ?? res
planList.value = data.list || []
pagination.total = data.total || 0
} finally {
loading.value = false
}
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleReset() {
searchForm.carrierId = ''
searchForm.q = ''
searchForm.planType = ''
searchForm.licensePlate = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
async function loadCarrierList() {
await searchCarriers('')
}
async function loadVehicleOptions() {
try {
vehicleOptions.value = await fetchAllPagedRecords<Vehicle>(async (page, pageSize) => {
const res = await vehicleApi.list({ page, pageSize })
return pickBxPageResult<Vehicle>(res)
})
} catch {
vehicleOptions.value = []
}
}
function handlePageChange() {
fetchData()
}
function handleSizeChange() {
pagination.page = 1
fetchData()
}
function handleSelectionChange(rows: UnsettledPlanItem[]) {
selectedRows.value = rows
}
function handleViewPlan(row: UnsettledPlanItem) {
viewPlanId.value = row.id
planViewVisible.value = true
}
async function handleCreateSettlement() {
if (!selectedRows.value.length) return
await ElMessageBox.confirm(
`确定将选中的 ${selectedRows.value.length} 条计划生成一张结算单吗?`,
'提示',
{ type: 'warning' }
)
createLoading.value = true
try {
const res = await freightApi.create({
planIds: selectedRows.value.map((r) => r.id)
})
const settlement = (res.data ?? res) as { id: string; settlementNo?: string }
ElMessage.success(`结算单 ${settlement.settlementNo || ''} 创建成功`)
selectedRows.value = []
emit('created', settlement.id)
fetchData()
} finally {
createLoading.value = false
}
}
function refresh() {
fetchData()
}
useDbInstantSearch(
() => ({
carrierId: searchForm.carrierId,
q: searchForm.q,
planType: searchForm.planType,
licensePlate: searchForm.licensePlate
}),
handleSearch
)
onMounted(() => {
loadCarrierList()
loadVehicleOptions()
fetchData()
})
defineExpose({ refresh })
</script>
<style scoped lang="scss">
.bx-list-table-wrap {
min-width: 0;
}
</style>