| | |
| | | <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> |