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 `${content}`; } function dataTagBadge(item) { return item?.dataTag ? `${escapeHtml(item.dataTag)}` : ""; } function dictionaryValues(key) { const values = dictionaries[key]; return Array.isArray(values) && values.length ? values : []; } function optionList(values, selected = "") { return values.map((value) => { const text = String(value); const isSelected = selected === text ? " selected" : ""; return ``; }).join(""); } function setSelectOptions(select, values, selectedValue) { if (!select) return; const currentValue = selectedValue ?? select.value; const safeValues = values.length ? values : [currentValue].filter(Boolean); select.innerHTML = optionList(safeValues, currentValue); if (safeValues.includes(currentValue)) select.value = currentValue; } function renderDictionaryOptions() { const departments = dictionaryValues("departments"); const customerSources = dictionaryValues("customerSources"); const issueSources = dictionaryValues("issueSources"); const issueTypes = dictionaryValues("issueTypes"); const progressStatuses = dictionaryValues("progressStatuses"); const activeDepartment = departmentFilter.value || "all"; departmentFilter.innerHTML = `${optionList(departments, activeDepartment)}`; departmentFilter.value = departments.includes(activeDepartment) ? activeDepartment : "all"; setSelectOptions(document.querySelector('#customerForm select[name="source"]'), customerSources); setSelectOptions(document.querySelector('#customerForm select[name="department"]'), departments); setSelectOptions(document.querySelector('#customerForm select[name="progressStatus"]'), progressStatuses); setSelectOptions(document.querySelector('#issueForm select[name="source"]'), issueSources); setSelectOptions(document.querySelector('#issueForm select[name="type"]'), issueTypes); } function customerCell(customer) { return ` `; } function filteredIssues() { const keyword = searchInput.value.trim().toLowerCase(); const tier = tierFilter.value; const department = departmentFilter.value; return issues.filter((issue) => { const customer = getCustomer(issue.customerId); const targetText = [ customer.name, customer.address, customer.stage, customer.startDate, customer.plannedFinishDate, customer.progressStatus, issue.type, issue.source, issue.rootCause, issue.owner, issue.action ].join(" ").toLowerCase(); const matchesKeyword = !keyword || targetText.includes(keyword); const matchesTier = tier === "all" || customer.tier === tier; const matchesDepartment = department === "all" || customer.department === department; const matchesStatus = activeStatus === "all" || issue.status === activeStatus; return matchesKeyword && matchesTier && matchesDepartment && matchesStatus; }); } function countBy(list, getter) { return list.reduce((acc, item) => { const key = getter(item); acc[key] = (acc[key] || 0) + 1; return acc; }, {}); } function renderKpis() { const visible = filteredIssues(); const red = visible.filter((issue) => issue.status === "红色预警").length; const audit = visible.filter((issue) => issue.status === "待核查").length; const inspectionDone = customers.filter((customer) => customer.inspectionStatus === "已巡查" || customer.inspectionStatus === "异常").length; const abnormalInspection = customers.filter((customer) => customer.inspectionStatus === "异常").length; document.querySelector("#kpiInspectionDone").textContent = inspectionDone; document.querySelector("#kpiAbnormalInspections").textContent = abnormalInspection; document.querySelector("#kpiRedIssues").textContent = red; document.querySelector("#kpiAuditIssues").textContent = audit; document.querySelector("#funnelRed").textContent = `${issues.filter((issue) => issue.status === "红色预警").length} 件`; document.querySelector("#funnelOrange").textContent = `${issues.filter((issue) => issue.status === "橙色预警").length} 件`; document.querySelector("#funnelBlue").textContent = `${issues.filter((issue) => issue.status === "蓝色关注").length} 件`; document.querySelector("#sideAlertCount").textContent = `${issues.filter((issue) => issue.status === "红色预警").length} 件`; } function renderBars() { const typeCounts = countBy(issues, (issue) => issue.type); const typeMax = Math.max(1, ...Object.values(typeCounts)); document.querySelector("#issueTypeBars").innerHTML = Object.entries(typeCounts).map(([type, count]) => `
${escapeHtml(type)}
${count}
`).join(""); const departmentCounts = countBy(issues, (issue) => getCustomer(issue.customerId).department); document.querySelector("#departmentRanks").innerHTML = Object.entries(departmentCounts) .sort((a, b) => b[1] - a[1]) .map(([department, count], index) => `
${index + 1}. ${escapeHtml(department)}${count} 件
`) .join(""); } function issueRow(issue, compact = false) { const customer = getCustomer(issue.customerId); const cells = compact ? ` ${cell("客户", customerCell(customer))} ${cell("等级", `${escapeHtml(customer.tier)}`)} ${cell("部门", escapeHtml(customer.department))} ${cell("问题来源", escapeHtml(issue.source || "-"))} ${cell("问题类型", escapeHtml(issue.type))} ${cell("预警状态", `${escapeHtml(issue.status)}`)} ${cell("责任人", escapeHtml(issue.owner))} ${cell("当前处置", escapeHtml(issue.action))} ` : ` ${cell("反馈编号", `${escapeHtml(issue.id)}${dataTagBadge(issue)}`)} ${cell("客户", customerCell(customer))} ${cell("问题来源", escapeHtml(issue.source || "-"))} ${cell("问题类型", escapeHtml(issue.type))} ${cell("问题描述", escapeHtml(issue.summary || "未填写"))} ${cell("预警状态", `${escapeHtml(issue.status)}`)} ${cell("责任人", escapeHtml(issue.owner))} ${cell("根因", escapeHtml(issue.rootCause))} ${cell("当前处置", escapeHtml(issue.action))} ${cell("处置后满意度", `${escapeHtml(issue.postSatisfaction || "待处置")}`)} `; return `${cells}${cell("操作", `
`)}`; } function renderIssueTables() { const visible = filteredIssues(); const highPriority = [...visible].sort((a, b) => b.score - a.score).slice(0, 5); document.querySelector("#dashboardRows").innerHTML = highPriority.map((issue) => issueRow(issue, true)).join("") || `暂无符合条件的问题。`; document.querySelector("#issueRows").innerHTML = visible.map((issue) => issueRow(issue)).join("") || `暂无符合条件的问题反馈。`; const auditIssues = issues.filter((issue) => issue.status === "待核查"); document.querySelector("#auditCount").textContent = `${auditIssues.length} 件`; document.querySelector("#auditRows").innerHTML = auditIssues.map((issue) => { const customer = getCustomer(issue.customerId); return ` ${cell("客户", customerCell(customer))} ${cell("问题", escapeHtml(issue.type))} ${cell("处置说明", escapeHtml(issue.handlerNote))} ${cell("提交人", escapeHtml(issue.submitter))} ${cell("操作", `
`)} `; }).join("") || `当前没有待核查问题。`; } function renderCustomers() { document.querySelector("#customerRows").innerHTML = customers.map((customer) => { const count = issues.filter((issue) => issue.customerId === customer.id).length; return ` ${cell("客户", customerCell(customer))} ${cell("等级", `${escapeHtml(customer.tier)}`)} ${cell("新居地址", escapeHtml(customer.address))} ${cell("客管负责人", escapeHtml(customer.customerOwner || "-"))} ${cell("入选说明", escapeHtml(customer.keyCustomerReason || "-"))} ${cell("部门", escapeHtml(customer.department))} ${cell("设计师", escapeHtml(customer.designer))} ${cell("项目经理", escapeHtml(customer.manager))} ${cell("施工阶段", escapeHtml(customer.stage))} ${cell("开工日期", escapeHtml(customer.startDate || "-"))} ${cell("计划完工日期", escapeHtml(customer.plannedFinishDate || "-"))} ${cell("当前进度", `${escapeHtml(customer.progressStatus || "正常")}`)} ${cell("巡查与否", `${escapeHtml(inspectionFlag(customer.inspectionStatus))}`)} ${cell("巡查状态", `${escapeHtml(customer.inspectionStatus || "未巡查")}`)} ${cell("最近巡查", `${escapeHtml(customer.inspectionAt || "-")}
${escapeHtml(customer.inspectionSummary || "")}`)} ${cell("问题数", count)} ${cell("操作", `
`)} `; }).join(""); renderCustomerSelects(); } function renderCustomerSelects(filter = "") { const keyword = filter.trim().toLowerCase(); const visibleCustomers = customers.filter((customer) => { const text = `${customer.name} ${customer.address} ${customer.manager} ${customer.designer} ${customer.stage} ${customer.startDate} ${customer.plannedFinishDate} ${customer.progressStatus}`.toLowerCase(); return !keyword || text.includes(keyword); }); const options = visibleCustomers.map((customer) => ``).join(""); const issueSelect = document.querySelector("#issueCustomerSelect"); const inspectionSelect = document.querySelector("#inspectionCustomerSelect"); if (issueSelect) issueSelect.innerHTML = options; if (inspectionSelect) inspectionSelect.innerHTML = customers.map((customer) => ``).join(""); } function renderInspections() { const done = customers.filter((customer) => customer.inspectionStatus === "已巡查" || customer.inspectionStatus === "异常").length; const abnormal = customers.filter((customer) => customer.inspectionStatus === "异常").length; const pending = customers.filter((customer) => customer.inspectionStatus === "未巡查").length; document.querySelector("#inspectionSummaryCards").innerHTML = `
今日已巡查${done}
异常巡查${abnormal}
未巡查${pending}
`; document.querySelector("#inspectionRows").innerHTML = inspections.map((item) => ` ${cell("巡查编号", `${escapeHtml(item.id)}${dataTagBadge(item)}`)} ${cell("客户", customerCell(getCustomer(item.customerId)))} ${cell("巡查与否", `${escapeHtml(inspectionFlag(item.status))}`)} ${cell("巡查状态", `${escapeHtml(item.status)}`)} ${cell("巡查人", escapeHtml(item.inspector))} ${cell("巡查时间", escapeHtml(item.createdAt))} ${cell("施工质量", escapeHtml(item.siteQuality))} ${cell("文明施工", escapeHtml(item.civility))} ${cell("客户群反馈", escapeHtml(item.groupFeedback))} ${cell("巡查结论", escapeHtml(item.summary))} `).join("") || `暂无每日线上巡查记录。`; } function adminRoleOptions(selectedRole) { if (!adminConfig) return ""; return Object.entries(adminConfig.roles).map(([roleKey, role]) => { const selected = roleKey === selectedRole ? " selected" : ""; return ``; }).join(""); } function renderAdminRoleCards() { return Object.entries(adminConfig.roles).map(([roleKey, role]) => { const isAdmin = roleKey === "admin"; const allPermissions = role.permissions.includes("*"); const pageControls = adminConfig.pageCatalog.map((page) => ` `).join(""); const permissionControls = adminConfig.permissionCatalog.map((permission) => ` `).join(""); return `
${escapeHtml(role.roleName)} ${escapeHtml(role.summary)}
可访问页面
${pageControls}
可执行操作
${permissionControls}
`; }).join(""); } function renderCustomerReviewRows() { const drafts = adminConfig.customerDrafts || []; return drafts.map((draft) => { const customer = draft.customer || {}; const canReview = draft.status === "待审核"; return ` ${cell("审核单", `${escapeHtml(draft.id)}${dataTagBadge(draft)}`)} ${cell("客户", `${escapeHtml(customer.name)}
${escapeHtml(customer.address || "-")}`)} ${cell("等级", `${escapeHtml(customer.tier || "-")}`)} ${cell("提交人", `${escapeHtml(draft.submittedBy || "-")}
${escapeHtml(draft.submittedRole || "-")}`)} ${cell("客管负责人", escapeHtml(customer.customerOwner || "-"))} ${cell("入选说明", escapeHtml(customer.keyCustomerReason || "-"))} ${cell("提交时间", escapeHtml(draft.submittedAt || "-"))} ${cell("状态", `${escapeHtml(draft.status)}`)} ${cell("操作", canReview ? `
` : escapeHtml(draft.reviewNote || draft.customerId || "-"))} `; }).join("") || `暂无客户入池审核单。`; } function renderAdmin() { if (!adminConfig) { document.querySelector("#adminUserRows").innerHTML = `当前登录人没有系统后台管理权限。`; document.querySelector("#adminRoleCards").innerHTML = `

后台配置仅系统管理员可维护。

`; 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) => ` ${cell("账号", `${escapeHtml(user.username)}`)} ${cell("姓名", ``)} ${cell("手机号", ``)} ${cell("部门", ``)} ${cell("岗位", ``)} ${cell("角色", ``)} ${cell("状态", `${escapeHtml(user.status)}`)} ${cell("重置密码", ``)} ${cell("操作", `
`)} `).join(""); document.querySelector("#adminRoleCards").innerHTML = renderAdminRoleCards(); const dictionaryForm = document.querySelector("#adminDictionaryForm"); Object.entries(adminConfig.dictionaries || {}).forEach(([key, values]) => { if (dictionaryForm.elements[key]) dictionaryForm.elements[key].value = values.join("\n"); }); } function renderAll() { renderKpis(); renderBars(); renderIssueTables(); renderCustomers(); renderInspections(); if (activePage === "admin") renderAdmin(); applyPermissions(); } async function loadData() { const issueRequest = hasPermission("issue:view") ? apiRequest("/api/issues") : Promise.resolve({ issues: [] }); const inspectionRequest = hasPermission("inspection:view") ? apiRequest("/api/inspections") : Promise.resolve({ inspections: [] }); const adminRequest = hasPermission("admin:manage") ? apiRequest("/api/admin/config") : Promise.resolve(null); const [customerPayload, issuePayload, inspectionPayload, metaPayload, adminPayload] = await Promise.all([ apiRequest("/api/customers"), issueRequest, inspectionRequest, apiRequest("/api/meta"), adminRequest ]); customers = customerPayload.customers; issues = issuePayload.issues; inspections = inspectionPayload.inspections; dictionaries = { ...dictionaries, ...(metaPayload.dictionaries || {}) }; adminConfig = adminPayload; renderDictionaryOptions(); } function switchPage(page) { if (!hasPermission(`page:${page}`)) { showToast("后台未授权访问该页面。"); return; } activePage = page; pageTitle.textContent = pageTitles[page] || pageTitles.dashboard; document.querySelectorAll(".page-section").forEach((section) => { section.classList.toggle("active", section.dataset.section === page); }); document.querySelectorAll("[data-page]").forEach((button) => { button.classList.toggle("active", button.dataset.page === page); }); renderAll(); } function openDrawer(issueId) { const issue = issues.find((item) => item.id === issueId); if (!issue) return; const customer = getCustomer(issue.customerId); selectedIssueId = issueId; selectedCustomerId = null; customerDrawer.classList.remove("open"); customerDrawer.setAttribute("aria-hidden", "true"); document.querySelector("#drawerName").textContent = `${customer.name} · ${issue.type}`; document.querySelector("#drawerScore").textContent = issue.score; document.querySelector("#drawerStatus").textContent = issue.status; document.querySelector("#drawerDepartment").textContent = customer.department; document.querySelector("#drawerType").textContent = issue.type; document.querySelector("#drawerOwner").textContent = issue.owner; document.querySelector("#drawerPostSatisfaction").textContent = issue.postSatisfaction || "待处置"; document.querySelector("#drawerLatestHandle").textContent = issue.handlerNote || "待提交处置记录"; document.querySelector("#drawerSummary").textContent = issue.summary || "未填写问题描述。"; const records = issue.handlingRecords || []; document.querySelector("#drawerHandlingRecords").innerHTML = records.length ? records.map((record) => `
${escapeHtml(record.handler)}${escapeHtml(record.handledAt)}

${escapeHtml(record.note)}

处置后满意度:${escapeHtml(record.satisfaction || "待回访")};客户反馈:${escapeHtml(record.feedback || "待补充")}
`).join("") : `

暂无处置记录,责任人提交后会按时间形成卡片流水。

`; document.querySelector("#drawerTimeline").innerHTML = issue.timeline.map((item) => `
  • ${escapeHtml(item)}
  • `).join(""); drawerBackdrop.hidden = false; drawer.classList.add("open"); drawer.setAttribute("aria-hidden", "false"); applyPermissions(); } function openCustomerDrawer(customerId) { const customer = getCustomer(customerId); selectedCustomerId = customer.id; selectedIssueId = null; drawer.classList.remove("open"); drawer.setAttribute("aria-hidden", "true"); const relatedIssues = issues.filter((issue) => issue.customerId === customer.id); document.querySelector("#customerDrawerName").textContent = customer.name; document.querySelector("#customerDrawerInspectionFlag").textContent = inspectionFlag(customer.inspectionStatus); document.querySelector("#customerDrawerTier").textContent = customer.tier; document.querySelector("#customerDrawerSource").textContent = customer.source || "-"; document.querySelector("#customerDrawerCustomerOwner").textContent = customer.customerOwner || "-"; document.querySelector("#customerDrawerDepartment").textContent = customer.department; document.querySelector("#customerDrawerDesigner").textContent = customer.designer; document.querySelector("#customerDrawerManager").textContent = customer.manager; document.querySelector("#customerDrawerStage").textContent = customer.stage; document.querySelector("#customerDrawerStartDate").textContent = customer.startDate || "-"; document.querySelector("#customerDrawerPlannedFinishDate").textContent = customer.plannedFinishDate || "-"; document.querySelector("#customerDrawerProgressStatus").innerHTML = `${escapeHtml(customer.progressStatus || "正常")}`; document.querySelector("#customerDrawerSatisfaction").textContent = `${customer.satisfaction || "-"} 分`; document.querySelector("#customerDrawerInspectionStatus").innerHTML = `${escapeHtml(customer.inspectionStatus || "未巡查")}`; document.querySelector("#customerDrawerInspector").textContent = customer.inspector || "-"; document.querySelector("#customerDrawerInspectionAt").textContent = customer.inspectionAt || "-"; document.querySelector("#customerDrawerAddress").textContent = customer.address || "-"; document.querySelector("#customerDrawerKeyReason").textContent = customer.keyCustomerReason || "暂无重点客户入选说明。"; document.querySelector("#customerDrawerInspectionSummary").textContent = customer.inspectionSummary || "暂无巡查结论。"; document.querySelector("#customerDrawerIssues").innerHTML = relatedIssues.length ? relatedIssues.map((issue) => `
    ${escapeHtml(issue.type)}${escapeHtml(issue.status)}

    ${escapeHtml(issue.summary || "未填写问题描述。")}

    责任人:${escapeHtml(issue.owner)};处置后满意度:${escapeHtml(issue.postSatisfaction || "待处置")}
    `).join("") : `

    暂无关联问题反馈。

    `; 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();