<template>
|
<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 { ref, reactive, computed, onMounted, nextTick, onBeforeUnmount } from "vue";
|
import * as echarts from 'echarts';
|
import { getVatDetail } from "@/api/procurementManagement/taxComparison";
|
|
defineOptions({
|
name: "增值税比对",
|
});
|
|
const vatChart = ref(null);
|
let chartInstance = null;
|
|
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 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(() => {
|
generateMonthOptions();
|
loadVatDetail();
|
window.addEventListener('resize', handleResize);
|
});
|
|
onBeforeUnmount(() => {
|
window.removeEventListener('resize', handleResize);
|
if (chartInstance) {
|
chartInstance.dispose();
|
}
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.vat-page {
|
height: calc(100vh - 84px);
|
}
|
|
.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>
|