Support filtered and selected export across freight settlement board panels. Co-authored-by: Cursor <cursoragent@cursor.com>
477 lines
15 KiB
Vue
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>
|