管理后台
/admin 管理后台:五个分区、用户抽屉、概览指标、运营设置,以及怎样加一个自己的页面。
管理后台位于 /admin,和用户自己的页面共用同一个登录后的外壳。只要角色持有admin.access,侧边栏底部就会出现 管理后台 入口;进入 /admin 后,侧边栏切换成后台的各个分区,外加 返回应用。每个人在里面能看到什么,取决于他的角色,见角色与权限。本地给自己设置内置的 admin角色,请按快速开始操作。
五个分区
侧边栏里每个分区占一行。分区下的页面以标签页的形式显示在页面标题下方;如果某个分区里当前用户只能打开一个页面,就不显示标签。角色无权访问的页面会被隐藏,一个页面都不剩的分区会从侧边栏里消失。
| 分区 | 页面(所需权限) |
|---|---|
| 概览 | 概览(admin.access) |
| 用户 | 用户管理(users.read)、等待列表(waitlist.read)、反馈(feedback.read) |
| 收入 | 订阅和订单(billing.read)、积分(credits.read)、推广返佣(affiliate.read) |
| 开发者 | API 密钥(api_keys.manage)、邮件日志(email.read)、审计日志(audit.read) |
| 设置 | 通用(settings.read)、角色权限(roles.write)、邮件(email.read) |
可选模块的页面会随模块一起删除,见删除功能。
这些行都在同一个数组里:src/components/app-sidebar.tsx 的 adminNav。每一行的 section 决定它归属哪个分区,它在数组里的位置决定标签顺序。
概览
/admin 展示最近 7、30 或 90 天的核心数字,每个数字都附带与上一个等长窗口相比的变化。点击任意一条序列,图表就会画出它。那些意味着“有人该去处理”的数字——未处理的反馈、等待列表里待审批的人、发送失败的邮件、排队中的推广返佣提现——会作为待办显示,并链接到处理它们的页面。自定义的 UTC 时间范围也可以通过 URL 指定:/admin?from=2026-01-01&to=2026-01-31。
所有数字都来自一个统计注册表。每个功能在自己的 server/stats.ts 里注册一个provider,并在 src/core/stats/handlers.ts 里加一行:
registerStats('feedback', async (range) => ({
metrics: [
{ key: 'feedback.open', value: open, format: 'count' },
{ key: 'feedback.received', value: current, format: 'count', previous },
],
series: [
{ key: 'feedback.received', format: 'count', points: fillDays(range, perDay) },
],
}))
带 previous 且有同名序列的指标是“期间”数字(窗口内发生了什么);不带的是“状态”数字(此刻是什么样)。每个新 key 都要在 src/routes/_app/admin/index.tsx的 labels 里配一个标签。同样的数据也通过 /api/v1/admin/stats 提供给 AI agent,见 API 密钥。
概览只检查 admin.access,所以任何能进后台的角色都能看到上面的每个数字,包括收入。如果这对你的团队有影响,就在想隐藏的 provider 里检查更细的权限。
用户与用户抽屉
后台表格里凡是出现某个人,名字都是一个 <UserLink userId>(src/components/user-link.tsx)。点击它会在当前 URL 上加 ?user=<id>,在页面上方打开用户抽屉;下面的列表保留原来的筛选和滚动位置,这个链接刷新或分享后依然有效。抽屉里有按钮可以打开完整页面 /admin/users/<id>。没有users.read 的人看到的是纯文本,而不是链接。
两种视图渲染的是同一个组件,内容包括:
- 账户信息:角色(可就地修改)、注册时间、最近活跃、id;
- 一排核心数字,比如当前套餐和积分余额;
- 每个功能一个标签页:账单、积分、审计、邮件、反馈、文件、等待列表;
- 一个菜单,里面有封禁/解封;对仍在保留期内的已注销账户,还有恢复。
这些标签页来自注册表。功能在自己的 components/admin-user-section.tsx 里调用registerAdminUserSection({ key, label, query, component }),并在src/core/user-detail/handlers.ts 里加一行 import。行的顺序就是标签的顺序。
表格
后台列表用的是 DataTable(src/components/data-table.tsx):搜索、下拉筛选、排序、分页,以及把当前视图导出为 CSV。搜索和筛选条件保存在 URL 里,筛好的视图就是一个可以直接分享的链接。
设置
设置 → 通用(/admin/settings)集中了运营人员无需重新部署就能修改的值:已注销账户、审计记录、邮件日志和 webhook 记录各保留多久,订阅到期未续费后的宽限期,积分余额偏低的提醒阈值,以及反馈图片的限制。推广返佣的条款也是设置,但它们放在推广返佣页面上,和它们约束的数据挨在一起。每个功能在一个客户端安全的 settings.ts 里注册自己的设置,并在 src/core/settings/handlers.ts 里加一行:
registerSetting({
key: 'auth.deleted_account_retention_days',
group: 'accounts',
groupLabel: () => m.admin_settings_group_accounts(),
kind: 'number',
fallback: ACCOUNT_RETENTION_DAYS,
min: 7,
max: 3650,
label: () => m.setting_account_retention(),
unit: () => m.setting_unit_days(),
})
数据库里只存修改过的值。没动过的设置会回落到代码里的默认值,所以你在代码里改了默认值,所有没覆盖过它的部署都会拿到新值。类型有 number、boolean、string 和 secret 四种;secret 会加密存储,存进去之后不会再发回浏览器。服务端用 src/core/settings/store.ts 里的 getSetting 或 getNumberSetting读取,每个 isolate 缓存 60 秒。修改设置需要 settings.write;只有settings.read 时页面是只读的。完整列表见配置。
设置分区的另外两个标签页:角色权限,即角色编辑器;邮件,可以用示例数据预览每个邮件模板的每种语言,并给自己的地址发一封测试邮件(见邮件与通知)。
新增一个页面
假设要在“收入”分区里加一个报表页面。
-
声明权限:在功能的
permissions.ts里声明reports.read,并在src/core/authz/handlers.ts里加上它的 import。细节见角色与权限。 -
编写 server function:从
adminFn开始,在 handler 的第一行检查权限:export const listReportsAdminFn = adminFn.handler(async ({ context }) => { await assertPermission(context.session, 'reports.read') return getReports() }) -
创建路由
src/routes/_app/admin/reports.tsx,包含守卫、loader 和组件。页面顶部用src/components/page-header.tsx的PageHeader,它会渲染分区标题和标签页:export const Route = createFileRoute('/_app/admin/reports')({ beforeLoad: async ({ context }) => { await requirePermission(context.queryClient, 'reports.read') }, loader: ({ context }) => context.queryClient.ensureQueryData(adminReportsQuery), component: ReportsPage, })然后运行
bun run generate-routes。 -
加一行导航:在
src/components/app-sidebar.tsx的adminNav里加上:{ to: '/admin/reports', section: 'revenue', permission: 'reports.read', label: () => m.admin_reports_title(), },如果页面属于某个可选功能,用
// [feature: <name>] start和end注释把这一行包起来,删除脚本才能把它切掉。 -
补上文案:写进
messages/en.json和messages/zh.json。以admin_开头的 key 会随后台一起加载,不会进入营销站点的包。
如果路由里没有 requirePermission,或者 server function 里没有assertPermission,bun run test 就会失败。包括数据层在内的完整流程见新增功能;/add-feature skill 会带着 AI agent一步步完成。