搜尋開發指南

試試「多租戶」、「啟動」或「outbox」。
搜尋僅使用本站文件,不會傳送至外部服務。

文件目錄

NEXUSPRO / HANDBOOK

前端專案結構與資料流

從 App Router、proxy、身份上下文到 SWR 與 Ant Design,沿著一條真實請求路徑讀懂前端。

內容核對 2026-09-28·圖文指南

本章目標

讀完後,你應能從一個頁面 route 找到它的頁面閘、owner、API hook、schema、身份與租戶邊界,並說明瀏覽器為什麼只打同源 /v1/**。本文描述目前原始碼形狀,不把 mock、fixture 或本地文件站測試當成業務驗收。

前置條件

先閱讀快速開始並準備 Node、pnpm 及前端 checkout。前端實際 scripts、版本與環境變數以 nexus-pro-web-plus/package.json、.nvmrc 和 .env.example 為準;不要複製任何 .env.local、token 或帳號值。前端 AGENTS.md 優先使用 nub run,pnpm 亦可,兩者都執行 package.json 的同名 script。

目錄與 ownership

text
app/                  # App Router 頁面、route group、layout 與 BFF Route Handler(app/api/*)
app/api/auth/         # Keycloak OIDC/PKCE、站內密碼登入、refresh、logout 與 httpOnly cookie 寫入
features/             # 多數業務域的 API、view-model、頁面與測試(forms、approval-roles、hr-* 等)
hooks/api/            # 身份、選單、IAM 治理、HR lookup 等 SWR hook 與 Zod schema;範本 useExampleApi.ts
libs/api/             # axios transport、envelope、錯誤、casing 邊界與分頁 helper
libs/auth*、proxy.ts  # cookie 名稱、路徑分類、全域登出;/v1/** 同源轉發與頁面守衛
libs/permission/      # PageGate、Can、權限集合與身份快取失效
libs/navigation/      # 後端選單投影、路由解析與側欄群組偏好
components/           # 跨頁共用 UI:資料表、feedback、layout、控制元件
constants/、store/    # theme token、錯誤碼、UI 尺寸等常數;Jotai atom(目前只有 AI 面板狀態)
test/                 # Vitest setup、MSW handler、fixture、契約與路由閘測試

業務頁目前有兩種既有放法:多數業務域把 API、view-model 與頁面元件放在 features/<domain>/;IAM 治理頁、/tasks、首頁與部分 workspace 頁則是 app/(platform)/<route>/_components/ 搭配 hooks/api/(例如 /iam/roles 直接使用 hooks/api/useIamRolesApi.ts)。前端 AGENTS.md §3、§7 與 docs/api-architecture-plan.md 目前的寫法是 hook 建在 hooks/api/、頁面私有 UI 放 app/<route>/_components/,尚未描述 features/;新增程式先沿用同一業務域的既有位置,規則衝突時以前端 AGENTS.md 為準。

docs/tech-stack.md 與前端 AGENTS.md §2 選用 Next.js App Router、React、TypeScript strict、Ant Design v6、antd-style 與 Emotion、SWR、Jotai、axios、Zod,測試用 Vitest、Testing Library 與 MSW;不引入第二套 UI library、Tailwind、styled-components、i18n 或 Sentry,UI 文案直接寫 zh-TW。不要因參考網站使用其他 UI library,就在本專案引入第二套元件系統。

一條真實請求路徑

text
Browser /v1/forms/...
  → proxy.ts(同源 rewrite + server-side Authorization)
  → nexus-pro-be-plus Gin route
  → Service / Repository / PostgreSQL
  → { data: ... } response
  → axios camelize + fetcher unwrap + Zod schema
  → SWR hook → feature view-model → AntD UI

proxy.ts 只把 /v1/** rewrite 到 NEXUS_API_BASE_URL;沒有 /api/x → /v1/x 的第二套路由表。瀏覽器 JavaScript 不讀 access token,server-side proxy 從 httpOnly cookie _t 注入 Authorization。NEXT_PUBLIC_API_BASE_URL 留空時維持同源,避免 cookie、CORS 與 token 注入失效。另外三個邊界行為:

  • 未設定 NEXUS_API_BASE_URL 時,proxy 直接回 503 與 code 10500 的錯誤信封,不會落到 Next 的 404。
  • 沒有 access token 仍照樣轉發、不帶 Authorization,由後端回 401/10100;前端不自己判定認證失敗。
  • 頁面守衛只檢查 _t 或 _session cookie 是否存在、不驗簽。沒有會話且不是公開頁(登入、註冊、隱私權、條款、403、404、/api/auth/* 與 health probe)時導向 /login?returnUrl=…;已有會話再開 /login 則導回 /。

路由群組與頁面閘

app/ 分成三個 route group:(auth) 是登入與註冊,(public) 是 403、404、隱私權與條款頁,(platform) 是登入後的產品區,由 app/(platform)/layout.tsx 掛上側欄與頂欄外殼 PlatformShell。

(platform) 底下每條產品路由都在自己的 layout.tsx 掛 PageGate,path 字面值等於該目錄路徑。test/routes/gates.test.ts 會機械檢查漏掛閘、path 寫錯,以及頁面路徑是否已在後端 page tree 註冊(本地快照為 test/fixtures/routeRegistry.json)。

檔案式路由沒有的路徑由 app/(platform)/[...segments]/page.tsx 承接:透過 libs/navigation/routeResolver.ts 在可見選單樹中精確比對 path,依 route_kind 顯示待開發頁、把目錄座標轉向第一個可開的子頁(沒有時顯示空目錄)、顯示未配置頁,或顯示 404 畫面。這個 catch-all 刻意沒有閘,授權仍由後端 API 決定。

認證、權限與租戶上下文

登入流程都在 BFF:/api/auth/login 產生 PKCE 與 state 後導向 Keycloak,/api/auth/callback 驗 state 並以 code 換 token;登入頁另有站內密碼登入 /api/auth/password-login(Keycloak Direct Access Grants)。會話 cookie 只由 app/api/auth/_session.ts 寫入且全部 httpOnly,refresh token 與 id_token 的 path 限定 /api/auth。libs/auth.ts 只負責全域登出:呼叫 /api/auth/logout 清 cookie,再導向它回傳的位址(Keycloak end_session 或 /login)。

GET /v1/me 由 hooks/api/useMeApi.ts 的 meSchema 驗證,提供 account、tenant、可為 null 的 employee、effectivePermissions、groups 和 elevationSession。isAccountUsable、isTenantUsable 對未知狀態 fail closed,但目前沒有執行期呼叫端(只有測試使用);帳號停用、租戶停權實際由後端 10101/10102 觸發 libs/api/client.ts 登出。

選單可見性來自 /v1/me/menus:nodes 與 pageKeys 給 PageGate 做精確 path 判斷,navigation 是後端已放置與排序的側欄,群組、順序與跨端切換都來自這裡,前端不另存選單表。按鈕或 action 能否操作來自 effective_permissions;兩者不能互相推導。治理寫入成功後用 libs/permission/revalidateIdentity.ts 同時重抓 /v1/me 與 /v1/me/menus。

需要身份的 hook 應把 tenant.id、account.id 或其他影響資料的條件納入 SWR key,身份切換時重設 local draft,避免把上一個租戶的 rows 留在畫面上。features/approval-roles/api.ts 是目前的完整範例;/tasks 另以身份為 key 的 SWRConfig provider 隔離整份快取。hooks/api/useExampleApi.ts 範本與多數 IAM hook(例如 useIamRolesApi、useIamGroupsApi)的 key 目前不含身份,新 hook 的資料若隨租戶或帳號改變,要自行加入。

ts
// 示例如下,對照 features/approval-roles/api.ts 簡化;實際 path、schema、sort 白名單與權限點需依該 feature 契約,不能直接複製執行。
const key = me && canRead ? [PATH, me.tenant.id, me.account.id, page, search] : null;
const result = useSWR(key, () =>
  fetcher({
    url: PATH,
    schema: pageEnvelopeSchema(itemSchema),
    config: { params: { page, pageSize: 20, sort, ...(search ? { q: search } : {}) } },
  }),
);

null key 代表尚未具備身份或權限時不發 request,不是把錯誤吞掉。跨租戶查詢、object ID 可見性與真正授權仍由後端與 PostgreSQL RLS 決定。

API、schema 與錯誤邊界

libs/api/client.ts 統一 request ID、camelCase/snake_case、可選 elevation session、refresh 與錯誤通知;FormData 請求、responseType: 'blob' 回應與含 - 的 key 會自動略過 casing 轉換。libs/api/request.ts 的 fetcher/mutator 先拆 { data } 信封,再以 Zod safeParse 驗證,不符就丟 SchemaContractError。schema 參數可以省略(例如 204 的 DELETE 回 undefined),這時 payload 不經驗證,所以回傳資料的 endpoint 都要給 schema。表單 values 這類動態字典由 caller 逐請求指定 preserveKeyPaths,不可全域放寬轉換;路徑寫法見頁面與元件規範。

libs/api/apiError.ts 只在一個邊界解析 code、message、traceId、fieldErrors 與 rowErrors。10100 先以 /api/auth/refresh 靜默更新並重送原請求一次,仍失敗才登出;10101/10102/10104 直接登出且不重試;10500/10501 是服務故障,不清 token、不導向登入頁。silentError 只壓掉全域通知、不跳過登出判定;呼叫端自己處理不了的錯誤用 surfaceApiError() 補回通知。client.ts 本身不重試 503,repo 也沒有全域 SWRConfig,退避由各 hook 的 SWR 選項決定(例如 approval-roles 只對 10500/10501 重試 3 次)。

分頁 helper

libs/api/pagination.ts 定義 be-plus 唯一的分頁形狀:pageEnvelopeSchema(item) 驗證 items、total、page、pageSize(1 至 100)與 sort;definePagePolicy 宣告逐端點的排序白名單,pageQueryParams 遇到白名單外的 sort 直接丟 InvalidSortKeyError,不要 fallback 成預設值(後端對應 400/10080)。

需要整份清單時用 fetchAllPages(fetchPage, concurrency):先單獨取第 1 頁拿 total 與後端實際採用的 pageSize,其餘頁每批最多 concurrency 頁(預設 8,只接受 1 至 8)平行請求,並依頁序拼接;遇到空頁就停止,每批以最新 total 判斷是否結束,任一頁失敗則整體 reject。features/attendance-daily-records/api.ts 取日紀錄時把併發降到 4。

SWR 與表單流程

以 features/forms/api.ts 為例:wire 的 snake_case 先經 transport,再用 strict Zod schema 驗證 definition、fields、stages、attachments、workflow preview 和 revision。forms 的 schema 一律 .strict(),但多數 hook 預設使用 .passthrough(),規則見頁面與元件規範。useGetFormWorkflowPreview 以草稿的 id 與 revision 組 SWR key;POST body 固定為 {},revision 不送給後端,只用來判斷回應是否落後:落後就重取一次,仍落後即拋錯(shouldRetryOnError: false),不無限輪詢。

預覽的 undetermined 是合法資訊不足,不是錯誤;頁面應用 warning 呈現,不能把它轉成審核失敗。server 回傳的 label、status 與 assignee 是 wire evidence,前端不要從 condition_result 或群組名稱自行拼出另一份規則。

NODE_ENV=development 且 NEXT_PUBLIC_MSW=true 時,features/forms/api.ts 與 features/reviews/api.ts 的 hook 直接回傳檔內 fixture,不經 fetcher、interceptor 或 MSW;這時的成功畫面不證明 transport 或後端契約正確。

UI 與響應式資料表

企業畫面優先使用 AntD public API。theme token 的單一來源是 constants/theme.ts,由 app/providers.tsx 的 antd-style ThemeProvider 與 setupStyled 同時提供給 AntD 和 @emotion/styled,產品主題目前固定 light。

共用 components/data/DataTable.tsx 預設關閉 AntD 內建 pagination,分頁由頁面在表格外渲染 TablePagination(呼叫端傳入 pagination 仍會覆蓋預設);未指定 scroll 時以 { x: 'max-content' } 交給內層 .ant-table-content 橫捲,避免手機裁掉最後的操作欄。列表必須分開 loading、error、empty、stale data 和正常零筆,不用 [] 同時表示請求失敗。

tsx
{
  /* 示例如下,僅展示 DataTable 的使用形狀;實際資料型別、columns 與狀態處理需依頁面契約實作。 */
}
<DataTable
  rowKey="id"
  loading={source.isLoading}
  dataSource={source.data?.items ?? []}
  scroll={{ x: 680 }}
  columns={columns}
