(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 [
|
'<article class="summary-card">',
|
' <div class="card-title" style="color:' + card.color + '">',
|
' <span class="title-mark"></span>',
|
' <span class="card-icon"></span>',
|
' <span>' + card.title + '</span>',
|
' </div>',
|
' <div class="metric-list">',
|
card.metrics.map(function (metric) {
|
return [
|
' <div class="metric">',
|
metric.label ? ' <div class="metric-label">' + metric.label + '</div>' : ' <div class="metric-label"> </div>',
|
' <div class="metric-value">' + metric.value + (metric.unit ? '<span class="metric-unit">' + metric.unit + '</span>' : "") + '</div>',
|
' </div>'
|
].join("");
|
}).join(""),
|
' </div>',
|
' <div class="card-foot">' + card.foot + '</div>',
|
'</article>'
|
].join("");
|
}).join("");
|
}
|
|
function renderQuickEntries() {
|
var root = document.getElementById("quickEntries");
|
root.innerHTML = quickEntries.map(function (item) {
|
return [
|
'<a class="quick-entry" href="' + (item.href || "#") + '">',
|
' <span class="quick-icon" style="--entry-color:' + item.color + '">' + getIcon(item.icon) + '</span>',
|
' <span>' + item.title + '</span>',
|
'</a>'
|
].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: '<svg viewBox="0 0 24 24"><path d="M12 4v16"></path><path d="M12 7H7v5"></path><path d="M12 12h5v5"></path><rect x="4" y="12" width="6" height="6" rx="1"></rect><rect x="14" y="17" width="6" height="4" rx="1"></rect></svg>',
|
user: '<svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"></circle><path d="M5 21a7 7 0 0 1 14 0"></path></svg>',
|
device: '<svg viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="11" rx="2"></rect><path d="M8 21h8"></path><path d="M12 16v5"></path></svg>',
|
book: '<svg viewBox="0 0 24 24"><path d="M6 4h11a2 2 0 0 1 2 2v14H8a2 2 0 0 1-2-2z"></path><path d="M8 4v14"></path></svg>',
|
map: '<svg viewBox="0 0 24 24"><path d="M4 6l5-2 6 2 5-2v14l-5 2-6-2-5 2z"></path><path d="M9 4v14"></path><path d="M15 6v14"></path></svg>',
|
account: '<svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="3"></circle><path d="M7 20v-2a5 5 0 0 1 10 0v2"></path></svg>',
|
config: '<svg viewBox="0 0 24 24"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"></path><path d="M19 12h2"></path><path d="M3 12h2"></path><path d="M12 3v2"></path><path d="M12 19v2"></path></svg>',
|
update: '<svg viewBox="0 0 24 24"><path d="M4 12a8 8 0 0 1 13-6"></path><path d="M17 4v5h-5"></path><path d="M20 12a8 8 0 0 1-13 6"></path><path d="M7 20v-5h5"></path></svg>'
|
};
|
return icons[type] || icons.config;
|
}
|
})();
|