Files
smart-go/.cursor/plans/next_全量对接_go_api_e862d400.plan.md
T
2026-04-23 18:58:13 +08:00

51 KiB
Raw Blame History

name, overview, todos, isProject
name overview todos isProject
Next 全量对接 Go API 全栈对接 Go APIOAuth2 Code + PKCE**401/403 与 200+`code` 分工****含:密码错=401,只看状态**);JSON 信封;login 的 401 不 refreshdev 跨域;登出多 Tabauth/login 改造 §3;租户关 Tabs;路由守卫。
id content status
init-next-env 初始化 Next+TS+Prettier,配置 NEXT_PUBLIC_API_ORIGIN 与目录结构(api/、stores/ completed
id content status
api-client-auth 实现 apiClientJSON + BearerOAuth token 用 form);auth/login 取 code 再 oauth/tokenlogoutZustand useAuthStore completed
id content status
go-auth-login-pkce Go:改造 POST /api/v1/auth/login 密码通过后签发 PKCE 绑定 code,与 CreateAuthorizationCode+token 复用;文档 oauth-v2 对齐 completed
id content status
api-iam-system 按 http_register 补齐 iamtenant/dept/role/user/menu)与 system/param 全部封装 completed
id content status
oauth-pkce-ui 实现 OAuth2 PKCE 授权链接、callback 页、换 token 与 token 刷新策略 completed
id content status
cors-cookie 与 Go 对齐 CORS、Cookie SameSite/credentials;生产 HTTPS 检查清单 completed
id content status
layout-shell-nav AppShell;顶栏用户下拉;侧栏后端 nav;经典/图标+localStoragepath 映射路由(首版 web 为顶栏+入口,侧栏与双模式待迭代) completed
id content status
workbench-tabs-content 主工作区 Tabs+页签状态+内容区(首版 TabStrip 骨架;右键/滚动钮/标准列表/左树右表待迭代) completed
false

Next.js 前端全量对接 Go 后端方案

后端路由清单(对接范围)

以下均基于当前仓库中的注册代码(internal/auth/http_register.gointernal/iam/http_register.gointernal/system/http_register.gointernal/server/http.go)。

