Compare commits
11 Commits
36db197e75
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a37d37945b | |||
| d1f32b9d52 | |||
| 44b72d3959 | |||
| 8535b4804b | |||
| acbd873fbc | |||
| e8c46311b9 | |||
| da610644d7 | |||
| d0e71f2aa9 | |||
| 0face72b8d | |||
| cc9636e48a | |||
| 0a386b81f9 |
@@ -18,8 +18,8 @@ the reusable UI units.
|
||||
sidebar, footer, pagination, and message banners.
|
||||
- Use `.jspf` includes for the current JSP presentation layer. The authenticated
|
||||
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
|
||||
navigation, global search, user display, and logout link.
|
||||
and owns the dark sidebar, top utility bar, module navigation, global search,
|
||||
user display, and logout link.
|
||||
- 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
|
||||
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
|
||||
inside `administrator or librarian`; reader-only links stay inside
|
||||
`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
|
||||
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
|
||||
reference exists.
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
Small JavaScript can improve interaction, such as confirm dialogs or local form
|
||||
|
||||
@@ -33,7 +33,11 @@ rendering.
|
||||
### 2. Signatures
|
||||
|
||||
- Login form: `POST /login`.
|
||||
- Request fields: `username`, `password`, and optional `redirect`.
|
||||
- Request fields consumed by `LoginServlet`: `username`, `password`, and
|
||||
optional `redirect`.
|
||||
- Presentation-only login controls may submit auxiliary fields such as
|
||||
`rememberUsername`; these must not participate in authentication or
|
||||
authorization unless the Servlet/service contract is deliberately changed.
|
||||
- Login JSP request attributes: `errorMessage`, `username`, and `redirect`.
|
||||
- Dashboard/role JSP session attributes: `authenticatedUser`, `userRole`, and
|
||||
`userPermissions`.
|
||||
@@ -47,6 +51,12 @@ rendering.
|
||||
attribute or session attribute.
|
||||
- `redirect` must be a same-application path beginning with one `/`; invalid
|
||||
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 may read safe session snapshots, but they must not call DAOs or inspect
|
||||
password hashes.
|
||||
@@ -67,10 +77,12 @@ rendering.
|
||||
|
||||
- Good: failed login keeps the escaped username and never redisplays the
|
||||
password.
|
||||
- Good: checking remember-me does not change the server-side authentication
|
||||
decision.
|
||||
- Base: dashboard reads `sessionScope.authenticatedUser.displayName` and
|
||||
`sessionScope.userRole` only for display/navigation.
|
||||
- Bad: JSP uses scriptlets, JDBC, or raw request parameters to decide
|
||||
authentication.
|
||||
- Bad: JSP, JavaScript, or Servlet code trusts a client-submitted role field to
|
||||
grant a role or stores the password in browser storage.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
@@ -79,6 +91,8 @@ rendering.
|
||||
files.
|
||||
- Run service-level auth checks for required fields, invalid credentials,
|
||||
success, DAO fallback, and permission checks.
|
||||
- When login page scripts change, scan them to confirm only usernames can be
|
||||
stored client-side and `password` is never persisted.
|
||||
- When Maven/Tomcat is available, run a Servlet/JSP compile or package check.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
@@ -87,6 +101,7 @@ rendering.
|
||||
|
||||
```jsp
|
||||
<%-- JSP checks request.getParameter("password") or runs SQL directly. --%>
|
||||
<%-- JavaScript stores the password or LoginServlet trusts a submitted role. --%>
|
||||
```
|
||||
|
||||
#### 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."}
|
||||
+9
@@ -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": {}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<!-- @@@auto:current-status -->
|
||||
- **Active File**: `journal-1.md`
|
||||
- **Total Sessions**: 12
|
||||
- **Total Sessions**: 15
|
||||
- **Last Active**: 2026-04-28
|
||||
<!-- @@@/auto:current-status -->
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<!-- @@@auto:active-documents -->
|
||||
| File | Lines | Status |
|
||||
|------|-------|--------|
|
||||
| `journal-1.md` | ~474 | Active |
|
||||
| `journal-1.md` | ~573 | Active |
|
||||
<!-- @@@/auto:active-documents -->
|
||||
|
||||
---
|
||||
@@ -29,6 +29,9 @@
|
||||
<!-- @@@auto:session-history -->
|
||||
| # | 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` |
|
||||
| 10 | 2026-04-28 | 中文详细 README | `2d4a7e2` | `master` |
|
||||
|
||||
@@ -472,3 +472,102 @@ Added safe login/database diagnostic logs, documented local demo credentials, up
|
||||
### 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
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
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.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 java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
@@ -14,13 +42,200 @@ import javax.servlet.http.HttpSession;
|
||||
public class DashboardServlet extends HttpServlet {
|
||||
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
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
HttpSession session = request.getSession(false);
|
||||
AuthenticatedUser user = session == null
|
||||
? null
|
||||
: (AuthenticatedUser) session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
|
||||
AuthenticatedUser user = currentUser(request);
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
('RD-0001', (SELECT id FROM users WHERE username = 'reader'), 'Demo Reader', '13800000000',
|
||||
'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
|
||||
('Computer Science', 'Programming, software engineering, and systems books'),
|
||||
('Literature', 'Classic and modern literature'),
|
||||
('History', 'World and regional history'),
|
||||
('Science', 'Natural science and popular science')
|
||||
('Science', 'Natural science and popular science'),
|
||||
('中国文学', '中国现当代文学、经典小说和散文作品'),
|
||||
('计算机技术', '程序设计、软件工程、数据库和信息技术图书'),
|
||||
('历史文化', '中国历史、世界历史和文化研究读物'),
|
||||
('自然科学', '数学、物理、生命科学和科普读物'),
|
||||
('社会科学', '社会学、管理学和公共事务读物')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
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',
|
||||
(SELECT id FROM book_categories WHERE name = 'Literature'), 3, 3, 'available'),
|
||||
('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>
|
||||
<meta charset="UTF-8">
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
@@ -14,11 +14,13 @@
|
||||
<main class="page-shell">
|
||||
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-users-title">
|
||||
<div>
|
||||
<p class="eyebrow">系统管理</p>
|
||||
<h1 id="manage-users-title">管理用户</h1>
|
||||
<p>创建、更新、停用和查看管理员、馆员与读者账户。</p>
|
||||
<p class="eyebrow">系统账户</p>
|
||||
<h1 id="manage-users-title">用户账户与角色</h1>
|
||||
<p>维护登录账户、角色、密码和启用状态;读者联系方式、借阅上限和资格请在读者管理中处理。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">新增用户账户</a>
|
||||
</div>
|
||||
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">新增用户</a>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty successMessage}">
|
||||
@@ -32,7 +34,7 @@
|
||||
</div>
|
||||
</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">
|
||||
<div class="search-field">
|
||||
<label for="keyword">关键词</label>
|
||||
|
||||
@@ -6,44 +6,92 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>登录 - MZH 图书馆</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||
<title>登录 - 图书管理系统</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-login-redesign">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<main class="auth-shell">
|
||||
<section class="login-panel" aria-labelledby="login-title">
|
||||
<div>
|
||||
<p class="eyebrow">图书馆管理</p>
|
||||
<h1 id="login-title">登录</h1>
|
||||
<div class="login-card-head">
|
||||
<h1 id="login-title">图书管理系统</h1>
|
||||
<p class="login-subtitle">欢迎登录图书管理平台</p>
|
||||
</div>
|
||||
|
||||
<c:if test="${not empty errorMessage}">
|
||||
<div class="message message-error" role="alert">
|
||||
<div class="message message-error login-error" role="alert">
|
||||
<c:out value="${errorMessage}" />
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate>
|
||||
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate data-login-form>
|
||||
<input type="hidden" name="redirect" value="${fn:escapeXml(redirect)}">
|
||||
<label for="username">用户名</label>
|
||||
<input id="username"
|
||||
<div class="login-field">
|
||||
<label class="sr-only" for="username">用户名</label>
|
||||
<div class="login-input-shell">
|
||||
<span class="login-input-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" focusable="false">
|
||||
<path d="M20 21a8 8 0 0 0-16 0" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="7.5" r="4" fill="none" stroke="currentColor" stroke-width="1.9"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input class="login-control"
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
value="${fn:escapeXml(username)}"
|
||||
autocomplete="username"
|
||||
placeholder="用户名"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="password">密码</label>
|
||||
<input id="password"
|
||||
<div class="login-field">
|
||||
<label class="sr-only" for="password">密码</label>
|
||||
<div class="login-input-shell login-password-shell">
|
||||
<span class="login-input-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" focusable="false">
|
||||
<rect x="5" y="10" width="14" height="10" rx="2" fill="none" stroke="currentColor" stroke-width="1.9"/>
|
||||
<path d="M8 10V7.5a4 4 0 0 1 8 0V10M12 14.5v2" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input class="login-control"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="密码"
|
||||
required>
|
||||
<button class="password-toggle"
|
||||
type="button"
|
||||
aria-label="显示密码"
|
||||
aria-controls="password"
|
||||
aria-pressed="false"
|
||||
data-password-toggle>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M2.8 12s3.3-5.5 9.2-5.5 9.2 5.5 9.2 5.5-3.3 5.5-9.2 5.5S2.8 12 2.8 12Z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="2.8" fill="none" stroke="currentColor" stroke-width="1.9"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="button button-primary" type="submit">登录</button>
|
||||
<div class="login-options-row">
|
||||
<label class="login-check">
|
||||
<input type="checkbox" name="rememberUsername" value="true" data-remember-username>
|
||||
<span>记住我</span>
|
||||
</label>
|
||||
<button class="forgot-password-link" type="button" data-forgot-password>
|
||||
忘记密码?
|
||||
</button>
|
||||
</div>
|
||||
<p class="login-help-message" id="password-help" tabindex="-1" hidden>
|
||||
请联系系统管理员重置密码。
|
||||
</p>
|
||||
|
||||
<button class="button button-primary login-submit" type="submit">登录</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<script src="${pageContext.request.contextPath}/static/js/login.js?v=20260428-login-redesign"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<main class="page-shell">
|
||||
<section class="dashboard-hero catalog-hero" aria-labelledby="catalog-title">
|
||||
<div>
|
||||
<p class="eyebrow">馆藏</p>
|
||||
<h1 id="catalog-title">馆藏检索</h1>
|
||||
<p>按图书编号、书名、作者或分类检索馆藏。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty errorMessage}">
|
||||
@@ -58,9 +60,6 @@
|
||||
|
||||
<button class="button button-primary" type="submit">检索</button>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -13,12 +13,13 @@
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<main class="page-shell">
|
||||
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-title">
|
||||
<div>
|
||||
<p class="eyebrow">图书管理</p>
|
||||
<h1 id="manage-title">管理图书</h1>
|
||||
<p>创建、更新、删除和查看馆藏记录的库存信息。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
@@ -67,8 +68,6 @@
|
||||
|
||||
<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}/catalog">查看馆藏</a>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">分类</a>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -4,105 +4,63 @@
|
||||
<header class="app-header ${not empty sessionScope.authenticatedUser ? 'app-header-auth' : 'app-header-public'}">
|
||||
<c:choose>
|
||||
<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="主导航">
|
||||
<a class="sidebar-brand" href="${pageContext.request.contextPath}/dashboard">
|
||||
<span class="brand-mark" aria-hidden="true">书</span>
|
||||
<span class="brand-text">图书管理系统</span>
|
||||
</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="模块导航">
|
||||
<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'}">
|
||||
<a class="side-nav-link ${fn:contains(currentUri, '/books') ? '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' : ''}"
|
||||
<a class="side-nav-link ${currentPath == '/reports' ? 'is-active' : ''}"
|
||||
href="${pageContext.request.contextPath}/reports">
|
||||
<span class="nav-icon" aria-hidden="true">报</span>
|
||||
<span class="nav-text">报表中心</span>
|
||||
</a>
|
||||
</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'}">
|
||||
<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">
|
||||
<span class="nav-icon" aria-hidden="true">历</span>
|
||||
<span class="nav-text">读者借阅历史</span>
|
||||
</a>
|
||||
</c:if>
|
||||
<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">
|
||||
<span class="nav-icon" aria-hidden="true">户</span>
|
||||
<span class="nav-text">用户管理</span>
|
||||
<span class="nav-text">用户账户</span>
|
||||
</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">
|
||||
<span class="nav-icon" aria-hidden="true">志</span>
|
||||
<span class="nav-text">系统日志</span>
|
||||
</a>
|
||||
</c:if>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<span class="sidebar-menu-dot" aria-hidden="true">≡</span>
|
||||
<a href="${pageContext.request.contextPath}/logout">退出登录</a>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -115,15 +73,7 @@
|
||||
<button type="submit" aria-label="搜索">搜</button>
|
||||
</form>
|
||||
<div class="topbar-actions">
|
||||
<span class="notification-dot" aria-label="通知">!</span>
|
||||
<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-pill">
|
||||
<c:out value="${sessionScope.authenticatedUser.displayName}" />
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
<c:otherwise>读者工作台</c:otherwise>
|
||||
</c:choose>
|
||||
</h1>
|
||||
<p>登录后进入 Dashboard,会话仅保存安全的 AuthenticatedUser 快照、角色代码与权限代码集合。</p>
|
||||
</div>
|
||||
<div class="welcome-user">
|
||||
<span>当前登录</span>
|
||||
@@ -31,39 +30,36 @@
|
||||
</div>
|
||||
</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="核心指标">
|
||||
<article class="metric-card">
|
||||
<span class="metric-icon metric-blue" aria-hidden="true">书</span>
|
||||
<c:choose>
|
||||
<c:when test="${empty dashboardMetrics}">
|
||||
<article class="metric-card metric-card-empty">
|
||||
<div>
|
||||
<h2>馆藏总量</h2>
|
||||
<p class="metric-value">12,586 <small>册</small></p>
|
||||
<p class="metric-trend trend-up">较上月 ↑ 5.2%</p>
|
||||
<h2>核心指标</h2>
|
||||
<p class="metric-value">--</p>
|
||||
<p class="metric-trend">暂无可展示的实时数据。</p>
|
||||
</div>
|
||||
</article>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<c:forEach var="metric" items="${dashboardMetrics}">
|
||||
<article class="metric-card">
|
||||
<span class="metric-icon metric-green" aria-hidden="true">借</span>
|
||||
<div>
|
||||
<h2>在借数量</h2>
|
||||
<p class="metric-value">1,258 <small>册</small></p>
|
||||
<p class="metric-trend trend-up">较上月 ↑ 3.1%</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<span class="metric-icon metric-orange" aria-hidden="true">期</span>
|
||||
<div>
|
||||
<h2>逾期数量</h2>
|
||||
<p class="metric-value">87 <small>册</small></p>
|
||||
<p class="metric-trend trend-down">较上月 ↓ 12.4%</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="metric-card">
|
||||
<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>
|
||||
<h2><c:out value="${metric.label}" /></h2>
|
||||
<p class="metric-value">
|
||||
<c:out value="${metric.value}" />
|
||||
<small><c:out value="${metric.unit}" /></small>
|
||||
</p>
|
||||
<p class="metric-trend"><c:out value="${metric.note}" /></p>
|
||||
</div>
|
||||
</article>
|
||||
</c:forEach>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-grid" aria-label="检索与排行">
|
||||
@@ -85,7 +81,10 @@
|
||||
<div class="search-field">
|
||||
<label for="dashCategory">分类</label>
|
||||
<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>
|
||||
</div>
|
||||
<div class="dashboard-form-actions">
|
||||
@@ -100,25 +99,36 @@
|
||||
<h2>热门图书排行</h2>
|
||||
<span>借阅次数TOP10</span>
|
||||
</div>
|
||||
<c:choose>
|
||||
<c:when test="${empty reportCenter or empty reportCenter.popularBooks}">
|
||||
<p class="empty-state">暂无热门排行数据。</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<c:set var="rankingMax" value="${reportCenter.popularBooks[0].borrowCount}" />
|
||||
<div class="rank-chart" aria-label="热门图书排行柱状图">
|
||||
<div class="rank-item"><span class="rank-value">230</span><span class="rank-bar" style="--bar-height: 92%;"></span><small>活着</small></div>
|
||||
<div class="rank-item"><span class="rank-value">198</span><span class="rank-bar" style="--bar-height: 79%;"></span><small>三体</small></div>
|
||||
<div class="rank-item"><span class="rank-value">175</span><span class="rank-bar" style="--bar-height: 70%;"></span><small>百年孤独</small></div>
|
||||
<div class="rank-item"><span class="rank-value">164</span><span class="rank-bar" style="--bar-height: 66%;"></span><small>围城</small></div>
|
||||
<div class="rank-item"><span class="rank-value">150</span><span class="rank-bar" style="--bar-height: 60%;"></span><small>平凡的世界</small></div>
|
||||
<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-item"><span class="rank-value">120</span><span class="rank-bar" style="--bar-height: 48%;"></span><small>红楼梦</small></div>
|
||||
<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"><span class="rank-value">98</span><span class="rank-bar" style="--bar-height: 39%;"></span><small>追风筝的人</small></div>
|
||||
<div class="rank-item"><span class="rank-value">85</span><span class="rank-bar" style="--bar-height: 34%;"></span><small>小王子</small></div>
|
||||
<c:forEach var="row" items="${reportCenter.popularBooks}" end="9">
|
||||
<div class="rank-item">
|
||||
<span class="rank-value"><c:out value="${row.borrowCount}" /></span>
|
||||
<span class="rank-bar"
|
||||
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>
|
||||
</section>
|
||||
|
||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||
<section class="dashboard-table-grid" aria-label="业务表格">
|
||||
<article class="dashboard-panel table-panel-compact table-panel-wide">
|
||||
<h2>借阅流通 <span>最新记录</span></h2>
|
||||
<h2>借阅流通 <span>实时记录</span></h2>
|
||||
<c:choose>
|
||||
<c:when test="${empty dashboardBorrowRecords}">
|
||||
<p class="empty-state">暂无借阅流通记录。</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table dashboard-data-table">
|
||||
<thead>
|
||||
@@ -134,63 +144,45 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach var="record" items="${dashboardBorrowRecords}" end="4">
|
||||
<tr>
|
||||
<td>L20240521001</td>
|
||||
<td>张晓明</td>
|
||||
<td>B001245</td>
|
||||
<td>活着</td>
|
||||
<td>2024-05-21</td>
|
||||
<td>2024-06-04</td>
|
||||
<td><span class="status-pill status-active">在借</span></td>
|
||||
<td><span class="stock-plus">库存-1</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>L20240521002</td>
|
||||
<td>李华</td>
|
||||
<td>B001026</td>
|
||||
<td>三体</td>
|
||||
<td>2024-05-20</td>
|
||||
<td>2024-06-03</td>
|
||||
<td><span class="status-pill status-active">在借</span></td>
|
||||
<td><span class="stock-plus">库存-1</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>L20240521003</td>
|
||||
<td>王丽</td>
|
||||
<td>B002031</td>
|
||||
<td>百年孤独</td>
|
||||
<td>2024-05-18</td>
|
||||
<td>2024-06-01</td>
|
||||
<td><span class="status-pill status-returned">已归还</span></td>
|
||||
<td><span class="stock-return">库存+1</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>L20240521004</td>
|
||||
<td>陈强</td>
|
||||
<td>B001895</td>
|
||||
<td>围城</td>
|
||||
<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>
|
||||
<td>#<c:out value="${record.id}" /></td>
|
||||
<td><c:out value="${record.readerName}" /></td>
|
||||
<td><c:out value="${record.bookIdentifier}" /></td>
|
||||
<td><c:out value="${record.bookTitle}" /></td>
|
||||
<td><c:out value="${record.borrowedAtText}" /></td>
|
||||
<td><c:out value="${record.dueAtText}" /></td>
|
||||
<td>
|
||||
<span class="status-pill status-${record.displayStatusCode}">
|
||||
<c:out value="${record.displayStatusName}" />
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<c:choose>
|
||||
<c:when test="${record.displayStatusCode == 'returned'}">
|
||||
<span class="stock-return">库存已返还</span>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<span class="stock-plus">借出占用</span>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</article>
|
||||
|
||||
<article class="dashboard-panel table-panel-compact">
|
||||
<h2>逾期列表 <span>待处理</span></h2>
|
||||
<c:choose>
|
||||
<c:when test="${empty reportCenter or empty reportCenter.overdueRows}">
|
||||
<p class="empty-state">当前没有逾期未还的借阅记录。</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table dashboard-data-table overdue-table">
|
||||
<thead>
|
||||
@@ -203,14 +195,20 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>陈强</td><td>B001895</td><td>围城</td><td>2024-05-24</td><td><span class="overdue-days">7天</span></td></tr>
|
||||
<tr><td>赵敏</td><td>B001122</td><td>平凡的世界</td><td>2024-05-20</td><td><span class="overdue-days">11天</span></td></tr>
|
||||
<tr><td>孙涛</td><td>B002003</td><td>红楼梦</td><td>2024-05-18</td><td><span class="overdue-days">13天</span></td></tr>
|
||||
<tr><td>周雨</td><td>B000987</td><td>追风筝的人</td><td>2024-05-17</td><td><span class="overdue-days">14天</span></td></tr>
|
||||
<tr><td>吴迪</td><td>B001776</td><td>白夜行</td><td>2024-05-15</td><td><span class="overdue-days">16天</span></td></tr>
|
||||
<c:forEach var="row" items="${reportCenter.overdueRows}" end="4">
|
||||
<tr>
|
||||
<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 class="dashboard-panel table-panel-compact table-panel-wide">
|
||||
@@ -218,6 +216,11 @@
|
||||
<h2>图书管理 <span>馆藏列表</span></h2>
|
||||
<a href="${pageContext.request.contextPath}/books">进入管理</a>
|
||||
</div>
|
||||
<c:choose>
|
||||
<c:when test="${empty dashboardBooks}">
|
||||
<p class="empty-state">暂无馆藏图书记录。</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<div class="table-scroll">
|
||||
<table class="data-table dashboard-data-table">
|
||||
<thead>
|
||||
@@ -226,79 +229,42 @@
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<c:forEach var="book" items="${dashboardBooks}" end="4">
|
||||
<tr>
|
||||
<td>B001245</td><td>活着</td><td>余华</td><td>文学 > 小说</td><td>2012-08-01</td>
|
||||
<td><span class="status-pill status-available">可借(15)</span></td><td>二楼文学区</td>
|
||||
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>B001026</td><td>三体</td><td>刘慈欣</td><td>文学 > 科幻</td><td>2008-01-01</td>
|
||||
<td><span class="status-pill status-available">可借(8)</span></td><td>三楼科幻区</td>
|
||||
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>B002031</td><td>百年孤独</td><td>加西亚·马尔克斯</td><td>文学 > 外国文学</td><td>2011-06-01</td>
|
||||
<td><span class="status-pill status-available">可借(6)</span></td><td>二楼文学区</td>
|
||||
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>B001895</td><td>围城</td><td>钱钟书</td><td>文学 > 小说</td><td>2008-05-01</td>
|
||||
<td><span class="status-pill status-available">可借(4)</span></td><td>二楼文学区</td>
|
||||
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>B002119</td><td>解忧杂货店</td><td>东野圭吾</td><td>文学 > 小说</td><td>2014-07-01</td>
|
||||
<td><span class="status-pill status-available">可借(10)</span></td><td>二楼文学区</td>
|
||||
<td><c:out value="${book.identifier}" /></td>
|
||||
<td><c:out value="${book.title}" /></td>
|
||||
<td><c:out value="${book.author}" /></td>
|
||||
<td><c:out value="${book.categoryName}" /></td>
|
||||
<td>
|
||||
<span class="status-pill status-${book.status.code}">
|
||||
<c:out value="${book.status.displayName}" />
|
||||
(<c:out value="${book.availableCopies}" />/<c:out value="${book.totalCopies}" />)
|
||||
</span>
|
||||
</td>
|
||||
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||
</tr>
|
||||
</c:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
</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>
|
||||
</c:if>
|
||||
|
||||
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||
<section class="shortcut-grid reader-shortcut-grid" aria-label="读者快捷入口">
|
||||
<a class="shortcut-card" href="${pageContext.request.contextPath}/reader/loans">
|
||||
<span class="shortcut-icon shortcut-blue" aria-hidden="true">历</span>
|
||||
<strong>我的借阅</strong>
|
||||
<small>查看在借、已还、续借次数和逾期状态</small>
|
||||
</a>
|
||||
<a class="shortcut-card" href="${pageContext.request.contextPath}/catalog">
|
||||
<span class="shortcut-icon shortcut-green" aria-hidden="true">搜</span>
|
||||
<strong>馆藏检索</strong>
|
||||
<small>按书名、作者、分类或图书编号查找馆藏</small>
|
||||
</a>
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<main class="page-shell">
|
||||
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-readers-title">
|
||||
<p class="eyebrow">读者管理</p>
|
||||
<h1 id="manage-readers-title">管理读者</h1>
|
||||
<p>创建、更新和查看读者资格及联系方式记录。</p>
|
||||
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">新增读者</a>
|
||||
<div>
|
||||
<p class="eyebrow">读者档案</p>
|
||||
<h1 id="manage-readers-title">读者档案与借阅资格</h1>
|
||||
<p>维护读者资料、联系方式、借阅上限和借阅资格;登录账户、角色和启用状态请在用户管理中处理。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">新增读者档案</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty successMessage}">
|
||||
@@ -30,7 +34,7 @@
|
||||
</div>
|
||||
</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">
|
||||
<div class="search-field">
|
||||
<label for="identifier">读者编号</label>
|
||||
@@ -68,10 +72,10 @@
|
||||
</section>
|
||||
|
||||
<section class="table-panel" aria-labelledby="reader-results-title">
|
||||
<h2 id="reader-results-title">读者记录</h2>
|
||||
<h2 id="reader-results-title">读者档案</h2>
|
||||
<c:choose>
|
||||
<c:when test="${empty readers}">
|
||||
<p class="empty-state">没有符合当前筛选条件的读者记录。</p>
|
||||
<p class="empty-state">没有符合当前筛选条件的读者档案。</p>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<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>
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<h1 id="reports-title">报表中心</h1>
|
||||
<p>查看馆藏库存、借阅状况、逾期借阅和热门图书。</p>
|
||||
</div>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">借阅记录</a>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty errorMessage}">
|
||||
|
||||
+285
-265
@@ -41,6 +41,10 @@ body {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
body:not(.auth-page) {
|
||||
min-width: calc(var(--sidebar-width) + 320px);
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
@@ -113,9 +117,8 @@ textarea {
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
min-height: 44px;
|
||||
padding: 0 10px;
|
||||
padding: 0 12px;
|
||||
color: #ffffff;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
@@ -130,93 +133,6 @@ textarea {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -226,29 +142,15 @@ textarea {
|
||||
.side-nav-link {
|
||||
min-height: 40px;
|
||||
display: grid;
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 7px;
|
||||
color: #c8d2df;
|
||||
line-height: 1.2;
|
||||
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 {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -263,12 +165,6 @@ textarea {
|
||||
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 {
|
||||
min-height: 34px;
|
||||
display: flex;
|
||||
@@ -287,11 +183,6 @@ textarea {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sidebar-menu-dot {
|
||||
color: #91a2bd;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.app-topbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
@@ -373,47 +264,18 @@ textarea {
|
||||
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 {
|
||||
min-width: 0;
|
||||
max-width: 240px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 10px 4px 4px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 999px;
|
||||
border-radius: 8px;
|
||||
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 {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
@@ -440,16 +302,40 @@ textarea {
|
||||
}
|
||||
|
||||
.auth-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
isolation: isolate;
|
||||
background: #edf4ff;
|
||||
}
|
||||
|
||||
.auth-page::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: -22px;
|
||||
z-index: -2;
|
||||
background:
|
||||
linear-gradient(rgba(245, 247, 251, 0.86), rgba(245, 247, 251, 0.94)),
|
||||
linear-gradient(90deg, rgba(241, 246, 255, 0.76), rgba(249, 252, 255, 0.64)),
|
||||
url("../images/library-login.svg") center / cover no-repeat;
|
||||
filter: blur(10px) saturate(0.78);
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.auth-page::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background:
|
||||
radial-gradient(circle at 50% 42%, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.18) 34%, rgba(227, 237, 255, 0.5) 100%),
|
||||
linear-gradient(180deg, rgba(247, 250, 255, 0.68), rgba(231, 239, 255, 0.78));
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
width: min(1120px, calc(100% - 32px));
|
||||
min-height: calc(100vh - 64px);
|
||||
width: min(960px, calc(100% - 32px));
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
align-items: center;
|
||||
place-items: center;
|
||||
margin: 0 auto;
|
||||
padding: 48px 0;
|
||||
}
|
||||
@@ -495,8 +381,42 @@ body:not(.auth-page) .dashboard-shell {
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
width: min(420px, 100%);
|
||||
padding: 32px;
|
||||
width: min(500px, 100%);
|
||||
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 {
|
||||
@@ -527,7 +447,7 @@ h2 {
|
||||
|
||||
.login-form {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.login-form label,
|
||||
@@ -539,7 +459,7 @@ h2 {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.login-form input,
|
||||
.login-form .login-control,
|
||||
.search-form input,
|
||||
.search-form select,
|
||||
.dashboard-search-form input,
|
||||
@@ -563,7 +483,7 @@ h2 {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.login-form input:focus,
|
||||
.login-form .login-control:focus,
|
||||
.search-form input:focus,
|
||||
.search-form select:focus,
|
||||
.dashboard-search-form input:focus,
|
||||
@@ -582,6 +502,141 @@ h2 {
|
||||
box-shadow: 0 0 0 3px rgba(40, 105, 232, 0.14);
|
||||
}
|
||||
|
||||
.login-field {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.login-input-shell {
|
||||
min-height: 58px;
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid rgba(203, 213, 225, 0.9);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.login-password-shell {
|
||||
grid-template-columns: 34px minmax(0, 1fr) 42px;
|
||||
}
|
||||
|
||||
.login-input-shell:focus-within {
|
||||
border-color: #2d7df0;
|
||||
box-shadow: 0 0 0 3px rgba(45, 125, 240, 0.13);
|
||||
}
|
||||
|
||||
.login-input-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.login-input-icon svg,
|
||||
.password-toggle svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.login-input-shell .login-control {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 56px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
color: #111827;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-input-shell .login-control:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-input-shell .login-control::placeholder {
|
||||
color: #7b8494;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
color: #747b89;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.password-toggle:hover,
|
||||
.password-toggle:focus-visible {
|
||||
color: #1d4fd7;
|
||||
background: #eef5ff;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.login-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 {
|
||||
min-height: 36px;
|
||||
display: inline-flex;
|
||||
@@ -632,8 +687,13 @@ h2 {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.login-form .button-primary {
|
||||
margin-top: 12px;
|
||||
.login-form .login-submit {
|
||||
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 {
|
||||
@@ -715,12 +775,14 @@ h2 {
|
||||
flex: 1 1 230px;
|
||||
min-width: 0;
|
||||
min-height: 98px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
display: block;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.metric-card-empty {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.metric-card > div {
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -731,35 +793,6 @@ h2 {
|
||||
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 {
|
||||
margin-bottom: 4px;
|
||||
font-size: 24px;
|
||||
@@ -933,7 +966,7 @@ h2 {
|
||||
.shortcut-card {
|
||||
min-height: 100px;
|
||||
display: grid;
|
||||
grid-template-columns: 46px 1fr 14px;
|
||||
grid-template-columns: minmax(0, 1fr) 14px;
|
||||
grid-template-rows: auto auto;
|
||||
gap: 5px 13px;
|
||||
align-items: center;
|
||||
@@ -944,7 +977,7 @@ h2 {
|
||||
.shortcut-card::after {
|
||||
content: "›";
|
||||
grid-row: 1 / 3;
|
||||
grid-column: 3;
|
||||
grid-column: 2;
|
||||
color: #94a3b8;
|
||||
font-size: 24px;
|
||||
}
|
||||
@@ -959,37 +992,6 @@ h2 {
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
|
||||
@@ -1280,60 +1282,27 @@ h2 {
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.app-sidebar,
|
||||
.app-topbar {
|
||||
position: static;
|
||||
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;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topbar-actions {
|
||||
justify-content: space-between;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.user-summary {
|
||||
max-width: none;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.user-pill,
|
||||
.role-label {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
body:not(.auth-page) .page-shell {
|
||||
width: min(1280px, calc(100% - 24px));
|
||||
margin: 0 auto;
|
||||
padding: 18px 0 40px;
|
||||
max-width: 140px;
|
||||
}
|
||||
|
||||
.global-search {
|
||||
@@ -1347,6 +1316,57 @@ h2 {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
width: min(100%, calc(100% - 24px));
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.auth-page .login-panel {
|
||||
padding: 30px 22px 34px;
|
||||
}
|
||||
|
||||
.login-card-head {
|
||||
gap: 8px;
|
||||
margin-bottom: 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 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
(function () {
|
||||
var form = document.querySelector("[data-login-form]");
|
||||
var username = document.getElementById("username");
|
||||
var password = document.getElementById("password");
|
||||
var remember = document.querySelector("[data-remember-username]");
|
||||
var toggle = document.querySelector("[data-password-toggle]");
|
||||
var forgot = document.querySelector("[data-forgot-password]");
|
||||
var passwordHelp = document.getElementById("password-help");
|
||||
var storageKey = "mzh.library.login.username";
|
||||
|
||||
function readStoredUsername() {
|
||||
try {
|
||||
return window.localStorage.getItem(storageKey) || "";
|
||||
} catch (ex) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function writeStoredUsername(value) {
|
||||
try {
|
||||
if (value) {
|
||||
window.localStorage.setItem(storageKey, value);
|
||||
} else {
|
||||
window.localStorage.removeItem(storageKey);
|
||||
}
|
||||
} catch (ex) {
|
||||
// Storage may be disabled; login should still submit normally.
|
||||
}
|
||||
}
|
||||
|
||||
if (username && remember) {
|
||||
var storedUsername = readStoredUsername();
|
||||
if (storedUsername) {
|
||||
remember.checked = true;
|
||||
if (!username.value) {
|
||||
username.value = storedUsername;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (form && username && remember) {
|
||||
form.addEventListener("submit", function () {
|
||||
writeStoredUsername(remember.checked ? username.value.trim() : "");
|
||||
});
|
||||
}
|
||||
|
||||
if (toggle && password) {
|
||||
toggle.addEventListener("click", function () {
|
||||
var nextVisible = password.type !== "text";
|
||||
password.type = nextVisible ? "text" : "password";
|
||||
toggle.setAttribute("aria-pressed", String(nextVisible));
|
||||
toggle.setAttribute("aria-label", nextVisible ? "隐藏密码" : "显示密码");
|
||||
password.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (forgot && passwordHelp) {
|
||||
forgot.addEventListener("click", function () {
|
||||
passwordHelp.hidden = !passwordHelp.hidden;
|
||||
if (!passwordHelp.hidden) {
|
||||
passwordHelp.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}());
|
||||
Reference in New Issue
Block a user