Skip to content
bitzorcas
中EN

Reference

Web 管理端

@bitz/app 是 React 19 + Vite 8 的管理端 SPA。它组合 widgets、platform-sdk context、Jotai 与 React Query,经 Vite dev proxy 同源抵达后端。

Last updated

@bitz/app(apps/app)是 Web 管理端单页应用。它是一个 Vite + React 19 应用,唯一的数据通道是 @bitz/platform-sdk,唯一的复合 UI 来源是 @bitz/widgets,唯一的样式系统是 Tailwind CSS v4。页面组合 widget 并调 usePlatform().api.<method>(...);它们不 fetch、不抄 DTO、不手搓 loading 状态。

1. 构建与开发拓扑

模式行为
yarn dev(默认)VITE_API_BASE_URL 为空 → Vite dev server 在 6800 把 /api/* 与 /health proxy 到本地 Api Host(6881)。浏览器保持同源 → 开发期无 CORS。
设置 VITE_API_BASE_URL前端直连后端;后端必须放行该 origin(CORS 在 Host 上配置)。此模式下 proxy 块不存在。
yarn buildtsc --noEmit && vite build。类型错误先于打包失败。

dev proxy 是本地开发零 CORS 摩擦的原因:浏览器只看到 http://localhost:6800,Vite 把 API 路径转发给 .NET Host。proxy 本身是有条件的——它只在 command === 'serve' && VITE_API_BASE_URL === '' 时存在,这样同一份配置同时服务 dev-proxy 与直连模式。

vite.config.ts — proxy 是有条件的,不是常驻
// 空 API 基址表示浏览器应保持同源。
const apiBaseUrl = process.env.VITE_API_BASE_URL ?? '';
// 只有开发服务器且未配置直连地址时才创建代理。
const useDevProxy = command === 'serve' && apiBaseUrl === '';
server: {
port: 6800,
strictPort: true,
...(useDevProxy
? {
proxy: {
'/api': { target: process.env.VITE_PROXY_TARGET ?? 'http://localhost:6881', changeOrigin: false },
'/health': { target: process.env.VITE_PROXY_TARGET ?? 'http://localhost:6881', changeOrigin: false },
},
}
: {}),
}

2. 环境变量

变量默认效果
VITE_API_BASE_URL''空 → dev-proxy 模式(同源)。设值 → 直连模式,后端必须放行 origin。也喂给 PlatformSdkConfig.baseUrl。
VITE_PROXY_TARGEThttp://localhost:6881/api、/health、/hubs 的 dev-proxy 目标;代码默认 6881,但仓库 .env.example 预填 5192,拷贝后需按需修改。
VITE_CLIENT_PLATFORMWeb作为 X-Client-Platform 头发送。
VITE_API_TIMEOUT_MS30000单请求超时(ms),喂给 PlatformSdkConfig.defaultTimeoutMs。
VITE_LOGIN_TEMPLATElegal-mis登录屏模板:mini-program、website-admin、business 或 legal-mis。
VITE_BRAND_THEMElegal-navy冻结 Brand Theme;非法值回退默认。
VITE_COLOR_MODEsystemlight、dark 或 system。
VITE_DENSITYdensedense、compact、comfortable 或 spacious。
VITE_SHELL_PRESETside登录后管理端使用 side 或 top;非法值回退 side,页面不得自行覆盖。

3. 运行时组合

应用组装一个固定的小集合:

关注点库在应用中的角色
路由react-router-dom 7import.meta.glob 懒加载;RouteGuard 包住需登录路由
客户端状态JotaiUI 局部状态的 atoms
URL 状态nuqs排序、过滤等查询参数状态放 URL
服务端状态@tanstack/react-query 5唯一被允许的数据获取通道;缓存键派生自 API 调用
认证/HTTP@bitz/platform-sdk作为 PlatformProvider 挂一次;页面读 usePlatform()
UI 原语@bitz/componentsshadcn 原语 barrel(数十个原子)+ cn
复合 UI@bitz/widgetsAppShell、ServerDataTable、FormField、ConfirmAction、各种 state
样式Tailwind v4@tailwindcss/vite
PWAvite-plugin-pwaregisterType: prompt;manifest 名 FD WORK
DXunplugin-auto-import、unplugin-icons、unplugin-svgr、Locator(仅 dev)自动导入 React/RR hooks、Lucide 图标、SVG-as-component、点击跳源

4. 入口、provider 与路由

main.tsx 按固定顺序接 provider 树:StrictMode > PlatformProvider(config) > WorkspacePreferencesProvider(注入首屏 brand/colorMode/density/shellPreset 安全默认值) > QueryClientProvider > PrincipalQueryCacheBoundary > RouterProvider + AppToaster。挂载点是 #root。没有根级 I18nProvider——i18n 由页面按需引入。

路由在 src/routes.tsx。路由扩展 React Router 的 RouteObject,带 meta(requiresAuth?、permission?、title?)与字符串 lazy,后者经 normalizeRoutes 转成 lazyRoute() glob 导入:

路由meta渲染什么
/login无需登录Design System 1.2 登录屏;凭证、验证码、MFA、外部登录
/forgot-password、/reset-password无需登录密码找回与重置
/accept-invitation、/activate无需登录邀请接受与账户激活
/verify-email、/verify-phone无需登录联系方式确认
/external-login/complete、/session-expired无需登录外部登录完成与会话恢复
/requiresAuth<RouteGuard> → <AppShell>
/(index)title: 工作区总览, requiresAuthhome——权限驱动的工作区目录,渲染 api.getNavigation()
/userspermission: ['identity.user.search']用户管理列表
/change-passwordrequiresAuth修改密码 / 强制改密
/settings/securityrequiresAuthMFA、验证与会话安全
/identity/accessrequiresAuth邀请与准入管理
/identity/organizationidentity.organization-unit.view大型组织目录、批量移动与同名子组织创建
/identity/applicationsplatform.oauth.manage 等OAuth、API Key 与 HMAC 应用凭据
/host/tenantspermission: ['operations.tenants.view'],surface: host租户治理(Host 面)
*title: 页面不存在,requiresAuth懒加载 pages/not-found 兜底页

RouteGuard 是路由级 gate(与按钮级 PermissionGate widget 互补)。它读 meta.requiresAuth 与 meta.permission,未登录或未授权时跳 /login。RouteLoading HydrateFallback 覆盖懒加载分片拉取。

5. widget 层

页面从 @bitz/widgets 组合,它隐藏了每个数据屏原本都要重写一遍的 glue:

Widget隐藏
AppShell + 导航 + HeaderBarSide/Top 壳层;服务端授权菜单;滚动收缩 Page Context;真实通知中心;账号浮层与退出
WorkspacePage + WorkspaceLocalTabs页面安全区、模块导航去重、Page Header 收缩和页面内 Tab 磁吸
ServerDataTable服务端分页、排序、列模型与 PlatformResult 映射
FormField文本输入的 label/error/help 接线
ConfirmAction破坏性操作的二次确认流程
QueryState / EmptyState / ErrorState任意查询的 loading/empty/error 分支(ErrorState 把 AppErrorType → i18n key)
PermissionGate声明式权限拦截(包装 SDK gate 的 widgets 版)

ServerDataTable<T> props

Prop类型说明
query{ isLoading, isError, error, data?: PlatformResult<Page<T>>, refetch? }一个 TanStack Query 结果形状
columnsreadonly DataTableColumn<T>[]{ key, header, cell, sortable?, className? }
getRowId(row: T) => string行 key
pageIndexnumber从 1 开始
pageSizenumber
onPageChange(pageIndex: number) => void
sortFieldstring | nullnull = 未排序
sortOrder'asc' | 'desc'
onSortChange(sortField, sortOrder) => void点击循环:asc → desc → 清空
emptyTitle?、ariaLabel?string

widget 自身不碰 URLSearchParams——页面拥有 URL 状态(见 users-list.tsx,参数键为 page/size/q/status/sort/order)。状态机:isLoading → QueryState;isError 或 data.ok === false → ErrorState(优先用 PlatformFailure.error,否则把抛出的错误映射进去);空 items → EmptyState;否则表格带分页页脚(共 N 条 · 第 X / Y 页)。

壳层与通知

AppShell 在部署级读取 VITE_SHELL_PRESET:

  • side:64px 模块轨道 + 176px 当前模块面板,桌面可有界调宽;
  • top:一级模块栏与上下文栏高度都由密度令牌 --cmp-shell-header-height 驱动(dense 48px / compact 56px / comfortable+ 64px),不是写死的像素常量;超出容量的一级入口进入“更多”,窄屏进入完整模块菜单;
  • 两种布局复用同一 api.getNavigation() 查询、URL、权限和页面 Recipe。

Identity 模块路由由 Shell 显示一次。页面只为真正的内容分区使用 WorkspaceLocalTabs;滚动时 Local Tabs 吸附在 Shell 固定栏下,标题与关键操作进入既有 Global bar。

HeaderBar 的通知入口调用 getNotificationInbox、getNotificationUnreadCount、markNotificationRead 和 markAllNotificationsRead。这些方法桥接生成的 OpenAPI 类型;通知 URL 只接受规范化的站内绝对路径。搜索、帮助或主题按钮在没有真实 owner 前不会渲染成空入口。

FormField props

Prop类型默认
name、label、value、onChange必填
error?string驱动 aria-invalid + 错误文案
type?'text' | 'email' | 'password' | 'search''text'
placeholder?、required?、disabled?

用 useId() 连 htmlFor/id。无内置校验引擎——错误由外部传入。

ConfirmAction props

Prop类型默认
trigger、title、onConfirm必填(onConfirm: () => Promise<void>)
description?string
confirmText? / cancelText?string'确认' / '取消'
destructive?booleantrue

onConfirm 在飞时按钮禁用;promise resolve 后才关弹窗。

6. 权限感知 UI(两层)

权限拦截发生在两层,都只算 UX:

按钮级拦截(pages/users/users-list.tsx)
import { PermissionGate } from "@bitz/widgets";
<PermissionGate require="identity.user.search" showFallback>
{/* 整个 users 页 */}
</PermissionGate>;
  • 路由级:RouteGuard 读 routes.tsx 的 meta.permission,把未授权用户跳走。
  • 按钮级:PermissionGate widget 按 currentUser?.permissions 隐藏/启用 UI。