前缀 说明
GET /health 探活
根路径 OAuth GET /oauth/authorizePOST /oauth/tokenPOST /oauth/introspect /api/v1 前缀)
/api/v1 全组挂载 Bearer 中间件(无 token 仍放行,见 internal/auth/middleware/bearer.go

/api/v1 下具体接口:

  • AuthPOST /api/v1/auth/loginPOST /api/v1/auth/logout
  • IAMinternal/iam/http_register.go):/api/v1/iam/tenant/*/dept/*/role/*/user/*/menu/*(含 treenavperms 等)
  • System/api/v1/system/param/*create/update/delete-batch/get/list

前端需要为上述路径提供 baseURL 配置(例如 NEXT_PUBLIC_API_ORIGIN=http://127.0.0.1:8000),并区分两类调用:

  • 业务 JSON APIfetch(\${origin}/api/v1/...`)`
  • OAuthfetch(\${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 禁止 * 若带 credentialsBearer 主路径下跨域以 Authorization 为主,但仍需为 预检 OPTIONS错误 JSON 配好 CORS。

2. HTTP 客户端层(对接「所有接口」的核心)

  • 封装单一 apiClientfetchky/axios 二选一),职责:
    • 统一 baseURLContent-Type: application/jsonOAuth 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/logout
    • api/oauth.ts — token、(若前端自己做 PKCEauthorize URL 构造
    • api/iam/tenant.tsdept.tsrole.tsuser.tsmenu.ts
    • api/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 端点可仍按 RFCdocs/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/tokengrant_type=authorization_code)换 opaque access_token;业务请求 Authorization: Bearer。见 docs/oauth-v2.md

现状与目标(后端)

  • 现状(偏差)internal/auth/handler/login.goPOST /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_idOAuth2 里标识 「哪一个客户端应用」(例如浏览器里的 SPA)。授权服务器(Go)在库里 登记过client_id 才合法。
  • redirect_uri:换发 authorization_code 之后,用户(或纯 API 流程)最终要把 code 送到的回调地址。为防止劫持,必须与该 client_id 在服务端登记的允许列表一致ParseRedirectURIs / RedirectURIMatch)。
  • 「种子 SPA」:指迁移/安装时 预置的一条 OAuth 客户端(如 client_id=sparedirect_urihttp://localhost:3000/oauth/callback 与生产 https://app.example.com/oauth/callback)。前端在 dev/prod 可共用同一 client_id,只要 各环境 redirect_uri 都已登记;若种子只含一条 URI,开发跨域时需在 后端配置/数据库补登记 localhost 回调,否则 token 交换会报 invalid_redirect_uri

推荐后端形态(与现有 Token 端复用)

  1. 扩展 POST /api/v1/auth/login 请求体:除 user_namepasswordtenant_id 外,携带 code_challengecode_challenge_method=S256client_idredirect_uri可选 state(成功时在响应 data 中原样返回)。须通过 /oauth/authorize 相同client_id / redirect_uri 校验。
  2. 校验通过后,写入 与 authorize 相同的 authorization_code(同一 oauth2.Store、PKCE 绑定)。HTTP 200body{ "code": 200, "msg": "操作成功", "data": { "authorization_code": "<code>", "state": "<echo>" } }authorization_code/oauth/tokencode 同义)。
  3. /api/v1/auth/login 密码错误HTTP 401(认证失败),前端 只认状态码 走统一 401 处理链(与 token 失效同类;不在此用 HTTP 200 + body.code 表示密码错)。参数不合法等非认证问题HTTP 200 + code≠200 等业务码(与上表一致)。
  4. 前端拿到 authorization_codePOST /oauth/tokenform)换 access_token / refresh_token前端主路径不依赖 Session CookieSet-Cookie 可选保留)。

Go 改造落点(备忘):在 LoginHandler 或抽取的 OAuth 服务方法中复用 oauth2.Service / Store授权码创建能力;注意 rate limitredirect_uri 校验client_id 与种子 SPA 配置一致;更新 docs/oauth-v2.md「JSON 登录」 小节与示例请求/响应。

步骤 前端要点
登录 生成 PKCE → POST /api/v1/auth/loginbody 含挑战 + client + redirect_uri + 可选 state)→ 信封取 data.authorization_code
换 token POST /oauth/tokenformauthorization_code + code_verifier)→ access_token / refresh_token;与全站 OAuth 流程一致。
调 API /api/v1/**Authorization: Bearer
静默刷新 access_token 将过期或 API 返回 401 时,refresh_tokengrant_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

  • useAuthStoreaccessTokenrefreshToken(存前端或安全存储策略团队定)、userId/tenantIdsetTokenslogout(清状态 + 调 auth/logout + 广播多 Tab)。
  • 可选:useTenantStore 与菜单/权限缓存,与 iam/menunavperms 联动。

5. Next.js 注意点

  • App RouterOAuth 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 / CollapsibleTooltip(图标模式下图标旁展示完整菜单名)等;与业务页面解耦。

导航数据:后端驱动(强制约定)

目标:左侧导航(含经典树状 / 图标模式下的浮层或抽屉)所展示的 层级、顺序、可见项、展示名、图标字段(若后端提供) 均以 后端返回的菜单树为准;前端 不在生产环境写死业务菜单列表(开发环境可保留极少量占位路由仅用于 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/失效策略(切换租户、重新登录时 重新请求)。
  • 将后端节点字段(如 pathcomponentpermschildren映射到 Next Link/router 路径;若后端 path 与前端路由表不一致,维护 一层显式映射表(仍由后端数据驱动「显示哪些项」,映射只解决 URL 形状)。
  • 经典模式:直接渲染树组件;图标模式:同一棵树做遍历,父节点走浮层/抽屉(见上文「图标模式下的二级、三级导航」)。

反模式(避免)

  • 侧栏 MenuItem 写死在 layout.tsx 内且与数据库菜单两套真相。
  • 仅首屏拉一次菜单后永不刷新(租户切换、权限变更会不同步)。

整体页面与布局(管理端壳层)

目标:登录后进入 统一壳顶栏 + 左侧导航 + 主内容区),业务模块(租户/部门/角色/用户/菜单/系统参数等)均在主内容区切换;登录/回调页 不使用 该壳,避免多余导航。

区域 职责
顶栏 左侧:Logo/产品名;右侧:用户信息区(见下「顶栏右侧:用户信息下拉」,内含侧栏经典/图标切换)、租户等全局摘要(按需);本阶段不在顶栏提供全局搜索框
左导航 见下文「双模式」;数据见上一节「导航数据:后端驱动」
主工作区 见下文「主工作区:Tabs 区 + 内容区」;非简单单页 children,而是 多页签 + 卡片式内容区(列表/表单等在内层渲染)

顶栏右侧:用户信息下拉(产品约定)

  • 位置Header 最右侧(或紧挨全局操作区)展示当前用户摘要(建议:头像/占位图 + 显示名或账号)。
  • 交互鼠标悬停在用户区域上时,展开 下拉列表(可用 Radix DropdownMenu 配合 onOpenChange / 延迟关闭,或 HoverCard+菜单组合;实现时注意 触控设备无悬停,需 点击同样可打开,并支持 Esc 关闭、键盘方向键,避免纯悬停导致无障碍与移动端不可用)。
  • 下拉项(固定项,顺序建议如下)
    1. 个人中心 — 跳转前端路由(如 /account/profile),展示当前用户资料;若后端暂无专用接口,可先读已有用户接口(如按 user_id GET)或占位页,后续与 IAM 对齐。
    2. 修改密码 — 跳转 /account/password弹窗表单;提交时调用后端修改密码接口(若当前 Go 未暴露,计划中单列为「需补接口」或与 iam/user 更新密码能力对齐)。
    3. 侧栏布局经典模式 / 图标模式 二选一(可用分段控件、单选行或两项可点菜单项),与 useShellStoresidebarMode + localStorage 一致;勿再单独放在顶栏
    4. 退出 — 调用 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 布局保证)。
圆角 白色主工作卡片 四角圆角(建议 8px12px 或 Tailwind rounded-lg/rounded-xl,全局一致)。

C. 一般列表页(内容区内「标准模板」)

适用于租户/用户/角色等 表格类 页面,内容区内再分 上 / 中 / 下 三块:

区域 内容 布局
上部 · 查询条 左侧:操作区(常见:新增批量删除 等,随业务增减);右侧:条件查询区(输入框、下拉等)。左右 两端对齐justify-content: space-between 或 grid 两列)。
中部 · 表格 数据表格,列随实体定义;数据随查询条件变化(前端驱动请求参数,或由 Zustand/React Query 绑定 filter state)。
下部 · 分页 分页器;每页条数 可选 10 / 20 / 50(默认可先 20);允许按业务页单独配置(通过 props 或页面级常量覆盖默认值)。

表格行内操作(约定)

操作 说明
修改 行级入口,跳转编辑页、侧滑/抽屉表单或行内编辑(按模块选型)。
删除 行级删除。

删除与批量删除(接口约定)

  • 单条删除与**批量删除(工具栏)**共用 同一后端批量删除能力(例如 POST .../batch-deleteDELETE + body 为 ids: []);单条删除时传 仅含一个 id 的列表,避免维护两套删除路径与权限点。
  • 前端:表格多选 + 「批量删除」与行内「删除」最终都走上述接口;确认弹窗文案区分「删除所选 N 条」与「删除本条」即可。

非列表页(表单页、详情页)可只使用白色卡片容器 + 内边距,不强制三区块,但 边距与圆角 与上表保持一致。

D. 内容区布局变体(除「纯表格式列表」外)

形态 适用场景 结构要点
左树 + 右表 + 分页 数据依赖树形上下文的列表(如 用户管理:左侧 部门/组织机构树,右侧用户表 + 查询条 + 分页) 左侧树与右侧表 同一张灰底白卡片容器内或左右分栏两卡片;选中树节点作为查询条件(如 dept_id / org_id),切换节点时刷新右侧表格与分页回到第 1 页。
整页树(可带工具栏) 层级数据本身即主对象(如 菜单管理、部分 目录/分类 主区为 可编辑树(拖拽排序、增删改节点等按后端能力);若需与列表混用,可树下方再挂从表(按产品)。

线框示意(左树右表,如用户管理)

 灰底 12px
+-----------------------------------------------------------------------+
| +-- 白卡片 ----------------------------------------------------------------+ |
| | +------------------+  +------------------------------------------------+ | |
| | | 部门 / 组织树     |  | [+新增] [批量删除]     [条件…] [查询] [重置]   | | |
| | | ▾ 公司            |  +------------------------------------------------+ | |
| | |   ▾ 研发部        |  | 表头 | 操作(修改/删除) | …                        | | |
| | |   ▾ 市场部        |  | 数据 | [改][删]       | …                        | | |
| | |   …               |  +------------------------------------------------+ | |
| | | (可选宽 240280)  |  | 共 N 条  [10|20|50/页]  < 1 2 3 >                  | | |
| | +------------------+  +------------------------------------------------+ | |
| +-------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------+

E. 组织机构与用户管理是否合一(计划约定)

  • 常见做法(推荐默认)**组织机构(部门树)**作为 用户管理 的左侧上下文,与 用户列表 放在 同一功能页 / 同一菜单入口(左树右表);用户的新增/编辑表单里 归属部门 与树联动。这样避免「组织」与「用户」两处维护同一棵部门树时的割裂感。
  • 何时拆页:若产品要求 组织机构 单独做 编制、合并、禁用 等重操作,且与用户列表 强解耦,可另开 「部门管理」 子菜单;数据上仍与用户的 dept_id 同源,前端避免重复实现两套树数据源(宜共用 hook / 同一 dept API)。
  • 结论:计划层面 默认采用「用户管理 = 部门树 + 用户表」一体化;是否再单列「组织机构」顶级菜单由产品命名决定(可做成 同路由别名子 Tab:用户 | 部门),实现上不强制两套壳。

推荐实现(一种可落地方案)

层次 做法
路由 一个主路由承载左树右表,例如 /iam/users(与后端菜单 path 对齐)。部门相关「重操作」优先做 同页 Modal / 右侧抽屉 / 全屏子路由(如 /iam/users/dept),避免先复制一套独立壳再维护两份树状态。
部门树数据源 封装 useDeptTreeReact QueryqueryKey: ['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 图标模式(约定含义)

业内常见两种叫法,与你描述的「经典 / 图标」一般对应如下(若你希望另一种交互,可再改一版文案):

模式 典型形态 体验要点
经典模式 侧栏 较宽(约 220260px),每项 图标 + 文字 并排展示;多级菜单可展开/折叠,当前选中态清晰 信息密度高,适合首次使用与菜单项较多的管理后台
图标模式 侧栏 收窄(约 5672px),仅显示图标;文字通过 Tooltip悬停浮层 展示;子菜单可用 Popover / 侧滑面板 或点击展开窄条下的二级 主内容区更宽,适合熟练用户;类似 VS Code 活动栏、许多 SaaS 的「收起侧边栏」

实现要点(计划内约定):

  • 使用 Zustand(如 useShellStore)保存 sidebarMode: 'classic' | 'icon',并用 localStorage 持久化(键名如 smart_sidebar_mode),刷新后保持用户选择。
  • 布局用 CSS 变量或 Tailwind 控制侧栏宽度;transition 做宽度切换动画(可选)。
  • 无障碍:图标模式下每个图标按钮必须带 aria-label,与 Tooltip 文案一致。
  • RadixTooltip + 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   |   +---------------------------------------------------------------+   |
|   用户    |   |  表格 / 卡片 / 步骤条 …                                        |   |
|   角色    |   +---------------------------------------------------------------+   |
|   ...     |                                                                       |
|           |                                                                       |
+----------+-----------------------------------------------------------------------+
     ^约 220260px^

图标模式(窄侧栏:仅图标,文字用 Tooltip)

+----------------------------------------------------------------------------------+
| [Logo]  Smart Admin          [租户]    [用户 ▼]  [退出]  ←「用户」下拉内含侧栏经典/图标      |
+--+-------------------------------------------------------------------------------+
|[]|                                                                               |
|[]|                        主内容区(更宽)                                         |
|[]|                                                                               |
|[]|   +---------------------------------------------------------------+           |
|[]|   |                                                              |           |
|[]|   +---------------------------------------------------------------+           |
|  |                                                                               |
+--+-------------------------------------------------------------------------------+
 ^约 5672px^
  悬停图标 → 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 能力对齐)。


实施顺序建议

  1. 初始化 Next 项目(TS + Prettier),配置 NEXT_PUBLIC_API_ORIGIN
  2. Go 按 §3 改造 POST /api/v1/auth/login:密码通过后签发 PKCE 绑定 code,与 /oauth/token 复用;同步更新 docs/oauth-v2.md
  3. 实现 apiClient + api/auth,跑通 login → code → token、logout 与一条 IAM 只读接口(如 menu/nav)。
  4. 搭建 app/(dashboard)/layout(或等价)实现 AppShell + 左导航双模式 与顶栏,菜单对接 menu/nav
  5. 按模块把 IAM、System 全部方法补齐(与 http_register 路径一一对应),页面放入内容区。
  6. 接入 OAuth2PKCE + callback + token 存储),与现有 Go 的 client_id/redirect_uri 配置一致(种子 spaconfigs/local.ymlfrontend_login_url)。
  7. Go 侧确认 CORS + Cookie 策略 与生产 HTTPS。
  8. (可选)后端增加 OpenAPI/Swagger 后,用 codegen 替换手写 DTO。

产品口径与待确认项

已定口径(按当前共识写入计划)

决定
鉴权 仅 OAuth2 Code + PKCE + Bearer/api/v1/auth/login 须改造为签发 PKCE 绑定 code 并与 /oauth/token 同一套逻辑(当前 Go 仅为 Session,属待改偏差)。
租户切换 必须关闭所有 Tabs(页签状态清空),并 重新拉 menu/navdept/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 403toast,留在当前页,不弹登录框
环境 生产同域开发跨域 → Go CORS 白名单 dev origin;种子 SPA 的 redirect_uri 须含各环境回调(见 §3 client_id 说明)。
登出 POST /auth/logout + 清前端 token多 TabBroadcastChannel / storage 事件 同步登出。

术语说明(白话)

部门树:根节点 =「全部」

  • 含义:左侧树最顶层(或「未选中具体部门」)表示 不按部门过滤,用户列表查 「全部」(具体 query 与后端约定)。
  • 是否含停用:部门可能被 停用,需约定树里 是否仍显示这些节点(显示则可选中;不显示则界面更干净)。
  • 是否懒加载懒加载 = 先只拉根,展开再拉子节点非懒加载 = 一次拉整棵树。部门特别多时常用懒加载;树小可一次拉齐。

Tabs 要不要写进 URL

  • 只存在内存:开了多个页签,一刷新浏览器页签全没——可接受则实现简单。
  • 写进 URL:刷新或 分享链接 能恢复多页签状态;实现更复杂,第一期不强制

部门「轻」vs「重」/ 独立页

  • :在用户管理页用 Modal加子部门、改名 等简单维护。
  • 整页合并部门、批量迁移、复杂拖拽 等,才需要 单独「部门管理」菜单MVP 建议先轻后重,复用同一套 dept API。

仍待与后端核对

说明
批量删除契约 路径、ids、软删以 Gin 为准;缺接口时通知负责人
部门树 · 停用与懒加载 与后端行为对齐;未约定则联调时定一版。
Tabs 是否进 URL 可第一期 仅内存,后续再加。

风险与约束

  • 「所有接口」 以当前 Gin 注册为准;若后续新增路由,前端需同步增加 api/* 方法。
  • Bearer 中间件对无 token 请求不 401业务上需登录由路由守卫与 apiClient:先 refresh、失败再弹登录框 协同处理。
  • 工作区若与 Go 仓库分离,请用 同一文档 维护 baseURL 与 client_id/redirect 列表。