Files
BMS/bms-front/src/views/ar/bill/components/GenerateBillDialog.vue
T
2026-11-11 17:56:14 +08:00

676 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="generate-bill-dialogs">
<Teleport to="body">
<div v-if="previewExportFloatVisible" class="preview-export-status-float">
<el-tooltip :content="previewExportMessage || '点击查看详情'" placement="bottom">
<el-button class="preview-export-status-button" type="warning" plain size="small" @click="previewExportProgressVisible = true">
<span class="preview-export-progress-ring" :style="{ '--export-progress': previewExportDegree + 'deg' }" aria-hidden="true">
<span class="preview-export-progress-value">{{ previewExportPercent }}%</span>
</span>
<span class="preview-export-status-text">
<strong>{{ previewExportFloatTitle }}</strong>
<small>{{ previewExportFloatSummary }}</small>
</span>
</el-button>
</el-tooltip>
</div>
</Teleport>
<el-dialog v-model="dialogOpen" class="generate-dialog" title="生成应收账单" width="1200px" append-to-body destroy-on-close>
<el-form ref="generateFormRef" :model="generateForm" :rules="generateRules" label-width="110px" class="generate-form">
<el-row :gutter="16">
<el-col :span="6"><el-form-item label="客户" prop="customerId"><customer-select :fetch-options="listArBillGenerateCustomerOptions" v-model="generateForm.customerId" placeholder="请输入客户名称或代码搜索" :remote-search="true" :page-size="20" @change="handleCustomerChange" /></el-form-item></el-col>
<el-col :span="7"><el-form-item label="币种" prop="currency"><el-select v-model="generateForm.currency" placeholder="请选择币种" style="width: 100%;" @change="handleCurrencyChange"><el-option label="人民币 CNY" value="CNY" /><el-option label="美元 USD" value="USD" /></el-select></el-form-item></el-col>
<el-col :span="11"><el-form-item label="账单截止日" prop="cutoffDate"><div class="cutoff-date-controls"><el-select v-model="generateForm.dateType" class="cutoff-date-type" @change="resetPreview"><el-option label="计费时间" value="BIZ_DATE" /><el-option label="收货时间" value="PICKUP_DATE" /></el-select><el-date-picker v-model="generateForm.cutoffDate" class="cutoff-date-picker" type="date" value-format="YYYY-MM-DD" placeholder="请选择账单截止日" @change="resetPreview" /></div></el-form-item></el-col>
<el-col :span="9">
<el-form-item label="单号">
<div class="order-identifier-input">
<el-input v-model="generateForm.orderOrTrackingNo" type="textarea" :rows="4" resize="vertical" placeholder="请输入订单号、运单号或转单号" clearable />
<el-button v-if="generateForm.orderOrTrackingNo" class="order-identifier-clear" link type="primary" @click="clearOrderIdentifiers">清空</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="payee-card" v-loading="bankAccountLoading">
<div class="payee-title">收款方信息(按币种自动匹配)</div>
<template v-if="bankAccount">
<span><b>开户名:</b>{{ bankAccount.accountName }}</span>
<span><b>开户行:</b>{{ bankAccount.bankName }}</span>
<span><b>账号:</b>{{ bankAccount.accountNo }}</span>
</template>
<span v-else class="payee-empty">当前币种尚未配置公司默认收款账户</span>
</div>
<div class="preview-toolbar">
<span v-if="previewSearched" class="preview-summary">可选 {{ previewTotal }} 条订单,已选 <b>{{ displaySelectedCount }}</b> 条,应收合计 <b>{{ money(displaySelectedAmount) }}</b></span>
<span v-else class="preview-summary">请选择客户、币种和账单截止日后查询可生成的计费订单</span>
<el-button v-if="previewSearched && previewTotal > 0" type="warning" plain :icon="Download" :loading="previewExportLoading" :disabled="previewLoading || previewExportLoading" @click="exportPreview">导出查询结果</el-button>
<el-button type="primary" plain :loading="previewLoading" :disabled="!bankAccount" @click="queryGeneratePreview">查询</el-button>
</div>
<div v-if="previewSearched" class="preview-selection-toolbar">
<el-tooltip content="全选全部查询结果" placement="top">
<el-checkbox
:model-value="allSelected"
:disabled="!previewTotal"
aria-label="全选全部查询结果"
@change="handleSelectAllChange"
>
<el-icon><Select /></el-icon>
</el-checkbox>
</el-tooltip>
<el-tooltip content="切换手动选择" placement="top">
<el-checkbox
:model-value="selectionMode === AR_BILL_SELECTION_MODE.EXPLICIT"
:disabled="!previewTotal || selectionMode === AR_BILL_SELECTION_MODE.EXPLICIT"
aria-label="切换手动选择"
@change="handleManualModeChange"
>
<el-icon><Refresh /></el-icon>
</el-checkbox>
</el-tooltip>
<el-badge v-if="allSelected" :value="confirmedExcludeCount" :hidden="!confirmedExcludeKeyword" class="exclude-order-badge">
<el-tooltip content="选择排除订单" placement="top">
<el-button circle text :icon="Search" aria-label="选择排除订单" @click="openExcludeDialog" />
</el-tooltip>
</el-badge>
</div>
<div v-if="allSelected && confirmedExcludeKeyword" class="excluded-order-preview">
<span>已排除 {{ confirmedExcludeCount }} 条订单,应收 {{ money(confirmedExcludeAmount) }}(按单号条件)</span>
<el-button link type="primary" @click="openExcludeDialog">修改</el-button>
<el-button link type="danger" @click="clearConfirmedExclusions">清除</el-button>
</div>
<div v-text-drag-copy="true" class="preview-table-copy-area">
<el-table ref="previewTableRef" v-loading="previewLoading" :data="previewRows" row-key="billId" border stripe max-height="400" empty-text="暂无符合条件的计费订单" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" fixed="left" :selectable="isMainRowSelectable" />
<el-table-column prop="orderNo" label="订单号" min-width="220" show-overflow-tooltip />
<el-table-column prop="trackingNo" label="运单号" min-width="140" show-overflow-tooltip />
<el-table-column prop="transferNo" label="转单号" min-width="240" show-overflow-tooltip />
<el-table-column prop="customerName" label="客户" min-width="130" show-overflow-tooltip />
<el-table-column :prop="dateColumnProp" :label="dateColumnLabel" width="170" :formatter="formatDateColumn" />
<el-table-column prop="productName" label="产品" min-width="140" show-overflow-tooltip />
<el-table-column prop="countryName" label="国家/地区" min-width="110" show-overflow-tooltip />
<el-table-column prop="actualWeight" label="实际重(kg)" width="105" align="right" :formatter="formatWeightColumn" />
<el-table-column prop="chargeWeight" label="计费重(kg)" width="105" align="right" :formatter="formatWeightColumn" />
<el-table-column prop="incomeTotal" label="应收金额" width="110" align="right" :formatter="formatNumberColumn" />
<el-table-column prop="currency" label="币种" width="75" />
</el-table>
</div>
<el-pagination
v-if="previewTotal > 0"
class="preview-pagination"
:current-page="previewPageNum"
:page-size="previewPageSize"
layout="total, prev, pager, next, jumper"
:total="previewTotal"
@current-change="handlePreviewPageChange"
/>
<template #footer>
<span class="generate-footer-summary">已选 {{ displaySelectedCount }} 条订单 · {{ money(displaySelectedAmount) }}</span>
<el-button type="primary" :loading="generateLoading" :disabled="!bankAccount || !previewSearched || !displaySelectedCount" @click="submitGenerate">生成账单</el-button>
<el-button @click="dialogOpen = false">取消</el-button>
</template>
</el-dialog>
<el-dialog v-model="previewExportProgressVisible" :title="previewExportDialogTitle" width="480px" append-to-body :close-on-click-modal="false">
<el-progress :percentage="previewExportPercent" :status="previewExportStatus" :stroke-width="14" striped :striped-flow="previewExportLoading" />
<div class="preview-export-progress__msg">{{ previewExportMessage }}</div>
<div class="preview-export-progress__text">导出行数 {{ previewExportRowCount }}</div>
<p v-if="previewExportLoading" class="preview-export-progress__hint">导出在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页</p>
<template #footer>
<el-button v-if="previewExportLoading" @click="previewExportProgressVisible = false">收起</el-button>
<el-button v-else type="primary" @click="previewExportProgressVisible = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="excludeDialogOpen" title="选择排除订单" width="1100px" append-to-body destroy-on-close @close="handleExcludeDialogClose">
<div class="exclude-toolbar">
<div class="order-identifier-input exclude-search-input">
<el-input v-model="excludeKeyword" type="textarea" :rows="4" resize="vertical" clearable placeholder="请输入订单号、运单号或转单号" @input="invalidateExcludeSearch" />
<el-button v-if="excludeKeyword" class="order-identifier-clear" link type="primary" @click="clearExcludeIdentifiers">清空</el-button>
</div>
<el-button type="primary" :loading="excludeLoading" :disabled="!excludeKeyword.trim()" @click="queryExcludeRows">查询</el-button>
</div>
<div v-if="!excludeSearchSubmitted && !excludeLoading" class="exclude-empty-hint">输入单号并点击查询后显示匹配订单</div>
<div v-if="excludeSearchSubmitted || excludeLoading" v-text-drag-copy="true" class="exclude-table-copy-area">
<el-table v-loading="excludeLoading" :data="excludeRowsPage" row-key="billId" border stripe max-height="420" empty-text="没有匹配的订单">
<el-table-column prop="orderNo" label="订单号" min-width="220" show-overflow-tooltip />
<el-table-column prop="trackingNo" label="运单号" min-width="180" show-overflow-tooltip />
<el-table-column prop="transferNo" label="转单号" min-width="180" show-overflow-tooltip />
<el-table-column prop="customerName" label="客户" min-width="150" show-overflow-tooltip />
<el-table-column :prop="dateColumnProp" :label="dateColumnLabel" width="170" :formatter="formatDateColumn" />
<el-table-column prop="incomeTotal" label="应收金额" width="130" align="right" :formatter="formatNumberColumn" />
<el-table-column prop="currency" label="币种" width="80" />
</el-table>
</div>
<el-pagination
v-if="excludeSearchSubmitted && excludeTotal > 0"
class="exclude-pagination"
:current-page="excludePageNum"
:page-size="excludePageSize"
layout="total, prev, pager, next, jumper"
:total="excludeTotal"
@current-change="handleExcludePageChange"
/>
<template #footer>
<span class="generate-footer-summary">{{ excludeSearchSubmitted ? `命中 ${excludeTotal} 条 · 应收合计 ${money(excludeTotalAmount)},确认后全部排除` : '查询后将把所有命中订单从本次账单中排除' }}</span>
<el-button @click="excludeDialogOpen = false">取消</el-button>
<el-button type="primary" :disabled="!excludeSearchSubmitted || excludeLoading" @click="confirmExcludeSelection">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="billNameOpen" title="填写账单名称" width="520px" append-to-body destroy-on-close>
<el-form ref="billNameFormRef" :model="billNameForm" :rules="billNameRules" label-width="90px">
<el-form-item label="账单名称" prop="billName">
<el-input v-model="billNameForm.billName" maxlength="100" show-word-limit placeholder="请输入账单名称" clearable @keyup.enter="confirmGenerate" />
</el-form-item>
</el-form>
<div class="bill-name-summary">已选择 {{ displaySelectedCount }} 条订单,应收合计 {{ money(displaySelectedAmount) }}</div>
<template #footer>
<el-button @click="billNameOpen = false">取消</el-button>
<el-button type="primary" :loading="generateLoading" @click="confirmGenerate">确认生成</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="GenerateBillDialog">
import { listArBillGenerateCustomerOptions, getDefaultArBankAccount, previewArBill, previewExcludedArBill, generateArBill } from '@/api/ar/bill'
import { Download, Refresh, Search, Select } from '@element-plus/icons-vue'
import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums'
import { exportAsync } from '@/utils/excelExport'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:modelValue', 'success'])
const { proxy } = getCurrentInstance()
const dialogOpen = computed({
get: () => props.modelValue,
set: value => emit('update:modelValue', value)
})
const billNameOpen = ref(false)
const generateLoading = ref(false)
const previewLoading = ref(false)
const previewExportLoading = ref(false)
const previewExportFinished = ref(false)
const previewExportProgressVisible = ref(false)
const previewExportPercent = ref(0)
const previewExportStatus = ref(undefined)
const previewExportMessage = ref('')
const previewExportRowCount = ref(0)
let previewExportHideTimer = null
const previewExportFloatVisible = computed(() => previewExportLoading.value || previewExportFinished.value)
const previewExportDegree = computed(() => Math.round(previewExportPercent.value / 100 * 360))
const previewExportFloatTitle = computed(() => {
if (previewExportLoading.value) return '导出中'
return previewExportStatus.value === 'exception' ? '导出失败' : '导出完成'
})
const previewExportFloatSummary = computed(() => previewExportRowCount.value > 0
? `${previewExportPercent.value}% / ${previewExportRowCount.value} 行`
: `${previewExportPercent.value}%`)
const previewExportDialogTitle = computed(() => {
if (previewExportLoading.value) return '异步导出进行中'
return previewExportStatus.value === 'exception' ? '异步导出失败' : '异步导出完成'
})
onBeforeUnmount(() => {
clearTimeout(previewExportHideTimer)
})
const previewSearched = ref(false)
const previewRows = ref([])
const previewTotal = ref(0)
const previewTotalAmount = ref(0)
const previewPageNum = ref(1)
// 主预览列表固定每页 5 条;Map 负责保存跨页勾选结果。
const previewPageSize = 5
const previewTableRef = ref()
const selectedRows = ref([])
const selectedRowMap = new Map()
// 选择模式只保存编码,提交时由后端枚举再次校验。
const selectionMode = ref(AR_BILL_SELECTION_MODE.EXPLICIT)
const excludeDialogOpen = ref(false)
const excludeLoading = ref(false)
const excludeKeyword = ref('')
const excludeSearchKeyword = ref('')
const excludeSearchSubmitted = ref(false)
const excludeRowsPage = ref([])
const excludeTotal = ref(0)
const excludeTotalAmount = ref(0)
const excludePageNum = ref(1)
// 排除弹窗每页固定 20 条;输入改变后必须重新查询才能确认。
const excludePageSize = 20
const confirmedExcludeKeyword = ref('')
const confirmedExcludeCount = ref(0)
const confirmedExcludeAmount = ref(0)
let excludeRequestSeq = 0
const previewQuery = ref(null)
let syncingPageSelection = false
const bankAccountLoading = ref(false)
const bankAccount = ref(null)
const generateRequestId = ref('')
const generateFormRef = ref()
const billNameFormRef = ref()
const generateForm = reactive({ customerId: '', customerCode: '', customerName: '', cutoffDate: '', dateType: 'BIZ_DATE', currency: 'CNY', orderOrTrackingNo: '' })
const billNameForm = reactive({ billName: '' })
const generateRules = {
customerId: [{ required: true, message: '客户不能为空', trigger: 'change' }],
cutoffDate: [{ required: true, message: '请选择账单截止日', trigger: 'change' }],
currency: [{ required: true, message: '请选择币种', trigger: 'change' }]
}
const billNameRules = { billName: [{ required: true, message: '请输入账单名称', trigger: 'blur' }] }
const selectedAmount = computed(() => selectedRows.value.reduce((sum, row) => sum + (Number(row.incomeTotal) || 0), 0))
const dateColumnProp = computed(() => generateForm.dateType === 'PICKUP_DATE' ? 'pickupDate' : 'bizDate')
const dateColumnLabel = computed(() => generateForm.dateType === 'PICKUP_DATE' ? '收货时间' : '计费时间')
const allSelected = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED)
const displaySelectedCount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
? Math.max(previewTotal.value - confirmedExcludeCount.value, 0) : selectedRows.value.length)
const displaySelectedAmount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
? Math.max(Number(previewTotalAmount.value || 0) - confirmedExcludeAmount.value, 0) : selectedAmount.value)
watch(() => props.modelValue, visible => {
if (visible) {
openGenerate()
}
})
// 每次打开生成弹窗都重新建立一次查询快照,避免复用上一次的选中订单。
function openGenerate() {
Object.assign(generateForm, { customerId: '', customerCode: '', customerName: '', cutoffDate: '', dateType: 'BIZ_DATE', currency: 'CNY', orderOrTrackingNo: '' })
Object.assign(billNameForm, { billName: '' })
billNameOpen.value = false
resetPreview()
bankAccount.value = null
loadDefaultBankAccount()
nextTick(() => generateFormRef.value?.clearValidate())
}
function handleCustomerChange(_, item) {
generateForm.customerCode = item?.customerCode || ''
generateForm.customerName = item?.customerName || ''
resetPreview()
}
function handleCurrencyChange() {
resetPreview()
loadDefaultBankAccount()
}
// 只清空输入内容,不自动刷新预览;用户点击“查询”后再按新条件查询。
function clearOrderIdentifiers() {
generateForm.orderOrTrackingNo = ''
}
// 只清空排除输入内容,不自动刷新排除结果;用户点击“查询”后再按新条件筛选。
function clearExcludeIdentifiers() {
excludeKeyword.value = ''
invalidateExcludeSearch()
}
function loadDefaultBankAccount() {
bankAccountLoading.value = true
bankAccount.value = null
getDefaultArBankAccount(generateForm.currency).then(res => {
bankAccount.value = res.data || null
}).finally(() => {
bankAccountLoading.value = false
})
}
// 条件变化后清理预览和跨页选中状态;用户点击查询时重新加载第一页。
function resetPreview() {
previewRows.value = []
previewTotal.value = 0
previewTotalAmount.value = 0
previewPageNum.value = 1
selectedRows.value = []
selectedRowMap.clear()
selectionMode.value = AR_BILL_SELECTION_MODE.EXPLICIT
clearConfirmedExclusionState()
invalidateExcludeSearch()
excludeDialogOpen.value = false
previewQuery.value = null
previewSearched.value = false
generateRequestId.value = ''
nextTick(() => previewTableRef.value?.clearSelection())
}
function buildGenerateQuery() {
return {
customerId: Number(generateForm.customerId),
customerCode: generateForm.customerCode?.trim() || undefined,
customerName: generateForm.customerName?.trim() || undefined,
billPeriodEnd: generateForm.cutoffDate,
dateType: generateForm.dateType || 'BIZ_DATE',
currency: generateForm.currency || undefined,
orderOrTrackingNo: generateForm.orderOrTrackingNo?.trim() || undefined
}
}
// 任务请求携带完整筛选快照;全选模式将排除单号条件交给后台按批次过滤。
function buildGenerateRequest(requestId) {
return {
...(previewQuery.value || buildGenerateQuery()),
billName: billNameForm.billName.trim(),
selectionMode: selectionMode.value,
sourceBillIds: selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED ? [] : selectedRows.value.map(row => row.billId),
excludedSourceBillIds: [],
excludedOrderOrTrackingNo: selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
? confirmedExcludeKeyword.value || undefined : undefined,
requestId
}
}
function buildPreviewQuery(pageNum = previewPageNum.value, pageSize = previewPageSize) {
return { ...(previewQuery.value || buildGenerateQuery()), pageNum, pageSize }
}
// 查询按钮是预览入口,后端返回当前页、总数和全量金额合计。
function queryGeneratePreview() {
generateFormRef.value.validate(valid => {
if (!valid) return
const query = buildGenerateQuery()
resetPreview()
previewQuery.value = query
fetchPreview(true)
})
}
function fetchPreview(initializeSelectionMode = false) {
previewLoading.value = true
previewArBill(buildPreviewQuery()).then(res => {
syncingPageSelection = true
previewRows.value = res.rows || []
previewTotal.value = Number(res.total) || 0
previewTotalAmount.value = Number(res.totalAmount) || 0
// 仅首次查询默认全选;翻页请求保留用户当前的选择模式。
if (initializeSelectionMode) {
selectionMode.value = previewTotal.value > 0
? AR_BILL_SELECTION_MODE.ALL_MATCHED
: AR_BILL_SELECTION_MODE.EXPLICIT
}
previewSearched.value = true
nextTick(syncPageSelection)
}).finally(() => {
previewLoading.value = false
})
}
function handlePreviewPageChange(page) {
previewPageNum.value = page
fetchPreview()
}
async function exportPreview() {
if (!previewSearched.value || !previewTotal.value || !previewQuery.value || previewExportLoading.value) return
clearTimeout(previewExportHideTimer)
previewExportLoading.value = true
previewExportFinished.value = false
previewExportProgressVisible.value = false
previewExportPercent.value = 0
previewExportStatus.value = undefined
previewExportMessage.value = '正在提交导出任务'
previewExportRowCount.value = 0
try {
const result = await exportAsync({
baseUrl: '/ar/bill/generate/preview/export',
query: { ...previewQuery.value },
filePrefix: 'ar_bill_generate_preview',
showLoading: false,
onProgress: task => {
previewExportPercent.value = Math.max(0, Math.min(100, Number(task.percent) || 0))
previewExportRowCount.value = Number(task.rowCount) || 0
previewExportMessage.value = task.error || task.message || '导出中'
if (task.status === 'FAIL' || task.status === 'FAILED') previewExportStatus.value = 'exception'
}
})
if (result) {
previewExportPercent.value = 100
previewExportStatus.value = 'success'
previewExportMessage.value = '导出完成'
} else {
if (previewExportStatus.value !== 'exception') {
previewExportMessage.value = '导出失败,请查看提示后重试'
}
previewExportStatus.value = 'exception'
}
} catch (error) {
previewExportStatus.value = 'exception'
previewExportMessage.value = error?.message || '导出失败,请稍后重试'
} finally {
previewExportLoading.value = false
previewExportFinished.value = true
previewExportHideTimer = window.setTimeout(() => {
previewExportFinished.value = false
previewExportHideTimer = null
}, 1000)
}
}
// 当前页变化只更新当前页 ID,selectedRowMap 因此可以跨页保留手动选择。
function handleSelectionChange(rows) {
if (syncingPageSelection || selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED) return
const currentPageIds = new Set(previewRows.value.map(row => row.billId))
currentPageIds.forEach(billId => selectedRowMap.delete(billId))
rows.forEach(row => selectedRowMap.set(row.billId, row))
selectedRows.value = Array.from(selectedRowMap.values())
if (!generateLoading.value) generateRequestId.value = ''
}
// 全选后主表复选框全部禁用,用户通过排除弹窗维护“不生成”的订单。
function handleSelectAllChange(checked) {
if (!checked) {
switchToManualMode()
return
}
selectionMode.value = AR_BILL_SELECTION_MODE.ALL_MATCHED
clearConfirmedExclusionState()
selectedRowMap.clear()
selectedRows.value = []
if (!generateLoading.value) generateRequestId.value = ''
nextTick(syncPageSelection)
}
function handleManualModeChange(checked) {
if (checked) switchToManualMode()
}
// 从全选切回手动选择时清除排除集合,恢复主表跨页勾选流程。
function switchToManualMode() {
selectionMode.value = AR_BILL_SELECTION_MODE.EXPLICIT
clearConfirmedExclusionState()
selectedRowMap.clear()
selectedRows.value = []
if (!generateLoading.value) generateRequestId.value = ''
nextTick(syncPageSelection)
}
function isMainRowSelectable() {
return selectionMode.value !== AR_BILL_SELECTION_MODE.ALL_MATCHED
}
// 打开时恢复已确认输入,但不自动请求;用户点击查询后才显示匹配订单。
function openExcludeDialog() {
if (!allSelected.value) return
excludeKeyword.value = confirmedExcludeKeyword.value
excludeSearchKeyword.value = ''
excludeSearchSubmitted.value = false
excludePageNum.value = 1
excludeRowsPage.value = []
excludeTotal.value = 0
excludeTotalAmount.value = 0
excludeLoading.value = false
excludeRequestSeq += 1
excludeDialogOpen.value = true
}
function buildExcludeQuery() {
return {
...(previewQuery.value || {}),
excludeOrderOrTrackingNo: excludeSearchKeyword.value,
pageNum: excludePageNum.value,
pageSize: excludePageSize
}
}
function queryExcludeRows() {
const keyword = excludeKeyword.value?.trim()
if (!keyword) {
proxy.$modal.msgWarning('请输入订单号、运单号或转单号后再查询')
invalidateExcludeSearch()
return
}
excludeSearchKeyword.value = keyword
excludePageNum.value = 1
fetchExcludeRows()
}
function invalidateExcludeSearch() {
excludeRequestSeq += 1
excludeSearchSubmitted.value = false
excludeRowsPage.value = []
excludeTotal.value = 0
excludeTotalAmount.value = 0
excludePageNum.value = 1
excludeLoading.value = false
}
function fetchExcludeRows() {
if (!previewQuery.value || !excludeSearchKeyword.value) return
const requestSeq = ++excludeRequestSeq
excludeLoading.value = true
previewExcludedArBill(buildExcludeQuery()).then(res => {
if (requestSeq !== excludeRequestSeq) return
excludeRowsPage.value = res.rows || []
excludeTotal.value = Number(res.total) || 0
excludeTotalAmount.value = Number(res.totalAmount) || 0
excludeSearchSubmitted.value = true
}).finally(() => {
if (requestSeq === excludeRequestSeq) excludeLoading.value = false
})
}
function handleExcludePageChange(page) {
excludePageNum.value = page
fetchExcludeRows()
}
function confirmExcludeSelection() {
if (!excludeSearchSubmitted.value || excludeLoading.value) return
confirmedExcludeKeyword.value = excludeSearchKeyword.value
confirmedExcludeCount.value = excludeTotal.value
confirmedExcludeAmount.value = excludeTotalAmount.value
if (!generateLoading.value) generateRequestId.value = ''
excludeDialogOpen.value = false
}
function clearConfirmedExclusionState() {
confirmedExcludeKeyword.value = ''
confirmedExcludeCount.value = 0
confirmedExcludeAmount.value = 0
}
function clearConfirmedExclusions() {
clearConfirmedExclusionState()
if (!generateLoading.value) generateRequestId.value = ''
}
function handleExcludeDialogClose() {
invalidateExcludeSearch()
}
// 翻页后把 Map 中的选择同步回 Element Plus 当前页复选框。
function syncPageSelection() {
if (!previewTableRef.value) {
syncingPageSelection = false
return
}
syncingPageSelection = true
previewTableRef.value.clearSelection()
previewRows.value.forEach(row => {
if (selectedRowMap.has(row.billId)) previewTableRef.value.toggleRowSelection(row, true)
})
syncingPageSelection = false
}
function submitGenerate() {
if (!previewSearched.value || !displaySelectedCount.value) {
proxy.$modal.msgError('请至少选择一条计费订单')
return
}
Object.assign(billNameForm, { billName: '' })
billNameOpen.value = true
nextTick(() => billNameFormRef.value?.clearValidate())
}
// 最终只提交异步任务,不在浏览器等待几十万条订单明细生成完成。
function confirmGenerate() {
billNameFormRef.value.validate(valid => {
if (!valid) return
if (!generateRequestId.value) generateRequestId.value = createRequestId()
generateLoading.value = true
generateArBill(buildGenerateRequest(generateRequestId.value)).then(res => {
proxy.$modal.msgSuccess('账单生成任务已提交,后台正在生成')
billNameOpen.value = false
dialogOpen.value = false
emit('success', res.data)
}).finally(() => {
generateLoading.value = false
})
})
}
function date(value) {
if (!value) return '-'
const s = String(value).replace('T', ' ')
return s.length >= 19 ? s.slice(0, 19) : (s.length >= 10 ? s.slice(0, 10) : s)
}
function money(value) { const num = Number(value); return Number.isNaN(num) ? '-' : num.toFixed(2) }
function formatDateColumn(_, __, value) { return date(value) }
function formatWeightColumn(_, __, value) { return value == null || value === '' ? '-' : value }
function formatNumberColumn(_, __, value) { return Number(value || 0).toFixed(2) }
function createRequestId() { return globalThis.crypto?.randomUUID?.() || `ar-${Date.now()}-${Math.random().toString(16).slice(2)}` }
</script>
<style scoped>
.generate-dialog :deep(.el-dialog__body) { padding: 18px 24px 10px; }
.generate-form { padding: 4px 6px 0; border: 1px solid var(--el-border-color-lighter); border-radius: 8px; background: var(--el-fill-color-blank); }
.generate-form :deep(.el-form-item) { margin-bottom: 16px; }
.cutoff-date-controls { display: flex; align-items: center; width: 100%; gap: 8px; }
.cutoff-date-type { flex: 0 0 120px; }
.cutoff-date-picker { flex: 1 1 auto; min-width: 180px; }
.order-identifier-input { position: relative; width: 100%; }
.order-identifier-input :deep(.el-textarea__inner) { padding-right: 48px; }
.order-identifier-clear { position: absolute; top: 4px; right: 6px; z-index: 1; padding: 2px 4px; }
.payee-card { display: flex; align-items: center; flex-wrap: wrap; gap: 16px; min-height: 42px; margin: 4px 0 12px; padding: 9px 12px; border: 1px solid var(--el-border-color-lighter); border-radius: 4px; background: var(--el-fill-color-light); color: var(--el-text-color-regular); font-size: 13px; }
.payee-title { color: var(--el-text-color-primary); font-weight: 600; }
.payee-empty { color: var(--el-color-danger); }
.preview-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 18px 0 10px; padding: 10px 14px; border-radius: 6px; background: var(--el-fill-color-light); color: var(--el-text-color-regular); }
.preview-summary { flex: 1; font-size: 14px; }
.preview-summary b { color: var(--el-color-primary); }
.preview-export-status-float { position: fixed; top: 92px; right: 24px; z-index: 3001; }
.preview-export-status-button { display: inline-flex; align-items: center; gap: 10px; min-width: 200px; min-height: 68px; padding: 6px 16px 6px 7px; border-radius: 36px; }
.preview-export-progress-ring { position: relative; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 54px; width: 54px; height: 54px; border-radius: 50%; background: conic-gradient(var(--el-color-warning) 0deg var(--export-progress, 0deg), var(--el-color-warning-light-8) var(--export-progress, 0deg) 360deg); }
.preview-export-progress-ring::after { position: absolute; inset: 5px; content: ''; border-radius: 50%; background: var(--el-bg-color); }
.preview-export-progress-value { position: relative; z-index: 1; color: var(--el-color-warning); font-size: 12px; font-weight: 600; line-height: 1; }
.preview-export-status-text { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; line-height: 1.2; white-space: nowrap; }
.preview-export-status-text strong { font-size: 14px; font-weight: 600; }
.preview-export-status-text small { color: var(--el-text-color-secondary); font-size: 11px; }
.preview-export-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); }
.preview-export-progress__text { margin-top: 8px; color: var(--el-text-color-secondary); font-size: 13px; }
.preview-export-progress__hint { margin-top: 12px; color: var(--el-color-warning); font-size: 12px; }
.preview-selection-toolbar { display: flex; align-items: center; gap: 6px; min-height: 32px; padding-left: 8px; }
.preview-selection-toolbar :deep(.el-checkbox) { margin-right: 6px; }
.preview-selection-toolbar :deep(.el-checkbox__label) { display: inline-flex; align-items: center; padding-left: 4px; font-size: 16px; }
.exclude-order-badge { margin-left: 4px; }
.excluded-order-preview { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-height: 34px; margin: 0 0 8px; padding: 4px 8px; border-radius: 4px; background: var(--el-fill-color-light); color: var(--el-text-color-secondary); font-size: 13px; }
.exclude-empty-hint { display: flex; align-items: center; justify-content: center; min-height: 180px; margin-top: 12px; border: 1px solid var(--el-border-color-lighter); color: var(--el-text-color-secondary); }
.exclude-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.exclude-search-input { width: 540px; }
.exclude-pagination { justify-content: flex-end; margin: 12px 0 4px; }
.preview-pagination { justify-content: flex-end; margin: 12px 0 4px; }
.bill-name-summary { margin: -4px 0 8px 90px; color: var(--el-text-color-secondary); font-size: 13px; }
.generate-footer-summary { margin-right: auto; color: var(--el-text-color-secondary); font-size: 13px; }
</style>