基于 Next.js 15 (App Router) 的 IDP 管理后台前端。不依赖 NextAuth / tRPC / Drizzle —— 所有数据都通过 fetch 直连后端 Spring Boot REST API,登录态使用 JWT + Zustand 持久化到 localStorage。
| 层 | 技术 |
|---|---|
| 语言 | TypeScript 5 |
| 框架 | Next.js 15(App Router)+ React 19 |
| 样式 | Tailwind CSS v4 |
| 数据请求 | 自封装 lib/api/http.ts (fetch 封装) + TanStack React Query |
| 状态 | Zustand(持久化登录态) |
| 表单 | React Hook Form + Zod + @hookform/resolvers |
| 提示 | sonner(Toast) |
| 图标 | lucide-react |
| 测试 | Vitest + Testing Library + jsdom |
| 包管理 | pnpm 10 |
frontend/
├── src/
│ ├── app/
│ │ ├── login/page.tsx # 登录页(账号密码 + 可选验证码 + SITE 回填)
│ │ ├── admin/
│ │ │ ├── layout.tsx # 后台 Shell(按权限过滤侧边栏 + 顶栏 + 登出)
│ │ │ ├── page.tsx # 概览页
│ │ │ ├── profile/
│ │ │ │ └── page.tsx # 个人中心:基本信息 + 安全设置(含修改密码)
│ │ │ ├── message/page.tsx # 站内消息中心(普通用户视角)
│ │ │ ├── monitor/
│ │ │ │ ├── online/page.tsx # 在线用户
│ │ │ │ └── log/page.tsx # 系统日志(登录日志 / 操作日志)
│ │ │ └── system/
│ │ │ ├── user/page.tsx # 用户管理
│ │ │ ├── role/page.tsx # 角色管理 + 分配菜单
│ │ │ ├── menu/page.tsx # 菜单管理(树形表格 + type 联动弹窗)
│ │ │ ├── config/page.tsx # 系统配置(SITE/PASSWORD/LOGIN/STORAGE Tab)
│ │ │ ├── dict/page.tsx # 字典管理(字典 + 明细)
│ │ │ ├── file/page.tsx # 文件管理(FileAside + FileMain)
│ │ │ └── notice/ # 通知公告
│ │ │ ├── page.tsx # 列表 + 搜索 + 删除
│ │ │ ├── add/page.tsx # 新增 / 编辑(query.type=update)
│ │ │ └── view/page.tsx # 预览
│ │ ├── layout.tsx # 根 Layout(QueryProvider + Toaster)
│ │ └── page.tsx # 入口:根据登录态跳 /login 或 /admin
│ ├── components/
│ │ ├── providers/query-provider.tsx
│ │ ├── ui/ # 基础组件(Button/Input/Modal/Tabs/Switch/UploadImage/Breadcrumb/Dropdown/Pagination/ContextMenu/Progress/Empty 等)
│ │ ├── system/ # 业务表单(user/role/menu-tree/notice/dict 等)
│ │ │ ├── dict-badge.tsx # 字典明细 → 带颜色的 Badge
│ │ │ ├── notice-detail-drawer.tsx # 通知公告右侧详情抽屉
│ │ │ ├── notice-popup.tsx # 登录后弹窗未读公告
│ │ │ ├── notification-bell.tsx # 顶栏未读数 + 最近消息下拉
│ │ │ ├── user-multi-select.tsx # 多选用户(指定通知用户场景)
│ │ │ ├── file/ # 文件管理:FileAside / FileMain / FileGrid / FileList / 各 modal / 预览 / multipart-uploader
│ │ │ └── storage/ # 存储配置:StorageConfigTab / StorageCard / StorageAddCard / StorageFormModal
│ │ └── permission-guard.tsx # <PermissionGuard codes=[...]>
│ ├── lib/
│ │ ├── api/ # http 封装 + auth/user/role/option/menu/notice/dict/message/file/storage/multipart-upload API + 类型
│ │ ├── hooks/
│ │ │ ├── use-permission.ts # 权限判断 Hook(admin 直通)
│ │ │ └── use-dict.ts # 按 code 拉字典 + getLabel/getColor 工具
│ │ ├── store/auth-store.ts # zustand 持久化登录态(含 menuTree 非持久字段)
│ │ └── utils.ts # cn / apiUrl
│ ├── styles/globals.css
│ └── env.js # 环境变量校验(仅 NEXT_PUBLIC_API_BASE_URL)
├── eslint.config.js
├── next.config.js
├── postcss.config.js
├── prettier.config.js
├── tsconfig.json
├── vitest.config.ts
├── vitest.setup.ts
└── package.json
.env:
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080| 变量 | 说明 |
|---|---|
NEXT_PUBLIC_API_BASE_URL |
后端 Spring Boot REST 地址,前端所有请求基地址 |
src/lib/api/http.ts 提供统一的 fetch 封装:
- 自动注入
Authorization: Bearer <token>(来自 zustandauth-store)。 - 自动反序列化后端
R<T> = {code, msg, data}结构,仅当code === 0时返回data,否则抛出HttpError。 401自动调用注册的 unauthorized 回调(默认会清空登录态并跳转/login)。
API 客户端文件 → 后端接口对应:
| 文件 | 后端接口 |
|---|---|
lib/api/auth.ts |
POST /auth/login、POST /auth/logout、GET /auth/user/info、GET /auth/captcha、POST /system/user/password、PUT /system/user/profile |
lib/api/user.ts |
GET/POST/PUT/DELETE /system/user 系列 + 重置密码 / 分配角色 |
lib/api/role.ts |
GET/POST/PUT/DELETE /system/role 系列 |
lib/api/option.ts |
GET/PUT/PATCH /system/option、POST /system/option/image、公开 GET /system/option/site 与 /system/option/login |
lib/api/menu.ts |
GET/POST/PUT/DELETE /system/menu 系列 + GET/PUT /system/role/{id}/menu + GET /auth/user/route(前端动态侧边栏数据源) |
lib/api/dict.ts |
GET /system/dict/list、GET/POST/PUT/DELETE /system/dict 与 /system/dict/{dictId}/item 系列、公开 GET /system/dict/{code}/item |
lib/api/notice.ts |
GET/POST/PUT/DELETE /system/notice + /popup、/{id}/read、/dashboard |
lib/api/message.ts |
GET /system/message 分页、/unread-count、POST /system/message/{id}/read 与 /read-all |
lib/api/monitor.ts |
GET/DELETE /monitor/online、GET /system/log、GET /system/log/{id} 与日志导出 |
lib/api/file.ts |
GET/POST/PUT/DELETE /system/file 系列 + 回收站 /system/file/recycle/* |
lib/api/storage.ts |
GET/POST/PUT/DELETE /system/storage 系列 + /status + /default |
lib/api/multipart-upload.ts |
分片上传 init / part / complete / cancel |
http.ts 额外提供 http.upload<T>(path, formData, { onProgress }),使用 XMLHttpRequest 以支持上传进度回调,普通文件 / 分片上传均通过它发起。
| 脚本 | 作用 |
|---|---|
pnpm dev |
启动开发服务器(Turbopack) |
pnpm build / pnpm start |
构建并启动生产模式 |
pnpm typecheck |
TypeScript 严格类型检查 |
pnpm lint / pnpm lint:fix |
ESLint 检查 / 自动修复 |
pnpm format:write / format:check |
Prettier 格式化 |
pnpm test |
运行 Vitest 单测(一次性) |
pnpm test:watch |
监听模式 |
pnpm test:ui |
启动 Vitest UI |
pnpm test:coverage |
生成覆盖率报告(v8) |
pnpm check |
lint + typecheck 组合 |
- 测试文件位置:与被测代码同目录,命名为
*.test.ts/*.test.tsx。 - 组件测试统一使用
@testing-library/react+@testing-library/jest-dom。 - 任何新增/修改的函数、组件、API 客户端都必须附带至少一个测试,并在 PR 中保证
pnpm test通过。
- 新增功能 = 新增测试 + 更新文档:详见
.cursor/rules/feature-workflow.mdc。 - 统一通过
~/别名引用src/:见tsconfig.json。 - 环境变量必须在
src/env.js中声明,不要直接读取process.env(仅lib/api/http.ts直接读,因 server / client 同时使用)。 - 登录态、JWT、用户信息只放在
auth-store,禁止再放到其它 store 或 cookie。 - 所有
export的函数 / Hook / 组件 / 类型必须有中文 JSDoc;组件 Props 接口的每个字段都要/** ... */行内注释;详见.cursor/rules/api-doc-comments.mdc。 - JSDoc 中如包含
*/序列(如**/model/*这种 glob)必须改写规避,否则tsc会报伪类型错误。
admin/layout.tsx 在登录后调用 GET /auth/user/route 拉取用户可见菜单({@code type=1} 目录、{@code type=2} 菜单按 sort 升序的树),按以下规则渲染:
- 顶级目录({@code type=1})渲染为可展开 / 折叠的分组;
- 菜单({@code type=2})渲染为
Link;当path命中当前路由时高亮; isExternal=true渲染为<a target="_blank">;isHidden=true或status=0的节点会被过滤;icon字段按ICON_MAP映射到 lucide-react 图标,未匹配时退化为Folder。
/admin(概览)与 /admin/profile(个人中心:基本信息 + 修改密码)保持硬编码(不属于菜单管理的内容)。详细的菜单数据模型见 ../docs/menu.md。
admin/layout.tsx 的最外层使用 h-screen + overflow-hidden 固定为视口高度,左侧 <aside> 内的菜单 <nav> 与右侧主内容 <main> 各自带 min-h-0 + overflow-(y-)auto,因此:
- 主内容超长时只在
<main>内部滚动,侧边栏保持不动; - 菜单超长时只在
<nav>内部滚动,不会撑高页面; - 浏览器页面级(
<html>/<body>)不会出现滚动条。
如果以后改回 min-h-screen 或去掉 overflow-hidden,主内容会撑高最外层并触发页面级滚动,侧边栏就会跟着一起滚 —— 这是已有过的回归,已在 src/app/admin/layout.test.tsx 中通过断言关键 className 锁定,不要轻易改动。
参考 continew-admin 的个人中心页布局,由 app/admin/profile/page.tsx 实现:
- 左侧 “基本信息” 卡:展示 ID / 用户名 / 昵称 / 邮箱 / 手机 / 性别 / 角色,并提供 “编辑” 按钮触发 Modal 修改昵称、邮箱、手机、性别;
- 右侧 “安全设置” 卡:展示登录密码(始终已设置)、安全邮箱、安全手机三行;点击 “修改” 调起对应 Modal(密码使用专门的 “原密码 + 新密码 + 确认” 表单,邮箱 / 手机复用基本信息 Modal);
- 修改基本信息成功后会立即
getUserInfo重拉并更新 zustand store,顶栏 / 侧边栏即时反映新的昵称; - 修改密码成功后会自动登出并跳回
/login,强制使用新密码重新登录。
后端配套接口:
| 方法 | 路径 | 说明 |
|---|---|---|
PUT |
/system/user/profile |
当前登录用户自助修改昵称 / 邮箱 / 手机 / 性别(任意登录用户可调用,无需 system:user:* 权限) |
POST |
/system/user/password |
当前登录用户自助修改密码 |
GET |
/auth/user/info |
拉取当前用户信息(含 gender) |
后端会自动初始化默认管理员账号 admin / 123456,登录后请尽快通过用户管理页重置密码。
新增 /admin/system/file 与系统配置页 STORAGE Tab,覆盖企业后台的文件管理需求:
| 入口 | 说明 |
|---|---|
/admin/system/file |
双列布局:左 FileAside(分类切换 + 资源统计),右 FileMain(面包屑 / 操作栏 / grid·list 视图 / 分页 / 右键菜单 / 多选 / 批量删除 / 回收站) |
/admin/system/config?tab=STORAGE |
本地 / 对象存储分组卡片网格,支持新增 / 编辑 / 启用 / 禁用 / 设为默认 / 删除 |
关键能力:
- 普通上传 + 分片上传:分片上传组件
multipart-uploader-core.ts用 Web Crypto 计算 SHA256,按 5MB 切片、并发 3 个分片上传;已成功的分片号写入sessionStorage,断点续传同一文件时跳过;触发AbortSignal自动调用后端 cancel。 - 秒传:分片上传 init 命中已有
SHA256时直接返回existing文件信息,无需再上传任何分片。 - 预览:
image-preview.tsx:原生 lightbox,支持缩放 / 旋转 / 重置 / 列表轮播 / 键盘 ←→Esc;video-preview.tsx、audio-preview.tsx:原生<video>/<audio>;office-preview.tsx:pdf走<iframe>,docx用docx-preview,xlsx用xlsx(SheetJS)渲染 HTML table,ppt/pptx等暂不支持时兜底 “下载 + 新标签页打开”;getOfficePreviewKind(extension)暴露给上层做扩展名分流;图片预览同时把当前页内所有图片传给ImagePreview实现轮播。
- 回收站:
FileRecycleModal提供分页 / 还原 / 物理删除 / 清空,所有写操作都会同步刷新文件列表与统计。 - 存储表单:
storage-form-modal.tsx在 LOCAL / S3 之间动态切换字段;编辑态下编码字段禁用,SecretKey留空表示不修改。 - 交互约定:
- 整张卡片 / 整行 单击 = 打开(文件夹进入下一级 / 文件触发对应预览);
- 卡片左上角勾选框(hover 或选中时显示)/ 列表行复选框 = 仅切换选中,不冒泡;
- 右键菜单:
详情/重命名(system:file:update)/下载(仅文件,浏览器<a download>触发)/删除(system:file:delete,红色高亮)。
详细架构与流程参见 ../docs/file-storage.md。
新增的三个模块共享同一组前端基础设施:
- 字典 Hook
useDict(code, enabled?):5 分钟 staleTime 缓存,返回{ items, getLabel, getColor };写入字典后调用queryClient.invalidateQueries({ queryKey: dictQueryKey(code) })即可刷新所有消费方。详见../docs/dict.md。 DictBadge:把字典明细的color字段(primary/success/warning/danger/info)渲染成统一的 Badge tone;匹配不到时回落为灰色 + 原 value。- 通知公告页面:列表 / 新增编辑 / 预览三页 + 右侧详情抽屉;正文用
<textarea>+ 预览端<pre className="whitespace-pre-wrap">,零额外依赖;定时发布 / 草稿 / 立即发布的状态切换在add/page.tsx中按status + isTiming推导,详见../docs/notice.md。 - 登录弹窗
NoticePopup:在/admin/layout.tsx内首次进入时 fetch/system/notice/popup,命中则用 Modal 依次展示;会话内通过sessionStorage记录已展示 ID 防重复弹出。 - 顶栏未读数
NotificationBell:拉/system/message/unread-count+ 最近 5 条消息,点击消息会跳转message.path并自动read(id)。详见../docs/message.md。 - Dashboard 卡片:
/admin概览页新增 “最新公告” 卡片,数据来自/system/notice/dashboard。
监控功能参考 continew-admin-ui 的在线用户、登录日志、操作日志页面,由后端 MenuSeeder 初始化 “系统监控” 目录:
/admin/monitor/online:在线用户列表,支持用户名/昵称、登录时间过滤;具备monitor:online:kickout时可强退其他会话;/admin/monitor/log:系统日志页面,包含 “登录日志 / 操作日志” 双 Tab;支持状态、时间、操作人、IP 等条件筛选;- 日志导出复用
http.download,当前后端返回 CSV 文件。
详细设计、接口与测试覆盖见 ../docs/monitor.md。
- 旧
/admin/system/permission页面已下线,改为/admin/system/menu;旧书签需要更新。 - 旧权限码
system:permission:*整体废弃,统一为system:menu:*;自定义角色绑定的旧权限需要在 “角色管理 → 分配菜单” 中重新分配。 - 数据库表
idp_sys_permission/idp_sys_role_permission由idp_sys_menu/idp_sys_role_menu取代,本地需要drop database idp;重建。