@@ -0,0 +1,557 @@
< template >
< div >
< h3 style = "margin-bottom: 12px;" > 收付款计划 · 资金缺口预测 < / h3 >
<!-- ─ ─ 顶部统计卡片 ─ ─ -- >
< el-row :gutter = "12" style = "margin-bottom: 12px;" >
< el-col :span = "4" >
< el-card shadow = "never" class = "stat-card" >
< div class = "stat-label" > 当前现金余额 ( 万元 ) < / div >
< div class = "stat-value" style = "display:flex;align-items:center;gap:6px;" >
< el-input-number v-model = "currentCash" :min="0" :precision="2" :controls="false" size="small" style="width:110px;" / >
< el-button size = "small" type = "primary" :loading = "balanceLoading" @click ="saveBalance" > 保存 < / el -button >
< / div >
< / el-card >
< / el-col >
< el-col :span = "4" >
< el-card shadow = "never" class = "stat-card" >
< div class = "stat-label" > { { dashMonth } } 应收计划 < / div >
< div class = "stat-value green" > + { { dash . month _receive ? . toFixed ( 1 ) ? ? '0.0' } } 万 < / div >
< / el-card >
< / el-col >
< el-col :span = "4" >
< el-card shadow = "never" class = "stat-card" >
< div class = "stat-label" > { { dashMonth } } 应付计划 < / div >
< div class = "stat-value red" > - { { dash . month _pay ? . toFixed ( 1 ) ? ? '0.0' } } 万 < / div >
< / el-card >
< / el-col >
< el-col :span = "4" >
< el-card shadow = "never" class = "stat-card" >
< div class = "stat-label" > { { dashMonth } } 净流入 < / div >
< div class = "stat-value" : class = "(dash.month_net ?? 0) >= 0 ? 'green' : 'red'" > { { dash . month _net ? . toFixed ( 1 ) ? ? '0.0' } } 万 < / div >
< / el-card >
< / el-col >
< el-col :span = "4" >
< el-card shadow = "never" class = "stat-card" >
< div class = "stat-label" > 未来30天最低余额 < / div >
< div class = "stat-value" :class = "minCashClass" > { { forecast . min _cash ? ? '--' } } 万 < / div >
< div class = "stat-sub" > { { forecast . min _cash _date || '' } } < / div >
< / el-card >
< / el-col >
< el-col :span = "4" >
< el-card shadow = "never" class = "stat-card" : class = "{ 'warn-card': (forecast.gap_dates?.length || 0) > 0 }" >
< div class = "stat-label" > 资金缺口预警 < / div >
< div class = "stat-value" : class = "(forecast.gap_dates?.length || 0) > 0 ? 'red' : 'green'" >
{ { forecast . gap _dates ? . length || 0 } } 天
< / div >
< el-button size = "small" style = "margin-top:6px;" @click ="runAlertCheck" :loading = "alertChecking" >
{ { alertCheckMsg } }
< / el-button >
< / el-card >
< / el-col >
< / el-row >
< el-row :gutter = "12" >
<!-- ─ ─ 收付款日历视图 ─ ─ -- >
< el-col :span = "16" >
< el-card shadow = "never" >
< template # header >
< div style = "display:flex;justify-content:space-between;align-items:center;" >
< span > 📅 收付款日历 — { { calTitle } } < / span >
< div >
< el-button size = "small" @click ="shiftMonth(-1)" > ‹ 上月 < / el -button >
< el-button size = "small" @click ="shiftMonth(1)" > 下月 › < / el -button >
< el-button size = "small" type = "primary" @click ="openCreate" > + 新增计划 < / el -button >
< / div >
< / div >
< / template >
< div class = "cal-grid" >
< div v-for = "wd in ['一','二','三','四','五','六','日']" :key="wd" class="cal-wd" > {{ wd }} < / div >
< div v-for = "(cell, i) in calCells" :key="i" class="cal-cell"
: class = "{ 'cal-today': cell && cell.date === todayStr, 'cal-gap': cell && cell.gap, 'cal-empty': !cell }" >
< template v-if = "cell" >
< div class = "cal-day" : class = "{ 'gap-day': cell.gap }" > { { cell . day } } < / div >
< div class = "cal-item in" v-for = "(it, k) in cell.cell.items.filter((x: any) => x.plan_type === 'receive').slice(0, 3)" :key="'i' + k" :title="it.counterparty + ' ' + it.description" >
+ {{ it.amount }} < span class = "cal-ct" > { { it . counterparty } } < / span >
< / div >
< div class = "cal-item out" v-for = "(it, k) in cell.cell.items.filter((x: any) => x.plan_type === 'pay').slice(0, 3)" :key="'o' + k" :title="it.counterparty + ' ' + it.description" >
- {{ it.amount }} < span class = "cal-ct" > { { it . counterparty } } < / span >
< / div >
< div class = "cal-item more" v-if = "cell.cell.items.length > 6" > … 还有 {{ cell.cell.items.length - 6 }} 笔 < / div >
< / template >
< / div >
< / div >
< div style = "margin-top:8px;font-size:12px;color:#909399;display:flex;gap:16px;" >
< span > < i class = "dot" style = "background:#67c23a;" > < / i > 收款 < / span >
< span > < i class = "dot" style = "background:#f56c6c;" > < / i > 付款 < / span >
< span > < i class = "dot" style = "background:#e6a23c;" > < / i > 资金缺口日 ( 余额 & lt ; 警戒线 ) < / span >
< span style = "margin-left:auto;" > 警戒线 { { forecast . warning _line ? ? 20 } } 万 / 危急线 { { forecast . critical _line ? ? 10 } } 万 < / span >
< / div >
< / el-card >
< / el-col >
<!-- ─ ─ 到期提醒 ─ ─ -- >
< el-col :span = "8" >
< el-card shadow = "never" >
< template # header >
< div style = "display:flex;justify-content:space-between;align-items:center;" >
< span > ⏰ 未来7天到期提醒 < / span >
< el-tag v-if = "(upcoming.upcoming?.length || 0) + (upcoming.overdue?.length || 0) > 0" type="danger" size="small" >
{{ ( upcoming.upcoming ? .length | | 0 ) + ( upcoming.overdue ? .length | | 0 ) }} 条
< / el -tag >
< / div >
< / template >
< div v-if = "upcoming.overdue?.length" class="remind-block" >
< div class = "remind-title" style = "color:#f56c6c;" > 🔴 已逾期 < / div >
< div v-for = "p in upcoming.overdue" :key="'od' + p.id" class="remind-item" >
< el -tag : type = "p.plan_type === 'receive' ? 'success' : 'danger'" size = "small" > { { p . plan _type _label } } < / el-tag >
< span class = "remind-amt" > { { p . amount } } 万 < / span >
< span class = "remind-ct" > { { p . counterparty || '—' } } < / span >
< span class = "remind-date" > { { p . plan _date } } < / span >
< el-button size = "small" text type = "primary" @click ="markComplete(p)" > 到账 < / el -button >
< / div >
< / div >
< div v-if = "upcoming.upcoming?.length" class="remind-block" >
< div class = "remind-title" > 🟡 未来7天到期 < / div >
< div v-for = "p in upcoming.upcoming" :key="'up' + p.id" class="remind-item" >
< el -tag : type = "p.plan_type === 'receive' ? 'success' : 'danger'" size = "small" > { { p . plan _type _label } } < / el-tag >
< span class = "remind-amt" > { { p . amount } } 万 < / span >
< span class = "remind-ct" > { { p . counterparty || '—' } } < / span >
< span class = "remind-date" > { { p . plan _date } } < / span >
< el-button v-if = "p.plan_type === 'receive'" size="small" text type="primary" @click="markComplete(p)" > 到账 < / el -button >
< / div >
< / div >
< el-empty v-if = "!(upcoming.overdue?.length) && !(upcoming.upcoming?.length)" description="未来7天无到期计划" :image-size="60" / >
< div class = "remind-title" style = "margin-top:14px;" > 🚨 缺口前3天预警 ( 预测 ) < / div >
< div v-if = "forecast.pre_alerts?.length" >
< el -alert v-for = "(a, i) in forecast.pre_alerts" :key="i" :type="a.level === 'red' ? 'error' : 'warning'"
:closable = "false" style = "margin-bottom:6px;" >
< template # title >
< b > { { a . alert _date } } < / b > 前安排资金 : { { a . gap _date } } 余额预计 { { a . gap _cash } } 万 ( 最低 { { a . worst _cash } } 万 / { { a . worst _date } } )
< / template >
< / el-alert >
< / div >
< el-empty v-else description = "近期无资金缺口预警" :image-size = "60" / >
< / el-card >
< / el-col >
< / el-row >
<!-- ─ ─ 资金余额预测图 ─ ─ -- >
< el-card shadow = "never" style = "margin-top:12px;" >
< template # header >
< div style = "display:flex;justify-content:space-between;align-items:center;" >
< span > 📈 未来30天资金余额预测 ( 趋势引擎 + 收付款计划 ) < / span >
< el-radio-group v-model = "forecastDays" size="small" @change="reloadForecast" >
< el -radio -button :value = "30" > 30 天 < / el-radio-button >
< el-radio-button :value = "60" > 60 天 < / el-radio-button >
< el-radio-button :value = "90" > 90 天 < / el-radio-button >
< / el-radio-group >
< / div >
< / template >
< div ref = "chartRef" style = "height:380px;width:100%;" > < / div >
< / el-card >
<!-- ─ ─ 收付款计划列表 ─ ─ -- >
< el-card shadow = "never" style = "margin-top:12px;" >
< template # header >
< div style = "display:flex;justify-content:space-between;align-items:center;" >
< span > 📋 收付款计划明细 < / span >
< div >
< el-select v-model = "planFilter.type" size="small" clearable placeholder="全部类型" style="width:110px;margin-right:6px;" @change="loadPlans" >
< el -option label = "收款" value = "receive" / >
< el-option label = "付款" value = "pay" / >
< / el-select >
< el-select v-model = "planFilter.status" size="small" clearable placeholder="全部状态" style="width:110px;" @change="loadPlans" >
< el -option label = "待执行" value = "pending" / >
< el-option label = "已完成" value = "completed" / >
< el-option label = "已取消" value = "cancelled" / >
< / el-select >
< / div >
< / div >
< / template >
< el-table :data = "plans" border stripe size = "small" >
< el-table-column prop = "plan_date" label = "计划日期" width = "110" / >
< el-table-column label = "类型" width = "80" >
< template # default = "{ row }" >
< el-tag : type = "row.plan_type === 'receive' ? 'success' : 'danger'" size = "small" > { { row . plan _type _label } } < / el-tag >
< / template >
< / el-table-column >
< el-table-column prop = "amount" label = "金额(万)" width = "100" align = "right" >
< template # default = "{ row }" >
< span : class = "row.plan_type === 'receive' ? 'green' : 'red'" > { { row . plan _type === 'receive' ? '+' : '-' } } { { row . amount } } < / span >
< / template >
< / el-table-column >
< el-table-column prop = "counterparty" label = "往来单位" min -width = " 130 " / >
< el-table-column prop = "description" label = "说明" min -width = " 160 " show -overflow -tooltip / >
< el-table-column label = "状态" width = "90" >
< template # default = "{ row }" >
< el-tag : type = "row.status === 'completed' ? 'info' : row.status === 'cancelled' ? 'warning' : 'primary'" size = "small" > { { row . status _label } } < / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "操作" width = "190" fixed = "right" >
< template # default = "{ row }" >
< el-button v-if = "row.status === 'pending'" size="small" text type="success" @click="markComplete(row)" > 完成 < / el -button >
< el-button size = "small" text type = "primary" @click ="openEdit(row)" > 编辑 < / el -button >
< el-button size = "small" text type = "danger" @click ="removePlan(row)" > 删除 < / el -button >
< / template >
< / el-table-column >
< / el-table >
< / el-card >
<!-- ─ ─ 新建 / 编辑弹窗 ─ ─ -- >
< el-dialog v-model = "dialogVisible" :title="form.id ? '编辑收付款计划' : '新建收付款计划'" width="480px" >
< el -form :model = "form" label -width = " 90px " >
< el-form-item label = "类型" required >
< el-radio-group v-model = "form.plan_type" >
< el -radio -button value = "receive" > 收款 < / el-radio-button >
< el-radio-button value = "pay" > 付款 < / el-radio-button >
< / el-radio-group >
< / el-form-item >
< el-form-item label = "金额(万)" required >
< el-input-number v-model = "form.amount" :min="0.01" :precision="2" style="width:100%;" / >
< / el-form-item >
< el-form-item label = "计划日期" required >
< el-date-picker v-model = "form.plan_date" type="date" value-format="YYYY-MM-DD" style="width:100%;" / >
< / el-form-item >
< el-form-item label = "往来单位" >
< el-input v-model = "form.counterparty" placeholder="客户/供应商名称" / >
< / el-form-item >
< el-form-item label = "说明" >
< el-input v-model = "form.description" type="textarea" :rows="2" placeholder="收款/付款事由" / >
< / el-form-item >
< el-form-item v-if = "form.id" label="状态" >
< el -select v-model = "form.status" style="width:100%;" >
< el -option label = "待执行" value = "pending" / >
< el-option label = "已完成" value = "completed" / >
< el-option label = "已取消" value = "cancelled" / >
< / el-select >
< / el-form-item >
< / el-form >
< template # footer >
< el-button @click ="dialogVisible = false" > 取消 < / el -button >
< el-button type = "primary" :loading = "saving" @click ="savePlan" > 保存 < / el -button >
< / template >
< / el-dialog >
< / div >
< / template >
< script setup lang = "ts" >
import { ref , computed , reactive , onMounted , onBeforeUnmount , nextTick } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { cashApi } from '../api/index'
const entityId = Number ( localStorage . getItem ( 'cma_entity_id' ) || 1 )
// ── 状态 ──
const currentCash = ref < number > ( 30 )
const balanceLoading = ref ( false )
const dash = ref < any > ( { } )
const forecast = ref < any > ( { } )
const upcoming = ref < any > ( { } )
const plans = ref < any [ ] > ( [ ] )
const forecastDays = ref ( 30 )
const alertChecking = ref ( false )
const alertCheckMsg = ref ( '触发预警检查' )
const saving = ref ( false )
// ── 日历 ──
const now = new Date ( )
const curY = now . getFullYear ( )
const curM = now . getMonth ( ) + 1
const viewMonth = ref ( ` ${ curY } - ${ String ( curM ) . padStart ( 2 , '0' ) } ` )
const todayStr = ` ${ curY } - ${ String ( curM ) . padStart ( 2 , '0' ) } - ${ String ( now . getDate ( ) ) . padStart ( 2 , '0' ) } `
const dashMonth = computed ( ( ) => dash . value . month || viewMonth . value . slice ( 0 , 7 ) )
const calTitle = computed ( ( ) => ` ${ viewMonth . value . slice ( 0 , 4 ) } 年 ${ Number ( viewMonth . value . slice ( 5 , 7 ) ) } 月 ` )
const minCashClass = computed ( ( ) => {
const v = forecast . value . min _cash
if ( v === undefined || v === null ) return ''
return v < ( forecast . value . critical _line ? ? 10 ) ? 'red' : v < ( forecast . value . warning _line ? ? 20 ) ? 'orange' : 'green'
} )
// 日历格子:周一开头
const calCells = computed ( ( ) => {
const [ y , m ] = viewMonth . value . split ( '-' ) . map ( Number )
const first = new Date ( y , m - 1 , 1 )
const offset = ( first . getDay ( ) + 6 ) % 7
const daysInMonth = new Date ( y , m , 0 ) . getDate ( )
const cells : any [ ] = [ ]
for ( let i = 0 ; i < offset ; i ++ ) cells . push ( null )
const gapSet = new Set ( ( forecast . value . gap _dates || [ ] ) . map ( ( g : any ) => g . date ) )
for ( let d = 1 ; d <= daysInMonth ; d ++ ) {
const key = ` ${ y } - ${ String ( m ) . padStart ( 2 , '0' ) } - ${ String ( d ) . padStart ( 2 , '0' ) } `
cells . push ( {
day : d ,
date : key ,
cell : ( dash . value . calendar || { } ) [ key ] || { receive : 0 , pay : 0 , items : [ ] } ,
gap : gapSet . has ( key ) ,
} )
}
while ( cells . length % 7 !== 0 ) cells . push ( null )
return cells
} )
// ── 筛选 ──
const planFilter = reactive ( { type : '' , status : '' } )
// ── 弹窗表单 ──
const dialogVisible = ref ( false )
const form = reactive < any > ( { id : null , plan _type : 'receive' , amount : 10 , plan _date : '' , counterparty : '' , description : '' , status : 'pending' } )
// ── 数据加载 ──
async function loadAll ( ) {
await Promise . all ( [ loadBalance ( ) , loadDashboard ( ) , loadPlans ( ) ] )
}
async function loadBalance ( ) {
try {
const r : any = await cashApi . getBalance ( { entity _id : entityId } )
if ( r . current _cash !== null && r . current _cash !== undefined ) currentCash . value = r . current _cash
} catch ( _ ) { }
}
async function loadDashboard ( ) {
try {
const params : any = { entity _id : entityId , days : forecastDays . value , month : viewMonth . value }
if ( currentCash . value > 0 ) params . current _cash = currentCash . value
const r : any = await cashApi . dashboard ( params )
dash . value = r
forecast . value = r . forecast || { }
upcoming . value = r . upcoming || { }
await nextTick ( )
renderChart ( )
} catch ( e : any ) {
ElMessage . error ( '加载看板失败: ' + ( e ? . response ? . data ? . detail || e . message ) )
}
}
async function reloadForecast ( ) {
await loadDashboard ( )
}
async function loadPlans ( ) {
try {
const params : any = { entity _id : entityId , page _size : 200 }
if ( planFilter . type ) params . plan _type = planFilter . type
if ( planFilter . status ) params . status = planFilter . status
const r : any = await cashApi . listPlans ( params )
plans . value = r . data || [ ]
} catch ( _ ) { }
}
async function saveBalance ( ) {
balanceLoading . value = true
try {
await cashApi . setBalance ( { current _cash : currentCash . value } )
ElMessage . success ( '当前现金余额已保存' )
await loadDashboard ( )
} catch ( e : any ) {
ElMessage . error ( '保存失败: ' + ( e ? . response ? . data ? . detail || e . message ) )
} finally {
balanceLoading . value = false
}
}
// ── 日历翻月 ──
function shiftMonth ( delta : number ) {
const [ y , m ] = viewMonth . value . split ( '-' ) . map ( Number )
const d = new Date ( y , m - 1 + delta , 1 )
viewMonth . value = ` ${ d . getFullYear ( ) } - ${ String ( d . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) } `
loadDashboard ( )
}
// ── 预测图 ──
const chartRef = ref < HTMLElement > ( )
let chart : any = null
function renderChart ( ) {
if ( ! chartRef . value || ! forecast . value . forecast ? . length ) return
import ( 'echarts' ) . then ( ( echarts : any ) => {
if ( chart ) chart . dispose ( )
chart = echarts . init ( chartRef . value ! )
const fc = forecast . value . forecast
const dates = fc . map ( ( d : any ) => d . date . slice ( 5 ) )
const cash = fc . map ( ( d : any ) => d . predicted _cash )
const plannedIn = fc . map ( ( d : any ) => d . planned _in )
const plannedOut = fc . map ( ( d : any ) => - d . planned _out )
const lower = fc . map ( ( d : any ) => d . lower _bound )
const upper = fc . map ( ( d : any ) => d . upper _bound )
const warningLine = forecast . value . warning _line ? ? 20
const criticalLine = forecast . value . critical _line ? ? 10
chart . setOption ( {
tooltip : {
trigger : 'axis' ,
formatter : function ( params : any ) {
const idx = params [ 0 ] ? . dataIndex
const d = fc [ idx ]
if ( ! d ) return ''
const st = d . alert _status === 'red' ? '🔴 预警' : d . alert _status === 'yellow' ? '🟡 关注' : '🟢 安全'
const plansTxt = ( d . plans || [ ] ) . map ( ( p : any ) => ` <br/> ${ p . type === 'receive' ? '收' : '付' } ${ p . counterparty || '' } ${ p . amount } 万 ` ) . join ( '' )
return ` <strong> ${ d . date } </strong><br/>余额: <strong> ${ d . predicted _cash . toFixed ( 2 ) } 万</strong> ( ${ st } )<br/>当日计划收: ${ d . planned _in . toFixed ( 2 ) } 万 / 付: ${ d . planned _out . toFixed ( 2 ) } 万<br/>净变动: ${ d . net _flow . toFixed ( 2 ) } 万 ${ plansTxt } `
}
} ,
legend : { bottom : 0 , icon : 'circle' , itemWidth : 8 , itemHeight : 8 } ,
grid : { left : 50 , right : 20 , top : 30 , bottom : 45 } ,
xAxis : { type : 'category' , data : dates , axisLabel : { interval : Math . ceil ( fc . length / 15 ) } } ,
yAxis : { type : 'value' , name : '万元' } ,
series : [
{
name : '预测余额' , type : 'line' , data : cash , smooth : true , symbol : 'none' ,
lineStyle : { width : 2.5 , color : '#409eff' } ,
markLine : {
silent : true , symbol : 'none' ,
label : { formatter : '警戒线 {c}万' } ,
data : [
{ yAxis : warningLine , lineStyle : { color : '#e6a23c' , type : 'dashed' , width : 1.5 } , label : { formatter : ` 警戒线 ${ warningLine } 万 ` , color : '#e6a23c' , position : 'insideEndTop' } } ,
{ yAxis : criticalLine , lineStyle : { color : '#f56c6c' , type : 'dashed' , width : 1.5 } , label : { formatter : ` 危急线 ${ criticalLine } 万 ` , color : '#f56c6c' } } ,
] ,
} ,
markPoint : {
symbolSize : 42 ,
data : ( forecast . value . gap _dates || [ ] ) . map ( ( g : any ) => ( {
coord : [ g . date . slice ( 5 ) , g . predicted _cash ] ,
value : '缺口' ,
itemStyle : { color : g . level === 'red' ? '#f56c6c' : '#e6a23c' } ,
} ) ) ,
} ,
} ,
{
name : '计划收款' , type : 'bar' , data : plannedIn , stack : 'plan' , barWidth : '60%' ,
itemStyle : { color : '#67c23a' , opacity : 0.55 } ,
} ,
{
name : '计划付款' , type : 'bar' , data : plannedOut , stack : 'plan' , barWidth : '60%' ,
itemStyle : { color : '#f56c6c' , opacity : 0.55 } ,
} ,
{
name : '置信上界' , type : 'line' , data : upper , smooth : true , symbol : 'none' ,
lineStyle : { width : 1 , color : '#909399' , type : 'dashed' } ,
} ,
{
name : '置信下界' , type : 'line' , data : lower , smooth : true , symbol : 'none' ,
lineStyle : { width : 1 , color : '#909399' , type : 'dashed' } ,
areaStyle : { color : 'rgba(144,147,153,0.05)' } ,
} ,
] ,
} )
} )
}
// ── CRUD ──
function openCreate ( ) {
Object . assign ( form , { id : null , plan _type : 'receive' , amount : 10 , plan _date : todayStr , counterparty : '' , description : '' , status : 'pending' } )
dialogVisible . value = true
}
function openEdit ( row : any ) {
Object . assign ( form , {
id : row . id , plan _type : row . plan _type , amount : row . amount , plan _date : row . plan _date ,
counterparty : row . counterparty , description : row . description , status : row . status ,
} )
dialogVisible . value = true
}
async function savePlan ( ) {
if ( ! form . plan _date ) { ElMessage . warning ( '请选择计划日期' ) ; return }
if ( ! form . amount || form . amount <= 0 ) { ElMessage . warning ( '请输入金额' ) ; return }
saving . value = true
try {
const payload = { plan _type : form . plan _type , amount : form . amount , plan _date : form . plan _date , counterparty : form . counterparty , description : form . description , status : form . status }
if ( form . id ) {
await cashApi . updatePlan ( form . id , payload )
ElMessage . success ( '计划已更新' )
} else {
await cashApi . createPlan ( { entity _id : entityId , ... payload } )
ElMessage . success ( '计划已创建' )
}
dialogVisible . value = false
await loadDashboard ( )
await loadPlans ( )
} catch ( e : any ) {
ElMessage . error ( '保存失败: ' + ( e ? . response ? . data ? . detail || e . message ) )
} finally {
saving . value = false
}
}
async function markComplete ( row : any ) {
try {
await cashApi . completePlan ( row . id )
ElMessage . success ( '已标记完成' )
await loadDashboard ( )
await loadPlans ( )
} catch ( e : any ) {
ElMessage . error ( '操作失败: ' + ( e ? . response ? . data ? . detail || e . message ) )
}
}
async function removePlan ( row : any ) {
try {
await ElMessageBox . confirm ( ` 确认删除 ${ row . plan _type _label } ${ row . amount } 万 ( ${ row . counterparty || '—' } )? ` , '删除确认' , { type : 'warning' } )
await cashApi . deletePlan ( row . id )
ElMessage . success ( '已删除' )
await loadDashboard ( )
await loadPlans ( )
} catch ( _ ) { }
}
// ── 预警检查 ──
async function runAlertCheck ( ) {
alertChecking . value = true
alertCheckMsg . value = '检查中…'
try {
const r : any = await cashApi . checkAlerts ( { entity _id : entityId } )
if ( r . new _alerts > 0 ) {
ElMessage . success ( ` 已生成 ${ r . new _alerts } 条资金预警 ` )
} else {
ElMessage . info ( '无新增预警(已检查)' )
}
await loadDashboard ( )
} catch ( e : any ) {
ElMessage . error ( '预警检查失败: ' + ( e ? . response ? . data ? . detail || e . message ) )
} finally {
alertChecking . value = false
alertCheckMsg . value = '触发预警检查'
}
}
// ── 生命周期 ──
onMounted ( ( ) => { loadAll ( ) } )
onBeforeUnmount ( ( ) => { if ( chart ) { chart . dispose ( ) ; chart = null } } )
< / script >
< style scoped >
. stat - card { text - align : center ; }
. stat - label { font - size : 12 px ; color : # 909399 ; margin - bottom : 8 px ; }
. stat - value { font - size : 20 px ; font - weight : 700 ; color : # 303133 ; }
. stat - sub { font - size : 12 px ; color : # 909399 ; margin - top : 4 px ; }
. stat - value . green { color : # 67 c23a ; }
. stat - value . red { color : # f56c6c ; }
. stat - value . orange { color : # e6a23c ; }
. warn - card { background : # fef0f0 ; }
. cal - grid { display : grid ; grid - template - columns : repeat ( 7 , 1 fr ) ; gap : 4 px ; }
. cal - wd { text - align : center ; font - size : 12 px ; color : # 909399 ; padding : 4 px 0 ; }
. cal - cell { min - height : 74 px ; border : 1 px solid # ebeef5 ; border - radius : 6 px ; padding : 4 px 6 px ; background : # fff ; cursor : default ; }
. cal - cell . cal - today { border - color : # 409 eff ; box - shadow : 0 0 0 1 px # 409 eff inset ; }
. cal - cell . cal - gap { background : # fdf6ec ; border - color : # e6a23c ; }
. cal - cell . cal - empty { background : # fafafa ; }
. cal - day { font - size : 13 px ; font - weight : 600 ; color : # 606266 ; margin - bottom : 2 px ; }
. cal - day . gap - day { color : # e6a23c ; }
. cal - item { font - size : 11 px ; line - height : 16 px ; white - space : nowrap ; overflow : hidden ; text - overflow : ellipsis ; }
. cal - item . in { color : # 67 c23a ; }
. cal - item . out { color : # f56c6c ; }
. cal - item . more { color : # 909399 ; }
. cal - ct { color : # 909399 ; }
. dot { display : inline - block ; width : 8 px ; height : 8 px ; border - radius : 50 % ; margin - right : 4 px ; }
. remind - block { margin - bottom : 6 px ; }
. remind - title { font - size : 13 px ; font - weight : 600 ; color : # 606266 ; margin : 6 px 0 4 px ; }
. remind - item { display : flex ; align - items : center ; gap : 6 px ; padding : 5 px 0 ; border - bottom : 1 px dashed # f0f0f0 ; font - size : 13 px ; }
. remind - item : last - child { border - bottom : none ; }
. remind - amt { font - weight : 700 ; color : # 303133 ; }
. remind - ct { color : # 606266 ; flex : 1 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. remind - date { color : # 909399 ; font - size : 12 px ; }
< / style >