<template>
|
<el-dialog
|
:model-value="dialogVisible"
|
@update:model-value="$emit('update:dialogVisible', $event)"
|
title="批量下载发票"
|
width="800px"
|
:close-on-click-modal="false"
|
:close-on-press-escape="false"
|
>
|
<div class="batch-download-container">
|
<!-- 发票列表信息 -->
|
<el-card class="invoice-list" shadow="never">
|
<template #header>
|
<div class="card-header">
|
<span>待下载发票列表</span>
|
<el-tag type="info" size="small" style="margin-left: 10px">
|
共 {{ invoices.length }} 张发票
|
</el-tag>
|
</div>
|
</template>
|
|
<el-table :data="invoices" style="width: 100%" max-height="300">
|
<el-table-column prop="invoiceNo" label="发票号码" width="120" />
|
<el-table-column prop="buyerName" label="购买方" width="150" />
|
<el-table-column prop="amount" label="金额" width="100">
|
<template #default="scope">
|
¥{{ scope.row.amount.toFixed(2) }}
|
</template>
|
</el-table-column>
|
<el-table-column prop="status" label="状态" width="100">
|
<template #default="scope">
|
<el-tag :type="getStatusType(scope.row.status)" size="small">
|
{{ getStatusText(scope.row.status) }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
</el-table>
|
</el-card>
|
|
<!-- 下载选项 -->
|
<el-card class="download-options" shadow="never">
|
<template #header>
|
<div class="card-header">下载选项</div>
|
</template>
|
|
<el-form :model="downloadOptions" label-width="120px">
|
<!-- 文件格式 -->
|
<el-form-item label="文件格式">
|
<el-radio-group v-model="downloadOptions.format">
|
<el-radio label="html">HTML格式</el-radio>
|
<el-radio label="excel">Excel格式</el-radio>
|
<el-radio label="zip">ZIP压缩包</el-radio>
|
</el-radio-group>
|
</el-form-item>
|
|
<!-- 下载内容 -->
|
<el-form-item label="下载内容">
|
<el-checkbox-group v-model="downloadOptions.content">
|
<el-checkbox label="invoice">发票正本</el-checkbox>
|
<el-checkbox label="details">明细清单</el-checkbox>
|
<el-checkbox label="summary">汇总报表</el-checkbox>
|
</el-checkbox-group>
|
</el-form-item>
|
|
<!-- 文件命名 -->
|
<el-form-item label="文件命名">
|
<el-select v-model="downloadOptions.naming" style="width: 100%">
|
<el-option label="发票号码_购买方名称" value="invoice_buyer" />
|
<el-option label="发票号码_日期" value="invoice_date" />
|
<el-option label="购买方名称_日期" value="buyer_date" />
|
<el-option label="自定义前缀" value="custom" />
|
</el-select>
|
</el-form-item>
|
|
<!-- 自定义前缀 -->
|
<el-form-item v-if="downloadOptions.naming === 'custom'" label="自定义前缀">
|
<el-input v-model="downloadOptions.customPrefix" placeholder="请输入文件前缀" />
|
</el-form-item>
|
|
<!-- 压缩选项 -->
|
<el-form-item label="压缩选项">
|
<el-checkbox v-model="downloadOptions.compress">启用压缩</el-checkbox>
|
<el-checkbox v-model="downloadOptions.password">设置解压密码</el-checkbox>
|
</el-form-item>
|
|
<!-- 解压密码 -->
|
<el-form-item v-if="downloadOptions.password" label="解压密码">
|
<el-input v-model="downloadOptions.extractPassword" placeholder="请输入解压密码" />
|
</el-form-item>
|
</el-form>
|
</el-card>
|
|
<!-- 下载进度 -->
|
<el-card v-if="downloading" class="download-progress" shadow="never">
|
<template #header>
|
<div class="card-header">下载进度</div>
|
</template>
|
|
<div class="progress-content">
|
<el-progress
|
:percentage="downloadProgress"
|
:status="downloadProgress === 100 ? 'success' : ''"
|
:stroke-width="20"
|
/>
|
<div class="progress-text">
|
{{ downloadProgress === 100 ? '下载完成' : `正在下载... ${downloadProgress}%` }}
|
</div>
|
<div class="progress-detail">
|
{{ downloadProgress === 100 ? '所有发票下载完成' : `已下载 ${downloadedCount} 张,剩余 ${invoices.length - downloadedCount} 张` }}
|
</div>
|
</div>
|
</el-card>
|
</div>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="handleClose" :disabled="downloading">取消</el-button>
|
<el-button
|
type="primary"
|
@click="handleBatchDownload"
|
:loading="downloading"
|
:disabled="!canDownload"
|
>
|
{{ downloading ? '下载中...' : '开始下载' }}
|
</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</template>
|
|
<script setup>
|
import { ref, computed, watch } from 'vue';
|
import { ElMessage } from 'element-plus';
|
|
// 定义props
|
const props = defineProps({
|
dialogVisible: {
|
type: Boolean,
|
default: false
|
},
|
invoices: {
|
type: Array,
|
default: () => []
|
}
|
});
|
|
// 定义emits
|
const emit = defineEmits(['update:dialogVisible', 'success']);
|
|
// 响应式数据
|
const downloading = ref(false);
|
const downloadProgress = ref(0);
|
const downloadedCount = ref(0);
|
const downloadOptions = ref({
|
format: 'pdf',
|
content: ['invoice'],
|
naming: 'invoice_buyer',
|
customPrefix: '',
|
compress: true,
|
password: false,
|
extractPassword: ''
|
});
|
|
// 计算属性
|
const canDownload = computed(() => {
|
return props.invoices.length > 0 && !downloading.value;
|
});
|
|
// 监听器
|
watch(() => props.invoices, () => {
|
// 重置下载状态
|
downloading.value = false;
|
downloadProgress.value = 0;
|
downloadedCount.value = 0;
|
}, { immediate: true });
|
|
// 获取状态类型
|
const getStatusType = (status) => {
|
const statusMap = {
|
'issued': 'success',
|
'pending': 'warning',
|
'cancelled': 'danger'
|
};
|
return statusMap[status] || 'info';
|
};
|
|
// 获取状态文本
|
const getStatusText = (status) => {
|
const statusMap = {
|
'issued': '已开票',
|
'pending': '待开票',
|
'cancelled': '已作废'
|
};
|
return statusMap[status] || '未知';
|
};
|
|
// 关闭对话框
|
const handleClose = () => {
|
if (!downloading.value) {
|
emit('update:dialogVisible', false);
|
}
|
};
|
|
// 批量下载
|
const handleBatchDownload = async () => {
|
if (downloading.value) return;
|
|
try {
|
downloading.value = true;
|
downloadProgress.value = 0;
|
downloadedCount.value = 0;
|
|
// 批量下载过程
|
if (downloadOptions.format === 'zip') {
|
// ZIP格式:生成单个压缩包
|
await generateZIPFile();
|
downloadProgress.value = 100;
|
downloadedCount.value = props.invoices.length;
|
} else {
|
// 其他格式:逐个下载文件
|
const totalInvoices = props.invoices.length;
|
const progressStep = 100 / totalInvoices;
|
|
for (let i = 0; i < totalInvoices; i++) {
|
// 生成单个发票文件
|
await generateInvoiceFile(props.invoices[i], i);
|
|
downloadedCount.value++;
|
downloadProgress.value = Math.round((i + 1) * progressStep);
|
|
// 短暂延迟,避免浏览器阻塞
|
await new Promise(resolve => setTimeout(resolve, 200));
|
}
|
|
// 生成汇总文件
|
if (downloadOptions.content.includes('summary')) {
|
await generateSummaryFile();
|
}
|
}
|
|
// 下载完成
|
ElMessage.success(`成功下载 ${totalInvoices} 张发票`);
|
emit('success');
|
|
} catch (error) {
|
console.error('批量下载失败:', error);
|
ElMessage.error("批量下载失败,请重试");
|
downloading.value = false;
|
downloadProgress.value = 0;
|
downloadedCount.value = 0;
|
}
|
};
|
|
// 生成单个发票文件
|
const generateInvoiceFile = async (invoice, index) => {
|
try {
|
let fileContent, fileName, mimeType;
|
|
if (downloadOptions.format === 'html') {
|
fileContent = generateHTMLContent(invoice);
|
fileName = `${getFileName(invoice, index)}.html`;
|
mimeType = 'text/html';
|
} else if (downloadOptions.content.includes('details')) {
|
fileContent = generateExcelContent(invoice);
|
fileName = `${getFileName(invoice, index)}.csv`;
|
mimeType = 'text/csv';
|
} else if (downloadOptions.format === 'excel') {
|
fileContent = generateExcelContent(invoice);
|
fileName = `${getFileName(invoice, index)}.csv`;
|
mimeType = 'text/csv';
|
} else if (downloadOptions.format === 'zip') {
|
// ZIP格式需要特殊处理,这里先跳过
|
return;
|
}
|
|
// 创建Blob对象
|
const blob = new Blob([fileContent], { type: mimeType });
|
|
// 创建下载链接
|
const url = window.URL.createObjectURL(blob);
|
const link = document.createElement('a');
|
link.href = url;
|
link.download = fileName;
|
|
// 触发下载
|
document.body.appendChild(link);
|
link.click();
|
|
// 清理
|
document.body.removeChild(link);
|
window.URL.revokeObjectURL(url);
|
|
} catch (error) {
|
console.error(`生成发票文件失败: ${invoice.invoiceNo}`, error);
|
}
|
};
|
|
// 生成ZIP压缩包
|
const generateZIPFile = async () => {
|
try {
|
// 动态导入JSZip库
|
const JSZip = await import('jszip');
|
const zip = new JSZip.default();
|
|
// 添加发票文件到ZIP
|
props.invoices.forEach((invoice, index) => {
|
let fileContent, fileName;
|
|
if (downloadOptions.content.includes('invoice')) {
|
if (downloadOptions.content.includes('details')) {
|
fileContent = generateExcelContent(invoice);
|
fileName = `${getFileName(invoice, index)}.csv`;
|
} else {
|
fileContent = generateHTMLContent(invoice);
|
fileName = `${getFileName(invoice, index)}.html`;
|
}
|
zip.file(fileName, fileContent);
|
}
|
});
|
|
// 添加汇总文件
|
if (downloadOptions.content.includes('summary')) {
|
const summaryContent = generateSummaryContent();
|
zip.file('发票汇总.csv', summaryContent);
|
}
|
|
// 生成ZIP文件
|
const zipBlob = await zip.generateAsync({
|
type: 'blob',
|
compression: downloadOptions.compress ? 'DEFLATE' : 'STORE'
|
});
|
|
// 下载ZIP文件
|
const fileName = `发票批量下载_${new Date().toISOString().split('T')[0]}.zip`;
|
const url = window.URL.createObjectURL(zipBlob);
|
const link = document.createElement('a');
|
link.href = url;
|
link.download = fileName;
|
|
document.body.appendChild(link);
|
link.click();
|
|
document.body.removeChild(link);
|
window.URL.revokeObjectURL(url);
|
|
} catch (error) {
|
console.error('生成ZIP文件失败:', error);
|
ElMessage.error('ZIP文件生成失败,请检查是否安装了jszip库');
|
}
|
};
|
|
// 生成汇总文件
|
const generateSummaryFile = async () => {
|
try {
|
const summaryContent = generateSummaryContent();
|
const fileName = `发票汇总_${new Date().toISOString().split('T')[0]}.csv`;
|
|
const blob = new Blob([summaryContent], { type: 'text/csv;charset=utf-8;' });
|
const url = window.URL.createObjectURL(blob);
|
const link = document.createElement('a');
|
link.href = url;
|
link.download = fileName;
|
|
document.body.appendChild(link);
|
link.click();
|
|
document.body.removeChild(link);
|
window.URL.revokeObjectURL(url);
|
|
} catch (error) {
|
console.error('生成汇总文件失败:', error);
|
}
|
};
|
|
// 获取文件名
|
const getFileName = (invoice, index) => {
|
let fileName = '';
|
|
if (downloadOptions.naming === 'invoice_buyer') {
|
fileName = `${invoice.invoiceNo}_${invoice.buyerName}`;
|
} else if (downloadOptions.naming === 'invoice_date') {
|
fileName = `${invoice.invoiceNo}_${invoice.invoiceDate}`;
|
} else if (downloadOptions.naming === 'buyer_date') {
|
fileName = `${invoice.buyerName}_${invoice.invoiceDate}`;
|
} else if (downloadOptions.naming === 'custom') {
|
fileName = `${downloadOptions.customPrefix || '发票'}_${index + 1}`;
|
}
|
|
// 清理文件名中的特殊字符
|
return fileName.replace(/[<>:"/\\|?*]/g, '_');
|
};
|
|
// 生成HTML内容
|
const generateHTMLContent = (invoice) => {
|
const content = `<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>发票信息 - ${invoice.invoiceNo || 'N/A'}</title>
|
<style>
|
body {
|
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
margin: 0;
|
padding: 20px;
|
background-color: #f5f5f5;
|
}
|
.invoice-container {
|
max-width: 800px;
|
margin: 0 auto;
|
background: white;
|
border-radius: 8px;
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
overflow: hidden;
|
}
|
.invoice-header {
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: white;
|
padding: 30px;
|
text-align: center;
|
}
|
.invoice-header h1 {
|
margin: 0;
|
font-size: 28px;
|
font-weight: 300;
|
}
|
.invoice-header .subtitle {
|
margin-top: 10px;
|
opacity: 0.9;
|
font-size: 16px;
|
}
|
.invoice-content {
|
padding: 30px;
|
}
|
.info-section {
|
margin-bottom: 25px;
|
}
|
.info-section h3 {
|
color: #333;
|
border-bottom: 2px solid #667eea;
|
padding-bottom: 8px;
|
margin-bottom: 15px;
|
}
|
.info-grid {
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
gap: 15px;
|
}
|
.info-item {
|
display: flex;
|
align-items: center;
|
padding: 12px;
|
background: #f8f9fa;
|
border-radius: 6px;
|
border-left: 4px solid #667eea;
|
}
|
.info-label {
|
font-weight: bold;
|
color: #555;
|
min-width: 100px;
|
}
|
.info-value {
|
color: #333;
|
margin-left: 10px;
|
}
|
.amount-section {
|
background: #f8f9fa;
|
padding: 20px;
|
border-radius: 8px;
|
margin-top: 20px;
|
}
|
.amount-grid {
|
display: grid;
|
grid-template-columns: repeat(3, 1fr);
|
gap: 20px;
|
}
|
.amount-item {
|
text-align: center;
|
padding: 15px;
|
background: white;
|
border-radius: 6px;
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
}
|
.amount-label {
|
font-size: 14px;
|
color: #666;
|
margin-bottom: 8px;
|
}
|
.amount-value {
|
font-size: 24px;
|
font-weight: bold;
|
color: #667eea;
|
}
|
.footer {
|
text-align: center;
|
padding: 20px;
|
color: #666;
|
border-top: 1px solid #eee;
|
margin-top: 20px;
|
}
|
@media print {
|
body { background: white; }
|
.invoice-container { box-shadow: none; }
|
}
|
</style>
|
</head>
|
<body>
|
<div class="invoice-container">
|
<div class="invoice-header">
|
<h1>发票信息</h1>
|
<div class="subtitle">Invoice Information</div>
|
</div>
|
|
<div class="invoice-content">
|
<div class="info-section">
|
<h3>基本信息</h3>
|
<div class="info-grid">
|
<div class="info-item">
|
<span class="info-label">发票号码:</span>
|
<span class="info-value">${invoice.invoiceNo || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">发票代码:</span>
|
<span class="info-value">${invoice.invoiceCode || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">开票日期:</span>
|
<span class="info-value">${invoice.invoiceDate || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">状态:</span>
|
<span class="info-value">${getStatusText(invoice.status)}</span>
|
</div>
|
</div>
|
</div>
|
|
<div class="info-section">
|
<h3>购买方信息</h3>
|
<div class="info-grid">
|
<div class="info-item">
|
<span class="info-label">购买方名称:</span>
|
<span class="info-value">${invoice.buyerName || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">购买方税号:</span>
|
<span class="info-value">${invoice.buyerTaxNo || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">购买方地址:</span>
|
<span class="info-value">${invoice.buyerAddress || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">银行账户:</span>
|
<span class="info-value">${invoice.buyerBankAccount || 'N/A'}</span>
|
</div>
|
</div>
|
</div>
|
|
<div class="info-section">
|
<h3>销售方信息</h3>
|
<div class="info-grid">
|
<div class="info-item">
|
<span class="info-label">销售方名称:</span>
|
<span class="info-value">${invoice.sellerName || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">销售方税号:</span>
|
<span class="info-value">${invoice.sellerTaxNo || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">销售方地址:</span>
|
<span class="info-value">${invoice.sellerAddress || 'N/A'}</span>
|
</div>
|
<div class="info-item">
|
<span class="info-label">银行账户:</span>
|
<span class="info-value">${invoice.sellerBankAccount || 'N/A'}</span>
|
</div>
|
</div>
|
</div>
|
|
<div class="amount-section">
|
<h3>金额信息</h3>
|
<div class="amount-grid">
|
<div class="amount-item">
|
<div class="amount-label">金额</div>
|
<div class="amount-value">¥${(invoice.amount || 0).toFixed(2)}</div>
|
</div>
|
<div class="info-item">
|
<div class="amount-label">税额</div>
|
<div class="amount-value">¥${(invoice.taxAmount || 0).toFixed(2)}</div>
|
</div>
|
<div class="amount-item">
|
<div class="amount-label">价税合计</div>
|
<div class="amount-value">¥${(invoice.totalAmount || 0).toFixed(2)}</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="footer">
|
<p>生成时间: ${new Date().toLocaleString()}</p>
|
<p>此文件由系统自动生成</p>
|
</div>
|
</div>
|
</div>
|
</body>
|
</html>`;
|
|
return content;
|
};
|
|
// 生成Excel内容
|
const generateExcelContent = (invoice) => {
|
const content = `发票信息
|
发票号码,${invoice.invoiceNo || 'N/A'}
|
发票代码,${invoice.invoiceCode || 'N/A'}
|
开票日期,${invoice.invoiceDate || 'N/A'}
|
购买方名称,${invoice.buyerName || 'N/A'}
|
销售方名称,${invoice.sellerName || 'N/A'}
|
金额,${(invoice.amount || 0).toFixed(2)}
|
税额,${(invoice.taxAmount || 0).toFixed(2)}
|
价税合计,${(invoice.totalAmount || 0).toFixed(2)}
|
状态,${getStatusText(invoice.status)}
|
创建时间,${invoice.createTime || 'N/A'}`;
|
return content;
|
};
|
|
// 生成汇总内容
|
const generateSummaryContent = () => {
|
let content = '发票汇总报表\n';
|
content += '发票号码,发票代码,开票日期,购买方名称,销售方名称,金额,税额,价税合计,状态,创建时间\n';
|
|
props.invoices.forEach(invoice => {
|
content += `${invoice.invoiceNo || 'N/A'},${invoice.invoiceCode || 'N/A'},${invoice.invoiceDate || 'N/A'},${invoice.buyerName || 'N/A'},${invoice.sellerName || 'N/A'},${(invoice.amount || 0).toFixed(2)},${(invoice.taxAmount || 0).toFixed(2)},${(invoice.totalAmount || 0).toFixed(2)},${getStatusText(invoice.status)},${invoice.createTime || 'N/A'}\n`;
|
});
|
|
// 添加合计行
|
const totalAmount = props.invoices.reduce((sum, item) => sum + (item.amount || 0), 0);
|
const totalTaxAmount = props.invoices.reduce((sum, item) => sum + (item.taxAmount || 0), 0);
|
const totalSum = props.invoices.reduce((sum, item) => sum + (item.totalAmount || 0), 0);
|
|
content += `合计,,,,,${totalAmount.toFixed(2)},${totalTaxAmount.toFixed(2)},${totalSum.toFixed(2)},,`;
|
|
return content;
|
};
|
</script>
|
|
<style scoped>
|
.batch-download-container {
|
padding: 0;
|
}
|
|
.invoice-list,
|
.download-options,
|
.download-progress {
|
margin-bottom: 20px;
|
}
|
|
.invoice-list:last-child,
|
.download-options:last-child,
|
.download-progress:last-child {
|
margin-bottom: 0;
|
}
|
|
.card-header {
|
font-weight: bold;
|
font-size: 16px;
|
display: flex;
|
align-items: center;
|
}
|
|
.progress-content {
|
padding: 20px 0;
|
text-align: center;
|
}
|
|
.progress-text {
|
margin-top: 15px;
|
font-size: 16px;
|
font-weight: bold;
|
color: #409eff;
|
}
|
|
.progress-detail {
|
margin-top: 10px;
|
color: #606266;
|
font-size: 14px;
|
}
|
|
.dialog-footer {
|
text-align: right;
|
}
|
|
.el-radio-group {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
|
.el-checkbox-group {
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
}
|
</style>
|