Files
opc-manager/static/modules/expense.js
mac 8a78c756b7 refactor: 项目模块重构 — 统一渲染 + 配置驱动 + 交互优化
核心重构:
- 提取 renderView() / tdRow() 统一函数,消除三视图 16000+ 字符重复代码
- METRIC_CARDS 配置驱动,调整卡片顺序/单位只需改配置
- 筛选区统一为下拉框(视图 + 状态 + 日期),删除筛选:前缀
- 日历选择器移至顶部工具栏,卡片内不再重复

功能优化:
- 项目状态简化为已签约/待签约
- 新增签约项目总数卡片(月度/季度按签约月份过滤)
- 签约金额/已确收/已回款/应付/已付改为万元单位
- 项目名称固定300px + 省略号 + title悬停
- body min-width 1400px + overflow-x:auto
- 首页费用金额→应付金额

代码清理:
- 删除 monthOpts/quarterOpts/qLabels 等死代码
- 删除 purchase.js(采购→费用模块已另表迁移)
- 财务→项目(侧边栏),采购→费用(全域重命名)
2026-07-06 15:39:39 +08:00

104 lines
8.7 KiB
JavaScript

// expense.js — 费用管理模块
function renderExpense() {
const records = state.data.expense || [];
const money = (v) => `¥ ${(Number(v || 0)).toLocaleString("zh-CN", { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`;
const badge = (s) => {
const m = { "已审批": "green", "已采购": "blue", "已完成": "slate", "待审批": "amber" };
const c = m[s] || "amber";
return `<span class="text-xs px-2 py-0.5 rounded-full bg-${c}-100 text-${c}-700 font-medium">${s}</span>`;
};
const esc = (s) => (s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
const totalAmount = records.reduce((s, r) => s + (r.amount || 0), 0);
document.querySelector("#expense").innerHTML = `<div class="grid gap-4">
<div class="grid grid-cols-3 gap-3">
<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500">费用记录</span><strong class="mt-2 block text-2xl">${records.length}</strong></div>
<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500">费用金额</span><strong class="mt-2 block text-2xl">${money(totalAmount)}</strong></div>
<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500">待审批</span><strong class="mt-2 block text-2xl">${records.filter(r => r.status === '待审批').length}</strong></div>
</div>
<div class="flex justify-between items-center">
<h3 class="font-bold text-slate-700">费用列表 <span class="text-slate-400 font-normal">(${records.length})</span></h3>
<button class="btn btn-primary btn-sm" onclick="openExpenseModal()">新增费用</button>
</div>
<div id="purchaseModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeExpenseModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-6 py-4 flex items-center justify-between"><h3 class="text-lg font-bold text-slate-800" id="purchaseModalTitle">新增费用</h3><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeExpenseModal()"><i data-lucide="x"></i></button></div><form onsubmit="saveExpense(event)" class="p-6 grid gap-4" novalidate><input type="hidden" name="expense_id" id="purchase-id-input" value=""><div class="grid grid-cols-2 gap-3"><label class="block"><span class="text-xs text-slate-500 mb-1 block">项目名称 <span class="text-red-500">*</span></span><input name="project_name" required class="form-ctrl" placeholder="请输入项目名称"></label><label class="block"><span class="text-xs text-slate-500 mb-1 block">采购内容 <span class="text-red-500">*</span></span><input name="purchase_item" required class="form-ctrl" placeholder="请输入采购内容"></label></div><div class="grid grid-cols-2 gap-3"><label class="block"><span class="text-xs text-slate-500 mb-1 block">供应商</span><input name="supplier" class="form-ctrl" placeholder="请输入供应商"></label><label class="block"><span class="text-xs text-slate-500 mb-1 block">金额(元)</span><input name="amount" type="number" step="0.01" class="form-ctrl" placeholder="0.00"></label></div><div class="grid grid-cols-2 gap-3"><label class="block"><span class="text-xs text-slate-500 mb-1 block">采购日期</span><input name="purchase_date" type="date" class="form-ctrl"></label><label class="block"><span class="text-xs text-slate-500 mb-1 block">状态</span><select name="status" class="form-ctrl bg-white"><option>待审批</option><option>已审批</option><option>已采购</option><option>已完成</option></select></label></div><div class="grid grid-cols-2 gap-3"><label class="block"><span class="text-xs text-slate-500 mb-1 block">分类</span><input name="category" class="form-ctrl" placeholder="如:办公用品、设备等"></label></div><div class="grid"><label class="block"><span class="text-xs text-slate-500 mb-1 block">备注</span><textarea name="notes" class="form-ctrl" rows="2" placeholder="备注信息"></textarea></label></div><div class="flex justify-end gap-2 pt-2 border-t border-slate-100"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="purchaseDeleteBtn" onclick="deleteExpenseItem()" type="button">删除</button><button class="btn btn-ghost btn-sm" onclick="closeExpenseModal()" type="button">取消</button><button class="btn btn-primary btn-sm" type="submit">保存</button></div></form></div></div>
<div class="overflow-x-auto card p-4"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-left font-semibold">项目名称</th><th class="p-2 text-left font-semibold">采购内容</th><th class="p-2 text-left font-semibold">供应商</th><th class="p-2 text-right font-semibold">金额</th><th class="p-2 text-center font-semibold">采购日期</th><th class="p-2 text-center font-semibold">状态</th><th class="p-2 text-left font-semibold">分类</th><th class="p-2 text-left font-semibold">备注</th></tr></thead><tbody>${records.length ? records.map(r => `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openExpenseEdit(${r.id})"><td class="p-2 font-medium">${esc(r.project_name)}</td><td class="p-2">${esc(r.purchase_item)}</td><td class="p-2">${esc(r.supplier)}</td><td class="p-2 text-right font-medium">${money(r.amount)}</td><td class="p-2 text-center text-xs">${r.purchase_date || '—'}</td><td class="p-2 text-center">${badge(r.status)}</td><td class="p-2">${esc(r.category)}</td><td class="p-2 text-xs text-slate-500">${esc(r.notes) || '—'}</td></tr>`).join("") : '<tr><td colspan="8" 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" colspan="3">合计(${records.length} 条)</td><td class="p-2 text-right">${money(totalAmount)}</td><td class="p-2" colspan="4"></td></tr></tbody></table></div>
</div>`;
if (window.lucide) window.lucide.createIcons();
}
window.openExpenseModal = () => {
const form = document.querySelector("#expenseModal form");
form.reset();
form.querySelector('[name="expense_id"]').value = "";
document.querySelector("#expenseModalTitle").textContent = "新增费用";
document.querySelector("#expenseDeleteBtn").classList.add("hidden");
document.querySelector("#expenseModal").classList.remove("hidden");
};
window.closeExpenseModal = () => {
document.querySelector("#expenseModal").classList.add("hidden");
};
window.openExpenseEdit = (id) => {
const r = (state.data.expense || []).find(x => x.id === id);
if (!r) return;
const form = document.querySelector("#expenseModal form");
form.reset();
const setVal = (name, val) => { const el = form.querySelector(`[name="${name}"]`); if (el) el.value = val || ""; };
setVal("expense_id", r.id);
setVal("project_name", r.project_name);
setVal("purchase_item", r.purchase_item);
setVal("supplier", r.supplier);
setVal("amount", r.amount);
setVal("purchase_date", r.purchase_date);
setVal("status", r.status);
setVal("category", r.category);
setVal("notes", r.notes);
document.querySelector("#expenseModalTitle").textContent = "编辑费用";
document.querySelector("#expenseDeleteBtn").classList.remove("hidden");
document.querySelector("#expenseModal").classList.remove("hidden");
};
window.saveExpense = async (event) => {
event.preventDefault();
const form = event.target;
const data = Object.fromEntries(new FormData(form).entries());
const isEdit = !!data.expense_id;
delete data.expense_id;
const url = isEdit
? `/api/expense/${form.querySelector('[name="expense_id"]').value}`
: "/api/expense";
const method = isEdit ? "PUT" : "POST";
const resp = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data: { ...data, tenant: state.tenant } }) });
if (!resp.ok) { alert("保存失败"); return; }
// Refresh data
const bResp = await fetch(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`);
if (bResp.ok) {
const bd = await bResp.json();
state.data = bd;
applyUserTenants();
renderExpense();
}
closeExpenseModal();
};
window.deleteExpenseItem = async () => {
const id = document.querySelector("#expenseModal form [name='expense_id']").value;
if (!id || !confirm("确定删除?")) return;
const resp = await fetch(`/api/expense/${id}`, { method: "DELETE" });
if (!resp.ok) { alert("删除失败"); return; }
const bResp = await fetch(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`);
if (bResp.ok) {
const bd = await bResp.json();
state.data = bd;
applyUserTenants();
renderExpense();
}
closeExpenseModal();
};