- 新增绩效模块(季度考核表,目标/完成/权重/评分,localStorage) - 产品模块增加版本迭代/产品运营双Tab - 方案模块Tab改为下划线样式 - 顶部标题随模块切换联动更新 - OPC来信弹窗恢复并采用信件样式排版 - 费用模块移入业务顶部Tab(平台费用) - 侧边栏:项目→业务,台账→Focus,新增绩效 - 顶部品牌区增加版本号显示v1.0.0.2
125 lines
5.8 KiB
JavaScript
125 lines
5.8 KiB
JavaScript
// performance.js — 绩效模块
|
|
window.renderPerformance = () => {
|
|
const data = state.data;
|
|
if (!data) return;
|
|
|
|
if (state.perfQuarter === undefined) state.perfQuarter = Math.floor(new Date().getMonth() / 3);
|
|
const q = state.perfQuarter;
|
|
const qLabel = ['Q1 (1-3月)', 'Q2 (4-6月)', 'Q3 (7-9月)', 'Q4 (10-12月)'];
|
|
const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
|
|
const qRange = qRanges[q];
|
|
const storageKey = 'opc-performance-Q' + (q + 1);
|
|
|
|
const pfs = (data.projectFinances || []).filter(p => p.status === '已签约');
|
|
|
|
const sumQ = (field) => {
|
|
let total = 0;
|
|
pfs.forEach(p => {
|
|
try { JSON.parse(p.budget_data || '[]').forEach(b => { const m = parseInt((b.month || '').substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {}
|
|
});
|
|
return Math.round(total);
|
|
};
|
|
|
|
const signTotal = Math.round(pfs.filter(x => { const m = parseInt((x.sign_month || '0').substring(5)) || 0; return qRange.includes(m); }).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
|
const revTotal = sumQ('rev');
|
|
const grossTotal = sumQ('gross');
|
|
const paymentTotal = sumQ('payment');
|
|
|
|
const rows = [
|
|
{ key: 'sign', label: '签约', complete: signTotal, icon: 'file-text', defWeight: 10 },
|
|
{ key: 'rev', label: '收入', complete: revTotal, icon: 'dollar-sign', defWeight: 20 },
|
|
{ key: 'gross', label: '毛利', complete: grossTotal, icon: 'trending-up', defWeight: 50 },
|
|
{ key: 'payment', label: '回款', complete: paymentTotal, icon: 'coins', defWeight: 20 },
|
|
];
|
|
|
|
const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
|
|
|
function val(key, field, def) {
|
|
const r = saved[key] || {};
|
|
return r[field] !== undefined && r[field] !== '' ? Number(r[field]) : def;
|
|
}
|
|
|
|
function moneyWan(v) {
|
|
if (v >= 10000) return (v / 10000).toFixed(1) + '万';
|
|
return v.toLocaleString();
|
|
}
|
|
|
|
let totalScore = 0;
|
|
const tableRows = rows.map(r => {
|
|
const targetWan = val(r.key, 'targetWan', 0);
|
|
const target = Math.round(targetWan * 10000);
|
|
const weight = val(r.key, 'weight', r.defWeight);
|
|
const score = target > 0 ? Math.round((r.complete / target) * weight * 10) / 10 : 0;
|
|
totalScore += score;
|
|
|
|
return `<tr class="border-b border-slate-100">
|
|
<td class="p-3 align-middle text-left font-medium text-slate-700"><i data-lucide="${r.icon}" style="width:16px;height:16px;display:inline-block;vertical-align:-3px;margin-right:6px" class="text-blue-500"></i>${r.label}</td>
|
|
<td class="p-3 align-middle text-center">
|
|
<div class="inline-flex items-center gap-1">
|
|
<input type="number" step="0.1" min="0" value="${targetWan.toFixed(1)}" class="perf-target form-ctrl form-ctrl-sm w-20 text-center" style="appearance:none;-webkit-appearance:none" data-key="${r.key}" onchange="updatePerfScore('${r.key}','targetWan',this.value)">
|
|
<span class="text-xs text-slate-400">万</span>
|
|
</div>
|
|
</td>
|
|
<td class="p-3 align-middle text-center text-slate-700 font-mono text-sm">${moneyWan(r.complete)}</td>
|
|
<td class="p-3 align-middle text-center">
|
|
<div class="inline-flex justify-center">
|
|
<input type="number" step="0.1" min="0" max="100" value="${weight}" class="perf-weight form-ctrl form-ctrl-sm w-16 text-center" style="appearance:none;-webkit-appearance:none" data-key="${r.key}" onchange="updatePerfScore('${r.key}','weight',this.value)">
|
|
</div>
|
|
</td>
|
|
<td class="p-3 align-middle text-center font-semibold text-blue-700 text-sm">${score.toFixed(1)}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
|
|
const quarterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0">` + qLabel.map((l, i) => {
|
|
return `<button onclick="switchPerfQuarter(${i})" class="finance-tab${i === q ? ' active' : ''}" style="font-size:14px;font-weight:600">${l}</button>`;
|
|
}).join('') + `</div>`;
|
|
|
|
document.querySelector("#performance").innerHTML = `
|
|
<style>
|
|
.perf-target::-webkit-outer-spin-button,
|
|
.perf-target::-webkit-inner-spin-button,
|
|
.perf-weight::-webkit-outer-spin-button,
|
|
.perf-weight::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
|
.perf-target, .perf-weight { -moz-appearance: textfield; }
|
|
</style>
|
|
<div class="grid gap-4">
|
|
${quarterTabs}
|
|
<section class="card p-4">
|
|
<table class="w-full text-sm">
|
|
<thead><tr class="bg-slate-50 border-b border-slate-200">
|
|
<th class="p-3 text-left font-semibold text-slate-500">指标</th>
|
|
<th class="p-3 text-center font-semibold text-slate-500">目标(万)</th>
|
|
<th class="p-3 text-center font-semibold text-slate-500">完成情况</th>
|
|
<th class="p-3 text-center font-semibold text-slate-500">权重</th>
|
|
<th class="p-3 text-center font-semibold text-slate-500">评分</th>
|
|
</tr></thead>
|
|
<tbody>${tableRows}</tbody>
|
|
<tfoot><tr class="border-t-2 border-slate-200 bg-slate-50 font-semibold">
|
|
<td class="p-3 text-left text-slate-700">合计</td>
|
|
<td class="p-3 text-center text-slate-500">—</td>
|
|
<td class="p-3 text-center text-slate-500">—</td>
|
|
<td class="p-3 text-center text-slate-700">100</td>
|
|
<td class="p-3 text-center text-blue-700 text-base">${totalScore.toFixed(1)}</td>
|
|
</tr></tfoot>
|
|
</table>
|
|
</section>
|
|
</div>`;
|
|
|
|
if (window.lucide) window.lucide.createIcons();
|
|
};
|
|
|
|
window.switchPerfQuarter = (q) => {
|
|
state.perfQuarter = q;
|
|
renderPerformance();
|
|
};
|
|
|
|
window.updatePerfScore = (key, field, value) => {
|
|
const q = state.perfQuarter || 0;
|
|
const storageKey = 'opc-performance-Q' + (q + 1);
|
|
const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
|
saved[key] = saved[key] || {};
|
|
saved[key][field] = value;
|
|
localStorage.setItem(storageKey, JSON.stringify(saved));
|
|
renderPerformance();
|
|
};
|