核心重构: - 提取 renderView() / tdRow() 统一函数,消除三视图 16000+ 字符重复代码 - METRIC_CARDS 配置驱动,调整卡片顺序/单位只需改配置 - 筛选区统一为下拉框(视图 + 状态 + 日期),删除筛选:前缀 - 日历选择器移至顶部工具栏,卡片内不再重复 功能优化: - 项目状态简化为已签约/待签约 - 新增签约项目总数卡片(月度/季度按签约月份过滤) - 签约金额/已确收/已回款/应付/已付改为万元单位 - 项目名称固定300px + 省略号 + title悬停 - body min-width 1400px + overflow-x:auto - 首页费用金额→应付金额 代码清理: - 删除 monthOpts/quarterOpts/qLabels 等死代码 - 删除 purchase.js(采购→费用模块已另表迁移) - 财务→项目(侧边栏),采购→费用(全域重命名)
104 lines
8.7 KiB
JavaScript
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
|
|
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();
|
|
};
|