diff --git a/.trellis/spec/frontend/quality-guidelines.md b/.trellis/spec/frontend/quality-guidelines.md index 8b94910..67f93c1 100644 --- a/.trellis/spec/frontend/quality-guidelines.md +++ b/.trellis/spec/frontend/quality-guidelines.md @@ -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. --- diff --git a/.trellis/spec/frontend/state-management.md b/.trellis/spec/frontend/state-management.md index 7e69842..be81b4f 100644 --- a/.trellis/spec/frontend/state-management.md +++ b/.trellis/spec/frontend/state-management.md @@ -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`. + - `dashboardBooks: List`. + - `dashboardMetrics: List`. + - `reportCenter: ReportCenter` for staff users when report loading succeeds. + - `dashboardBorrowRecords: List` 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 diff --git a/.trellis/tasks/04-28-frontend-workbench-display-fix/check.jsonl b/.trellis/tasks/04-28-frontend-workbench-display-fix/check.jsonl new file mode 100644 index 0000000..c161f4b --- /dev/null +++ b/.trellis/tasks/04-28-frontend-workbench-display-fix/check.jsonl @@ -0,0 +1,5 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. 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"} diff --git a/.trellis/tasks/04-28-frontend-workbench-display-fix/implement.jsonl b/.trellis/tasks/04-28-frontend-workbench-display-fix/implement.jsonl new file mode 100644 index 0000000..34bbe25 --- /dev/null +++ b/.trellis/tasks/04-28-frontend-workbench-display-fix/implement.jsonl @@ -0,0 +1,8 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. 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"} diff --git a/.trellis/tasks/04-28-frontend-workbench-display-fix/prd.md b/.trellis/tasks/04-28-frontend-workbench-display-fix/prd.md new file mode 100644 index 0000000..463865b --- /dev/null +++ b/.trellis/tasks/04-28-frontend-workbench-display-fix/prd.md @@ -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`. diff --git a/.trellis/tasks/04-28-frontend-workbench-display-fix/task.json b/.trellis/tasks/04-28-frontend-workbench-display-fix/task.json new file mode 100644 index 0000000..eda477f --- /dev/null +++ b/.trellis/tasks/04-28-frontend-workbench-display-fix/task.json @@ -0,0 +1,26 @@ +{ + "id": "frontend-workbench-display-fix", + "name": "frontend-workbench-display-fix", + "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": {} +} \ No newline at end of file diff --git a/src/main/java/com/mzh/library/controller/DashboardServlet.java b/src/main/java/com/mzh/library/controller/DashboardServlet.java index 0dd7c0f..01238af 100644 --- a/src/main/java/com/mzh/library/controller/DashboardServlet.java +++ b/src/main/java/com/mzh/library/controller/DashboardServlet.java @@ -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> categoryResult = bookService.listCategories(); + request.setAttribute("categories", categoryResult.isSuccessful() + ? listOrEmpty(categoryResult.getData()) + : Collections.emptyList()); + if (!categoryResult.isSuccessful()) { + setErrorMessage(request, categoryResult.getMessage()); + } + + ServiceResult> bookResult = bookService.searchBooks(new BookSearchCriteria()); + List dashboardBooks = bookResult.isSuccessful() + ? listOrEmpty(bookResult.getData()) + : Collections.emptyList(); + request.setAttribute("dashboardBooks", dashboardBooks); + if (!bookResult.isSuccessful()) { + setErrorMessage(request, bookResult.getMessage()); + } + + List metrics = Collections.emptyList(); + if (isStaff(user)) { + Integer readerTotal = null; + ServiceResult> readerResult = readerService.searchReaders(new ReaderSearchCriteria()); + if (readerResult.isSuccessful()) { + readerTotal = listOrEmpty(readerResult.getData()).size(); + } else { + setErrorMessage(request, readerResult.getMessage()); + } + + ServiceResult 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> 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 List listOrEmpty(List values) { + return values == null ? Collections.emptyList() : values; + } + + private List metricsFromReport(ReportCenter reportCenter, Integer readerTotal) { + if (reportCenter == null) { + return Collections.emptyList(); + } + InventorySummary inventory = reportCenter.getInventorySummary(); + BorrowingSummary borrowing = reportCenter.getBorrowingSummary(); + List 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 metricsFromBooks(List 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 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; + } + } } diff --git a/src/main/webapp/WEB-INF/jsp/common/header.jspf b/src/main/webapp/WEB-INF/jsp/common/header.jspf index 29f3acf..7500f14 100644 --- a/src/main/webapp/WEB-INF/jsp/common/header.jspf +++ b/src/main/webapp/WEB-INF/jsp/common/header.jspf @@ -7,7 +7,6 @@ @@ -115,15 +100,7 @@
- ! - diff --git a/src/main/webapp/WEB-INF/jsp/dashboard.jsp b/src/main/webapp/WEB-INF/jsp/dashboard.jsp index 237d8fe..16cdef1 100644 --- a/src/main/webapp/WEB-INF/jsp/dashboard.jsp +++ b/src/main/webapp/WEB-INF/jsp/dashboard.jsp @@ -31,39 +31,36 @@
+ +

