This commit is contained in:
Zzzz
2026-04-28 20:15:47 +08:00
parent 0face72b8d
commit d0e71f2aa9
12 changed files with 206 additions and 145 deletions
+15 -2
View File
@@ -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.
@@ -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": "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": {}
}
@@ -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,14 @@
<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>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">读者档案</a>
</div>
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">新增用户</a>
</section>
<c:if test="${not empty successMessage}">
@@ -32,7 +35,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>
@@ -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">
<p class="eyebrow">馆藏</p>
<h1 id="catalog-title">馆藏检索</h1>
<p>按图书编号、书名、作者或分类检索馆藏。</p>
<div>
<p class="eyebrow">馆藏</p>
<h1 id="catalog-title">馆藏检索</h1>
<p>按图书编号、书名、作者或分类检索馆藏。</p>
</div>
</section>
<c:if test="${not empty errorMessage}">
+5 -3
View File
@@ -13,9 +13,11 @@
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-title">
<p class="eyebrow">图书管理</p>
<h1 id="manage-title">管理图书</h1>
<p>创建、更新、删除和查看馆藏记录的库存信息。</p>
<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>
+31 -58
View File
@@ -4,83 +4,56 @@
<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-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-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-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-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-text">工作台</span>
</a>
<a class="side-nav-link ${fn:contains(currentUri, '/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 ${fn:contains(currentUri, '/books') ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/books">
<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-text">图书分类管理</span>
</a>
<a class="side-nav-link ${fn:contains(currentUri, '/readers') ? 'is-active' : ''}"
href="${pageContext.request.contextPath}/readers">
<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-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-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-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-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-text">系统日志</span>
</a>
@@ -23,7 +23,6 @@
<c:otherwise>读者工作台</c:otherwise>
</c:choose>
</h1>
<p>登录后进入 Dashboard,会话仅保存安全的 AuthenticatedUser 快照、角色代码与权限代码集合。</p>
</div>
<div class="welcome-user">
<span>当前登录</span>
+16 -9
View File
@@ -6,17 +6,24 @@
<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>
<c:if test="${sessionScope.userRole == 'administrator'}">
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">管理登录账户</a>
</c:if>
</div>
</section>
<c:if test="${not empty successMessage}">
@@ -30,7 +37,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 +75,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 +88,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>
-63
View File
@@ -133,69 +133,6 @@ textarea {
white-space: nowrap;
}
.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: minmax(0, 1fr);
gap: 2px;
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-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;