| | |
| | | "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", |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="å票代ç ">{{ 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 ? 'ä¸è½½å®æ' : `æ£å¨ä¸è½½... ${downloadProgress}%` }} |
| | | </div> |
| | | <div class="progress-detail" v-if="downloadProgress < 100"> |
| | | <span>æ£å¨çæ{{ 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: "æ£å¨éªè¯å票信æ¯..." }, |
| | | { progress: 40, message: "æ£å¨çææä»¶å
容..." }, |
| | | { progress: 60, message: "æ£å¨åºç¨æ ¼å¼è®¾ç½®..." }, |
| | | { progress: 80, message: "æ£å¨çææä»¶..." }, |
| | | { 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å
容ï¼CSVæ ¼å¼ï¼å
¼å®¹æ§æ´å¥½ï¼ |
| | | fileContent = generateExcelContent(); |
| | | fileName = `${downloadOptions.fileName}.csv`; |
| | | mimeType = 'text/csv'; |
| | | } else if (downloadOptions.format === 'image') { |
| | | // çæå¾çå
容ï¼SVGæ ¼å¼ï¼ |
| | | 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 { |
| | | // 卿坼å
¥JSZipåº |
| | | 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å
容ï¼CSVæ ¼å¼ï¼ |
| | | const generateExcelContent = () => { |
| | | const invoice = props.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; |
| | | }; |
| | | |
| | | // çæå¾çå
容ï¼SVGæ ¼å¼ï¼ |
| | | 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'} |
| | | å票代ç ,${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> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="å票代ç ">{{ 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> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <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="å票代ç " 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> |