| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223 |
- 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);
- if (!element.matches("button")) element.disabled = !allowed;
- if (element.matches("button")) element.disabled = false;
- element.toggleAttribute("aria-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 `<td data-label="${escapeHtml(label)}">${content}</td>`;
- }
- function dataTagBadge(item) {
- return item?.dataTag ? `<span class="data-tag">${escapeHtml(item.dataTag)}</span>` : "";
- }
- 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 `<option value="${escapeHtml(text)}"${isSelected}>${escapeHtml(text)}</option>`;
- }).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 = `<option value="all">全部部门</option>${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 `
- <button class="customer-cell-button" type="button" data-view-customer="${escapeHtml(customer.id)}">
- <strong>${escapeHtml(customer.name)}${dataTagBadge(customer)}</strong>
- <small>${escapeHtml(customer.address)}</small>
- <span class="customer-cell-meta">
- <span class="badge">${escapeHtml(customer.tier)}</span>
- <span class="status-badge ${progressClass(customer.progressStatus)}">${escapeHtml(customer.progressStatus || "正常")}</span>
- <span class="status-badge ${inspectionFlagClass(customer.inspectionStatus)}">${escapeHtml(inspectionFlag(customer.inspectionStatus))}</span>
- </span>
- </button>
- `;
- }
- 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]) => `
- <div class="bar-item">
- <span>${escapeHtml(type)}</span>
- <div><i style="width: ${Math.max(22, Math.round((count / typeMax) * 100))}%;"></i></div>
- <strong>${count}</strong>
- </div>
- `).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) => `<div><strong>${index + 1}. ${escapeHtml(department)}</strong><span>${count} 件</span></div>`)
- .join("");
- }
- function issueRow(issue, compact = false) {
- const customer = getCustomer(issue.customerId);
- const cells = compact ? `
- ${cell("客户", customerCell(customer))}
- ${cell("等级", `<span class="badge">${escapeHtml(customer.tier)}</span>`)}
- ${cell("部门", escapeHtml(customer.department))}
- ${cell("问题来源", escapeHtml(issue.source || "-"))}
- ${cell("问题类型", escapeHtml(issue.type))}
- ${cell("预警状态", `<span class="status-badge ${statusClass(issue.status)}">${escapeHtml(issue.status)}</span>`)}
- ${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("预警状态", `<span class="status-badge ${statusClass(issue.status)}">${escapeHtml(issue.status)}</span>`)}
- ${cell("责任人", escapeHtml(issue.owner))}
- ${cell("根因", escapeHtml(issue.rootCause))}
- ${cell("当前处置", escapeHtml(issue.action))}
- ${cell("处置后满意度", `<span class="status-badge ${statusClass(issue.postSatisfaction)}">${escapeHtml(issue.postSatisfaction || "待处置")}</span>`)}
- `;
- return `<tr>${cells}${cell("操作", `<div class="action-cell"><button class="small-button" type="button" data-view-issue="${escapeHtml(issue.id)}" data-requires="issue:view">查看</button><button class="small-button" type="button" data-quick-handle="${escapeHtml(issue.id)}" data-requires="issue:handle">处置</button></div>`)}</tr>`;
- }
- 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("") ||
- `<tr><td colspan="9">暂无符合条件的问题。</td></tr>`;
- document.querySelector("#issueRows").innerHTML = visible.map((issue) => issueRow(issue)).join("") ||
- `<tr><td colspan="11">暂无符合条件的问题反馈。</td></tr>`;
- 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 `
- <tr>
- ${cell("客户", customerCell(customer))}
- ${cell("问题", escapeHtml(issue.type))}
- ${cell("处置说明", escapeHtml(issue.handlerNote))}
- ${cell("提交人", escapeHtml(issue.submitter))}
- ${cell("操作", `<div class="action-cell"><button class="small-button" type="button" data-view-issue="${escapeHtml(issue.id)}" data-requires="issue:view">查看</button><button class="small-button" type="button" data-audit-pass="${escapeHtml(issue.id)}" data-requires="audit:review">通过</button><button class="small-button" type="button" data-audit-reject="${escapeHtml(issue.id)}" data-requires="audit:review">驳回</button></div>`)}
- </tr>
- `;
- }).join("") || `<tr><td colspan="5">当前没有待核查问题。</td></tr>`;
- }
- function renderCustomers() {
- document.querySelector("#customerRows").innerHTML = customers.map((customer) => {
- const count = issues.filter((issue) => issue.customerId === customer.id).length;
- return `
- <tr>
- ${cell("客户", customerCell(customer))}
- ${cell("等级", `<span class="badge">${escapeHtml(customer.tier)}</span>`)}
- ${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("当前进度", `<span class="status-badge ${progressClass(customer.progressStatus)}">${escapeHtml(customer.progressStatus || "正常")}</span>`)}
- ${cell("巡查与否", `<span class="status-badge ${inspectionFlagClass(customer.inspectionStatus)}">${escapeHtml(inspectionFlag(customer.inspectionStatus))}</span>`)}
- ${cell("巡查状态", `<span class="status-badge ${inspectionClass(customer.inspectionStatus)}">${escapeHtml(customer.inspectionStatus || "未巡查")}</span>`)}
- ${cell("最近巡查", `${escapeHtml(customer.inspectionAt || "-")}<br><small>${escapeHtml(customer.inspectionSummary || "")}</small>`)}
- ${cell("问题数", count)}
- ${cell("操作", `<div class="action-cell"><button class="small-button" type="button" data-open-modal="inspectionModal" data-inspection-customer="${escapeHtml(customer.id)}" data-requires="inspection:create">日查</button><button class="small-button" type="button" data-open-modal="issueModal" data-customer="${escapeHtml(customer.id)}" data-requires="issue:create">反馈</button></div>`)}
- </tr>
- `;
- }).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) => `<option value="${escapeHtml(customer.id)}">${escapeHtml(customer.name)} · ${escapeHtml(customer.address)}</option>`).join("");
- const issueSelect = document.querySelector("#issueCustomerSelect");
- const inspectionSelect = document.querySelector("#inspectionCustomerSelect");
- if (issueSelect) issueSelect.innerHTML = options;
- if (inspectionSelect) inspectionSelect.innerHTML = customers.map((customer) => `<option value="${escapeHtml(customer.id)}">${escapeHtml(customer.name)} · ${escapeHtml(customer.address)}</option>`).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 = `
- <article><span>今日已巡查</span><strong>${done}</strong></article>
- <article><span>异常巡查</span><strong>${abnormal}</strong></article>
- <article><span>未巡查</span><strong>${pending}</strong></article>
- `;
- document.querySelector("#inspectionRows").innerHTML = inspections.map((item) => `
- <tr>
- ${cell("巡查编号", `${escapeHtml(item.id)}${dataTagBadge(item)}`)}
- ${cell("客户", customerCell(getCustomer(item.customerId)))}
- ${cell("巡查与否", `<span class="status-badge ${inspectionFlagClass(item.status)}">${escapeHtml(inspectionFlag(item.status))}</span>`)}
- ${cell("巡查状态", `<span class="status-badge ${inspectionClass(item.status)}">${escapeHtml(item.status)}</span>`)}
- ${cell("巡查人", escapeHtml(item.inspector))}
- ${cell("巡查时间", escapeHtml(item.createdAt))}
- ${cell("施工质量", escapeHtml(item.siteQuality))}
- ${cell("文明施工", escapeHtml(item.civility))}
- ${cell("客户群反馈", escapeHtml(item.groupFeedback))}
- ${cell("巡查结论", escapeHtml(item.summary))}
- </tr>
- `).join("") || `<tr><td colspan="10">暂无每日线上巡查记录。</td></tr>`;
- }
- function adminRoleOptions(selectedRole) {
- if (!adminConfig) return "";
- return Object.entries(adminConfig.roles).map(([roleKey, role]) => {
- const selected = roleKey === selectedRole ? " selected" : "";
- return `<option value="${escapeHtml(roleKey)}"${selected}>${escapeHtml(role.roleName)}</option>`;
- }).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) => `
- <label>
- <input type="checkbox" data-role-page="${escapeHtml(roleKey)}" value="${escapeHtml(page.key)}" ${role.pages.includes(page.key) || isAdmin ? "checked" : ""} ${isAdmin ? "disabled" : ""}>
- <span>${escapeHtml(page.name)}</span>
- </label>
- `).join("");
- const permissionControls = adminConfig.permissionCatalog.map((permission) => `
- <label>
- <input type="checkbox" data-role-permission="${escapeHtml(roleKey)}" value="${escapeHtml(permission.key)}" ${allPermissions || role.permissions.includes(permission.key) ? "checked" : ""} ${isAdmin ? "disabled" : ""}>
- <span>${escapeHtml(permission.name)}</span>
- </label>
- `).join("");
- return `
- <article class="role-card">
- <div class="role-card-head">
- <div>
- <strong>${escapeHtml(role.roleName)}</strong>
- <span>${escapeHtml(role.summary)}</span>
- </div>
- <button class="small-button" type="button" data-admin-save-role="${escapeHtml(roleKey)}" ${isAdmin ? "disabled" : ""}>保存角色权限</button>
- </div>
- <div class="role-card-section">
- <b>可访问页面</b>
- <div class="checkbox-grid">${pageControls}</div>
- </div>
- <div class="role-card-section">
- <b>可执行操作</b>
- <div class="checkbox-grid">${permissionControls}</div>
- </div>
- </article>
- `;
- }).join("");
- }
- function renderCustomerReviewRows() {
- const drafts = adminConfig.customerDrafts || [];
- return drafts.map((draft) => {
- const customer = draft.customer || {};
- const canReview = draft.status === "待审核";
- return `
- <tr>
- ${cell("审核单", `${escapeHtml(draft.id)}${dataTagBadge(draft)}`)}
- ${cell("客户", `<strong>${escapeHtml(customer.name)}</strong><br><small>${escapeHtml(customer.address || "-")}</small>`)}
- ${cell("等级", `<span class="badge">${escapeHtml(customer.tier || "-")}</span>`)}
- ${cell("提交人", `${escapeHtml(draft.submittedBy || "-")}<br><small>${escapeHtml(draft.submittedRole || "-")}</small>`)}
- ${cell("客管负责人", escapeHtml(customer.customerOwner || "-"))}
- ${cell("入选说明", escapeHtml(customer.keyCustomerReason || "-"))}
- ${cell("提交时间", escapeHtml(draft.submittedAt || "-"))}
- ${cell("状态", `<span class="status-badge ${draft.status === "待审核" ? "orange" : draft.status === "已通过" ? "closed" : "risk"}">${escapeHtml(draft.status)}</span>`)}
- ${cell("操作", canReview ? `<div class="action-cell"><button class="small-button" type="button" data-admin-customer-approve="${escapeHtml(draft.id)}">通过</button><button class="small-button" type="button" data-admin-customer-reject="${escapeHtml(draft.id)}">驳回</button></div>` : escapeHtml(draft.reviewNote || draft.customerId || "-"))}
- </tr>
- `;
- }).join("") || `<tr><td colspan="9">暂无客户入池审核单。</td></tr>`;
- }
- function renderAdmin() {
- if (!adminConfig) {
- document.querySelector("#adminUserRows").innerHTML = `<tr><td colspan="9">当前登录人没有系统后台管理权限。</td></tr>`;
- document.querySelector("#adminRoleCards").innerHTML = `<p class="muted-text">后台配置仅系统管理员可维护。</p>`;
- 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) => `
- <tr>
- ${cell("账号", `<strong>${escapeHtml(user.username)}</strong>`)}
- ${cell("姓名", `<input class="inline-input" data-admin-user-name="${escapeHtml(user.id)}" value="${escapeHtml(user.displayName)}">`)}
- ${cell("手机号", `<input class="inline-input" data-admin-user-phone="${escapeHtml(user.id)}" value="${escapeHtml(user.phone || "")}" placeholder="可选">`)}
- ${cell("部门", `<input class="inline-input" data-admin-user-department="${escapeHtml(user.id)}" value="${escapeHtml(user.department || "")}" placeholder="用于范围">`)}
- ${cell("岗位", `<input class="inline-input" data-admin-user-position="${escapeHtml(user.id)}" value="${escapeHtml(user.position || "")}" placeholder="用于范围">`)}
- ${cell("角色", `<select data-admin-user-role="${escapeHtml(user.id)}">${adminRoleOptions(user.role)}</select>`)}
- ${cell("状态", `<span class="status-badge ${user.status === "停用" ? "orange" : "closed"}">${escapeHtml(user.status)}</span>`)}
- ${cell("重置密码", `<input class="inline-input" type="password" data-admin-user-password="${escapeHtml(user.id)}" value="" placeholder="留空不修改">`)}
- ${cell("操作", `<div class="action-cell"><button class="small-button" type="button" data-admin-save-user="${escapeHtml(user.id)}">保存账号</button><button class="small-button" type="button" data-admin-toggle-user="${escapeHtml(user.id)}">${user.status === "停用" ? "启用" : "停用"}</button></div>`)}
- </tr>
- `).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) => `
- <article class="record-card">
- <div><strong>${escapeHtml(record.handler)}</strong><span>${escapeHtml(record.handledAt)}</span></div>
- <p>${escapeHtml(record.note)}</p>
- <small>处置后满意度:${escapeHtml(record.satisfaction || "待回访")};客户反馈:${escapeHtml(record.feedback || "待补充")}</small>
- </article>
- `).join("") : `<p class="muted-text">暂无处置记录,责任人提交后会按时间形成卡片流水。</p>`;
- document.querySelector("#drawerTimeline").innerHTML = issue.timeline.map((item) => `<li>${escapeHtml(item)}</li>`).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 = `<span class="status-badge ${progressClass(customer.progressStatus)}">${escapeHtml(customer.progressStatus || "正常")}</span>`;
- document.querySelector("#customerDrawerSatisfaction").textContent = `${customer.satisfaction || "-"} 分`;
- document.querySelector("#customerDrawerInspectionStatus").innerHTML = `<span class="status-badge ${inspectionClass(customer.inspectionStatus)}">${escapeHtml(customer.inspectionStatus || "未巡查")}</span>`;
- 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) => `
- <article class="record-card">
- <div><strong>${escapeHtml(issue.type)}</strong><span class="status-badge ${statusClass(issue.status)}">${escapeHtml(issue.status)}</span></div>
- <p>${escapeHtml(issue.summary || "未填写问题描述。")}</p>
- <small>责任人:${escapeHtml(issue.owner)};处置后满意度:${escapeHtml(issue.postSatisfaction || "待处置")}</small>
- </article>
- `).join("") : `<p class="muted-text">暂无关联问题反馈。</p>`;
- 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 (!modal) return;
- if ((id === "issueModal" || id === "inspectionModal") && customers.length === 0) {
- showToast("当前账号暂无可操作客户,请先新增客户或联系管理员分配客户。");
- return;
- }
- if ((id === "issueModal" || id === "inspectionModal") && customerId && !customers.some((customer) => customer.id === customerId)) {
- showToast("当前登录人无权操作该客户。");
- return;
- }
- 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();
|