// utils.js — 全局工具函数与共享状态 const state = { active: "home", data: null, tenant: "科普·无界", opFilter: "all", finFilter: "已签约", selectedProject: null, taskQuery: "", taskView: localStorage.getItem("opc-task-view") || "detail", finView: localStorage.getItem("opc-fin-view") || "rev", proposalTab: "standard", chart: null, chart2: null, chart3: null, productPlatform: "all", uploadTasks: [], }; const money = (value) => `${Number(value || 0).toLocaleString("zh-CN")} 元`; const text = (value) => value === undefined || value === null || value === "" ? "—" : esc(value); function escapeHtml(str) { return String(str || "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } const html = escapeHtml; const esc = escapeHtml; // Toast 通知 function toast(message, type = "info", duration = 3000) { let container = document.querySelector(".toast-container"); if (!container) { container = document.createElement("div"); container.className = "toast-container"; document.body.appendChild(container); } const el = document.createElement("div"); el.className = `toast toast-${type}`; const icon = type === "success" ? "check-circle" : type === "error" ? "alert-circle" : "info"; el.innerHTML = `${esc(message)}`; container.appendChild(el); if (window.lucide) window.lucide.createIcons(); setTimeout(() => { el.classList.add("fade-out"); setTimeout(() => el.remove(), 250); }, duration); } window.toast = toast; function monthOptions(selected = '') { const now = new Date(); const startYear = now.getFullYear() - 1; const endYear = now.getFullYear() + 1; let options = selected ? '' : ''; for (let y = startYear; y <= endYear; y++) { for (const m of ["01","02","03","04","05","06","07","08","09","10","11","12"]) { const val = y + "-" + m; const sel = val === selected ? " selected" : ""; options += ``; } } return options; } async function api(path, options = {}) { const response = await fetch(path, { headers: options.body instanceof FormData ? undefined : { "Content-Type": "application/json" }, ...options, }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "请求失败"); return data; } async function logActivity(targetType, targetId, content) { try { await api(`/api/followups/${targetType}/${targetId}`, { method: "POST", body: JSON.stringify({ data: { content, tenant: state.tenant } }), }); } catch (e) { /* non-critical */ } } function badge(value) { const val = String(value || "—"); let cls = "badge-slate"; if (["P0", "有风险", "已丢单", "已延期"].includes(val)) cls = "badge-red"; if (["P1", "方案中", "方案已提交", "商务谈判", "待客户确认"].includes(val)) cls = "badge-amber"; if (["已签约", "已上线", "已完成", "已归档"].includes(val)) cls = "badge-green"; if (["execution", "已签约执行项目"].includes(val)) cls = "badge-blue"; return `${val === "execution" ? "已签约执行项目" : val === "opportunity" ? "业务机会项目" : val}`; } function card(content, cls = "") { return `
${content}
`; } function renderTable(headers, rows, rowClicks) { const trAttrs = (rowClicks || []).map((rc) => rc ? `onclick="openDrawer('${rc.resource}', ${rc.id})" class="clickable-row"` : ""); return card(`
${headers.map((h) => ``).join("")}${rows.map((row, i) => `${row.map((c) => ``).join("")}`).join("")}
${h}
${c}
`); } async function load() { state.data = await api(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`); // 首次加载时确保标准资料库 7 项已初始化 if (typeof ensureStandardProposals === "function") { const existing = (state.data.proposals || []).filter(p => p.proposal_type === "标准资料"); const missingCount = 7 - existing.length; if (missingCount > 0) { await ensureStandardProposals(); return; // ensureStandardProposals 内部会再次 render } } render(); } function switchTab(tab) { state.active = tab; localStorage.setItem("opc-active-tab", tab); document.querySelectorAll(".sidebar-tab").forEach((btn) => btn.classList.toggle("active", btn.dataset.tab === tab)); document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab)); render(); } function render() { if (!state.data) return; renderHome(); renderProjects(); renderProposals(); renderProducts(); renderFinance(); if (window.lucide) window.lucide.createIcons(); } function renderActive() { if (!state.data) return; const tab = state.active; if (tab === "home") renderHome(); else if (tab === "projects") renderProjects(); else if (tab === "proposals") renderProposals(); else if (tab === "products") renderProducts(); else if (tab === "finance") renderFinance(); if (window.lucide) window.lucide.createIcons(); } window.setTaskView = (view) => { state.taskView = view; localStorage.setItem("opc-task-view", view); // 更新按钮选中状态 const toggle = document.querySelector("#taskViewToggle"); if (toggle) { toggle.querySelectorAll("button").forEach((btn, i) => { const isCompact = i === 0; btn.className = `btn btn-sm ${(isCompact ? view === 'compact' : view !== 'compact') ? 'btn-primary' : 'btn-ghost'} p-1.5`; }); } if (state.selectedProject) { const body = document.querySelector(".task-feed-body"); if (body) body.innerHTML = renderTaskListHTML(state.selectedProject); if (window.lucide) window.lucide.createIcons(); } }; window.switchTab = switchTab; window.setFinView = (view) => { state.finView = view; localStorage.setItem("opc-fin-view", view); renderFinance(); }; window.switchTenant = (tenant) => { state.tenant = tenant; state.selectedProject = null; localStorage.setItem("opc-active-tenant", tenant); document.querySelector("#workspaceTitle").textContent = tenant.replace("·无界", "") + " OPC 工作台"; const label = document.querySelector("#currentTenantLabel"); if (label) { label.textContent = tenant.replace("·无界", "") || "工作台"; label.title = tenant; } load(); }; window.doLogout = async () => { await api("/api/auth/logout", { method: "POST" }); location.href = "/login"; };