两层读同一份权限集,它来自 /api/auth/me——绝不来自 localStorage。后端必须在授权管道里强制同样的能力。

7. 种子屏

文件演示什么
pages/login/login.tsx完整登录状态机;按 LoginOutcome 进入验证码、MFA、强制改密或成功分支
pages/identity/*密码、邀请、激活、验证、外部登录、安全设置、会话与准入管理;见 Identity 前端
pages/users/users-list.tsx参照数据屏:ServerDataTable + usePlatform().api.searchUsers + 启用/禁用的 ConfirmAction,URL 同步参数
pages/home.tsx权限驱动的工作区总览:服务端授权菜单 → 入口目录

这些存在是为了让 platform-sdk 契约与 widget 组合有可运行样例;扩展管理端就是按同样的组合模式加路由。

8. PWA 配置

config/pwa-options.ts 配置 vite-plugin-pwa:registerType: 'prompt'、injectRegister: false、manifest name/short_name: 'FD WORK'、display: 'standalone'、start_url: '/'、两个 SVG 图标(any + maskable)。Workbox 预缓存 **/*.{js,mjs,css,html,svg,png,ico,jpg,webp}(不含 mp4——登录视频等大媒体不进预缓存),上限 10 MB,开 cleanupOutdatedCaches 与 clientsClaim、skipWaiting: false(由 prompt 注册流程控制激活)。/web/viewer.html* 在 navigateFallbackDenyList 里。

