✨ 将应收账单预览导出改为异步处理
This commit is contained in:
+21
-19
@@ -3,6 +3,7 @@ package com.bms.web.controller.receivable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -30,13 +31,12 @@ import com.bms.receivable.domain.ArBillGenerateTask;
|
||||
import com.bms.receivable.domain.ArBillLog;
|
||||
import com.bms.receivable.domain.vo.ArBillActionRequest;
|
||||
import com.bms.receivable.domain.vo.ArBillGenerateQuery;
|
||||
import com.bms.receivable.domain.vo.ArBillGeneratePreviewExportRow;
|
||||
import com.bms.receivable.domain.vo.ArBillGenerateRequest;
|
||||
import com.bms.receivable.domain.vo.ArBillListExportRow;
|
||||
import com.bms.receivable.domain.vo.ArBillQuery;
|
||||
import com.bms.receivable.enums.ArBillDateType;
|
||||
import com.bms.receivable.service.IArBillService;
|
||||
import com.bms.receivable.security.ArBillScopeResolver;
|
||||
import com.bms.receivable.task.ArBillGeneratePreviewExportRunner;
|
||||
import com.bms.common.annotation.Log;
|
||||
import com.bms.common.core.controller.BaseController;
|
||||
import com.bms.common.core.domain.AjaxResult;
|
||||
@@ -64,6 +64,9 @@ public class ArBillController extends BaseController {
|
||||
@Autowired
|
||||
private ArBillScopeResolver billScope;
|
||||
|
||||
@Autowired
|
||||
private ArBillGeneratePreviewExportRunner previewExportRunner;
|
||||
|
||||
/** 应收筛选专用客户选项,与账单使用同一角色规则。 */
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:list')")
|
||||
@GetMapping("/customer-options")
|
||||
@@ -174,26 +177,25 @@ public class ArBillController extends BaseController {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 按生成预览使用的相同筛选条件导出全部匹配订单,不受分页参数影响。 */
|
||||
/** 按生成预览使用的相同筛选条件提交异步导出任务。 */
|
||||
@Log(title = "应收账单生成订单", businessType = BusinessType.EXPORT)
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||
@PostMapping("/generate/preview/export")
|
||||
public void exportGeneratePreview(HttpServletResponse response, ArBillGenerateQuery query,
|
||||
HttpServletRequest request) {
|
||||
rejectMultiValueParameter(request, "customerId");
|
||||
ArBillGenerateQuery exportQuery = query == null ? new ArBillGenerateQuery() : query;
|
||||
List<PrcOrderBill> bills = billService.previewBill(exportQuery);
|
||||
List<ArBillGeneratePreviewExportRow> rows = new ArrayList<>(bills.size());
|
||||
for (PrcOrderBill bill : bills) {
|
||||
rows.add(ArBillGeneratePreviewExportRow.from(bill));
|
||||
}
|
||||
ExcelUtil<ArBillGeneratePreviewExportRow> util = new ExcelUtil<>(ArBillGeneratePreviewExportRow.class);
|
||||
if (ArBillDateType.PICKUP_DATE == ArBillDateType.of(exportQuery.getDateType())) {
|
||||
util.hideColumn("bizDate");
|
||||
} else {
|
||||
util.hideColumn("pickupDate");
|
||||
}
|
||||
util.exportExcel(response, rows, "应收账单待生成订单");
|
||||
public AjaxResult exportGeneratePreview(@RequestBody ArBillGenerateQuery query) {
|
||||
return success(Map.of("taskId", previewExportRunner.submit(query, getUserId())));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||
@GetMapping("/generate/preview/export/task/{taskId}")
|
||||
public AjaxResult generatePreviewExportTask(@PathVariable String taskId) {
|
||||
return success(previewExportRunner.getTask(taskId, getUserId()));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('ar:bill:generate')")
|
||||
@GetMapping("/generate/preview/export/download/{taskId}")
|
||||
public void downloadGeneratePreviewExport(@PathVariable String taskId, HttpServletResponse response)
|
||||
throws Exception {
|
||||
previewExportRunner.download(taskId, getUserId(), response);
|
||||
}
|
||||
|
||||
/** 查询需要从全选结果中排除的订单;空条件不执行查询,避免返回全部订单。 */
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.bms.receivable.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
import lombok.Data;
|
||||
|
||||
/** 应收账单生成预览导出任务的临时状态。 */
|
||||
@Data
|
||||
public class ArBillGeneratePreviewExportTask implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String taskId;
|
||||
private Long userId;
|
||||
private String status;
|
||||
private int percent;
|
||||
private String message;
|
||||
private String error;
|
||||
private int rowCount;
|
||||
private String fileName;
|
||||
private String storageKey;
|
||||
}
|
||||
+6
-2
@@ -69,8 +69,12 @@ public class ArBillScopeResolver {
|
||||
}
|
||||
|
||||
public BmsCustomerOption requireCustomerAccessible(Long customerId) {
|
||||
ArBillRowScope scope = resolve();
|
||||
BmsCustomerOption customer = customerId == null ? null
|
||||
return requireCustomerAccessible(customerId, resolve());
|
||||
}
|
||||
|
||||
/** 异步任务使用提交时由服务端解析的权限范围。 */
|
||||
public BmsCustomerOption requireCustomerAccessible(Long customerId, ArBillRowScope scope) {
|
||||
BmsCustomerOption customer = customerId == null || scope == null ? null
|
||||
: withoutPagination(() -> mapper.selectAccessibleCustomer(customerId, scope));
|
||||
if (customer == null) throw new ServiceException("客户不存在或无权访问");
|
||||
return customer;
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.bms.receivable.domain.vo.ArBillGenerateQuery;
|
||||
import com.bms.receivable.domain.vo.ArBillActionRequest;
|
||||
import com.bms.receivable.domain.vo.ArBillQuery;
|
||||
import com.bms.receivable.domain.vo.ArBillSummary;
|
||||
import com.bms.receivable.security.ArBillRowScope;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
|
||||
/**
|
||||
@@ -40,6 +41,8 @@ public interface IArBillService
|
||||
|
||||
List<PrcOrderBill> previewBill(ArBillGenerateQuery query);
|
||||
|
||||
List<PrcOrderBill> previewBill(ArBillGenerateQuery query, ArBillRowScope scope);
|
||||
|
||||
List<PrcOrderBill> previewExcludedBill(ArBillGenerateQuery query);
|
||||
|
||||
BigDecimal sumPreviewBill(ArBillGenerateQuery query);
|
||||
|
||||
+7
-1
@@ -73,6 +73,7 @@ import com.bms.receivable.mapper.ArBillExportMapper;
|
||||
import com.bms.receivable.mapper.ArBillLogMapper;
|
||||
import com.bms.receivable.mapper.ArBillMapper;
|
||||
import com.bms.receivable.security.ArBillScopeResolver;
|
||||
import com.bms.receivable.security.ArBillRowScope;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomerOption;
|
||||
import com.bms.customer.customerinfo.domain.BmsCustomer;
|
||||
import com.bms.customer.customerinfo.mapper.BmsCustomerMapper;
|
||||
@@ -333,9 +334,14 @@ public class ArBillServiceImpl implements IArBillService {
|
||||
*/
|
||||
@Override
|
||||
public List<PrcOrderBill> previewBill(ArBillGenerateQuery query) {
|
||||
return previewBill(query, billScope.resolve());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PrcOrderBill> previewBill(ArBillGenerateQuery query, ArBillRowScope scope) {
|
||||
long startedAt = System.nanoTime();
|
||||
validatePreviewQuery(query);
|
||||
BmsCustomerOption customer = billScope.requireCustomerAccessible(query.getCustomerId());
|
||||
BmsCustomerOption customer = billScope.requireCustomerAccessible(query.getCustomerId(), scope);
|
||||
query.setCustomerCode(customer.getCustomerCode());
|
||||
List<String> orderIdentifiers = normalizeOrderIdentifiers(query.getOrderOrTrackingNo());
|
||||
List<String> excludeOrderIdentifiers = normalizeOrderIdentifiers(query.getExcludeOrderOrTrackingNo());
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package com.bms.receivable.task;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.task.TaskRejectedException;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.bms.common.config.BmsConfig;
|
||||
import com.bms.common.core.redis.RedisCache;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.file.FileUtils;
|
||||
import com.bms.common.utils.poi.ExcelUtil;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.price.domain.PrcOrderBill;
|
||||
import com.bms.receivable.domain.ArBillGeneratePreviewExportTask;
|
||||
import com.bms.receivable.domain.vo.ArBillGeneratePreviewExportRow;
|
||||
import com.bms.receivable.domain.vo.ArBillGenerateQuery;
|
||||
import com.bms.receivable.enums.ArBillDateType;
|
||||
import com.bms.receivable.security.ArBillRowScope;
|
||||
import com.bms.receivable.security.ArBillScopeResolver;
|
||||
import com.bms.receivable.service.IArBillService;
|
||||
|
||||
/** 生成账单预览的异步导出;使用发起人的权限查询订单。 */
|
||||
@Component
|
||||
public class ArBillGeneratePreviewExportRunner {
|
||||
private static final Logger log = LoggerFactory.getLogger(ArBillGeneratePreviewExportRunner.class);
|
||||
private static final String CACHE_PREFIX = "ar:bill:generate:preview:export:";
|
||||
private static final String REL_DIR = "arBillGeneratePreviewExport";
|
||||
private static final int TTL_MINUTES = 60;
|
||||
private static final long FILE_RETENTION_MILLIS = TimeUnit.HOURS.toMillis(24);
|
||||
|
||||
private final IArBillService billService;
|
||||
private final RedisCache redisCache;
|
||||
private final ThreadPoolTaskExecutor executor;
|
||||
private final ArBillScopeResolver billScope;
|
||||
|
||||
public ArBillGeneratePreviewExportRunner(IArBillService billService, RedisCache redisCache, ArBillScopeResolver billScope,
|
||||
@Qualifier("arBillExportTaskExecutor") ThreadPoolTaskExecutor executor) {
|
||||
this.billService = billService;
|
||||
this.redisCache = redisCache;
|
||||
this.billScope = billScope;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public String submit(ArBillGenerateQuery query, Long userId) {
|
||||
String taskId = IdUtils.fastSimpleUUID();
|
||||
String fileName = "应收账单待生成订单_" + DateUtils.dateTimeNow() + ".xlsx";
|
||||
ArBillRowScope scope = billScope.resolve();
|
||||
put(taskId, userId, "PENDING", 0, "排队中", null, 0, fileName, null);
|
||||
try {
|
||||
executor.execute(() -> run(taskId, userId, query, fileName, scope));
|
||||
} catch (TaskRejectedException e) {
|
||||
put(taskId, userId, "FAIL", 100, "导出失败", "导出任务较多,请稍后重试", 0, fileName, null);
|
||||
}
|
||||
return taskId;
|
||||
}
|
||||
|
||||
private void run(String taskId, Long userId, ArBillGenerateQuery query, String fileName,
|
||||
ArBillRowScope scope) {
|
||||
Path file = null;
|
||||
try {
|
||||
put(taskId, userId, "RUNNING", 5, "正在查询订单", null, 0, fileName, null);
|
||||
List<PrcOrderBill> bills = billService.previewBill(query, scope);
|
||||
put(taskId, userId, "RUNNING", 40, "正在生成 Excel", null, bills.size(), fileName, null);
|
||||
List<ArBillGeneratePreviewExportRow> rows = new ArrayList<>(bills.size());
|
||||
for (PrcOrderBill bill : bills) {
|
||||
rows.add(ArBillGeneratePreviewExportRow.from(bill));
|
||||
}
|
||||
ExcelUtil<ArBillGeneratePreviewExportRow> util = new ExcelUtil<>(ArBillGeneratePreviewExportRow.class);
|
||||
if (ArBillDateType.PICKUP_DATE == ArBillDateType.of(query.getDateType())) {
|
||||
util.hideColumn("bizDate");
|
||||
} else {
|
||||
util.hideColumn("pickupDate");
|
||||
}
|
||||
Path dir = Paths.get(BmsConfig.getDownloadPath(), REL_DIR).toAbsolutePath().normalize();
|
||||
Files.createDirectories(dir);
|
||||
file = dir.resolve(taskId + ".xlsx").normalize();
|
||||
if (!file.startsWith(dir)) {
|
||||
throw new IllegalStateException("invalid export path");
|
||||
}
|
||||
try (OutputStream out = Files.newOutputStream(file)) {
|
||||
util.exportExcelToStream(rows, "应收账单待生成订单", out);
|
||||
}
|
||||
put(taskId, userId, "SUCCESS", 100, "导出完成,正在下载", null, bills.size(), fileName,
|
||||
REL_DIR + "/" + taskId + ".xlsx");
|
||||
} catch (Exception e) {
|
||||
log.error("应收账单生成预览导出失败 taskId={}", taskId, e);
|
||||
if (file != null) {
|
||||
try { Files.deleteIfExists(file); } catch (Exception cleanupError) {
|
||||
log.warn("清理导出文件失败 taskId={}", taskId, cleanupError);
|
||||
}
|
||||
}
|
||||
String error = e instanceof ServiceException ? e.getMessage() : "导出失败,请稍后重试";
|
||||
put(taskId, userId, "FAIL", 100, "导出失败", error, 0, fileName, null);
|
||||
}
|
||||
}
|
||||
|
||||
public ArBillGeneratePreviewExportTask getTask(String taskId, Long userId) {
|
||||
ArBillGeneratePreviewExportTask task = redisCache.getCacheObject(CACHE_PREFIX + taskId);
|
||||
if (task == null || !userId.equals(task.getUserId())) {
|
||||
throw new ServiceException("导出任务不存在或已过期");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
public void download(String taskId, Long userId, HttpServletResponse response) throws Exception {
|
||||
ArBillGeneratePreviewExportTask task = getTask(taskId, userId);
|
||||
String storageKey = task.getStorageKey();
|
||||
if (!"SUCCESS".equals(task.getStatus()) || storageKey == null || !FileUtils.checkAllowDownload(storageKey)) {
|
||||
throw new ServiceException("导出文件尚未生成");
|
||||
}
|
||||
Path root = Paths.get(BmsConfig.getDownloadPath()).toAbsolutePath().normalize();
|
||||
Path file = root.resolve(storageKey).normalize();
|
||||
if (!file.startsWith(root) || !Files.isRegularFile(file)) {
|
||||
throw new ServiceException("导出文件不存在");
|
||||
}
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
FileUtils.setAttachmentResponseHeader(response, task.getFileName());
|
||||
FileUtils.writeBytes(file.toString(), response.getOutputStream());
|
||||
}
|
||||
|
||||
/** 清理未下载或因客户端断开而遗留的过期文件。 */
|
||||
@Scheduled(cron = "0 15 * * * ?")
|
||||
public void cleanupExpiredFiles() {
|
||||
Path dir = Paths.get(BmsConfig.getDownloadPath(), REL_DIR).toAbsolutePath().normalize();
|
||||
if (!Files.isDirectory(dir)) {
|
||||
return;
|
||||
}
|
||||
long cutoff = System.currentTimeMillis() - FILE_RETENTION_MILLIS;
|
||||
try (Stream<Path> files = Files.list(dir)) {
|
||||
files.filter(file -> file.getFileName().toString().matches("[a-f0-9]{32}\\.xlsx"))
|
||||
.filter(file -> Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS))
|
||||
.forEach(file -> {
|
||||
try {
|
||||
if (Files.getLastModifiedTime(file, LinkOption.NOFOLLOW_LINKS).toMillis() < cutoff) {
|
||||
Files.deleteIfExists(file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("清理过期导出文件失败: {}", file, e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.warn("扫描过期导出文件失败: {}", dir, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void put(String taskId, Long userId, String status, int percent, String message, String error,
|
||||
int rowCount, String fileName, String storageKey) {
|
||||
ArBillGeneratePreviewExportTask task = new ArBillGeneratePreviewExportTask();
|
||||
task.setTaskId(taskId);
|
||||
task.setUserId(userId);
|
||||
task.setStatus(status);
|
||||
task.setPercent(percent);
|
||||
task.setMessage(message);
|
||||
task.setError(error);
|
||||
task.setRowCount(rowCount);
|
||||
task.setFileName(fileName);
|
||||
task.setStorageKey(storageKey);
|
||||
redisCache.setCacheObject(CACHE_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
<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">
|
||||
@@ -28,7 +43,7 @@
|
||||
<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" :icon="Download" :loading="previewExportLoading" :disabled="previewLoading" @click="exportPreview">导出查询结果</el-button>
|
||||
<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">
|
||||
@@ -95,6 +110,17 @@
|
||||
</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">
|
||||
@@ -150,6 +176,7 @@
|
||||
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: {
|
||||
@@ -168,6 +195,29 @@ 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)
|
||||
@@ -352,13 +402,50 @@ function handlePreviewPageChange(page) {
|
||||
fetchPreview()
|
||||
}
|
||||
|
||||
function exportPreview() {
|
||||
async function exportPreview() {
|
||||
if (!previewSearched.value || !previewTotal.value || !previewQuery.value || previewExportLoading.value) return
|
||||
clearTimeout(previewExportHideTimer)
|
||||
previewExportLoading.value = true
|
||||
proxy.download('/ar/bill/generate/preview/export', { ...previewQuery.value }, `ar_bill_generate_preview_${Date.now()}.xlsx`)
|
||||
.finally(() => {
|
||||
previewExportLoading.value = false
|
||||
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 因此可以跨页保留手动选择。
|
||||
@@ -562,6 +649,17 @@ function createRequestId() { return globalThis.crypto?.randomUUID?.() || `ar-${D
|
||||
.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; }
|
||||
|
||||
Reference in New Issue
Block a user