/>;

開發期 mock 模式

沒有後端或 Keycloak 時可用 pnpm dev:mock,它同時開啟兩個互相獨立、只認字串 'true' 的開關:

  • NEXT_PUBLIC_MSW:app/MockBootstrap.tsx 在 MSW worker 就緒前不渲染頁面,由 worker 攔截瀏覽器發出的 /v1/**。handler 在 test/msw/handlers/,目前只涵蓋 /v1/me、/v1/me/menus 與部分 /v1/iam/**;其他請求以 bypass 照常經 proxy 送往後端。
  • NEXT_PUBLIC_MOCK_AUTH:開啟 GET /api/auth/dev-login,用正式的 setSessionCookies 種一組假會話,讓 proxy 的頁面守衛放行;開關關閉時該路由回 404。假 token 仍會被 proxy 注入,沒開 MSW 時真後端會回 401。

兩個開關只在 development 生效:next.config.ts 在 production build 發現任一開關為 'true' 就讓建構失敗,libs/mockMode.ts 在 runtime 再擋一次;開啟時畫面左下角顯示 MOCK 徽章。

操作與驗證

  1. 從 app/(platform)/<route>/page.tsx 與同層 layout.tsx 的 PageGate 找頁面 route,再依 import 進 features/<domain>,或該 route 的 _components/ 與 hooks/api/。
  2. 找 API hook 使用的 path、SWR key、Zod schema 與 mutation。
  3. 對照 proxy.ts、useMeApi.ts 和 libs/api/client.ts,確認身份、tenant、request ID、錯誤與 retry 沒有被 feature 重寫。
  4. 在已授權環境登入正確租戶,觀察 network path 仍為 /v1/**,再驗證成功、401、403、422、503 和空資料畫面。
bash
# 工作目錄:nexus-pro-web-plus;命令形狀已對照 package.json,本輪未跑業務環境
pnpm dev        # 連真實服務:.env.local 需設定 NEXUS_API_BASE_URL 與三個 KEYCLOAK_* 變數
pnpm dev:mock   # 沒有後端或 Keycloak 時的 UI 開發:MSW + 假會話,不是業務驗收
pnpm typecheck
pnpm test:run features/forms/workflow-preview.contract.test.tsx

交付前的完整檢查(lint、deadcode 與四個寬度的瀏覽器驗收)見頁面與元件規範。

預期結果與常見錯誤

預期結果是:URL 仍由 App Router 管理,每條產品路由都有自己的 PageGate,token 不進 client,request 有 schema/envelope 邊界,租戶切換不顯示舊資料,表格在窄螢幕可橫捲,錯誤可用 trace ID 追查。

常見錯誤:在元件內直接 fetch、把 /api 對照表重新建立、將 effective_permissions 當選單資料、把 503 當登入失效、把 pnpm dev:mock 或 hook 內 fixture 的成功當真實 API、新增頁面漏掛同層 layout.tsx 的 PageGate、或把 overflow: hidden 用來裁掉表格欄位。

相關文件

接著讀前端頁面與元件模式;表單契約可對照新增業務功能實戰;測試層級見測試策略。

內容來源與核實範圍

以下路徑相對於所列業務倉庫;核實層級為「已讀原始碼」,不是本輪業務測試或部署驗收。

  • nexus-pro-web-plus/docs/tech-stack.md
  • nexus-pro-web-plus/AGENTS.md
  • nexus-pro-web-plus/package.json
  • nexus-pro-web-plus/next.config.ts
  • nexus-pro-web-plus/proxy.ts
  • nexus-pro-web-plus/libs/authRouting.ts
  • nexus-pro-web-plus/libs/auth.ts
  • nexus-pro-web-plus/app/api/auth/_oidc.ts
  • nexus-pro-web-plus/app/api/auth/_session.ts
  • nexus-pro-web-plus/libs/api/client.ts
  • nexus-pro-web-plus/libs/api/request.ts
  • nexus-pro-web-plus/libs/api/pagination.ts
  • nexus-pro-web-plus/hooks/api/useMeApi.ts
  • nexus-pro-web-plus/hooks/api/useMenusApi.ts
  • nexus-pro-web-plus/libs/navigation/routeResolver.ts
  • nexus-pro-web-plus/test/routes/gates.test.ts
  • nexus-pro-web-plus/libs/mockMode.ts
  • nexus-pro-web-plus/app/MockBootstrap.tsx
  • nexus-pro-web-plus/features/forms/api.ts
  • nexus-pro-web-plus/components/data/DataTable.tsx
NexusPro 開發指南以程式碼為準 · 以驗證為據