diff --git a/static/modules/finance.js b/static/modules/finance.js
index 00e9d0f..0d44f26 100644
--- a/static/modules/finance.js
+++ b/static/modules/finance.js
@@ -98,7 +98,7 @@ function renderFinance() {
})();
const sm = pf.sign_month || "";
const signMonthCell = `
${sm || '—'} | `;
- return `| ${esc(pf.client_name || "")} | ${esc(pf.customer_name)} | ${signMonthCell}${money(pf.sign_amount)} | ${mCols}${totalCol}
`;
+ return `| ${esc(pf.customer_name)} | ${signMonthCell}${money(pf.sign_amount)} | ${mCols}${totalCol}
`;
};
const now2 = new Date();
@@ -285,14 +285,14 @@ function renderFinance() {
};
const SIGNED_COLS = [
{ key: '_expand', label: '' },
- { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
+ { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
{ key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'gross_rate', label: '毛利率' },
{ key: 'payment', label: '回款' }, { key: 'pending', label: '应收' }, { key: 'pay_rate', label: '回款率' },
{ key: 'cost', label: '成本' }, { key: 'paid', label: '支出' }, { key: 'payable', label: '应付' }, { key: 'paid_rate', label: '付款率' },
{ key: 'cashflow', label: '现金流' },
];
const UNSIGNED_COLS = [
- { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
+ { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
{ key: 'sign_month', label: '签约月份' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' },
,
];
@@ -317,7 +317,7 @@ function renderFinance() {
const cardHtml = (r, i) => '';
const fs = '' + cardHtml(rows_data[0],0) + cardHtml(rows_data[1],1) + cardHtml(rows_data[2],2) + '' + cardHtml(rows_data[3],3) + cardHtml(rows_data[4],4) + cardHtml(rows_data[5],5) + '' + cardHtml(rows_data[6],6) + cardHtml(rows_data[7],7) + cardHtml(rows_data[8],8) + '
';
const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
- const theadCols = isUnsigned ? 6 : 15;
+ const theadCols = isUnsigned ? 5 : 14;
const tbody = rows.length ? `${rows.join("")}` : `| ${emptyText} |
`;
const subtitle = isUnsigned
? '合同 → 毛利 → 成本 → 项目利润'
@@ -366,11 +366,11 @@ function renderFinance() {
var bCf = bPay - ePaid;
return ' | | ' + m + ' | ' + (bRev ? money(bRev) : '—') + ' | ' + (bGross ? money(bGross) : '—') + ' | | ' + (bPay ? money(bPay) : '—') + ' | | ' + (eCost ? money(eCost) : '—') + ' | ' + (ePaid ? money(ePaid) : '—') + ' | | ' + (bCf ? money(bCf) : '—') + ' | ' + (bRev - bPay ? money(bRev - bPay) : '—') + ' |
';
}).join('');
- return ` | ${esc(pf.client_name || "")} | ${esc(pf.customer_name)} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${grossR} | ${fmtNoUnit(payment)} | ${pendingVal} | ${payR} | ${fmtNoUnit(cost)} | ${fmtNoUnit(paid)} | ${payableVal} | ${paidR} | ${cf} |
| | 月份 | 确收 | 毛利 | | 回款 | | 成本 | 支出 | | 现金流 | 应收 | ${detailRows}
|
`;
+ return ` | ${esc(pf.customer_name)} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${grossR} | ${fmtNoUnit(payment)} | ${pendingVal} | ${payR} | ${fmtNoUnit(cost)} | ${fmtNoUnit(paid)} | ${payableVal} | ${paidR} | ${cf} |
| | 月份 | 确收 | 毛利 | | 回款 | | 成本 | 支出 | | 现金流 | 应收 | ${detailRows}
|
`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
- return `| ${esc(pf.customer_name)} | ${money(pf.sign_amount)} | ${pf.sign_month || '—'} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} |
`;
+ return `| ${esc(pf.customer_name)} | ${money(pf.sign_amount)} | ${pf.sign_month || '—'} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} |
`;
};
if (state.finView === 'monthly') {
@@ -958,7 +958,8 @@ window.createFinance = async (event) => {
document.querySelector("#financeModalTitle").textContent = "新增项目财务";
closeFinanceModal();
state.data.projectFinances = await api("/api/projectFinances/list?tenant=" + encodeURIComponent(state.tenant));
- renderFinance();
+ localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance();
} catch (error) {
toast("保存失败:" + error.message, "error");
}
@@ -974,7 +975,8 @@ window.deleteFinanceItem = async () => {
await api(`/api/projectFinances/${pfId}`, { method: "DELETE" });
closeFinanceModal();
state.data.projectFinances = await api("/api/projectFinances/list?tenant=" + encodeURIComponent(state.tenant));
- renderFinance();
+ localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance();
toast("已删除", "success");
} catch (error) {
toast("删除失败:" + error.message, "error");
@@ -1254,8 +1256,51 @@ function renderOverviewCharts(data) {
};
// ====== 筛选器控制函数 ======
-window.switchFinFilter = (f) => { state.finFilter = f; renderFinance(); };
-window.setFinView = (v) => { state.finView = v; renderFinance(); };
-window.setFinYear = (y) => { state.finYear = parseInt(y); renderFinance(); };
-window.setFinQuarter = (q) => { state.finView = 'quarterly'; state.finQuarter = q; renderFinance(); };
-window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; renderFinance(); };
+window.switchFinFilter = (f) => { state.finFilter = f; localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance(); };
+window.setFinView = (v) => { state.finView = v; localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance(); };
+window.setFinYear = (y) => { state.finYear = parseInt(y); localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance(); };
+window.setFinQuarter = (q) => { state.finView = 'quarterly'; state.finQuarter = q; localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance(); };
+window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance(); };
+
+
+// ---- 项目标记上下文菜单 ----
+if (!state._fmMarks) state._fmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_fm_marks')) || { red: {} }; } catch(e) { return { red: {} }; } })();
+window.finShowCtx = function(e, pfId) {
+ e.preventDefault();
+ var menu = document.getElementById('finCtxMenu');
+ if (!menu) {
+ menu = document.createElement('div');
+ menu.id = 'finCtxMenu';
+ menu.style.cssText = 'display:none;position:fixed;z-index:9999;background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,.15);border:1px solid #e2e8f0;padding:4px 0;min-width:130px';
+ menu.innerHTML = '项目标记
' +
+ '标红
';
+ menu.addEventListener('click', function(ev) {
+ ev.stopPropagation();
+ var id = menu.getAttribute('data-pfid');
+ menu.style.display = 'none';
+ if (state._fmMarks.red[id]) { delete state._fmMarks.red[id]; } else { state._fmMarks.red[id] = true; }
+ localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance();
+ });
+ document.body.appendChild(menu);
+ }
+ menu.setAttribute('data-pfid', pfId);
+ menu.style.display = 'block';
+ menu.style.left = e.pageX + 'px';
+ menu.style.top = Math.min(e.pageY, window.innerHeight - 200) + 'px';
+};
+document.addEventListener('click', function() { var m = document.getElementById('finCtxMenu'); if (m) m.style.display = 'none'; });
+
+window.finCtxMenu_act = function(e, type) {
+ e.stopPropagation();
+ var pfId = document.getElementById("finCtxMenu").getAttribute("data-pfid");
+ if (type === "clear") { delete state._fmMarks.red[pfId]; delete state._fmMarks.pin[pfId]; }
+ else { if (state._fmMarks[type][pfId]) { delete state._fmMarks[type][pfId]; } else { state._fmMarks[type][pfId] = true; } }
+ localStorage.setItem('opc_fm_marks', JSON.stringify(state._fmMarks));
+ renderFinance();
+};
diff --git a/static/modules/plan.js b/static/modules/plan.js
index c270d69..88bb88b 100644
--- a/static/modules/plan.js
+++ b/static/modules/plan.js
@@ -96,7 +96,7 @@ function renderPlan() {
})();
const sm = pf.sign_month || "";
const signMonthCell = `${sm || '—'} | `;
- return `| ${esc(pf.client_name || "")} | ${esc(pf.customer_name)} | ${esc(pf.weight || "20%")} | ${esc(pf.project_type || "待签约")} | ${signMonthCell}${money(pf.sign_amount)} | ${mCols}${totalCol}
`;
+ return `| ${esc(pf.customer_name)} | ${esc(pf.weight || "20%")} | ${esc(pf.project_type || "待签约")} | ${signMonthCell}${money(pf.sign_amount)} | ${mCols}${totalCol}
`;
};
const now2 = new Date();
@@ -336,7 +336,7 @@ function renderPlan() {
const cardHtml = (r, i) => '';
const fs = '' + cardHtml(rows_data[0],0) + cardHtml(rows_data[1],1) + cardHtml(rows_data[2],2) + '' + cardHtml(rows_data[3],3) + cardHtml(rows_data[4],4) + cardHtml(rows_data[5],5) + '' + cardHtml(rows_data[6],6) + cardHtml(rows_data[7],7) + cardHtml(rows_data[8],8) + '
';
const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
- const theadCols = isUnsigned ? 9 : 15;
+ const theadCols = isUnsigned ? 8 : 14;
const tbody = rows.length ? `${rows.join("")}` : `| ${emptyText} |
`;
const subtitle = isUnsigned
? '合同 → 毛利 → 成本 → 项目利润'
@@ -398,14 +398,14 @@ function renderPlan() {
var bCf = bPay - ePaid;
return ' | ' + m + ' | ' + (bRev ? money(bRev) : '—') + ' | ' + (bGross ? money(bGross) : '—') + ' | | ' + (bPay ? money(bPay) : '—') + ' | | ' + (eCost ? money(eCost) : '—') + ' | ' + (ePaid ? money(ePaid) : '—') + ' | | ' + (bCf ? money(bCf) : '—') + ' | ' + (bRev - bPay ? money(bRev - bPay) : '—') + ' |
';
}).join('');
- return ` | ${esc(pf.client_name || "")} | ${esc(pf.customer_name)} | ${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])} | ${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${grossR} | ${fmtNoUnit(payment)} | ${pendingVal} | ${payR} | ${fmtNoUnit(cost)} | ${fmtNoUnit(paid)} | ${payableVal} | ${cf} |
| 月份 | 确收 | 毛利 | | 回款 | | 当期成本 | 当期流出 | | 现金流 | 应收 | ${detailRows}
|
`;
+ return ` | ${esc(pf.customer_name)} | ${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])} | ${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${grossR} | ${fmtNoUnit(payment)} | ${pendingVal} | ${payR} | ${fmtNoUnit(cost)} | ${fmtNoUnit(paid)} | ${payableVal} | ${cf} |
| 月份 | 确收 | 毛利 | | 回款 | | 当期成本 | 当期流出 | | 现金流 | 应收 | ${detailRows}
|
`;
};
// 待签约简版行
const tdRowUnsigned = (pf, cost, gross) => {
const profit = gross;
const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600';
const pfVal = profit ? money(profit) : '—';
- return `| ${esc(pf.client_name || "")} | ${esc(pf.customer_name)} | ${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])} | ${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])} | ${money(pf.sign_amount)} | ${pf.sign_month || '—'} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} |
`;
+ return `| ${esc(pf.customer_name)} | ${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])} | ${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])} | ${money(pf.sign_amount)} | ${pf.sign_month || '—'} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} |
`;
};
if (state.planView === 'monthly') {
@@ -1012,7 +1012,8 @@ window.createPlanFinance = async (event) => {
document.querySelector("#planModalTitle").textContent = "新增项目财务";
closePlanModal();
state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
- renderPlan();
+ localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan();
} catch (error) {
toast("刷新失败:" + error.message, "error");
}
@@ -1028,7 +1029,8 @@ window.deletePlanItem = async () => {
await api(`/api/planFinances/${pfId}`, { method: "DELETE" });
closePlanModal();
state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
- renderPlan();
+ localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan();
toast("已删除", "success");
} catch (error) {
toast("删除失败:" + error.message, "error");
@@ -1356,7 +1358,8 @@ window.planInlineUpdate = async (pfId, field, value) => {
try {
await api("/api/planFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data: { [field]: value } }) });
state.data.planFinances = await api("/api/planFinances/list?tenant=" + encodeURIComponent(state.tenant));
- renderPlan();
+ localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan();
toast("已更新", "success");
} catch (error) {
toast("更新失败:" + error.message, "error");
@@ -1364,9 +1367,53 @@ window.planInlineUpdate = async (pfId, field, value) => {
};
// ====== 筛选器控制函数 ======
-window.switchPlanFilter = (f) => { state.planFilter = f; renderPlan(); };
-window.setPlanView = (v) => { state.planView = v; renderPlan(); };
-window.setPlanYear = (y) => { state.planYear = parseInt(y); renderPlan(); };
-window.setPlanQuarter = (q) => { state.planView = 'quarterly'; state.planQuarter = q; renderPlan(); };
-window.setPlanMonth = (m) => { state.planView = 'monthly'; state.planMonth = m; renderPlan(); };
-window.setPlanStatusFilter = (f) => { state.planStatusFilter = f; renderPlan(); };
+window.switchPlanFilter = (f) => { state.planFilter = f; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan(); };
+window.setPlanView = (v) => { state.planView = v; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan(); };
+window.setPlanYear = (y) => { state.planYear = parseInt(y); localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan(); };
+window.setPlanQuarter = (q) => { state.planView = 'quarterly'; state.planQuarter = q; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan(); };
+window.setPlanMonth = (m) => { state.planView = 'monthly'; state.planMonth = m; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan(); };
+window.setPlanStatusFilter = (f) => { state.planStatusFilter = f; localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan(); };
+
+
+// ---- 项目标记上下文菜单 ----
+if (!state._pmMarks) state._pmMarks = (function() { try { return JSON.parse(localStorage.getItem('opc_pm_marks')) || { red: {} }; } catch(e) { return { red: {} }; } })();
+window.planShowCtx = function(e, pfId) {
+ e.preventDefault();
+ var menu = document.getElementById('planCtxMenu');
+ if (!menu) {
+ menu = document.createElement('div');
+ menu.id = 'planCtxMenu';
+ menu.style.cssText = 'display:none;position:fixed;z-index:9999;background:#fff;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,.15);border:1px solid #e2e8f0;padding:4px 0;min-width:130px';
+ menu.innerHTML = '项目标记
' +
+ '标红
';
+ menu.addEventListener('click', function(ev) {
+ ev.stopPropagation();
+ var id = menu.getAttribute('data-pfid');
+ menu.style.display = 'none';
+ if (state._pmMarks.red[id]) { delete state._pmMarks.red[id]; } else { state._pmMarks.red[id] = true; }
+ localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan();
+ });
+ document.body.appendChild(menu);
+ }
+ menu.setAttribute('data-pfid', pfId);
+ menu.style.display = 'block';
+ menu.style.left = e.pageX + 'px';
+ menu.style.top = Math.min(e.pageY, window.innerHeight - 200) + 'px';
+};
+document.addEventListener('click', function() { var m = document.getElementById('planCtxMenu'); if (m) m.style.display = 'none'; });
+
+window.planCtxMenu_act = function(e, type) {
+ e.stopPropagation();
+ var pfId = document.getElementById("planCtxMenu").getAttribute("data-pfid");
+ if (type === "clear") { delete state._pmMarks.red[pfId]; delete state._pmMarks.pin[pfId]; }
+ else { if (state._pmMarks[type][pfId]) { delete state._pmMarks[type][pfId]; } else { state._pmMarks[type][pfId] = true; } }
+ localStorage.setItem('opc_pm_marks', JSON.stringify(state._pmMarks));
+ renderPlan();
+};
diff --git a/templates/index.html b/templates/index.html
index 79d687c..6badee7 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -84,7 +84,7 @@
-
OPC Manager v1.2.0.38
+
OPC Manager v1.2.0.39