✨ 支持按单号批量排除账单订单

This commit is contained in:
caojia
2026-10-20 15:11:27 +08:00
parent 29e804dae2
commit b772c95813
9 changed files with 159 additions and 107 deletions
@@ -134,6 +134,23 @@ public class ArBillController extends BaseController {
return result; return result;
} }
/** 查询需要从全选结果中排除的订单;空条件不执行查询,避免返回全部订单。 */
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
@PostMapping("/generate/exclusion-preview")
public AjaxResult previewExcluded(@RequestBody ArBillGenerateQuery query) {
if (query == null || StringUtils.isEmpty(StringUtils.trim(query.getExcludeOrderOrTrackingNo()))) {
throw new ServiceException("请输入订单号、运单号或转单号后再查询");
}
PageHelper.startPage(query.getPageNum() == null ? 1 : query.getPageNum(), 20).setReasonable(true);
List<PrcOrderBill> list = billService.previewExcludedBill(query);
TableDataInfo pageData = getDataTable(list);
AjaxResult result = success();
result.put("rows", pageData.getRows());
result.put("total", pageData.getTotal());
result.put("totalAmount", billService.sumPreviewBill(query));
return result;
}
/** /**
* 创建账单头和异步生成任务,接口不等待全部订单明细处理完成。 * 创建账单头和异步生成任务,接口不等待全部订单明细处理完成。
*/ */
@@ -32,6 +32,8 @@ public class ArBillGenerateRequest
private String selectionMode = ArBillSelectionMode.EXPLICIT.getCode(); private String selectionMode = ArBillSelectionMode.EXPLICIT.getCode();
/** 全选模式中用户明确排除的来源订单 ID。 */ /** 全选模式中用户明确排除的来源订单 ID。 */
private List<String> excludedSourceBillIds; private List<String> excludedSourceBillIds;
/** 全选模式下按订单号、运单号或转单号排除;后台生成任务按批次应用该条件。 */
private String excludedOrderOrTrackingNo;
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
@JsonDeserialize(using = StrictDateDeserializer.class) @JsonDeserialize(using = StrictDateDeserializer.class)
private Date billDate; private Date billDate;
@@ -8,7 +8,7 @@ public enum ArBillSelectionMode
{ {
/** 手动选择当前预览结果中的订单。 */ /** 手动选择当前预览结果中的订单。 */
EXPLICIT("EXPLICIT", "手动选择"), EXPLICIT("EXPLICIT", "手动选择"),
/** 选择全部查询结果,仅排除用户明确勾选的订单。 */ /** 选择全部查询结果,再按用户提供的排除条件移除匹配订单。 */
ALL_MATCHED("ALL_MATCHED", "全选查询结果"); ALL_MATCHED("ALL_MATCHED", "全选查询结果");
private final String code; private final String code;
@@ -37,6 +37,8 @@ public interface IArBillService
List<PrcOrderBill> previewBill(ArBillGenerateQuery query); List<PrcOrderBill> previewBill(ArBillGenerateQuery query);
List<PrcOrderBill> previewExcludedBill(ArBillGenerateQuery query);
BigDecimal sumPreviewBill(ArBillGenerateQuery query); BigDecimal sumPreviewBill(ArBillGenerateQuery query);
ArBillGenerateTask generateBill(ArBillGenerateRequest request, String operator, String requestId); ArBillGenerateTask generateBill(ArBillGenerateRequest request, String operator, String requestId);
@@ -299,6 +299,15 @@ public class ArBillServiceImpl implements IArBillService {
return result; return result;
} }
/** 查询待排除的订单;必须带至少一个精确单号,避免空条件返回整个账单范围。 */
@Override
public List<PrcOrderBill> previewExcludedBill(ArBillGenerateQuery query) {
if (query == null || normalizeOrderIdentifiers(query.getExcludeOrderOrTrackingNo()).isEmpty()) {
throw new ServiceException("请输入订单号、运单号或转单号后再查询");
}
return previewBill(query);
}
/** /**
* 汇总当前生成条件下的订单应收金额,用于弹窗顶部合计展示。 * 汇总当前生成条件下的订单应收金额,用于弹窗顶部合计展示。
*/ */
@@ -358,8 +367,18 @@ public class ArBillServiceImpl implements IArBillService {
throw new ServiceException("全选模式最多排除 " + EXCLUDED_SOURCE_ID_LIMIT throw new ServiceException("全选模式最多排除 " + EXCLUDED_SOURCE_ID_LIMIT
+ " 条订单,请缩小查询条件后再生成"); + " 条订单,请缩小查询条件后再生成");
} }
List<String> excludedOrderIdentifiers = allMatched
? normalizeOrderIdentifiers(request.getExcludedOrderOrTrackingNo()) : new ArrayList<>();
if (!allMatched) {
request.setExcludedOrderOrTrackingNo(null);
}
int orderIdentifierCount = normalizeOrderIdentifiers(request.getOrderOrTrackingNo()).size()
+ excludedOrderIdentifiers.size();
if (orderIdentifierCount > ORDER_IDENTIFIER_LIMIT) {
throw new ServiceException("筛选单号和排除单号合计最多输入 " + ORDER_IDENTIFIER_LIMIT + " 个");
}
validateOrderIdentifierParameterCount( validateOrderIdentifierParameterCount(
normalizeOrderIdentifiers(request.getOrderOrTrackingNo()).size(), orderIdentifierCount,
allMatched ? excludedSourceBillIds.size() : sourceBillIds.size()); allMatched ? excludedSourceBillIds.size() : sourceBillIds.size());
request.setSelectionMode(selectionMode); request.setSelectionMode(selectionMode);
@@ -535,6 +554,8 @@ public class ArBillServiceImpl implements IArBillService {
ArBillGenerateRequest request = JSON.parseObject(task.getRequestPayload(), ArBillGenerateRequest.class); ArBillGenerateRequest request = JSON.parseObject(task.getRequestPayload(), ArBillGenerateRequest.class);
List<String> sourceBillIds = normalizeSourceBillIds(request.getSourceBillIds()); List<String> sourceBillIds = normalizeSourceBillIds(request.getSourceBillIds());
List<String> excludedSourceBillIds = normalizeSourceBillIds(request.getExcludedSourceBillIds()); List<String> excludedSourceBillIds = normalizeSourceBillIds(request.getExcludedSourceBillIds());
List<String> excludedOrderIdentifiers = ArBillSelectionMode.ALL_MATCHED.getCode().equals(task.getSelectionMode())
? normalizeOrderIdentifiers(request.getExcludedOrderOrTrackingNo()) : new ArrayList<>();
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager); TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setTimeout(300); transactionTemplate.setTimeout(300);
@@ -544,8 +565,8 @@ public class ArBillServiceImpl implements IArBillService {
request.getCustomerId(), request.getCustomerCode(), request.getCustomerId(), request.getCustomerCode(),
normalizeOrderIdentifiers(request.getOrderOrTrackingNo()), normalizeOrderIdentifiers(request.getOrderOrTrackingNo()),
request.getBillPeriodEnd(), request.getCurrency(), eligibleOrderBillStatuses(), request.getBillPeriodEnd(), request.getCurrency(), eligibleOrderBillStatuses(),
ArIncomeBillFeeStatus.UNBILLED.getCode(), ArIncomeBillFeeStatus.UNBILLED.getCode(), task.getSnapshotCreateTime(), null,
task.getSnapshotCreateTime(), null, excludedSourceBillIds); excludedSourceBillIds, excludedOrderIdentifiers);
if (totalCount <= 0) { if (totalCount <= 0) {
throw new ServiceException("当前全选范围内没有可生成的订单"); throw new ServiceException("当前全选范围内没有可生成的订单");
} }
@@ -572,7 +593,7 @@ public class ArBillServiceImpl implements IArBillService {
} }
ArBillGenerateTask currentTask = task; ArBillGenerateTask currentTask = task;
GenerateBatchResult result = transactionTemplate.execute(status -> processGenerateBatch( GenerateBatchResult result = transactionTemplate.execute(status -> processGenerateBatch(
currentTask, request, sourceBillIds, excludedSourceBillIds)); currentTask, request, sourceBillIds, excludedSourceBillIds, excludedOrderIdentifiers));
if (result == null || result.finished) { if (result == null || result.finished) {
transactionTemplate.execute(status -> { transactionTemplate.execute(status -> {
completeGenerateTask(taskId); completeGenerateTask(taskId);
@@ -659,7 +680,8 @@ public class ArBillServiceImpl implements IArBillService {
* 每批最多 GENERATE_PAGE_SIZE 条,成功后原子更新任务进度。 * 每批最多 GENERATE_PAGE_SIZE 条,成功后原子更新任务进度。
*/ */
private GenerateBatchResult processGenerateBatch(ArBillGenerateTask task, ArBillGenerateRequest request, private GenerateBatchResult processGenerateBatch(ArBillGenerateTask task, ArBillGenerateRequest request,
List<String> sourceBillIds, List<String> excludedSourceBillIds) { List<String> sourceBillIds, List<String> excludedSourceBillIds,
List<String> excludedOrderIdentifiers) {
ArBillSelectionMode selectionMode = ArBillSelectionMode.of(task.getSelectionMode()); ArBillSelectionMode selectionMode = ArBillSelectionMode.of(task.getSelectionMode());
if (selectionMode == null) { if (selectionMode == null) {
throw new ServiceException("账单生成任务的订单选择模式无效"); throw new ServiceException("账单生成任务的订单选择模式无效");
@@ -673,6 +695,7 @@ public class ArBillServiceImpl implements IArBillService {
eligibleOrderBillStatuses(), ArIncomeBillFeeStatus.UNBILLED.getCode(), eligibleOrderBillStatuses(), ArIncomeBillFeeStatus.UNBILLED.getCode(),
task.getSnapshotCreateTime(), task.getLastSourceBillId(), task.getSnapshotCreateTime(), task.getLastSourceBillId(),
explicitIds, selectionMode == ArBillSelectionMode.ALL_MATCHED ? excludedSourceBillIds : null, explicitIds, selectionMode == ArBillSelectionMode.ALL_MATCHED ? excludedSourceBillIds : null,
selectionMode == ArBillSelectionMode.ALL_MATCHED ? excludedOrderIdentifiers : null,
GENERATE_PAGE_SIZE); GENERATE_PAGE_SIZE);
if (sources == null || sources.isEmpty()) { if (sources == null || sources.isEmpty()) {
return new GenerateBatchResult(true); return new GenerateBatchResult(true);
@@ -68,14 +68,16 @@ public interface PrcOrderBillMapper
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus, @Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus,
@Param("snapshotCreateTime") Date snapshotCreateTime, @Param("snapshotCreateTime") Date snapshotCreateTime,
@Param("lastSourceBillId") String lastSourceBillId, @Param("billIds") List<String> billIds, @Param("lastSourceBillId") String lastSourceBillId, @Param("billIds") List<String> billIds,
@Param("excludedBillIds") List<String> excludedBillIds, @Param("limit") int limit); @Param("excludedBillIds") List<String> excludedBillIds,
@Param("excludedOrderIdentifiers") List<String> excludedOrderIdentifiers, @Param("limit") int limit);
int countPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId, int countPrcOrderBillByCustomerPeriod(@Param("customerId") Long customerId,
@Param("customerCode") String customerCode, @Param("orderIdentifiers") List<String> orderIdentifiers, @Param("customerCode") String customerCode, @Param("orderIdentifiers") List<String> orderIdentifiers,
@Param("cutoffDate") Date cutoffDate, @Param("currency") String currency, @Param("cutoffDate") Date cutoffDate, @Param("currency") String currency,
@Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus, @Param("arBillStatuses") List<String> arBillStatuses, @Param("feeArBillStatus") String feeArBillStatus,
@Param("snapshotCreateTime") Date snapshotCreateTime, @Param("snapshotCreateTime") Date snapshotCreateTime,
@Param("billIds") List<String> billIds, @Param("excludedBillIds") List<String> excludedBillIds); @Param("billIds") List<String> billIds, @Param("excludedBillIds") List<String> excludedBillIds,
@Param("excludedOrderIdentifiers") List<String> excludedOrderIdentifiers);
/** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */ /** 按生成账单时用户勾选的订单读取,并再次校验账单筛选条件。 */
public List<PrcOrderBill> selectPrcOrderBillByCustomerCutoffAndIds(@Param("customerId") Long customerId, public List<PrcOrderBill> selectPrcOrderBillByCustomerCutoffAndIds(@Param("customerId") Long customerId,
@@ -693,6 +693,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{excludedBillId} #{excludedBillId}
</foreach> </foreach>
</if> </if>
<if test="excludedOrderIdentifiers != null and excludedOrderIdentifiers.size() > 0">
and not (
coalesce(p.order_no, '') in
<foreach collection="excludedOrderIdentifiers" item="excludedIdentifier" open="(" separator="," close=")">
#{excludedIdentifier}
</foreach>
or coalesce(p.tracking_no, '') in
<foreach collection="excludedOrderIdentifiers" item="excludedIdentifier" open="(" separator="," close=")">
#{excludedIdentifier}
</foreach>
or coalesce(p.transfer_no, '') in
<foreach collection="excludedOrderIdentifiers" item="excludedIdentifier" open="(" separator="," close=")">
#{excludedIdentifier}
</foreach>
)
</if>
</sql> </sql>
<!-- 异步生成使用 bill_id 游标批量读取,避免深分页和一次加载全量订单。 --> <!-- 异步生成使用 bill_id 游标批量读取,避免深分页和一次加载全量订单。 -->
+9
View File
@@ -50,6 +50,15 @@ export function previewArBill(query) {
}) })
} }
export function previewExcludedArBill(query) {
return request({
url: '/ar/bill/generate/exclusion-preview',
method: 'post',
data: query,
timeout: 60000
})
}
export function confirmArBill(arBillId, data = {}) { export function confirmArBill(arBillId, data = {}) {
return request({ url: '/ar/bill/' + arBillId + '/confirm', method: 'put', data }) return request({ url: '/ar/bill/' + arBillId + '/confirm', method: 'put', data })
} }
@@ -51,17 +51,16 @@
<el-icon><Refresh /></el-icon> <el-icon><Refresh /></el-icon>
</el-checkbox> </el-checkbox>
</el-tooltip> </el-tooltip>
<el-badge v-if="allSelected" :value="excludedRows.length" :hidden="!excludedRows.length" class="exclude-order-badge"> <el-badge v-if="allSelected" :value="confirmedExcludeCount" :hidden="!confirmedExcludeKeyword" class="exclude-order-badge">
<el-tooltip content="选择排除订单" placement="top"> <el-tooltip content="选择排除订单" placement="top">
<el-button circle text :icon="Search" aria-label="选择排除订单" @click="openExcludeDialog" /> <el-button circle text :icon="Search" aria-label="选择排除订单" @click="openExcludeDialog" />
</el-tooltip> </el-tooltip>
</el-badge> </el-badge>
</div> </div>
<div v-if="allSelected && excludedRows.length" class="excluded-order-preview"> <div v-if="allSelected && confirmedExcludeKeyword" class="excluded-order-preview">
<span class="excluded-order-title">排除订单:</span> <span>已排除 {{ confirmedExcludeCount }} 条订单,应收 {{ money(confirmedExcludeAmount) }}(按单号条件)</span>
<el-tag v-for="row in excludedRows" :key="row.billId" closable size="small" @close="removeExcludedRow(row.billId)"> <el-button link type="primary" @click="openExcludeDialog">修改</el-button>
{{ row.orderNo || row.trackingNo || row.billId }} <el-button link type="danger" @click="clearConfirmedExclusions">清除</el-button>
</el-tag>
</div> </div>
<el-table ref="previewTableRef" v-loading="previewLoading" :data="previewRows" row-key="billId" border stripe max-height="400" empty-text="暂无符合条件的计费订单" @selection-change="handleSelectionChange"> <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 type="selection" width="50" fixed="left" :selectable="isMainRowSelectable" />
@@ -96,13 +95,13 @@
<el-dialog v-model="excludeDialogOpen" title="选择排除订单" width="1100px" append-to-body destroy-on-close @close="handleExcludeDialogClose"> <el-dialog v-model="excludeDialogOpen" title="选择排除订单" width="1100px" append-to-body destroy-on-close @close="handleExcludeDialogClose">
<div class="exclude-toolbar"> <div class="exclude-toolbar">
<div class="order-identifier-input exclude-search-input"> <div class="order-identifier-input exclude-search-input">
<el-input v-model="excludeKeyword" type="textarea" :rows="4" resize="vertical" clearable placeholder="请输入订单号、运单号或转单号" /> <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> <el-button v-if="excludeKeyword" class="order-identifier-clear" link type="primary" @click="clearExcludeIdentifiers">清空</el-button>
</div> </div>
<el-button type="primary" :loading="excludeLoading" @click="queryExcludeRows">查询</el-button> <el-button type="primary" :loading="excludeLoading" :disabled="!excludeKeyword.trim()" @click="queryExcludeRows">查询</el-button>
</div> </div>
<el-table ref="excludeTableRef" v-loading="excludeLoading" :data="excludeRowsPage" row-key="billId" border stripe max-height="420" empty-text="暂无符合条件的订单" @selection-change="handleExcludeSelectionChange"> <div v-if="!excludeSearchSubmitted && !excludeLoading" class="exclude-empty-hint">输入单号并点击查询后显示匹配订单</div>
<el-table-column type="selection" width="50" fixed="left" /> <el-table v-if="excludeSearchSubmitted || excludeLoading" 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="orderNo" label="订单号" min-width="220" show-overflow-tooltip />
<el-table-column prop="trackingNo" label="运单号" min-width="180" 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="transferNo" label="转单号" min-width="180" show-overflow-tooltip />
@@ -112,7 +111,7 @@
<el-table-column prop="currency" label="币种" width="80" /> <el-table-column prop="currency" label="币种" width="80" />
</el-table> </el-table>
<el-pagination <el-pagination
v-if="excludeTotal > 0" v-if="excludeSearchSubmitted && excludeTotal > 0"
class="exclude-pagination" class="exclude-pagination"
:current-page="excludePageNum" :current-page="excludePageNum"
:page-size="excludePageSize" :page-size="excludePageSize"
@@ -121,9 +120,9 @@
@current-change="handleExcludePageChange" @current-change="handleExcludePageChange"
/> />
<template #footer> <template #footer>
<span class="generate-footer-summary">已选择 {{ excludeDraftRows.length }} 条排除订单</span> <span class="generate-footer-summary">{{ excludeSearchSubmitted ? `命中 ${excludeTotal} 条 · 应收合计 ${money(excludeTotalAmount)},确认后全部排除` : '查询后将把所有命中订单从本次账单中排除' }}</span>
<el-button @click="cancelExcludeSelection">取消</el-button> <el-button @click="excludeDialogOpen = false">取消</el-button>
<el-button type="primary" @click="confirmExcludeSelection">确定</el-button> <el-button type="primary" :disabled="!excludeSearchSubmitted || excludeLoading" @click="confirmExcludeSelection">确定</el-button>
</template> </template>
</el-dialog> </el-dialog>
@@ -143,7 +142,7 @@
</template> </template>
<script setup name="GenerateBillDialog"> <script setup name="GenerateBillDialog">
import { getDefaultArBankAccount, previewArBill, generateArBill } from '@/api/ar/bill' import { getDefaultArBankAccount, previewArBill, previewExcludedArBill, generateArBill } from '@/api/ar/bill'
import { Refresh, Search, Select } from '@element-plus/icons-vue' import { Refresh, Search, Select } from '@element-plus/icons-vue'
import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums' import { AR_BILL_SELECTION_MODE } from '@/constants/ar/enums'
@@ -175,21 +174,21 @@ const selectedRows = ref([])
const selectedRowMap = new Map() const selectedRowMap = new Map()
// 选择模式只保存编码,提交时由后端枚举再次校验。 // 选择模式只保存编码,提交时由后端枚举再次校验。
const selectionMode = ref(AR_BILL_SELECTION_MODE.EXPLICIT) const selectionMode = ref(AR_BILL_SELECTION_MODE.EXPLICIT)
const excludedRows = ref([])
const excludedRowMap = new Map()
const excludeDialogOpen = ref(false) const excludeDialogOpen = ref(false)
const excludeLoading = ref(false) const excludeLoading = ref(false)
const excludeKeyword = ref('') const excludeKeyword = ref('')
const excludeSearchKeyword = ref('')
const excludeSearchSubmitted = ref(false)
const excludeRowsPage = ref([]) const excludeRowsPage = ref([])
const excludeTotal = ref(0) const excludeTotal = ref(0)
const excludeTotalAmount = ref(0)
const excludePageNum = ref(1) const excludePageNum = ref(1)
// 全选后的排除弹窗也按 5 条分页,查询条件固定继承主弹窗条件。 // 排除弹窗每页固定 20 条;输入改变后必须重新查询才能确认。
const excludePageSize = 5 const excludePageSize = 20
const excludeTableRef = ref() const confirmedExcludeKeyword = ref('')
const excludeDraftRows = ref([]) const confirmedExcludeCount = ref(0)
let excludeDraftMap = new Map() const confirmedExcludeAmount = ref(0)
let excludeDialogConfirmed = false let excludeRequestSeq = 0
let syncingExcludeSelection = false
const previewQuery = ref(null) const previewQuery = ref(null)
let syncingPageSelection = false let syncingPageSelection = false
const bankAccountLoading = ref(false) const bankAccountLoading = ref(false)
@@ -206,12 +205,11 @@ const generateRules = {
} }
const billNameRules = { billName: [{ required: true, message: '请输入账单名称', trigger: 'blur' }] } const billNameRules = { billName: [{ required: true, message: '请输入账单名称', trigger: 'blur' }] }
const selectedAmount = computed(() => selectedRows.value.reduce((sum, row) => sum + (Number(row.incomeTotal) || 0), 0)) const selectedAmount = computed(() => selectedRows.value.reduce((sum, row) => sum + (Number(row.incomeTotal) || 0), 0))
const excludedAmount = computed(() => excludedRows.value.reduce((sum, row) => sum + (Number(row.incomeTotal) || 0), 0))
const allSelected = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED) const allSelected = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED)
const displaySelectedCount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED const displaySelectedCount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
? Math.max(previewTotal.value - excludedRows.value.length, 0) : selectedRows.value.length) ? Math.max(previewTotal.value - confirmedExcludeCount.value, 0) : selectedRows.value.length)
const displaySelectedAmount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED const displaySelectedAmount = computed(() => selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
? Math.max(Number(previewTotalAmount.value || 0) - excludedAmount.value, 0) : selectedAmount.value) ? Math.max(Number(previewTotalAmount.value || 0) - confirmedExcludeAmount.value, 0) : selectedAmount.value)
watch(() => props.modelValue, visible => { watch(() => props.modelValue, visible => {
if (visible) { if (visible) {
@@ -249,6 +247,7 @@ function clearOrderIdentifiers() {
// 只清空排除输入内容,不自动刷新排除结果;用户点击“查询”后再按新条件筛选。 // 只清空排除输入内容,不自动刷新排除结果;用户点击“查询”后再按新条件筛选。
function clearExcludeIdentifiers() { function clearExcludeIdentifiers() {
excludeKeyword.value = '' excludeKeyword.value = ''
invalidateExcludeSearch()
} }
function loadDefaultBankAccount() { function loadDefaultBankAccount() {
@@ -270,10 +269,8 @@ function resetPreview() {
selectedRows.value = [] selectedRows.value = []
selectedRowMap.clear() selectedRowMap.clear()
selectionMode.value = AR_BILL_SELECTION_MODE.EXPLICIT selectionMode.value = AR_BILL_SELECTION_MODE.EXPLICIT
excludedRows.value = [] clearConfirmedExclusionState()
excludedRowMap.clear() invalidateExcludeSearch()
excludeDraftRows.value = []
excludeDraftMap = new Map()
excludeDialogOpen.value = false excludeDialogOpen.value = false
previewQuery.value = null previewQuery.value = null
previewSearched.value = false previewSearched.value = false
@@ -292,14 +289,16 @@ function buildGenerateQuery() {
} }
} }
// 任务请求携带完整筛选快照;全选模式只提交排除 ID,后台按条件游标扫描其余订单。 // 任务请求携带完整筛选快照;全选模式将排除单号条件交给后台按批次过滤。
function buildGenerateRequest(requestId) { function buildGenerateRequest(requestId) {
return { return {
...(previewQuery.value || buildGenerateQuery()), ...(previewQuery.value || buildGenerateQuery()),
billName: billNameForm.billName.trim(), billName: billNameForm.billName.trim(),
selectionMode: selectionMode.value, selectionMode: selectionMode.value,
sourceBillIds: selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED ? [] : selectedRows.value.map(row => row.billId), sourceBillIds: selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED ? [] : selectedRows.value.map(row => row.billId),
excludedSourceBillIds: selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED ? Array.from(excludedRowMap.keys()) : [], excludedSourceBillIds: [],
excludedOrderOrTrackingNo: selectionMode.value === AR_BILL_SELECTION_MODE.ALL_MATCHED
? confirmedExcludeKeyword.value || undefined : undefined,
requestId requestId
} }
} }
@@ -359,8 +358,7 @@ function handleSelectAllChange(checked) {
return return
} }
selectionMode.value = AR_BILL_SELECTION_MODE.ALL_MATCHED selectionMode.value = AR_BILL_SELECTION_MODE.ALL_MATCHED
excludedRows.value = [] clearConfirmedExclusionState()
excludedRowMap.clear()
selectedRowMap.clear() selectedRowMap.clear()
selectedRows.value = [] selectedRows.value = []
if (!generateLoading.value) generateRequestId.value = '' if (!generateLoading.value) generateRequestId.value = ''
@@ -374,8 +372,7 @@ function handleManualModeChange(checked) {
// 从全选切回手动选择时清除排除集合,恢复主表跨页勾选流程。 // 从全选切回手动选择时清除排除集合,恢复主表跨页勾选流程。
function switchToManualMode() { function switchToManualMode() {
selectionMode.value = AR_BILL_SELECTION_MODE.EXPLICIT selectionMode.value = AR_BILL_SELECTION_MODE.EXPLICIT
excludedRows.value = [] clearConfirmedExclusionState()
excludedRowMap.clear()
selectedRowMap.clear() selectedRowMap.clear()
selectedRows.value = [] selectedRows.value = []
if (!generateLoading.value) generateRequestId.value = '' if (!generateLoading.value) generateRequestId.value = ''
@@ -386,52 +383,64 @@ function isMainRowSelectable() {
return selectionMode.value !== AR_BILL_SELECTION_MODE.ALL_MATCHED return selectionMode.value !== AR_BILL_SELECTION_MODE.ALL_MATCHED
} }
function syncExcludedRows() { // 打开时恢复已确认输入,但不自动请求;用户点击查询后才显示匹配订单。
excludedRows.value = Array.from(excludedRowMap.values())
}
function syncExcludeDraftRows() {
excludeDraftRows.value = Array.from(excludeDraftMap.values())
}
// 排除弹窗继承客户、币种、截止日和主关键字,只额外允许订单号/运单号 OR 查询。
function openExcludeDialog() { function openExcludeDialog() {
if (!allSelected.value) return if (!allSelected.value) return
excludeDialogConfirmed = false excludeKeyword.value = confirmedExcludeKeyword.value
excludeDraftMap = new Map(excludedRowMap) excludeSearchKeyword.value = ''
syncExcludeDraftRows() excludeSearchSubmitted.value = false
excludeKeyword.value = ''
excludePageNum.value = 1 excludePageNum.value = 1
excludeRowsPage.value = [] excludeRowsPage.value = []
excludeTotal.value = 0 excludeTotal.value = 0
excludeTotalAmount.value = 0
excludeLoading.value = false
excludeRequestSeq += 1
excludeDialogOpen.value = true excludeDialogOpen.value = true
fetchExcludeRows()
} }
function buildExcludeQuery() { function buildExcludeQuery() {
return { return {
...(previewQuery.value || {}), ...(previewQuery.value || {}),
excludeOrderOrTrackingNo: excludeKeyword.value?.trim() || undefined, excludeOrderOrTrackingNo: excludeSearchKeyword.value,
pageNum: excludePageNum.value, pageNum: excludePageNum.value,
pageSize: excludePageSize pageSize: excludePageSize
} }
} }
function queryExcludeRows() { function queryExcludeRows() {
const keyword = excludeKeyword.value?.trim()
if (!keyword) {
proxy.$modal.msgWarning('请输入订单号、运单号或转单号后再查询')
invalidateExcludeSearch()
return
}
excludeSearchKeyword.value = keyword
excludePageNum.value = 1 excludePageNum.value = 1
fetchExcludeRows() fetchExcludeRows()
} }
function invalidateExcludeSearch() {
excludeRequestSeq += 1
excludeSearchSubmitted.value = false
excludeRowsPage.value = []
excludeTotal.value = 0
excludeTotalAmount.value = 0
excludePageNum.value = 1
excludeLoading.value = false
}
function fetchExcludeRows() { function fetchExcludeRows() {
if (!previewQuery.value) return if (!previewQuery.value || !excludeSearchKeyword.value) return
const requestSeq = ++excludeRequestSeq
excludeLoading.value = true excludeLoading.value = true
previewArBill(buildExcludeQuery()).then(res => { previewExcludedArBill(buildExcludeQuery()).then(res => {
syncingExcludeSelection = true if (requestSeq !== excludeRequestSeq) return
excludeRowsPage.value = res.rows || [] excludeRowsPage.value = res.rows || []
excludeTotal.value = Number(res.total) || 0 excludeTotal.value = Number(res.total) || 0
nextTick(syncExcludePageSelection) excludeTotalAmount.value = Number(res.totalAmount) || 0
excludeSearchSubmitted.value = true
}).finally(() => { }).finally(() => {
excludeLoading.value = false if (requestSeq === excludeRequestSeq) excludeLoading.value = false
}) })
} }
@@ -440,56 +449,28 @@ function handleExcludePageChange(page) {
fetchExcludeRows() fetchExcludeRows()
} }
// 排除弹窗使用独立 Map 保存跨页选择,关闭或翻页不会丢失已勾选订单。
function handleExcludeSelectionChange(rows) {
if (syncingExcludeSelection) return
const currentPageIds = new Set(excludeRowsPage.value.map(row => row.billId))
currentPageIds.forEach(billId => excludeDraftMap.delete(billId))
rows.forEach(row => excludeDraftMap.set(row.billId, row))
syncExcludeDraftRows()
}
function syncExcludePageSelection() {
if (!excludeTableRef.value) {
syncingExcludeSelection = false
return
}
syncingExcludeSelection = true
excludeTableRef.value.clearSelection()
excludeRowsPage.value.forEach(row => {
if (excludeDraftMap.has(row.billId)) excludeTableRef.value.toggleRowSelection(row, true)
})
syncingExcludeSelection = false
}
function confirmExcludeSelection() { function confirmExcludeSelection() {
excludedRowMap.clear() if (!excludeSearchSubmitted.value || excludeLoading.value) return
excludeDraftMap.forEach((row, billId) => excludedRowMap.set(billId, row)) confirmedExcludeKeyword.value = excludeSearchKeyword.value
syncExcludedRows() confirmedExcludeCount.value = excludeTotal.value
confirmedExcludeAmount.value = excludeTotalAmount.value
if (!generateLoading.value) generateRequestId.value = '' if (!generateLoading.value) generateRequestId.value = ''
excludeDialogConfirmed = true
excludeDialogOpen.value = false excludeDialogOpen.value = false
} }
function removeExcludedRow(billId) { function clearConfirmedExclusionState() {
excludedRowMap.delete(billId) confirmedExcludeKeyword.value = ''
excludeDraftMap.delete(billId) confirmedExcludeCount.value = 0
syncExcludeDraftRows() confirmedExcludeAmount.value = 0
syncExcludedRows()
if (!generateLoading.value) generateRequestId.value = ''
} }
function cancelExcludeSelection() { function clearConfirmedExclusions() {
excludeDialogConfirmed = false clearConfirmedExclusionState()
excludeDialogOpen.value = false if (!generateLoading.value) generateRequestId.value = ''
} }
function handleExcludeDialogClose() { function handleExcludeDialogClose() {
if (!excludeDialogConfirmed) { invalidateExcludeSearch()
excludeDraftMap = new Map(excludedRowMap)
syncExcludeDraftRows()
}
excludeDialogConfirmed = false
} }
// 翻页后把 Map 中的选择同步回 Element Plus 当前页复选框。 // 翻页后把 Map 中的选择同步回 Element Plus 当前页复选框。
@@ -561,8 +542,8 @@ function createRequestId() { return globalThis.crypto?.randomUUID?.() || `ar-${D
.preview-selection-toolbar :deep(.el-checkbox) { margin-right: 6px; } .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; } .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; } .exclude-order-badge { margin-left: 4px; }
.excluded-order-preview { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; max-height: 64px; margin: 0 0 8px; padding: 4px 8px; overflow-y: auto; border-radius: 4px; background: var(--el-fill-color-light); } .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; }
.excluded-order-title { 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-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.exclude-search-input { width: 540px; } .exclude-search-input { width: 540px; }
.exclude-pagination { justify-content: flex-end; margin: 12px 0 4px; } .exclude-pagination { justify-content: flex-end; margin: 12px 0 4px; }