gongchunyi
2026-08-11 3ee27be7fadb2fd30bd42991ef810014f1e91a19
src/views/reportAnalysis/taxComparison/index.vue
@@ -1,118 +1,411 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="日期">
        <el-date-picker
          style="width: 240px"
          v-model="filters.month"
          value-format="YYYY-MM"
          format="YYYY-MM"
          type="month"
          placeholder="选择月份"
          clearable
          @change="getTableData"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData"> 搜索 </el-button>
        <el-button @click="resetFilters"> 重置 </el-button>
        <el-button @click="handleOut"> 导出 </el-button>
      </el-form-item>
    </el-form>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="columns"
        :tableData="dataList"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      />
    </div>
  </div>
   <div class="app-container vat-page">
      <el-row :gutter="20" class="vat-row">
         <!-- 左侧:增值税明细列表 -->
         <el-col :span="15" class="vat-col">
            <el-card shadow="never" class="vat-card">
               <template #header>
                  <div class="card-header">
                     <span class="card-title">增值税明细</span>
                     <el-select v-model="queryMonth" placeholder="选择月份" clearable style="width: 150px;" @change="handleMonthChange">
                        <el-option v-for="m in monthOptions" :key="m" :label="m" :value="m" />
                     </el-select>
                  </div>
               </template>
               <template v-if="!loading && vatDetailList.length === 0">
                  <div class="empty-full">
                     <el-empty description="暂无增值税明细数据" :image-size="160" />
                  </div>
               </template>
               <template v-else>
                  <div class="table-wrapper">
                     <el-table :data="vatDetailList" border v-loading="loading" stripe height="100%">
                        <el-table-column prop="orderType" label="类型" width="65" align="center">
                           <template #default="scope">
                              <el-tag :type="scope.row.orderType === '进项' ? '' : 'warning'" size="small" effect="plain">
                                 {{ scope.row.orderType }}
                              </el-tag>
                           </template>
                        </el-table-column>
                        <el-table-column prop="invoiceNo" label="发票号" min-width="140" show-overflow-tooltip />
                        <el-table-column prop="salesContractNo" label="合同号" min-width="140" show-overflow-tooltip />
                        <el-table-column prop="supplierName" label="供应商" min-width="100" show-overflow-tooltip />
                        <el-table-column prop="customerName" label="客户" min-width="100" show-overflow-tooltip />
                        <el-table-column prop="invoiceDate" label="开票日期" width="110" align="center" />
                        <el-table-column prop="taxRate" label="税率" width="75" align="center">
                           <template #default="scope">
                              {{ scope.row.taxRate }}%
                           </template>
                        </el-table-column>
                        <el-table-column prop="taxAmount" label="税额" width="110" align="right">
                           <template #default="scope">
                    <span class="tax-amount" :class="scope.row.orderType === '进项' ? 'input-tax' : 'output-tax'">
                      ¥{{ formatNumber(scope.row.taxAmount) }}
                    </span>
                           </template>
                        </el-table-column>
                     </el-table>
                  </div>
                  <el-pagination
                     v-if="page.total > 0"
                     class="vat-pagination"
                     background
                     layout="total, sizes, prev, pager, next, jumper"
                     :current-page="page.current"
                     :page-sizes="[10, 20, 50]"
                     :page-size="page.size"
                     :total="page.total"
                     @size-change="handleSizeChange"
                     @current-change="handleCurrentChange" />
               </template>
            </el-card>
         </el-col>
         <!-- 右侧:柱状对比图 -->
         <el-col :span="9" class="vat-col">
            <el-card shadow="never" class="vat-card">
               <template #header>
                  <span class="card-title">进销项增值税对比</span>
               </template>
               <div class="summary-row">
                  <div class="summary-item input-bg">
                     <div class="summary-label">进项税额合计</div>
                     <div class="summary-value">¥{{ formatNumber(inputTotal) }}</div>
                  </div>
                  <div class="summary-item output-bg">
                     <div class="summary-label">销项税额合计</div>
                     <div class="summary-value">¥{{ formatNumber(outputTotal) }}</div>
                  </div>
                  <div class="summary-item diff-bg">
                     <div class="summary-label">差额</div>
                     <div class="summary-value" :class="diffValue >= 0 ? 'positive' : 'negative'">
                        {{ diffValue >= 0 ? '+' : '' }}¥{{ formatNumber(diffValue) }}
                     </div>
                  </div>
               </div>
               <div v-if="vatDetailList.length > 0" ref="vatChart" class="chart-container"></div>
               <div v-else class="chart-empty">暂无数据</div>
            </el-card>
         </el-col>
      </el-row>
   </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
