未结算报表zip导出

This commit is contained in:
hongchao 2025-12-10 17:03:03 +08:00
parent 61c40efe2b
commit 0d14a3d349
1 changed files with 351 additions and 207 deletions

View File

@ -38,17 +38,52 @@
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery" >重置</el-button>
<el-button type="success" icon="el-icon-download" size="mini" @click="exportExcel" :disabled="tableList.length === 0">导出Excel</el-button>
<el-button type="info" icon="el-icon-document-copy" size="mini" @click="showHistoryReportDialog">查看历史报表</el-button>
<el-button type="success" icon="el-icon-download" size="mini" @click="exportZip" :disabled="exporting">{{ exporting ? '导出中...' : '批量导出ZIP' }}</el-button>
<div v-if="exporting" class="progress-container">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
<div class="progress-text">{{ progress }}% ({{ current }}/{{ totalZip }})</div>
<div class="status-text">{{ statusText }}</div>
</div>
<el-button
type="success"
icon="el-icon-download"
size="mini"
@click="exportZip"
:disabled="!canDownload"
:loading="isExporting">
{{ exportButtonText }}
</el-button>
</el-form-item>
</el-form>
<!-- 进度条显示区域 -->
<div v-if="showProgress" class="progress-container">
<el-progress
:percentage="progressPercentage"
:status="progressStatus"
:stroke-width="18"
:text-inside="true"
:show-text="true">
</el-progress>
<div class="progress-info">
<span class="progress-text">{{ progressText }}</span>
<span class="progress-detail">{{ currentItem }}/{{ totalItems }} {{ currentFileName }}</span>
<span class="progress-speed" v-if="downloadSpeed > 0">
速度: {{ formatBytes(downloadSpeed) }}/s
</span>
</div>
<div class="progress-actions" v-if="showProgress">
<el-button
size="mini"
@click="cancelDownload"
:disabled="!isExporting"
v-if="!downloadComplete">
取消导出
</el-button>
<el-button
size="mini"
type="success"
@click="clearProgress"
v-if="downloadComplete">
完成
</el-button>
</div>
</div>
<el-table v-loading="loading" ref="tableRef" :data="tableList" @selection-change="handleSelectionChange" :max-height="650">
<el-table-column
type="selection"
@ -680,16 +715,43 @@ export default {
//zip
exporting: false,
exportFlag: false,
progress: 0,
current: 0,
totalZip: 0,
statusText: '',
taskId: null,
pollInterval: null
isExporting: false,
showProgress: false,
downloadComplete: false,
progressPercentage: 0,
currentItem: 0,
totalItems: 0,
currentFileName: '',
progressText: '准备导出...',
progressStatus: '',
downloadSpeed: 0,
downloadController: null,
retryCount: 0,
maxRetries: 3,
downloadStartTime: 0,
downloadedBytes: 0,
}
},
computed: {
/** 是否可以下载 */
canDownload() {
const can = this.ids.length > 0 && !this.isExporting;
console.log('canDownload检查:', {
ids: this.ids.length,
isExporting: this.isExporting,
can: can
});
return can;
},
/** 下载按钮文字 */
exportButtonText() {
if (this.isExporting) {
return `导出中 ${this.progressPercentage}%`;
}
return `批量导出ZIP${this.ids.length > 0 ? ` (${this.ids.length})` : ''}`;
},
},
created() {
this.GetUnitData()
this.GetProData()
@ -841,113 +903,221 @@ export default {
//********************************************************************** */
//zip
// exportZip() {
// if (!this.ids.length) {
// this.$message.error('')
// return false
// }
// let param = []
// this.ids.map(item => {
// param.push({ agreementId: item.agreementId,settlementType:item.settlementType })
// })
// // exportLeaseAll(this.exportParams).then(res => {
// // downloadFile({ fileName: `_${new Date().getTime()}.zip`, fileData: res, fileType: 'application/zip;charset=utf-8' })
// // })
// this.downloadZip(
// 'material/slt_agreement_info/exportUnsettled',
// JSON.stringify(param),
// `_${new Date().getTime()}.zip`,
// { background: true, showLoading: false, timeout: 600000 }
// )
// this.$refs.tableRef.clearSelection()
// },
async exportZip() {
if (!this.ids.length) {
this.$message.error('请选择要导出的记录')
return false
}
let param = []
this.ids.map(item => {
param.push({ agreementId: item.agreementId,settlementType:item.settlementType })
})
//
try {
this.exporting = true
this.progress = 0
this.current = 0
this.totalZip = 0
this.statusText = '开始导出...'
const response = await getExportZipUnsettled(param)
const result = response
this.taskId = result.taskId
this.totalZip = result.total
this.statusText = '导出任务已开始...'
this.exportFlag = true
//
this.startPolling()
} catch (error) {
console.error('导出请求失败', error)
this.statusText = '导出请求失败'
this.exporting = false
await this.$confirm(`确定要导出 ${this.ids.length} 个选中项吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
center: true
});
} catch (cancel) {
console.log('用户取消导出');
return;
}
this.$refs.tableRef.clearSelection()
},
startPolling() {
this.pollInterval = setInterval(async () => {
try {
const param = { taskId: this.taskId }
const response = await getExportZipProgress(param)
const progress = response
this.progress = progress.percentage
this.current = progress.current
if (progress.status === 'completed') {
this.statusText = '导出完成,开始下载...'
clearInterval(this.pollInterval)
this.downloadFile()
this.exporting = false
this.exportFlag = false
} else if (progress.status === 'error') {
this.statusText = `导出失败: ${progress.errorMsg}`
clearInterval(this.pollInterval)
this.exporting = false
} else {
this.statusText = `处理中: ${progress.percentage}%`
}
} catch (error) {
console.error('查询进度失败', error)
this.statusText = '查询进度失败'
this.exportFlag = false
}
}, 5000)
},
downloadFile() {
if (this.taskId) {
console.log("xxxxxxxxxxtaskId",this.taskId)
const param = { taskId: this.taskId }
this.downloadZip(
'material/slt_agreement_info/dlExProgress',
JSON.stringify(param),
`未结算批量明细导出_${new Date().getTime()}.zip`,
)
//
this.initializeDownload();
try {
let param = []
this.ids.map(item => {
param.push({ agreementId: item.agreementId,settlementType:item.settlementType })
})
this.totalItems = param.length;
const taskId = crypto.randomUUID()
const payload = {
list: param,
taskId:taskId,
zipName: `未结算批量明细导出_${new Date().getTime()}`,
stream: true
};
//
await this.streamDownload(payload);
this.$refs.tableRef.clearSelection()
} catch (err) {
console.error('未结算批量明细导出失败', err);
this.handleDownloadError(err);
}
},
beforeUnmount() {
if (this.pollInterval) {
clearInterval(this.pollInterval)
/** 初始化下载状态 */
initializeDownload() {
this.isExporting = true;
this.showProgress = true;
this.downloadComplete = false;
this.progressPercentage = 0;
this.currentItem = 0;
this.totalItems = 0;
this.currentFileName = '';
this.progressText = '准备导出...';
this.progressStatus = null;
this.downloadSpeed = 0;
this.retryCount = 0;
this.downloadedBytes = 0;
this.downloadStartTime = Date.now();
this.downloadController = new AbortController();
},
/** 流式下载方法 - 使用原始的request方式 */
async streamDownload(payload) {
try {
this.progressText = '正在连接服务器...';
// 使request
const response = await request({
url: '/material/slt_agreement_info/dlExProgress',
method: 'POST',
data: payload,
responseType: 'blob',
timeout: 0,
onDownloadProgress: (progressEvent) => {
this.handleDownloadProgress(progressEvent);
}
});
//
const blob = new Blob([response]);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `${payload.zipName}.zip`;
document.body.appendChild(a);
a.click();
//
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
//
this.handleDownloadComplete();
} catch (error) {
if (error.message && error.message.includes('canceled')) {
this.progressText = '导出已取消';
this.progressPercentage = 0;
this.progressStatus = 'exception';
} else {
console.error('导出失败:', error);
this.handleDownloadError(error);
}
}
},
/** 处理下载进度 */
handleDownloadProgress(progressEvent) {
if (progressEvent.total) {
// ZIP
const percentage = Math.round((progressEvent.loaded * 100) / progressEvent.total);
this.progressPercentage = Math.min(percentage, 99);
const elapsed = Date.now() - this.downloadStartTime;
if (elapsed > 0) {
this.downloadSpeed = progressEvent.loaded * 1000 / elapsed;
this.progressText = `${this.formatBytes(progressEvent.loaded)} / ${this.formatBytes(progressEvent.total)}`;
}
} else {
//
// 1. currentItem
if (this.totalItems > 0) {
this.currentItem = Math.min(this.currentItem + 1, this.totalItems);
}
// 2. 100
this.progressPercentage = Math.min(
Math.round((this.currentItem / this.totalItems) * 100),
100
);
this.progressText = `处理中:${this.currentItem}/${this.totalItems}`;
}
},
/** 处理下载完成 */
handleDownloadComplete() {
// 2.
this.currentItem = this.totalItems;
this.progressPercentage = 100;
this.progressText = '导出完成';
this.progressStatus = 'success';
this.downloadComplete = true;
this.isExporting = false;
this.downloadSpeed = 0;
this.$message({
message: '导出完成!',
type: 'success',
duration: 3000
});
},
/** 处理下载错误 */
handleDownloadError(error) {
console.error('导出失败:', error);
if (this.retryCount < this.maxRetries) {
this.retryCount++;
this.progressText = `导出失败,正在重试 (${this.retryCount}/${this.maxRetries})...`;
this.progressStatus = 'warning';
// 3
setTimeout(() => {
this.exportZip();
}, 3000);
} else {
this.progressText = '导出失败,请重试';
this.progressStatus = 'exception';
this.isExporting = false;
this.$message.error('导出失败: ' + (error.message || '未知错误'));
}
},
/** 取消下载 */
cancelDownload() {
if (this.downloadController) {
this.downloadController.abort();
}
this.isExporting = false;
this.showProgress = false;
this.$message.info('导出已取消');
},
/** 清除进度显示 */
clearProgress() {
this.showProgress = false;
this.downloadComplete = false;
this.progressPercentage = 0;
this.currentItem = 0;
this.totalItems = 0;
this.downloadSpeed = 0;
this.progressStatus = null;
this.currentFileName = '';
},
/** 格式化字节大小 */
formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
},
//********************************************************************** */
@ -1707,41 +1877,97 @@ export default {
}
//zip
/* 进度条容器样式 */
.progress-container {
margin-top: 20px;
padding: 15px;
margin: 20px 0;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 4px;
border-radius: 8px;
background-color: #f9f9f9;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
animation: fadeIn 0.3s ease;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.progress-fill {
height: 100%;
background-color: #4CAF50;
transition: width 0.3s ease;
.progress-info {
margin-top: 15px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.progress-text {
margin-top: 8px;
text-align: center;
font-size: 14px;
color: #333;
font-weight: 500;
}
.progress-detail {
font-size: 12px;
color: #666;
}
.status-text {
margin-top: 5px;
text-align: center;
.progress-speed {
font-size: 12px;
color: #999;
color: #409eff;
font-weight: 500;
}
.progress-actions {
margin-top: 15px;
display: flex;
justify-content: center;
gap: 10px;
}
/* Element UI Progress 自定义样式 */
::v-deep .el-progress-bar__inner {
background: linear-gradient(90deg, #409eff 0%, #67c23a 100%);
transition: width 0.3s ease;
}
::v-deep .el-progress-bar__innerText {
color: #fff;
font-weight: bold;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}
/* 响应式调整 */
@media (max-width: 768px) {
.progress-container {
padding: 15px;
margin: 15px 0;
}
.progress-text {
font-size: 13px;
}
.progress-detail,
.progress-speed {
font-size: 11px;
}
.el-button--mini {
min-width: 80px;
padding: 5px 12px;
font-size: 12px;
}
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
@ -1749,85 +1975,3 @@ button:disabled {
//
</style>
<!-- 为了让CSS生效需要在组件的style部分添加以下样式 -->
<style scoped>
/* 优化的进度条样式 */
.progress-container.improved {
background: #f5f7fa;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-top: 12px;
width: 100%;
max-width: 600px;
}
.progress-bar {
height: 10px;
background-color: #e4e7ed;
border-radius: 5px;
overflow: hidden;
position: relative;
margin-bottom: 12px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #409eff 0%, #67c23a 100%);
border-radius: 5px;
transition: width 0.3s ease;
position: relative;
display: flex;
align-items: center;
justify-content: flex-end;
}
.progress-indicator {
position: absolute;
right: 8px;
color: #fff;
font-size: 10px;
font-weight: bold;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-text {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.status-text {
font-size: 13px;
color: #409eff;
font-weight: 500;
}
/* 添加一些额外的动画效果 */
.progress-fill::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 50%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 200%;
}
}
</style>