Compare commits

..

4 Commits

Author SHA1 Message Date
Zzzz a37d37945b 前端 2026-04-28 22:08:36 +08:00
Zzzz d1f32b9d52 chore: record journal 2026-04-28 21:38:08 +08:00
Zzzz 44b72d3959 chore(task): archive 04-28-remove-redundant-actions-add-cn-data 2026-04-28 21:38:04 +08:00
Zzzz 8535b4804b 登录界面 2026-04-28 21:35:26 +08:00
14 changed files with 556 additions and 43 deletions
+18 -3
View File
@@ -33,7 +33,11 @@ rendering.
### 2. Signatures ### 2. Signatures
- Login form: `POST /login`. - 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
`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`. - Login JSP request attributes: `errorMessage`, `username`, and `redirect`.
- Dashboard/role JSP session attributes: `authenticatedUser`, `userRole`, and - Dashboard/role JSP session attributes: `authenticatedUser`, `userRole`, and
`userPermissions`. `userPermissions`.
@@ -47,6 +51,12 @@ rendering.
attribute or session attribute. attribute or session attribute.
- `redirect` must be a same-application path beginning with one `/`; invalid - `redirect` must be a same-application path beginning with one `/`; invalid
values are ignored. values are ignored.
- Login pages must not include a client-side role selector. The authenticated
role is determined by the `users.role_code` row returned through
`AuthService`, not by client-submitted form state.
- 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 render data with JSP EL/JSTL, not scriptlet Java.
- JSPs may read safe session snapshots, but they must not call DAOs or inspect - JSPs may read safe session snapshots, but they must not call DAOs or inspect
password hashes. password hashes.
@@ -67,10 +77,12 @@ rendering.
- Good: failed login keeps the escaped username and never redisplays the - Good: failed login keeps the escaped username and never redisplays the
password. password.
- Good: checking remember-me does not change the server-side authentication
decision.
- Base: dashboard reads `sessionScope.authenticatedUser.displayName` and - Base: dashboard reads `sessionScope.authenticatedUser.displayName` and
`sessionScope.userRole` only for display/navigation. `sessionScope.userRole` only for display/navigation.
- Bad: JSP uses scriptlets, JDBC, or raw request parameters to decide - Bad: JSP, JavaScript, or Servlet code trusts a client-submitted role field to
authentication. grant a role or stores the password in browser storage.
### 6. Tests Required ### 6. Tests Required
@@ -79,6 +91,8 @@ rendering.
files. files.
- Run service-level auth checks for required fields, invalid credentials, - Run service-level auth checks for required fields, invalid credentials,
success, DAO fallback, and permission checks. 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. - When Maven/Tomcat is available, run a Servlet/JSP compile or package check.
### 7. Wrong vs Correct ### 7. Wrong vs Correct
@@ -87,6 +101,7 @@ rendering.
```jsp ```jsp
<%-- JSP checks request.getParameter("password") or runs SQL directly. --%> <%-- JSP checks request.getParameter("password") or runs SQL directly. --%>
<%-- JavaScript stores the password or LoginServlet trusts a submitted role. --%>
``` ```
#### Correct #### Correct
@@ -0,0 +1,4 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend checklist for reviewing login page UI changes"}
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Verify login form contract remains unchanged"}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Verify UI layout quality after removal"}
@@ -0,0 +1,7 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS guidelines for login page UI changes"}
{"file": ".trellis/spec/frontend/directory-structure.md", "reason": "JSP and static asset placement conventions"}
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Form and page component conventions"}
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered form state conventions"}
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Login form request contract and loginRole behavior"}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "UI quality checks for JSP/CSS changes"}
@@ -0,0 +1,52 @@
# 调整登录页登录选项与布局
## Goal
简化登录界面:移除登录身份单选项和标题旁的图书图标,并微调表单布局,让登录卡片在元素减少后仍保持居中、紧凑和视觉平衡。
## What I Already Know
* 用户要求删除登录界面中的“登录身份”选项。
* 用户要求删除登录界面中的图书图标。
* 登录页 JSP 位于 `src/main/webapp/WEB-INF/jsp/auth/login.jsp`
* 登录页样式位于 `src/main/webapp/static/css/app.css`
* 登录页脚本位于 `src/main/webapp/static/js/login.js`,当前主要处理记住用户名、密码显示切换和忘记密码提示。
* 前端规范说明登录页不应包含客户端角色选择,认证后的角色由 `AuthService` 返回的用户角色决定。
## Assumptions
* “图书的图标”指登录页标题旁内联 SVG 的 `login-brand-mark`,不是背景插画 `static/images/library-login.svg`
* “微调布局”指因移除图标和登录身份单选后,调整标题区域、表单间距和卡片留白,不做整页视觉重设计。
## Requirements
* 移除登录页的登录身份单选区域,包括“登录身份”“管理员”“馆员”“读者”选项。
* 移除登录页标题旁的图书图标。
* 保留用户名、密码、记住我、忘记密码提示和登录提交功能。
* 表单提交仍只依赖后端已消费的 `username``password`、可选 `redirect`,不改变认证/授权逻辑。
* 调整登录页布局,使标题、副标题、输入框、选项行和按钮在桌面与移动端都保持合理间距。
## Acceptance Criteria
* [x] 登录页不再渲染“登录身份”文案和角色单选按钮。
* [x] 登录页标题旁不再渲染图书 SVG 图标。
* [x] 登录页在桌面和移动端没有明显空洞、错位或文本重叠。
* [x] 用户名/密码登录表单仍可提交到 `POST /login`
* [x] 项目可通过 Maven 构建或等价检查。
## Definition of Done
* JSP/CSS 改动范围聚焦在登录页 UI。
* Lint/typecheck/build 可用检查已运行;如无法运行,记录原因。
* 不修改后端认证授权逻辑。
## Out of Scope
* 不重做整套登录页视觉风格。
* 不修改用户角色、权限、认证服务或数据库。
* 不删除登录页背景插画,除非代码检查证明它就是用户所指图标。
## Technical Notes
* 前端规范入口: `.trellis/spec/frontend/index.md`
* 相关规范: `.trellis/spec/frontend/type-safety.md` 中说明 `LoginServlet` 消费 `username``password` 和可选 `redirect`,登录角色不由客户端表单状态决定。
@@ -0,0 +1,26 @@
{
"id": "login-page-simplify-layout",
"name": "login-page-simplify-layout",
"title": "调整登录页登录选项与布局",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "Zzzz",
"assignee": "Zzzz",
"createdAt": "2026-04-28",
"completedAt": null,
"branch": null,
"base_branch": "master",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -4,3 +4,4 @@
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Run UI-oriented quality review for removed redundant actions."} {"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/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/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 ## Goal
精简已登录页面中与侧边栏重复的右侧跨模块跳转按钮,补充更贴近中文图书馆场景的演示图书与读者数据。 精简已登录页面中与侧边栏重复的右侧跨模块跳转按钮,补充更贴近中文图书馆场景的演示图书与读者数据,并按参考截图重构真实可用的登录界面
## What I already know ## What I already know
@@ -11,6 +11,8 @@
* “新增图书”“新增分类”“新增读者档案”“新增账户”等当前页面内的主要操作仍应保留。 * “新增图书”“新增分类”“新增读者档案”“新增账户”等当前页面内的主要操作仍应保留。
* 演示数据位于 `src/main/resources/db/schema.sql`,当前包含英文读者名、英文分类和英文图书。 * 演示数据位于 `src/main/resources/db/schema.sql`,当前包含英文读者名、英文分类和英文图书。
* 项目是 JSP + Servlet + MySQL 架构,前端页面在 `src/main/webapp/WEB-INF/jsp/`,数据库初始化脚本使用 `utf8mb4` * 项目是 JSP + Servlet + MySQL 架构,前端页面在 `src/main/webapp/WEB-INF/jsp/`,数据库初始化脚本使用 `utf8mb4`
* 用户补充要求:仿照参考截图重构登录界面,必须是真实可用的登录表单,而不是静态展示页。
* 参考截图特征:浅色模糊图书馆背景、居中的白色登录卡片、蓝色书本图标与“图书管理系统”标题、用户名/密码输入框图标、密码显隐按钮、身份单选项、记住我和忘记密码入口、蓝色主登录按钮。
## Assumptions ## Assumptions
@@ -28,6 +30,10 @@
* 用户账户与角色页面不再显示跳转到读者档案的右侧按钮;保留新增账户入口。 * 用户账户与角色页面不再显示跳转到读者档案的右侧按钮;保留新增账户入口。
* 数据库初始化脚本加入中文图书分类、中文书名、中文作者和中文读者姓名。 * 数据库初始化脚本加入中文图书分类、中文书名、中文作者和中文读者姓名。
* 本地演示账号仍能用于登录验证。 * 本地演示账号仍能用于登录验证。
* 登录页按参考截图重构视觉,但保留现有 `POST /login``username``password``redirect`、错误提示和回填用户名等真实登录能力。
* 登录页新增或保留真实可交互控件:密码显隐切换、登录身份单选项、记住我选项和忘记密码入口。
* 登录身份选择不应破坏现有服务端认证;当前后端仍以账号密码和账号角色为准,前端角色选项仅作为登录意图提示或表单辅助字段。
* 登录页需要在桌面和移动端保持可用,输入框、按钮和错误提示不能溢出或遮挡。
## Acceptance Criteria ## Acceptance Criteria
@@ -36,6 +42,10 @@
* [x] `schema.sql` 包含多条中文图书数据和多条中文读者数据。 * [x] `schema.sql` 包含多条中文图书数据和多条中文读者数据。
* [x] 中文演示数据使用 `utf8mb4` 兼容的文本,不引入新表或迁移机制。 * [x] 中文演示数据使用 `utf8mb4` 兼容的文本,不引入新表或迁移机制。
* [x] 相关检查或可用的构建验证通过;若环境缺少 Maven,记录 fallback 验证。 * [x] 相关检查或可用的构建验证通过;若环境缺少 Maven,记录 fallback 验证。
* [x] 登录页视觉接近参考截图,并使用真实表单提交到现有 `/login`
* [x] 密码显隐、记住我、身份单选项在浏览器中可交互且不破坏登录流程。
* [x] 登录失败时继续显示服务端错误提示并保留用户名/redirect。
* [x] 登录页在移动端和桌面端布局稳定,无文字或控件重叠。
## Definition of Done ## Definition of Done
@@ -48,11 +58,13 @@
* 不重设计侧边栏或整体视觉风格。 * 不重设计侧边栏或整体视觉风格。
* 不新增页面、权限、路由或服务层能力。 * 不新增页面、权限、路由或服务层能力。
* 不改变借阅记录、报表、用户账户或读者档案的业务逻辑。 * 不改变借阅记录、报表、用户账户或读者档案的业务逻辑。
* 不实现真实找回密码流程;忘记密码入口可展示当前系统暂未开放或指向安全的占位交互。
## Technical Notes ## 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`. * 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`. * Data file: `src/main/resources/db/schema.sql`.
* Relevant specs: frontend JSP/component/state/quality guidelines and backend database/quality guidelines. * 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. * 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: no `.trellis/spec/` update needed because this task did not introduce new routes, APIs, tables, cross-layer contracts, or reusable implementation conventions. * 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.
@@ -3,7 +3,7 @@
"name": "remove-redundant-actions-add-cn-data", "name": "remove-redundant-actions-add-cn-data",
"title": "remove redundant page actions and add Chinese demo data", "title": "remove redundant page actions and add Chinese demo data",
"description": "", "description": "",
"status": "in_progress", "status": "completed",
"dev_type": null, "dev_type": null,
"scope": null, "scope": null,
"package": null, "package": null,
@@ -11,7 +11,7 @@
"creator": "Zzzz", "creator": "Zzzz",
"assignee": "Zzzz", "assignee": "Zzzz",
"createdAt": "2026-04-28", "createdAt": "2026-04-28",
"completedAt": null, "completedAt": "2026-04-28",
"branch": null, "branch": null,
"base_branch": "master", "base_branch": "master",
"worktree_path": null, "worktree_path": null,
+3 -2
View File
@@ -8,7 +8,7 @@
<!-- @@@auto:current-status --> <!-- @@@auto:current-status -->
- **Active File**: `journal-1.md` - **Active File**: `journal-1.md`
- **Total Sessions**: 14 - **Total Sessions**: 15
- **Last Active**: 2026-04-28 - **Last Active**: 2026-04-28
<!-- @@@/auto:current-status --> <!-- @@@/auto:current-status -->
@@ -19,7 +19,7 @@
<!-- @@@auto:active-documents --> <!-- @@@auto:active-documents -->
| File | Lines | Status | | File | Lines | Status |
|------|-------|--------| |------|-------|--------|
| `journal-1.md` | ~540 | Active | | `journal-1.md` | ~573 | Active |
<!-- @@@/auto:active-documents --> <!-- @@@/auto:active-documents -->
--- ---
@@ -29,6 +29,7 @@
<!-- @@@auto:session-history --> <!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch | | # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------| |---|------|-------|---------|--------|
| 15 | 2026-04-28 | 登录界面重构 | `8535b4804bc48e6f23d3107f1b34e0a16479e020` | `master` |
| 14 | 2026-04-28 | Sidebar layout and management UX cleanup | `d0e71f2` | `master` | | 14 | 2026-04-28 | Sidebar layout and management UX cleanup | `d0e71f2` | `master` |
| 13 | 2026-04-28 | Frontend workbench display fix | `0a386b8` | `master` | | 13 | 2026-04-28 | Frontend workbench display fix | `0a386b8` | `master` |
| 12 | 2026-04-28 | Windows login diagnostics and demo credentials | `781ce46` | `master` | | 12 | 2026-04-28 | Windows login diagnostics and demo credentials | `781ce46` | `master` |
+33
View File
@@ -538,3 +538,36 @@ Fixed sidebar active-state routing and navigation order, corrected management pa
### Next Steps ### Next Steps
- None - task complete - None - task complete
## Session 15: 登录界面重构
**Date**: 2026-04-28
**Task**: 登录界面重构
**Branch**: `master`
### Summary
按参考截图重构真实可用登录页,保留 /login 认证流程,补充登录辅助控件规范并完成 Trellis 质量检查。
### Main Changes
(Add details)
### Git Commits
| Hash | Message |
|------|---------|
| `8535b4804bc48e6f23d3107f1b34e0a16479e020` | (see git log) |
### Testing
- [OK] (Add test results)
### Status
[OK] **Completed**
### Next Steps
- None - task complete
+70 -22
View File
@@ -6,44 +6,92 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录 - MZH 图书馆</title> <title>登录 - 图书管理系统</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell"> <link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-login-redesign">
</head> </head>
<body class="auth-page"> <body class="auth-page">
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="auth-shell"> <main class="auth-shell">
<section class="login-panel" aria-labelledby="login-title"> <section class="login-panel" aria-labelledby="login-title">
<div> <div class="login-card-head">
<p class="eyebrow">图书管理</p> <h1 id="login-title">图书管理系统</h1>
<h1 id="login-title">登录</h1> <p class="login-subtitle">欢迎登录图书管理平台</p>
</div> </div>
<c:if test="${not empty errorMessage}"> <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}" /> <c:out value="${errorMessage}" />
</div> </div>
</c:if> </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)}"> <input type="hidden" name="redirect" value="${fn:escapeXml(redirect)}">
<label for="username">用户名</label> <div class="login-field">
<input id="username" <label class="sr-only" for="username">用户名</label>
name="username" <div class="login-input-shell">
type="text" <span class="login-input-icon" aria-hidden="true">
value="${fn:escapeXml(username)}" <svg viewBox="0 0 24 24" focusable="false">
autocomplete="username" <path d="M20 21a8 8 0 0 0-16 0" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
required> <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> <div class="login-field">
<input id="password" <label class="sr-only" for="password">密码</label>
name="password" <div class="login-input-shell login-password-shell">
type="password" <span class="login-input-icon" aria-hidden="true">
autocomplete="current-password" <svg viewBox="0 0 24 24" focusable="false">
required> <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-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> </form>
</section> </section>
</main> </main>
<script src="${pageContext.request.contextPath}/static/js/login.js?v=20260428-login-redesign"></script>
</body> </body>
</html> </html>
+260 -11
View File
@@ -302,16 +302,40 @@ textarea {
} }
.auth-page { .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: 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; 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 { .auth-shell {
width: min(1120px, calc(100% - 32px)); width: min(960px, calc(100% - 32px));
min-height: calc(100vh - 64px); min-height: 100vh;
display: grid; display: grid;
align-items: center; place-items: center;
margin: 0 auto; margin: 0 auto;
padding: 48px 0; padding: 48px 0;
} }
@@ -357,8 +381,42 @@ body:not(.auth-page) .dashboard-shell {
} }
.login-panel { .login-panel {
width: min(420px, 100%); width: min(500px, 100%);
padding: 32px; padding: 42px 56px 50px;
}
.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: 8px;
justify-items: center;
margin-bottom: 30px;
text-align: center;
}
.login-card-head h1 {
margin: 0;
color: #0f2546;
font-size: 32px;
font-weight: 900;
line-height: 1.16;
}
.login-subtitle {
margin: 0;
color: #6f7b8a;
font-size: 17px;
line-height: 1.4;
}
.login-error {
margin: 0 0 20px;
} }
.eyebrow { .eyebrow {
@@ -389,7 +447,7 @@ h2 {
.login-form { .login-form {
display: grid; display: grid;
gap: 10px; gap: 18px;
} }
.login-form label, .login-form label,
@@ -401,7 +459,7 @@ h2 {
font-weight: 800; font-weight: 800;
} }
.login-form input, .login-form .login-control,
.search-form input, .search-form input,
.search-form select, .search-form select,
.dashboard-search-form input, .dashboard-search-form input,
@@ -425,7 +483,7 @@ h2 {
outline: 0; outline: 0;
} }
.login-form input:focus, .login-form .login-control:focus,
.search-form input:focus, .search-form input:focus,
.search-form select:focus, .search-form select:focus,
.dashboard-search-form input:focus, .dashboard-search-form input:focus,
@@ -444,6 +502,141 @@ h2 {
box-shadow: 0 0 0 3px rgba(40, 105, 232, 0.14); 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-check {
display: inline-flex;
align-items: center;
gap: 8px;
color: #4b5563;
font-size: 16px;
line-height: 1.2;
white-space: nowrap;
cursor: pointer;
}
.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: 0;
padding: 0 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: -8px 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 { .button {
min-height: 36px; min-height: 36px;
display: inline-flex; display: inline-flex;
@@ -494,8 +687,13 @@ h2 {
opacity: 0.58; opacity: 0.58;
} }
.login-form .button-primary { .login-form .login-submit {
margin-top: 12px; width: 100%;
min-height: 56px;
margin-top: 2px;
border-radius: 8px;
font-size: 20px;
box-shadow: 0 10px 22px rgba(20, 104, 234, 0.26);
} }
.message { .message {
@@ -1118,6 +1316,57 @@ h2 {
padding: 0 16px; 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: 24px;
}
.login-card-head h1 {
font-size: 25px;
}
.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-form {
gap: 16px;
}
.login-check,
.forgot-password-link {
font-size: 14px;
}
.login-form .login-submit {
min-height: 52px;
font-size: 18px;
}
h1 { h1 {
font-size: 24px; 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();
}
});
}
}());