@@ -116,11 +116,11 @@ function renderFinance() {
const finHeaderBase = ` <span class="text-sm text-slate-500">视图:</span><select onchange="setFinView(this.value)" class="text-sm font-medium py-1 px-2.5 cursor-pointer" style="appearance:none;-webkit-appearance:none;background-color:transparent;border:0;outline:none;background:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E') no-repeat right 4px center;padding-right:22px;min-height:30px"><option value="overview" ${ state . finView === 'overview' ? 'selected' : '' } >总视图</option><option value="quarterly" ${ state . finView === 'quarterly' ? 'selected' : '' } >季度视图</option><option value="monthly" ${ state . finView === 'monthly' ? 'selected' : '' } >月度视图</option></select> ${ toolDateSelect } ` ;
const finAddBtn = ` <button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button> ` ;
const finFilterTabs = ` <div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('已签约')" class="finance-tab ${ state . finFilter === '已签约' ? ' active' : '' } " style="font-size:14px;font-weight:600">已签约 ( ${ pfs . filter ( x => x . status === '已签约' ) . length } )</button><button onclick="switchFinFilter('待签约')" class="finance-tab ${ state . finFilter === '待签约' ? ' active' : '' } " style="font-size:14px;font-weight:600">待签约 ( ${ pfs . filter ( x => x . status === '待签约' ) . length } )</button><button onclick="switchFinFilter('expense')" class="finance-tab ${ state . finFilter === 'expense' ? ' active' : '' } " style="font-size:14px;font-weight:600">平台费用</button></div> ` ;
const finFilterTabs = ` <div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0"><button onclick="switchFinFilter('overview')" class="finance-tab ${ state . finFilter === 'overview' ? ' active' : '' } " style="font-size:14px;font-weight:600">总览</button><button onclick="switchFinFilter('已签约')" class="finance-tab ${ state . finFilter === '已签约' ? ' active' : '' } " style="font-size:14px;font-weight:600">已签约 ( ${ pfs . filter ( x => x . status === '已签约' ) . length } )</button><button onclick="switchFinFilter('待签约')" class="finance-tab ${ state . finFilter === '待签约' ? ' active' : '' } " style="font-size:14px;font-weight:600">待签约 ( ${ pfs . filter ( x => x . status === '待签约' ) . length } )</button><button onclick="switchFinFilter('expense')" class="finance-tab ${ state . finFilter === 'expense' ? ' active' : '' } " style="font-size:14px;font-weight:600">平台费用</button></div> ` ;
document . querySelector ( "#finance" ) . innerHTML = ` <div class="grid gap-4">
${ finFilterTabs }
${ state . finFilter !== 'expense' ? card ( ` <div class="flex justify-between items-center"><div class="flex items-center gap-2"> ${ finHeaderBase } </div> ${ finAddBtn } </div> ` , "p-4" ) : '' }
${ state . finFilter !== 'expense' && state . finFilter !== 'overview' ? card ( ` <div class="flex justify-between items-center"><div class="flex items-center gap-2"> ${ finHeaderBase } </div> ${ finAddBtn } </div> ` , "p-4" ) : '' }
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="financeModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
<div class="finance-tabs">
<button class="finance-tab active" data-tab="info" onclick="switchFinanceTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
@@ -237,6 +237,10 @@ function renderFinance() {
< / d i v >
< / d i v > < d i v c l a s s = " f l e x j u s t i f y - e n d g a p - 3 p t - 2 f i n a n c e - f o r m - a c t i o n s " > < b u t t o n t y p e = " b u t t o n " c l a s s = " b t n b t n - g h o s t b t n - s m p x - 6 " o n c l i c k = " c l o s e F i n a n c e M o d a l ( ) " > 取 消 < / b u t t o n > < b u t t o n t y p e = " s u b m i t " c l a s s = " b t n b t n - p r i m a r y b t n - s m p x - 8 " > 保 存 < / b u t t o n > < / d i v > < / f o r m > < / d i v > < / d i v >
$ { ( ( ) => {
if ( state . finFilter === 'overview' ) {
setTimeout ( ( ) => renderFinanceOverview ( ) , 10 ) ;
return ` <div id="financeOverview"></div> ` ;
}
if ( state . finFilter === 'expense' ) {
setTimeout ( ( ) => renderExpense ( ) , 10 ) ;
return ` <div id="financeExpense"></div> ` ;
@@ -876,4 +880,95 @@ window.deleteFinanceItem = async () => {
} catch ( error ) {
toast ( "删除失败:" + error . message , "error" ) ;
}
}
// 总览渲染
function renderFinanceOverview ( ) {
const { summary , financeMonthly } = state . data ;
if ( ! summary ) return ;
const m = summary . metrics ;
const moneyIntL = ( v ) => ` ${ Math . round ( Number ( v || 0 ) ) . toLocaleString ( "zh-CN" ) } 元 ` ;
const moneyWan = ( v ) => {
const n = Number ( v || 0 ) ;
return n >= 10000 ? ( n / 10000 ) . toFixed ( 0 ) + "万" : n . toLocaleString ( "zh-CN" ) ;
} ;
const card = ( body , cls ) => '<div class="card ' + ( cls || 'p-4' ) + '">' + body + '</div>' ;
const rows1 = [ [ "年度累计" , moneyIntL ( m . signed _annual || m . signed _amount ) ] , [ "本季度累计" , moneyIntL ( m . signed _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . signed _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . signed _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . signed _prev _month || 0 ) ] ] ;
const rows2 = [ [ "年度累计" , moneyIntL ( m . revenue _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . revenue _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . monthly _revenue || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . revenue _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . revenue _prev _month || 0 ) ] ] ;
const rows3 = [ [ "年度累计" , moneyIntL ( m . gross _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . gross _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . monthly _net _profit || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . gross _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . gross _prev _month || 0 ) ] ] ;
const rows4 = [ [ "年度累计" , moneyIntL ( m . cost _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . cost _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . cost _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . cost _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . cost _prev _month || 0 ) ] ] ;
const rows5 = [ [ "年度累计" , moneyIntL ( m . profit _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . profit _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . profit _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . profit _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . profit _prev _month || 0 ) ] ] ;
const rows7 = [ [ "年度累计" , moneyIntL ( m . payment _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . payment _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . payment _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . payment _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . payment _prev _month || 0 ) ] ] ;
const rows8 = [ [ "年度累计" , moneyIntL ( m . paid _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . paid _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . paid _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . paid _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . paid _prev _month || 0 ) ] ] ;
const rows9 = [ [ "年度累计" , moneyIntL ( m . cashflow _annual || 0 ) ] , [ "本季度累计" , moneyIntL ( m . cashflow _q2 || 0 ) ] , [ "本月累计" , moneyIntL ( m . cashflow _month || 0 ) ] , [ "上本季度累计" , moneyIntL ( m . cashflow _prev _q || 0 ) ] , [ "上月累计" , moneyIntL ( m . cashflow _prev _month || 0 ) ] ] ;
const qoq = ( cur , prev ) => { if ( ! prev ) return '<span class="text-slate-300">—</span>' ; const pct = Math . round ( ( cur - prev ) / prev * 100 ) ; const cls = pct >= 0 ? 'text-green-600' : 'text-red-600' ; const sign = pct >= 0 ? '+' : '' ; return '<span class="' + cls + '">' + sign + pct + '%</span>' ; } ;
const LABELS = [ '合同金额' , '确收金额' , '确收毛利' , '成本' , '项目利润' , '回款金额' , '已付' , '现金流' ] ;
const Q _FIELDS = [ m . signed _q2 || 0 , m . revenue _q2 , m . gross _q2 , m . cost _q2 || 0 , m . profit _q2 || 0 , m . payment _q2 || 0 , m . paid _q2 || 0 , m . cashflow _q2 || 0 ] ;
const Q _PREV = [ m . signed _prev _q || 0 , m . revenue _prev _q || 0 , m . gross _prev _q || 0 , m . cost _prev _q || 0 , m . profit _prev _q || 0 , m . payment _prev _q || 0 , m . paid _prev _q || 0 , m . cashflow _prev _q || 0 ] ;
const M _FIELDS = [ m . signed _month || 0 , m . monthly _revenue , m . monthly _net _profit , m . cost _month || 0 , m . profit _month || 0 , m . payment _month || 0 , m . paid _month || 0 , m . cashflow _month || 0 ] ;
const M _PREV = [ m . signed _prev _month || 0 , m . revenue _prev _month || 0 , m . gross _prev _month || 0 , m . cost _prev _month || 0 , m . profit _prev _month || 0 , m . payment _prev _month || 0 , m . paid _prev _month || 0 , m . cashflow _prev _month || 0 ] ;
const ROWS = [ rows1 , rows2 , rows3 , rows4 , rows5 , rows7 , rows8 , rows9 ] ;
const CF _RAW = [ m . cashflow _annual || 0 , m . cashflow _q2 || 0 , m . cashflow _month || 0 , m . cashflow _prev _q || 0 , m . cashflow _prev _month || 0 ] ;
const PF _RAW = [ m . profit _annual || 0 , m . profit _q2 || 0 , m . profit _month || 0 , m . profit _prev _q || 0 , m . profit _prev _month || 0 ] ;
var tdVal = function ( ri , col , val ) {
if ( ri === 4 ) return '<td class="py-2 text-right font-semibold ' + ( PF _RAW [ col ] >= 0 ? 'text-green-600' : 'text-red-600' ) + '">' + val + '</td>' ;
if ( ri === 7 ) return '<td class="py-2 text-right font-semibold ' + ( CF _RAW [ col ] >= 0 ? 'text-green-600' : 'text-red-600' ) + '">' + val + '</td>' ;
return '<td class="py-2 text-right font-semibold text-slate-800">' + val + '</td>' ;
} ;
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">上季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th><th class="py-2 text-right font-semibold text-slate-700">本季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月累计</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th></tr>' ;
var tbody = '' ;
for ( var ri = 0 ; ri < 8 ; ri ++ ) {
var vals = ROWS [ ri ] ;
tbody += '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700">' + LABELS [ ri ] + '</td>' +
tdVal ( ri , 0 , vals [ 0 ] [ 1 ] ) + tdVal ( ri , 3 , vals [ 3 ] [ 1 ] ) + tdVal ( ri , 4 , vals [ 4 ] [ 1 ] ) + tdVal ( ri , 1 , vals [ 1 ] [ 1 ] ) +
'<td class="py-2 text-right">' + qoq ( Q _FIELDS [ ri ] , Q _PREV [ ri ] ) + '</td>' +
tdVal ( ri , 2 , vals [ 2 ] [ 1 ] ) +
'<td class="py-2 text-right">' + qoq ( M _FIELDS [ ri ] , M _PREV [ ri ] ) + '</td></tr>' ;
}
document . querySelector ( "#financeOverview" ) . innerHTML = '<div class="grid gap-5">' +
card ( '<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>' , 'p-4' ) +
'<div class="grid grid-cols-4 gap-5">' +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartSign"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartRev"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与已付</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartCash"></canvas></div>' , 'p-4' ) +
card ( '<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度项目利润</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="finChartProfit"></canvas></div>' , 'p-4' ) +
'</div></div>' ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
// 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels)
if ( financeMonthly && window . Chart ) {
renderOverviewCharts ( financeMonthly ) ;
}
}
function renderOverviewCharts ( data ) {
if ( typeof monthLabels !== 'function' ) return ;
const labels = monthLabels ( data ) ;
const baseOpts = typeof chartOptions === 'function' ? chartOptions ( moneyTick ) : { responsive : true , maintainAspectRatio : false } ;
var iconf = { type : "line" , options : baseOpts , data : { labels : labels } } ;
var c1 = document . querySelector ( "#finChartSign" ) ;
if ( c1 && window . Chart ) {
if ( state . finChart1 ) state . finChart1 . destroy ( ) ;
state . finChart1 = new Chart ( c1 , Object . assign ( { } , iconf , { data : { labels : labels , datasets : [ { label : "签约金额" , data : data . map ( function ( x ) { return x . sign || 0 ; } ) , borderColor : "#6366f1" , backgroundColor : "rgba(99,102,241,0.06)" , fill : true , tension : 0.3 } ] } } ) ) ;
}
var c2 = document . querySelector ( "#finChartRev" ) ;
if ( c2 && window . Chart ) {
if ( state . finChart2 ) state . finChart2 . destroy ( ) ;
state . finChart2 = new Chart ( c2 , Object . assign ( { } , iconf , { data : { labels : labels , datasets : [ { label : "确收" , data : data . map ( function ( x ) { return x . revenue || 0 ; } ) , borderColor : "#2563eb" , backgroundColor : "rgba(37,99,235,0.06)" , fill : true , tension : 0.3 } , { label : "毛利" , data : data . map ( function ( x ) { return x . gross || 0 ; } ) , borderColor : "#059669" , backgroundColor : "rgba(5,150,105,0.06)" , fill : true , tension : 0.3 } ] } } ) ) ;
}
var c3 = document . querySelector ( "#finChartCash" ) ;
if ( c3 && window . Chart ) {
if ( state . finChart3 ) state . finChart3 . destroy ( ) ;
state . finChart3 = new Chart ( c3 , Object . assign ( { } , iconf , { data : { labels : labels , datasets : [ { label : "回款" , data : data . map ( function ( x ) { return x . payment || 0 ; } ) , borderColor : "#d97706" , backgroundColor : "rgba(217,119,6,0.06)" , fill : true , tension : 0.3 } , { label : "已付" , data : data . map ( function ( x ) { return x . cost || 0 ; } ) , borderColor : "#7c3aed" , backgroundColor : "rgba(124,58,237,0.06)" , fill : true , tension : 0.3 } ] } } ) ) ;
}
var c4 = document . querySelector ( "#finChartProfit" ) ;
if ( c4 && window . Chart ) {
if ( state . finChart4 ) state . finChart4 . destroy ( ) ;
state . finChart4 = new Chart ( c4 , Object . assign ( { } , iconf , { data : { labels : labels , datasets : [ { label : "利润" , data : data . map ( function ( x ) { return x . gross || 0 ; } ) , borderColor : "#6366f1" , backgroundColor : "rgba(99,102,241,0.06)" , fill : true , tension : 0.3 } ] } } ) ) ;
}
} ;