@@ -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 : 4 px ;
}
. stat - cards {
display : flex ;
align - items : center ;
gap : 14 px ;
margin - bottom : 14 px ;
flex - wrap : wrap ;
}
. stat - card {
background : # fff ;
border - radius : 8 px ;
padding : 12 px 18 px ;
min - width : 96 px ;
cursor : pointer ;
box - shadow : 0 1 px 4 px rgba ( 0 , 0 , 0 , 0.06 ) ;
text - align : center ;
transition : transform 0.15 s ;
}
. stat - card : hover { transform : translateY ( - 2 px ) ; }
. stat - num { font - size : 24 px ; 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 : # 409 EFF ; }
. stat - label { font - size : 12 px ; color : # 909399 ; margin - top : 2 px ; }
. stat - actions { margin - left : auto ; display : flex ; gap : 8 px ; }
. tax - tabs : deep ( . el - tabs _ _item ) { font - weight : 500 ; }
. toolbar {
display : flex ;
gap : 8 px ;
margin - bottom : 12 px ;
flex - wrap : wrap ;
align - items : center ;
}
. panel {
background : # fff ;
border - radius : 8 px ;
padding : 14 px 16 px ;
margin - bottom : 14 px ;
box - shadow : 0 1 px 4 px rgba ( 0 , 0 , 0 , 0.06 ) ;
}
. panel - title {
font - size : 14 px ;
font - weight : 600 ;
color : # 303133 ;
margin - bottom : 10 px ;
display : flex ;
align - items : center ;
justify - content : space - between ;
}
. panel - sub { font - size : 12 px ; color : # 909399 ; font - weight : 400 ; }
. chart - lg { width : 100 % ; height : 320 px ; }
. chart - md { width : 100 % ; height : 240 px ; }
. empty - tip { color : # 909399 ; font - size : 13 px ; padding : 20 px 0 ; text - align : center ; }
. text - alert { color : # F56C6C ; font - weight : 600 ; }
< / style >