feat: 税务合规智能体—税负监控+发票校验+社保比对
This commit is contained in:
@@ -0,0 +1,787 @@
|
||||
<template>
|
||||
<div class="tax-compliance">
|
||||
<!-- ── 顶部统计卡片 ── -->
|
||||
<div class="stat-cards">
|
||||
<div class="stat-card" @click="switchTab('burden')">
|
||||
<div class="stat-num alert">{{ stats.burden?.alert_count ?? 0 }}</div>
|
||||
<div class="stat-label">税负预警</div>
|
||||
</div>
|
||||
<div class="stat-card" @click="switchTab('invoice')">
|
||||
<div class="stat-num danger">{{ (stats.invoice?.status_count?.invalid || 0) + (stats.invoice?.status_count?.warning || 0) }}</div>
|
||||
<div class="stat-label">发票异常</div>
|
||||
</div>
|
||||
<div class="stat-card" @click="switchTab('ss')">
|
||||
<div class="stat-num warning">{{ (stats.ss?.status_count?.alert || 0) + (stats.ss?.status_count?.warning || 0) }}</div>
|
||||
<div class="stat-label">社保异常</div>
|
||||
</div>
|
||||
<div class="stat-card" @click="switchTab('invoice')">
|
||||
<div class="stat-num normal">{{ stats.invoice?.total ?? 0 }}</div>
|
||||
<div class="stat-label">发票总量</div>
|
||||
</div>
|
||||
<div class="stat-card" @click="switchTab('ss')">
|
||||
<div class="stat-num normal">{{ stats.ss?.total ?? 0 }}</div>
|
||||
<div class="stat-label">社保记录</div>
|
||||
</div>
|
||||
<div class="stat-actions">
|
||||
<el-button type="primary" plain size="small" :loading="seeding" @click="seedDemo">生成演示数据</el-button>
|
||||
<el-button type="success" plain size="small" :loading="checkingAll" @click="runAllChecks">执行全部检查</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 标签页 ── -->
|
||||
<el-tabs v-model="activeTab" class="tax-tabs">
|
||||
<!-- ═══ 看板 ═══ -->
|
||||
<el-tab-pane label="税务看板" name="dashboard">
|
||||
<div class="panel">
|
||||
<div class="panel-title">税负率趋势 vs 行业均值 <span class="panel-sub">(增值税3.5% / 所得税2.5% / 附加税0.5%,超±20%预警)</span></div>
|
||||
<div ref="trendChartRef" class="chart-lg"></div>
|
||||
<div v-if="!hasTrend" class="empty-tip">暂无税负数据,请先在「税负监控」录入或点击右上角「生成演示数据」</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<div class="panel">
|
||||
<div class="panel-title">发票异常列表 <el-button text type="primary" size="small" @click="switchTab('invoice')">查看全部 →</el-button></div>
|
||||
<el-table :data="stats.invoice?.abnormal || []" size="small" max-height="320" empty-text="暂无异常发票">
|
||||
<el-table-column prop="invoice_no" label="发票号" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="金额" width="110">
|
||||
<template #default="{ row }">¥{{ fmt(row.amount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }"><el-tag :type="invStatusTag(row.check_status)" size="small">{{ invStatusLabel(row.check_status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="check_reason" label="异常原因" min-width="200" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="panel">
|
||||
<div class="panel-title">社保比对异常列表 <el-button text type="primary" size="small" @click="switchTab('ss')">查看全部 →</el-button></div>
|
||||
<el-table :data="stats.ss?.abnormal || []" size="small" max-height="320" empty-text="暂无社保异常">
|
||||
<el-table-column prop="employee" label="人员" width="90" />
|
||||
<el-table-column prop="period" label="期间" width="90" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }"><el-tag :type="ssStatusTag(row.check_status)" size="small">{{ ssStatusLabel(row.check_status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="warning_msg" label="异常提醒" min-width="220" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ═══ 税负监控 ═══ -->
|
||||
<el-tab-pane label="税负监控" name="burden">
|
||||
<div class="toolbar">
|
||||
<el-select v-model="taxFilters.tax_type" placeholder="税种" clearable size="small" style="width:130px" @change="loadTaxRecords">
|
||||
<el-option label="增值税" value="vat" /><el-option label="所得税" value="income" /><el-option label="附加税" value="surtax" />
|
||||
</el-select>
|
||||
<el-input v-model="taxFilters.period" placeholder="期间 YYYY-MM" clearable size="small" style="width:150px" @keyup.enter="loadTaxRecords" @clear="loadTaxRecords" />
|
||||
<el-button size="small" type="primary" @click="loadTaxRecords">查询</el-button>
|
||||
<el-button size="small" type="warning" plain :loading="checkingTax" @click="runTaxCheck">重算税负率+预警</el-button>
|
||||
<el-button size="small" type="primary" plain @click="openTaxDialog()">新增税务记录</el-button>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div class="panel-title">税负率趋势</div>
|
||||
<div ref="burdenChartRef" class="chart-md"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<el-table :data="taxRecords" size="small" v-loading="loadingTax" empty-text="暂无税务记录">
|
||||
<el-table-column prop="period" label="期间" width="90" />
|
||||
<el-table-column label="税种" width="90">
|
||||
<template #default="{ row }"><el-tag size="small" :type="taxTypeTag(row.tax_type)">{{ row.tax_type_label }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="应纳税额" width="110"><template #default="{ row }">¥{{ fmt(row.tax_payable) }}</template></el-table-column>
|
||||
<el-table-column label="实缴税额" width="110"><template #default="{ row }">¥{{ fmt(row.tax_paid) }}</template></el-table-column>
|
||||
<el-table-column label="收入" width="120"><template #default="{ row }">¥{{ fmt(row.income) }}</template></el-table-column>
|
||||
<el-table-column label="税率" width="80"><template #default="{ row }">{{ row.tax_rate ?? '-' }}%</template></el-table-column>
|
||||
<el-table-column label="税负率" width="100">
|
||||
<template #default="{ row }">
|
||||
<span :class="{ 'text-alert': row.burden_status === 'alert' }">{{ row.tax_burden_rate != null ? row.tax_burden_rate + '%' : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="行业均值" width="90"><template #default="{ row }">{{ row.benchmark != null ? row.benchmark + '%' : '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }"><el-tag size="small" :type="burdenTag(row.burden_status)">{{ burdenLabel(row.burden_status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="warning_msg" label="预警信息" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" size="small" @click="openTaxDialog(row)">编辑</el-button>
|
||||
<el-button text type="danger" size="small" @click="deleteTax(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ═══ 发票校验 ═══ -->
|
||||
<el-tab-pane label="发票校验" name="invoice">
|
||||
<div class="toolbar">
|
||||
<el-select v-model="invFilters.status" placeholder="校验状态" clearable size="small" style="width:130px" @change="loadInvoices">
|
||||
<el-option label="待校验" value="pending" /><el-option label="通过" value="valid" /><el-option label="异常" value="invalid" /><el-option label="提醒" value="warning" />
|
||||
</el-select>
|
||||
<el-input v-model="invFilters.keyword" placeholder="发票号/供应商/报销单号" clearable size="small" style="width:220px" @keyup.enter="loadInvoices" @clear="loadInvoices" />
|
||||
<el-button size="small" type="primary" @click="loadInvoices">查询</el-button>
|
||||
<el-button size="small" type="warning" plain :loading="checkingInv" @click="batchCheckInvoices">批量校验</el-button>
|
||||
<el-button size="small" type="danger" plain @click="loadAbnormalInvoices">只看异常</el-button>
|
||||
<el-button size="small" type="primary" plain @click="openInvoiceDialog()">录入发票</el-button>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<el-table :data="invoices" size="small" v-loading="loadingInv" empty-text="暂无发票">
|
||||
<el-table-column prop="invoice_no" label="发票号" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="金额" width="110"><template #default="{ row }">¥{{ fmt(row.amount) }}</template></el-table-column>
|
||||
<el-table-column label="类型" width="110"><template #default="{ row }">{{ row.invoice_type_label }}</template></el-table-column>
|
||||
<el-table-column label="开票日期" width="105"><template #default="{ row }">{{ (row.invoice_date || '').slice(0, 10) }}</template></el-table-column>
|
||||
<el-table-column prop="supplier" label="供应商" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="reimb_no" label="报销单号" width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="contract_no" label="合同号" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }"><el-tag size="small" :type="invStatusTag(row.check_status)">{{ invStatusLabel(row.check_status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="check_reason" label="异常原因" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" size="small" @click="openInvoiceDialog(row)">编辑</el-button>
|
||||
<el-button text type="danger" size="small" @click="deleteInvoice(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ═══ 社保比对 ═══ -->
|
||||
<el-tab-pane label="社保比对" name="ss">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="ssFilters.employee" placeholder="人员姓名" clearable size="small" style="width:150px" @keyup.enter="loadSs" @clear="loadSs" />
|
||||
<el-input v-model="ssFilters.period" placeholder="期间 YYYY-MM" clearable size="small" style="width:150px" @keyup.enter="loadSs" @clear="loadSs" />
|
||||
<el-button size="small" type="primary" @click="loadSs">查询</el-button>
|
||||
<el-button size="small" type="warning" plain :loading="checkingSs" @click="batchCheckSs">批量比对</el-button>
|
||||
<el-button size="small" type="danger" plain @click="loadAbnormalSs">只看异常</el-button>
|
||||
<el-button size="small" type="primary" plain @click="openSsDialog()">新增缴费记录</el-button>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<el-table :data="ssRecords" size="small" v-loading="loadingSs" empty-text="暂无社保记录">
|
||||
<el-table-column prop="employee" label="人员" width="100" />
|
||||
<el-table-column prop="period" label="期间" width="90" />
|
||||
<el-table-column label="缴费基数" width="110"><template #default="{ row }">¥{{ fmt(row.base_amount) }}</template></el-table-column>
|
||||
<el-table-column label="申报工资" width="110"><template #default="{ row }">¥{{ fmt(row.salary) }}</template></el-table-column>
|
||||
<el-table-column label="单位缴纳" width="110"><template #default="{ row }">¥{{ fmt(row.company_amount) }}</template></el-table-column>
|
||||
<el-table-column label="个人缴纳" width="110"><template #default="{ row }">¥{{ fmt(row.personal_amount) }}</template></el-table-column>
|
||||
<el-table-column label="单位比例" width="90"><template #default="{ row }">{{ row.company_rate != null ? row.company_rate + '%' : '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }"><el-tag size="small" :type="ssStatusTag(row.check_status)">{{ ssStatusLabel(row.check_status) }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="warning_msg" label="异常提醒" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button text type="primary" size="small" @click="openSsDialog(row)">编辑</el-button>
|
||||
<el-button text type="danger" size="small" @click="deleteSs(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- ═══ 税务记录对话框 ═══ -->
|
||||
<MyDialog v-model="taxDialogVisible" title="税务记录" :width="520">
|
||||
<el-form label-width="90px" size="small">
|
||||
<el-form-item label="期间" required>
|
||||
<el-input v-model="taxForm.period" placeholder="YYYY-MM,如 2026-07" />
|
||||
</el-form-item>
|
||||
<el-form-item label="税种" required>
|
||||
<el-select v-model="taxForm.tax_type" style="width:100%">
|
||||
<el-option label="增值税" value="vat" /><el-option label="所得税" value="income" /><el-option label="附加税" value="surtax" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="应纳税额"><el-input-number v-model="taxForm.tax_payable" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="实缴税额"><el-input-number v-model="taxForm.tax_paid" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="当期收入"><el-input-number v-model="taxForm.income" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="适用税率"><el-input-number v-model="taxForm.tax_rate" :min="0" :precision="1" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="taxForm.remark" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="taxDialogVisible = false">取消</el-button>
|
||||
<el-button size="small" type="primary" :loading="savingTax" @click="saveTax">保存(自动计算税负率)</el-button>
|
||||
</template>
|
||||
</MyDialog>
|
||||
|
||||
<!-- ═══ 发票对话框 ═══ -->
|
||||
<MyDialog v-model="invDialogVisible" title="录入/编辑发票" :width="520">
|
||||
<el-form label-width="100px" size="small">
|
||||
<el-form-item label="发票号" required>
|
||||
<el-input v-model="invForm.invoice_no" placeholder="33位数字发票号码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="金额" required>
|
||||
<el-input-number v-model="invForm.amount" :min="0" :precision="2" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发票类型">
|
||||
<el-select v-model="invForm.invoice_type" style="width:100%">
|
||||
<el-option label="增值税专用发票" value="vat" /><el-option label="增值税普通发票" value="vat_normal" />
|
||||
<el-option label="电子发票" value="electronic" /><el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="开票日期"><el-input v-model="invForm.invoice_date" placeholder="YYYY-MM-DD" /></el-form-item>
|
||||
<el-form-item label="供应商"><el-input v-model="invForm.supplier" /></el-form-item>
|
||||
<el-form-item label="报销单号"><el-input v-model="invForm.reimb_no" placeholder="如 BX202607050003" /></el-form-item>
|
||||
<el-form-item label="合同编号"><el-input v-model="invForm.contract_no" placeholder="如 HT-2026-018" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="invDialogVisible = false">取消</el-button>
|
||||
<el-button size="small" type="primary" :loading="savingInv" @click="saveInvoice">保存(自动校验)</el-button>
|
||||
</template>
|
||||
</MyDialog>
|
||||
|
||||
<!-- ═══ 社保对话框 ═══ -->
|
||||
<MyDialog v-model="ssDialogVisible" title="社保缴费记录" :width="520">
|
||||
<el-form label-width="100px" size="small">
|
||||
<el-form-item label="人员" required><el-input v-model="ssForm.employee" /></el-form-item>
|
||||
<el-form-item label="期间" required><el-input v-model="ssForm.period" placeholder="YYYY-MM" /></el-form-item>
|
||||
<el-form-item label="缴费基数"><el-input-number v-model="ssForm.base_amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="申报工资"><el-input-number v-model="ssForm.salary" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="单位缴纳"><el-input-number v-model="ssForm.company_amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="个人缴纳"><el-input-number v-model="ssForm.personal_amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="ssForm.remark" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button size="small" @click="ssDialogVisible = false">取消</el-button>
|
||||
<el-button size="small" type="primary" :loading="savingSs" @click="saveSs">保存(自动比对)</el-button>
|
||||
</template>
|
||||
</MyDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { taxApi } from '../api/index'
|
||||
import MyDialog from '../components/MyDialog.vue'
|
||||
|
||||
function getEntityId() {
|
||||
return Number(localStorage.getItem('cma_entity_id') || 1)
|
||||
}
|
||||
|
||||
// ── 状态 ──
|
||||
const activeTab = ref('dashboard')
|
||||
const stats = ref<any>({})
|
||||
const seeding = ref(false)
|
||||
const checkingAll = ref(false)
|
||||
|
||||
// 税负
|
||||
const taxRecords = ref<any[]>([])
|
||||
const loadingTax = ref(false)
|
||||
const checkingTax = ref(false)
|
||||
const taxFilters = reactive({ tax_type: '', period: '' })
|
||||
const trendChartRef = ref<HTMLElement | null>(null)
|
||||
const burdenChartRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// 发票
|
||||
const invoices = ref<any[]>([])
|
||||
const loadingInv = ref(false)
|
||||
const checkingInv = ref(false)
|
||||
const invFilters = reactive({ status: '', keyword: '' })
|
||||
|
||||
// 社保
|
||||
const ssRecords = ref<any[]>([])
|
||||
const loadingSs = ref(false)
|
||||
const checkingSs = ref(false)
|
||||
const ssFilters = reactive({ employee: '', period: '' })
|
||||
|
||||
// 对话框
|
||||
const taxDialogVisible = ref(false)
|
||||
const savingTax = ref(false)
|
||||
const taxForm = reactive<any>({ id: 0, period: '', tax_type: 'vat', tax_payable: 0, tax_paid: 0, income: 0, tax_rate: null, remark: '' })
|
||||
|
||||
const invDialogVisible = ref(false)
|
||||
const savingInv = ref(false)
|
||||
const invForm = reactive<any>({ id: 0, invoice_no: '', amount: 0, invoice_type: 'vat', invoice_date: '', supplier: '', reimb_no: '', contract_no: '' })
|
||||
|
||||
const ssDialogVisible = ref(false)
|
||||
const savingSs = ref(false)
|
||||
const ssForm = reactive<any>({ id: 0, employee: '', period: '', base_amount: 0, salary: null, company_amount: 0, personal_amount: 0, remark: '' })
|
||||
|
||||
// ── 工具函数 ──
|
||||
function fmt(v: any): string {
|
||||
if (v === null || v === undefined) return '0'
|
||||
return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
function taxTypeTag(t: string): string {
|
||||
const m: Record<string, string> = { vat: 'primary', income: 'success', surtax: 'warning' }
|
||||
return m[t] || 'info'
|
||||
}
|
||||
function burdenLabel(s: string): string {
|
||||
const m: Record<string, string> = { normal: '正常', alert: '超限' }
|
||||
return m[s] || s
|
||||
}
|
||||
function burdenTag(s: string): string {
|
||||
const m: Record<string, string> = { normal: 'success', alert: 'danger' }
|
||||
return m[s] || 'info'
|
||||
}
|
||||
function invStatusLabel(s: string): string {
|
||||
const m: Record<string, string> = { pending: '待校验', valid: '通过', invalid: '异常', warning: '提醒' }
|
||||
return m[s] || s
|
||||
}
|
||||
function invStatusTag(s: string): string {
|
||||
const m: Record<string, string> = { pending: 'info', valid: 'success', invalid: 'danger', warning: 'warning' }
|
||||
return m[s] || 'info'
|
||||
}
|
||||
function ssStatusLabel(s: string): string {
|
||||
const m: Record<string, string> = { normal: '正常', warning: '异常', alert: '漏缴' }
|
||||
return m[s] || s
|
||||
}
|
||||
function ssStatusTag(s: string): string {
|
||||
const m: Record<string, string> = { normal: 'success', warning: 'warning', alert: 'danger' }
|
||||
return m[s] || 'info'
|
||||
}
|
||||
|
||||
const hasTrend = ref(false)
|
||||
function refreshHasTrend() {
|
||||
const trend = stats.value?.burden?.trend || []
|
||||
hasTrend.value = trend.length > 0
|
||||
}
|
||||
|
||||
function switchTab(name: string) {
|
||||
activeTab.value = name
|
||||
}
|
||||
|
||||
// ── 看板 ──
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
const r: any = await taxApi.dashboard({ entity_id: getEntityId() })
|
||||
stats.value = r || {}
|
||||
refreshHasTrend()
|
||||
renderTrendChart()
|
||||
} catch (e: any) {
|
||||
ElMessage.error('看板加载失败: ' + (e?.response?.data?.detail || e.message))
|
||||
}
|
||||
}
|
||||
|
||||
async function seedDemo() {
|
||||
seeding.value = true
|
||||
try {
|
||||
const r: any = await taxApi.seedDemo({ entity_id: getEntityId() })
|
||||
ElMessage.success(r?.message || '演示数据已生成')
|
||||
await Promise.all([loadDashboard(), loadTaxRecords(), loadInvoices(), loadSs()])
|
||||
} catch (e: any) {
|
||||
ElMessage.error('生成失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
seeding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runAllChecks() {
|
||||
checkingAll.value = true
|
||||
try {
|
||||
await taxApi.runTaxCheck({ entity_id: getEntityId() })
|
||||
await taxApi.batchCheckInvoices({ entity_id: getEntityId() })
|
||||
await taxApi.batchCheckSs({ entity_id: getEntityId() })
|
||||
ElMessage.success('税负/发票/社保全部检查完成')
|
||||
await Promise.all([loadDashboard(), loadTaxRecords(), loadInvoices(), loadSs()])
|
||||
} catch (e: any) {
|
||||
ElMessage.error('检查失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
checkingAll.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── 税负 ──
|
||||
async function loadTaxRecords() {
|
||||
loadingTax.value = true
|
||||
try {
|
||||
const r: any = await taxApi.listTaxRecords({
|
||||
entity_id: getEntityId(),
|
||||
tax_type: taxFilters.tax_type || undefined,
|
||||
period: taxFilters.period || undefined,
|
||||
})
|
||||
taxRecords.value = r.data || []
|
||||
renderBurdenChart()
|
||||
} catch (e: any) {
|
||||
ElMessage.error('税负记录加载失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
loadingTax.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function runTaxCheck() {
|
||||
checkingTax.value = true
|
||||
try {
|
||||
const r: any = await taxApi.runTaxCheck({ entity_id: getEntityId() })
|
||||
ElMessage.success(r?.message || '税负检查完成')
|
||||
await Promise.all([loadDashboard(), loadTaxRecords()])
|
||||
} catch (e: any) {
|
||||
ElMessage.error('检查失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
checkingTax.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openTaxDialog(row?: any) {
|
||||
if (row) {
|
||||
Object.assign(taxForm, { id: row.id, period: row.period, tax_type: row.tax_type, tax_payable: row.tax_payable, tax_paid: row.tax_paid, income: row.income, tax_rate: row.tax_rate, remark: row.remark || '' })
|
||||
} else {
|
||||
Object.assign(taxForm, { id: 0, period: '', tax_type: 'vat', tax_payable: 0, tax_paid: 0, income: 0, tax_rate: null, remark: '' })
|
||||
}
|
||||
taxDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveTax() {
|
||||
if (!taxForm.period || !taxForm.tax_type) {
|
||||
ElMessage.warning('请填写期间和税种')
|
||||
return
|
||||
}
|
||||
savingTax.value = true
|
||||
try {
|
||||
const payload = { ...taxForm }
|
||||
delete payload.id
|
||||
if (taxForm.id) {
|
||||
await taxApi.updateTaxRecord(taxForm.id, payload)
|
||||
ElMessage.success('税务记录已更新')
|
||||
} else {
|
||||
await taxApi.createTaxRecord(payload)
|
||||
ElMessage.success('税务记录已创建')
|
||||
}
|
||||
taxDialogVisible.value = false
|
||||
await Promise.all([loadDashboard(), loadTaxRecords()])
|
||||
} catch (e: any) {
|
||||
ElMessage.error('保存失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
savingTax.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTax(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除 ${row.period} ${row.tax_type_label} 记录?`, '删除确认', { type: 'warning' })
|
||||
await taxApi.deleteTaxRecord(row.id)
|
||||
ElMessage.success('已删除')
|
||||
await Promise.all([loadDashboard(), loadTaxRecords()])
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── 发票 ──
|
||||
async function loadInvoices() {
|
||||
loadingInv.value = true
|
||||
try {
|
||||
const r: any = await taxApi.listInvoices({
|
||||
entity_id: getEntityId(),
|
||||
status: invFilters.status || undefined,
|
||||
keyword: invFilters.keyword || undefined,
|
||||
})
|
||||
invoices.value = r.data || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error('发票加载失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
loadingInv.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAbnormalInvoices() {
|
||||
loadingInv.value = true
|
||||
try {
|
||||
const r: any = await taxApi.abnormalInvoices({ entity_id: getEntityId(), limit: 100 })
|
||||
invoices.value = r.data || []
|
||||
invFilters.status = ''
|
||||
invFilters.keyword = ''
|
||||
if (!invoices.value.length) ElMessage.info('暂无异常发票')
|
||||
} catch (e: any) {
|
||||
ElMessage.error('加载失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
loadingInv.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function batchCheckInvoices() {
|
||||
checkingInv.value = true
|
||||
try {
|
||||
const r: any = await taxApi.batchCheckInvoices({ entity_id: getEntityId() })
|
||||
ElMessage.success(r?.message || '批量校验完成')
|
||||
await Promise.all([loadDashboard(), loadInvoices()])
|
||||
} catch (e: any) {
|
||||
ElMessage.error('校验失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
checkingInv.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openInvoiceDialog(row?: any) {
|
||||
if (row) {
|
||||
Object.assign(invForm, { id: row.id, invoice_no: row.invoice_no, amount: row.amount, invoice_type: row.invoice_type, invoice_date: (row.invoice_date || '').slice(0, 10), supplier: row.supplier || '', reimb_no: row.reimb_no || '', contract_no: row.contract_no || '' })
|
||||
} else {
|
||||
Object.assign(invForm, { id: 0, invoice_no: '', amount: 0, invoice_type: 'vat', invoice_date: '', supplier: '', reimb_no: '', contract_no: '' })
|
||||
}
|
||||
invDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveInvoice() {
|
||||
if (!invForm.invoice_no || invForm.amount == null) {
|
||||
ElMessage.warning('请填写发票号和金额')
|
||||
return
|
||||
}
|
||||
savingInv.value = true
|
||||
try {
|
||||
const payload: any = { ...invForm }
|
||||
delete payload.id
|
||||
if (!payload.invoice_date) delete payload.invoice_date
|
||||
if (!payload.supplier) delete payload.supplier
|
||||
if (!payload.reimb_no) delete payload.reimb_no
|
||||
if (!payload.contract_no) delete payload.contract_no
|
||||
if (invForm.id) {
|
||||
await taxApi.updateInvoice(invForm.id, payload)
|
||||
ElMessage.success('发票已更新并重新校验')
|
||||
} else {
|
||||
await taxApi.createInvoice(payload)
|
||||
ElMessage.success('发票已录入并校验')
|
||||
}
|
||||
invDialogVisible.value = false
|
||||
await Promise.all([loadDashboard(), loadInvoices()])
|
||||
} catch (e: any) {
|
||||
ElMessage.error('保存失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
savingInv.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteInvoice(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除发票 ${row.invoice_no}?`, '删除确认', { type: 'warning' })
|
||||
await taxApi.deleteInvoice(row.id)
|
||||
ElMessage.success('已删除')
|
||||
await Promise.all([loadDashboard(), loadInvoices()])
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── 社保 ──
|
||||
async function loadSs() {
|
||||
loadingSs.value = true
|
||||
try {
|
||||
const r: any = await taxApi.listSsRecords({
|
||||
entity_id: getEntityId(),
|
||||
employee: ssFilters.employee || undefined,
|
||||
period: ssFilters.period || undefined,
|
||||
})
|
||||
ssRecords.value = r.data || []
|
||||
} catch (e: any) {
|
||||
ElMessage.error('社保记录加载失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
loadingSs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAbnormalSs() {
|
||||
loadingSs.value = true
|
||||
try {
|
||||
const r: any = await taxApi.abnormalSs({ entity_id: getEntityId(), limit: 100 })
|
||||
ssRecords.value = r.data || []
|
||||
ssFilters.employee = ''
|
||||
ssFilters.period = ''
|
||||
if (!ssRecords.value.length) ElMessage.info('暂无社保异常')
|
||||
} catch (e: any) {
|
||||
ElMessage.error('加载失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
loadingSs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function batchCheckSs() {
|
||||
checkingSs.value = true
|
||||
try {
|
||||
const r: any = await taxApi.batchCheckSs({ entity_id: getEntityId() })
|
||||
ElMessage.success(r?.message || '社保比对完成')
|
||||
await Promise.all([loadDashboard(), loadSs()])
|
||||
} catch (e: any) {
|
||||
ElMessage.error('比对失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
checkingSs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openSsDialog(row?: any) {
|
||||
if (row) {
|
||||
Object.assign(ssForm, { id: row.id, employee: row.employee, period: row.period, base_amount: row.base_amount, salary: row.salary, company_amount: row.company_amount, personal_amount: row.personal_amount, remark: row.remark || '' })
|
||||
} else {
|
||||
Object.assign(ssForm, { id: 0, employee: '', period: '', base_amount: 0, salary: null, company_amount: 0, personal_amount: 0, remark: '' })
|
||||
}
|
||||
ssDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveSs() {
|
||||
if (!ssForm.employee || !ssForm.period) {
|
||||
ElMessage.warning('请填写人员和期间')
|
||||
return
|
||||
}
|
||||
savingSs.value = true
|
||||
try {
|
||||
const payload: any = { ...ssForm }
|
||||
delete payload.id
|
||||
if (payload.salary == null) delete payload.salary
|
||||
if (ssForm.id) {
|
||||
await taxApi.updateSsRecord(ssForm.id, payload)
|
||||
ElMessage.success('社保记录已更新')
|
||||
} else {
|
||||
await taxApi.createSsRecord(payload)
|
||||
ElMessage.success('社保记录已创建')
|
||||
}
|
||||
ssDialogVisible.value = false
|
||||
await Promise.all([loadDashboard(), loadSs()])
|
||||
} catch (e: any) {
|
||||
ElMessage.error('保存失败: ' + (e?.response?.data?.detail || e.message))
|
||||
} finally {
|
||||
savingSs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSs(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除 ${row.employee} ${row.period} 缴费记录?`, '删除确认', { type: 'warning' })
|
||||
await taxApi.deleteSsRecord(row.id)
|
||||
ElMessage.success('已删除')
|
||||
await Promise.all([loadDashboard(), loadSs()])
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// ── 图表 ──
|
||||
async function renderTrendChart() {
|
||||
await nextTick()
|
||||
if (!trendChartRef.value) return
|
||||
const echarts: any = await import('echarts')
|
||||
const el = trendChartRef.value
|
||||
const chart = echarts.getInstanceByDom(el) || echarts.init(el)
|
||||
const trend = stats.value?.burden?.trend || []
|
||||
const periods = trend.map((x: any) => x.period)
|
||||
const series: any[] = []
|
||||
const types: [string, string, string][] = [
|
||||
['vat_rate', '增值税税负率', '#F56C6C'],
|
||||
['income_rate', '所得税税负率', '#E6A23C'],
|
||||
['surtax_rate', '附加税税负率', '#909399'],
|
||||
]
|
||||
for (const [key, name, color] of types) {
|
||||
series.push({ name, type: 'line', smooth: true, data: trend.map((x: any) => x[key] ?? null), itemStyle: { color }, lineStyle: { width: 2.5 } })
|
||||
}
|
||||
const benchSeries: any[] = []
|
||||
const benchDefs: [string, string, string][] = [
|
||||
['vat_benchmark', '增值税行业均值 3.5%', '#F56C6C'],
|
||||
['income_benchmark', '所得税行业均值 2.5%', '#E6A23C'],
|
||||
]
|
||||
for (const [key, name, color] of benchDefs) {
|
||||
benchSeries.push({ name, type: 'line', data: trend.map((x: any) => x[key] ?? null), lineStyle: { type: 'dashed', width: 1.5, color }, itemStyle: { color }, symbol: 'none' })
|
||||
}
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { data: [...types.map(t => t[1]), ...benchDefs.map(b => b[1])], top: 0 },
|
||||
grid: { left: 60, right: 30, top: 40, bottom: 30 },
|
||||
xAxis: { type: 'category', data: periods },
|
||||
yAxis: { type: 'value', name: '税负率 %' },
|
||||
series: [...series, ...benchSeries],
|
||||
})
|
||||
}
|
||||
|
||||
async function renderBurdenChart() {
|
||||
await nextTick()
|
||||
if (!burdenChartRef.value) return
|
||||
const echarts: any = await import('echarts')
|
||||
const el = burdenChartRef.value
|
||||
const chart = echarts.getInstanceByDom(el) || echarts.init(el)
|
||||
const periods = [...new Set(taxRecords.value.map((r: any) => r.period))].sort()
|
||||
const byType: Record<string, Record<string, number>> = {}
|
||||
for (const r of taxRecords.value) {
|
||||
if (r.tax_burden_rate == null) continue
|
||||
;(byType[r.tax_type] = byType[r.tax_type] || {})[r.period] = r.tax_burden_rate
|
||||
}
|
||||
const series = Object.entries(byType).map(([type, m]: any) => ({
|
||||
name: TAX_LABELS[type] || type,
|
||||
type: 'line' as const,
|
||||
smooth: true,
|
||||
data: periods.map(p => m[p] ?? null),
|
||||
}))
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
legend: { top: 0 },
|
||||
grid: { left: 60, right: 30, top: 40, bottom: 30 },
|
||||
xAxis: { type: 'category', data: periods },
|
||||
yAxis: { type: 'value', name: '税负率 %' },
|
||||
series,
|
||||
})
|
||||
}
|
||||
|
||||
const TAX_LABELS: Record<string, string> = { vat: '增值税', income: '所得税', surtax: '附加税' }
|
||||
|
||||
// ── 生命周期 ──
|
||||
watch(activeTab, async (tab) => {
|
||||
if (tab === 'dashboard') {
|
||||
await nextTick()
|
||||
renderTrendChart()
|
||||
}
|
||||
if (tab === 'burden') {
|
||||
await nextTick()
|
||||
renderBurdenChart()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadDashboard()
|
||||
loadTaxRecords()
|
||||
loadInvoices()
|
||||
loadSs()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tax-compliance {
|
||||
padding: 4px;
|
||||
}
|
||||
.stat-cards {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px 18px;
|
||||
min-width: 96px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
text-align: center;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-2px); }
|
||||
.stat-num { font-size: 24px; font-weight: 700; line-height: 1.2; }
|
||||
.stat-num.alert { color: #F56C6C; }
|
||||
.stat-num.danger { color: #E6A23C; }
|
||||
.stat-num.warning { color: #F56C6C; }
|
||||
.stat-num.normal { color: #409EFF; }
|
||||
.stat-label { font-size: 12px; color: #909399; margin-top: 2px; }
|
||||
.stat-actions { margin-left: auto; display: flex; gap: 8px; }
|
||||
.tax-tabs :deep(.el-tabs__item) { font-weight: 500; }
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.panel {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.panel-sub { font-size: 12px; color: #909399; font-weight: 400; }
|
||||
.chart-lg { width: 100%; height: 320px; }
|
||||
.chart-md { width: 100%; height: 240px; }
|
||||
.empty-tip { color: #909399; font-size: 13px; padding: 20px 0; text-align: center; }
|
||||
.text-alert { color: #F56C6C; font-weight: 600; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user