51 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Next 全量对接 Go API | 全栈对接 Go API;OAuth2 Code + PKCE;**401/403 与 200+`code` 分工**(**含:密码错=401,只看状态**);JSON 信封;login 的 401 不 refresh;dev 跨域;登出多 Tab;auth/login 改造 §3;租户关 Tabs;路由守卫。 |
|
false |
Next.js 前端全量对接 Go 后端方案
后端路由清单(对接范围)
以下均基于当前仓库中的注册代码(internal/auth/http_register.go、internal/iam/http_register.go、internal/system/http_register.go、internal/server/http.go)。
| 前缀 | 说明 |
|---|---|
GET /health |
探活 |
| 根路径 OAuth | GET /oauth/authorize、POST /oauth/token、POST /oauth/introspect(无 /api/v1 前缀) |
/api/v1 |
全组挂载 Bearer 中间件(无 token 仍放行,见 internal/auth/middleware/bearer.go) |
/api/v1 下具体接口:
- Auth:
POST /api/v1/auth/login、POST /api/v1/auth/logout - IAM(
internal/iam/http_register.go):/api/v1/iam/tenant/*、/dept/*、/role/*、/user/*、/menu/*(含tree、nav、perms等) - System:
/api/v1/system/param/*(create/update/delete-batch/get/list)
前端需要为上述路径提供 baseURL 配置(例如 NEXT_PUBLIC_API_ORIGIN=http://127.0.0.1:8000),并区分两类调用:
- 业务 JSON API:
fetch(\${origin}/api/v1/...`)` - OAuth:
fetch(\${origin}/oauth/token`, …)等(表单application/x-www-form-urlencoded`)
flowchart LR
subgraph next [Nextjs]
Pages[页面与Radix组件]
Store[Zustand状态]
Client[api客户端层]
end
subgraph go [Go]
Health["/health"]
OAuth["/oauth/*"]
API["/api/v1/*"]
end
Pages --> Store
Store --> Client
Client --> Health
Client --> OAuth
Client --> API
架构建议
1. 环境与请求基址
- 使用
NEXT_PUBLIC_API_ORIGIN(仅 scheme+host+port,不要带尾部/api/v1,便于拼/api/v1与/oauth)。 - 部署:上线后 Next 与 Go 同一站点/同域(或反代为同源),Cookie(若仍用于 authorize 兼容)与 CSRF 更简单。开发阶段 常为 前端 localhost:3000 + 后端 :8000 跨域,Go 必须配置 CORS 白名单(具体
http://localhost:3000等),Access-Control-Allow-Origin禁止*若带credentials;Bearer 主路径下跨域以Authorization为主,但仍需为 预检 OPTIONS 与 错误 JSON 配好 CORS。
2. HTTP 客户端层(对接「所有接口」的核心)
- 封装单一
apiClient(fetch或ky/axios二选一),职责:- 统一
baseURL、Content-Type: application/json(OAuth token 端点单独走 form-urlencoded)。 - 从 Zustand(或内存)读取 access_token,对
/api/v1/**自动加Authorization: Bearer <opaque>。 - HTTP 401:先 refresh(见 §3),失败再 弹窗;HTTP 200 时读
body.code判断业务成败(见 §「JSON 统一响应」)。 - 可选:
X-Tenant-ID等与后端internal/iam/handler/helpers.go兼容。
- 统一
- 按领域拆模块(与后端包对齐,便于维护):
api/auth.ts— login/logoutapi/oauth.ts— token、(若前端自己做 PKCE)authorize URL 构造api/iam/tenant.ts、dept.ts、role.ts、user.ts、menu.tsapi/system/param.ts
这样「对接所有接口」= 模块方法覆盖上表每一条路由,类型用 TypeScript 手写 DTO(后端暂无统一 OpenAPI 时可从 handler 结构体对齐,后续可加 swagger 生成)。
**JSON 统一响应(/api/v1/**业务接口,含改造后的auth/login)**
HTTP 状态码(已定)
| HTTP | 含义 |
|---|---|
401 |
认证失败:未登录、token 无效/过期、**以及 POST /api/v1/auth/login 账号密码错误(凭据不成立)**等。此类场景 只看 HTTP 状态即可;不必再依赖响应体 code 区分是否密码错误(体可仍带统一信封或极简 JSON,供文案时选用)。 |
403 |
已认证但无权限访问该资源或操作(无全权限/禁止访问)。 |
200 |
其余业务层响应(含非认证类校验失败、参数错误、业务规则不满足等)。具体失败类型看响应体 code。认证类失败(含密码错误)不归入此类,见上 401。 |
5xx |
服务端异常(可选)仍用 5xx,可与信封并存或单独约定;不要求强行改成 200。 |
响应体信封
| 字段 | 说明 |
|---|---|
code |
业务码:200 表示业务成功;非 200 表示在 HTTP=200 前提下的业务失败(参数错、业务规则不满足等)。凭据/认证问题已用 HTTP 401 表达时,前端以 HTTP 为准,可不解析 code。 |
msg |
文案,供 toast。 |
data |
成功时为载荷;失败时 null 或省略。 |
- 请求可传
state,成功时经data回显。 /oauth/token、/oauth/introspect等 OAuth 端点可仍按 RFC,以docs/oauth-v2.md为准(可与业务信封并存)。
前端 apiClient:先读 HTTP;若为 401 → 若请求不是「正在调 /auth/login」则 先 refresh,失败再弹窗;若 401 来自 POST /auth/login(含密码错误) → 不 refresh,直接 弹窗或提示(与 token 无关)。403 → toast。200 → 解析 body.code。
3. 登录与鉴权策略
Cookie 登录 vs OAuth2 Code + PKCE(概念)
- Cookie 会话:
Set-Cookie会话 id,后续请求自动带 Cookie。 - OAuth2 Code + PKCE:用
code_verifier/code_challenge(S256)绑定授权码,再通过POST /oauth/token(grant_type=authorization_code)换 opaqueaccess_token;业务请求Authorization: Bearer。见docs/oauth-v2.md。
现状与目标(后端)
- 现状(偏差):
internal/auth/handler/login.go中POST /api/v1/auth/login仅校验账号密码并下发 Session Cookie,返回{"ok":true},未纳入 PKCE,也未与internal/auth/oauth2/service.go的授权码/token 管线统一。 - 目标(已定需求):
/api/v1/auth/login也必须走 OAuth2 Code + PKCE 语义——即:账号密码验证成功后,签发可与现有/oauth/token交换的 authorization_code(且绑定 PKCE),前端仍用 同一套POST /oauth/token换 Bearer;禁止仅靠 Session Cookie 作为 SPA 主鉴权路径(Session 若保留,仅作与/oauth/authorize浏览器跳转兼容的可选项,可逐步弱化)。
client_id / redirect_uri 是什么?为何常和「种子 SPA」绑在一起?
client_id:OAuth2 里标识 「哪一个客户端应用」(例如浏览器里的 SPA)。授权服务器(Go)在库里 登记过 的client_id才合法。redirect_uri:换发authorization_code之后,用户(或纯 API 流程)最终要把code送到的回调地址。为防止劫持,必须与该client_id在服务端登记的允许列表一致(ParseRedirectURIs/RedirectURIMatch)。- 「种子 SPA」:指迁移/安装时 预置的一条 OAuth 客户端(如
client_id=spa,redirect_uri含http://localhost:3000/oauth/callback与生产https://app.example.com/oauth/callback)。前端在 dev/prod 可共用同一client_id,只要 各环境redirect_uri都已登记;若种子只含一条 URI,开发跨域时需在 后端配置/数据库 里 补登记localhost回调,否则 token 交换会报invalid_redirect_uri。
推荐后端形态(与现有 Token 端复用)
- 扩展
POST /api/v1/auth/login请求体:除user_name、password、tenant_id外,携带code_challenge、code_challenge_method=S256、client_id、redirect_uri;可选state(成功时在响应data中原样返回)。须通过 与/oauth/authorize相同的client_id/redirect_uri校验。 - 校验通过后,写入 与 authorize 相同的 authorization_code(同一
oauth2.Store、PKCE 绑定)。HTTP 200,body:{ "code": 200, "msg": "操作成功", "data": { "authorization_code": "<code>", "state": "<echo>" } }(authorization_code与/oauth/token的code同义)。 /api/v1/auth/login密码错误:HTTP 401(认证失败),前端 只认状态码 走统一 401 处理链(与 token 失效同类;不在此用HTTP 200+body.code表示密码错)。参数不合法等非认证问题:HTTP 200+code≠200 等业务码(与上表一致)。- 前端拿到
authorization_code后POST /oauth/token(form)换 access_token / refresh_token;前端主路径不依赖 Session Cookie(Set-Cookie可选保留)。
Go 改造落点(备忘):在 LoginHandler 或抽取的 OAuth 服务方法中复用 oauth2.Service / Store 的 授权码创建能力;注意 rate limit、redirect_uri 校验、client_id 与种子 SPA 配置一致;更新 docs/oauth-v2.md 中 「JSON 登录」 小节与示例请求/响应。
| 步骤 | 前端要点 |
|---|---|
| 登录 | 生成 PKCE → POST /api/v1/auth/login(body 含挑战 + client + redirect_uri + 可选 state)→ 信封取 data.authorization_code。 |
| 换 token | POST /oauth/token(form,authorization_code + code_verifier)→ access_token / refresh_token;与全站 OAuth 流程一致。 |
| 调 API | /api/v1/** 带 Authorization: Bearer。 |
| 静默刷新 | access_token 将过期或 API 返回 401 时,先用 refresh_token 调 grant_type=refresh_token 无感换票;仅当 refresh 也失败(无 refresh、过期、服务端拒绝)再 弹登录框(再走 login→code→token)。apiClient 内建议 单飞刷新队列,避免并发请求重复 refresh。 |
401 时的交互(已定):先静默 refresh → 仍 401 再弹窗;弹窗 仅账号 + 密码 + PKCE;/api/v1/auth/login → authorization_code → /oauth/token;成功后 关弹窗并重试原请求。用户主动「退出」:POST /api/v1/auth/logout、清本地 token、多 Tab 可用 BroadcastChannel / localStorage 事件 同步登出(与 §4 一致)。
4. Zustand
useAuthStore:accessToken、refreshToken(存前端或安全存储策略团队定)、userId/tenantId、setTokens、logout(清状态 + 调auth/logout+ 广播多 Tab)。- 可选:
useTenantStore与菜单/权限缓存,与iam/menu的nav、perms联动。
5. Next.js 注意点
- App Router:OAuth callback、首次引导授权等放在
app/(auth)/...;业务页在app/(dashboard)/...。layout 可对「冷启动无 token」做跳转授权等策略;业务 API 返回 401 时优先用全局弹窗登录(见 §3),避免打断用户操作。 - 服务端组件默认不能带浏览器 Cookie 调浏览器域下的私有 API unless 使用 Route Handler 作 BFF;若希望「全 CSR + 直连 Go」,以 Client Component +
apiClient为主即可。 - Prettier:与 ESLint 一起在仓库根配置,与现有 Go 仓库分属不同目录时各自一份配置即可。
6. Radix UI
- 用于表单、Dialog、Dropdown、Navigation Menu / Collapsible、Tooltip(图标模式下图标旁展示完整菜单名)等;与业务页面解耦。
导航数据:后端驱动(强制约定)
目标:左侧导航(含经典树状 / 图标模式下的浮层或抽屉)所展示的 层级、顺序、可见项、展示名、图标字段(若后端提供) 均以 后端返回的菜单树为准;前端 不在生产环境写死业务菜单列表(开发环境可保留极少量占位路由仅用于 Storybook/演示,与正式壳分离)。
主数据源(与当前 Go 能力对齐)
GET /api/v1/iam/menu/nav:面向当前用户/租户的 导航菜单树(侧栏渲染首选)。- 必要时配合
GET /api/v1/iam/menu/perms等做 按钮级/路由级权限;与iam_menu及角色绑定一致。 - 若需全量配置态菜单(管理端「菜单管理」页),可用
/api/v1/iam/menu/tree等接口;运行时侧栏仍以 nav 为主,避免混用两套源。
前端职责
- 登录成功且具备 token 后 拉取 nav(及 perms),存入 Zustand/React Query 等,带 SWR/失效策略(切换租户、重新登录时 重新请求)。
- 将后端节点字段(如
path、component、perms、children)映射到 NextLink/router路径;若后端path与前端路由表不一致,维护 一层显式映射表(仍由后端数据驱动「显示哪些项」,映射只解决 URL 形状)。 - 经典模式:直接渲染树组件;图标模式:同一棵树做遍历,父节点走浮层/抽屉(见上文「图标模式下的二级、三级导航」)。
反模式(避免)
- 侧栏
MenuItem写死在layout.tsx内且与数据库菜单两套真相。 - 仅首屏拉一次菜单后永不刷新(租户切换、权限变更会不同步)。
整体页面与布局(管理端壳层)
目标:登录后进入 统一壳(顶栏 + 左侧导航 + 主内容区),业务模块(租户/部门/角色/用户/菜单/系统参数等)均在主内容区切换;登录/回调页 不使用 该壳,避免多余导航。
| 区域 | 职责 |
|---|---|
| 顶栏 | 左侧:Logo/产品名;右侧:用户信息区(见下「顶栏右侧:用户信息下拉」,内含侧栏经典/图标切换)、租户等全局摘要(按需);本阶段不在顶栏提供全局搜索框 |
| 左导航 | 见下文「双模式」;数据见上一节「导航数据:后端驱动」 |
| 主工作区 | 见下文「主工作区:Tabs 区 + 内容区」;非简单单页 children,而是 多页签 + 卡片式内容区(列表/表单等在内层渲染) |
顶栏右侧:用户信息下拉(产品约定)
- 位置:Header 最右侧(或紧挨全局操作区)展示当前用户摘要(建议:头像/占位图 + 显示名或账号)。
- 交互:鼠标悬停在用户区域上时,展开 下拉列表(可用 Radix
DropdownMenu配合onOpenChange/ 延迟关闭,或HoverCard+菜单组合;实现时注意 触控设备无悬停,需 点击同样可打开,并支持 Esc 关闭、键盘方向键,避免纯悬停导致无障碍与移动端不可用)。 - 下拉项(固定项,顺序建议如下):
- 个人中心 — 跳转前端路由(如
/account或/profile),展示当前用户资料;若后端暂无专用接口,可先读已有用户接口(如按user_idGET)或占位页,后续与 IAM 对齐。 - 修改密码 — 跳转
/account/password或 弹窗表单;提交时调用后端修改密码接口(若当前 Go 未暴露,计划中单列为「需补接口」或与iam/user更新密码能力对齐)。 - 侧栏布局 — 经典模式 / 图标模式 二选一(可用分段控件、单选行或两项可点菜单项),与
useShellStore的sidebarMode+localStorage一致;勿再单独放在顶栏。 - 退出 — 调用
POST /api/v1/auth/logout,清除前端 token/状态并跳转登录页。
- 个人中心 — 跳转前端路由(如
+------------------------------------------------------------------------+
| [Logo] Smart Admin [租户…] [○ 张三 ▼] ← 悬停/点击展开 |
+------------------------------------------------------------------------+
+---------------------------+
| 个人中心 |
| 修改密码 |
| 侧栏:[ 经典 | 图标 ] |
| 退出 |
+---------------------------+
主工作区:Tabs 区 + 内容区(产品约定)
导航栏 右侧为 主工作区,纵向分为两层:上部 Tabs 页签区 + 下部内容区。左侧菜单点击路由时,优先在 Tabs 中打开/激活对应页签(与常见「多标签后台」一致;实现可用 Zustand 维护页签列表与当前激活项)。
A. Tabs 页签区
| 项 | 约定 |
|---|---|
| 结构 | 顶部一条 Tabs 栏;每个 Tab 对应一个已打开页面(路由 + 关键参数可序列化进 tab id)。 |
| 固定首页签 | 最左侧固定 「概览」 页签(路由如 /dashboard 或 /overview),不可关闭、不可被「关闭全部」关闭。 |
| 右键菜单 | 在 页签条区域(建议在 某个 tab 标签上)点击右键弹出菜单(Radix Context Menu),包含:关闭、关闭左侧、关闭右侧、关闭全部(关闭全部时保留「概览」)。 |
| 横向溢出 | 当 Tab 过多超出可视宽度时:左侧、右侧各一个常驻小按钮(如 ‹ ›),用于将 Tab 条 向左/向右滚动;按钮始终占位可见(禁用态亦可,避免布局跳动)。Tab 容器使用 overflow-x: auto + scrollBy 或等效实现。 |
线框示意
| ◀ | [ 概览 | 用户管理 × | 角色管理 × | ... ] | ▶ |
^固定不可关^ ^右键关闭等^
B. 内容区(包裹在 Tabs 下方)
| 项 | 约定 |
|---|---|
| 外层边距 | 内容区相对主工作区容器:左、上、右 外边距均为 12px;外层背景为 浅灰(具体色值用设计 token,如 hsl 中性灰,勿写死纯黑字对比不足)。 |
| 内层卡片 | 实际承载页面的是 白色背景 容器;内边距 12px(与外边距统一节奏)。内容很短时,底部仍保持 至少 12px 外边距(可用 min-height + padding-bottom 或 flex 布局保证)。 |
| 圆角 | 白色主工作卡片 四角圆角(建议 8px–12px 或 Tailwind rounded-lg/rounded-xl,全局一致)。 |
C. 一般列表页(内容区内「标准模板」)
适用于租户/用户/角色等 表格类 页面,内容区内再分 上 / 中 / 下 三块:
| 区域 | 内容 | 布局 |
|---|---|---|
| 上部 · 查询条 | 左侧:操作区(常见:新增、批量删除 等,随业务增减);右侧:条件查询区(输入框、下拉等)。左右 两端对齐(justify-content: space-between 或 grid 两列)。 |
|
| 中部 · 表格 | 数据表格,列随实体定义;数据随查询条件变化(前端驱动请求参数,或由 Zustand/React Query 绑定 filter state)。 | |
| 下部 · 分页 | 分页器;每页条数 可选 10 / 20 / 50(默认可先 20);允许按业务页单独配置(通过 props 或页面级常量覆盖默认值)。 |
表格行内操作(约定)
| 操作 | 说明 |
|---|---|
| 修改 | 行级入口,跳转编辑页、侧滑/抽屉表单或行内编辑(按模块选型)。 |
| 删除 | 行级删除。 |
删除与批量删除(接口约定)
- 单条删除与**批量删除(工具栏)**共用 同一后端批量删除能力(例如
POST .../batch-delete或DELETE+ body 为ids: []);单条删除时传 仅含一个 id 的列表,避免维护两套删除路径与权限点。 - 前端:表格多选 + 「批量删除」与行内「删除」最终都走上述接口;确认弹窗文案区分「删除所选 N 条」与「删除本条」即可。
非列表页(表单页、详情页)可只使用白色卡片容器 + 内边距,不强制三区块,但 边距与圆角 与上表保持一致。
D. 内容区布局变体(除「纯表格式列表」外)
| 形态 | 适用场景 | 结构要点 |
|---|---|---|
| 左树 + 右表 + 分页 | 数据依赖树形上下文的列表(如 用户管理:左侧 部门/组织机构树,右侧用户表 + 查询条 + 分页) | 左侧树与右侧表 同一张灰底白卡片容器内或左右分栏两卡片;选中树节点作为查询条件(如 dept_id / org_id),切换节点时刷新右侧表格与分页回到第 1 页。 |
| 整页树(可带工具栏) | 层级数据本身即主对象(如 菜单管理、部分 目录/分类) | 主区为 可编辑树(拖拽排序、增删改节点等按后端能力);若需与列表混用,可树下方再挂从表(按产品)。 |
线框示意(左树右表,如用户管理)
灰底 12px
+-----------------------------------------------------------------------+
| +-- 白卡片 ----------------------------------------------------------------+ |
| | +------------------+ +------------------------------------------------+ | |
| | | 部门 / 组织树 | | [+新增] [批量删除] [条件…] [查询] [重置] | | |
| | | ▾ 公司 | +------------------------------------------------+ | |
| | | ▾ 研发部 | | 表头 | 操作(修改/删除) | … | | |
| | | ▾ 市场部 | | 数据 | [改][删] | … | | |
| | | … | +------------------------------------------------+ | |
| | | (可选宽 240–280) | | 共 N 条 [10|20|50/页] < 1 2 3 > | | |
| | +------------------+ +------------------------------------------------+ | |
| +-------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------+
E. 组织机构与用户管理是否合一(计划约定)
- 常见做法(推荐默认):**组织机构(部门树)**作为 用户管理 的左侧上下文,与 用户列表 放在 同一功能页 / 同一菜单入口(左树右表);用户的新增/编辑表单里 归属部门 与树联动。这样避免「组织」与「用户」两处维护同一棵部门树时的割裂感。
- 何时拆页:若产品要求 组织机构 单独做 编制、合并、禁用 等重操作,且与用户列表 强解耦,可另开 「部门管理」 子菜单;数据上仍与用户的
dept_id同源,前端避免重复实现两套树数据源(宜共用 hook / 同一deptAPI)。 - 结论:计划层面 默认采用「用户管理 = 部门树 + 用户表」一体化;是否再单列「组织机构」顶级菜单由产品命名决定(可做成 同路由别名 或 子 Tab:用户 | 部门),实现上不强制两套壳。
推荐实现(一种可落地方案)
| 层次 | 做法 |
|---|---|
| 路由 | 一个主路由承载左树右表,例如 /iam/users(与后端菜单 path 对齐)。部门相关「重操作」优先做 同页 Modal / 右侧抽屉 / 全屏子路由(如 /iam/users/dept),避免先复制一套独立壳再维护两份树状态。 |
| 部门树数据源 | 封装 useDeptTree(React Query:queryKey: ['dept', 'tree', tenantId]),全应用 仅此一处拉树;用户页、(若有的)部门管理页、用户表单里的部门选择器 都复用同一 query,树更新后 invalidateQueries(['dept', 'tree', …]) 一处失效即可。 |
| 选中部门 | 用 useState + 可选 URL ?deptId=(nuqs 或 Next useSearchParams):刷新、分享链接可恢复上下文;切换节点时 用户列表页码重置为 1。 |
| 用户列表 | useUserList({ deptId, page, pageSize, …filters }),deptId 来自选中节点(根节点可表示「全部」或 undefined 由后端约定);查询条与树筛选 合并为同一请求参数。 |
| 行内 / 批量删 | 与上文约定一致,走 同一 batch-delete;表格 rowSelection 与批量按钮共用 ids。 |
| 用户表单与树联动 | 新增/编辑用 受控的部门选择(下拉树、TreeSelect 或内嵌窄树);dept_id 初始值 = 当前左侧选中节点或行数据;若用户在表单里改部门,不必自动改左侧选中(避免抢焦点),保存成功后 刷新列表 即可。 |
| 部门 CRUD(轻) | 在树工具条放「新增子部门」「编辑」「禁用」→ Modal;成功后 invalidate 部门树 + 用户列表(若影响可见范围)。 |
| 部门 CRUD(重) | 若合并、批量迁移等交互很重,可 另开菜单 指向 独立页面,该页仍 import 同一套 features/dept 模块(hooks + api),禁止复制 DeptTree 组件实现。 |
小结:用 「单一路由 + 单一 dept tree query + 可选 searchParam 记录选中节点」 最省事;重功能用 子路由或抽屉 消化,数据层仍 一套 dept API、一套 React Query key 前缀。
计划采纳:上述 「推荐实现」表格 + 小结 为本方案 用户/部门一体化页的默认实现;后续除非产品另有要求,按此执行。
线框示意(列表页)
灰色背景 (外层 12px 边距)
+------------------------------------------------------------------+
| +-- 白色圆角卡片 (padding 12px) --------------------------------+ |
| | [+新增][批量删除] [条件A][条件B][查询][重置] | |
| | ---------------------------------------------------------------- | |
| | | 表头 | 表头 | ... | |
| | | 数据 | 数据 | ... | |
| | ---------------------------------------------------------------- | |
| | 共 N 条 [10|20|50/页] < 1 2 3 > | |
| +----------------------------------------------------------------+ |
+------------------------------------------------------------------+
整页综合线框图(Header + 左导航 + Tabs + 内容区标准列表)
+======================================================================================+
|| Header ||
|| [Logo] Smart [租户…] [○ 用户 ▼] ← 展开含侧栏经典/图标、退出等 ||
+===+==================================================================================+
|| || Tabs 栏(右键:关闭 / 关左 / 关右 / 关全部;概览不可关) ||
|| || +---+ +--------------------------------------------------------------------+ ||
|| || | ◀ | | 概览 | 用户× | 角色× | 租户× | …overflow… | ▶ | ||
|| || +---+ +--------------------------------------------------------------------+ ||
|| || ^固定^ ^滚动钮常显^ ||
|| || +-----------------------------------------------------------------------------+|
|| || | 灰底 12px 边距(左/上/右;底同) | ||
|| || | +-------------------------------------------------------------------------+ | ||
|| || | | 白底圆角卡片 (内边距 12px) | | ||
|| || | | [+新增] [批量删除] [条件…] [查询] [重置] ←上:左右对齐 | | ||
|| || | |-------------------------------------------------------------------------| | ||
|| || | | | 列1 | 列2 | 列3 | ... ←中:表格 | | ||
|| || | | | 数据| 数据| 数据| | | ||
|| || | |-------------------------------------------------------------------------| | ||
|| || | | 共 N 条 每页 [10▼] [20] [50] < 1 2 3 > ←下:分页 | | ||
|| || | +-------------------------------------------------------------------------+ | ||
|| || +-----------------------------------------------------------------------------+|
|| || ||
||左|| ||
||侧|| ||
||导|| ||
||航|| ||
|| || ||
||树|| ||
||状|| ||
|| || ||
+===+==================================================================================+
实现提示:Tabs 状态与路由 可双向同步(新开 tab 推 history 或仅内存,按团队选择);右键菜单项需 禁用态(例如当前 tab 左侧无 tab 时「关闭左侧」禁用)。
左导航:经典模式 vs 图标模式(约定含义)
业内常见两种叫法,与你描述的「经典 / 图标」一般对应如下(若你希望另一种交互,可再改一版文案):
| 模式 | 典型形态 | 体验要点 |
|---|---|---|
| 经典模式 | 侧栏 较宽(约 220–260px),每项 图标 + 文字 并排展示;多级菜单可展开/折叠,当前选中态清晰 | 信息密度高,适合首次使用与菜单项较多的管理后台 |
| 图标模式 | 侧栏 收窄(约 56–72px),仅显示图标;文字通过 Tooltip 或 悬停浮层 展示;子菜单可用 Popover / 侧滑面板 或点击展开窄条下的二级 | 主内容区更宽,适合熟练用户;类似 VS Code 活动栏、许多 SaaS 的「收起侧边栏」 |
实现要点(计划内约定):
- 使用 Zustand(如
useShellStore)保存sidebarMode: 'classic' | 'icon',并用localStorage持久化(键名如smart_sidebar_mode),刷新后保持用户选择。 - 布局用 CSS 变量或 Tailwind 控制侧栏宽度;
transition做宽度切换动画(可选)。 - 无障碍:图标模式下每个图标按钮必须带
aria-label,与 Tooltip 文案一致。 - 与 Radix:
Tooltip+NavigationMenu或自研侧栏;避免纯 div 堆叠导致键盘无法操作。
flowchart TB
subgraph shell [AppShell]
Top[顶栏]
subgraph left [左导航]
Classic[经典: 宽栏+图文]
IconOnly[图标: 窄栏+Tooltip]
end
Main[内容区]
end
Top --> left
left --> Main
线框图(ASCII)
经典模式(宽侧栏:图标 + 文字)
+----------------------------------------------------------------------------------+
| [Logo] Smart Admin [租户: 平台] [用户 ▼] [退出] ←「用户」下拉内含侧栏经典/图标 |
+----------+-----------------------------------------------------------------------+
| [i] 首页 | |
| [i] 系统 | 主内容区(列表 / 表单 / 详情) |
| 参数 | |
| [i] IAM | +---------------------------------------------------------------+ |
| 用户 | | 表格 / 卡片 / 步骤条 … | |
| 角色 | +---------------------------------------------------------------+ |
| ... | |
| | |
+----------+-----------------------------------------------------------------------+
^约 220–260px^
图标模式(窄侧栏:仅图标,文字用 Tooltip)
+----------------------------------------------------------------------------------+
| [Logo] Smart Admin [租户] [用户 ▼] [退出] ←「用户」下拉内含侧栏经典/图标 |
+--+-------------------------------------------------------------------------------+
|[]| |
|[]| 主内容区(更宽) |
|[]| |
|[]| +---------------------------------------------------------------+ |
|[]| | | |
|[]| +---------------------------------------------------------------+ |
| | |
+--+-------------------------------------------------------------------------------+
^约 56–72px^
悬停图标 → Tooltip「用户管理」;子菜单可 Popover 或右侧滑出
两种模式对比(同一壳,仅侧栏宽度与是否显示标签变化)
经典 图标
+--------------------+ +--+---------------+
| [≡] 用户管理 | |👤| 用户列表… |
| [≡] 角色管理 | ↔ |👥| |
| [≡] … | |⚙ | (Tooltip) |
+--------------------+ +--+---------------+
图标模式下的二级、三级导航(常见交互)
窄栏里无法像经典模式那样纵向展开整棵树,多级菜单通常用下面几类方式之一(可混用,按菜单深度与数量选型):
| 方案 | 行为 | 适用 |
|---|---|---|
| A. 右侧飞出级联面板 | 点击一级图标 → 在侧栏右侧弹出 浮层 1 列出二级;在某项上悬停或点击 → 再向右弹出 浮层 2 列三级(「手风琴式」级联,类似旧版 Windows 开始菜单多级) | 二、三级都多、需要快速扫视 |
| B. 单层面板 + 树/分组 | 点击一级图标 → 一个较宽的 Popover / DropdownMenu 内用 可折叠分组 或 缩进树 展示二、三级(可滚动) | 总项数中等、希望少移动鼠标 |
| C. 抽屉 | 点击带子女的图标 → 从左侧 滑出抽屉,内部为完整树(与经典侧栏同结构,只是按需出现) | 层级很深或名称很长 |
| D. 临时加宽 | 点击某父级后,侧栏在图标列旁 临时展开一条「迷你文字列」 仅显示该支路的二、三级 | 想兼顾窄栏与可读性 |
线框示意(方案 A:向右级联)
侧栏(窄) 浮层1(二级) 浮层2(三级)
+--+ +-----------+ +-----------+
|👤| 点击 → | 用户管理 | | 列表用户 |
| | | 角色管理 | hover| 导入用户 |
| | | 部门 ─────┼────→| 导出 |
+--+ +-----------+ +-----------+
线框示意(方案 B:单 Popover 内多级)
+--+
|👤| 点击 → +-------------------------+
+--+ | ▼ IAM |
| 用户管理 |
| 角色管理 |
| ▼ 系统 |
| 参数配置 ← 三级作为子项 |
+-------------------------+
计划约定:实现时 同一套菜单树数据(如 menu/nav)驱动经典树与图标模式;图标模式需为 带 children 的节点 绑定上述一种交互,并在设计稿中统一 键盘操作(Esc 关闭、方向键在级联中移动,可与 Radix DropdownMenu / NavigationMenu 能力对齐)。
实施顺序建议
- 初始化 Next 项目(TS + Prettier),配置
NEXT_PUBLIC_API_ORIGIN。 - (Go) 按 §3 改造
POST /api/v1/auth/login:密码通过后签发 PKCE 绑定code,与/oauth/token复用;同步更新docs/oauth-v2.md。 - 实现
apiClient+api/auth,跑通 login → code → token、logout 与一条 IAM 只读接口(如menu/nav)。 - 搭建
app/(dashboard)/layout(或等价)实现 AppShell + 左导航双模式 与顶栏,菜单对接menu/nav。 - 按模块把 IAM、System 全部方法补齐(与
http_register路径一一对应),页面放入内容区。 - 接入 OAuth2(PKCE + callback + token 存储),与现有 Go 的
client_id/redirect_uri配置一致(种子spa与configs/local.yml中frontend_login_url)。 - Go 侧确认 CORS + Cookie 策略 与生产 HTTPS。
- (可选)后端增加 OpenAPI/Swagger 后,用 codegen 替换手写 DTO。
产品口径与待确认项
已定口径(按当前共识写入计划)
| 项 | 决定 |
|---|---|
| 鉴权 | 仅 OAuth2 Code + PKCE + Bearer;/api/v1/auth/login 须改造为签发 PKCE 绑定 code 并与 /oauth/token 同一套逻辑(当前 Go 仅为 Session,属待改偏差)。 |
| 租户切换 | 必须关闭所有 Tabs(页签状态清空),并 重新拉 menu/nav、dept/tree 及当前内容区数据(与 tenantId / token 声明一致)。 |
| 权限 · 第一期 | 仅做路由级守卫(未登录或整页无权限 → 拦截/提示);不做表格内每颗按钮与 perms 的细粒度联动(后续迭代再加)。 |
| 批量删除 | 与 Go 路由对齐;若某资源 没有 batch 接口,实现侧先通知负责人,由负责人决定补接口或临时方案,不擅自拍板。 |
| HTTP 与 body.code | 401 = 认证失败(含登录密码错误),前端以 HTTP 为准;403 = 无权限;其余业务 HTTP 200 + 体 code。密码错误不单独用 200+业务码表达。 |
| JSON 与 state | 信封 { code, msg, data };auth/login 可带 state,成功 data 回显。 |
| 401 / refresh | 一般 API 的 401 → 先 refresh,失败再弹窗;POST /auth/login 的 401(密码错等)→ 不 refresh,直接按需弹窗(§3)。 |
| 403 | HTTP 403 → toast,留在当前页,不弹登录框。 |
| 环境 | 生产同域;开发跨域 → Go CORS 白名单 dev origin;种子 SPA 的 redirect_uri 须含各环境回调(见 §3 client_id 说明)。 |
| 登出 | POST /auth/logout + 清前端 token;多 Tab 用 BroadcastChannel / storage 事件 同步登出。 |
术语说明(白话)
部门树:根节点 =「全部」
- 含义:左侧树最顶层(或「未选中具体部门」)表示 不按部门过滤,用户列表查 「全部」(具体 query 与后端约定)。
- 是否含停用:部门可能被 停用,需约定树里 是否仍显示这些节点(显示则可选中;不显示则界面更干净)。
- 是否懒加载:懒加载 = 先只拉根,展开再拉子节点;非懒加载 = 一次拉整棵树。部门特别多时常用懒加载;树小可一次拉齐。
Tabs 要不要写进 URL
- 只存在内存:开了多个页签,一刷新浏览器页签全没——可接受则实现简单。
- 写进 URL:刷新或 分享链接 能恢复多页签状态;实现更复杂,第一期不强制。
部门「轻」vs「重」/ 独立页
- 轻:在用户管理页用 Modal 做 加子部门、改名 等简单维护。
- 重:整页做 合并部门、批量迁移、复杂拖拽 等,才需要 单独「部门管理」菜单。MVP 建议先轻后重,复用同一套
deptAPI。
仍待与后端核对
| 项 | 说明 |
|---|---|
| 批量删除契约 | 路径、ids、软删以 Gin 为准;缺接口时通知负责人。 |
| 部门树 · 停用与懒加载 | 与后端行为对齐;未约定则联调时定一版。 |
| Tabs 是否进 URL | 可第一期 仅内存,后续再加。 |
风险与约束
- 「所有接口」 以当前 Gin 注册为准;若后续新增路由,前端需同步增加
api/*方法。 - Bearer 中间件对无 token 请求不 401;业务上需登录由路由守卫与
apiClient:先 refresh、失败再弹登录框 协同处理。 - 工作区若与 Go 仓库分离,请用 同一文档 维护 baseURL 与
client_id/redirect 列表。