let currentUser = null; let authToken = window.localStorage.getItem("keyissueToken") || ""; let customers = []; let issues = []; let inspections = []; let adminConfig = null; let activePage = "dashboard"; let activeStatus = "all"; let selectedIssueId = null; let selectedCustomerId = null; let dictionaries = { departments: ["光宸", "澜光", "蘭堂", "焕境"], customerSources: ["转介绍", "公司特殊关系", "标杆项目", "风险关注"], issueSources: ["线下巡检", "客户群反馈", "每日线上巡查", "客户主动反馈", "管理层转办", "监察部发现"], issueTypes: ["施工质量", "设计变更", "沟通响应", "材料交付", "客户满意度"], progressStatuses: ["正常", "延期"] }; const pageTitles = { dashboard: "A+在建客户动态管理驾驶舱", customers: "A/A+客户库", inspections: "每日线上巡查", issues: "问题反馈池", audit: "监察核查", analysis: "问题分析", rules: "预警规则", admin: "系统后台" }; const pageTitle = document.querySelector("#pageTitle"); const searchInput = document.querySelector("#searchInput"); const tierFilter = document.querySelector("#tierFilter"); const departmentFilter = document.querySelector("#departmentFilter"); const statusButtons = document.querySelectorAll("[data-status]"); const drawer = document.querySelector("#detailDrawer"); const customerDrawer = document.querySelector("#customerDrawer"); const drawerBackdrop = document.querySelector("#drawerBackdrop"); const modalBackdrop = document.querySelector("#modalBackdrop"); const toast = document.querySelector("#toast"); const loginScreen = document.querySelector("#loginScreen"); const appShell = document.querySelector("#appShell"); const currentUserName = document.querySelector("#currentUserName"); const currentRoleName = document.querySelector("#currentRoleName"); const logoutBtn = document.querySelector("#logoutBtn"); const loginForm = document.querySelector("#loginForm"); const loginUsername = document.querySelector("#loginUsername"); const loginPassword = document.querySelector("#loginPassword"); const loginMessage = document.querySelector("#loginMessage"); const registerForm = document.querySelector("#registerForm"); const registerName = document.querySelector("#registerName"); const registerPhone = document.querySelector("#registerPhone"); const registerPassword = document.querySelector("#registerPassword"); const registerPasswordConfirm = document.querySelector("#registerPasswordConfirm"); const registerMessage = document.querySelector("#registerMessage"); const issueCustomerSearch = document.querySelector("#issueCustomerSearch"); function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[char]); } function showToast(message) { toast.textContent = message; toast.classList.add("show"); window.clearTimeout(showToast.timer); showToast.timer = window.setTimeout(() => toast.classList.remove("show"), 2200); } async function apiRequest(path, options = {}) { const headers = { Accept: "application/json", ...(options.headers || {}) }; const request = { ...options, headers }; if (authToken) headers.Authorization = `Bearer ${authToken}`; if (options.body && typeof options.body !== "string") { headers["Content-Type"] = "application/json"; request.body = JSON.stringify(options.body); } const response = await fetch(path, request); const contentType = response.headers.get("content-type") || ""; const payload = contentType.includes("application/json") ? await response.json() : await response.text(); if (!response.ok) { const error = new Error(payload.message || `请求失败:${response.status}`); error.status = response.status; error.payload = payload; throw error; } return payload; } function hasPermission(permission) { if (!currentUser) return false; if (currentUser.permissions.includes("*")) return true; if (permission.startsWith("page:")) { return currentUser.pages.includes(permission.replace("page:", "")); } return currentUser.permissions.includes(permission); } function requirePermission(permission, actionName = "执行该操作") { if (hasPermission(permission)) return true; showToast(`当前登录人无权${actionName}`); return false; } function applyPermissions() { document.querySelectorAll("[data-page]").forEach((button) => { const allowed = hasPermission(`page:${button.dataset.page}`); button.disabled = !allowed; button.title = allowed ? "" : "后台未授权访问该页面"; }); document.querySelectorAll("[data-requires]").forEach((element) => { const allowed = hasPermission(element.dataset.requires); element.disabled = !allowed; element.classList.toggle("is-disabled", !allowed); element.title = allowed ? "" : "后台未授权执行该操作"; }); document.querySelectorAll(".permission-list div").forEach((item) => { const label = item.querySelector("strong")?.textContent || ""; item.classList.toggle("is-current", currentUser && label === currentUser.roleName); }); } function handleApiError(error) { if (error.status === 401) { clearSession(); showToast("登录已失效,请重新登录。"); return; } showToast(error.message || "操作失败,请稍后再试。"); } function beginFormSubmit(form) { if (form.dataset.submitting === "true") return false; form.dataset.submitting = "true"; form.querySelectorAll('button[type="submit"]').forEach((button) => { button.dataset.wasDisabled = button.disabled ? "true" : "false"; button.disabled = true; }); return true; } function endFormSubmit(form) { delete form.dataset.submitting; form.querySelectorAll('button[type="submit"]').forEach((button) => { button.disabled = button.dataset.wasDisabled === "true"; delete button.dataset.wasDisabled; }); } function getCustomer(id) { return customers.find((customer) => customer.id === id) || { id, name: "未匹配客户", tier: "-", source: "-", keyCustomerReason: "-", customerOwner: "-", address: "-", department: "-", designer: "-", manager: "-", stage: "-", startDate: "-", plannedFinishDate: "-", progressStatus: "正常", satisfaction: "-", inspectionStatus: "-", inspectionAt: "-", inspector: "-", inspectionSummary: "-" }; } function statusClass(status) { if (status === "红色预警") return "risk"; if (status === "橙色预警") return "orange"; if (status === "蓝色关注") return "blue"; if (status === "待核查") return "audit"; if (status === "已处理") return "closed"; return "normal"; } function inspectionClass(status) { if (status === "异常") return "risk"; if (status === "未巡查" || status === "-" || !status) return "orange"; return "closed"; } function inspectionFlag(status) { return status === "未巡查" || status === "-" || !status ? "未巡查" : "已巡查"; } function inspectionFlagClass(status) { return status === "未巡查" || status === "-" || !status ? "orange" : "closed"; } function progressClass(status) { return status === "延期" ? "risk" : "closed"; } function cell(label, content) { return `
后台配置仅系统管理员可维护。
`; return; } const activeUsers = adminConfig.users.filter((user) => user.status !== "停用").length; const pendingDrafts = (adminConfig.customerDrafts || []).filter((draft) => draft.status === "待审核").length; document.querySelector("#adminUserCount").textContent = adminConfig.users.length; document.querySelector("#adminActiveUserCount").textContent = activeUsers; document.querySelector("#adminRoleCount").textContent = Object.keys(adminConfig.roles).length; document.querySelector("#adminPermissionCount").textContent = adminConfig.permissionCatalog.length; document.querySelector("#adminPendingCustomerCount").textContent = pendingDrafts; document.querySelector("#adminCustomerReviewCount").textContent = `${pendingDrafts} 单待审核`; document.querySelector("#adminUserRoleSelect").innerHTML = adminRoleOptions("delivery"); document.querySelector("#customerReviewRows").innerHTML = renderCustomerReviewRows(); document.querySelector("#adminUserRows").innerHTML = adminConfig.users.map((user) => `${escapeHtml(record.note)}
处置后满意度:${escapeHtml(record.satisfaction || "待回访")};客户反馈:${escapeHtml(record.feedback || "待补充")}暂无处置记录,责任人提交后会按时间形成卡片流水。
`; document.querySelector("#drawerTimeline").innerHTML = issue.timeline.map((item) => `${escapeHtml(issue.summary || "未填写问题描述。")}
责任人:${escapeHtml(issue.owner)};处置后满意度:${escapeHtml(issue.postSatisfaction || "待处置")}暂无关联问题反馈。
`; drawerBackdrop.hidden = false; customerDrawer.classList.add("open"); customerDrawer.setAttribute("aria-hidden", "false"); applyPermissions(); } function closeDrawer() { drawer.classList.remove("open"); drawer.setAttribute("aria-hidden", "true"); customerDrawer.classList.remove("open"); customerDrawer.setAttribute("aria-hidden", "true"); drawerBackdrop.hidden = true; selectedIssueId = null; selectedCustomerId = null; } function openModal(id, customerId) { const modal = document.querySelector(`#${id}`); if (id === "issueModal" && customerId) { if (issueCustomerSearch) issueCustomerSearch.value = ""; renderCustomerSelects(); document.querySelector("#issueCustomerSelect").value = customerId; } if (id === "issueModal" && !customerId) { if (issueCustomerSearch) issueCustomerSearch.value = ""; renderCustomerSelects(); } if (id === "inspectionModal" && customerId) { document.querySelector("#inspectionCustomerSelect").value = customerId; } modalBackdrop.hidden = false; modal.classList.add("open"); modal.setAttribute("aria-hidden", "false"); } function closeModal() { document.querySelectorAll(".modal.open").forEach((modal) => { modal.classList.remove("open"); modal.setAttribute("aria-hidden", "true"); }); modalBackdrop.hidden = true; } async function refreshAfterMutation(issueId, message) { await loadData(); renderAll(); if (issueId && drawer.classList.contains("open")) openDrawer(issueId); if (message) showToast(message); } async function refreshAdminConfig(message) { adminConfig = await apiRequest("/api/admin/config"); dictionaries = { ...dictionaries, ...(adminConfig.dictionaries || {}) }; renderDictionaryOptions(); renderAdmin(); applyPermissions(); if (message) showToast(message); } async function mutateIssue(issueId, action, body = {}) { try { const payload = await apiRequest(`/api/issues/${encodeURIComponent(issueId)}/${action}`, { method: "PATCH", body }); await refreshAfterMutation(issueId, payload.message); } catch (error) { handleApiError(error); } } async function enterApp(user, token, silent = false) { currentUser = user; authToken = token; window.localStorage.setItem("keyissueToken", token); loginScreen.hidden = true; appShell.hidden = false; currentUserName.textContent = currentUser.userName; currentRoleName.textContent = currentUser.roleName; activePage = currentUser.pages[0] || "dashboard"; await loadData(); switchPage(activePage); if (!silent) showToast(`已通过后台登录:${currentUser.roleName}`); } function clearSession() { currentUser = null; authToken = ""; adminConfig = null; customers = []; issues = []; inspections = []; window.localStorage.removeItem("keyissueToken"); appShell.hidden = true; loginScreen.hidden = false; currentUserName.textContent = "未登录"; currentRoleName.textContent = "-"; loginMessage.textContent = ""; if (registerMessage) registerMessage.textContent = ""; closeDrawer(); closeModal(); } async function restoreSession() { if (!authToken) return; try { const payload = await apiRequest("/api/auth/me"); await enterApp(payload.user, authToken, true); } catch { clearSession(); } } document.querySelectorAll("[data-page]").forEach((button) => { button.addEventListener("click", () => switchPage(button.dataset.page)); }); document.querySelectorAll("[data-page-jump]").forEach((button) => { button.addEventListener("click", () => switchPage(button.dataset.pageJump)); }); [searchInput, tierFilter, departmentFilter].forEach((control) => { control.addEventListener("input", renderAll); control.addEventListener("change", renderAll); }); statusButtons.forEach((button) => { button.addEventListener("click", () => { activeStatus = button.dataset.status; statusButtons.forEach((item) => item.classList.toggle("active", item === button)); renderAll(); }); }); document.addEventListener("click", async (event) => { const saveUserButton = event.target.closest("[data-admin-save-user]"); if (saveUserButton) { if (!requirePermission("admin:manage", "保存后台账号")) return; const userId = saveUserButton.dataset.adminSaveUser; try { const payload = await apiRequest(`/api/admin/users/${encodeURIComponent(userId)}`, { method: "PATCH", body: { displayName: document.querySelector(`[data-admin-user-name="${userId}"]`)?.value, phone: document.querySelector(`[data-admin-user-phone="${userId}"]`)?.value, department: document.querySelector(`[data-admin-user-department="${userId}"]`)?.value, position: document.querySelector(`[data-admin-user-position="${userId}"]`)?.value, password: document.querySelector(`[data-admin-user-password="${userId}"]`)?.value, role: document.querySelector(`[data-admin-user-role="${userId}"]`)?.value } }); await refreshAdminConfig(payload.message); } catch (error) { handleApiError(error); } return; } const toggleUserButton = event.target.closest("[data-admin-toggle-user]"); if (toggleUserButton) { if (!requirePermission("admin:manage", "停启用后台账号")) return; const userId = toggleUserButton.dataset.adminToggleUser; const user = adminConfig?.users.find((item) => item.id === userId); if (!user) return; try { const payload = await apiRequest(`/api/admin/users/${encodeURIComponent(userId)}`, { method: "PATCH", body: { status: user.status === "停用" ? "启用" : "停用" } }); await refreshAdminConfig(payload.message); } catch (error) { handleApiError(error); } return; } const saveRoleButton = event.target.closest("[data-admin-save-role]"); if (saveRoleButton) { if (!requirePermission("admin:manage", "保存角色权限")) return; const roleKey = saveRoleButton.dataset.adminSaveRole; let pages = [...document.querySelectorAll(`[data-role-page="${roleKey}"]`)].filter((input) => input.checked).map((input) => input.value); const permissions = [...document.querySelectorAll(`[data-role-permission="${roleKey}"]`)].filter((input) => input.checked).map((input) => input.value); if (!permissions.includes("admin:manage")) pages = pages.filter((page) => page !== "admin"); try { const payload = await apiRequest(`/api/admin/roles/${encodeURIComponent(roleKey)}`, { method: "PATCH", body: { pages, permissions } }); await refreshAdminConfig(payload.message); } catch (error) { handleApiError(error); } return; } const approveCustomerButton = event.target.closest("[data-admin-customer-approve]"); if (approveCustomerButton) { if (!requirePermission("customer:approve", "审核客户入池")) return; try { const payload = await apiRequest(`/api/admin/customer-drafts/${encodeURIComponent(approveCustomerButton.dataset.adminCustomerApprove)}`, { method: "PATCH", body: { action: "approve" } }); await loadData(); renderAll(); showToast(payload.message); } catch (error) { handleApiError(error); } return; } const rejectCustomerButton = event.target.closest("[data-admin-customer-reject]"); if (rejectCustomerButton) { if (!requirePermission("customer:approve", "驳回客户入池")) return; try { const payload = await apiRequest(`/api/admin/customer-drafts/${encodeURIComponent(rejectCustomerButton.dataset.adminCustomerReject)}`, { method: "PATCH", body: { action: "reject", reviewNote: "管理员驳回,需补充入选说明或责任信息。" } }); await loadData(); renderAll(); showToast(payload.message); } catch (error) { handleApiError(error); } return; } const openButton = event.target.closest("[data-open-modal]"); if (openButton) { const modalName = openButton.dataset.openModal; const permissionMap = { customerModal: ["customer:submit", "提交客户入池申请"], issueModal: ["issue:create", "新增问题反馈"], inspectionModal: ["inspection:create", "提交每日巡查"], handleModal: ["issue:handle", "提交处置"] }; const [required, actionName] = permissionMap[modalName] || ["issue:view", "打开表单"]; if (!requirePermission(required, actionName)) return; openModal(modalName, openButton.dataset.customer || openButton.dataset.inspectionCustomer); return; } const customerButton = event.target.closest("[data-view-customer]"); if (customerButton) { if (!requirePermission("issue:view", "查看客户详情")) return; openCustomerDrawer(customerButton.dataset.viewCustomer); return; } const viewButton = event.target.closest("[data-view-issue]"); if (viewButton) { if (!requirePermission("issue:view", "查看问题详情")) return; openDrawer(viewButton.dataset.viewIssue); return; } const handleButton = event.target.closest("[data-quick-handle]"); if (handleButton) { if (!requirePermission("issue:handle", "处置问题")) return; selectedIssueId = handleButton.dataset.quickHandle; openModal("handleModal"); return; } const passButton = event.target.closest("[data-audit-pass]"); if (passButton) { if (!requirePermission("audit:review", "核查通过")) return; mutateIssue(passButton.dataset.auditPass, "audit-pass"); return; } const rejectButton = event.target.closest("[data-audit-reject]"); if (rejectButton) { if (!requirePermission("audit:review", "驳回整改")) return; mutateIssue(rejectButton.dataset.auditReject, "audit-reject"); } }); document.querySelectorAll("[data-close-modal]").forEach((button) => button.addEventListener("click", closeModal)); modalBackdrop.addEventListener("click", closeModal); document.querySelector("[data-close-drawer]").addEventListener("click", closeDrawer); document.querySelector("[data-close-customer-drawer]").addEventListener("click", closeDrawer); drawerBackdrop.addEventListener("click", closeDrawer); document.querySelectorAll("[data-customer-drawer-action]").forEach((button) => { button.addEventListener("click", () => { if (!selectedCustomerId) return; if (button.dataset.customerDrawerAction === "inspection") { if (!requirePermission("inspection:create", "提交每日巡查")) return; openModal("inspectionModal", selectedCustomerId); } if (button.dataset.customerDrawerAction === "feedback") { if (!requirePermission("issue:create", "提交问题反馈")) return; openModal("issueModal", selectedCustomerId); } }); }); document.querySelectorAll("[data-drawer-action]").forEach((button) => { button.addEventListener("click", () => { if (!selectedIssueId) return; const action = button.dataset.drawerAction; if (action === "assign") { if (!requirePermission("issue:assign", "指派责任人")) return; mutateIssue(selectedIssueId, "assign", { owner: "部门负责人" }); } if (action === "handle") { if (!requirePermission("issue:handle", "提交处置")) return; openModal("handleModal"); } if (action === "approve") { if (!requirePermission("audit:review", "核查通过")) return; mutateIssue(selectedIssueId, "audit-pass"); } if (action === "reject") { if (!requirePermission("audit:review", "驳回整改")) return; mutateIssue(selectedIssueId, "audit-reject"); } }); }); document.querySelector("#issueForm").addEventListener("submit", async (event) => { event.preventDefault(); if (!requirePermission("issue:create", "提交问题反馈")) return; const form = event.currentTarget; if (!beginFormSubmit(form)) return; try { const data = Object.fromEntries(new FormData(form).entries()); const payload = await apiRequest("/api/issues", { method: "POST", body: data }); form.reset(); closeModal(); await refreshAfterMutation(payload.issue.id, payload.message); switchPage("issues"); } catch (error) { handleApiError(error); } finally { endFormSubmit(form); } }); document.querySelector("#inspectionForm").addEventListener("submit", async (event) => { event.preventDefault(); if (!requirePermission("inspection:create", "提交每日线上巡查")) return; const form = event.currentTarget; if (!beginFormSubmit(form)) return; try { const data = Object.fromEntries(new FormData(form).entries()); const payload = await apiRequest("/api/inspections", { method: "POST", body: data }); form.reset(); closeModal(); await loadData(); switchPage("inspections"); showToast(payload.message); } catch (error) { handleApiError(error); } finally { endFormSubmit(form); } }); document.querySelector("#handleForm").addEventListener("submit", async (event) => { event.preventDefault(); if (!selectedIssueId) return; if (!requirePermission("issue:handle", "提交处置")) return; const form = event.currentTarget; if (!beginFormSubmit(form)) return; try { const data = Object.fromEntries(new FormData(form).entries()); const payload = await apiRequest(`/api/issues/${encodeURIComponent(selectedIssueId)}/handle`, { method: "PATCH", body: data }); form.reset(); closeModal(); await refreshAfterMutation(selectedIssueId, payload.message); } catch (error) { handleApiError(error); } finally { endFormSubmit(form); } }); document.querySelector("#customerForm").addEventListener("submit", async (event) => { event.preventDefault(); if (!requirePermission("customer:submit", "提交客户入池申请")) return; const form = event.currentTarget; if (!beginFormSubmit(form)) return; try { const data = Object.fromEntries(new FormData(form).entries()); const payload = await apiRequest("/api/customers", { method: "POST", body: data }); form.reset(); closeModal(); await loadData(); if (payload.customer) switchPage("customers"); if (payload.draft && hasPermission("page:admin")) switchPage("admin"); showToast(payload.message); } catch (error) { handleApiError(error); } finally { endFormSubmit(form); } }); document.querySelector("#adminUserForm")?.addEventListener("submit", async (event) => { event.preventDefault(); if (!requirePermission("admin:manage", "新增后台账号")) return; const form = event.currentTarget; if (!beginFormSubmit(form)) return; try { const data = Object.fromEntries(new FormData(form).entries()); const payload = await apiRequest("/api/admin/users", { method: "POST", body: data }); form.reset(); await refreshAdminConfig(payload.message); } catch (error) { handleApiError(error); } finally { endFormSubmit(form); } }); document.querySelector("#adminDictionaryForm")?.addEventListener("submit", async (event) => { event.preventDefault(); if (!requirePermission("admin:manage", "保存基础字典")) return; const form = event.currentTarget; if (!beginFormSubmit(form)) return; try { const data = Object.fromEntries(new FormData(form).entries()); const payload = await apiRequest("/api/admin/dictionaries", { method: "PATCH", body: data }); dictionaries = { ...dictionaries, ...(payload.dictionaries || {}) }; await refreshAdminConfig(payload.message); } catch (error) { handleApiError(error); } finally { endFormSubmit(form); } }); issueCustomerSearch?.addEventListener("input", () => { renderCustomerSelects(issueCustomerSearch.value); }); document.querySelector("#saveRulesBtn").addEventListener("click", async () => { if (!requirePermission("rules:edit", "保存预警规则")) return; try { const payload = await apiRequest("/api/rules", { method: "POST", body: { source: "official" } }); showToast(payload.message); } catch (error) { handleApiError(error); } }); loginForm.addEventListener("submit", async (event) => { event.preventDefault(); loginMessage.textContent = "正在请求后台登录..."; try { const payload = await apiRequest("/api/auth/login", { method: "POST", body: { username: loginUsername.value.trim(), password: loginPassword.value } }); loginMessage.textContent = ""; await enterApp(payload.user, payload.token); } catch (error) { loginMessage.textContent = error.message; } }); registerForm?.addEventListener("submit", async (event) => { event.preventDefault(); const form = event.currentTarget; const password = registerPassword.value.trim(); const confirmPassword = registerPasswordConfirm.value.trim(); if (password.length < 6) { registerMessage.textContent = "密码至少需要 6 位。"; return; } if (password !== confirmPassword) { registerMessage.textContent = "两次输入的密码不一致。"; return; } if (!beginFormSubmit(form)) return; registerMessage.textContent = "正在核对花名册并创建账号..."; try { const payload = await apiRequest("/api/auth/register", { method: "POST", body: { name: registerName.value.trim(), phone: registerPhone.value.trim(), password, confirmPassword } }); registerMessage.textContent = ""; form.reset(); await enterApp(payload.user, payload.token); showToast(payload.message); } catch (error) { registerMessage.textContent = error.message; } finally { endFormSubmit(form); } }); logoutBtn.addEventListener("click", async () => { try { await apiRequest("/api/auth/logout", { method: "POST" }); } catch { // 即使后台会话已经失效,也要清理本地登录态。 } clearSession(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeDrawer(); closeModal(); } }); restoreSession();