From 92e38481fc2f68dcd540434c6428d790b470a84d Mon Sep 17 00:00:00 2001 From: spring <2396852758@qq.com> Date: 星期四, 28 八月 2025 15:42:44 +0800 Subject: [PATCH] 发票协同 --- src/views/invoiceCollaboration/components/InvoiceViewDialog.vue | 291 +++++ src/views/invoiceCollaboration/index.vue | 554 +++++++++++ package.json | 1 src/views/invoiceCollaboration/components/TaxControlSyncDialog.vue | 362 +++++++ src/views/invoiceCollaboration/components/BatchDownloadDialog.vue | 704 ++++++++++++++ src/views/invoiceCollaboration/components/InvoiceDialog.vue | 484 +++++++++ src/views/invoiceCollaboration/components/DownloadDialog.vue | 580 +++++++++++ 7 files changed, 2,976 insertions(+), 0 deletions(-) diff --git a/package.json b/package.json index b241ca8..6d2877a 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "js-beautify": "1.14.11", "js-cookie": "3.0.5", "jsencrypt": "3.3.2", + "jszip": "^3.10.1", "nprogress": "0.2.0", "pinia": "2.1.7", "print-js": "^1.6.0", diff --git a/src/views/invoiceCollaboration/components/BatchDownloadDialog.vue b/src/views/invoiceCollaboration/components/BatchDownloadDialog.vue new file mode 100644 index 0000000..18e40a0 --- /dev/null +++ b/src/views/invoiceCollaboration/components/BatchDownloadDialog.vue @@ -0,0 +1,704 @@ +<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">鍙戠エ姝f湰</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">璁剧疆瑙e帇瀵嗙爜</el-checkbox> + </el-form-item> + + <!-- 瑙e帇瀵嗙爜 --> + <el-form-item v-if="downloadOptions.password" label="瑙e帇瀵嗙爜"> + <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 ? '涓嬭浇瀹屾垚' : `姝e湪涓嬭浇... ${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 { + // 鍔ㄦ�佸鍏SZip搴� + const JSZip = await import('jszip'); + const zip = new JSZip.default(); + + // 娣诲姞鍙戠エ鏂囦欢鍒癦IP + 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">鍙戠エ浠g爜:</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'} +鍙戠エ浠g爜,${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 += '鍙戠エ鍙风爜,鍙戠エ浠g爜,寮�绁ㄦ棩鏈�,璐拱鏂瑰悕绉�,閿�鍞柟鍚嶇О,閲戦,绋庨,浠风◣鍚堣,鐘舵��,鍒涘缓鏃堕棿\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> diff --git a/src/views/invoiceCollaboration/components/DownloadDialog.vue b/src/views/invoiceCollaboration/components/DownloadDialog.vue new file mode 100644 index 0000000..bd0ade3 --- /dev/null +++ b/src/views/invoiceCollaboration/components/DownloadDialog.vue @@ -0,0 +1,580 @@ +<template> + <el-dialog + :model-value="dialogVisible" + @update:model-value="$emit('update:dialogVisible', $event)" + title="涓嬭浇鍙戠エ" + width="600px" + :close-on-click-modal="false" + > + <div class="download-container"> + <!-- 鍙戠エ淇℃伅 --> + <el-card class="invoice-info" shadow="never"> + <template #header> + <div class="card-header"> + <span>鍙戠エ淇℃伅</span> + </div> + </template> + + <el-descriptions :column="2" border> + <el-descriptions-item label="鍙戠エ鍙风爜">{{ invoice.invoiceNo || '-' }}</el-descriptions-item> + <el-descriptions-item label="鍙戠エ浠g爜">{{ invoice.invoiceCode || '-' }}</el-descriptions-item> + <el-descriptions-item label="寮�绁ㄦ棩鏈�">{{ invoice.invoiceDate || '-' }}</el-descriptions-item> + <el-descriptions-item label="璐拱鏂�">{{ invoice.buyerName || '-' }}</el-descriptions-item> + <el-descriptions-item label="閲戦">{{ (invoice.amount || 0).toFixed(2) }} 鍏�</el-descriptions-item> + <el-descriptions-item label="浠风◣鍚堣">{{ (invoice.totalAmount || 0).toFixed(2) }} 鍏�</el-descriptions-item> + </el-descriptions> + </el-card> + + <!-- 涓嬭浇閫夐」 --> + <el-card class="download-options" shadow="never"> + <template #header> + <div class="card-header"> + <span>涓嬭浇閫夐」</span> + </div> + </template> + + <el-form :model="downloadOptions" label-width="120px"> + <el-form-item label="鏂囦欢鏍煎紡"> + <el-radio-group v-model="downloadOptions.format"> + <el-radio label="pdf">PDF鏍煎紡</el-radio> + <el-radio label="excel">Excel鏍煎紡</el-radio> + <el-radio label="image">鍥剧墖鏍煎紡</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="basic">鍩烘湰淇℃伅</el-checkbox> + <el-checkbox label="buyer">璐拱鏂逛俊鎭�</el-checkbox> + <el-checkbox label="seller">閿�鍞柟淇℃伅</el-checkbox> + <el-checkbox label="items">鍟嗗搧鏄庣粏</el-checkbox> + <el-checkbox label="summary">鍚堣淇℃伅</el-checkbox> + </el-checkbox-group> + </el-form-item> + + <el-form-item label="鏂囦欢鍛藉悕"> + <el-input + v-model="downloadOptions.fileName" + placeholder="璇疯緭鍏ユ枃浠跺悕锛堜笉鍖呭惈鎵╁睍鍚嶏級" + style="width: 100%" + /> + </el-form-item> + + <el-form-item label="姘村嵃璁剧疆"> + <el-switch + v-model="downloadOptions.watermark" + active-text="娣诲姞姘村嵃" + inactive-text="鏃犳按鍗�" + /> + </el-form-item> + + <el-form-item label="鍘嬬缉璁剧疆" v-if="downloadOptions.format === 'image'"> + <el-select v-model="downloadOptions.compression" placeholder="閫夋嫨鍘嬬缉璐ㄩ噺" style="width: 100%"> + <el-option label="楂樿川閲忥紙鏂囦欢杈冨ぇ锛�" value="high" /> + <el-option label="涓瓑璐ㄩ噺" value="medium" /> + <el-option label="浣庤川閲忥紙鏂囦欢杈冨皬锛�" value="low" /> + </el-select> + </el-form-item> + </el-form> + </el-card> + + <!-- 涓嬭浇杩涘害 --> + <el-card v-if="downloading" class="download-progress" shadow="never"> + <template #header> + <div class="card-header"> + <span>涓嬭浇杩涘害</span> + </div> + </template> + + <div class="progress-content"> + <el-progress + :percentage="downloadProgress" + :status="downloadProgress === 100 ? 'success' : ''" + :stroke-width="20" + /> + <div class="progress-text"> + {{ downloadProgress === 100 ? '涓嬭浇瀹屾垚' : `姝e湪涓嬭浇... ${downloadProgress}%` }} + </div> + <div class="progress-detail" v-if="downloadProgress < 100"> + <span>姝e湪鐢熸垚{{ getFormatText(downloadOptions.format) }}鏂囦欢...</span> + </div> + </div> + </el-card> + </div> + + <template #footer> + <div class="dialog-footer"> + <el-button @click="handleClose" :disabled="downloading">鍙栨秷</el-button> + <el-button + type="primary" + @click="handleDownload" + :loading="downloading" + :disabled="!canDownload" + > + {{ downloading ? '涓嬭浇涓�...' : '寮�濮嬩笅杞�' }} + </el-button> + </div> + </template> + </el-dialog> +</template> + +<script setup> +import { ref, reactive, computed, watch } from "vue"; +import { ElMessage } from "element-plus"; + +// Props +const props = defineProps({ + dialogVisible: { + type: Boolean, + default: false + }, + invoice: { + type: Object, + default: () => ({}) + } +}); + +// Emits +const emit = defineEmits(['update:dialogVisible', 'success']); + +// 鍝嶅簲寮忔暟鎹� +const downloading = ref(false); +const downloadProgress = ref(0); + +// 涓嬭浇閫夐」 +const downloadOptions = reactive({ + format: 'pdf', + content: ['basic', 'buyer', 'seller', 'items', 'summary'], + fileName: '', + watermark: true, + compression: 'medium' +}); + +// 璁$畻灞炴�� +const canDownload = computed(() => { + return downloadOptions.content.length > 0 && downloadOptions.fileName.trim() !== ''; +}); + +// 鐩戝惉鍙戠エ鍙樺寲锛岃嚜鍔ㄨ缃枃浠跺悕 +watch(() => props.invoice, (newInvoice) => { + if (newInvoice && newInvoice.invoiceNo) { + downloadOptions.fileName = `${newInvoice.invoiceNo}_${newInvoice.invoiceDate}`; + } +}, { immediate: true }); + +// 鑾峰彇鏍煎紡鏂囨湰 +const getFormatText = (format) => { + const formatMap = { + pdf: 'PDF', + excel: 'Excel', + image: '鍥剧墖' + }; + return formatMap[format] || format; +}; + +// 鍏抽棴瀵硅瘽妗� +const handleClose = () => { + if (downloading.value) { + ElMessage.warning("涓嬭浇杩涜涓紝璇风瓑寰呭畬鎴�"); + return; + } + emit('update:dialogVisible', false); + // 閲嶇疆鐘舵�� + downloading.value = false; + downloadProgress.value = 0; +}; + +// 寮�濮嬩笅杞� +const handleDownload = async () => { + if (!canDownload.value) { + ElMessage.warning("璇峰畬鍠勪笅杞介�夐」"); + return; + } + + downloading.value = true; + downloadProgress.value = 0; + + try { + // 妯℃嫙涓嬭浇杩囩▼ + const steps = [ + { progress: 20, message: "姝e湪楠岃瘉鍙戠エ淇℃伅..." }, + { progress: 40, message: "姝e湪鐢熸垚鏂囦欢鍐呭..." }, + { progress: 60, message: "姝e湪搴旂敤鏍煎紡璁剧疆..." }, + { progress: 80, message: "姝e湪鐢熸垚鏂囦欢..." }, + { progress: 100, message: "涓嬭浇瀹屾垚" } + ]; + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + await new Promise(resolve => { + setTimeout(() => { + downloadProgress.value = step.progress; + resolve(); + }, 800); + }); + } + + // 鐢熸垚鐪熷疄鐨勬枃浠跺苟涓嬭浇 + await generateAndDownloadFile(); + + } catch (error) { + ElMessage.error("涓嬭浇澶辫触锛岃閲嶈瘯"); + downloading.value = false; + downloadProgress.value = 0; + } +}; + +// 鐢熸垚骞朵笅杞芥枃浠� +const generateAndDownloadFile = async () => { + try { + let fileContent, fileName, mimeType; + + if (downloadOptions.format === 'pdf') { + // 鐢熸垚PDF鍐呭锛堟ā鎷燂級 + fileContent = generatePDFContent(); + fileName = `${downloadOptions.fileName}.pdf`; + mimeType = 'application/pdf'; + } else if (downloadOptions.format === 'excel') { + // 鐢熸垚Excel鍐呭锛圕SV鏍煎紡锛屽吋瀹规�ф洿濂斤級 + fileContent = generateExcelContent(); + fileName = `${downloadOptions.fileName}.csv`; + mimeType = 'text/csv'; + } else if (downloadOptions.format === 'image') { + // 鐢熸垚鍥剧墖鍐呭锛圫VG鏍煎紡锛� + fileContent = generateImageContent(); + fileName = `${downloadOptions.fileName}.svg`; + mimeType = 'image/svg+xml'; + } else if (downloadOptions.format === 'zip') { + // 鐢熸垚ZIP鍘嬬缉鍖� + await generateZIPFile(); + return; // ZIP涓嬭浇瀹屾垚鍚庣洿鎺ヨ繑鍥� + } + + // 鍒涘缓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); + + ElMessage.success(`鍙戠エ涓嬭浇鎴愬姛锛佹枃浠跺悕锛�${fileName}`); + emit('success'); + + // 寤惰繜鍏抽棴瀵硅瘽妗� + setTimeout(() => { + handleClose(); + }, 1500); + + } catch (error) { + console.error('鏂囦欢鐢熸垚澶辫触:', error); + ElMessage.error("鏂囦欢鐢熸垚澶辫触锛岃閲嶈瘯"); + } +}; + +// 鐢熸垚ZIP鍘嬬缉鍖� +const generateZIPFile = async () => { + try { + // 鍔ㄦ�佸鍏SZip搴� + const JSZip = await import('jszip'); + const zip = new JSZip.default(); + + // 鏍规嵁閫夋嫨鐨勫唴瀹规坊鍔犳枃浠跺埌ZIP + if (downloadOptions.content.includes('basic')) { + const basicContent = generateBasicContent(); + zip.file('鍩烘湰淇℃伅.csv', basicContent); + } + + if (downloadOptions.content.includes('buyer')) { + const buyerContent = generateBuyerContent(); + zip.file('璐拱鏂逛俊鎭�.csv', buyerContent); + } + + if (downloadOptions.content.includes('seller')) { + const sellerContent = generateSellerContent(); + zip.file('閿�鍞柟淇℃伅.csv', sellerContent); + } + + if (downloadOptions.content.includes('items')) { + const itemsContent = generateItemsContent(); + zip.file('鍟嗗搧鏄庣粏.csv', itemsContent); + } + + if (downloadOptions.content.includes('summary')) { + const summaryContent = generateSummaryContent(); + zip.file('鍚堣淇℃伅.csv', summaryContent); + } + + // 鐢熸垚ZIP鏂囦欢 + const zipBlob = await zip.generateAsync({ + type: 'blob', + compression: 'DEFLATE' + }); + + // 涓嬭浇ZIP鏂囦欢 + const fileName = `${downloadOptions.fileName}.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); + + ElMessage.success(`鍙戠エ涓嬭浇鎴愬姛锛佹枃浠跺悕锛�${fileName}`); + emit('success'); + + // 寤惰繜鍏抽棴瀵硅瘽妗� + setTimeout(() => { + handleClose(); + }, 1500); + + } catch (error) { + console.error('ZIP鏂囦欢鐢熸垚澶辫触:', error); + ElMessage.error('ZIP鏂囦欢鐢熸垚澶辫触锛岃妫�鏌ユ槸鍚﹀畨瑁呬簡jszip搴�'); + } +}; + +// 鐢熸垚PDF鍐呭锛堟ā鎷燂級 +const generatePDFContent = () => { + const invoice = props.invoice; + const content = ` +%PDF-1.4 +1 0 obj +<< +/Type /Catalog +/Pages 2 0 R +>> +endobj + +2 0 obj +<< +/Type /Pages +/Kids [3 0 R] +/Count 1 +>> +endobj + +3 0 obj +<< +/Type /Page +/Parent 2 0 R +/MediaBox [0 0 612 792] +/Contents 4 0 R +>> +endobj + +4 0 obj +<< +/Length 200 +>> +stream +BT +/F1 12 Tf +72 720 Td +(鍙戠エ鍙风爜: ${invoice.invoiceNo || 'N/A'}) Tj +0 -20 Td +(寮�绁ㄦ棩鏈�: ${invoice.invoiceDate || 'N/A'}) Tj +0 -20 Td +(璐拱鏂�: ${invoice.buyerName || 'N/A'}) Tj +0 -20 Td +(閲戦: ${(invoice.amount || 0).toFixed(2)} 鍏�) Tj +0 -20 Td +(浠风◣鍚堣: ${(invoice.totalAmount || 0).toFixed(2)} 鍏�) Tj +ET +endstream +endobj + +xref +0 5 +0000000000 65535 f +0000000009 00000 n +0000000058 00000 n +0000000115 00000 n +0000000204 00000 n +trailer +<< +/Size 5 +/Root 1 0 R +>> +startxref +295 +%%EOF + `; + return content; +}; + +// 鐢熸垚Excel鍐呭锛圕SV鏍煎紡锛� +const generateExcelContent = () => { + const invoice = props.invoice; + const content = `鍙戠エ淇℃伅 +鍙戠エ鍙风爜,${invoice.invoiceNo || 'N/A'} +鍙戠エ浠g爜,${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; +}; + +// 鐢熸垚鍥剧墖鍐呭锛圫VG鏍煎紡锛� +const generateImageContent = () => { + const invoice = props.invoice; + const content = `<?xml version="1.0" encoding="UTF-8"?> +<svg width="600" height="400" xmlns="http://www.w3.org/2000/svg"> + <rect width="600" height="400" fill="white" stroke="black" stroke-width="2"/> + <text x="20" y="40" font-family="Arial" font-size="24" fill="black">鍙戠エ</text> + <text x="20" y="80" font-family="Arial" font-size="16" fill="black">鍙戠エ鍙风爜: ${invoice.invoiceNo || 'N/A'}</text> + <text x="20" y="110" font-family="Arial" font-size="16" fill="black">寮�绁ㄦ棩鏈�: ${invoice.invoiceDate || 'N/A'}</text> + <text x="20" y="140" font-family="Arial" font-size="16" fill="black">璐拱鏂�: ${invoice.buyerName || 'N/A'}</text> + <text x="20" y="170" font-family="Arial" font-size="16" fill="black">閲戦: ${(invoice.amount || 0).toFixed(2)} 鍏�</text> + <text x="20" y="200" font-family="Arial" font-size="16" fill="black">浠风◣鍚堣: ${(invoice.totalAmount || 0).toFixed(2)} 鍏�</text> + <text x="20" y="230" font-family="Arial" font-size="16" fill="black">鐘舵��: ${getStatusText(invoice.status)}</text> +</svg>`; + return content; +}; + +// 鑾峰彇鐘舵�佹枃鏈� +const getStatusText = (status) => { + const statusMap = { + 'draft': '鑽夌', + 'pending': '寰呭紑绁�', + 'issuing': '寮�绁ㄤ腑', + 'issued': '宸插紑绁�', + 'failed': '寮�绁ㄥけ璐�', + 'cancelled': '宸蹭綔搴�' + }; + return statusMap[status] || status; +}; + +// 鐢熸垚鍩烘湰淇℃伅鍐呭 +const generateBasicContent = () => { + const invoice = props.invoice; + return `鍩烘湰淇℃伅 +鍙戠エ鍙风爜,${invoice.invoiceNo || 'N/A'} +鍙戠エ浠g爜,${invoice.invoiceCode || 'N/A'} +寮�绁ㄦ棩鏈�,${invoice.invoiceDate || 'N/A'} +鐘舵��,${getStatusText(invoice.status)} +鍒涘缓鏃堕棿,${invoice.createTime || 'N/A'}`; +}; + +// 鐢熸垚璐拱鏂逛俊鎭唴瀹� +const generateBuyerContent = () => { + const invoice = props.invoice; + return `璐拱鏂逛俊鎭� +璐拱鏂瑰悕绉�,${invoice.buyerName || 'N/A'} +璐拱鏂圭◣鍙�,${invoice.buyerTaxNo || 'N/A'} +璐拱鏂瑰湴鍧�,${invoice.buyerAddress || 'N/A'} +璐拱鏂归摱琛岃处鎴�,${invoice.buyerBankAccount || 'N/A'}`; +}; + +// 鐢熸垚閿�鍞柟淇℃伅鍐呭 +const generateSellerContent = () => { + const invoice = props.invoice; + return `閿�鍞柟淇℃伅 +閿�鍞柟鍚嶇О,${invoice.sellerName || 'N/A'} +閿�鍞柟绋庡彿,${invoice.sellerTaxNo || 'N/A'} +閿�鍞柟鍦板潃,${invoice.sellerAddress || 'N/A'} +閿�鍞柟閾惰璐︽埛,${invoice.sellerBankAccount || 'N/A'}`; +}; + +// 鐢熸垚鍟嗗搧鏄庣粏鍐呭 +const generateItemsContent = () => { + const invoice = props.invoice; + if (!invoice.items || invoice.items.length === 0) { + return `鍟嗗搧鏄庣粏 +鏆傛棤鍟嗗搧鏄庣粏淇℃伅`; + } + + let content = '鍟嗗搧鏄庣粏\n鍟嗗搧鍚嶇О,瑙勬牸鍨嬪彿,鏁伴噺,鍗曚环,閲戦,绋庣巼,绋庨,浠风◣鍚堣\n'; + invoice.items.forEach(item => { + content += `${item.name || 'N/A'},${item.spec || 'N/A'},${item.quantity || 0},${(item.price || 0).toFixed(2)},${(item.amount || 0).toFixed(2)},${(item.taxRate || 0).toFixed(2)}%,${(item.taxAmount || 0).toFixed(2)},${(item.totalAmount || 0).toFixed(2)}\n`; + }); + + return content; +}; + +// 鐢熸垚鍚堣淇℃伅鍐呭 +const generateSummaryContent = () => { + const invoice = props.invoice; + return `鍚堣淇℃伅 +閲戦鍚堣,${(invoice.amount || 0).toFixed(2)} 鍏� +绋庨鍚堣,${(invoice.taxAmount || 0).toFixed(2)} 鍏� +浠风◣鍚堣,${(invoice.totalAmount || 0).toFixed(2)} 鍏� +澶囨敞,${invoice.remark || 'N/A'}`; +}; +</script> + +<style scoped> +.download-container { + padding: 0; +} + +.invoice-info, +.download-options, +.download-progress { + margin-bottom: 20px; +} + +.invoice-info:last-child, +.download-options:last-child, +.download-progress:last-child { + margin-bottom: 0; +} + +.card-header { + font-weight: bold; + font-size: 16px; +} + +.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-checkbox-group { + display: flex; + flex-direction: column; + gap: 10px; +} + +.el-radio-group { + display: flex; + flex-direction: column; + gap: 10px; +} +</style> \ No newline at end of file diff --git a/src/views/invoiceCollaboration/components/InvoiceDialog.vue b/src/views/invoiceCollaboration/components/InvoiceDialog.vue new file mode 100644 index 0000000..3b8f2a6 --- /dev/null +++ b/src/views/invoiceCollaboration/components/InvoiceDialog.vue @@ -0,0 +1,484 @@ +<template> + <el-dialog + :model-value="dialogFormVisible" + @update:model-value="$emit('update:dialogFormVisible', $event)" + :title="title" + width="1200px" + :close-on-click-modal="false" + @close="handleClose" + > + <el-form + ref="formRef" + :model="formData" + :rules="rules" + label-width="120px" + class="invoice-form" + > + <!-- 璐拱鏂逛俊鎭� --> + <el-card class="buyer-card" shadow="never"> + <template #header> + <div class="card-header"> + <span>璐拱鏂逛俊鎭�</span> + </div> + </template> + + <el-row :gutter="20"> + <el-col :span="12"> + <el-form-item label="璐拱鏂瑰悕绉�" prop="buyerName"> + <el-input + v-model="formData.buyerName" + placeholder="璇疯緭鍏ヨ喘涔版柟鍚嶇О" + style="width: 100%" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="绾崇◣浜鸿瘑鍒彿" prop="buyerTaxNo"> + <el-input + v-model="formData.buyerTaxNo" + placeholder="璇疯緭鍏ョ撼绋庝汉璇嗗埆鍙�" + style="width: 100%" + /> + </el-form-item> + </el-col> + </el-row> + + <el-row :gutter="20"> + <el-col :span="12"> + <el-form-item label="鍦板潃鐢佃瘽" prop="buyerAddress"> + <el-input + v-model="formData.buyerAddress" + placeholder="璇疯緭鍏ュ湴鍧�鐢佃瘽" + style="width: 100%" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="寮�鎴疯鍙婅处鍙�" prop="buyerBankAccount"> + <el-input + v-model="formData.buyerBankAccount" + placeholder="璇疯緭鍏ュ紑鎴疯鍙婅处鍙�" + style="width: 100%" + /> + </el-form-item> + </el-col> + </el-row> + </el-card> + + <!-- 閿�鍞柟淇℃伅 --> + <el-card class="seller-card" shadow="never"> + <template #header> + <div class="card-header"> + <span>閿�鍞柟淇℃伅</span> + </div> + </template> + + <el-row :gutter="20"> + <el-col :span="12"> + <el-form-item label="閿�鍞柟鍚嶇О" prop="sellerName"> + <el-input + v-model="formData.sellerName" + placeholder="璇疯緭鍏ラ攢鍞柟鍚嶇О" + style="width: 100%" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="绾崇◣浜鸿瘑鍒彿" prop="sellerTaxNo"> + <el-input + v-model="formData.sellerTaxNo" + placeholder="璇疯緭鍏ョ撼绋庝汉璇嗗埆鍙�" + style="width: 100%" + /> + </el-form-item> + </el-col> + </el-row> + + <el-row :gutter="20"> + <el-col :span="12"> + <el-form-item label="鍦板潃鐢佃瘽" prop="sellerAddress"> + <el-input + v-model="formData.sellerAddress" + placeholder="璇疯緭鍏ュ湴鍧�鐢佃瘽" + style="width: 100%" + /> + </el-form-item> + </el-col> + <el-col :span="12"> + <el-form-item label="寮�鎴疯鍙婅处鍙�" prop="sellerBankAccount"> + <el-input + v-model="formData.sellerBankAccount" + placeholder="璇疯緭鍏ュ紑鎴疯鍙婅处鍙�" + style="width: 100%" + /> + </el-form-item> + </el-col> + </el-row> + </el-card> + + <!-- 鍟嗗搧鏄庣粏 --> + <el-card class="items-card" shadow="never"> + <template #header> + <div class="card-header"> + <span>鍟嗗搧鏄庣粏</span> + <el-button type="primary" size="small" @click="addItem"> + 娣诲姞鍟嗗搧 + </el-button> + </div> + </template> + + <el-table :data="formData.items" border style="width: 100%"> + <el-table-column label="鍟嗗搧鍚嶇О" width="200"> + <template #default="scope"> + <el-input + v-model="scope.row.name" + placeholder="鍟嗗搧鍚嶇О" + style="width: 100%" + /> + </template> + </el-table-column> + <el-table-column label="瑙勬牸鍨嬪彿" width="150"> + <template #default="scope"> + <el-input + v-model="scope.row.specification" + placeholder="瑙勬牸鍨嬪彿" + style="width: 100%" + /> + </template> + </el-table-column> + <el-table-column label="鍗曚綅" width="100"> + <template #default="scope"> + <el-input + v-model="scope.row.unit" + placeholder="鍗曚綅" + style="width: 100%" + /> + </template> + </el-table-column> + <el-table-column label="鏁伴噺" width="120"> + <template #default="scope"> + <el-input + v-model.number="scope.row.quantity" + placeholder="鏁伴噺" + type="number" + @input="calculateItemAmount(scope.$index)" + style="width: 100%" + /> + </template> + </el-table-column> + <el-table-column label="鍗曚环" width="120"> + <template #default="scope"> + <el-input + v-model.number="scope.row.unitPrice" + placeholder="鍗曚环" + type="number" + @input="calculateItemAmount(scope.$index)" + style="width: 100%" + > + <template v-slot:suffix> + <span>鍏�</span> + </template> + </el-input> + </template> + </el-table-column> + <el-table-column label="閲戦" width="120"> + <template #default="scope"> + <span>{{ (scope.row.amount || 0).toFixed(2) }} 鍏�</span> + </template> + </el-table-column> + <el-table-column label="绋庣巼" width="120"> + <template #default="scope"> + <el-select + v-model="scope.row.taxRate" + placeholder="閫夋嫨绋庣巼" + @change="calculateItemAmount(scope.$index)" + style="width: 100%" + > + <el-option label="0%" value="0" /> + <el-option label="1%" value="0.01" /> + <el-option label="3%" value="0.03" /> + <el-option label="6%" value="0.06" /> + <el-option label="9%" value="0.09" /> + <el-option label="13%" value="0.13" /> + </el-select> + </template> + </el-table-column> + <el-table-column label="绋庨" width="120"> + <template #default="scope"> + <span>{{ (scope.row.taxAmount || 0).toFixed(2) }} 鍏�</span> + </template> + </el-table-column> + <el-table-column label="浠风◣鍚堣" width="120"> + <template #default="scope"> + <span>{{ (scope.row.totalAmount || 0).toFixed(2) }} 鍏�</span> + </template> + </el-table-column> + <el-table-column label="鎿嶄綔" width="80"> + <template #default="scope"> + <el-button + type="danger" + size="small" + @click="removeItem(scope.$index)" + > + 鍒犻櫎 + </el-button> + </template> + </el-table-column> + </el-table> + + <!-- 鍚堣淇℃伅 --> + <div class="summary-info"> + <el-row :gutter="20"> + <el-col :span="6"> + <span class="summary-label">閲戦鍚堣锛�</span> + <span class="summary-value">{{ totalAmount.toFixed(2) }} 鍏�</span> + </el-col> + <el-col :span="6"> + <span class="summary-label">绋庨鍚堣锛�</span> + <span class="summary-value">{{ totalTaxAmount.toFixed(2) }} 鍏�</span> + </el-col> + <el-col :span="6"> + <span class="summary-label">浠风◣鍚堣锛�</span> + <span class="summary-value">{{ totalTotalAmount.toFixed(2) }} 鍏�</span> + </el-col> + </el-row> + </div> + </el-card> + + <!-- 澶囨敞淇℃伅 --> + <el-form-item label="澶囨敞" prop="remark"> + <el-input + v-model="formData.remark" + type="textarea" + :rows="3" + placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" + /> + </el-form-item> + </el-form> + + <template #footer> + <div class="dialog-footer"> + <el-button @click="handleClose">鍙栨秷</el-button> + <el-button type="primary" @click="handleSubmit" :loading="submitLoading"> + 鎻愪氦 + </el-button> + </div> + </template> + </el-dialog> +</template> + +<script setup> +import { ref, reactive, watch, computed } from "vue"; +import { ElMessage } from "element-plus"; + +// Props +const props = defineProps({ + dialogFormVisible: { + type: Boolean, + default: false + }, + form: { + type: Object, + default: () => ({}) + }, + title: { + type: String, + default: "" + }, + isEdit: { + type: Boolean, + default: false + } +}); + +// Emits +const emit = defineEmits(['update:dialogFormVisible', 'update:form', 'submit', 'success']); + +// 鍝嶅簲寮忔暟鎹� +const formRef = ref(null); +const submitLoading = ref(false); + +// 琛ㄥ崟鏁版嵁 +const formData = reactive({ + buyerName: "", + buyerTaxNo: "", + buyerAddress: "", + buyerBankAccount: "", + sellerName: "鏈叕鍙�", + sellerTaxNo: "123456789012345678", + sellerAddress: "鍏徃鍦板潃", + sellerBankAccount: "閾惰璐︽埛", + items: [], + remark: "" +}); + +// 琛ㄥ崟楠岃瘉瑙勫垯 +const rules = { + buyerName: [ + { required: true, message: "璇疯緭鍏ヨ喘涔版柟鍚嶇О", trigger: "blur" } + ], + buyerTaxNo: [ + { required: true, message: "璇疯緭鍏ョ撼绋庝汉璇嗗埆鍙�", trigger: "blur" } + ], + items: [ + { + type: "array", + required: true, + message: "璇疯嚦灏戞坊鍔犱竴涓晢鍝�", + trigger: "change", + validator: (rule, value, callback) => { + if (!value || value.length === 0) { + callback(new Error("璇疯嚦灏戞坊鍔犱竴涓晢鍝�")); + } else { + callback(); + } + } + } + ] +}; + +// 璁$畻灞炴�� +const totalAmount = computed(() => { + return formData.items.reduce((sum, item) => sum + (item.amount || 0), 0); +}); + +const totalTaxAmount = computed(() => { + return formData.items.reduce((sum, item) => sum + (item.taxAmount || 0), 0); +}); + +const totalTotalAmount = computed(() => { + return formData.items.reduce((sum, item) => sum + (item.totalAmount || 0), 0); +}); + +// 鐩戝惉琛ㄥ崟鏁版嵁鍙樺寲 +watch(() => props.form, (newVal) => { + Object.assign(formData, newVal); + if (!formData.items || formData.items.length === 0) { + formData.items = []; + } +}, { deep: true, immediate: true }); + +// 娣诲姞鍟嗗搧 +const addItem = () => { + formData.items.push({ + name: "", + specification: "", + unit: "", + quantity: 0, + unitPrice: 0, + amount: 0, + taxRate: "0.13", + taxAmount: 0, + totalAmount: 0 + }); +}; + +// 鍒犻櫎鍟嗗搧 +const removeItem = (index) => { + formData.items.splice(index, 1); +}; + +// 璁$畻鍟嗗搧閲戦 +const calculateItemAmount = (index) => { + const item = formData.items[index]; + if (item.quantity && item.unitPrice) { + item.amount = item.quantity * item.unitPrice; + item.taxAmount = item.amount * parseFloat(item.taxRate); + item.totalAmount = item.amount + item.taxAmount; + } +}; + +// 鍏抽棴瀵硅瘽妗� +const handleClose = () => { + emit('update:dialogFormVisible', false); + formRef.value?.resetFields(); +}; + +// 鎻愪氦琛ㄥ崟 +const handleSubmit = async () => { + if (!formRef.value) return; + + try { + await formRef.value.validate(); + + // 楠岃瘉鍟嗗搧淇℃伅 + if (formData.items.length === 0) { + ElMessage.warning("璇疯嚦灏戞坊鍔犱竴涓晢鍝�"); + return; + } + + for (let item of formData.items) { + if (!item.name) { + ElMessage.warning("璇疯緭鍏ュ晢鍝佸悕绉�"); + return; + } + if (!item.quantity || item.quantity <= 0) { + ElMessage.warning("璇疯緭鍏ユ湁鏁堢殑鍟嗗搧鏁伴噺"); + return; + } + if (!item.unitPrice || item.unitPrice <= 0) { + ElMessage.warning("璇疯緭鍏ユ湁鏁堢殑鍟嗗搧鍗曚环"); + return; + } + } + + submitLoading.value = true; + + // 妯℃嫙鎻愪氦 + setTimeout(() => { + submitLoading.value = false; + ElMessage.success("鎻愪氦鎴愬姛"); + emit('submit', { ...formData }); + handleClose(); + }, 1000); + + } catch (error) { + console.error('琛ㄥ崟楠岃瘉澶辫触:', error); + } +}; +</script> + +<style scoped> +.invoice-form { + padding: 20px 0; +} + +.buyer-card, +.seller-card, +.items-card { + margin-bottom: 20px; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: bold; +} + +.summary-info { + margin-top: 15px; + padding: 15px; + background-color: #f5f7fa; + border-radius: 4px; +} + +.summary-label { + font-weight: bold; + margin-right: 10px; +} + +.summary-value { + color: #409eff; + font-size: 16px; + font-weight: bold; +} + +.dialog-footer { + text-align: right; +} + +.el-table { + margin-top: 10px; +} +</style> \ No newline at end of file diff --git a/src/views/invoiceCollaboration/components/InvoiceViewDialog.vue b/src/views/invoiceCollaboration/components/InvoiceViewDialog.vue new file mode 100644 index 0000000..72e7091 --- /dev/null +++ b/src/views/invoiceCollaboration/components/InvoiceViewDialog.vue @@ -0,0 +1,291 @@ +<template> + <el-dialog + :model-value="dialogViewVisible" + @update:model-value="$emit('update:dialogViewVisible', $event)" + :title="title" + width="1000px" + :close-on-click-modal="false" + > + <div class="invoice-view"> + <!-- 鍩烘湰淇℃伅 --> + <el-card class="basic-info" shadow="never"> + <template #header> + <div class="card-header"> + <span>鍩烘湰淇℃伅</span> + </div> + </template> + + <el-descriptions :column="2" border> + <el-descriptions-item label="鍙戠エ鍙风爜">{{ form.invoiceNo || '-' }}</el-descriptions-item> + <el-descriptions-item label="鍙戠エ浠g爜">{{ form.invoiceCode || '-' }}</el-descriptions-item> + <el-descriptions-item label="寮�绁ㄦ棩鏈�">{{ form.invoiceDate || '-' }}</el-descriptions-item> + <el-descriptions-item label="寮�绁ㄧ姸鎬�"> + <el-tag :type="getStatusType(form.status)"> + {{ getStatusText(form.status) }} + </el-tag> + </el-descriptions-item> + <el-descriptions-item label="绋庢帶骞冲彴鐘舵��"> + <el-tag :type="getTaxControlStatusType(form.taxControlStatus)"> + {{ getTaxControlStatusText(form.taxControlStatus) }} + </el-tag> + </el-descriptions-item> + <el-descriptions-item label="鍒涘缓鏃堕棿">{{ form.createTime || '-' }}</el-descriptions-item> + </el-descriptions> + </el-card> + + <!-- 璐拱鏂逛俊鎭� --> + <el-card class="buyer-info" shadow="never"> + <template #header> + <div class="card-header"> + <span>璐拱鏂逛俊鎭�</span> + </div> + </template> + + <el-descriptions :column="2" border> + <el-descriptions-item label="璐拱鏂瑰悕绉�">{{ form.buyerName || '-' }}</el-descriptions-item> + <el-descriptions-item label="绾崇◣浜鸿瘑鍒彿">{{ form.buyerTaxNo || '-' }}</el-descriptions-item> + <el-descriptions-item label="鍦板潃鐢佃瘽">{{ form.buyerAddress || '-' }}</el-descriptions-item> + <el-descriptions-item label="寮�鎴疯鍙婅处鍙�">{{ form.buyerBankAccount || '-' }}</el-descriptions-item> + </el-descriptions> + </el-card> + + <!-- 閿�鍞柟淇℃伅 --> + <el-card class="seller-info" shadow="never"> + <template #header> + <div class="card-header"> + <span>閿�鍞柟淇℃伅</span> + </div> + </template> + + <el-descriptions :column="2" border> + <el-descriptions-item label="閿�鍞柟鍚嶇О">{{ form.sellerName || '-' }}</el-descriptions-item> + <el-descriptions-item label="绾崇◣浜鸿瘑鍒彿">{{ form.sellerTaxNo || '-' }}</el-descriptions-item> + <el-descriptions-item label="鍦板潃鐢佃瘽">{{ form.sellerAddress || '-' }}</el-descriptions-item> + <el-descriptions-item label="寮�鎴疯鍙婅处鍙�">{{ form.sellerBankAccount || '-' }}</el-descriptions-item> + </el-descriptions> + </el-card> + + <!-- 鍟嗗搧鏄庣粏 --> + <el-card class="items-info" shadow="never"> + <template #header> + <div class="card-header"> + <span>鍟嗗搧鏄庣粏</span> + </div> + </template> + + <el-table :data="form.items || []" border style="width: 100%"> + <el-table-column label="鍟嗗搧鍚嶇О" prop="name" width="200" /> + <el-table-column label="瑙勬牸鍨嬪彿" prop="specification" width="150" /> + <el-table-column label="鍗曚綅" prop="unit" width="100" /> + <el-table-column label="鏁伴噺" prop="quantity" width="100" /> + <el-table-column label="鍗曚环" prop="unitPrice" width="120"> + <template #default="scope"> + {{ scope.row.unitPrice }} 鍏� + </template> + </el-table-column> + <el-table-column label="閲戦" prop="amount" width="120"> + <template #default="scope"> + {{ (scope.row.amount || 0).toFixed(2) }} 鍏� + </template> + </el-table-column> + <el-table-column label="绋庣巼" prop="taxRate" width="100"> + <template #default="scope"> + {{ (parseFloat(scope.row.taxRate || 0) * 100).toFixed(0) }}% + </template> + </el-table-column> + <el-table-column label="绋庨" prop="taxAmount" width="120"> + <template #default="scope"> + {{ (scope.row.taxAmount || 0).toFixed(2) }} 鍏� + </template> + </el-table-column> + <el-table-column label="浠风◣鍚堣" prop="totalAmount" width="120"> + <template #default="scope"> + {{ (scope.row.totalAmount || 0).toFixed(2) }} 鍏� + </template> + </el-table-column> + </el-table> + + <!-- 鍚堣淇℃伅 --> + <div class="summary-info"> + <el-row :gutter="20"> + <el-col :span="6"> + <span class="summary-label">閲戦鍚堣锛�</span> + <span class="summary-value">{{ getTotalAmount().toFixed(2) }} 鍏�</span> + </el-col> + <el-col :span="6"> + <span class="summary-label">绋庨鍚堣锛�</span> + <span class="summary-value">{{ getTotalTaxAmount().toFixed(2) }} 鍏�</span> + </el-col> + <el-col :span="6"> + <span class="summary-label">浠风◣鍚堣锛�</span> + <span class="summary-value">{{ getTotalTotalAmount().toFixed(2) }} 鍏�</span> + </el-col> + </el-row> + </div> + </el-card> + + <!-- 澶囨敞淇℃伅 --> + <el-card class="remark-info" shadow="never"> + <template #header> + <div class="card-header"> + <span>澶囨敞淇℃伅</span> + </div> + </template> + + <div class="remark-content"> + {{ form.remark || '鏃�' }} + </div> + </el-card> + </div> + + <template #footer> + <div class="dialog-footer"> + <el-button @click="handleClose">鍏抽棴</el-button> + </div> + </template> + </el-dialog> +</template> + +<script setup> +import { computed } from "vue"; + +// Props +const props = defineProps({ + dialogViewVisible: { + type: Boolean, + default: false + }, + form: { + type: Object, + default: () => ({}) + }, + title: { + type: String, + default: "" + } +}); + +// Emits +const emit = defineEmits(['update:dialogViewVisible']); + +// 鑾峰彇鐘舵�佺被鍨� +const getStatusType = (status) => { + const statusMap = { + draft: "", + pending: "warning", + issuing: "warning", + issued: "success", + failed: "danger", + cancelled: "info" + }; + return statusMap[status] || ""; +}; + +// 鑾峰彇鐘舵�佹枃鏈� +const getStatusText = (status) => { + const statusMap = { + draft: "鑽夌", + pending: "寰呭紑绁�", + issuing: "寮�绁ㄤ腑", + issued: "宸插紑绁�", + failed: "寮�绁ㄥけ璐�", + cancelled: "宸蹭綔搴�" + }; + return statusMap[status] || status; +}; + +// 鑾峰彇绋庢帶骞冲彴鐘舵�佺被鍨� +const getTaxControlStatusType = (status) => { + const statusMap = { + pending: "warning", + syncing: "warning", + synced: "success", + failed: "danger" + }; + return statusMap[status] || ""; +}; + +// 鑾峰彇绋庢帶骞冲彴鐘舵�佹枃鏈� +const getTaxControlStatusText = (status) => { + const statusMap = { + pending: "寰呭悓姝�", + syncing: "鍚屾涓�", + synced: "宸插悓姝�", + failed: "鍚屾澶辫触" + }; + return statusMap[status] || status; +}; + +// 璁$畻鎬婚噾棰� +const getTotalAmount = () => { + return (props.form.items || []).reduce((sum, item) => sum + (item.amount || 0), 0); +}; + +// 璁$畻鎬荤◣棰� +const getTotalTaxAmount = () => { + return (props.form.items || []).reduce((sum, item) => sum + (item.taxAmount || 0), 0); +}; + +// 璁$畻鎬讳环绋庡悎璁� +const getTotalTotalAmount = () => { + return (props.form.items || []).reduce((sum, item) => sum + (item.totalAmount || 0), 0); +}; + +// 鍏抽棴瀵硅瘽妗� +const handleClose = () => { + emit('update:dialogViewVisible', false); +}; +</script> + +<style scoped> +.invoice-view { + padding: 20px 0; +} + +.basic-info, +.buyer-info, +.seller-info, +.items-info, +.remark-info { + margin-bottom: 20px; +} + +.card-header { + font-weight: bold; + font-size: 16px; +} + +.summary-info { + margin-top: 15px; + padding: 15px; + background-color: #f5f7fa; + border-radius: 4px; +} + +.summary-label { + font-weight: bold; + margin-right: 10px; +} + +.summary-value { + color: #409eff; + font-size: 16px; + font-weight: bold; +} + +.remark-content { + padding: 15px; + background-color: #f5f7fa; + border-radius: 4px; + min-height: 60px; + line-height: 1.6; +} + +.dialog-footer { + text-align: right; +} + +.el-table { + margin-top: 10px; +} +</style> diff --git a/src/views/invoiceCollaboration/components/TaxControlSyncDialog.vue b/src/views/invoiceCollaboration/components/TaxControlSyncDialog.vue new file mode 100644 index 0000000..19bcfcd --- /dev/null +++ b/src/views/invoiceCollaboration/components/TaxControlSyncDialog.vue @@ -0,0 +1,362 @@ +<template> + <el-dialog + :model-value="dialogSyncVisible" + @update:model-value="$emit('update:dialogSyncVisible', $event)" + title="绋庢帶骞冲彴鍚屾" + width="800px" + :close-on-click-modal="false" + > + <div class="sync-container"> + <!-- 鍚屾鐘舵�� --> + <el-card class="sync-status" shadow="never"> + <template #header> + <div class="card-header"> + <span>鍚屾鐘舵��</span> + </div> + </template> + + <div class="status-content"> + <el-row :gutter="20"> + <el-col :span="8"> + <div class="status-item"> + <div class="status-icon success"> + <el-icon><Check /></el-icon> + </div> + <div class="status-text"> + <div class="status-title">宸插悓姝�</div> + <div class="status-count">{{ syncedCount }}</div> + </div> + </div> + </el-col> + <el-col :span="8"> + <div class="status-item"> + <div class="status-icon warning"> + <el-icon><Clock /></el-icon> + </div> + <div class="status-text"> + <div class="status-title">寰呭悓姝�</div> + <div class="status-count">{{ pendingCount }}</div> + </div> + </div> + </el-col> + <el-col :span="8"> + <div class="status-item"> + <div class="status-icon danger"> + <el-icon><Close /></el-icon> + </div> + <div class="status-text"> + <div class="status-title">鍚屾澶辫触</div> + <div class="status-count">{{ failedCount }}</div> + </div> + </div> + </el-col> + </el-row> + </div> + </el-card> + + <!-- 鍚屾閰嶇疆 --> + <el-card class="sync-config" shadow="never"> + <template #header> + <div class="card-header"> + <span>鍚屾閰嶇疆</span> + </div> + </template> + + <el-form :model="syncConfig" label-width="120px"> + <el-form-item label="绋庢帶骞冲彴鍦板潃"> + <el-input + v-model="syncConfig.taxControlUrl" + placeholder="璇疯緭鍏ョ◣鎺у钩鍙板湴鍧�" + style="width: 100%" + /> + </el-form-item> + <el-form-item label="鍚屾棰戠巼"> + <el-select + v-model="syncConfig.syncFrequency" + placeholder="璇烽�夋嫨鍚屾棰戠巼" + style="width: 100%" + > + <el-option label="瀹炴椂鍚屾" value="realtime" /> + <el-option label="姣忓皬鏃跺悓姝�" value="hourly" /> + <el-option label="姣忓ぉ鍚屾" value="daily" /> + <el-option label="鎵嬪姩鍚屾" value="manual" /> + </el-select> + </el-form-item> + <el-form-item label="鑷姩閲嶈瘯"> + <el-switch + v-model="syncConfig.autoRetry" + active-text="寮�鍚�" + inactive-text="鍏抽棴" + /> + </el-form-item> + <el-form-item label="閲嶈瘯娆℃暟" v-if="syncConfig.autoRetry"> + <el-input-number + v-model="syncConfig.retryCount" + :min="1" + :max="10" + style="width: 100%" + /> + </el-form-item> + </el-form> + </el-card> + + <!-- 鍚屾鏃ュ織 --> + <el-card class="sync-log" shadow="never"> + <template #header> + <div class="card-header"> + <span>鍚屾鏃ュ織</span> + <el-button type="primary" size="small" @click="refreshLog"> + 鍒锋柊鏃ュ織 + </el-button> + </div> + </template> + + <el-table :data="syncLogs" border style="width: 100%" max-height="300"> + <el-table-column label="鏃堕棿" prop="time" width="160" /> + <el-table-column label="鎿嶄綔" prop="action" width="120" /> + <el-table-column label="鐘舵��" prop="status" width="100"> + <template #default="scope"> + <el-tag :type="getLogStatusType(scope.row.status)"> + {{ getLogStatusText(scope.row.status) }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="璇︽儏" prop="detail" show-overflow-tooltip /> + </el-table> + </el-card> + </div> + + <template #footer> + <div class="dialog-footer"> + <el-button @click="handleClose">鍙栨秷</el-button> + <el-button type="primary" @click="handleSync" :loading="syncLoading"> + 寮�濮嬪悓姝� + </el-button> + </div> + </template> + </el-dialog> +</template> + +<script setup> +import { ref, reactive, onMounted } from "vue"; +import { ElMessage } from "element-plus"; +import { Check, Clock, Close } from "@element-plus/icons-vue"; + +// Props +const props = defineProps({ + dialogSyncVisible: { + type: Boolean, + default: false + } +}); + +// Emits +const emit = defineEmits(['update:dialogSyncVisible', 'success']); + +// 鍝嶅簲寮忔暟鎹� +const syncLoading = ref(false); +const syncedCount = ref(15); +const pendingCount = ref(8); +const failedCount = ref(2); + +// 鍚屾閰嶇疆 +const syncConfig = reactive({ + taxControlUrl: "https://tax-control.example.com/api", + syncFrequency: "manual", + autoRetry: true, + retryCount: 3 +}); + +// 鍚屾鏃ュ織 +const syncLogs = ref([ + { + time: "2024-12-01 15:30:00", + action: "鍙戠エ鍚屾", + status: "success", + detail: "鎴愬姛鍚屾15寮犲彂绁ㄥ埌绋庢帶骞冲彴" + }, + { + time: "2024-12-01 15:25:00", + action: "鍙戠エ鍚屾", + status: "success", + detail: "鎴愬姛鍚屾8寮犲彂绁ㄥ埌绋庢帶骞冲彴" + }, + { + time: "2024-12-01 15:20:00", + action: "鍙戠エ鍚屾", + status: "failed", + detail: "鍚屾澶辫触锛氱綉缁滆繛鎺ヨ秴鏃�" + }, + { + time: "2024-12-01 15:15:00", + action: "鍙戠エ鍚屾", + status: "success", + detail: "鎴愬姛鍚屾12寮犲彂绁ㄥ埌绋庢帶骞冲彴" + } +]); + +// 鑾峰彇鏃ュ織鐘舵�佺被鍨� +const getLogStatusType = (status) => { + const statusMap = { + success: "success", + failed: "danger", + pending: "warning" + }; + return statusMap[status] || ""; +}; + +// 鑾峰彇鏃ュ織鐘舵�佹枃鏈� +const getLogStatusText = (status) => { + const statusMap = { + success: "鎴愬姛", + failed: "澶辫触", + pending: "杩涜涓�" + }; + return statusMap[status] || status; +}; + +// 鍒锋柊鏃ュ織 +const refreshLog = () => { + ElMessage.success("鏃ュ織宸插埛鏂�"); +}; + +// 寮�濮嬪悓姝� +const handleSync = async () => { + if (!syncConfig.taxControlUrl) { + ElMessage.warning("璇峰厛閰嶇疆绋庢帶骞冲彴鍦板潃"); + return; + } + + syncLoading.value = true; + + try { + // 妯℃嫙鍚屾杩囩▼ + await new Promise(resolve => setTimeout(resolve, 2000)); + + // 鏇存柊鍚屾鐘舵�� + const newSynced = Math.min(pendingCount.value, 5); + syncedCount.value += newSynced; + pendingCount.value -= newSynced; + + // 娣诲姞鍚屾鏃ュ織 + syncLogs.value.unshift({ + time: new Date().toLocaleString(), + action: "鍙戠エ鍚屾", + status: "success", + detail: `鎴愬姛鍚屾${newSynced}寮犲彂绁ㄥ埌绋庢帶骞冲彴` + }); + + ElMessage.success("鍚屾瀹屾垚"); + emit('success'); + } catch (error) { + ElMessage.error("鍚屾澶辫触"); + failedCount.value++; + + // 娣诲姞澶辫触鏃ュ織 + syncLogs.value.unshift({ + time: new Date().toLocaleString(), + action: "鍙戠エ鍚屾", + status: "failed", + detail: "鍚屾澶辫触锛氱郴缁熼敊璇�" + }); + } finally { + syncLoading.value = false; + } +}; + +// 鍏抽棴瀵硅瘽妗� +const handleClose = () => { + emit('update:dialogSyncVisible', false); +}; + +// 缁勪欢鎸傝浇鏃跺垵濮嬪寲鏁版嵁 +onMounted(() => { + // 鍙互鍦ㄨ繖閲屽姞杞藉垵濮嬫暟鎹� +}); +</script> + +<style scoped> +.sync-container { + padding: 0; +} + +.sync-status, +.sync-config, +.sync-log { + margin-bottom: 20px; +} + +.sync-status:last-child, +.sync-config:last-child, +.sync-log:last-child { + margin-bottom: 0; +} + +.card-header { + font-weight: bold; + font-size: 16px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.status-content { + padding: 20px 0; +} + +.status-item { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + text-align: center; +} + +.status-icon { + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-right: 15px; + font-size: 24px; + color: white; +} + +.status-icon.success { + background-color: #67c23a; +} + +.status-icon.warning { + background-color: #e6a23c; +} + +.status-icon.danger { + background-color: #f56c6c; +} + +.status-text { + text-align: left; +} + +.status-title { + font-size: 14px; + color: #606266; + margin-bottom: 5px; +} + +.status-count { + font-size: 24px; + font-weight: bold; + color: #303133; +} + +.dialog-footer { + text-align: right; +} + +.el-table { + margin-top: 10px; +} +</style> diff --git a/src/views/invoiceCollaboration/index.vue b/src/views/invoiceCollaboration/index.vue new file mode 100644 index 0000000..b44ee92 --- /dev/null +++ b/src/views/invoiceCollaboration/index.vue @@ -0,0 +1,554 @@ +<template> + <div class="app-container"> + <!-- 鎼滅储琛ㄥ崟 --> + <el-form :inline="true" :model="queryParams" class="search-form"> + <el-form-item label="鍙戠エ鍙风爜"> + <el-input + v-model="queryParams.invoiceNo" + placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" + clearable + :style="{ width: '200px' }" + /> + </el-form-item> + <el-form-item label="寮�绁ㄧ姸鎬�"> + <el-select + v-model="queryParams.status" + placeholder="璇烽�夋嫨寮�绁ㄧ姸鎬�" + clearable + :style="{ width: '150px' }" + > + <el-option + :label="item.label" + v-for="item in statusList" + :key="item.value" + :value="item.value" + /> + </el-select> + </el-form-item> + <el-form-item label="寮�绁ㄦ棩鏈�"> + <el-date-picker + v-model="queryParams.dateRange" + type="daterange" + range-separator="鑷�" + start-placeholder="寮�濮嬫棩鏈�" + end-placeholder="缁撴潫鏃ユ湡" + format="YYYY-MM-DD" + value-format="YYYY-MM-DD" + :style="{ width: '240px' }" + /> + </el-form-item> + <el-form-item> + <el-button type="primary" @click="handleQuery">鏌ヨ</el-button> + <el-button @click="resetQuery">閲嶇疆</el-button> + </el-form-item> + </el-form> + + <el-card> + <!-- 鎿嶄綔鎸夐挳鍖� --> + <el-row :gutter="24" class="table-toolbar" justify="space-between"> + <el-button type="primary" :icon="Plus" @click="handleAdd"> + 鏂板鍙戠エ + </el-button> + <el-button type="success" :icon="Refresh" @click="handleSyncTaxControl"> + 鍚屾绋庢帶骞冲彴 + </el-button> + <el-button type="warning" :icon="Download" @click="handleBatchDownload"> + 鎵归噺涓嬭浇 + </el-button> + <el-button type="danger" :icon="Delete" @click="handleBatchDelete" :disabled="selectedIds.length === 0"> + 鎵归噺鍒犻櫎 + </el-button> + </el-row> + + <!-- 琛ㄦ牸缁勪欢 --> + <el-table + v-loading="loading" + :data="tableData" + @selection-change="handleSelectionChange" + border + style="width: 100%" + > + <el-table-column type="selection" width="55" /> + <el-table-column label="鍙戠エ鍙风爜" prop="invoiceNo" width="180" /> + <el-table-column label="鍙戠エ浠g爜" prop="invoiceCode" width="150" /> + <el-table-column label="寮�绁ㄦ棩鏈�" prop="invoiceDate" width="120" /> + <el-table-column label="璐拱鏂瑰悕绉�" prop="buyerName" width="200" show-overflow-tooltip /> + <el-table-column label="閿�鍞柟鍚嶇О" prop="sellerName" width="200" show-overflow-tooltip /> + <el-table-column label="閲戦" prop="amount" width="120"> + <template #default="scope"> + {{ scope.row.amount }} 鍏� + </template> + </el-table-column> + <el-table-column label="绋庨" prop="taxAmount" width="120"> + <template #default="scope"> + {{ scope.row.taxAmount }} 鍏� + </template> + </el-table-column> + <el-table-column label="浠风◣鍚堣" prop="totalAmount" width="120"> + <template #default="scope"> + {{ scope.row.totalAmount }} 鍏� + </template> + </el-table-column> + <el-table-column label="寮�绁ㄧ姸鎬�" prop="status" width="100"> + <template #default="scope"> + <el-tag :type="getStatusType(scope.row.status)"> + {{ getStatusText(scope.row.status) }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="绋庢帶骞冲彴鐘舵��" prop="taxControlStatus" width="120"> + <template #default="scope"> + <el-tag :type="getTaxControlStatusType(scope.row.taxControlStatus)"> + {{ getTaxControlStatusText(scope.row.taxControlStatus) }} + </el-tag> + </template> + </el-table-column> + <el-table-column label="鍒涘缓鏃堕棿" prop="createTime" width="160" /> + <el-table-column label="鎿嶄綔" width="250" fixed="right"> + <template #default="scope"> + <el-button + size="small" + type="primary" + @click="handleView(scope.row)" + > + 鏌ョ湅 + </el-button> + <el-button + size="small" + type="success" + @click="handleDownload(scope.row)" + v-if="scope.row.status === 'issued'" + > + 涓嬭浇 + </el-button> + <el-button + size="small" + type="warning" + @click="handleEdit(scope.row)" + v-if="scope.row.status === 'draft'" + > + 缂栬緫 + </el-button> + <el-button + size="small" + type="danger" + @click="handleDelete(scope.row)" + v-if="scope.row.status === 'draft'" + > + 鍒犻櫎 + </el-button> + </template> + </el-table-column> + </el-table> + + <!-- 鍒嗛〉缁勪欢 --> + <pagination + v-if="total > 0" + :page="current" + :limit="pageSize" + :total="total" + @pagination="handlePagination" + :layout="'total, prev, pager, next, jumper'" + /> + </el-card> + + <!-- 鏂板/缂栬緫瀵硅瘽妗� --> + <InvoiceDialog + v-model:dialogFormVisible="dialogFormVisible" + v-model:form="form" + :title="title" + :is-edit="isEdit" + @submit="handleSubmit" + @success="handleSuccess" + ref="invoiceDialog" + /> + + <!-- 鏌ョ湅璇︽儏瀵硅瘽妗� --> + <InvoiceViewDialog + v-model:dialogViewVisible="dialogViewVisible" + :form="viewForm" + title="鍙戠エ璇︽儏" + /> + + <!-- 绋庢帶骞冲彴鍚屾瀵硅瘽妗� --> + <TaxControlSyncDialog + v-model:dialogSyncVisible="dialogSyncVisible" + @success="handleSyncSuccess" + /> + + <!-- 鍗曚釜鍙戠エ涓嬭浇瀵硅瘽妗� --> + <DownloadDialog + v-model:dialogVisible="downloadDialogVisible" + :invoice="currentDownloadInvoice" + @success="handleDownloadSuccess" + /> + + <!-- 鎵归噺涓嬭浇瀵硅瘽妗� --> + <BatchDownloadDialog + v-model:dialogVisible="batchDownloadDialogVisible" + :invoices="batchDownloadInvoices" + @success="handleBatchDownloadSuccess" + /> + </div> +</template> + +<script setup> +import { ref, reactive, onMounted } from "vue"; +import { ElMessage, ElMessageBox } from "element-plus"; +import { Plus, Edit, Delete, Refresh, Download, View } from "@element-plus/icons-vue"; +import Pagination from "@/components/Pagination"; +import InvoiceDialog from "./components/InvoiceDialog.vue"; +import InvoiceViewDialog from "./components/InvoiceViewDialog.vue"; +import TaxControlSyncDialog from "./components/TaxControlSyncDialog.vue"; +import DownloadDialog from "./components/DownloadDialog.vue"; +import BatchDownloadDialog from "./components/BatchDownloadDialog.vue"; + +// 鍝嶅簲寮忔暟鎹� +const loading = ref(false); +const tableData = ref([]); +const selectedIds = ref([]); +const current = ref(1); +const pageSize = ref(10); +const total = ref(0); +const dialogFormVisible = ref(false); +const dialogViewVisible = ref(false); +const dialogSyncVisible = ref(false); +const downloadDialogVisible = ref(false); +const batchDownloadDialogVisible = ref(false); +const isEdit = ref(false); +const title = ref(""); +const form = ref({}); +const viewForm = ref({}); +const currentDownloadInvoice = ref({}); +const batchDownloadInvoices = ref([]); + +// 鏌ヨ鍙傛暟 +const queryParams = reactive({ + invoiceNo: "", + status: "", + dateRange: [] +}); + +// 鐘舵�佸垪琛� +const statusList = ref([ + { value: "draft", label: "鑽夌" }, + { value: "pending", label: "寰呭紑绁�" }, + { value: "issuing", label: "寮�绁ㄤ腑" }, + { value: "issued", label: "宸插紑绁�" }, + { value: "failed", label: "寮�绁ㄥけ璐�" }, + { value: "cancelled", label: "宸蹭綔搴�" } +]); + +// 妯℃嫙鏁版嵁 +const mockData = [ + { + id: "1", + invoiceNo: "FP20241201001", + invoiceCode: "123456789", + invoiceDate: "2024-12-01", + buyerName: "瀹㈡埛A鍏徃", + sellerName: "鏈叕鍙�", + amount: 10000.00, + taxAmount: 1300.00, + totalAmount: 11300.00, + status: "issued", + taxControlStatus: "synced", + createTime: "2024-12-01 10:00:00" + }, + { + id: "2", + invoiceNo: "FP20241201002", + invoiceCode: "123456790", + invoiceDate: "2024-12-01", + buyerName: "瀹㈡埛B鍏徃", + sellerName: "鏈叕鍙�", + amount: 5000.00, + taxAmount: 650.00, + totalAmount: 5650.00, + status: "pending", + taxControlStatus: "pending", + createTime: "2024-12-01 14:30:00" + } +]; + +// 鑾峰彇鐘舵�佺被鍨� +const getStatusType = (status) => { + const statusMap = { + draft: "", + pending: "warning", + issuing: "warning", + issued: "success", + failed: "danger", + cancelled: "info" + }; + return statusMap[status] || ""; +}; + +// 鑾峰彇鐘舵�佹枃鏈� +const getStatusText = (status) => { + const statusMap = { + draft: "鑽夌", + pending: "寰呭紑绁�", + issuing: "寮�绁ㄤ腑", + issued: "宸插紑绁�", + failed: "寮�绁ㄥけ璐�", + cancelled: "宸蹭綔搴�" + }; + return statusMap[status] || status; +}; + +// 鑾峰彇绋庢帶骞冲彴鐘舵�佺被鍨� +const getTaxControlStatusType = (status) => { + const statusMap = { + pending: "warning", + syncing: "warning", + synced: "success", + failed: "danger" + }; + return statusMap[status] || ""; +}; + +// 鑾峰彇绋庢帶骞冲彴鐘舵�佹枃鏈� +const getTaxControlStatusText = (status) => { + const statusMap = { + pending: "寰呭悓姝�", + syncing: "鍚屾涓�", + synced: "宸插悓姝�", + failed: "鍚屾澶辫触" + }; + return statusMap[status] || status; +}; + +// 鏌ヨ +const handleQuery = () => { + current.value = 1; + loadData(); +}; + +// 閲嶇疆鏌ヨ +const resetQuery = () => { + Object.assign(queryParams, { + invoiceNo: "", + status: "", + dateRange: [] + }); + handleQuery(); +}; + +// 鍔犺浇鏁版嵁 +const loadData = () => { + loading.value = true; + // 妯℃嫙API璋冪敤 + setTimeout(() => { + tableData.value = mockData; + total.value = mockData.length; + loading.value = false; + }, 500); +}; + +// 鍒嗛〉澶勭悊 +const handlePagination = (pagination) => { + current.value = pagination.page; + pageSize.value = pagination.limit; + loadData(); +}; + +// 閫夋嫨鍙樺寲 +const handleSelectionChange = (selection) => { + selectedIds.value = selection.map(item => item.id); +}; + +// 鏂板 +const handleAdd = () => { + isEdit.value = false; + title.value = "鏂板鍙戠エ"; + form.value = { + buyerName: "", + buyerTaxNo: "", + buyerAddress: "", + buyerBankAccount: "", + sellerName: "鏈叕鍙�", + sellerTaxNo: "123456789012345678", + sellerAddress: "鍏徃鍦板潃", + sellerBankAccount: "閾惰璐︽埛", + items: [], + remark: "" + }; + dialogFormVisible.value = true; +}; + +// 缂栬緫 +const handleEdit = (row) => { + isEdit.value = true; + title.value = "缂栬緫鍙戠エ"; + form.value = { ...row }; + dialogFormVisible.value = true; +}; + +// 鏌ョ湅 +const handleView = (row) => { + viewForm.value = { ...row }; + dialogViewVisible.value = true; +}; + +// 鍒犻櫎 +const handleDelete = (row) => { + ElMessageBox.confirm( + `纭畾瑕佸垹闄ゅ彂绁� ${row.invoiceNo} 鍚楋紵`, + "鎻愮ず", + { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "warning" + } + ).then(() => { + // 妯℃嫙鍒犻櫎 + const index = tableData.value.findIndex(item => item.id === row.id); + if (index > -1) { + tableData.value.splice(index, 1); + total.value--; + ElMessage.success("鍒犻櫎鎴愬姛"); + } + }); +}; + +// 鎵归噺鍒犻櫎 +const handleBatchDelete = () => { + if (selectedIds.value.length === 0) { + ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑璁板綍"); + return; + } + + ElMessageBox.confirm( + `纭畾瑕佸垹闄ら�変腑鐨� ${selectedIds.value.length} 鏉¤褰曞悧锛焋, + "鎻愮ず", + { + confirmButtonText: "纭畾", + cancelButtonText: "鍙栨秷", + type: "warning" + } + ).then(() => { + // 妯℃嫙鎵归噺鍒犻櫎 + tableData.value = tableData.value.filter(item => !selectedIds.value.includes(item.id)); + total.value = tableData.value.length; + selectedIds.value = []; + ElMessage.success("鎵归噺鍒犻櫎鎴愬姛"); + }); +}; + +// 涓嬭浇鍙戠エ +const handleDownload = (row) => { + if (row.status !== 'issued') { + ElMessage.warning("鍙湁宸插紑绁ㄧ殑鍙戠エ鎵嶈兘涓嬭浇"); + return; + } + + // 鏄剧ず涓嬭浇閫夐」瀵硅瘽妗� + downloadDialogVisible.value = true; + currentDownloadInvoice.value = row; +}; + +// 鎵归噺涓嬭浇 +const handleBatchDownload = () => { + if (selectedIds.value.length === 0) { + ElMessage.warning("璇烽�夋嫨瑕佷笅杞界殑璁板綍"); + return; + } + + // 妫�鏌ラ�変腑鐨勫彂绁ㄧ姸鎬� + const selectedInvoices = tableData.value.filter(item => selectedIds.value.includes(item.id)); + const issuedInvoices = selectedInvoices.filter(item => item.status === 'issued'); + + if (issuedInvoices.length === 0) { + ElMessage.warning("閫変腑鐨勫彂绁ㄤ腑娌℃湁宸插紑绁ㄧ殑鍙戠エ"); + return; + } + + if (issuedInvoices.length < selectedInvoices.length) { + ElMessage.warning(`閫変腑鐨�${selectedInvoices.length}寮犲彂绁ㄤ腑锛屽彧鏈�${issuedInvoices.length}寮犲凡寮�绁紝灏嗗彧涓嬭浇宸插紑绁ㄧ殑鍙戠エ`); + } + + // 鏄剧ず鎵归噺涓嬭浇閫夐」瀵硅瘽妗� + batchDownloadDialogVisible.value = true; + batchDownloadInvoices.value = issuedInvoices; +}; + +// 鍚屾绋庢帶骞冲彴 +const handleSyncTaxControl = () => { + dialogSyncVisible.value = true; +}; + +// 鎻愪氦琛ㄥ崟 +const handleSubmit = (formData) => { + if (isEdit.value) { + // 缂栬緫 + const index = tableData.value.findIndex(item => item.id === formData.id); + if (index > -1) { + tableData.value[index] = { ...formData }; + ElMessage.success("缂栬緫鎴愬姛"); + } + } else { + // 鏂板 + const newItem = { + id: Date.now().toString(), + invoiceNo: `FP${Date.now()}`, + invoiceCode: "123456789", + invoiceDate: new Date().toISOString().split('T')[0], + buyerName: formData.buyerName, + sellerName: formData.sellerName, + amount: formData.items.reduce((sum, item) => sum + (item.amount || 0), 0), + taxAmount: formData.items.reduce((sum, item) => sum + (item.taxAmount || 0), 0), + totalAmount: formData.items.reduce((sum, item) => sum + (item.totalAmount || 0), 0), + status: "draft", + taxControlStatus: "pending", + createTime: new Date().toLocaleString() + }; + tableData.value.unshift(newItem); + total.value++; + ElMessage.success("鏂板鎴愬姛"); + } + dialogFormVisible.value = false; +}; + +// 琛ㄥ崟鎴愬姛鍥炶皟 +const handleSuccess = () => { + loadData(); +}; + +// 鍚屾鎴愬姛鍥炶皟 +const handleSyncSuccess = () => { + loadData(); + ElMessage.success("绋庢帶骞冲彴鍚屾鎴愬姛"); +}; + +// 鍗曚釜涓嬭浇鎴愬姛鍥炶皟 +const handleDownloadSuccess = () => { + downloadDialogVisible.value = false; + ElMessage.success("鍙戠エ涓嬭浇鎴愬姛"); +}; + +// 鎵归噺涓嬭浇鎴愬姛鍥炶皟 +const handleBatchDownloadSuccess = () => { + batchDownloadDialogVisible.value = false; + ElMessage.success("鎵归噺涓嬭浇鎴愬姛"); +}; + +// 椤甸潰鍔犺浇 +onMounted(() => { + loadData(); +}); +</script> + +<style scoped> +.search-form { + margin-bottom: 20px; +} + +.table-toolbar { + margin-bottom: 20px; +} + +.el-card { + margin-bottom: 20px; +} +</style> \ No newline at end of file -- Gitblit v1.9.3