import { onMounted, getCurrentInstance } from "vue";
import { getTaxList } from "@/api/procurementManagement/taxComparison";
import { ElMessageBox } from "element-plus";
const { proxy } = getCurrentInstance();
import { ref, reactive, computed, onMounted, nextTick, onBeforeUnmount } from "vue";
import * as echarts from 'echarts';
import { getVatDetail } from "@/api/procurementManagement/taxComparison";
defineOptions({
  name: "增值税比对",
   name: "增值税比对",
});
const {
  loading,
  filters,
  columns,
  dataList,
  pagination,
  getTableData,
  resetFilters,
  onCurrentChange,
} = usePaginationApi(
  getTaxList,
  {
    month: [], // 来票日期
  },
  [
    {
      label: "月份",
      prop: "month",
      align: "center",
    },
    {
      label: "销项税额",
      prop: "jtaxAmount",
      align: "center",
    },
    {
      label: "进项税额",
      prop: "xtaxAmount",
      align: "center",
    },
    {
      label: "销-进",
      prop: "taxAmount",
      align: "center",
    },
  ],
  {}
);
const vatChart = ref(null);
let chartInstance = null;
const changePage = ({ page }) => {
  pagination.currentPage = page;
  onCurrentChange(page);
const queryMonth = ref(new Date().toISOString().slice(0, 7));
const monthOptions = ref([]);
const vatDetailList = ref([]);
const loading = ref(false);
const page = reactive({ current: 1, size: 20, total: 0 });
// 汇总额
const inputTotal = computed(() => {
   return vatDetailList.value
      .filter(i => i.orderType === '进项')
      .reduce((sum, i) => sum + (Number(i.taxAmount) || 0), 0);
});
const outputTotal = computed(() => {
   return vatDetailList.value
      .filter(i => i.orderType === '销项')
      .reduce((sum, i) => sum + (Number(i.taxAmount) || 0), 0);
});
const diffValue = computed(() => outputTotal.value - inputTotal.value);
const formatNumber = (val) => {
   const num = Number(val);
   if (isNaN(num)) return '0.00';
   return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
// 导出
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      proxy.download("/purchase/report/exportTwo", {}, "增值税比对.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    });
const loadVatDetail = () => {
   loading.value = true;
   const params = {
      current: page.current,
      size: page.size,
      month: queryMonth.value || undefined,
   };
   getVatDetail(params)
      .then((res) => {
         if (res.code === 200) {
            vatDetailList.value = res.data.records || [];
            page.total = res.data.total || 0;
            nextTick(() => renderChart());
         }
      })
      .finally(() => {
         loading.value = false;
      });
};
const renderChart = () => {
   if (!vatChart.value) return;
   if (chartInstance) {
      chartInstance.dispose();
   }
   chartInstance = echarts.init(vatChart.value);
   const data = vatDetailList.value;
   const invoices = data.map(i => i.invoiceNo);
   chartInstance.setOption({
      tooltip: {
         trigger: 'axis',
         backgroundColor: 'rgba(255,255,255,0.95)',
         borderColor: '#e0e0e0',
         borderWidth: 1,
         textStyle: { color: '#333', fontSize: 13 },
         formatter: function (params) {
            let html = `<b>${params[0].axisValue}</b><br/>`;
            params.forEach(p => {
               if (p.value > 0) {
                  html += `${p.marker} ${p.seriesName}: ¥${formatNumber(p.value)}<br/>`;
               }
            });
            return html;
         },
      },
      legend: {
         data: ['进项税额', '销项税额'],
         bottom: 0,
         textStyle: { fontSize: 12 },
      },
      grid: {
         left: '10%',
         right: '8%',
         top: '8%',
         bottom: '12%',
      },
      xAxis: {
         type: 'category',
         data: invoices,
         axisLabel: {
            rotate: 45,
            fontSize: 10,
            interval: 0,
         },
         axisTick: { alignWithLabel: true },
      },
      yAxis: {
         type: 'value',
         name: '税额(元)',
         nameTextStyle: { fontSize: 11 },
         axisLabel: {
            formatter: (val) => val >= 10000 ? `${(val / 10000).toFixed(1)}万` : val,
         },
      },
      series: [
         {
            name: '进项税额',
            type: 'bar',
            barWidth: '35%',
            data: data.map(i => i.orderType === '进项' ? Number(i.taxAmount) || 0 : null),
            itemStyle: {
               color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                  { offset: 0, color: '#667eea' },
                  { offset: 1, color: '#764ba2' },
               ]),
               borderRadius: [4, 4, 0, 0],
            },
            emphasis: {
               itemStyle: { color: '#667eea' },
            },
         },
         {
            name: '销项税额',
            type: 'bar',
            barWidth: '35%',
            data: data.map(i => i.orderType === '销项' ? Number(i.taxAmount) || 0 : null),
            itemStyle: {
               color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                  { offset: 0, color: '#f093fb' },
                  { offset: 1, color: '#f5576c' },
               ]),
               borderRadius: [4, 4, 0, 0],
            },
            emphasis: {
               itemStyle: { color: '#f093fb' },
            },
         },
      ],
   });
};
const handleMonthChange = () => {
   page.current = 1;
   loadVatDetail();
};
const handleSizeChange = (val) => {
   page.size = val;
   loadVatDetail();
};
const handleCurrentChange = (val) => {
   page.current = val;
   loadVatDetail();
};
const generateMonthOptions = () => {
   const now = new Date();
   const options = [];
   for (let i = 11; i >= 0; i--) {
      const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
      const y = d.getFullYear();
      const m = String(d.getMonth() + 1).padStart(2, '0');
      options.push(`${y}-${m}`);
   }
   monthOptions.value = options;
};
const handleResize = () => {
   if (chartInstance) {
      chartInstance.resize();
   }
};
onMounted(() => {
  getTableData();
   generateMonthOptions();
   loadVatDetail();
   window.addEventListener('resize', handleResize);
});
onBeforeUnmount(() => {
   window.removeEventListener('resize', handleResize);
   if (chartInstance) {
      chartInstance.dispose();
   }
});
</script>
<style lang="scss" scoped>
.table_list {
  margin-top: unset;
.vat-page {
   height: calc(100vh - 84px);
}
</style>
.vat-row {
   height: 100%;
}
.vat-col {
   height: 100%;
}
.vat-card {
   height: 100%;
   display: flex;
   flex-direction: column;
   :deep(.el-card__header) {
      flex-shrink: 0;
   }
   :deep(.el-card__body) {
      flex: 1;
      display: flex;
      flex-direction: column;
      overflow: hidden;
   }
}
.card-header {
   display: flex;
   justify-content: space-between;
   align-items: center;
}
.card-title {
   font-size: 15px;
   font-weight: 600;
   color: #303133;
}
.table-wrapper {
   flex: 1;
   overflow: hidden;
}
.vat-pagination {
   flex-shrink: 0;
   margin-top: 12px;
   justify-content: flex-end;
}
.tax-amount {
   font-weight: 600;
   font-family: 'Monaco', 'Menlo', monospace;
   &.input-tax { color: #667eea; }
   &.output-tax { color: #f5576c; }
}
.summary-row {
   display: flex;
   gap: 12px;
   margin-bottom: 16px;
   flex-shrink: 0;
   .summary-item {
      flex: 1;
      padding: 12px 14px;
      border-radius: 8px;
      text-align: center;
      &.input-bg {
         background: linear-gradient(135deg, #f3f0ff 0%, #e8e5ff 100%);
         .summary-value { color: #667eea; }
      }
      &.output-bg {
         background: linear-gradient(135deg, #fff0f3 0%, #ffe0e6 100%);
         .summary-value { color: #f5576c; }
      }
      &.diff-bg {
         background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
         .summary-value {
            &.positive { color: #f5576c; }
            &.negative { color: #22c55e; }
         }
      }
      .summary-label {
         font-size: 12px;
         color: #909399;
         margin-bottom: 4px;
      }
      .summary-value {
         font-size: 18px;
         font-weight: 700;
         font-family: 'Monaco', 'Menlo', monospace;
      }
   }
}
.chart-container {
   flex: 1;
   min-height: 0;
}
.empty-full {
   flex: 1;
   display: flex;
   align-items: center;
   justify-content: center;
}
.chart-empty {
   flex: 1;
   display: flex;
   align-items: center;
   justify-content: center;
   color: #c0c4cc;
   font-size: 13px;
}
</style>