Compare commits

..

13 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
Zzzz acbd873fbc uiux 2026-04-28 20:57:31 +08:00
Zzzz e8c46311b9 chore(task): archive 04-28-sidebar-layout-management-ux 2026-04-28 20:16:59 +08:00
Zzzz da610644d7 chore: record journal 2026-04-28 20:16:51 +08:00
Zzzz d0e71f2aa9 uiux 2026-04-28 20:15:47 +08:00
Zzzz 0face72b8d chore: record journal 2026-04-28 19:28:24 +08:00
Zzzz cc9636e48a chore(task): archive 04-28-frontend-workbench-display-fix 2026-04-28 19:28:17 +08:00
Zzzz 0a386b81f9 前端优化 2026-04-28 19:26:08 +08:00
Zzzz 36db197e75 trellis元数据 2026-04-28 18:51:37 +08:00
Zzzz 781ce4697e add logging 2026-04-28 18:37:26 +08:00
45 changed files with 1720 additions and 626 deletions
@@ -630,6 +630,14 @@ reports/dashboard.jsp <- ReportServlet <- ReportService <- ReportDao <- books/re
- `users.username`: unique login identifier submitted by `LoginServlet`. - `users.username`: unique login identifier submitted by `LoginServlet`.
- `users.password_hash`: PBKDF2 hash in - `users.password_hash`: PBKDF2 hash in
`pbkdf2_sha256$iterations$saltBase64$hashBase64` format. `pbkdf2_sha256$iterations$saltBase64$hashBase64` format.
- Local scaffold demo users must have documented, known initial passwords for
new deployments: `admin/admin123`, `librarian/librarian123`, and
`reader/reader123`. Their `schema.sql` hashes must verify through
`PasswordHasher.verify` and must be treated as local/demo-only credentials,
never production credentials.
- `schema.sql` uses `INSERT IGNORE` for demo `users` rows. Replaying the schema
must not be assumed to reset existing account passwords; README reset
guidance must call this out explicitly.
- `users.role_code`: foreign key to `roles.code`; supported scaffold values - `users.role_code`: foreign key to `roles.code`; supported scaffold values
are `administrator`, `librarian`, and `reader`. are `administrator`, `librarian`, and `reader`.
- `users.active`: only rows with `active = 1` can authenticate. - `users.active`: only rows with `active = 1` can authenticate.
+15 -2
View File
@@ -18,8 +18,8 @@ the reusable UI units.
sidebar, footer, pagination, and message banners. sidebar, footer, pagination, and message banners.
- Use `.jspf` includes for the current JSP presentation layer. The authenticated - Use `.jspf` includes for the current JSP presentation layer. The authenticated
application frame lives in `src/main/webapp/WEB-INF/jsp/common/header.jspf` application frame lives in `src/main/webapp/WEB-INF/jsp/common/header.jspf`
and owns the dark sidebar, top utility bar, role workbench links, module and owns the dark sidebar, top utility bar, module navigation, global search,
navigation, global search, user display, and logout link. user display, and logout link.
- Any `.jspf` fragment that contains user-visible Simplified Chinese text must - Any `.jspf` fragment that contains user-visible Simplified Chinese text must
declare `<%@ page pageEncoding="UTF-8" %>` at the top. Do not rely only on the declare `<%@ page pageEncoding="UTF-8" %>` at the top. Do not rely only on the
including JSP page or response `Content-Type`; Tomcat/Jasper can otherwise including JSP page or response `Content-Type`; Tomcat/Jasper can otherwise
@@ -31,6 +31,19 @@ the reusable UI units.
links stay inside `sessionScope.userRole == 'administrator'`; staff links stay links stay inside `sessionScope.userRole == 'administrator'`; staff links stay
inside `administrator or librarian`; reader-only links stay inside inside `administrator or librarian`; reader-only links stay inside
`sessionScope.userRole == 'reader'`. `sessionScope.userRole == 'reader'`.
- For active navigation in forwarded JSPs, derive the current location from the
public Servlet path before falling back to the JSP servlet path. Use exact
matches or slash-delimited prefixes; do not use broad `fn:contains` checks
against `requestURI`, because forwarded pages expose `/WEB-INF/jsp/...` paths
and can activate unrelated sidebar items.
```jsp
<c:set var="currentPath" value="${requestScope['javax.servlet.forward.servlet_path']}" />
<c:if test="${empty currentPath}">
<c:set var="currentPath" value="${pageContext.request.servletPath}" />
</c:if>
<a class="${(currentPath == '/books' or fn:startsWith(currentPath, '/books/')) ? 'is-active' : ''}">
```
- Keep fragments presentation-focused. They should not open database - Keep fragments presentation-focused. They should not open database
connections or call DAOs. connections or call DAOs.
@@ -45,6 +45,9 @@ image-first design and preserve the Servlet/JSP layered architecture.
- Do not implement UI only from text descriptions when an approved image - Do not implement UI only from text descriptions when an approved image
reference exists. reference exists.
- Do not put SQL, DAO calls, or business workflows in JSP pages. - Do not put SQL, DAO calls, or business workflows in JSP pages.
- Do not hard-code operational dashboard/report metrics, sample people, fixed
borrow dates, or fake table rows in JSP pages; use Servlet-provided request
attributes and empty states.
- Do not rely only on browser validation for protected workflows. - Do not rely only on browser validation for protected workflows.
--- ---
@@ -36,6 +36,100 @@ changes the frontend architecture.
--- ---
## Scenario: Dashboard Workbench Request Contract
### 1. Scope / Trigger
- Trigger: the authenticated workbench spans Servlet request attributes,
service-derived report/catalog/borrowing data, and role-specific JSP display.
- Route: `GET /dashboard`.
- JSP path: `WEB-INF/jsp/dashboard.jsp`.
### 2. Signatures
- Servlet: `DashboardServlet.doGet(HttpServletRequest, HttpServletResponse)`.
- Services used for page data:
- `BookService.listCategories()`.
- `BookService.searchBooks(new BookSearchCriteria())`.
- `ReaderService.searchReaders(new ReaderSearchCriteria())` for staff reader
totals.
- `ReportService.loadReportCenter(AuthenticatedUser actor)` for
administrator/librarian users.
- `BorrowingService.searchRecords(actor, new BorrowRecordSearchCriteria())`
for administrator/librarian users.
- Request attributes:
- `currentUser: AuthenticatedUser`.
- `categories: List<BookCategory>`.
- `dashboardBooks: List<Book>`.
- `dashboardMetrics: List<DashboardMetric>`.
- `reportCenter: ReportCenter` for staff users when report loading succeeds.
- `dashboardBorrowRecords: List<BorrowRecord>` for staff users.
- `errorMessage: String` when a service returns a safe failure.
### 3. Contracts
- Workbench values must come from request attributes populated by the Servlet;
JSP must not embed operational sample rows, fixed dates, or fake totals.
- Staff metrics use `ReportCenter` values derived from `books` and
`borrow_records`, plus reader totals from `ReaderService`; reader fallback
metrics may derive from `dashboardBooks`.
- Popular ranking, overdue rows, and borrowing rows render only real service
results and show empty states when lists are empty.
- Category filters render from `categories`, the same source used by catalog and
book-management pages.
- Role-gated sections stay in JSP conditionals based on `sessionScope.userRole`;
staff-only data is not requested for reader users.
### 4. Validation & Error Matrix
- Category load failure -> `categories` is an empty list and `errorMessage` is
set.
- Book search failure -> `dashboardBooks` is an empty list and `errorMessage`
is set.
- Reader total load failure -> staff metrics fall back to another real
service-derived metric and `errorMessage` is set.
- Staff report load failure -> report-backed sections show empty states and
`errorMessage` is set.
- Staff borrowing search failure -> `dashboardBorrowRecords` is an empty list
and `errorMessage` is set.
- Empty service result -> render a stable empty state, not hard-coded fallback
sample data.
### 5. Good/Base/Bad Cases
- Good: a librarian opens `/dashboard` and sees report-backed metrics, current
borrowing rows, overdue rows, popular ranking, and real book rows.
- Base: no borrow records exist; the workbench keeps the layout and shows empty
states for ranking, borrowing, and overdue panels.
- Bad: `dashboard.jsp` contains names, book IDs, 2024 dates, or counts that do
not come from request attributes.
### 6. Tests Required
- Run Maven compile/test for Servlet and JavaBean contract checks.
- Run standalone service checks covering report, borrowing, catalog/book, and
permission policy behavior when available.
- Scan `dashboard.jsp` for static sample names, fixed dates, and decorative
sample-only values after dashboard changes.
- Verify staff and reader role conditionals still show only the intended
sections.
### 7. Wrong vs Correct
#### Wrong
```text
dashboard.jsp -> hard-coded metric "12,586" and fixed rows like "L20240521001"
```
#### Correct
```text
dashboard.jsp <- DashboardServlet <- ReportService/BookService/ReaderService/BorrowingService
```
---
## Page Scripts ## Page Scripts
Small JavaScript can improve interaction, such as confirm dialogs or local form Small JavaScript can improve interaction, such as confirm dialogs or local form
+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,2 @@
{"_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 context for verifying the refreshed preview"}
@@ -0,0 +1,2 @@
{"_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 context for preview verification"}
@@ -0,0 +1,47 @@
# Rebuild Current Frontend Preview
## Goal
Rebuild the current Java Web application and refresh the local Tomcat deployment so the user can view the latest frontend effect in the browser.
## What I already know
* The user asked to rebuild the program to inspect the new frontend.
* The project is a Java 11 Maven WAR application.
* Maven produces `target/library-management.war`.
* Frontend JSP/CSS assets live under `src/main/webapp`.
* Local Tomcat path recorded by prior work is `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117`.
* The Tomcat context should be `/library-management`.
## Requirements
* Run a clean Maven package build.
* Deploy the new WAR to the local Tomcat `webapps` directory.
* Remove the expanded old deployment directory before restart so stale frontend assets are not reused.
* Start or restart Tomcat.
* Verify the frontend login URL is reachable.
* Provide the local browser URL to the user.
## Acceptance Criteria
* [x] `mvn clean package` succeeds.
* [x] `target/library-management.war` exists.
* [x] Tomcat deployment is refreshed with the new WAR.
* [x] `/library-management/login` returns HTTP 200.
* [x] User receives the local preview URL.
## Out of Scope
* Source code changes.
* New UI requirements or redesign decisions.
* Database schema or seed data changes.
## Technical Notes
* Build command from README: `mvn clean package`; fallback Maven path: `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package`.
* Deployment target: `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117/webapps/library-management.war`.
* Build completed at 2026-04-28 20:21 +0800.
* Previous Tomcat deployment was moved to `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117/deploy-backups/_pre-preview-20260428-202206/`.
* Tomcat is running in tmux session `mzh-library-tomcat`.
* Verified `http://localhost:8080/library-management/login` returns `HTTP 200` with `Content-Type: text/html;charset=UTF-8`.
* Verified demo login redirects to `/library-management/dashboard`, and the authenticated dashboard returns `HTTP 200`.
@@ -0,0 +1,26 @@
{
"id": "04-28-rebuild-current-frontend-preview",
"name": "04-28-rebuild-current-frontend-preview",
"title": "rebuild current frontend preview",
"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": {}
}
@@ -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": {}
}
@@ -0,0 +1,5 @@
{"_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": "Verify frontend work follows JSP/CSS conventions"}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Verify responsive UI and simplified authenticated shell"}
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Verify backend layering and no static data regressions"}
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Verify dashboard uses existing derived report data correctly"}
@@ -0,0 +1,8 @@
{"_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 conventions for authenticated shell and dashboard UI"}
{"file": ".trellis/spec/backend/index.md", "reason": "Backend Servlet/service/DAO layering for dashboard real data"}
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "JSP fragments, cards, tables, and reusable presentation rules"}
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered request/session state conventions"}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "UI quality checks for JSP/CSS changes"}
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Existing report data contracts and database-derived summary rules"}
{"file": ".trellis/spec/backend/error-handling.md", "reason": "ServiceResult and servlet error handling conventions"}
@@ -0,0 +1,54 @@
# Fix Frontend Workbench Display
## Goal
Make the authenticated workbench reflect real application data and simplify the navigation-heavy UI so it does not duplicate the sidebar.
## What I Already Know
- The user reported that the frontend workbench data does not match actual data.
- The current `dashboard.jsp` hard-codes metric values, popular book ranking rows, borrowing rows, overdue rows, and book rows.
- The workbench shortcut cards for 读者管理, 报表中心, 借阅流通, and 系统日志 duplicate links already present in the sidebar.
- The UI uses circular single-character markers beside text in metrics, shortcut cards, sidebar links, role chips, and topbar user summary.
- The sidebar is fixed on desktop, but responsive CSS changes `.app-sidebar` and `.app-topbar` to static layout under 960px, effectively removing the persistent sidebar behavior.
- Existing report infrastructure already exposes actual inventory summary, borrowing summary, overdue rows, and popular books through `ReportService.loadReportCenter(...)`.
## Assumptions
- "UI text beside an unnecessary circle with one character" applies to decorative single-character icon circles in the authenticated shell and workbench, not to plain text labels or table status pills.
- The workbench should reuse existing server-rendered JSP/Servlet patterns rather than introducing client-side state.
- When a specific real data source does not yet exist, prefer showing an existing real metric over keeping a static fake metric.
## Requirements
- Replace hard-coded workbench summary metrics with real data.
- Replace the hard-coded popular book ranking with real ranking data.
- Replace hard-coded borrowing/overdue/book table samples with real data or remove the fake sample rows in favor of empty states.
- Keep the workbench catalog search category selector populated from real categories.
- Remove the workbench shortcut entry block containing 读者管理, 报表中心, 借阅流通, and 系统日志.
- Remove the decorative circular single-character UI markers around text in the authenticated shell/workbench where they are not functionally necessary.
- Ensure the sidebar cannot be hidden or collapsed by responsive layout rules.
- Keep role-based visibility and permissions intact for administrator, librarian, and reader users.
## Acceptance Criteria
- [ ] Workbench metrics are rendered from request attributes populated by backend services, not hard-coded numbers.
- [ ] Popular ranking and table content no longer contain static sample records such as 张晓明, 活着, 三体, or fixed 2024 dates unless those values come from the database.
- [ ] The workbench no longer shows shortcut cards for 读者管理, 报表中心, 借阅流通, or 系统日志.
- [ ] Decorative single-character circles next to UI text are removed or restyled as plain text/spacing without circular badges.
- [ ] Sidebar remains visible and occupies its sidebar column across responsive breakpoints.
- [ ] Existing navigation links still work and remain role-aware.
- [ ] Project lint/type-check or the closest available Java build/test command passes.
## Out Of Scope
- Adding new major dashboard modules beyond the current workbench content.
- Redesigning unrelated pages outside the shared authenticated shell and workbench.
- Changing database schema unless necessary to replace static workbench data.
## Technical Notes
- Likely files: `src/main/java/com/mzh/library/controller/DashboardServlet.java`, `src/main/webapp/WEB-INF/jsp/dashboard.jsp`, `src/main/webapp/WEB-INF/jsp/common/header.jspf`, and `src/main/webapp/static/css/app.css`.
- Existing actual report data: `ReportServiceImpl`, `JdbcReportDao`, `ReportCenter`, `InventorySummary`, `BorrowingSummary`, `OverdueReportRow`, and `PopularBookReportRow`.
- Existing category/book patterns: `BookServiceImpl`, `JdbcBookDao`, and `BookCatalogServlet`.
- Existing borrowing list pattern: `BorrowingServiceImpl.searchRecords(...)` and `BorrowingManagementServlet`.
@@ -0,0 +1,26 @@
{
"id": "frontend-workbench-display-fix",
"name": "frontend-workbench-display-fix",
"title": "修复前端工作台展示",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "Zzzz",
"assignee": "Zzzz",
"createdAt": "2026-04-28",
"completedAt": "2026-04-28",
"branch": null,
"base_branch": "master",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -0,0 +1,7 @@
{"file": ".trellis/spec/frontend/index.md", "reason": "Review JSP changes against presentation-layer conventions."}
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Verify button/action removal keeps page composition and primary operations intact."}
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Ensure JSP changes do not alter request/session contracts."}
{"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."}
@@ -0,0 +1,9 @@
{"file": ".trellis/spec/frontend/index.md", "reason": "JSP presentation-layer conventions and required pre-development checklist for removing redundant page actions."}
{"file": ".trellis/spec/frontend/directory-structure.md", "reason": "Location and ownership of JSP pages, shared fragments, and static assets."}
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Rules for JSP fragments, forms, tables, buttons, and page composition."}
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered request/session/form state constraints for JSP changes."}
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "JSP/Servlet display contracts and safe rendering expectations."}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "UI quality checks for JSP/CSS changes."}
{"file": ".trellis/spec/backend/index.md", "reason": "Backend architecture overview for database initialization changes."}
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "MySQL schema and seed-data conventions for readers, categories, and books."}
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Layer-boundary and verification expectations for database-only backend changes."}
@@ -0,0 +1,70 @@
# remove redundant page actions and add Chinese demo data
## Goal
精简已登录页面中与侧边栏重复的右侧跨模块跳转按钮,补充更贴近中文图书馆场景的演示图书与读者数据,并按参考截图重构真实可用的登录界面。
## What I already know
* 用户希望移除以下重复入口:报表中心右侧“借阅记录”;馆藏检索右侧“管理图书”;图书管理右侧“分类”“查看馆藏”;管理分类右侧“管理图书”;读者档案右侧“管理登录账户”;用户账户与角色右侧“读者档案”。
* 侧边栏已经提供这些模块之间的跳转,因此页面标题栏和工具栏中的跨模块二级入口会显得重复。
* “新增图书”“新增分类”“新增读者档案”“新增账户”等当前页面内的主要操作仍应保留。
* 演示数据位于 `src/main/resources/db/schema.sql`,当前包含英文读者名、英文分类和英文图书。
* 项目是 JSP + Servlet + MySQL 架构,前端页面在 `src/main/webapp/WEB-INF/jsp/`,数据库初始化脚本使用 `utf8mb4`
* 用户补充要求:仿照参考截图重构登录界面,必须是真实可用的登录表单,而不是静态展示页。
* 参考截图特征:浅色模糊图书馆背景、居中的白色登录卡片、蓝色书本图标与“图书管理系统”标题、用户名/密码输入框图标、密码显隐按钮、身份单选项、记住我和忘记密码入口、蓝色主登录按钮。
## Assumptions
* 本任务只调整冗余跨模块按钮,不改变侧边栏导航、权限控制、Servlet 路由或业务流程。
* 数据初始化仍使用 `INSERT IGNORE` / `ON DUPLICATE KEY UPDATE` 的现有风格,避免重复执行脚本破坏已有本地数据。
* 中文演示数据可以替换或扩充现有英文样例,但登录测试账号用户名和密码保持不变。
## Requirements
* 报表中心页面不再显示跳转到借阅记录的右侧按钮。
* 馆藏检索页面不再显示跳转到管理图书的右侧按钮。
* 图书管理页面不再显示跳转到分类管理或馆藏检索的右侧按钮;保留新增图书入口。
* 分类管理页面不再显示跳转到管理图书的右侧按钮;保留新增分类入口。
* 读者档案页面不再显示跳转到管理登录账户的右侧按钮;保留新增读者档案入口。
* 用户账户与角色页面不再显示跳转到读者档案的右侧按钮;保留新增账户入口。
* 数据库初始化脚本加入中文图书分类、中文书名、中文作者和中文读者姓名。
* 本地演示账号仍能用于登录验证。
* 登录页按参考截图重构视觉,但保留现有 `POST /login``username``password``redirect`、错误提示和回填用户名等真实登录能力。
* 登录页新增或保留真实可交互控件:密码显隐切换、登录身份单选项、记住我选项和忘记密码入口。
* 登录身份选择不应破坏现有服务端认证;当前后端仍以账号密码和账号角色为准,前端角色选项仅作为登录意图提示或表单辅助字段。
* 登录页需要在桌面和移动端保持可用,输入框、按钮和错误提示不能溢出或遮挡。
## Acceptance Criteria
* [x] 指定页面中的重复跨模块按钮被移除,侧边栏仍能导航到对应模块。
* [x] 页面内新增操作按钮未被误删。
* [x] `schema.sql` 包含多条中文图书数据和多条中文读者数据。
* [x] 中文演示数据使用 `utf8mb4` 兼容的文本,不引入新表或迁移机制。
* [x] 相关检查或可用的构建验证通过;若环境缺少 Maven,记录 fallback 验证。
* [x] 登录页视觉接近参考截图,并使用真实表单提交到现有 `/login`
* [x] 密码显隐、记住我、身份单选项在浏览器中可交互且不破坏登录流程。
* [x] 登录失败时继续显示服务端错误提示并保留用户名/redirect。
* [x] 登录页在移动端和桌面端布局稳定,无文字或控件重叠。
## Definition of Done
* Tests/checks run where available.
* Lint/typecheck/build status reported.
* Specs reviewed for whether new conventions need recording.
## Out of Scope
* 不重设计侧边栏或整体视觉风格。
* 不新增页面、权限、路由或服务层能力。
* 不改变借阅记录、报表、用户账户或读者档案的业务逻辑。
* 不实现真实找回密码流程;忘记密码入口可展示当前系统暂未开放或指向安全的占位交互。
## 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`, `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.
@@ -0,0 +1,26 @@
{
"id": "remove-redundant-actions-add-cn-data",
"name": "remove-redundant-actions-add-cn-data",
"title": "remove redundant page actions and add Chinese demo data",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "Zzzz",
"assignee": "Zzzz",
"createdAt": "2026-04-28",
"completedAt": "2026-04-28",
"branch": null,
"base_branch": "master",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -0,0 +1,6 @@
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend stack and checklist for final review."}
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Check JSP fragments, role-conditioned navigation, Chinese copy, and reusable UI patterns."}
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Check session/request state usage remains server-rendered and safe."}
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Check JSP/Servlet display contracts and safe EL/JSTL rendering."}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Check navigation, layout, accessibility, and JSP/CSS architecture quality."}
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Check the change preserves the agreed JSP + Servlet + Tomcat stack."}
@@ -0,0 +1,6 @@
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend stack and checklist for JSP/CSS implementation."}
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Shared JSP fragment, role-conditioned navigation, Simplified Chinese copy, form, table, and CSS conventions."}
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered request/session state conventions while using session role data in navigation."}
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "JSP/Servlet display contracts and safe EL/JSTL rendering constraints."}
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Frontend verification expectations for navigation, layout, accessibility, and JSP/CSS boundaries."}
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Project stack constraints for JSP, Servlet, MySQL, and Tomcat."}
@@ -0,0 +1,87 @@
# Sidebar Active State And Management UX Cleanup
## Goal
Fix several visible JSP/CSS navigation and layout issues in the authenticated library-management UI, and reduce confusion between reader profile management and user account management without changing the backend data model.
## What I Already Know
* The application is a Java 11 Maven WAR using JSP, Servlet, JSTL, CSS, and Tomcat.
* Authenticated navigation lives in `src/main/webapp/WEB-INF/jsp/common/header.jspf`.
* Sidebar active state currently uses `fn:contains(currentUri, ...)`, but rendered JSP paths can differ from public servlet paths after `RequestDispatcher.forward`.
* This explains reported false positives and false negatives:
* `/catalog` can render through `/WEB-INF/jsp/books/catalog.jsp`, causing the books nav item to look active.
* `/book-categories` renders through `/WEB-INF/jsp/books/categories.jsp`, causing books to look active while categories may not.
* `/reports` renders `reports/dashboard.jsp`, which can make dashboard/workbench look active.
* `/admin/system-logs` renders `maintenance/system-logs.jsp`, so the system log item may not activate.
* The catalog, book management, and reader management hero sections put eyebrow/title/body/actions directly under a flex container; pages that wrap text in a child `<div>` avoid the horizontal layout break.
* `dashboard.jsp` contains the small technical sentence the user wants removed.
* `ReaderManagementServlet` manages reader profiles/eligibility/contact/borrowing limits; `UserManagementServlet` manages login accounts/roles/active status. These are overlapping concepts to users but distinct backend workflows.
## Requirements
* Sidebar active state must be based on the original public servlet path, not the forwarded JSP path.
* Only the matching sidebar item should be active for catalog, books, book categories, reports, and system logs.
* Remove the sidebar "角色工作台" block.
* Remove the sidebar "工作台" nav item.
* Move "报表中心" to the top of the main module navigation for administrator/librarian roles.
* Fix the header/hero layout on catalog, book management, and reader management so eyebrow/title/description stay grouped vertically.
* Remove the dashboard sentence: `登录后进入 Dashboard,会话仅保存安全的 AuthenticatedUser 快照、角色代码与权限代码集合。`
* Reduce the perceived duplication between reader management and user management using conservative UI changes:
* Treat reader management as reader profile/borrowing eligibility management.
* Treat user management as account/role/login status management.
* Prefer clearer labels, descriptions, and cross-links over merging backend flows.
## Acceptance Criteria
* [x] Opening `/catalog` highlights only "馆藏检索".
* [x] Opening `/books` highlights only "图书管理".
* [x] Opening `/book-categories` highlights only "图书分类管理".
* [x] Opening `/reports` highlights only "报表中心" and does not highlight "工作台".
* [x] Opening `/admin/system-logs` highlights "系统日志".
* [x] The sidebar no longer displays the role workbench cards or a "工作台" nav item.
* [x] "报表中心" appears before catalog/books/readers/borrowing for administrator/librarian navigation.
* [x] Catalog, book management, and reader management hero copy is vertically grouped and does not lay out as separate horizontal items.
* [x] The dashboard technical session sentence is absent.
* [x] Reader/user management labels and descriptions make the distinction between reader profiles and user accounts clearer.
* [x] Maven verification passes or the closest available build command is reported.
## Definition Of Done
* Focused JSP/CSS changes only unless a backend change is required by verification.
* Existing Servlet/JSP rendering and JSTL escaping behavior remains intact.
* Maven build/test verification run where available.
* Trellis quality check completed before final response.
## Technical Approach
* In `header.jspf`, derive a `currentPath` from `requestScope['javax.servlet.forward.servlet_path']` with a fallback to `pageContext.request.servletPath`.
* Replace broad `fn:contains` checks with exact or prefix checks against public servlet paths.
* Reorder and trim sidebar markup according to the requested information architecture.
* Wrap catalog/book/reader hero text in a child `<div>` to match pages that already render correctly.
* Remove only the requested dashboard small text, leaving role-specific workbench headings and metrics intact.
* Use copy changes and cross-links to clarify reader profiles versus user accounts without changing controllers, entities, DAOs, or database schema.
## Out Of Scope
* Merging reader and user management into a single page.
* Changing authentication, authorization, database schema, or service-layer behavior.
* Redesigning the whole dashboard or adding new frontend libraries.
## Technical Notes
* Relevant frontend spec index: `.trellis/spec/frontend/index.md`.
* Relevant files inspected:
* `src/main/webapp/WEB-INF/jsp/common/header.jspf`
* `src/main/webapp/WEB-INF/jsp/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`
* `src/main/webapp/static/css/app.css`
* Build command from README: `mvn clean package`; fallback path documented as `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` if `mvn` is not on `PATH`.
* Verification on 2026-04-28:
* `git diff --check` passed.
* Search for removed sidebar role/workbench and old active-state patterns returned no matches.
* `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` passed with `BUILD SUCCESS`.
@@ -0,0 +1,26 @@
{
"id": "sidebar-layout-management-ux",
"name": "sidebar-layout-management-ux",
"title": "修复侧边栏高亮与管理页布局优化",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "Zzzz",
"assignee": "Zzzz",
"createdAt": "2026-04-28",
"completedAt": "2026-04-28",
"branch": null,
"base_branch": "master",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
@@ -27,6 +27,11 @@ Add safe server-side diagnostic logs to the login/authentication path so a Windo
* Preserve the current user-facing Chinese error message and login behavior. * Preserve the current user-facing Chinese error message and login behavior.
* Keep the implementation in the existing Servlet + service + DAO + JDBC stack. * Keep the implementation in the existing Servlet + service + DAO + JDBC stack.
* Prefer `java.util.logging` patterns already used in the project. * Prefer `java.util.logging` patterns already used in the project.
* Document and seed explicit local/demo initial credentials so new deployments are not blocked by unrecoverable password hashes:
* `admin` / `admin123`
* `librarian` / `librarian123`
* `reader` / `reader123`
* Make clear that these demo passwords are for local scaffold verification only and must be changed or removed before non-local/production use.
## Acceptance Criteria ## Acceptance Criteria
@@ -36,6 +41,9 @@ Add safe server-side diagnostic logs to the login/authentication path so a Windo
* [x] No log statement outputs a raw password, password hash, salt, or database password. * [x] No log statement outputs a raw password, password hash, salt, or database password.
* [x] Existing login success/failure behavior remains unchanged for users. * [x] Existing login success/failure behavior remains unchanged for users.
* [x] `mvn test` or the closest available Maven verification command succeeds. * [x] `mvn test` or the closest available Maven verification command succeeds.
* [x] README lists the local/demo initial login accounts and passwords with an explicit non-production warning.
* [x] `schema.sql` seed user hashes verify against the documented demo passwords for new deployments.
* [x] Existing deployments have a documented SQL reset path or warning explaining that `INSERT IGNORE` will not overwrite existing user rows.
## Definition Of Done ## Definition Of Done
@@ -68,3 +76,7 @@ Add safe server-side diagnostic logs to the login/authentication path so a Windo
* `/home/sjy/.sdkman/candidates/maven/current/bin/mvn package` passed with `BUILD SUCCESS` and produced `target/library-management.war`. * `/home/sjy/.sdkman/candidates/maven/current/bin/mvn package` passed with `BUILD SUCCESS` and produced `target/library-management.war`.
* `git diff --check` passed. * `git diff --check` passed.
* Sensitive logger scan only found boolean password state fields, `password=<redacted>`, and `password-mismatch` category labels. * Sensitive logger scan only found boolean password state fields, `password=<redacted>`, and `password-mismatch` category labels.
* Verification completed at 2026-04-28 18:33 +0800:
* `PasswordHasher.verify` returned `true` for `admin/admin123`, `librarian/librarian123`, and `reader/reader123` against the updated `schema.sql` PBKDF2 hashes.
* `/home/sjy/.sdkman/candidates/maven/current/bin/mvn verify` passed with `BUILD SUCCESS`.
* `git diff --check` passed.
@@ -3,7 +3,7 @@
"name": "windows-login-diagnostic-logs", "name": "windows-login-diagnostic-logs",
"title": "Add Windows login diagnostic logs", "title": "Add Windows login diagnostic logs",
"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,
+6 -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**: 11 - **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` | ~441 | Active | | `journal-1.md` | ~573 | Active |
<!-- @@@/auto:active-documents --> <!-- @@@/auto:active-documents -->
--- ---
@@ -29,6 +29,10 @@
<!-- @@@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` |
| 13 | 2026-04-28 | Frontend workbench display fix | `0a386b8` | `master` |
| 12 | 2026-04-28 | Windows login diagnostics and demo credentials | `781ce46` | `master` |
| 11 | 2026-04-28 | Frontend Reference Redesign | `89b6dd1` | `master` | | 11 | 2026-04-28 | Frontend Reference Redesign | `89b6dd1` | `master` |
| 10 | 2026-04-28 | 中文详细 README | `2d4a7e2` | `master` | | 10 | 2026-04-28 | 中文详细 README | `2d4a7e2` | `master` |
| 9 | 2026-04-28 | Frontend Chinese UI | `ff044e6` | `master` | | 9 | 2026-04-28 | Frontend Chinese UI | `ff044e6` | `master` |
+132
View File
@@ -439,3 +439,135 @@ Refactored the JSP frontend to match the provided library dashboard reference im
### Next Steps ### Next Steps
- None - task complete - None - task complete
## Session 12: Windows login diagnostics and demo credentials
**Date**: 2026-04-28
**Task**: Windows login diagnostics and demo credentials
**Branch**: `master`
### Summary
Added safe login/database diagnostic logs, documented local demo credentials, updated seed hashes, verified Maven build.
### Main Changes
(Add details)
### Git Commits
| Hash | Message |
|------|---------|
| `781ce46` | (see git log) |
### Testing
- [OK] (Add test results)
### Status
[OK] **Completed**
### Next Steps
- None - task complete
## Session 13: Frontend workbench display fix
**Date**: 2026-04-28
**Task**: Frontend workbench display fix
**Branch**: `master`
### Summary
Replaced hard-coded dashboard data with service-backed workbench data, simplified sidebar/workbench UI, kept sidebar persistent, updated frontend specs, and verified with Maven/service checks.
### Main Changes
(Add details)
### Git Commits
| Hash | Message |
|------|---------|
| `0a386b8` | (see git log) |
### Testing
- [OK] (Add test results)
### Status
[OK] **Completed**
### Next Steps
- None - task complete
## Session 14: Sidebar layout and management UX cleanup
**Date**: 2026-04-28
**Task**: Sidebar layout and management UX cleanup
**Branch**: `master`
### Summary
Fixed sidebar active-state routing and navigation order, corrected management page hero layouts, removed dashboard technical copy, clarified reader profile versus user account UI, updated frontend navigation spec, and verified Maven package build.
### Main Changes
(Add details)
### Git Commits
| Hash | Message |
|------|---------|
| `d0e71f2` | (see git log) |
### Testing
- [OK] (Add test results)
### Status
[OK] **Completed**
### Next Steps
- 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
+32 -3
View File
@@ -109,7 +109,36 @@ src/main/resources/db/schema.sql
mysql -u root -p < src/main/resources/db/schema.sql mysql -u root -p < src/main/resources/db/schema.sql
``` ```
脚本内包含本地验证用的演示角色、权限、用户、读者、分类和图书数据。演示账户只用于本地脚手架验证;在非本地数据库中使用前应更换或删除这些数据。本文档不提供任何登录明文密码。 脚本内包含本地验证用的演示角色、权限、用户、读者、分类和图书数据。演示账户只用于本地脚手架验证;在非本地数据库中使用前应更换或删除这些数据。
本地/demo 初始登录账号如下。这些是应用登录账号,不是 MySQL 数据库账号:
| 角色 | 用户名 | 初始密码 |
| --- | --- | --- |
| 管理员 | `admin` | `admin123` |
| 馆员 | `librarian` | `librarian123` |
| 读者 | `reader` | `reader123` |
这些明文密码只用于新部署本地环境的首次验证。非本地或生产环境上线前,必须通过系统用户管理功能改密,或删除/替换这些演示账号。
`schema.sql` 使用 `INSERT IGNORE INTO users` 写入演示账号。如果目标数据库里已经存在同名 `admin``librarian``reader` 行,重新执行脚本不会覆盖现有密码哈希。需要重置本地演示账号时,优先在系统用户管理功能中修改密码;如果无法登录,可在确认这是本地/demo 数据库后执行以下 SQL:
```sql
UPDATE users
SET password_hash = 'pbkdf2_sha256$60000$Ren1B30RDysysnApRiFVaQ==$1XwzMHaALqC7dKffwjbQkilBedfAuiMOXbR/xTMr5+Y=',
active = 1
WHERE username = 'admin';
UPDATE users
SET password_hash = 'pbkdf2_sha256$60000$PV/DJwZlMRm8vy0lKMAM4g==$+Aijfop3YoPp6HTePN5r4wG8N3qgxJE+yZHkTfzfbaw=',
active = 1
WHERE username = 'librarian';
UPDATE users
SET password_hash = 'pbkdf2_sha256$60000$wBzxTIT4ep79hgEzYDV9aQ==$w3oO5iSKRSfG4++b4558yiTHy6Tz9BB2+wuV9UOAKhs=',
active = 1
WHERE username = 'reader';
```
## 本地配置 ## 本地配置
@@ -327,9 +356,9 @@ Maven 当前将 WAR 产物命名为 `library-management.war`。Tomcat 通常会
不可以。`src/main/resources/db.properties` 是本地私密配置,已经被 `.gitignore` 忽略。只应提交 `src/main/resources/db.properties.example` 不可以。`src/main/resources/db.properties` 是本地私密配置,已经被 `.gitignore` 忽略。只应提交 `src/main/resources/db.properties.example`
### README 为什么不列出演示账号密码? ### 重新执行 `schema.sql` 后演示账号密码为什么没变
数据库脚本包含本地验证用演示数据,但项目要求 README 不写入未经确认的默认登录明文密码,也不扩散任何凭据。需要本地调试时,请由维护者按当前数据库脚本和安全要求单独确认或重置账号 `schema.sql` 使用 `INSERT IGNORE INTO users` 写入本地/demo 账号。已有同名用户时,MySQL 会跳过插入,不会覆盖现有密码哈希。需要重置时,请参考“数据库初始化”里的本地/demo 账号说明;不要在非本地数据库中直接恢复这些演示密码
## 维护提示 ## 维护提示
@@ -1,9 +1,37 @@
package com.mzh.library.controller; package com.mzh.library.controller;
import com.mzh.library.dao.impl.JdbcBookDao;
import com.mzh.library.dao.impl.JdbcBorrowRecordDao;
import com.mzh.library.dao.impl.JdbcReaderDao;
import com.mzh.library.dao.impl.JdbcReportDao;
import com.mzh.library.entity.AuthenticatedUser; import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Book;
import com.mzh.library.entity.BookCategory;
import com.mzh.library.entity.BookSearchCriteria;
import com.mzh.library.entity.BookStatus;
import com.mzh.library.entity.BorrowRecord;
import com.mzh.library.entity.BorrowRecordSearchCriteria;
import com.mzh.library.entity.BorrowingSummary;
import com.mzh.library.entity.InventorySummary;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderSearchCriteria;
import com.mzh.library.entity.ReportCenter;
import com.mzh.library.entity.Role;
import com.mzh.library.service.BookService;
import com.mzh.library.service.BorrowingService;
import com.mzh.library.service.ReaderService;
import com.mzh.library.service.ReportService;
import com.mzh.library.service.ServiceResult;
import com.mzh.library.service.impl.BookServiceImpl;
import com.mzh.library.service.impl.BorrowingServiceImpl;
import com.mzh.library.service.impl.ReaderServiceImpl;
import com.mzh.library.service.impl.ReportServiceImpl;
import com.mzh.library.util.SessionAttributes; import com.mzh.library.util.SessionAttributes;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.servlet.ServletException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
@@ -14,13 +42,200 @@ import javax.servlet.http.HttpSession;
public class DashboardServlet extends HttpServlet { public class DashboardServlet extends HttpServlet {
private static final String DASHBOARD_JSP = "/WEB-INF/jsp/dashboard.jsp"; private static final String DASHBOARD_JSP = "/WEB-INF/jsp/dashboard.jsp";
private BookService bookService;
private BorrowingService borrowingService;
private ReaderService readerService;
private ReportService reportService;
@Override
public void init() {
this.bookService = new BookServiceImpl(new JdbcBookDao());
this.borrowingService = new BorrowingServiceImpl(new JdbcBorrowRecordDao());
this.readerService = new ReaderServiceImpl(new JdbcReaderDao());
this.reportService = new ReportServiceImpl(new JdbcReportDao());
}
@Override @Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(false); AuthenticatedUser user = currentUser(request);
AuthenticatedUser user = session == null
? null
: (AuthenticatedUser) session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
request.setAttribute("currentUser", user); request.setAttribute("currentUser", user);
ServiceResult<List<BookCategory>> categoryResult = bookService.listCategories();
request.setAttribute("categories", categoryResult.isSuccessful()
? listOrEmpty(categoryResult.getData())
: Collections.emptyList());
if (!categoryResult.isSuccessful()) {
setErrorMessage(request, categoryResult.getMessage());
}
ServiceResult<List<Book>> bookResult = bookService.searchBooks(new BookSearchCriteria());
List<Book> dashboardBooks = bookResult.isSuccessful()
? listOrEmpty(bookResult.getData())
: Collections.emptyList();
request.setAttribute("dashboardBooks", dashboardBooks);
if (!bookResult.isSuccessful()) {
setErrorMessage(request, bookResult.getMessage());
}
List<DashboardMetric> metrics = Collections.emptyList();
if (isStaff(user)) {
Integer readerTotal = null;
ServiceResult<List<Reader>> readerResult = readerService.searchReaders(new ReaderSearchCriteria());
if (readerResult.isSuccessful()) {
readerTotal = listOrEmpty(readerResult.getData()).size();
} else {
setErrorMessage(request, readerResult.getMessage());
}
ServiceResult<ReportCenter> reportResult = reportService.loadReportCenter(user);
if (reportResult.isSuccessful()) {
ReportCenter reportCenter = reportResult.getData();
request.setAttribute("reportCenter", reportCenter);
metrics = metricsFromReport(reportCenter, readerTotal);
} else {
setErrorMessage(request, reportResult.getMessage());
}
ServiceResult<List<BorrowRecord>> borrowResult =
borrowingService.searchRecords(user, new BorrowRecordSearchCriteria());
request.setAttribute("dashboardBorrowRecords", borrowResult.isSuccessful()
? listOrEmpty(borrowResult.getData())
: Collections.emptyList());
if (!borrowResult.isSuccessful()) {
setErrorMessage(request, borrowResult.getMessage());
}
}
if (metrics.isEmpty() && bookResult.isSuccessful()) {
metrics = metricsFromBooks(dashboardBooks);
}
request.setAttribute("dashboardMetrics", metrics);
request.getRequestDispatcher(DASHBOARD_JSP).forward(request, response); request.getRequestDispatcher(DASHBOARD_JSP).forward(request, response);
} }
private AuthenticatedUser currentUser(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Object value = session == null ? null : session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
}
private boolean isStaff(AuthenticatedUser user) {
return user != null && (user.getRole() == Role.ADMINISTRATOR || user.getRole() == Role.LIBRARIAN);
}
private <T> List<T> listOrEmpty(List<T> values) {
return values == null ? Collections.emptyList() : values;
}
private List<DashboardMetric> metricsFromReport(ReportCenter reportCenter, Integer readerTotal) {
if (reportCenter == null) {
return Collections.emptyList();
}
InventorySummary inventory = reportCenter.getInventorySummary();
BorrowingSummary borrowing = reportCenter.getBorrowingSummary();
List<DashboardMetric> metrics = new ArrayList<>();
metrics.add(new DashboardMetric("馆藏总册", valueOf(inventory, MetricField.TOTAL_COPIES), "", "来自报表中心"));
metrics.add(new DashboardMetric("当前借出", valueOf(borrowing, MetricField.ACTIVE_LOANS), "", "实时借阅记录"));
metrics.add(new DashboardMetric("逾期借阅", valueOf(borrowing, MetricField.OVERDUE_LOANS), "", "需跟进记录"));
if (readerTotal == null) {
metrics.add(new DashboardMetric("可借册数", valueOf(inventory, MetricField.AVAILABLE_COPIES),
"", "馆藏可借库存"));
} else {
metrics.add(new DashboardMetric("读者总数", readerTotal, "", "实时读者档案"));
}
return metrics;
}
private List<DashboardMetric> metricsFromBooks(List<Book> books) {
int totalTitles = 0;
int totalCopies = 0;
int availableCopies = 0;
int unavailableOrEmptyTitles = 0;
for (Book book : books) {
totalTitles++;
totalCopies += book.getTotalCopies();
availableCopies += book.getAvailableCopies();
if (book.getStatus() != BookStatus.AVAILABLE || book.getAvailableCopies() <= 0) {
unavailableOrEmptyTitles++;
}
}
List<DashboardMetric> metrics = new ArrayList<>();
metrics.add(new DashboardMetric("图书种类", totalTitles, "", "来自馆藏检索"));
metrics.add(new DashboardMetric("馆藏总册", totalCopies, "", "来自馆藏检索"));
metrics.add(new DashboardMetric("可借册数", availableCopies, "", "来自馆藏检索"));
metrics.add(new DashboardMetric("需关注馆藏", unavailableOrEmptyTitles, "", "不可借或无库存"));
return metrics;
}
private int valueOf(InventorySummary summary, MetricField field) {
if (summary == null) {
return 0;
}
switch (field) {
case TOTAL_COPIES:
return summary.getTotalCopies();
case AVAILABLE_COPIES:
return summary.getAvailableCopies();
default:
return 0;
}
}
private int valueOf(BorrowingSummary summary, MetricField field) {
if (summary == null) {
return 0;
}
switch (field) {
case ACTIVE_LOANS:
return summary.getActiveLoans();
case OVERDUE_LOANS:
return summary.getOverdueLoans();
default:
return 0;
}
}
private void setErrorMessage(HttpServletRequest request, String message) {
if (message != null && !message.isEmpty() && request.getAttribute("errorMessage") == null) {
request.setAttribute("errorMessage", message);
}
}
private enum MetricField {
TOTAL_COPIES,
AVAILABLE_COPIES,
ACTIVE_LOANS,
OVERDUE_LOANS
}
public static final class DashboardMetric {
private final String label;
private final int value;
private final String unit;
private final String note;
private DashboardMetric(String label, int value, String unit, String note) {
this.label = label;
this.value = value;
this.unit = unit;
this.note = note;
}
public String getLabel() {
return label;
}
public int getValue() {
return value;
}
public String getUnit() {
return unit;
}
public String getNote() {
return note;
}
}
} }
+30 -8
View File
@@ -179,23 +179,33 @@ INSERT IGNORE INTO role_permissions (role_code, permission_code) VALUES
('reader', 'view_catalog'), ('reader', 'view_catalog'),
('reader', 'borrow_books'); ('reader', 'borrow_books');
-- Demo accounts for local scaffold verification only. Change or remove them -- Demo accounts for local scaffold verification only:
-- before using a non-local database. -- admin/admin123, librarian/librarian123, reader/reader123.
-- Change or remove them before using a non-local database.
INSERT IGNORE INTO users (username, password_hash, display_name, role_code, active) VALUES INSERT IGNORE INTO users (username, password_hash, display_name, role_code, active) VALUES
('admin', 'pbkdf2_sha256$60000$bXpoLWFkbWluLWRlbW8tc2FsdA==$RwBCvhf3Wsc0jemnHlir4mdNZF4ZhHjrfHx/b1Bera0=', 'System Administrator', 'administrator', 1), ('admin', 'pbkdf2_sha256$60000$Ren1B30RDysysnApRiFVaQ==$1XwzMHaALqC7dKffwjbQkilBedfAuiMOXbR/xTMr5+Y=', 'System Administrator', 'administrator', 1),
('librarian', 'pbkdf2_sha256$60000$bXpoLWxpYnJhcmlhbi1kZW1vLXNhbHQ=$StIdJGDRIiF4aCr+qKuwvob5sL3+6j1caF2sQNqFi78=', 'Library Staff', 'librarian', 1), ('librarian', 'pbkdf2_sha256$60000$PV/DJwZlMRm8vy0lKMAM4g==$+Aijfop3YoPp6HTePN5r4wG8N3qgxJE+yZHkTfzfbaw=', 'Library Staff', 'librarian', 1),
('reader', 'pbkdf2_sha256$60000$bXpoLXJlYWRlci1kZW1vLXNhbHQ=$iaiZPGhaIQ+2R2o9UQRj6wsrmYSJ4efqS3jCzM/XU7g=', 'Demo Reader', 'reader', 1); ('reader', 'pbkdf2_sha256$60000$wBzxTIT4ep79hgEzYDV9aQ==$w3oO5iSKRSfG4++b4558yiTHy6Tz9BB2+wuV9UOAKhs=', 'Demo Reader', 'reader', 1);
INSERT IGNORE INTO readers (reader_identifier, user_id, full_name, phone, email, status, max_borrow_count) VALUES INSERT IGNORE INTO readers (reader_identifier, user_id, full_name, phone, email, status, max_borrow_count) VALUES
('RD-0001', (SELECT id FROM users WHERE username = 'reader'), 'Demo Reader', '13800000000', ('RD-0001', (SELECT id FROM users WHERE username = 'reader'), 'Demo Reader', '13800000000',
'reader@example.com', 'active', 5), 'reader@example.com', 'active', 5),
('RD-0002', NULL, 'Suspended Reader', '13900000000', 'suspended.reader@example.com', 'suspended', 3); ('RD-0002', NULL, 'Suspended Reader', '13900000000', 'suspended.reader@example.com', 'suspended', 3),
('RD-0101', NULL, '张晓雨', '13600010001', 'zhang.xiaoyu@example.com', 'active', 6),
('RD-0102', NULL, '李明远', '13600010002', 'li.mingyuan@example.com', 'active', 5),
('RD-0103', NULL, '王思涵', '13600010003', 'wang.sihan@example.com', 'active', 4),
('RD-0104', NULL, '赵晨', '13600010004', 'zhao.chen@example.com', 'suspended', 3);
INSERT INTO book_categories (name, description) VALUES INSERT INTO book_categories (name, description) VALUES
('Computer Science', 'Programming, software engineering, and systems books'), ('Computer Science', 'Programming, software engineering, and systems books'),
('Literature', 'Classic and modern literature'), ('Literature', 'Classic and modern literature'),
('History', 'World and regional history'), ('History', 'World and regional history'),
('Science', 'Natural science and popular science') ('Science', 'Natural science and popular science'),
('中国文学', '中国现当代文学、经典小说和散文作品'),
('计算机技术', '程序设计、软件工程、数据库和信息技术图书'),
('历史文化', '中国历史、世界历史和文化研究读物'),
('自然科学', '数学、物理、生命科学和科普读物'),
('社会科学', '社会学、管理学和公共事务读物')
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
description = VALUES(description); description = VALUES(description);
@@ -207,4 +217,16 @@ INSERT IGNORE INTO books (book_identifier, title, author, category_id, total_cop
('BK-0003', 'Pride and Prejudice', 'Jane Austen', ('BK-0003', 'Pride and Prejudice', 'Jane Austen',
(SELECT id FROM book_categories WHERE name = 'Literature'), 3, 3, 'available'), (SELECT id FROM book_categories WHERE name = 'Literature'), 3, 3, 'available'),
('BK-0004', 'A Brief History of Time', 'Stephen Hawking', ('BK-0004', 'A Brief History of Time', 'Stephen Hawking',
(SELECT id FROM book_categories WHERE name = 'Science'), 2, 1, 'available'); (SELECT id FROM book_categories WHERE name = 'Science'), 2, 1, 'available'),
('BK-0101', '活着', '余华',
(SELECT id FROM book_categories WHERE name = '中国文学'), 6, 5, 'available'),
('BK-0102', '平凡的世界', '路遥',
(SELECT id FROM book_categories WHERE name = '中国文学'), 5, 5, 'available'),
('BK-0103', '深入理解Java虚拟机', '周志明',
(SELECT id FROM book_categories WHERE name = '计算机技术'), 4, 3, 'available'),
('BK-0104', '中国通史', '吕思勉',
(SELECT id FROM book_categories WHERE name = '历史文化'), 3, 3, 'available'),
('BK-0105', '乡土中国', '费孝通',
(SELECT id FROM book_categories WHERE name = '社会科学'), 4, 4, 'available'),
('BK-0106', '科学史十五讲', '江晓原',
(SELECT id FROM book_categories WHERE name = '自然科学'), 3, 2, 'available');
@@ -6,7 +6,7 @@
<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>用户账户管理 - MZH 图书馆</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-visual-shell">
</head> </head>
<body> <body>
@@ -14,11 +14,13 @@
<main class="page-shell"> <main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-users-title"> <section class="dashboard-hero catalog-hero" aria-labelledby="manage-users-title">
<div> <div>
<p class="eyebrow">系统管理</p> <p class="eyebrow">系统账户</p>
<h1 id="manage-users-title">管理用户</h1> <h1 id="manage-users-title">用户账户与角色</h1>
<p>创建、更新、停用和查看管理员、馆员与读者账户。</p> <p>维护登录账户、角色、密码和启用状态;读者联系方式、借阅上限和资格请在读者管理中处理。</p>
</div>
<div class="hero-actions">
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">新增用户账户</a>
</div> </div>
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">新增用户</a>
</section> </section>
<c:if test="${not empty successMessage}"> <c:if test="${not empty successMessage}">
@@ -32,7 +34,7 @@
</div> </div>
</c:if> </c:if>
<section class="toolbar-panel" aria-label="用户管理检索"> <section class="toolbar-panel" aria-label="用户账户检索">
<form class="search-form" action="${pageContext.request.contextPath}/admin/users" method="get"> <form class="search-form" action="${pageContext.request.contextPath}/admin/users" method="get">
<div class="search-field"> <div class="search-field">
<label for="keyword">关键词</label> <label for="keyword">关键词</label>
+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>
@@ -13,9 +13,11 @@
<%@ include file="/WEB-INF/jsp/common/header.jspf" %> <%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell"> <main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="catalog-title"> <section class="dashboard-hero catalog-hero" aria-labelledby="catalog-title">
<p class="eyebrow">馆藏</p> <div>
<h1 id="catalog-title">馆藏检索</h1> <p class="eyebrow">馆藏</p>
<p>按图书编号、书名、作者或分类检索馆藏。</p> <h1 id="catalog-title">馆藏检索</h1>
<p>按图书编号、书名、作者或分类检索馆藏。</p>
</div>
</section> </section>
<c:if test="${not empty errorMessage}"> <c:if test="${not empty errorMessage}">
@@ -58,9 +60,6 @@
<button class="button button-primary" type="submit">检索</button> <button class="button button-primary" type="submit">检索</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">清空</a> <a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">清空</a>
<c:if test="${canManageBooks}">
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">管理图书</a>
</c:if>
</form> </form>
</section> </section>
@@ -19,7 +19,6 @@
</div> </div>
<div class="hero-actions"> <div class="hero-actions">
<a class="button button-primary" href="${pageContext.request.contextPath}/book-categories/new">新增分类</a> <a class="button button-primary" href="${pageContext.request.contextPath}/book-categories/new">新增分类</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">管理图书</a>
</div> </div>
</section> </section>
+5 -6
View File
@@ -13,12 +13,13 @@
<%@ include file="/WEB-INF/jsp/common/header.jspf" %> <%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell"> <main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-title"> <section class="dashboard-hero catalog-hero" aria-labelledby="manage-title">
<p class="eyebrow">图书管理</p> <div>
<h1 id="manage-title">管理图书</h1> <p class="eyebrow">图书管理</p>
<p>创建、更新、删除和查看馆藏记录的库存信息。</p> <h1 id="manage-title">管理图书</h1>
<p>创建、更新、删除和查看馆藏记录的库存信息。</p>
</div>
<div class="hero-actions"> <div class="hero-actions">
<a class="button button-primary" href="${pageContext.request.contextPath}/books/new">新增图书</a> <a class="button button-primary" href="${pageContext.request.contextPath}/books/new">新增图书</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">分类</a>
</div> </div>
</section> </section>
@@ -67,8 +68,6 @@
<button class="button button-primary" type="submit">检索</button> <button class="button button-primary" type="submit">检索</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">清空</a> <a class="button button-secondary" href="${pageContext.request.contextPath}/books">清空</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">查看馆藏</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">分类</a>
</form> </form>
</section> </section>
+31 -81
View File
@@ -4,105 +4,63 @@
<header class="app-header ${not empty sessionScope.authenticatedUser ? 'app-header-auth' : 'app-header-public'}"> <header class="app-header ${not empty sessionScope.authenticatedUser ? 'app-header-auth' : 'app-header-public'}">
<c:choose> <c:choose>
<c:when test="${not empty sessionScope.authenticatedUser}"> <c:when test="${not empty sessionScope.authenticatedUser}">
<c:set var="currentUri" value="${pageContext.request.requestURI}" /> <c:set var="currentPath" value="${requestScope['javax.servlet.forward.servlet_path']}" />
<c:if test="${empty currentPath}">
<c:set var="currentPath" value="${pageContext.request.servletPath}" />
</c:if>
<aside class="app-sidebar" aria-label="主导航"> <aside class="app-sidebar" aria-label="主导航">
<a class="sidebar-brand" href="${pageContext.request.contextPath}/dashboard"> <a class="sidebar-brand" href="${pageContext.request.contextPath}/dashboard">
<span class="brand-mark" aria-hidden="true">书</span>
<span class="brand-text">图书管理系统</span> <span class="brand-text">图书管理系统</span>
</a> </a>
<section class="role-workbench" aria-label="角色工作台">
<p class="sidebar-section-title">角色工作台</p>
<c:if test="${sessionScope.userRole == 'administrator'}">
<a class="role-chip role-chip-admin" href="${pageContext.request.contextPath}/admin/home">
<span class="role-chip-icon" aria-hidden="true">管</span>
<span class="role-chip-copy">
<strong>管理员</strong>
<small>系统管理</small>
</span>
</a>
</c:if>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<a class="role-chip role-chip-librarian" href="${pageContext.request.contextPath}/librarian/home">
<span class="role-chip-icon" aria-hidden="true">馆</span>
<span class="role-chip-copy">
<strong>馆员</strong>
<small>流通工作</small>
</span>
</a>
</c:if>
<c:if test="${sessionScope.userRole == 'reader'}">
<a class="role-chip role-chip-reader" href="${pageContext.request.contextPath}/reader/home">
<span class="role-chip-icon" aria-hidden="true">读</span>
<span class="role-chip-copy">
<strong>读者</strong>
<small>自助服务</small>
</span>
</a>
</c:if>
</section>
<nav class="side-nav" aria-label="模块导航"> <nav class="side-nav" aria-label="模块导航">
<a class="side-nav-link ${fn:contains(currentUri, '/dashboard') ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/dashboard">
<span class="nav-icon" aria-hidden="true">台</span>
<span class="nav-text">工作台</span>
</a>
<a class="side-nav-link ${fn:contains(currentUri, '/catalog') ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/catalog">
<span class="nav-icon" aria-hidden="true">搜</span>
<span class="nav-text">馆藏检索</span>
</a>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}"> <c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<a class="side-nav-link ${fn:contains(currentUri, '/books') ? 'is-active' : ''}" <a class="side-nav-link ${currentPath == '/reports' ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/books">
<span class="nav-icon" aria-hidden="true">书</span>
<span class="nav-text">图书管理</span>
</a>
<a class="side-nav-link ${fn:contains(currentUri, '/book-categories') ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/book-categories">
<span class="nav-icon" aria-hidden="true">类</span>
<span class="nav-text">图书分类管理</span>
</a>
<a class="side-nav-link ${fn:contains(currentUri, '/readers') ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/readers">
<span class="nav-icon" aria-hidden="true">人</span>
<span class="nav-text">读者管理</span>
</a>
<a class="side-nav-link ${fn:contains(currentUri, '/borrowing') ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/borrowing">
<span class="nav-icon" aria-hidden="true">借</span>
<span class="nav-text">借阅流通</span>
</a>
<a class="side-nav-link ${fn:contains(currentUri, '/reports') ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/reports"> href="${pageContext.request.contextPath}/reports">
<span class="nav-icon" aria-hidden="true">报</span>
<span class="nav-text">报表中心</span> <span class="nav-text">报表中心</span>
</a> </a>
</c:if> </c:if>
<a class="side-nav-link ${currentPath == '/catalog' ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/catalog">
<span class="nav-text">馆藏检索</span>
</a>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<a class="side-nav-link ${(currentPath == '/books' or fn:startsWith(currentPath, '/books/')) ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/books">
<span class="nav-text">图书管理</span>
</a>
<a class="side-nav-link ${(currentPath == '/book-categories' or fn:startsWith(currentPath, '/book-categories/')) ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/book-categories">
<span class="nav-text">图书分类管理</span>
</a>
<a class="side-nav-link ${(currentPath == '/readers' or fn:startsWith(currentPath, '/readers/')) ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/readers">
<span class="nav-text">读者档案</span>
</a>
<a class="side-nav-link ${(currentPath == '/borrowing' or fn:startsWith(currentPath, '/borrowing/')) ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/borrowing">
<span class="nav-text">借阅流通</span>
</a>
</c:if>
<c:if test="${sessionScope.userRole == 'reader'}"> <c:if test="${sessionScope.userRole == 'reader'}">
<a class="side-nav-link ${fn:contains(currentUri, '/reader/loans') ? 'is-active' : ''}" <a class="side-nav-link ${(currentPath == '/reader/loans' or fn:startsWith(currentPath, '/reader/loans/')) ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/reader/loans"> href="${pageContext.request.contextPath}/reader/loans">
<span class="nav-icon" aria-hidden="true">历</span>
<span class="nav-text">读者借阅历史</span> <span class="nav-text">读者借阅历史</span>
</a> </a>
</c:if> </c:if>
<c:if test="${sessionScope.userRole == 'administrator'}"> <c:if test="${sessionScope.userRole == 'administrator'}">
<a class="side-nav-link ${fn:contains(currentUri, '/admin/users') ? 'is-active' : ''}" <a class="side-nav-link ${(currentPath == '/admin/users' or fn:startsWith(currentPath, '/admin/users/')) ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/admin/users"> href="${pageContext.request.contextPath}/admin/users">
<span class="nav-icon" aria-hidden="true">户</span> <span class="nav-text">用户账户</span>
<span class="nav-text">用户管理</span>
</a> </a>
<a class="side-nav-link ${fn:contains(currentUri, '/admin/system-logs') ? 'is-active' : ''}" <a class="side-nav-link ${(currentPath == '/admin/system-logs' or fn:startsWith(currentPath, '/admin/system-logs/')) ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/admin/system-logs"> href="${pageContext.request.contextPath}/admin/system-logs">
<span class="nav-icon" aria-hidden="true">志</span>
<span class="nav-text">系统日志</span> <span class="nav-text">系统日志</span>
</a> </a>
</c:if> </c:if>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<span class="sidebar-menu-dot" aria-hidden="true">≡</span>
<a href="${pageContext.request.contextPath}/logout">退出登录</a> <a href="${pageContext.request.contextPath}/logout">退出登录</a>
</div> </div>
</aside> </aside>
@@ -115,15 +73,7 @@
<button type="submit" aria-label="搜索">搜</button> <button type="submit" aria-label="搜索">搜</button>
</form> </form>
<div class="topbar-actions"> <div class="topbar-actions">
<span class="notification-dot" aria-label="通知">!</span>
<span class="user-summary"> <span class="user-summary">
<span class="avatar" aria-hidden="true">
<c:choose>
<c:when test="${sessionScope.userRole == 'administrator'}">管</c:when>
<c:when test="${sessionScope.userRole == 'librarian'}">馆</c:when>
<c:otherwise>读</c:otherwise>
</c:choose>
</span>
<span class="user-meta"> <span class="user-meta">
<span class="user-pill"> <span class="user-pill">
<c:out value="${sessionScope.authenticatedUser.displayName}" /> <c:out value="${sessionScope.authenticatedUser.displayName}" />
+171 -205
View File
@@ -23,7 +23,6 @@
<c:otherwise>读者工作台</c:otherwise> <c:otherwise>读者工作台</c:otherwise>
</c:choose> </c:choose>
</h1> </h1>
<p>登录后进入 Dashboard,会话仅保存安全的 AuthenticatedUser 快照、角色代码与权限代码集合。</p>
</div> </div>
<div class="welcome-user"> <div class="welcome-user">
<span>当前登录</span> <span>当前登录</span>
@@ -31,39 +30,36 @@
</div> </div>
</section> </section>
<c:if test="${not empty errorMessage}">
<p class="message message-error"><c:out value="${errorMessage}" /></p>
</c:if>
<section class="dashboard-metrics" aria-label="核心指标"> <section class="dashboard-metrics" aria-label="核心指标">
<article class="metric-card"> <c:choose>
<span class="metric-icon metric-blue" aria-hidden="true">书</span> <c:when test="${empty dashboardMetrics}">
<div> <article class="metric-card metric-card-empty">
<h2>馆藏总量</h2> <div>
<p class="metric-value">12,586 <small>册</small></p> <h2>核心指标</h2>
<p class="metric-trend trend-up">较上月 ↑ 5.2%</p> <p class="metric-value">--</p>
</div> <p class="metric-trend">暂无可展示的实时数据。</p>
</article> </div>
<article class="metric-card"> </article>
<span class="metric-icon metric-green" aria-hidden="true">借</span> </c:when>
<div> <c:otherwise>
<h2>在借数量</h2> <c:forEach var="metric" items="${dashboardMetrics}">
<p class="metric-value">1,258 <small>册</small></p> <article class="metric-card">
<p class="metric-trend trend-up">较上月 ↑ 3.1%</p> <div>
</div> <h2><c:out value="${metric.label}" /></h2>
</article> <p class="metric-value">
<article class="metric-card"> <c:out value="${metric.value}" />
<span class="metric-icon metric-orange" aria-hidden="true">期</span> <small><c:out value="${metric.unit}" /></small>
<div> </p>
<h2>逾期数量</h2> <p class="metric-trend"><c:out value="${metric.note}" /></p>
<p class="metric-value">87 <small>册</small></p> </div>
<p class="metric-trend trend-down">较上月 ↓ 12.4%</p> </article>
</div> </c:forEach>
</article> </c:otherwise>
<article class="metric-card"> </c:choose>
<span class="metric-icon metric-purple" aria-hidden="true">者</span>
<div>
<h2>读者总数</h2>
<p class="metric-value">3,682 <small>人</small></p>
<p class="metric-trend trend-up">较上月 ↑ 4.8%</p>
</div>
</article>
</section> </section>
<section class="dashboard-grid" aria-label="检索与排行"> <section class="dashboard-grid" aria-label="检索与排行">
@@ -85,7 +81,10 @@
<div class="search-field"> <div class="search-field">
<label for="dashCategory">分类</label> <label for="dashCategory">分类</label>
<select id="dashCategory" name="categoryId"> <select id="dashCategory" name="categoryId">
<option value="">请选择分类</option> <option value="">全部分类</option>
<c:forEach var="category" items="${categories}">
<option value="${category.id}"><c:out value="${category.name}" /></option>
</c:forEach>
</select> </select>
</div> </div>
<div class="dashboard-form-actions"> <div class="dashboard-form-actions">
@@ -100,117 +99,116 @@
<h2>热门图书排行</h2> <h2>热门图书排行</h2>
<span>借阅次数TOP10</span> <span>借阅次数TOP10</span>
</div> </div>
<div class="rank-chart" aria-label="热门图书排行柱状图"> <c:choose>
<div class="rank-item"><span class="rank-value">230</span><span class="rank-bar" style="--bar-height: 92%;"></span><small>活着</small></div> <c:when test="${empty reportCenter or empty reportCenter.popularBooks}">
<div class="rank-item"><span class="rank-value">198</span><span class="rank-bar" style="--bar-height: 79%;"></span><small>三体</small></div> <p class="empty-state">暂无热门排行数据。</p>
<div class="rank-item"><span class="rank-value">175</span><span class="rank-bar" style="--bar-height: 70%;"></span><small>百年孤独</small></div> </c:when>
<div class="rank-item"><span class="rank-value">164</span><span class="rank-bar" style="--bar-height: 66%;"></span><small>围城</small></div> <c:otherwise>
<div class="rank-item"><span class="rank-value">150</span><span class="rank-bar" style="--bar-height: 60%;"></span><small>平凡的世界</small></div> <c:set var="rankingMax" value="${reportCenter.popularBooks[0].borrowCount}" />
<div class="rank-item"><span class="rank-value">138</span><span class="rank-bar" style="--bar-height: 55%;"></span><small>解忧杂货店</small></div> <div class="rank-chart" aria-label="热门图书排行柱状图">
<div class="rank-item"><span class="rank-value">120</span><span class="rank-bar" style="--bar-height: 48%;"></span><small>红楼梦</small></div> <c:forEach var="row" items="${reportCenter.popularBooks}" end="9">
<div class="rank-item"><span class="rank-value">112</span><span class="rank-bar" style="--bar-height: 45%;"></span><small>白夜行</small></div> <div class="rank-item">
<div class="rank-item"><span class="rank-value">98</span><span class="rank-bar" style="--bar-height: 39%;"></span><small>追风筝的人</small></div> <span class="rank-value"><c:out value="${row.borrowCount}" /></span>
<div class="rank-item"><span class="rank-value">85</span><span class="rank-bar" style="--bar-height: 34%;"></span><small>小王子</small></div> <span class="rank-bar"
</div> style="--bar-height: ${rankingMax > 0 ? row.borrowCount * 100 / rankingMax : 0}%;"></span>
<small><c:out value="${row.title}" /></small>
</div>
</c:forEach>
</div>
</c:otherwise>
</c:choose>
</article> </article>
</section> </section>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}"> <c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<section class="dashboard-table-grid" aria-label="业务表格"> <section class="dashboard-table-grid" aria-label="业务表格">
<article class="dashboard-panel table-panel-compact table-panel-wide"> <article class="dashboard-panel table-panel-compact table-panel-wide">
<h2>借阅流通 <span>最新记录</span></h2> <h2>借阅流通 <span>实时记录</span></h2>
<div class="table-scroll"> <c:choose>
<table class="data-table dashboard-data-table"> <c:when test="${empty dashboardBorrowRecords}">
<thead> <p class="empty-state">暂无借阅流通记录。</p>
<tr> </c:when>
<th scope="col">流水号</th> <c:otherwise>
<th scope="col">读者姓名</th> <div class="table-scroll">
<th scope="col">图书编号</th> <table class="data-table dashboard-data-table">
<th scope="col">书名</th> <thead>
<th scope="col">借阅日期</th> <tr>
<th scope="col">应还日期</th> <th scope="col">流水号</th>
<th scope="col">状态</th> <th scope="col">读者姓名</th>
<th scope="col">库存联动</th> <th scope="col">图书编号</th>
</tr> <th scope="col">书名</th>
</thead> <th scope="col">借阅日期</th>
<tbody> <th scope="col">应还日期</th>
<tr> <th scope="col">状态</th>
<td>L20240521001</td> <th scope="col">库存联动</th>
<td>张晓明</td> </tr>
<td>B001245</td> </thead>
<td>活着</td> <tbody>
<td>2024-05-21</td> <c:forEach var="record" items="${dashboardBorrowRecords}" end="4">
<td>2024-06-04</td> <tr>
<td><span class="status-pill status-active">在借</span></td> <td>#<c:out value="${record.id}" /></td>
<td><span class="stock-plus">库存-1</span></td> <td><c:out value="${record.readerName}" /></td>
</tr> <td><c:out value="${record.bookIdentifier}" /></td>
<tr> <td><c:out value="${record.bookTitle}" /></td>
<td>L20240521002</td> <td><c:out value="${record.borrowedAtText}" /></td>
<td>李华</td> <td><c:out value="${record.dueAtText}" /></td>
<td>B001026</td> <td>
<td>三体</td> <span class="status-pill status-${record.displayStatusCode}">
<td>2024-05-20</td> <c:out value="${record.displayStatusName}" />
<td>2024-06-03</td> </span>
<td><span class="status-pill status-active">在借</span></td> </td>
<td><span class="stock-plus">库存-1</span></td> <td>
</tr> <c:choose>
<tr> <c:when test="${record.displayStatusCode == 'returned'}">
<td>L20240521003</td> <span class="stock-return">库存已返还</span>
<td>王丽</td> </c:when>
<td>B002031</td> <c:otherwise>
<td>百年孤独</td> <span class="stock-plus">借出占用</span>
<td>2024-05-18</td> </c:otherwise>
<td>2024-06-01</td> </c:choose>
<td><span class="status-pill status-returned">已归还</span></td> </td>
<td><span class="stock-return">库存+1</span></td> </tr>
</tr> </c:forEach>
<tr> </tbody>
<td>L20240521004</td> </table>
<td>陈强</td> </div>
<td>B001895</td> </c:otherwise>
<td>围城</td> </c:choose>
<td>2024-05-10</td>
<td>2024-05-24</td>
<td><span class="status-pill status-overdue">逾期</span></td>
<td><span class="stock-plus">库存-1</span></td>
</tr>
<tr>
<td>L20240521005</td>
<td>刘洋</td>
<td>B002119</td>
<td>解忧杂货店</td>
<td>2024-05-12</td>
<td>2024-05-26</td>
<td><span class="status-pill status-overdue">逾期</span></td>
<td><span class="stock-plus">库存-1</span></td>
</tr>
</tbody>
</table>
</div>
</article> </article>
<article class="dashboard-panel table-panel-compact"> <article class="dashboard-panel table-panel-compact">
<h2>逾期列表 <span>待处理</span></h2> <h2>逾期列表 <span>待处理</span></h2>
<div class="table-scroll"> <c:choose>
<table class="data-table dashboard-data-table overdue-table"> <c:when test="${empty reportCenter or empty reportCenter.overdueRows}">
<thead> <p class="empty-state">当前没有逾期未还的借阅记录。</p>
<tr> </c:when>
<th scope="col">读者姓名</th> <c:otherwise>
<th scope="col">图书编号</th> <div class="table-scroll">
<th scope="col">书名</th> <table class="data-table dashboard-data-table overdue-table">
<th scope="col">应还日期</th> <thead>
<th scope="col">逾期天数</th> <tr>
</tr> <th scope="col">读者姓名</th>
</thead> <th scope="col">图书编号</th>
<tbody> <th scope="col">书名</th>
<tr><td>陈强</td><td>B001895</td><td>围城</td><td>2024-05-24</td><td><span class="overdue-days">7天</span></td></tr> <th scope="col">应还日期</th>
<tr><td>赵敏</td><td>B001122</td><td>平凡的世界</td><td>2024-05-20</td><td><span class="overdue-days">11天</span></td></tr> <th scope="col">逾期天数</th>
<tr><td>孙涛</td><td>B002003</td><td>红楼梦</td><td>2024-05-18</td><td><span class="overdue-days">13天</span></td></tr> </tr>
<tr><td>周雨</td><td>B000987</td><td>追风筝的人</td><td>2024-05-17</td><td><span class="overdue-days">14天</span></td></tr> </thead>
<tr><td>吴迪</td><td>B001776</td><td>白夜行</td><td>2024-05-15</td><td><span class="overdue-days">16天</span></td></tr> <tbody>
</tbody> <c:forEach var="row" items="${reportCenter.overdueRows}" end="4">
</table> <tr>
</div> <td><c:out value="${row.readerName}" /></td>
<td><c:out value="${row.bookIdentifier}" /></td>
<td><c:out value="${row.bookTitle}" /></td>
<td><c:out value="${row.dueAtText}" /></td>
<td><span class="overdue-days"><c:out value="${row.overdueDays}" />天</span></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</c:otherwise>
</c:choose>
</article> </article>
<article class="dashboard-panel table-panel-compact table-panel-wide"> <article class="dashboard-panel table-panel-compact table-panel-wide">
@@ -218,87 +216,55 @@
<h2>图书管理 <span>馆藏列表</span></h2> <h2>图书管理 <span>馆藏列表</span></h2>
<a href="${pageContext.request.contextPath}/books">进入管理</a> <a href="${pageContext.request.contextPath}/books">进入管理</a>
</div> </div>
<div class="table-scroll"> <c:choose>
<table class="data-table dashboard-data-table"> <c:when test="${empty dashboardBooks}">
<thead> <p class="empty-state">暂无馆藏图书记录。</p>
<tr> </c:when>
<th scope="col">图书编号</th> <c:otherwise>
<th scope="col">书名</th> <div class="table-scroll">
<th scope="col">作者</th> <table class="data-table dashboard-data-table">
<th scope="col">分类</th> <thead>
<th scope="col">出版日期</th> <tr>
<th scope="col">库存状态</th> <th scope="col">图书编号</th>
<th scope="col">馆藏地</th> <th scope="col">书名</th>
<th scope="col">作</th> <th scope="col">作</th>
</tr> <th scope="col">分类</th>
</thead> <th scope="col">库存状态</th>
<tbody> <th scope="col">操作</th>
<tr> </tr>
<td>B001245</td><td>活着</td><td>余华</td><td>文学 &gt; 小说</td><td>2012-08-01</td> </thead>
<td><span class="status-pill status-available">可借(15</span></td><td>二楼文学区</td> <tbody>
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td> <c:forEach var="book" items="${dashboardBooks}" end="4">
</tr> <tr>
<tr> <td><c:out value="${book.identifier}" /></td>
<td>B001026</td><td>三体</td><td>刘慈欣</td><td>文学 &gt; 科幻</td><td>2008-01-01</td> <td><c:out value="${book.title}" /></td>
<td><span class="status-pill status-available">可借(8</span></td><td>三楼科幻区</td> <td><c:out value="${book.author}" /></td>
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td> <td><c:out value="${book.categoryName}" /></td>
</tr> <td>
<tr> <span class="status-pill status-${book.status.code}">
<td>B002031</td><td>百年孤独</td><td>加西亚·马尔克斯</td><td>文学 &gt; 外国文学</td><td>2011-06-01</td> <c:out value="${book.status.displayName}" />
<td><span class="status-pill status-available">可借(6</span></td><td>二楼文学区</td> <c:out value="${book.availableCopies}" />/<c:out value="${book.totalCopies}" />
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td> </span>
</tr> </td>
<tr> <td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
<td>B001895</td><td>围城</td><td>钱钟书</td><td>文学 &gt; 小说</td><td>2008-05-01</td> </tr>
<td><span class="status-pill status-available">可借(4</span></td><td>二楼文学区</td> </c:forEach>
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td> </tbody>
</tr> </table>
<tr> </div>
<td>B002119</td><td>解忧杂货店</td><td>东野圭吾</td><td>文学 &gt; 小说</td><td>2014-07-01</td> </c:otherwise>
<td><span class="status-pill status-available">可借(10</span></td><td>二楼文学区</td> </c:choose>
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
</tr>
</tbody>
</table>
</div>
</article> </article>
<aside class="shortcut-grid" aria-label="快捷入口">
<a class="shortcut-card" href="${pageContext.request.contextPath}/readers">
<span class="shortcut-icon shortcut-blue" aria-hidden="true">者</span>
<strong>读者管理</strong>
<small>管理读者信息、证件办理与权限设置</small>
</a>
<a class="shortcut-card" href="${pageContext.request.contextPath}/reports">
<span class="shortcut-icon shortcut-green" aria-hidden="true">报</span>
<strong>报表中心</strong>
<small>生成各类统计报表,支持导出与分析</small>
</a>
<a class="shortcut-card" href="${pageContext.request.contextPath}/borrowing">
<span class="shortcut-icon shortcut-orange" aria-hidden="true">借</span>
<strong>借阅流通</strong>
<small>借书、还书、续借与逾期处理</small>
</a>
<c:if test="${sessionScope.userRole == 'administrator'}">
<a class="shortcut-card" href="${pageContext.request.contextPath}/admin/system-logs">
<span class="shortcut-icon shortcut-purple" aria-hidden="true">志</span>
<strong>系统日志</strong>
<small>系统操作日志与安全审计记录查询</small>
</a>
</c:if>
</aside>
</section> </section>
</c:if> </c:if>
<c:if test="${sessionScope.userRole == 'reader'}"> <c:if test="${sessionScope.userRole == 'reader'}">
<section class="shortcut-grid reader-shortcut-grid" aria-label="读者快捷入口"> <section class="shortcut-grid reader-shortcut-grid" aria-label="读者快捷入口">
<a class="shortcut-card" href="${pageContext.request.contextPath}/reader/loans"> <a class="shortcut-card" href="${pageContext.request.contextPath}/reader/loans">
<span class="shortcut-icon shortcut-blue" aria-hidden="true">历</span>
<strong>我的借阅</strong> <strong>我的借阅</strong>
<small>查看在借、已还、续借次数和逾期状态</small> <small>查看在借、已还、续借次数和逾期状态</small>
</a> </a>
<a class="shortcut-card" href="${pageContext.request.contextPath}/catalog"> <a class="shortcut-card" href="${pageContext.request.contextPath}/catalog">
<span class="shortcut-icon shortcut-green" aria-hidden="true">搜</span>
<strong>馆藏检索</strong> <strong>馆藏检索</strong>
<small>按书名、作者、分类或图书编号查找馆藏</small> <small>按书名、作者、分类或图书编号查找馆藏</small>
</a> </a>
+13 -9
View File
@@ -6,17 +6,21 @@
<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>读者档案 - MZH 图书馆</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-visual-shell">
</head> </head>
<body> <body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %> <%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell"> <main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-readers-title"> <section class="dashboard-hero catalog-hero" aria-labelledby="manage-readers-title">
<p class="eyebrow">读者管理</p> <div>
<h1 id="manage-readers-title">管理读者</h1> <p class="eyebrow">读者档案</p>
<p>创建、更新和查看读者资格及联系方式记录。</p> <h1 id="manage-readers-title">读者档案与借阅资格</h1>
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">新增读者</a> <p>维护读者资料、联系方式、借阅上限和借阅资格;登录账户、角色和启用状态请在用户管理中处理。</p>
</div>
<div class="hero-actions">
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">新增读者档案</a>
</div>
</section> </section>
<c:if test="${not empty successMessage}"> <c:if test="${not empty successMessage}">
@@ -30,7 +34,7 @@
</div> </div>
</c:if> </c:if>
<section class="toolbar-panel" aria-label="读者管理检索"> <section class="toolbar-panel" aria-label="读者档案检索">
<form class="search-form" action="${pageContext.request.contextPath}/readers" method="get"> <form class="search-form" action="${pageContext.request.contextPath}/readers" method="get">
<div class="search-field"> <div class="search-field">
<label for="identifier">读者编号</label> <label for="identifier">读者编号</label>
@@ -68,10 +72,10 @@
</section> </section>
<section class="table-panel" aria-labelledby="reader-results-title"> <section class="table-panel" aria-labelledby="reader-results-title">
<h2 id="reader-results-title">读者记录</h2> <h2 id="reader-results-title">读者档案</h2>
<c:choose> <c:choose>
<c:when test="${empty readers}"> <c:when test="${empty readers}">
<p class="empty-state">没有符合当前筛选条件的读者记录。</p> <p class="empty-state">没有符合当前筛选条件的读者档案。</p>
</c:when> </c:when>
<c:otherwise> <c:otherwise>
<div class="table-scroll"> <div class="table-scroll">
@@ -81,7 +85,7 @@
<th scope="col">读者编号</th> <th scope="col">读者编号</th>
<th scope="col">姓名</th> <th scope="col">姓名</th>
<th scope="col">联系方式</th> <th scope="col">联系方式</th>
<th scope="col">关联账户</th> <th scope="col">关联登录账户</th>
<th scope="col">借阅上限</th> <th scope="col">借阅上限</th>
<th scope="col">状态</th> <th scope="col">状态</th>
<th scope="col">操作</th> <th scope="col">操作</th>
@@ -17,7 +17,6 @@
<h1 id="reports-title">报表中心</h1> <h1 id="reports-title">报表中心</h1>
<p>查看馆藏库存、借阅状况、逾期借阅和热门图书。</p> <p>查看馆藏库存、借阅状况、逾期借阅和热门图书。</p>
</div> </div>
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">借阅记录</a>
</section> </section>
<c:if test="${not empty errorMessage}"> <c:if test="${not empty errorMessage}">
+285 -265
View File
@@ -41,6 +41,10 @@ body {
line-height: 1.45; line-height: 1.45;
} }
body:not(.auth-page) {
min-width: calc(var(--sidebar-width) + 320px);
}
a { a {
color: inherit; color: inherit;
} }
@@ -113,9 +117,8 @@ textarea {
.sidebar-brand { .sidebar-brand {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 11px;
min-height: 44px; min-height: 44px;
padding: 0 10px; padding: 0 12px;
color: #ffffff; color: #ffffff;
font-size: 17px; font-size: 17px;
font-weight: 800; font-weight: 800;
@@ -130,93 +133,6 @@ textarea {
white-space: nowrap; white-space: nowrap;
} }
.brand-mark {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 7px;
color: #102033;
background: #ffffff;
font-size: 15px;
}
.role-workbench {
display: grid;
gap: 8px;
margin-top: 22px;
padding: 0 0 16px;
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
}
.sidebar-section-title {
grid-column: 1 / -1;
margin: 0 0 10px;
padding: 0 12px;
color: #91a2bd;
font-size: 13px;
font-weight: 700;
}
.role-chip {
min-height: 44px;
display: grid;
grid-template-columns: 30px minmax(0, 1fr);
gap: 0 10px;
align-items: center;
padding: 9px 11px;
border-radius: 7px;
color: #ffffff;
text-decoration: none;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.16);
}
.role-chip-icon {
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: rgba(255, 255, 255, 0.24);
font-size: 13px;
font-weight: 800;
}
.role-chip-copy {
min-width: 0;
display: grid;
gap: 2px;
}
.role-chip strong {
overflow: hidden;
line-height: 1.1;
text-overflow: ellipsis;
white-space: nowrap;
}
.role-chip small {
overflow: hidden;
color: rgba(255, 255, 255, 0.82);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.role-chip-admin {
background: linear-gradient(135deg, #316cf4, #1f57d8);
}
.role-chip-librarian {
background: linear-gradient(135deg, #4db7ad, #278f87);
}
.role-chip-reader {
background: linear-gradient(135deg, #ffac48, #f08a24);
}
.side-nav { .side-nav {
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -226,29 +142,15 @@ textarea {
.side-nav-link { .side-nav-link {
min-height: 40px; min-height: 40px;
display: grid; display: grid;
grid-template-columns: 28px minmax(0, 1fr); grid-template-columns: minmax(0, 1fr);
align-items: center; align-items: center;
gap: 10px; padding: 10px 12px;
padding: 8px 10px;
border-radius: 7px; border-radius: 7px;
color: #c8d2df; color: #c8d2df;
line-height: 1.2; line-height: 1.2;
text-decoration: none; text-decoration: none;
} }
.side-nav-link .nav-icon {
width: 26px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
color: #9aa9bd;
background: rgba(255, 255, 255, 0.06);
font-size: 13px;
font-weight: 800;
}
.side-nav-link .nav-text { .side-nav-link .nav-text {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
@@ -263,12 +165,6 @@ textarea {
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
} }
.side-nav-link:hover .nav-icon,
.side-nav-link.is-active .nav-icon {
color: #ffffff;
background: rgba(255, 255, 255, 0.18);
}
.sidebar-footer { .sidebar-footer {
min-height: 34px; min-height: 34px;
display: flex; display: flex;
@@ -287,11 +183,6 @@ textarea {
text-decoration: none; text-decoration: none;
} }
.sidebar-menu-dot {
color: #91a2bd;
font-size: 20px;
}
.app-topbar { .app-topbar {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -373,47 +264,18 @@ textarea {
white-space: nowrap; white-space: nowrap;
} }
.notification-dot {
width: 24px;
height: 24px;
flex: 0 0 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid var(--color-border);
border-radius: 999px;
color: #ffffff;
background: #ef4444;
font-size: 12px;
font-weight: 800;
}
.user-summary { .user-summary {
min-width: 0; min-width: 0;
max-width: 240px; max-width: 240px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 4px 10px 4px 4px; padding: 8px 12px;
border: 1px solid var(--color-border); border: 1px solid var(--color-border);
border-radius: 999px; border-radius: 8px;
background: #f8fafc; background: #f8fafc;
} }
.avatar {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
color: #102033;
background: linear-gradient(135deg, #dbeafe, #ffffff);
font-weight: 800;
flex: 0 0 32px;
box-shadow: inset 0 0 0 1px #bfdbfe;
}
.user-meta { .user-meta {
min-width: 0; min-width: 0;
display: grid; display: grid;
@@ -440,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;
} }
@@ -495,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 {
@@ -527,7 +447,7 @@ h2 {
.login-form { .login-form {
display: grid; display: grid;
gap: 10px; gap: 18px;
} }
.login-form label, .login-form label,
@@ -539,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,
@@ -563,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,
@@ -582,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;
@@ -632,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 {
@@ -715,12 +775,14 @@ h2 {
flex: 1 1 230px; flex: 1 1 230px;
min-width: 0; min-width: 0;
min-height: 98px; min-height: 98px;
display: flex; display: block;
align-items: center;
gap: 18px;
padding: 18px 20px; padding: 18px 20px;
} }
.metric-card-empty {
grid-column: 1 / -1;
}
.metric-card > div { .metric-card > div {
min-width: 0; min-width: 0;
} }
@@ -731,35 +793,6 @@ h2 {
font-size: 13px; font-size: 13px;
} }
.metric-icon {
width: 52px;
height: 52px;
display: inline-flex;
flex: 0 0 52px;
align-items: center;
justify-content: center;
border-radius: 999px;
color: #ffffff;
font-size: 18px;
font-weight: 800;
}
.metric-blue {
background: linear-gradient(135deg, #4b83f3, #2869e8);
}
.metric-green {
background: linear-gradient(135deg, #5ccaae, #1f9d68);
}
.metric-orange {
background: linear-gradient(135deg, #ffb25c, #f08a24);
}
.metric-purple {
background: linear-gradient(135deg, #9187f1, #6f60e5);
}
.metric-value { .metric-value {
margin-bottom: 4px; margin-bottom: 4px;
font-size: 24px; font-size: 24px;
@@ -933,7 +966,7 @@ h2 {
.shortcut-card { .shortcut-card {
min-height: 100px; min-height: 100px;
display: grid; display: grid;
grid-template-columns: 46px 1fr 14px; grid-template-columns: minmax(0, 1fr) 14px;
grid-template-rows: auto auto; grid-template-rows: auto auto;
gap: 5px 13px; gap: 5px 13px;
align-items: center; align-items: center;
@@ -944,7 +977,7 @@ h2 {
.shortcut-card::after { .shortcut-card::after {
content: ""; content: "";
grid-row: 1 / 3; grid-row: 1 / 3;
grid-column: 3; grid-column: 2;
color: #94a3b8; color: #94a3b8;
font-size: 24px; font-size: 24px;
} }
@@ -959,37 +992,6 @@ h2 {
line-height: 1.35; line-height: 1.35;
} }
.shortcut-icon {
grid-row: 1 / 3;
width: 42px;
height: 42px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
font-weight: 900;
}
.shortcut-blue {
color: #2454cb;
background: #eaf1ff;
}
.shortcut-green {
color: #16825a;
background: #dcfce7;
}
.shortcut-orange {
color: #c46615;
background: #fff3df;
}
.shortcut-purple {
color: #6254d6;
background: #eeeaff;
}
.card-grid { .card-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
@@ -1280,60 +1282,27 @@ h2 {
} }
@media (max-width: 960px) { @media (max-width: 960px) {
.app-sidebar,
.app-topbar { .app-topbar {
position: static; grid-template-columns: minmax(0, 1fr) auto;
top: auto;
right: auto;
bottom: auto;
left: auto;
width: auto;
inset: auto;
}
.app-sidebar {
min-height: auto;
border-radius: 0;
}
.role-workbench {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
}
.side-nav {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sidebar-footer {
margin-top: 14px;
}
.app-topbar {
height: auto;
flex-direction: column;
grid-template-columns: minmax(0, 1fr);
align-items: stretch;
gap: 10px; gap: 10px;
padding: 14px 16px; padding: 0 14px;
}
.breadcrumb {
display: none;
} }
.topbar-actions { .topbar-actions {
justify-content: space-between; justify-content: flex-end;
} }
.user-summary { .user-summary {
max-width: none; max-width: 180px;
} }
.user-pill, .user-pill,
.role-label { .role-label {
max-width: none; max-width: 140px;
}
body:not(.auth-page) .page-shell {
width: min(1280px, calc(100% - 24px));
margin: 0 auto;
padding: 18px 0 40px;
} }
.global-search { .global-search {
@@ -1347,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();
}
});
}
}());