feat: 应收款催收闭环 — 催收视图+博海230万录入+回款登记联动
This commit is contained in:
@@ -345,6 +345,10 @@ export const cashApi = {
|
||||
updatePlan: (id: number, data: any) => api.put(`/cash/plans/${id}`, data),
|
||||
deletePlan: (id: number) => api.delete(`/cash/plans/${id}`),
|
||||
completePlan: (id: number) => api.post(`/cash/plans/${id}/complete`),
|
||||
// 应收款催收
|
||||
receivables: (params?: any) => api.get('/cash/receivables', { params }),
|
||||
registerPayment: (id: number, data: any) => api.post(`/cash/receivables/${id}/payment`, data),
|
||||
importBohaiAR: () => api.post('/cash/import/bohai-ar', {}),
|
||||
// 到期提醒
|
||||
upcoming: (params?: any) => api.get('/cash/upcoming', { params }),
|
||||
// 页面看板(日历+预测+提醒)
|
||||
|
||||
@@ -9,8 +9,8 @@ interface MenuItem {
|
||||
|
||||
// ── 角色路由映射 ──
|
||||
export const ROLE_ROUTES: Record<string, string[]> = {
|
||||
ceo: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/growth-quality', '/tax-compliance'],
|
||||
finance: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/tax-compliance'],
|
||||
ceo: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/tax-compliance'],
|
||||
finance: ['/my-dashboard', '/dashboard', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/tax-compliance'],
|
||||
business: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/budget', '/deviations', '/action-plans', '/knowledge', '/guide', '/customer', '/expenses', '/cash-plan', '/tax-compliance'],
|
||||
it: ['/my-dashboard', '/dashboard', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/analysis-confidence', '/expenses', '/cash-plan', '/tax-compliance'],
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export const MENU_ITEMS: MenuItem[] = [
|
||||
{ path: '/expenses', label: '费用审核', icon: 'Money', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' },
|
||||
{ path: '/tax-compliance', label: '税务合规', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' },
|
||||
{ path: '/cash-plan', label: '收付款计划', icon: 'Money', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' },
|
||||
{ path: '/receivables', label: '应收款催收', icon: 'Money', roles: ['ceo', 'finance', 'business'], group: '🟢 D 执行与控制' },
|
||||
{ path: '/growth-quality', label: '增长质量诊断', icon: 'TrendCharts', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' },
|
||||
{ path: '/deviations', label: '差异分析', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business', 'it'], group: '🟢 D 执行与控制' },
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ const routes = [
|
||||
{ path: 'analysis-confidence', name: 'AnalysisConfidence', component: () => import('@/views/AnalysisConfidence.vue'), meta: { title: '分析置信度', roles: ['ceo', 'finance', 'it'] } },
|
||||
{ path: 'expenses', name: 'ExpenseManage', component: () => import('@/views/ExpenseManage.vue'), meta: { title: '费用审核', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'cash-plan', name: 'CashPlan', component: () => import('@/views/CashPlan.vue'), meta: { title: '收付款计划', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'receivables', name: 'Receivables', component: () => import('@/views/Receivables.vue'), meta: { title: '应收款催收', roles: ['ceo', 'finance', 'business'] } },
|
||||
{ path: 'growth-quality', name: 'GrowthQuality', component: () => import('@/views/GrowthQuality.vue'), meta: { title: '增长质量诊断', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
{ path: 'tax-compliance', name: 'TaxCompliance', component: () => import('@/views/TaxCompliance.vue'), meta: { title: '税务合规', roles: ['ceo', 'finance', 'business', 'it'] } },
|
||||
]
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div class="receivables-page">
|
||||
<div class="page-header">
|
||||
<h2>💰 应收款催收</h2>
|
||||
<div class="header-right">
|
||||
<el-select v-model="owner" size="small" clearable placeholder="按负责人筛选" style="width:140px" @change="loadData">
|
||||
<el-option v-for="o in owners" :key="o" :value="o" :label="o" />
|
||||
</el-select>
|
||||
<el-select v-model="status" size="small" clearable placeholder="按状态筛选" style="width:130px" @change="loadData">
|
||||
<el-option value="pending" label="待收款" />
|
||||
<el-option value="overdue" label="已逾期" />
|
||||
<el-option value="completed" label="已回款" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" @click="loadData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 汇总卡片 -->
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="6"><el-card shadow="never" class="stat-card red"><div class="stat-val">¥{{ (summary.total_balance || 0).toFixed(2) }}万</div><div class="stat-label">应收总额</div></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="never" class="stat-card orange"><div class="stat-val">¥{{ (summary.overdue_amount || 0).toFixed(2) }}万</div><div class="stat-label">逾期金额 ({{ summary.overdue_count || 0 }}笔)</div></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="never" class="stat-card green"><div class="stat-val">¥{{ (summary.total_paid || 0).toFixed(2) }}万</div><div class="stat-label">已回款</div></el-card></el-col>
|
||||
<el-col :span="6"><el-card shadow="never" class="stat-card blue"><div class="stat-val">{{ summary.completed_count || 0 }}</div><div class="stat-label">已完成笔数</div></el-card></el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 账龄分布 -->
|
||||
<el-row style="margin-top:16px">
|
||||
<el-col :span="24">
|
||||
<el-card shadow="never">
|
||||
<template #header>账龄分布(万元)</template>
|
||||
<div ref="agingRef" style="height:220px"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 应收明细 -->
|
||||
<el-row style="margin-top:16px">
|
||||
<el-col :span="24">
|
||||
<el-card shadow="never">
|
||||
<template #header>应收明细</template>
|
||||
<el-table :data="items" size="small" v-loading="loading">
|
||||
<el-table-column prop="name" label="客户/单位" min-width="140" />
|
||||
<el-table-column prop="owner" label="负责人" width="90" />
|
||||
<el-table-column prop="amount" label="金额(万)" width="90">
|
||||
<template #default="{ row }"><span style="font-weight:600">¥{{ row.amount }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="paid_amount" label="已回款(万)" width="100" />
|
||||
<el-table-column prop="due_date" label="到期日" width="100" />
|
||||
<el-table-column prop="aging_label" label="账龄" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.overdue ? 'danger' : (row.aging_label === '未到期' ? 'success' : 'warning')" size="small">
|
||||
{{ row.aging_label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'completed' ? 'success' : (row.overdue ? 'danger' : 'info')" size="small">
|
||||
{{ row.status === 'completed' ? '已回款' : row.overdue ? '已逾期' : '待收款' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status !== 'completed'" type="primary" size="small" @click="openPayment(row)">登记回款</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 回款登记弹窗 -->
|
||||
<el-dialog v-model="paymentVisible" title="登记回款" width="420px" append-to-body>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="客户">{{ currentRow?.name }}</el-form-item>
|
||||
<el-form-item label="应收金额">¥{{ currentRow?.amount }}万</el-form-item>
|
||||
<el-form-item label="回款金额">
|
||||
<el-input-number v-model="paymentAmount" :min="0" :max="currentRow?.amount || 0" :precision="2" style="width:180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="paymentNote" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="paymentVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="paying" @click="submitPayment">确认回款</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import api from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const items = ref<any[]>([])
|
||||
const owners = ref<string[]>([])
|
||||
const summary = ref<any>({})
|
||||
const owner = ref('')
|
||||
const status = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const paymentVisible = ref(false)
|
||||
const currentRow = ref<any>(null)
|
||||
const paymentAmount = ref(0)
|
||||
const paymentNote = ref('')
|
||||
const paying = ref(false)
|
||||
|
||||
const agingRef = ref<HTMLElement>()
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {}
|
||||
if (owner.value) params.owner = owner.value
|
||||
if (status.value) params.status = status.value
|
||||
const r: any = await api.get('/cash/receivables', { params })
|
||||
const d = r.data || r || {}
|
||||
summary.value = d.summary || {}
|
||||
owners.value = d.owners || []
|
||||
const rawItems = d.data || d.items || d.receivables || []
|
||||
items.value = rawItems
|
||||
await nextTick()
|
||||
renderAging()
|
||||
} catch (e) {
|
||||
ElMessage.error('加载应收数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function renderAging() {
|
||||
if (!agingRef.value) return
|
||||
const chart = echarts.init(agingRef.value)
|
||||
const aging = summary.value.aging || {}
|
||||
chart.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { type: 'category', data: Object.keys(aging) },
|
||||
yAxis: { type: 'value', name: '万元' },
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: Object.values(aging),
|
||||
itemStyle: { color: '#409EFF', borderRadius: [4, 4, 0, 0] },
|
||||
label: { show: true, position: 'top' },
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
function openPayment(row: any) {
|
||||
currentRow.value = row
|
||||
paymentAmount.value = row.amount || 0
|
||||
paymentNote.value = ''
|
||||
paymentVisible.value = true
|
||||
}
|
||||
|
||||
async function submitPayment() {
|
||||
if (!currentRow.value || paymentAmount.value <= 0) return
|
||||
paying.value = true
|
||||
try {
|
||||
await api.post(`/cash/receivables/${currentRow.value.id}/payment`, {
|
||||
amount: paymentAmount.value,
|
||||
note: paymentNote.value,
|
||||
})
|
||||
ElMessage.success('回款登记成功')
|
||||
paymentVisible.value = false
|
||||
loadData()
|
||||
} catch (e) {
|
||||
ElMessage.error('回款登记失败')
|
||||
} finally {
|
||||
paying.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadData)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.receivables-page { max-width: 1400px; margin: 0 auto; padding: 16px; }
|
||||
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
|
||||
.header-right { display: flex; gap: 8px; }
|
||||
.stat-card { text-align: center; }
|
||||
.stat-val { font-size: 24px; font-weight: 700; }
|
||||
.stat-label { font-size: 12px; color: #888; margin-top: 4px; }
|
||||
.red .stat-val { color: #f56c6c; }
|
||||
.orange .stat-val { color: #e6a23c; }
|
||||
.green .stat-val { color: #67c23a; }
|
||||
.blue .stat-val { color: #409EFF; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user