9. 路径别名

apps/app/tsconfig.json 继承 tsconfig.base.json,映射 @/* → ./src/*,以及 @bitz/components、@bitz/i18n、@bitz/platform-sdk 的 package 根。其余 @bitz/* 解析(widgets、editor、hooks、utils、materials、scripts)来自 tsconfig.base.json,后者映射了每个 workspace package。

10. 审一次 Web 应用改动

  1. 确认任何新增 HTTP 调用走 usePlatform().api——不是页面级 fetch。
  2. 确认任何新增后端类型从 @bitz/platform-sdk 导入,并最终桥接自 generated schema——不是手写。
  3. 若新增数据屏,优先组合 @bitz/widgets(ServerDataTable、QueryState),而不是重写各种 state。
  4. 若新增路由,给它 meta.requiresAuth / meta.permission,让 RouteGuard 强制。
  5. 提交前跑 yarn workspace @bitz/app build——tsc --noEmit 先跑,类型错误会让构建失败;产物还需通过 verify-bundle-budget.mjs 的体积预算门。

11. 源核对

Terminal window
# 确认页面经 context 消费 SDK、经 barrel 消费 widgets。
rg -n "usePlatform|from '@bitz/widgets'|from '@bitz/platform-sdk'" \
frontend/apps/app/src/pages
# 确认 provider 树顺序与有条件的 dev proxy。
sed -n '1,40p' frontend/apps/app/src/main.tsx
sed -n '1,70p' frontend/apps/app/vite.config.ts
# 确认路由 meta 与 RouteGuard 接线。
rg -n "requiresAuth|permission|RouteGuard" frontend/apps/app/src/routes.tsx

返回前端 · 平台契约层 · 移动端套壳

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%