@@ -1,6 +1,7 @@
// finance.js — 经营管理(财务)模块
const moneyInt = ( v ) => ` ${ Math . round ( Number ( v || 0 ) ) . toLocaleString ( "zh-CN" ) } 元 ` ;
const moneyWan = ( v ) => ` ${ ( Number ( v || 0 ) / 10000 ) . toFixed ( 1 ) } 万 ` ;
function renderFinance ( ) {
const pfs = state . data . projectFinances || [ ] ;
@@ -73,7 +74,7 @@ function renderFinance() {
const budget = JSON . parse ( pf . budget _data || "[]" ) ;
budget . forEach ( b => { budgetMap [ ( b . month || "" ) . replace ( "-" , "_" ) ] = b ; } ) ;
} catch ( e ) { }
const isRevView = state . finView !== "cashflow" && state . finView !== "overview" && state . finView !== "monthly" ;
const isRevView = state . finView !== "cashflow" && state . finView !== "overview" && state . finView !== "monthly" && state . finView !== "quarterly" ;
const mCols = months . map ( m => {
const b = budgetMap [ m ] || { } ;
if ( isRevView ) {
@@ -99,53 +100,55 @@ function renderFinance() {
} ) ( ) ;
const sm = pf . sign _month || "" ;
const signMonthCell = ` <td class="p-2 text-center text-sm"><span class="pf-sm-text cursor-pointer hover:text-blue-600" id="pf-sm- ${ pf . id } " onclick="event.stopPropagation(); editPfSignMonth(event, ${ pf . id } )"> ${ sm || '—' } </span></td> ` ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center"> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-sm text-center"> ${ esc ( pf . business _type ) } </td><td class="p-2 text-sm text-center"> ${ pf . status === "已签约" ? badge ( "已签约" ) : pf . status === "流程中" ? badge ( "流程中" , "blue" ) : badge ( "待签约" , "amber" ) } </td> ${ signMonthCell } <td class="p-2 text-center text-sm"> ${ money ( pf . sign _amount ) } </td> ${ mCols } ${ totalCol } </tr> ` ;
return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center"> ${ esc ( pf . customer _name ) } </td> ${ signMonthCell } <td class="p-2 text-center text-sm"> ${ money ( pf . sign _amount ) } </td> ${ mCols } ${ totalCol } </tr> ` ;
} ;
const finHeaderBase = ` <button class="btn btn-sm ${ state . finView === 'overview' ? 'btn-primary' : 'btn-ghost' } px-2 py-1.5" data-view="overview" onclick="setFinView('overview')">总视图</button><button class="btn btn-sm ${ state . finView === 'monthly' ? 'btn-primary' : 'btn-ghost' } px-2 py-1.5" data-view="monthly" onclick="setFinView('monthly')">月度</button><button class="btn btn-sm ${ state . finView === 'quarterly' ? 'btn-primary' : 'btn-ghost' } px-2 py-1.5" data-view="quarterly" onclick="setFinView('quarterly')">季度</button><span class="text-slate-300 mx-0.5">|</span><span class="text-xs text-slate-400">筛选:</span><span class="text-xs text-slate-500">状态:</span><select onchange="state.finFilter=this.value;renderFinance()" class="text-xs 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="已签约" ${ state . finFilter === '已签约' ? 'selected' : '' } >已签约 ( ${ pfs . filter ( x => x . status === '已签约' ) . length } )</option><option value="流程中" ${ state . finFilter === '流程中' ? 'selected' : '' } >流程中 ( ${ pfs . filter ( x => x . status === '流程中' ) . length } )</option><option value="待签约" ${ state . finFilter === '待签约' ? 'selected' : '' } >待签约 ( ${ pfs . filter ( x => x . status === '待签约' ) . length } )</option></select> ` ;
const finAddBtn = ` <button class="btn btn-primary btn-sm" onclick="openFinanceModal()">新增财务项目</button> ` ;
document . querySelector ( "#finance" ) . innerHTML = ` <div class="grid gap-4">
<div class="grid grid-cols-4 gap-3">
${ [ [ "已签项目" , "" + signed . length , "file-check-2" ] , [ "签约金额" , moneyInt ( sumSign ) , "coins" ] , [ "待签项目" , "" + pending . length , "file-question" ] , [ "待签金额" , moneyInt ( sumPending ) , "hourglass" ] ] . map ( ( [ l , v , icon ] ) => ` <div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide=" ${ icon } " style="width:14px;height:14px"></i> ${ l } </span><strong class="mt-2 block text-2xl"> ${ v } </strong></div> ` ) . join ( "" ) }
${ [ [ "已签项目" , "" + signed . length , "file-check-2" ] , [ "签约金额" , moneyWan ( sumSign ) , "coins" ] , [ "待签项目" , "" + pending . length , "file-question" ] , [ "待签金额" , moneyWan ( sumPending ) , "hourglass" ] ] . map ( ( [ l , v , icon ] ) => ` <div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide=" ${ icon } " style="width:14px;height:14px"></i> ${ l } </span><strong class="mt-2 block text-2xl"> ${ v } </strong></div> ` ) . join ( "" ) }
</div>
<div class="grid grid-cols-5 gap-3">
${ [ [ "本月确收" , moneyInt ( thisMonthRev ) , "trending-up" ] , [ "本月毛利" , moneyInt ( thisMonthGross ) , "percent" ] , [ "本月回款" , moneyInt ( monthPayment ) , "wallet" ] , [ "本月应付" , moneyInt ( monthCost ) , "receipt" ] , [ "本月现金流" , moneyInt ( monthCashflow ) , "repeat" ] ] . map ( ( [ l , v , icon ] ) => ` <div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide=" ${ icon } " style="width:14px;height:14px"></i> ${ l } </span><strong class="mt-2 block text-2xl"> ${ v } </strong></div> ` ) . join ( "" ) }
</div>
<div class="flex justify-between items-center"><div class="flex items-center gap-1" id="finViewToggle"><button class="btn btn-sm ${ state . finView === 'overview' ? 'btn-primary' : 'btn-ghost' } px-2 py-1.5" data-view="overview" onclick="setFinView('overview')" title="总视图"><i data-lucide="layout-dashboard" style="width:16px;height:16px"></i><span class="text-xs ml-1">总视图</span></button><button class="btn btn-sm ${ state . finView === 'monthly' ? 'btn-primary' : 'btn-ghost' } px-2 py-1.5" data-view="monthly" onclick="setFinView('monthly')" title="月度视图"><i data-lucide="calendar-days" style="width:16px;height:16px"></i><span class="text-xs ml-1">月度视图</span></button></div><button class="btn btn-primary btn-sm" onclick="openFinanceModal()"><i data-lucide="plus"></i>新增财务项目</button></div>
<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>
<button class="finance-tab" data-tab="budget" onclick="switchFinanceTab('budget')"><i data-lucide="calendar" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>月度 流水</button>
<button class="finance-tab" data-tab="budget" onclick="switchFinanceTab('budget')"><i data-lucide="calendar" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>项目 流水</button>
<button class="finance-tab" data-tab="exec" onclick="switchFinanceTab('exec')"><i data-lucide="play-circle" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>执行信息</button>
<button class="finance-tab" data-tab="tasks" onclick="switchFinanceTab('tasks')"><i data-lucide="list-checks" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>任务管理</button>
<button class="finance-tab" data-tab="activity" onclick="switchFinanceTab('activity')"><i data-lucide="message-square" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>活动与跟进</button>
</div>
<form onsubmit="createFinance(event)" class="p-8 grid gap-6 " novalidate><input type="hidden" name="pf_id" id="pf-id-input" value="">
<form onsubmit="createFinance(event)" class="finance-form " novalidate><input type="hidden" name="pf_id" id="pf-id-input" value="">
<div class="finance-tab-body">
<div id="financeTabInfo">
<div class="grid grid-cols-2 gap-5">
<div class="fin-field-group">
<p class="fin-section-label">项目信息</p>
<div class="grid gap-4">
<div class="grid grid-cols-3 gap-3">
<label class="block"><span class="fin-label">部门</span><input type="hidden" name="project_id" value=" ${ state . tenant } "><input class="form-ctrl bg-slate-50 cursor-not-allowed" value=" ${ state . tenant } " disabled></label>
<div class="grid grid-cols-2 gap-3">
<label class="block"><span class="fin-label">项目编号</span><input name="project_code" class="form-ctrl" placeholder="如: KP-2026-001"></label>
<label class="block"><span class="fin-label">业务类型</span><select name="business_type" class="form-ctrl bg-white"> ${ fmTypes . map ( t => ` <option> ${ t } </option> ` ) . join ( "" ) } </select></label>
</div>
<label class="block"><span class="fin-label">项目名称 <span class="text-red-500">*</span></span><input name="customer_name" required class="form-ctrl" placeholder="请输入项目名称"></label>
</div>
</ div>
<div class="fin-field-group">
<p class="fin-section-label">合同信息</p>
<div class="grid gap-4">
<div class="grid grid-cols-3 gap-3" >
<label class="block"><span class="fin-label">签约金额(元) <span class="text-red-500">*</span></span><input name="sign_amount" class="form-ctrl" placeholder="必须大于 0"></label>
<div class="grid grid-cols-2 gap-3">
<label class="block"><span class="fin-label">签约月份 <span class="text-red-500">*</span></span><select name="sign_month" required class="form-ctrl bg-white"><option value="">选择</option> ${ monthOptions ( '' ) } </select></label>
<label class="block"><span class="fin-label">项目状态</span><select name="status" class="form-ctrl bg-white"><option>已签约</option><option>流程中</option><option>待签约</option></select></label>
</div>
<div class="grid grid-cols-2 gap-3">
<div class="grid grid-cols-3 gap-3">
<label class="block"><span class="fin-label">商务负责人 <span class="text-red-500">*</span></span><input name="sales_person" required class="form-ctrl" placeholder="请输入商务负责人"></label>
<label class="block"><span class="fin-label">经营负责人 <span class="text-red-500">*</span></span><input name="owner" required class="form-ctrl" placeholder="请输入经营负责人"></label>
<label class="block"><span class="fin-label">业务联系人</span><input name="contact_name" class="form-ctrl" placeholder="请输入业务联系人"></label>
</div>
<div class="grid grid-cols-3 gap-3">
<label class="block"><span class="fin-label">联系电话</span><input name="contact_phone" class="form-ctrl" placeholder="请输入联系电话"></label>
<label class="block"><span class="fin-label">其他</span><input name="other_info" class="form-ctrl" placeholder="备注信息"></label>
</div>
<div class="block"><span class="fin-label">业务类型</span><div class="flex flex-wrap gap-2 mt-1" id="businessTypeChecks"> ${ fmTypes . map ( t => ` <label class="inline-flex items-center gap-1 px-2.5 py-1 rounded-full border border-slate-200 cursor-pointer hover:border-blue-300 text-sm bt-chip" onclick="toggleBtChip(this)"><input type="checkbox" name="business_type[]" value=" ${ t } " class="hidden"><span> ${ t } </span></label> ` ) . join ( "" ) } </div></div>
</div>
</div>
<div class="fin-field-group mt-5 ">
</div>
<div id="financeTabExec" class="hidden ">
<div class="fin-field-group">
<p class="fin-section-label">执行信息</p>
<div class="grid grid-cols-3 gap-4">
<label class="block"><span class="fin-label">开始时间</span><input name="start_date" type="date" class="form-ctrl"></label>
@@ -191,7 +194,30 @@ function renderFinance() {
< / t a b l e >
< button type = "button" class = "btn btn-ghost btn-sm mt-3" onclick = "addTaskRow()" > < i data - lucide = "plus" > < / i > 添 加 任 务 < / b u t t o n >
< / d i v >
< div class = "flex justify-end gap-3 pt-2" > < button type = "button" class = "btn btn-ghost btn-sm px-6" onclick = "closeFinanceModal()" > 取消 < / 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 >
< div id = "financeTabActivity" class = "hidden" >
< div class = "grid gap-2" id = "finActivityList" > < / d i v >
< div class = "comment-box mt-3" >
< div class = "squire-toolbar" >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('bold')" title = "加粗" > < i data - lucide = "bold" > < / i > < / b u t t o n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('italic')" title = "斜体" > < i data - lucide = "italic" > < / i > < / b u t t o n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('underline')" title = "下划线" > < i data - lucide = "underline" > < / i > < / b u t t o n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('strikethrough')" title = "删除线" > < i data - lucide = "strikethrough" > < / i > < / b u t t o n >
< span class = "squire-sep" > < / s p a n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('makeUnorderedList')" title = "无序列表" > < i data - lucide = "list" > < / i > < / b u t t o n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('makeOrderedList')" title = "有序列表" > < i data - lucide = "list-ordered" > < / i > < / b u t t o n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('blockquote')" title = "引用" > < i data - lucide = "quote" > < / i > < / b u t t o n >
< span class = "squire-sep" > < / s p a n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('undo')" title = "撤销" > < i data - lucide = "undo" > < / i > < / b u t t o n >
< button type = "button" class = "squire-btn" onmousedown = "event.preventDefault();squireCmd('redo')" title = "重做" > < i data - lucide = "redo" > < / i > < / b u t t o n >
< / d i v >
< div class = "squire-editor" id = "squire_finance" placeholder = "添加评论" > < / d i v >
< div class = "comment-toolbar" >
< span class = "comment-hint" > 支持富文本编辑 < / s p a n >
< button class = "btn btn-primary btn-sm comment-submit" type = "button" onclick = "submitFinComment()" > 评论 < / b u t t o n >
< / d i v >
< / d i v >
< / 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 >
$ { state . finView === 'monthly' ? ( ( ) => {
const allPfs = pfs . filter ( x => x . status === state . finFilter ) ;
const now = new Date ( ) ;
@@ -219,9 +245,45 @@ function renderFinance() {
const costDiff = cost - paid ;
const cashflow = payment - paid ;
sumRev += rev ; sumPay += payment ; sumCost += cost ; sumPaid += paid ;
rows . push ( ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center"> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-sm text-center text-slate-500"> ${ esc ( pf . business _type ) } </td><td class="p-2 text-sm text-center "> ${ pf . status === '已签约' ? badge ( '已签约' ) : pf . status === '流程中' ? badge ( '流程中' , 'blue' ) : badge ( '待签约' , 'amber' ) } </td><td class="p-2 text-center text-blue-700 align-middle"> ${ fmt ( rev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ fmt ( payment ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( payDiff ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmt ( cost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ fmt ( paid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( costDiff ) } </td><td class="p-2 text-center font-semibold align-middle ${ cashflow >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ cashflow ? money ( cashflow ) : '<span class="text-slate-300">—</span>' } </td></tr> ` ) ;
rows . push ( ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center"> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-center text-sm "> ${ pf . status === '已签约' ? '<span class="text-green-600">已签约</span>' : pf . status === '流程中' ? '<span class="text-blue-600">流程中</span>' : '<span class="text-amber-600">待签约</span>' } </td><td class="p-2 text-center text-blue-700 align-middle"> ${ fmt ( rev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ fmt ( payment ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( payDiff ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmt ( cost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ fmt ( paid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( costDiff ) } </td><td class="p-2 text-center font-semibold align-middle ${ cashflow >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ cashflow ? money ( cashflow ) : '<span class="text-slate-300">—</span>' } </td></tr> ` ) ;
} ) ;
return card ( ` <div class="flex items-center justify-between mb-3"><h3 class="font-bold text-slate-700">月度视图 <span class="text-slate-400 font-normal">( ${ allPfs . length } 项目)</span></h3></div><div class="flex items-center justify-between mb-3"><div class="flex gap-2"> ${ [ [ "已签约" , "已签约" ] , [ "流程中" , "流程中" ] , [ "待签约" , "待签约" ] ] . map ( ( [ k , v ] ) => ` <button class="btn btn-sm ${ state . finFilter === k ? 'btn-primary' : 'btn-ghost' } " onclick="state.finFilter=' ${ k } ';renderFinance()"> ${ v } ( ${ pfs . filter ( x => x . status === k ) . length } )</button> ` ) . join ( "" ) } </div><label class="inline-flex items-center gap-1 text-sm text-slate-500 cursor-pointer relative"><i data-lucide="calendar" style="width:14px;height:14px"></i ><select onchange="state.finMonth=this.value;renderFinance()" class="bg-transparent border-0 text-sm text-slate-600 font-medium cursor-pointer" style="appearance:none;-webkit-appearance:none;-moz-appearance:none;outline:none;background-image:none;padding-right:4 px"> ${ monthOpts } </select></label> </div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold align-middle">项目名称</th><th class="p-2 text-center font-semibold align-middle">类型</th><th class="p-2 text-center font-semibold align-middle">状态</th><th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th><th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th><th class="p-2 text-center font-semibold align-middle">回款差额</th><th class="p-2 text-center font-semibold align-middle text-rose-600">应付</th><th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th><th class="p-2 text-center font-semibold align-middle">应付差额</th><th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th></tr></thead><tbody> ${ rows . length ? rows . join ( "" ) : '<tr><td colspan="10 " class="p-6 text-center text-slate-400">该月份暂无数据</td></tr>' } <tr class="border-t-2 border-slate-200 bg-slate-50 font-bold"><td class="p-2 text-center" colspan="3 ">合计</td><td class="p-2 text-center text-blue-700 align-middle"> ${ money ( sumRev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ money ( sumPay ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumRev - sumPay ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ money ( sumCost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ money ( sumPaid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumCost - sumPaid ) } </td><td class="p-2 text-center font-semibold align-middle ${ sumPay - sumPaid >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ money ( sumPay - sumPaid ) } </td></tr></tbody></table></div> ` , "p-4" ) ;
return card ( ` <div class="flex justify-between items-center mb-3"><div class="flex items-center gap-2"> ${ finHeaderBase } <span class="text-xs text-slate-500">月份:</span ><select onchange="state.finMonth=this.value;renderFinance()" class="text-xs 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:30 px"> ${ monthOpts } </select></div> ${ finAddBtn } </div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold align-middle">项目名称</th><th class="p-2 text-center font-semibold align-middle">状态</th><th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th><th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th><th class="p-2 text-center font-semibold align-middle">回款差额</th><th class="p-2 text-center font-semibold align-middle text-rose-600">应付</th><th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th><th class="p-2 text-center font-semibold align-middle">应付差额</th><th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th></tr></thead><tbody> ${ rows . length ? rows . join ( "" ) : '<tr><td colspan="9 " class="p-6 text-center text-slate-400">该月份暂无数据</td></tr>' } <tr class="border-t-2 border-slate-200 bg-slate-50 font-bold"><td class="p-2 text-center" colspan="2 ">合计</td><td class="p-2 text-center text-blue-700 align-middle"> ${ money ( sumRev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ money ( sumPay ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumRev - sumPay ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ money ( sumCost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ money ( sumPaid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumCost - sumPaid ) } </td><td class="p-2 text-center font-semibold align-middle ${ sumPay - sumPaid >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ money ( sumPay - sumPaid ) } </td></tr></tbody></table></div> ` , "p-4" ) ;
} ) ( ) : state . finView === 'quarterly' ? ( ( ) => {
const allPfs = pfs . filter ( x => x . status === state . finFilter ) ;
const qRanges = [ [ 1 , 2 , 3 ] , [ 4 , 5 , 6 ] , [ 7 , 8 , 9 ] , [ 10 , 11 , 12 ] ] ;
const qLabels = [ "Q1 (1-3月)" , "Q2 (4-6月)" , "Q3 (7-9月)" , "Q4 (10-12月)" ] ;
const now = new Date ( ) ;
if ( state . finQuarter === undefined ) {
const saved = localStorage . getItem ( "opc-fin-quarter" ) ;
state . finQuarter = saved !== null ? parseInt ( saved ) : Math . floor ( now . getMonth ( ) / 3 ) ;
}
const selQ = state . finQuarter ;
const qRange = qRanges [ selQ ] ;
const quarterOpts = qLabels . map ( ( l , i ) => ` <option value=" ${ i } " ${ i === selQ ? 'selected' : '' } > ${ l } </option> ` ) . join ( "" ) ;
const sumBudget = ( pf , field ) => {
let total = 0 ;
try { JSON . parse ( pf . budget _data || "[]" ) . forEach ( b => {
const m = parseInt ( ( b . month || "" ) . substring ( 5 ) ) || 0 ;
if ( qRange . includes ( m ) ) total += parseFloat ( b [ field ] || 0 ) ;
} ) ; } catch ( e ) { }
return total ;
} ;
const fmt = ( v ) => v ? ` <span class="font-medium"> ${ money ( v ) } </span> ` : '<span class="text-slate-300">—</span>' ;
const fmtDiff = ( v ) => { if ( ! v ) return '<span class="text-slate-300">—</span>' ; return ` <span class=" ${ v > 0 ? 'text-amber-600 font-medium' : 'text-green-600 font-medium' } "> ${ money ( Math . abs ( v ) ) } </span> ` ; } ;
let sumRev = 0 , sumPay = 0 , sumCost = 0 , sumPaid = 0 ;
const rows = [ ] ;
allPfs . forEach ( pf => {
const rev = sumBudget ( pf , "rev" ) ;
const payment = sumBudget ( pf , "payment" ) ;
const cost = sumBudget ( pf , "cost" ) ;
const paid = sumBudget ( pf , "paid" ) ;
const payDiff = rev - payment ;
const costDiff = cost - paid ;
const cashflow = payment - paid ;
sumRev += rev ; sumPay += payment ; sumCost += cost ; sumPaid += paid ;
rows . push ( ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center"> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-center text-sm"> ${ pf . status === '已签约' ? '<span class="text-green-600">已签约</span>' : pf . status === '流程中' ? '<span class="text-blue-600">流程中</span>' : '<span class="text-amber-600">待签约</span>' } </td><td class="p-2 text-center text-blue-700 align-middle"> ${ fmt ( rev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ fmt ( payment ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( payDiff ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmt ( cost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ fmt ( paid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( costDiff ) } </td><td class="p-2 text-center font-semibold align-middle ${ cashflow >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ cashflow ? money ( cashflow ) : '<span class="text-slate-300">—</span>' } </td></tr> ` ) ;
} ) ;
return card ( ` <div class="flex justify-between items-center mb-3"><div class="flex items-center gap-2"> ${ finHeaderBase } <span class="text-xs text-slate-500">季度:</span><select onchange="state.finQuarter=parseInt(this.value);localStorage.setItem('opc-fin-quarter',this.value);renderFinance()" class="text-xs 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"> ${ quarterOpts } </select></div> ${ finAddBtn } </div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold align-middle">项目名称</th><th class="p-2 text-center font-semibold align-middle">状态</th><th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th><th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th><th class="p-2 text-center font-semibold align-middle">回款差额</th><th class="p-2 text-center font-semibold align-middle text-rose-600">应付</th><th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th><th class="p-2 text-center font-semibold align-middle">应付差额</th><th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th></tr></thead><tbody> ${ rows . length ? rows . join ( "" ) : '<tr><td colspan="9" class="p-6 text-center text-slate-400">该季度暂无数据</td></tr>' } <tr class="border-t-2 border-slate-200 bg-slate-50 font-bold"><td class="p-2 text-center" colspan="2">合计</td><td class="p-2 text-center text-blue-700 align-middle"> ${ money ( sumRev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ money ( sumPay ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumRev - sumPay ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ money ( sumCost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ money ( sumPaid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumCost - sumPaid ) } </td> <td class="p-2 text-center font-semibold align-middle ${ sumPay - sumPaid >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ money ( sumPay - sumPaid ) } </td></tr></tbody></table></div> ` , "p-4" ) ;
} ) ( ) : ( ( ) => {
const calcTotals = ( pf ) => {
let budget = [ ] ; try { budget = JSON . parse ( pf . budget _data || "[]" ) ; } catch ( e ) { }
@@ -235,7 +297,7 @@ function renderFinance() {
allPfs . forEach ( pf => { const t = calcTotals ( pf ) ; sumRev += t . rev ; sumPay += t . payment ; sumCost += t . cost ; sumPaid += t . paid ; } ) ;
const fmt = ( v ) => v ? ` <span class="font-medium"> ${ money ( v ) } </span> ` : '<span class="text-slate-300">—</span>' ;
const fmtDiff = ( v ) => { if ( ! v ) return '<span class="text-slate-300">—</span>' ; return ` <span class=" ${ v > 0 ? 'text-amber-600 font-medium' : 'text-green-600 font-medium' } "> ${ money ( Math . abs ( v ) ) } </span> ` ; } ;
return card ( ` <div class="flex items-center justify-between mb-3"><h3 class="font-bold text-slate-700">总视图 <span class="text-slate-400 font-normal">( ${ allPfs . length } )</span></h3></div><div class="flex gap-2 mb-3 "> ${ [ [ "已签约" , "已签约" ] , [ "流程中" , "流程中" ] , [ "待签约" , "待签约" ] ] . map ( ( [ k , v ] ) => ` <button class="btn btn-sm ${ state . finFilter === k ? 'btn-primary' : 'btn-ghost' } " onclick="state.finFilter=' ${ k } ';renderFinance()"> ${ v } ( ${ pfs . filter ( x => x . status === k ) . length } )</button> ` ) . join ( "" ) } </div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold align-middle">项目名称</th><th class="p-2 text-center font-semibold align-middle">类型</th><th class="p-2 text-center font-semibold align-middle">状态</th><th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th><th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th><th class="p-2 text-center font-semibold align-middle">回款差额</th><th class="p-2 text-center font-semibold align-middle text-rose-600">应付</th><th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th><th class="p-2 text-center font-semibold align-middle">应付差额</th><th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th></tr></thead><tbody> ${ allPfs . map ( pf => { const t = calcTotals ( pf ) ; const payDiff = t . rev - t . payment ; const costDiff = t . cost - t . paid ; const cashflow = t . payment - t . paid ; return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center"> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-sm text-center text-slate-500"> ${ esc ( pf . business _type ) } </td><td class="p-2 text-sm text-center "> ${ pf . status === '已签约' ? badge ( '已签约' ) : pf . status === '流程中' ? badge ( '流程中' , 'blue' ) : badge ( '待签约' , 'amber' ) } </td><td class="p-2 text-center text-blue-700 align-middle"> ${ fmt ( t . rev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ fmt ( t . payment ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( payDiff ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmt ( t . cost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ fmt ( t . paid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( costDiff ) } </td><td class="p-2 text-center font-semibold align-middle ${ cashflow >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ cashflow ? money ( cashflow ) : '<span class="text-slate-300">—</span>' } </td></tr> ` ; } ).join("")}<tr class="border-t-2 border-slate-200 bg-slate-50 font-bold"><td class="p-2 text-center" colspan="3 ">合计</td><td class="p-2 text-center text-blue-700 align-middle"> ${ money ( sumRev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ money ( sumPay ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumRev - sumPay ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ money ( sumCost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ money ( sumPaid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumCost - sumPaid ) } </td><td class="p-2 text-center font-semibold align-middle ${ sumPay - sumPaid >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ money ( sumPay - sumPaid ) } </td></tr></tbody></table></div> ` , "p-4" ) ;
return card ( ` <div class="flex justify-between items-center mb-3"><div class="flex items-center gap-2"> ${ finHeaderBase } </div> ${ finAddBtn } </div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold align-middle">项目名称</th><th class="p-2 text-center font-semibold align-middle">状态</th><th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th><th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th><th class="p-2 text-center font-semibold align-middle">回款差额</th><th class="p-2 text-center font-semibold align-middle text-rose-600">应付</th><th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th><th class="p-2 text-center font-semibold align-middle">应付差额</th><th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th></tr></thead><tbody> ${ allPfs . map ( pf => { const t = calcTotals ( pf ) ; const payDiff = t . rev - t . payment ; const costDiff = t . cost - t . paid ; const cashflow = t . payment - t . paid ; return ` <tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal( ${ pf . id } )"><td class="p-2 text-sm font-medium text-center"> ${ esc ( pf . customer _name ) } </td><td class="p-2 text-center text-sm "> ${ pf . status === '已签约' ? '<span class="text-green-600">已签约</span>' : pf . status === '流程中' ? '<span class="text-blue-600">流程中</span>' : '<span class="text-amber-600">待签约</span>' } </td><td class="p-2 text-center text-blue-700 align-middle"> ${ fmt ( t . rev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ fmt ( t . payment ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( payDiff ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ fmt ( t . cost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ fmt ( t . paid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( costDiff ) } </td><td class="p-2 text-center font-semibold align-middle ${ cashflow >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ cashflow ? money ( cashflow ) : '<span class="text-slate-300">—</span>' } </td></tr> ` ; } ).join("")}<tr class="border-t-2 border-slate-200 bg-slate-50 font-bold"><td class="p-2 text-center" colspan="2 ">合计</td><td class="p-2 text-center text-blue-700 align-middle"> ${ money ( sumRev ) } </td><td class="p-2 text-center text-amber-700 align-middle"> ${ money ( sumPay ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumRev - sumPay ) } </td><td class="p-2 text-center text-rose-700 align-middle"> ${ money ( sumCost ) } </td><td class="p-2 text-center text-purple-700 align-middle"> ${ money ( sumPaid ) } </td><td class="p-2 text-center align-middle"> ${ fmtDiff ( sumCost - sumPaid ) } </td><td class="p-2 text-center font-semibold align-middle ${ sumPay - sumPaid >= 0 ? 'text-green-600' : 'text-red-600' } "> ${ money ( sumPay - sumPaid ) } </td></tr></tbody></table></div> ` , "p-4" ) ;
} ) ( ) }
< / d i v > ` ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
@@ -306,7 +368,7 @@ window.initBudgetTable = (budgetData) => {
} ;
// ---------- 任务管理 tab ----------
const TASK _TYPES = [ "科普视频" , "科普专访" , "科普文章" ] ;
const TASK _TYPES = [ "科普视频" , "科普专访" , "科普文章" , "问卷调研" , "病例征集" ];
function taskMonthOptions ( selected ) {
const now = new Date ( ) ;
@@ -324,8 +386,12 @@ window.addTaskRow = (taskMonth = '', taskType = '', taskCount = '', executedCoun
if ( ! tbody ) return ;
const row = document . createElement ( "tr" ) ;
const defaultMonth = ( ( ) => { const n = new Date ( ) ; return n . getFullYear ( ) + "-" + String ( n . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) ; } ) ( ) ;
const isPreset = TASK _TYPES . includes ( taskType ) ;
const typeCell = isPreset || ! taskType
? ` <select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="onTaskTypeChange(this)"><option value="">选择</option> ${ TASK _TYPES . map ( t => ` <option ${ t === taskType ? 'selected' : '' } > ${ t } </option> ` ) . join ( "" ) } <option value="__custom__" ${ ! isPreset && taskType ? 'selected' : '' } >自定义...</option></select> `
: ` <input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" value=" ${ esc ( taskType ) } " placeholder="输入自定义类型"><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="revertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button> ` ;
row . innerHTML = ` <td><select name="task_month[]" class="form-ctrl form-ctrl-sm w-full"> ${ taskMonthOptions ( taskMonth || defaultMonth ) } </select></td>
<td><select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="updateTaskDiff(this)"><option value="">选择</option> ${ TASK _TYPES . map ( t => ` <option ${ t === taskType ? 'selected' : '' } > ${ t } </option> ` ) . join ( "" ) } </select> </td>
<td class="flex items-center"> ${ typeCell } </td>
<td><input name="task_count[]" type="number" step="1" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:100px" placeholder="0" value=" ${ taskCount } " oninput="updateTaskDiff(this)"></td>
<td><input name="task_executed[]" type="number" step="1" min="0" class="form-ctrl form-ctrl-sm text-right" style="width:100px" placeholder="0" value=" ${ executedCount } " oninput="updateTaskDiff(this)"></td>
<td><input name="task_diff[]" type="number" class="form-ctrl form-ctrl-sm text-right" style="width:80px" placeholder="—" disabled></td>
@@ -365,6 +431,14 @@ function updateRowCalc(row) {
unexecAmtInput . value = unexecAmt ? unexecAmt . toFixed ( 2 ) : '' ;
}
window . toggleBtChip = ( chip ) => {
const cb = chip . querySelector ( 'input' ) ;
cb . checked = ! cb . checked ;
chip . classList . toggle ( 'bg-blue-50' , cb . checked ) ;
chip . classList . toggle ( 'border-blue-400' , cb . checked ) ;
chip . classList . toggle ( 'text-blue-600' , cb . checked ) ;
} ;
window . initTaskTable = ( taskData ) => {
const tbody = document . querySelector ( "#taskTbody" ) ;
if ( ! tbody ) return ;
@@ -373,6 +447,21 @@ window.initTaskTable = (taskData) => {
rows . forEach ( r => addTaskRow ( r . task _month || '' , r . task _type || '' , r . task _count || '' , r . task _executed || '' , r . unit _price || '' ) ) ;
} ;
window . onTaskTypeChange = ( sel ) => {
if ( sel . value !== '__custom__' ) return ;
const td = sel . parentElement ;
const oldVal = sel . value ;
td . innerHTML = ` <input name="task_type[]" class="form-ctrl form-ctrl-sm w-full" placeholder="输入自定义类型" autofocus><button type="button" class="btn btn-ghost btn-sm text-slate-400 p-0 w-5 h-5 ml-1" onclick="revertTaskType(this)" title="返回选择"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button> ` ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
td . querySelector ( 'input' ) . focus ( ) ;
} ;
window . revertTaskType = ( btn ) => {
const td = btn . parentElement ;
td . innerHTML = ` <select name="task_type[]" class="form-ctrl form-ctrl-sm w-full" onchange="onTaskTypeChange(this)"><option value="">选择</option> ${ TASK _TYPES . map ( t => ` <option> ${ t } </option> ` ) . join ( "" ) } <option value="__custom__">自定义...</option></select> ` ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
} ;
window . closeFinanceModal = ( ) => {
const modal = document . querySelector ( "#financeModal" ) ;
modal . classList . add ( "hidden" ) ;
@@ -409,7 +498,63 @@ window.switchFinanceTab = (tab) => {
document . querySelectorAll ( ".finance-tab" ) . forEach ( b => b . classList . toggle ( "active" , b . dataset . tab === tab ) ) ;
document . querySelector ( "#financeTabInfo" ) . classList . toggle ( "hidden" , tab !== "info" ) ;
document . querySelector ( "#financeTabBudget" ) . classList . toggle ( "hidden" , tab !== "budget" ) ;
document . querySelector ( "#financeTabExec" ) . classList . toggle ( "hidden" , tab !== "exec" ) ;
document . querySelector ( "#financeTabTasks" ) . classList . toggle ( "hidden" , tab !== "tasks" ) ;
document . querySelector ( "#financeTabActivity" ) . classList . toggle ( "hidden" , tab !== "activity" ) ;
document . querySelector ( ".finance-form-actions" ) . classList . toggle ( "hidden" , tab === "activity" ) ;
if ( tab === "activity" ) initFinSquire ( ) ;
} ;
// ---------- 活动与跟进 ----------
async function loadFinFollowups ( pfId ) {
const list = document . querySelector ( "#finActivityList" ) ;
if ( ! list || ! pfId ) return ;
try {
const fups = await api ( ` /api/followups/project_finance/ ${ pfId } ` ) ;
list . innerHTML = fups . length
? fups . map ( f => ` <div class="activity-item"><div class="activity-icon"><i data-lucide="message-square"></i></div><div class="min-w-0 flex-1"><div class="flex justify-between gap-3 text-[12px] text-slate-500"><span> ${ esc ( f . follower ) } · ${ esc ( f . follow _up _method ) } </span><span> ${ esc ( f . followed _at ) } </span></div><div class="mt-1 leading-5 text-slate-800 rich-content" data-html=" ${ encodeURIComponent ( f . content || '' ) } "></div> ${ f . next _action ? ` <p class="mt-1 leading-5 text-blue-700">下一步: ${ text ( f . next _action ) } </p> ` : "" } </div><button class="activity-delete" type="button" onclick="deleteFinFollowup(event, ${ f . id } )" title="删除评论"><i data-lucide="trash-2"></i></button></div> ` ) . join ( "" )
: '<p class="text-sm text-slate-400 py-4 text-center">暂无跟进记录</p>' ;
if ( window . lucide ) window . lucide . createIcons ( ) ;
list . querySelectorAll ( ".rich-content" ) . forEach ( el => {
const html = el . dataset . html ;
if ( html ) el . innerHTML = decodeURIComponent ( html ) ;
} ) ;
} catch ( e ) { /* ignore */ }
}
function initFinSquire ( ) {
const ed = document . querySelector ( "#squire_finance" ) ;
if ( ! ed || ! window . Squire ) return ;
if ( window . squireInstances [ "squire_finance" ] ) { window . squireInstances [ "squire_finance" ] . destroy ( ) ; }
const sq = new Squire ( ed , { blockTag : "P" } ) ;
window . squireInstances [ "squire_finance" ] = sq ;
ed . addEventListener ( "focus" , ( ) => ed . classList . add ( "focused" ) ) ;
ed . addEventListener ( "blur" , ( ) => { if ( ! ed . textContent . trim ( ) ) ed . classList . remove ( "focused" ) ; } ) ;
}
window . submitFinComment = async ( ) => {
const pfId = document . querySelector ( "#pf-id-input" ) . value ;
if ( ! pfId ) return ;
const sq = window . squireInstances [ "squire_finance" ] ;
const content = sq ? sq . getHTML ( ) . trim ( ) : "" ;
if ( ! content || content === "<div><br></div>" || content === "<p><br></p>" ) return ;
const btn = document . querySelector ( "#financeTabActivity .comment-submit" ) ;
btn . disabled = true ;
btn . textContent = "发送中…" ;
await api ( ` /api/followups/project_finance/ ${ pfId } ` , { method : "POST" , body : JSON . stringify ( { data : { content } } ) } ) ;
sq . setHTML ( "" ) ;
btn . disabled = false ;
btn . textContent = "评论" ;
await loadFinFollowups ( pfId ) ;
} ;
window . deleteFinFollowup = async ( event , followupId ) => {
event . stopPropagation ( ) ;
if ( ! confirm ( "确认删除这条评论?" ) ) return ;
await api ( ` /api/followups/ ${ followupId } ` , { method : "DELETE" } ) ;
const pfId = document . querySelector ( "#pf-id-input" ) . value ;
if ( pfId ) await loadFinFollowups ( pfId ) ;
} ;
window . openPfEditModal = ( pfId ) => {
@@ -422,7 +567,17 @@ window.openPfEditModal = (pfId) => {
form . querySelector ( '[name="project_id"]' ) . value = pf . project _id || "" ;
const deptDisplay = form . querySelector ( '.bg-slate-50 [disabled]' ) ;
if ( deptDisplay ) deptDisplay . value = pf . project _id || "" ;
form . querySelector ( '[name="business_type"]' ) . value = pf . business _type || "" ;
// 回填业务类型多选
const btValues = ( pf . business _type || "" ) . split ( /[,, ]/ ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
form . querySelectorAll ( '[name="business_type[]"]' ) . forEach ( cb => {
cb . checked = btValues . includes ( cb . value ) ;
const chip = cb . closest ( '.bt-chip' ) ;
if ( chip ) {
chip . classList . toggle ( 'bg-blue-50' , cb . checked ) ;
chip . classList . toggle ( 'border-blue-400' , cb . checked ) ;
chip . classList . toggle ( 'text-blue-600' , cb . checked ) ;
}
} ) ;
form . querySelector ( '[name="customer_name"]' ) . value = pf . customer _name || "" ;
const setVal = ( name , val ) => { const el = form . querySelector ( ` [name=" ${ name } "] ` ) ; if ( el ) el . value = val || "" ; } ;
setVal ( "project_code" , pf . project _code ) ;
@@ -442,6 +597,9 @@ window.openPfEditModal = (pfId) => {
setVal ( "task_count" , pf . task _count ) ;
setVal ( "service_fee_standard" , pf . service _fee _standard || 5 ) ;
setVal ( "project_manager" , pf . project _manager ) ;
setVal ( "contact_name" , pf . contact _name ) ;
setVal ( "contact_phone" , pf . contact _phone ) ;
setVal ( "other_info" , pf . other _info ) ;
let budgetData = [ ] ;
try { budgetData = JSON . parse ( pf . budget _data || "[]" ) ; } catch ( e ) { budgetData = [ ] ; }
initBudgetTable ( budgetData . length ? budgetData : null ) ;
@@ -449,6 +607,7 @@ window.openPfEditModal = (pfId) => {
try { taskData = JSON . parse ( pf . task _data || "[]" ) ; } catch ( e ) { taskData = [ ] ; }
initTaskTable ( taskData . length ? taskData : null ) ;
setTimeout ( ( ) => updateBudgetSummary ( ) , 100 ) ;
loadFinFollowups ( pf . id ) ;
openFinanceModal ( ) ;
} ;
@@ -456,6 +615,9 @@ window.createFinance = async (event) => {
event . preventDefault ( ) ;
const form = event . currentTarget ;
const data = Object . fromEntries ( new FormData ( form ) . entries ( ) ) ;
// 业务类型多选:合并为逗号分隔
const btChecked = form . querySelectorAll ( '[name="business_type[]"]:checked' ) ;
data . business _type = Array . from ( btChecked ) . map ( cb => cb . value ) . join ( "," ) ;
data . tenant = state . tenant ;
// 必填校验
if ( ! data . customer _name || ! data . customer _name . trim ( ) ) { toast ( "项目名称必填" , "error" ) ; return ; }