676 lines
34 KiB
Vue
676 lines
34 KiB
Vue
<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>
|