zip批量导出

This commit is contained in:
hongchao 2025-11-21 15:07:52 +08:00
parent cc24edb8a9
commit d7f7b85783
3 changed files with 277 additions and 60 deletions

View File

@ -228,49 +228,27 @@ export function downloadJson(url, params, filename, config) {
// 通用下载方法
export function downloadZip(url, params, filename, config) {
const backgroundMode = config && config.background === true
const showLoading = !(config && config.showLoading === false) && !backgroundMode
const finalAxiosConfig = {
transformRequest: [
(params) => {
return params
},
],
headers: { 'Content-Type': 'application/json', encryptResponse: false },
responseType: 'blob',
// if background mode and no explicit timeout, extend to 10 minutes
timeout: backgroundMode && !(config && typeof config.timeout === 'number') ? 600000 : undefined,
...config,
}
if (showLoading) {
downloadLoadingInstance = Loading.service({
text: '正在下载数据,请稍候',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)',
})
} else if (backgroundMode) {
Notification.info({
title: '已开始生成',
message: '批量导出任务已在后台执行,准备就绪后将自动下载。',
duration: 4000,
})
}
return service
.post(url, params, finalAxiosConfig)
.post(url, params, {
transformRequest: [
(params) => {
return params
},
],
headers: { 'Content-Type': 'application/json',encryptResponse: false },
responseType: 'blob',
...config,
})
.then(async (data) => {
const isBlob = blobValidate(data)
if (isBlob) {
const blob = new Blob([data])
saveAs(blob, filename)
if (backgroundMode) {
Notification.success({
title: '导出完成',
message: 'ZIP 文件已准备好,正在开始下载。',
duration: 3000,
})
}
} else {
const resText = await data.text()
const rspObj = JSON.parse(resText)
@ -278,20 +256,12 @@ export function downloadZip(url, params, filename, config) {
errorCode[rspObj.code] || rspObj.msg || errorCode['default']
Message.error(errMsg)
}
if (showLoading && downloadLoadingInstance) {
downloadLoadingInstance.close()
}
})
.catch((r) => {
console.error(r)
if (r && r.message && r.message.includes('timeout')) {
Message.error('导出超时,请减少选择数量或稍后重试')
} else {
Message.error('下载文件出现错误,请联系管理员!')
}
if (showLoading && downloadLoadingInstance) {
downloadLoadingInstance.close()
}
})
}
export default service

View File

@ -37,8 +37,15 @@
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery" >查询</el-button>
<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="success" icon="el-icon-download" size="mini" @click="exportZip" :disabled="tableList.length === 0">批量导出ZIP</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-form-item>
</el-form>
@ -568,6 +575,9 @@ import vueEasyPrint from "vue-easy-print";
import Treeselect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.css";
import * as XLSX from 'xlsx';
import request from '@/utils/request'
export default {
name: 'UnreportHome',
dicts: ['cost_status'],
@ -664,7 +674,18 @@ export default {
proStatus:[
{ id: 0, name: '未竣工' },
{ id: 1, name: '已竣工' },
]
],
//zip
exporting: false,
exportFlag: false,
progress: 0,
current: 0,
totalZip: 0,
statusText: '',
taskId: null,
pollInterval: null
}
},
created() {
@ -815,8 +836,32 @@ export default {
this.aform = { status: '2' };
},
//********************************************************************** */
//zip
exportZip() {
// 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
@ -825,19 +870,92 @@ export default {
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 }
)
try {
this.exporting = true
this.progress = 0
this.current = 0
this.totalZip = 0
this.statusText = '开始导出...'
const response = await request({
url: '/material/slt_agreement_info/exportUnsettled',
method: 'post',
data: JSON.stringify(param),
timeout: 60000 // 10
})
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
}
this.$refs.tableRef.clearSelection()
},
startPolling() {
this.pollInterval = setInterval(async () => {
try {
const response = await request({
url: `/material/slt_agreement_info/exportProgress/${this.taskId}`,
method: 'get'
})
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
}
}, 1000)
},
downloadFile() {
if (this.taskId) {
console.log("xxxxxxxxxxtaskId",this.taskId)
const param = { taskId: this.taskId }
this.downloadZip(
'material/slt_agreement_info/downloadExport',
JSON.stringify(param),
`未结算批量明细导出_${new Date().getTime()}.zip`,
)
}
},
beforeUnmount() {
if (this.pollInterval) {
clearInterval(this.pollInterval)
}
},
//********************************************************************** */
openPrintDialog(row){
this.openPrint = true
@ -1593,4 +1711,129 @@ export default {
text-align: center;
border-left: 1px solid #9c9c9c;
}
//zip
.progress-container {
margin-top: 20px;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #f0f0f0;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: #4CAF50;
transition: width 0.3s ease;
}
.progress-text {
margin-top: 8px;
text-align: center;
font-size: 14px;
color: #666;
}
.status-text {
margin-top: 5px;
text-align: center;
font-size: 12px;
color: #999;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
//
</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>

View File

@ -250,6 +250,8 @@ export default {
keyWord: undefined,
proId: undefined,
jijuType: undefined,
startTime: undefined,
endTime: undefined
},
deviceRecordList: [], //
dialogDeviceTotal: 0, //
@ -353,6 +355,8 @@ export default {
this.dialogDeviceQuery.keyWord = ''
this.dialogDeviceQuery.pageNum = 1
this.dialogDeviceQuery.pageSize = 10
this.dialogDeviceQuery.startTime = this.queryParams.startTime
this.dialogDeviceQuery.endTime = this.queryParams.endTime
this.deviceRecordList = []
this.getDeviceRecords()
},