spring
8 天以前 92e38481fc2f68dcd540434c6428d790b470a84d
发票协同
已修改1个文件
已添加6个文件
2976 ■■■■■ 文件已修改
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/BatchDownloadDialog.vue 704 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/DownloadDialog.vue 580 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/InvoiceDialog.vue 484 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/InvoiceViewDialog.vue 291 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/TaxControlSyncDialog.vue 362 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/index.vue 554 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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",
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">发票正本</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>
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="发票代码">{{ 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>
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>
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="发票代码">{{ 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>
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>
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="发票代码" 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>