登录界面

This commit is contained in:
Zzzz
2026-04-28 21:35:26 +08:00
parent acbd873fbc
commit 8535b4804b
6 changed files with 528 additions and 37 deletions
+97 -22
View File
@@ -6,44 +6,119 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录 - MZH 图书馆</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
<title>登录 - 图书管理系统</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-login-redesign">
</head>
<body class="auth-page">
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="auth-shell">
<section class="login-panel" aria-labelledby="login-title">
<div>
<p class="eyebrow">图书馆管理</p>
<h1 id="login-title">登录</h1>
<div class="login-card-head">
<div class="login-brand-row">
<span class="login-brand-mark" aria-hidden="true">
<svg viewBox="0 0 48 48" focusable="false">
<path d="M8 11.5c0-2 1.6-3.5 3.5-3.5H22c1.5 0 2.8.6 3.8 1.6V39c-1-.8-2.3-1.2-3.8-1.2H11.5A3.5 3.5 0 0 1 8 34.3V11.5Z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round"/>
<path d="M40 11.5c0-2-1.6-3.5-3.5-3.5H26c-1.5 0-2.8.6-3.8 1.6V39c1-.8 2.3-1.2 3.8-1.2h10.5a3.5 3.5 0 0 0 3.5-3.5V11.5Z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round"/>
<path d="M14 15.5h7M14 22h7M27 15.5h7M27 22h7" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
</svg>
</span>
<h1 id="login-title">图书管理系统</h1>
</div>
<p class="login-subtitle">欢迎登录图书管理平台</p>
</div>
<c:if test="${not empty errorMessage}">
<div class="message message-error" role="alert">
<div class="message message-error login-error" role="alert">
<c:out value="${errorMessage}" />
</div>
</c:if>
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate>
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate data-login-form>
<input type="hidden" name="redirect" value="${fn:escapeXml(redirect)}">
<label for="username">用户名</label>
<input id="username"
name="username"
type="text"
value="${fn:escapeXml(username)}"
autocomplete="username"
required>
<div class="login-field">
<label class="sr-only" for="username">用户名</label>
<div class="login-input-shell">
<span class="login-input-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path d="M20 21a8 8 0 0 0-16 0" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
<circle cx="12" cy="7.5" r="4" fill="none" stroke="currentColor" stroke-width="1.9"/>
</svg>
</span>
<input class="login-control"
id="username"
name="username"
type="text"
value="${fn:escapeXml(username)}"
autocomplete="username"
placeholder="用户名"
required>
</div>
</div>
<label for="password">密码</label>
<input id="password"
name="password"
type="password"
autocomplete="current-password"
required>
<div class="login-field">
<label class="sr-only" for="password">密码</label>
<div class="login-input-shell login-password-shell">
<span class="login-input-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<rect x="5" y="10" width="14" height="10" rx="2" fill="none" stroke="currentColor" stroke-width="1.9"/>
<path d="M8 10V7.5a4 4 0 0 1 8 0V10M12 14.5v2" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
</svg>
</span>
<input class="login-control"
id="password"
name="password"
type="password"
autocomplete="current-password"
placeholder="密码"
required>
<button class="password-toggle"
type="button"
aria-label="显示密码"
aria-controls="password"
aria-pressed="false"
data-password-toggle>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M2.8 12s3.3-5.5 9.2-5.5 9.2 5.5 9.2 5.5-3.3 5.5-9.2 5.5S2.8 12 2.8 12Z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="2.8" fill="none" stroke="currentColor" stroke-width="1.9"/>
</svg>
</button>
</div>
</div>
<button class="button button-primary" type="submit">登录</button>
<div class="login-role-group" role="radiogroup" aria-labelledby="login-role-label">
<span class="login-role-title" id="login-role-label">登录身份</span>
<div class="login-role-options">
<label>
<input type="radio" name="loginRole" value="administrator" checked>
<span>管理员</span>
</label>
<label>
<input type="radio" name="loginRole" value="librarian">
<span>馆员</span>
</label>
<label>
<input type="radio" name="loginRole" value="reader">
<span>读者</span>
</label>
</div>
</div>
<div class="login-options-row">
<label class="login-check">
<input type="checkbox" name="rememberUsername" value="true" data-remember-username>
<span>记住我</span>
</label>
<button class="forgot-password-link" type="button" data-forgot-password>
忘记密码?
</button>
</div>
<p class="login-help-message" id="password-help" tabindex="-1" hidden>
请联系系统管理员重置密码。
</p>
<button class="button button-primary login-submit" type="submit">登录</button>
</form>
</section>
</main>
<script src="${pageContext.request.contextPath}/static/js/login.js?v=20260428-login-redesign"></script>
</body>
</html>
+331 -9
View File
@@ -302,16 +302,40 @@ textarea {
}
.auth-page {
position: relative;
min-height: 100vh;
overflow-x: hidden;
isolation: isolate;
background: #edf4ff;
}
.auth-page::before {
content: "";
position: fixed;
inset: -22px;
z-index: -2;
background:
linear-gradient(rgba(245, 247, 251, 0.86), rgba(245, 247, 251, 0.94)),
linear-gradient(90deg, rgba(241, 246, 255, 0.76), rgba(249, 252, 255, 0.64)),
url("../images/library-login.svg") center / cover no-repeat;
filter: blur(10px) saturate(0.78);
transform: scale(1.04);
}
.auth-page::after {
content: "";
position: fixed;
inset: 0;
z-index: -1;
background:
radial-gradient(circle at 50% 42%, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.18) 34%, rgba(227, 237, 255, 0.5) 100%),
linear-gradient(180deg, rgba(247, 250, 255, 0.68), rgba(231, 239, 255, 0.78));
}
.auth-shell {
width: min(1120px, calc(100% - 32px));
min-height: calc(100vh - 64px);
width: min(960px, calc(100% - 32px));
min-height: 100vh;
display: grid;
align-items: center;
place-items: center;
margin: 0 auto;
padding: 48px 0;
}
@@ -357,8 +381,64 @@ body:not(.auth-page) .dashboard-shell {
}
.login-panel {
width: min(420px, 100%);
padding: 32px;
width: min(540px, 100%);
padding: 44px 64px 56px;
}
.auth-page .login-panel {
border-color: rgba(219, 229, 244, 0.78);
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 18px 42px rgba(51, 65, 85, 0.16);
}
.login-card-head {
display: grid;
gap: 12px;
justify-items: center;
margin-bottom: 34px;
text-align: center;
}
.login-brand-row {
display: flex;
align-items: center;
justify-content: center;
gap: 22px;
}
.login-brand-mark {
width: 58px;
height: 58px;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
color: #1372e8;
}
.login-brand-mark svg {
width: 100%;
height: 100%;
}
.login-card-head h1 {
margin: 0;
color: #0f2546;
font-size: 34px;
font-weight: 900;
line-height: 1.14;
}
.login-subtitle {
margin: 0;
color: #6f7b8a;
font-size: 18px;
line-height: 1.4;
}
.login-error {
margin: 0 0 20px;
}
.eyebrow {
@@ -389,7 +469,7 @@ h2 {
.login-form {
display: grid;
gap: 10px;
gap: 20px;
}
.login-form label,
@@ -401,7 +481,7 @@ h2 {
font-weight: 800;
}
.login-form input,
.login-form .login-control,
.search-form input,
.search-form select,
.dashboard-search-form input,
@@ -425,7 +505,7 @@ h2 {
outline: 0;
}
.login-form input:focus,
.login-form .login-control:focus,
.search-form input:focus,
.search-form select:focus,
.dashboard-search-form input:focus,
@@ -444,6 +524,169 @@ h2 {
box-shadow: 0 0 0 3px rgba(40, 105, 232, 0.14);
}
.login-field {
display: grid;
gap: 6px;
}
.login-input-shell {
min-height: 58px;
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
align-items: center;
gap: 12px;
padding: 0 18px;
border: 1px solid rgba(203, 213, 225, 0.9);
border-radius: 8px;
background: rgba(255, 255, 255, 0.96);
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.login-password-shell {
grid-template-columns: 34px minmax(0, 1fr) 42px;
}
.login-input-shell:focus-within {
border-color: #2d7df0;
box-shadow: 0 0 0 3px rgba(45, 125, 240, 0.13);
}
.login-input-icon {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
color: #6b7280;
}
.login-input-icon svg,
.password-toggle svg {
width: 24px;
height: 24px;
}
.login-input-shell .login-control {
width: 100%;
min-width: 0;
min-height: 56px;
padding: 0;
border: 0;
color: #111827;
background: transparent;
box-shadow: none;
}
.login-input-shell .login-control:focus {
box-shadow: none;
}
.login-input-shell .login-control::placeholder {
color: #7b8494;
}
.password-toggle {
width: 38px;
height: 38px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
border-radius: 8px;
color: #747b89;
background: transparent;
cursor: pointer;
}
.password-toggle:hover,
.password-toggle:focus-visible {
color: #1d4fd7;
background: #eef5ff;
outline: 0;
}
.login-role-group {
min-width: 0;
display: flex;
align-items: center;
gap: 26px;
margin: 4px 0 0;
padding: 0;
border: 0;
}
.login-role-title {
flex: 0 0 auto;
padding: 0;
color: #1f2937;
font-size: 16px;
font-weight: 800;
}
.login-role-options {
min-width: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 26px;
flex: 1 1 auto;
}
.login-role-options label,
.login-check {
display: inline-flex;
align-items: center;
gap: 8px;
color: #4b5563;
font-size: 16px;
line-height: 1.2;
white-space: nowrap;
cursor: pointer;
}
.login-role-options input,
.login-check input {
width: 18px;
height: 18px;
flex: 0 0 auto;
margin: 0;
accent-color: #1478ef;
}
.login-options-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 2px;
}
.forgot-password-link {
border: 0;
padding: 0;
color: #1478c8;
background: transparent;
font-size: 16px;
cursor: pointer;
}
.forgot-password-link:hover,
.forgot-password-link:focus-visible {
color: #0f5da8;
text-decoration: underline;
outline: 0;
}
.login-help-message {
margin: -10px 0 0;
padding: 9px 12px;
border: 1px solid rgba(20, 120, 200, 0.16);
border-radius: 8px;
color: #31536f;
background: #f3f8ff;
font-size: 13px;
}
.button {
min-height: 36px;
display: inline-flex;
@@ -498,6 +741,15 @@ h2 {
margin-top: 12px;
}
.login-form .login-submit {
width: 100%;
min-height: 58px;
margin-top: 4px;
border-radius: 8px;
font-size: 21px;
box-shadow: 0 10px 22px rgba(20, 104, 234, 0.26);
}
.message {
margin-bottom: 16px;
padding: 10px 12px;
@@ -1118,6 +1370,76 @@ h2 {
padding: 0 16px;
}
.auth-shell {
width: min(100%, calc(100% - 24px));
padding: 24px 0;
}
.auth-page .login-panel {
padding: 30px 22px 34px;
}
.login-card-head {
gap: 8px;
margin-bottom: 26px;
}
.login-brand-row {
gap: 12px;
}
.login-brand-mark {
width: 42px;
height: 42px;
}
.login-card-head h1 {
font-size: 26px;
}
.login-subtitle {
font-size: 15px;
}
.login-input-shell {
min-height: 52px;
grid-template-columns: 28px minmax(0, 1fr);
gap: 10px;
padding: 0 14px;
}
.login-password-shell {
grid-template-columns: 28px minmax(0, 1fr) 38px;
}
.login-input-shell .login-control {
min-height: 50px;
}
.login-role-group {
align-items: flex-start;
flex-direction: column;
gap: 12px;
}
.login-role-options {
width: 100%;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
}
.login-role-options label,
.login-check,
.forgot-password-link {
font-size: 14px;
}
.login-form .login-submit {
min-height: 52px;
font-size: 18px;
}
h1 {
font-size: 24px;
}
+65
View File
@@ -0,0 +1,65 @@
(function () {
var form = document.querySelector("[data-login-form]");
var username = document.getElementById("username");
var password = document.getElementById("password");
var remember = document.querySelector("[data-remember-username]");
var toggle = document.querySelector("[data-password-toggle]");
var forgot = document.querySelector("[data-forgot-password]");
var passwordHelp = document.getElementById("password-help");
var storageKey = "mzh.library.login.username";
function readStoredUsername() {
try {
return window.localStorage.getItem(storageKey) || "";
} catch (ex) {
return "";
}
}
function writeStoredUsername(value) {
try {
if (value) {
window.localStorage.setItem(storageKey, value);
} else {
window.localStorage.removeItem(storageKey);
}
} catch (ex) {
// Storage may be disabled; login should still submit normally.
}
}
if (username && remember) {
var storedUsername = readStoredUsername();
if (storedUsername) {
remember.checked = true;
if (!username.value) {
username.value = storedUsername;
}
}
}
if (form && username && remember) {
form.addEventListener("submit", function () {
writeStoredUsername(remember.checked ? username.value.trim() : "");
});
}
if (toggle && password) {
toggle.addEventListener("click", function () {
var nextVisible = password.type !== "text";
password.type = nextVisible ? "text" : "password";
toggle.setAttribute("aria-pressed", String(nextVisible));
toggle.setAttribute("aria-label", nextVisible ? "隐藏密码" : "显示密码");
password.focus();
});
}
if (forgot && passwordHelp) {
forgot.addEventListener("click", function () {
passwordHelp.hidden = !passwordHelp.hidden;
if (!passwordHelp.hidden) {
passwordHelp.focus();
}
});
}
}());