Compare commits

...

7 Commits

Author SHA1 Message Date
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
32 changed files with 1077 additions and 580 deletions
+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
@@ -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,6 @@
{"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."}
@@ -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,58 @@
# 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
* 报表中心页面不再显示跳转到借阅记录的右侧按钮。
* 馆藏检索页面不再显示跳转到管理图书的右侧按钮。
* 图书管理页面不再显示跳转到分类管理或馆藏检索的右侧按钮;保留新增图书入口。
* 分类管理页面不再显示跳转到管理图书的右侧按钮;保留新增分类入口。
* 读者档案页面不再显示跳转到管理登录账户的右侧按钮;保留新增读者档案入口。
* 用户账户与角色页面不再显示跳转到读者档案的右侧按钮;保留新增账户入口。
* 数据库初始化脚本加入中文图书分类、中文书名、中文作者和中文读者姓名。
* 本地演示账号仍能用于登录验证。
## Acceptance Criteria
* [x] 指定页面中的重复跨模块按钮被移除,侧边栏仍能导航到对应模块。
* [x] 页面内新增操作按钮未被误删。
* [x] `schema.sql` 包含多条中文图书数据和多条中文读者数据。
* [x] 中文演示数据使用 `utf8mb4` 兼容的文本,不引入新表或迁移机制。
* [x] 相关检查或可用的构建验证通过;若环境缺少 Maven,记录 fallback 验证。
## 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`.
* Data file: `src/main/resources/db/schema.sql`.
* Relevant specs: frontend JSP/component/state/quality guidelines and backend database/quality guidelines.
* Final verification: `git diff --check`, JSP scriptlet/SQL/JDBC scan, removed-link scan, and `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` passed.
* Spec update decision: no `.trellis/spec/` update needed because this task did not introduce new routes, APIs, tables, cross-layer contracts, or reusable implementation conventions.
@@ -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": "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,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": {}
}
+4 -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**: 12 - **Total Sessions**: 14
- **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` | ~474 | Active | | `journal-1.md` | ~540 | Active |
<!-- @@@/auto:active-documents --> <!-- @@@/auto:active-documents -->
--- ---
@@ -29,6 +29,8 @@
<!-- @@@auto:session-history --> <!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch | | # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------| |---|------|-------|---------|--------|
| 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` | | 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` |
+66
View File
@@ -472,3 +472,69 @@ Added safe login/database diagnostic logs, documented local demo credentials, up
### Next Steps ### Next Steps
- None - task complete - 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
@@ -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;
}
}
} }
+24 -3
View File
@@ -190,13 +190,22 @@ INSERT IGNORE INTO users (username, password_hash, display_name, role_code, acti
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);
@@ -208,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>
@@ -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}">
+25 -254
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;
@@ -715,12 +577,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 +595,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 +768,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 +779,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 +794,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 +1084,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 {