登录界面
This commit is contained in:
@@ -33,7 +33,12 @@ rendering.
|
||||
### 2. Signatures
|
||||
|
||||
- Login form: `POST /login`.
|
||||
- Request fields: `username`, `password`, and optional `redirect`.
|
||||
- Request fields consumed by `LoginServlet`: `username`, `password`, and
|
||||
optional `redirect`.
|
||||
- Presentation-only login controls may submit auxiliary fields such as
|
||||
`loginRole` and `rememberUsername`; these must not participate in
|
||||
authentication or authorization unless the Servlet/service contract is
|
||||
deliberately changed.
|
||||
- Login JSP request attributes: `errorMessage`, `username`, and `redirect`.
|
||||
- Dashboard/role JSP session attributes: `authenticatedUser`, `userRole`, and
|
||||
`userPermissions`.
|
||||
@@ -47,6 +52,12 @@ rendering.
|
||||
attribute or session attribute.
|
||||
- `redirect` must be a same-application path beginning with one `/`; invalid
|
||||
values are ignored.
|
||||
- `loginRole` is only a login-intent hint in the JSP. The authenticated role is
|
||||
determined by the `users.role_code` row returned through `AuthService`, not by
|
||||
a client-side radio selection.
|
||||
- Remember-me behavior may persist only the username in browser storage. It must
|
||||
never persist passwords, password hashes, redirects, permission state, or
|
||||
extend the server session.
|
||||
- JSPs render data with JSP EL/JSTL, not scriptlet Java.
|
||||
- JSPs may read safe session snapshots, but they must not call DAOs or inspect
|
||||
password hashes.
|
||||
@@ -67,10 +78,12 @@ rendering.
|
||||
|
||||
- Good: failed login keeps the escaped username and never redisplays the
|
||||
password.
|
||||
- Good: selecting a role radio option or checking remember-me does not change
|
||||
the server-side authentication decision.
|
||||
- Base: dashboard reads `sessionScope.authenticatedUser.displayName` and
|
||||
`sessionScope.userRole` only for display/navigation.
|
||||
- Bad: JSP uses scriptlets, JDBC, or raw request parameters to decide
|
||||
authentication.
|
||||
- Bad: JSP, JavaScript, or Servlet code trusts `loginRole` to grant a role or
|
||||
stores the password in browser storage.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
@@ -79,6 +92,8 @@ rendering.
|
||||
files.
|
||||
- Run service-level auth checks for required fields, invalid credentials,
|
||||
success, DAO fallback, and permission checks.
|
||||
- When login page scripts change, scan them to confirm only usernames can be
|
||||
stored client-side and `password` is never persisted.
|
||||
- When Maven/Tomcat is available, run a Servlet/JSP compile or package check.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
@@ -87,6 +102,7 @@ rendering.
|
||||
|
||||
```jsp
|
||||
<%-- JSP checks request.getParameter("password") or runs SQL directly. --%>
|
||||
<%-- JavaScript stores the password or LoginServlet trusts loginRole. --%>
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Run UI-oriented quality review for removed redundant actions."}
|
||||
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Review Chinese demo data against schema and seed-data conventions."}
|
||||
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Verify backend layer boundaries and checks for schema-only data changes."}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Verify the login JSP keeps the POST /login contract, request fields, and safe rendering behavior."}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Goal
|
||||
|
||||
精简已登录页面中与侧边栏重复的右侧跨模块跳转按钮,并补充更贴近中文图书馆场景的演示图书与读者数据。
|
||||
精简已登录页面中与侧边栏重复的右侧跨模块跳转按钮,补充更贴近中文图书馆场景的演示图书与读者数据,并按参考截图重构真实可用的登录界面。
|
||||
|
||||
## What I already know
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
* “新增图书”“新增分类”“新增读者档案”“新增账户”等当前页面内的主要操作仍应保留。
|
||||
* 演示数据位于 `src/main/resources/db/schema.sql`,当前包含英文读者名、英文分类和英文图书。
|
||||
* 项目是 JSP + Servlet + MySQL 架构,前端页面在 `src/main/webapp/WEB-INF/jsp/`,数据库初始化脚本使用 `utf8mb4`。
|
||||
* 用户补充要求:仿照参考截图重构登录界面,必须是真实可用的登录表单,而不是静态展示页。
|
||||
* 参考截图特征:浅色模糊图书馆背景、居中的白色登录卡片、蓝色书本图标与“图书管理系统”标题、用户名/密码输入框图标、密码显隐按钮、身份单选项、记住我和忘记密码入口、蓝色主登录按钮。
|
||||
|
||||
## Assumptions
|
||||
|
||||
@@ -28,6 +30,10 @@
|
||||
* 用户账户与角色页面不再显示跳转到读者档案的右侧按钮;保留新增账户入口。
|
||||
* 数据库初始化脚本加入中文图书分类、中文书名、中文作者和中文读者姓名。
|
||||
* 本地演示账号仍能用于登录验证。
|
||||
* 登录页按参考截图重构视觉,但保留现有 `POST /login`、`username`、`password`、`redirect`、错误提示和回填用户名等真实登录能力。
|
||||
* 登录页新增或保留真实可交互控件:密码显隐切换、登录身份单选项、记住我选项和忘记密码入口。
|
||||
* 登录身份选择不应破坏现有服务端认证;当前后端仍以账号密码和账号角色为准,前端角色选项仅作为登录意图提示或表单辅助字段。
|
||||
* 登录页需要在桌面和移动端保持可用,输入框、按钮和错误提示不能溢出或遮挡。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
@@ -36,6 +42,10 @@
|
||||
* [x] `schema.sql` 包含多条中文图书数据和多条中文读者数据。
|
||||
* [x] 中文演示数据使用 `utf8mb4` 兼容的文本,不引入新表或迁移机制。
|
||||
* [x] 相关检查或可用的构建验证通过;若环境缺少 Maven,记录 fallback 验证。
|
||||
* [x] 登录页视觉接近参考截图,并使用真实表单提交到现有 `/login`。
|
||||
* [x] 密码显隐、记住我、身份单选项在浏览器中可交互且不破坏登录流程。
|
||||
* [x] 登录失败时继续显示服务端错误提示并保留用户名/redirect。
|
||||
* [x] 登录页在移动端和桌面端布局稳定,无文字或控件重叠。
|
||||
|
||||
## Definition of Done
|
||||
|
||||
@@ -48,11 +58,13 @@
|
||||
* 不重设计侧边栏或整体视觉风格。
|
||||
* 不新增页面、权限、路由或服务层能力。
|
||||
* 不改变借阅记录、报表、用户账户或读者档案的业务逻辑。
|
||||
* 不实现真实找回密码流程;忘记密码入口可展示当前系统暂未开放或指向安全的占位交互。
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* Likely JSP files: `src/main/webapp/WEB-INF/jsp/reports/dashboard.jsp`, `src/main/webapp/WEB-INF/jsp/books/catalog.jsp`, `src/main/webapp/WEB-INF/jsp/books/manage.jsp`, `src/main/webapp/WEB-INF/jsp/books/categories.jsp`, `src/main/webapp/WEB-INF/jsp/readers/manage.jsp`, `src/main/webapp/WEB-INF/jsp/admin/users/manage.jsp`.
|
||||
* Login files: `src/main/webapp/WEB-INF/jsp/auth/login.jsp`, `src/main/webapp/static/css/app.css`, and possibly small inline or static JavaScript for password visibility/remember-me interactions.
|
||||
* Data file: `src/main/resources/db/schema.sql`.
|
||||
* Relevant specs: frontend JSP/component/state/quality guidelines and backend database/quality guidelines.
|
||||
* Final verification: `git diff --check`, JSP scriptlet/SQL/JDBC scan, removed-link scan, and `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` passed.
|
||||
* Spec update decision: no `.trellis/spec/` update needed because this task did not introduce new routes, APIs, tables, cross-layer contracts, or reusable implementation conventions.
|
||||
* Final verification: `git diff --check`, `node --check src/main/webapp/static/js/login.js`, JSP scriptlet/SQL/JDBC scans, removed-link scan, password persistence scan, and `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` passed.
|
||||
* Spec update decision: `.trellis/spec/frontend/type-safety.md` documents the new presentation-only login controls (`loginRole`, `rememberUsername`) and the username-only remember-me constraint.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}());
|
||||
Reference in New Issue
Block a user