未结算报表zip导出
This commit is contained in:
parent
61c40efe2b
commit
0d14a3d349
|
|
@ -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>
|
||||
Loading…
Reference in New Issue