(function () { var config = Object.assign({ apiBaseUrl: "http://localhost:9098" }, window.VISITOR_LOGIN_CONFIG || {}); var currentProfile = null; var selectedAvatarFile = null; var summaryCards = [ { title: "线上预约板块", color: "#2277ff", metrics: [ { label: "预约登记数", value: "2837" }, { label: "来访车辆数", value: "637" } ], foot: "近7日预约数据概览" }, { title: "现场预约板块", color: "#1f9a6b", metrics: [ { label: "预约登记数", value: "2837" }, { label: "来访车辆数", value: "637" } ], foot: "近7日预约数据概览" }, { title: "来访中", color: "#2e78ff", metrics: [ { label: "", value: "6", unit: "人" } ], foot: "实时状态统计" }, { title: "已签离", color: "#2fb579", metrics: [ { label: "", value: "3", unit: "人" } ], foot: "实时状态统计" }, { title: "逾期滞留", color: "#f39822", metrics: [ { label: "", value: "0", unit: "人" } ], foot: "实时状态统计" } ]; var quickEntries = [ { title: "部门管理", color: "#2580ff", icon: "tree" }, { title: "员工管理", color: "#ff7545", icon: "user" }, { title: "设备管理", color: "#ff4c5d", icon: "device", href: "./visitor-machine.html" }, { title: "预约记录", color: "#ff9e33", icon: "book" }, { title: "区域管理", color: "#43bd19", icon: "map", href: "./area-management.html" }, { title: "账号管理", color: "#5b77f1", icon: "account" }, { title: "系统配置", color: "#416bea", icon: "config", href: "./system-config.html" }, { title: "系统更新", color: "#22bfc4", icon: "update", href: "./version-update.html" } ]; var chartData = { labels: ["09-08", "09-09", "09-10", "09-11", "09-12", "09-13", "09-14"], 现场登记: [86, 132, 58, 92, 146, 138, 82], 预约登记: [118, 168, 72, 105, 186, 174, 126], 车辆来访: [70, 106, 78, 54, 95, 118, 72] }; renderSummaryCards(); renderQuickEntries(); drawTrendChart(); bindActions(); loadProfile(); window.addEventListener("resize", drawTrendChart); function renderSummaryCards() { var root = document.getElementById("summaryCards"); root.innerHTML = summaryCards.map(function (card) { return [ '
', '
', ' ', ' ', ' ' + card.title + '', '
', '
', card.metrics.map(function (metric) { return [ '
', metric.label ? '
' + metric.label + '
' : '
 
', '
' + metric.value + (metric.unit ? '' + metric.unit + '' : "") + '
', '
' ].join(""); }).join(""), '
', '
' + card.foot + '
', '
' ].join(""); }).join(""); } function renderQuickEntries() { var root = document.getElementById("quickEntries"); root.innerHTML = quickEntries.map(function (item) { return [ '', ' ' + getIcon(item.icon) + '', ' ' + item.title + '', '' ].join(""); }).join(""); } function drawTrendChart() { var canvas = document.getElementById("trendChart"); var rect = canvas.getBoundingClientRect(); var dpr = window.devicePixelRatio || 1; canvas.width = Math.max(760, Math.floor(rect.width * dpr)); canvas.height = Math.floor(282 * dpr); var ctx = canvas.getContext("2d"); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); var width = canvas.width / dpr; var height = canvas.height / dpr; var padding = { left: 42, right: 16, top: 24, bottom: 36 }; var chartWidth = width - padding.left - padding.right; var chartHeight = height - padding.top - padding.bottom; var max = 180; var ticks = [0, 30, 60, 90, 120, 150, 180]; ctx.clearRect(0, 0, width, height); ctx.font = "10px Microsoft YaHei, Arial"; ctx.textAlign = "right"; ctx.textBaseline = "middle"; ticks.forEach(function (tick) { var y = padding.top + chartHeight - (tick / max) * chartHeight; ctx.strokeStyle = "#edf2f8"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(padding.left, y); ctx.lineTo(width - padding.right, y); ctx.stroke(); ctx.fillStyle = "#9aabc0"; ctx.fillText(String(tick), padding.left - 8, y); }); ctx.textAlign = "center"; ctx.textBaseline = "top"; chartData.labels.forEach(function (label, index) { var x = padding.left + (chartWidth / (chartData.labels.length - 1)) * index; ctx.fillStyle = "#9aabc0"; ctx.fillText(label, x, height - 20); }); drawSeries(ctx, chartData["现场登记"], "#3c7cff", padding, chartWidth, chartHeight, max, true); drawSeries(ctx, chartData["预约登记"], "#23bd8e", padding, chartWidth, chartHeight, max, true); drawSeries(ctx, chartData["车辆来访"], "#f3a019", padding, chartWidth, chartHeight, max, false); } function drawSeries(ctx, data, color, padding, chartWidth, chartHeight, max, fill) { var points = data.map(function (value, index) { return { x: padding.left + (chartWidth / (data.length - 1)) * index, y: padding.top + chartHeight - (value / max) * chartHeight }; }); ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.beginPath(); points.forEach(function (point, index) { if (index === 0) { ctx.moveTo(point.x, point.y); } else { var previous = points[index - 1]; var midX = (previous.x + point.x) / 2; ctx.bezierCurveTo(midX, previous.y, midX, point.y, point.x, point.y); } }); ctx.stroke(); if (fill) { var gradient = ctx.createLinearGradient(0, padding.top, 0, padding.top + chartHeight); gradient.addColorStop(0, color + "22"); gradient.addColorStop(1, color + "03"); ctx.lineTo(points[points.length - 1].x, padding.top + chartHeight); ctx.lineTo(points[0].x, padding.top + chartHeight); ctx.closePath(); ctx.fillStyle = gradient; ctx.fill(); } points.forEach(function (point) { ctx.fillStyle = color; ctx.beginPath(); ctx.arc(point.x, point.y, 2.2, 0, Math.PI * 2); ctx.fill(); }); } function bindActions() { var userMenuButton = document.getElementById("userMenuButton"); var userDropdown = document.getElementById("userDropdown"); var profileForm = document.getElementById("profileForm"); var passwordForm = document.getElementById("passwordForm"); var fullscreenButton = document.querySelector(".icon-button"); var avatarBox = document.getElementById("avatarBox"); var avatarFile = document.getElementById("avatarFile"); userMenuButton.addEventListener("click", function (event) { event.stopPropagation(); var hidden = userDropdown.hidden; userDropdown.hidden = !hidden; userMenuButton.setAttribute("aria-expanded", String(hidden)); }); userDropdown.addEventListener("click", function (event) { var action = event.target.getAttribute("data-action"); if (!action) { return; } userDropdown.hidden = true; userMenuButton.setAttribute("aria-expanded", "false"); if (action === "profile") { openProfileModal(); } else if (action === "password") { openPasswordModal(); } else if (action === "logout") { logout(); } }); document.addEventListener("click", function () { userDropdown.hidden = true; userMenuButton.setAttribute("aria-expanded", "false"); }); document.querySelectorAll("[data-close]").forEach(function (button) { button.addEventListener("click", function () { closeModal(button.getAttribute("data-close")); }); }); profileForm.addEventListener("submit", function (event) { event.preventDefault(); saveProfile(); }); avatarBox.addEventListener("click", function () { avatarFile.click(); }); avatarFile.addEventListener("change", function () { var file = avatarFile.files && avatarFile.files[0]; if (!file) { return; } if (!/^image\//.test(file.type)) { setModalMessage("profileMessage", "请选择图片文件", "error"); avatarFile.value = ""; return; } selectedAvatarFile = file; showAvatarPreview(URL.createObjectURL(file)); setModalMessage("profileMessage", "", ""); }); passwordForm.addEventListener("submit", function (event) { event.preventDefault(); updatePassword(); }); fullscreenButton.addEventListener("click", function () { if (!document.fullscreenElement && document.documentElement.requestFullscreen) { document.documentElement.requestFullscreen(); } else if (document.exitFullscreen) { document.exitFullscreen(); } }); } function loadProfile() { if (!getToken()) { redirectToLogin(); return; } apiRequest("/system/user/profile", { method: "GET" }).then(function (result) { currentProfile = result.data || {}; applyUserInfo(currentProfile); }).catch(function (error) { applyUserInfo(readLoginUser()); AppMessage.error("操作失败:" + (error.message || "用户信息加载失败")); }); } function openProfileModal() { var profile = currentProfile || readLoginUser(); fillProfileForm(profile); setModalMessage("profileMessage", "", ""); openModal("profile"); apiRequest("/system/user/profile", { method: "GET" }).then(function (result) { currentProfile = result.data || {}; applyUserInfo(currentProfile); fillProfileForm(currentProfile); }).catch(function (error) { setModalMessage("profileMessage", error.message || "账号信息加载失败", "error"); }); } function saveProfile() { var payload = { nickName: valueOf("profileNickName"), phonenumber: currentProfile && currentProfile.phonenumber ? currentProfile.phonenumber : "", email: currentProfile && currentProfile.email ? currentProfile.email : "", sex: currentProfile && currentProfile.sex != null ? String(currentProfile.sex) : "2" }; if (!payload.nickName) { setModalMessage("profileMessage", "请输入用户名称", "error"); return; } apiRequest("/system/user/profile", { method: "PUT", body: JSON.stringify(payload) }).then(function () { if (!selectedAvatarFile) { return null; } var formData = new FormData(); formData.append("avatarfile", selectedAvatarFile); return apiRequest("/system/user/profile/avatar", { method: "POST", body: formData, skipJson: true }); }).then(function (avatarResult) { currentProfile = Object.assign({}, currentProfile || {}, payload); if (avatarResult && avatarResult.imgUrl) { currentProfile.avatar = avatarResult.imgUrl; } selectedAvatarFile = null; document.getElementById("avatarFile").value = ""; applyUserInfo(currentProfile); setModalMessage("profileMessage", "保存成功", "success"); AppMessage.success("操作成功"); }).catch(function (error) { var message = error.message || "保存失败"; setModalMessage("profileMessage", message, "error"); AppMessage.error("操作失败:" + message); }); } function openPasswordModal() { document.getElementById("passwordForm").reset(); setModalMessage("passwordMessage", "", ""); openModal("password"); } function updatePassword() { var oldPassword = valueOf("oldPassword"); var newPassword = valueOf("newPassword"); var confirmPassword = valueOf("confirmPassword"); if (!oldPassword || !newPassword || !confirmPassword) { setModalMessage("passwordMessage", "请完整填写密码", "error"); return; } if (!isStrongPassword(newPassword)) { setModalMessage("passwordMessage", "新密码至少12位,需包含大小写字母和特殊字符", "error"); return; } if (newPassword !== confirmPassword) { setModalMessage("passwordMessage", "两次输入的新密码不一致", "error"); return; } var params = new URLSearchParams(); params.set("oldPassword", oldPassword); params.set("newPassword", newPassword); apiRequest("/system/user/profile/updatePwd?" + params.toString(), { method: "PUT", skipJson: true }).then(function () { setModalMessage("passwordMessage", "密码修改成功,请重新登录", "success"); AppMessage.success("操作成功"); window.setTimeout(function () { clearLoginState(); redirectToLogin(); }, 600); }).catch(function (error) { var message = error.message || "密码修改失败"; setModalMessage("passwordMessage", message, "error"); AppMessage.error("操作失败:" + message); }); } function logout() { apiRequest("/logout", { method: "POST", skipJson: true }).catch(function () { return null; }).then(function () { clearLoginState(); redirectToLogin(); }); } function apiRequest(path, options) { options = options || {}; var headers = options.headers || {}; var token = getToken(); if (token) { headers.Authorization = "Bearer " + token; } if (!options.skipJson) { headers["Content-Type"] = "application/json"; } return fetch(getApiBaseUrl() + normalizePath(path), { method: options.method || "GET", headers: headers, body: options.body }).then(function (response) { return response.json().catch(function () { return { code: response.ok ? 200 : response.status, msg: response.statusText }; }); }).then(function (result) { if (result.code === 401) { clearLoginState(); redirectToLogin(); throw new Error(result.msg || "登录状态已过期"); } if (result.code !== 200) { throw new Error(result.msg || "请求失败"); } return result; }); } function applyUserInfo(user) { var displayName = user.nickName || user.userName || user.username || "用户"; document.getElementById("userDisplayName").textContent = displayName; document.querySelector(".user-avatar").textContent = displayName.substring(0, 1); } function fillProfileForm(user) { selectedAvatarFile = null; document.getElementById("avatarFile").value = ""; document.getElementById("profileNickName").value = user.nickName || user.userName || user.username || ""; if (user.avatar) { showAvatarPreview(resolveResourceUrl(user.avatar)); } else { hideAvatarPreview(); } } function readLoginUser() { try { var loginResult = JSON.parse(localStorage.getItem("visitor_admin_login_result") || "{}"); return (loginResult.userInfo && loginResult.userInfo.user) || loginResult.user || {}; } catch (error) { return {}; } } function openModal(type) { document.getElementById(type + "Modal").hidden = false; } function closeModal(type) { document.getElementById(type + "Modal").hidden = true; } function setModalMessage(id, text, type) { var element = document.getElementById(id); element.textContent = text; element.classList.toggle("success", type === "success"); } function showAvatarPreview(src) { var preview = document.getElementById("avatarPreview"); document.getElementById("avatarPlus").hidden = true; preview.src = src; preview.hidden = false; } function hideAvatarPreview() { var preview = document.getElementById("avatarPreview"); preview.removeAttribute("src"); preview.hidden = true; document.getElementById("avatarPlus").hidden = false; } function resolveResourceUrl(path) { if (/^https?:\/\//i.test(path)) { return path; } return getApiBaseUrl() + normalizePath(path); } function isStrongPassword(password) { return password.length >= 12 && /[a-z]/.test(password) && /[A-Z]/.test(password) && /[^A-Za-z0-9]/.test(password); } function getApiBaseUrl() { return trimRightSlash(localStorage.getItem("visitor_admin_api_base") || config.apiBaseUrl || window.location.origin); } function getToken() { return localStorage.getItem("visitor_admin_token") || ""; } function clearLoginState() { localStorage.removeItem("visitor_admin_token"); localStorage.removeItem("visitor_admin_login_result"); localStorage.removeItem("visitor_admin_api_base"); } function redirectToLogin() { window.location.href = "../index.html"; } function valueOf(id) { return document.getElementById(id).value.trim(); } function normalizePath(path) { return "/" + String(path || "").replace(/^\/+/, ""); } function trimRightSlash(value) { return String(value || "").replace(/\/+$/, ""); } function getIcon(type) { var icons = { tree: '', user: '', device: '', book: '', map: '', account: '', config: '', update: '' }; return icons[type] || icons.config; } })();