+
+
-
- -
-

馆藏总量

-

12,586

-

较上月 ↑ 5.2%

-
-
-
- -
-

在借数量

-

1,258

-

较上月 ↑ 3.1%

-
-
-
- -
-

逾期数量

-

87

-

较上月 ↓ 12.4%

-
-
-
- -
-

读者总数

-

3,682

-

较上月 ↑ 4.8%

-
-
+ + +
+
+

核心指标

+

--

+

暂无可展示的实时数据。

+
+
+
+ + +
+
+

+

+ + +

+

+
+
+
+
+
@@ -85,7 +82,10 @@
@@ -100,117 +100,116 @@

热门图书排行

借阅次数TOP10
-
-
230活着
-
198三体
-
175百年孤独
-
164围城
-
150平凡的世界
-
138解忧杂货店
-
120红楼梦
-
112白夜行
-
98追风筝的人
-
85小王子
-
+ + +

暂无热门排行数据。

+
+ + +
+ +
+ + + +
+
+
+
+
-

借阅流通 最新记录

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
流水号读者姓名图书编号书名借阅日期应还日期状态库存联动
L20240521001张晓明B001245活着2024-05-212024-06-04在借库存-1
L20240521002李华B001026三体2024-05-202024-06-03在借库存-1
L20240521003王丽B002031百年孤独2024-05-182024-06-01已归还库存+1
L20240521004陈强B001895围城2024-05-102024-05-24逾期库存-1
L20240521005刘洋B002119解忧杂货店2024-05-122024-05-26逾期库存-1
-
+

借阅流通 实时记录

+ + +

暂无借阅流通记录。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
流水号读者姓名图书编号书名借阅日期应还日期状态库存联动
# + + + + + + + 库存已返还 + + + 借出占用 + + +
+
+
+

逾期列表 待处理

-
- - - - - - - - - - - - - - - - - -
读者姓名图书编号书名应还日期逾期天数
陈强B001895围城2024-05-247天
赵敏B001122平凡的世界2024-05-2011天
孙涛B002003红楼梦2024-05-1813天
周雨B000987追风筝的人2024-05-1714天
吴迪B001776白夜行2024-05-1516天
-
+ + +

当前没有逾期未还的借阅记录。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
读者姓名图书编号书名应还日期逾期天数
+
+
+
@@ -218,87 +217,55 @@

图书管理 馆藏列表

进入管理 -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
图书编号书名作者分类出版日期库存状态馆藏地操作
B001245活着余华文学 > 小说2012-08-01可借(15)二楼文学区管理
B001026三体刘慈欣文学 > 科幻2008-01-01可借(8)三楼科幻区管理
B002031百年孤独加西亚·马尔克斯文学 > 外国文学2011-06-01可借(6)二楼文学区管理
B001895围城钱钟书文学 > 小说2008-05-01可借(4)二楼文学区管理
B002119解忧杂货店东野圭吾文学 > 小说2014-07-01可借(10)二楼文学区管理
-
+ + +

暂无馆藏图书记录。

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
图书编号书名作者分类库存状态操作
+ + + (/) + + 管理
+
+
+
- -
- 我的借阅 查看在借、已还、续借次数和逾期状态 - 馆藏检索 按书名、作者、分类或图书编号查找馆藏 diff --git a/src/main/webapp/static/css/app.css b/src/main/webapp/static/css/app.css index d15df03..fb496e8 100644 --- a/src/main/webapp/static/css/app.css +++ b/src/main/webapp/static/css/app.css @@ -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,18 +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; @@ -162,8 +153,8 @@ textarea { .role-chip { min-height: 44px; display: grid; - grid-template-columns: 30px minmax(0, 1fr); - gap: 0 10px; + grid-template-columns: minmax(0, 1fr); + gap: 2px; align-items: center; padding: 9px 11px; border-radius: 7px; @@ -172,18 +163,6 @@ textarea { 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; @@ -226,29 +205,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 +228,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 +246,6 @@ textarea { text-decoration: none; } -.sidebar-menu-dot { - color: #91a2bd; - font-size: 20px; -} - .app-topbar { position: fixed; top: 0; @@ -373,47 +327,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; @@ -715,12 +640,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 +658,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 +831,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 +842,7 @@ h2 { .shortcut-card::after { content: "›"; grid-row: 1 / 3; - grid-column: 3; + grid-column: 2; color: #94a3b8; font-size: 24px; } @@ -959,37 +857,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 +1147,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 {