refactor: 财务模块卡片头部重构为单行功能区 + 表格增加状态列
- 卡片头部简化为单行: 视图标签 | 筛选:状态/月份/季度下拉 + 新增按钮 - 状态筛选从标签按钮改为统一风格下拉框(自定义SVG箭头) - 下拉框字体/高度与视图标签 btn-sm 完全对齐 - 表格增加状态列(已签约/流程中/待签约,分色显示) - 季度视图 p-4 padding 修复
This commit is contained in:
@@ -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">
|
||||
<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 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>
|
||||
<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 class="text-red-500">*</span></span><input name="customer_name" required class="form-ctrl" placeholder="请输入项目名称"></label>
|
||||
</div>
|
||||
<div class="fin-field-group">
|
||||
<p class="fin-section-label">合同信息</p>
|
||||
<div class="grid gap-4">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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-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 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() {
|
||||
</table>
|
||||
<button type="button" class="btn btn-ghost btn-sm mt-3" onclick="addTaskRow()"><i data-lucide="plus"></i>添加任务</button>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 pt-2"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>
|
||||
<div id="financeTabActivity" class="hidden">
|
||||
<div class="grid gap-2" id="finActivityList"></div>
|
||||
<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></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('italic')" title="斜体"><i data-lucide="italic"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('underline')" title="下划线"><i data-lucide="underline"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('strikethrough')" title="删除线"><i data-lucide="strikethrough"></i></button>
|
||||
<span class="squire-sep"></span>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('makeUnorderedList')" title="无序列表"><i data-lucide="list"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('makeOrderedList')" title="有序列表"><i data-lucide="list-ordered"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('blockquote')" title="引用"><i data-lucide="quote"></i></button>
|
||||
<span class="squire-sep"></span>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('undo')" title="撤销"><i data-lucide="undo"></i></button>
|
||||
<button type="button" class="squire-btn" onmousedown="event.preventDefault();squireCmd('redo')" title="重做"><i data-lucide="redo"></i></button>
|
||||
</div>
|
||||
<div class="squire-editor" id="squire_finance" placeholder="添加评论"></div>
|
||||
<div class="comment-toolbar">
|
||||
<span class="comment-hint">支持富文本编辑</span>
|
||||
<button class="btn btn-primary btn-sm comment-submit" type="button" onclick="submitFinComment()">评论</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><div class="flex justify-end gap-3 pt-2 finance-form-actions"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>
|
||||
${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:4px">${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:30px">${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");
|
||||
})()}
|
||||
</div>`;
|
||||
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; }
|
||||
|
||||
Reference in New Issue
Block a user