diff --git a/apps/docs/src/content/docs/en/guides/first-module.md b/apps/docs/src/content/docs/en/guides/first-module.md index 79f270cf..f2118881 100644 --- a/apps/docs/src/content/docs/en/guides/first-module.md +++ b/apps/docs/src/content/docs/en/guides/first-module.md @@ -1000,6 +1000,7 @@ import { QueryBar, countActive, type FilterField } from '@admin/ui/components/qu import { Can } from '../../auth/can' import { ConfirmDialog } from '../../shell/confirm-dialog' import { PageHeader } from '../../shell/page-header' +import { PageShell } from '../_shared/page-shell' import { BulkBar, RefreshButton, ResetButton } from '../_shared/filters' import { listState } from '../_shared/list-query' import { DEFAULT_PAGE_SIZE } from '../_shared/pagination' @@ -1131,68 +1132,66 @@ export function CustomerPage({ const hasFilter = countActive(q.applied, FIELDS) > 0 return ( -
-
- - -
- - - - - - - - setBulkOpen(true)} /> - - + + + +
+ + + + + + + + setBulkOpen(true)} /> + + + } + /> + +
+ + ) : undefined } + {...list} + pagination={{ + pageIndex: page - 1, + pageCount: data?.total_pages ?? 1, + pageSize: size, + selectedCount: selectedIds.length, + totalCount: total, + onPageChange: (i) => patch({ page: i === 0 ? undefined : i + 1 }), + onPageSizeChange: (s) => patch({ size: s, page: undefined }), + }} /> - -
- - ) : undefined - } - {...list} - pagination={{ - pageIndex: page - 1, - pageCount: data?.total_pages ?? 1, - pageSize: size, - selectedCount: selectedIds.length, - totalCount: total, - onPageChange: (i) => patch({ page: i === 0 ? undefined : i + 1 }), - onPageSizeChange: (s) => patch({ size: s, page: undefined }), - }} - /> -
@@ -1232,7 +1231,7 @@ export function CustomerPage({ } }} /> -
+ ) } ``` diff --git a/apps/docs/src/content/docs/guides/first-module.md b/apps/docs/src/content/docs/guides/first-module.md index cc3afa12..07b7e931 100644 --- a/apps/docs/src/content/docs/guides/first-module.md +++ b/apps/docs/src/content/docs/guides/first-module.md @@ -1000,6 +1000,7 @@ import { QueryBar, countActive, type FilterField } from '@admin/ui/components/qu import { Can } from '../../auth/can' import { ConfirmDialog } from '../../shell/confirm-dialog' import { PageHeader } from '../../shell/page-header' +import { PageShell } from '../_shared/page-shell' import { BulkBar, RefreshButton, ResetButton } from '../_shared/filters' import { listState } from '../_shared/list-query' import { DEFAULT_PAGE_SIZE } from '../_shared/pagination' @@ -1131,68 +1132,66 @@ export function CustomerPage({ const hasFilter = countActive(q.applied, FIELDS) > 0 return ( -
-
- - -
- - - - - - - - setBulkOpen(true)} /> - - + + + +
+ + + + + + + + setBulkOpen(true)} /> + + + } + /> + +
+ + ) : undefined } + {...list} + pagination={{ + pageIndex: page - 1, + pageCount: data?.total_pages ?? 1, + pageSize: size, + selectedCount: selectedIds.length, + totalCount: total, + onPageChange: (i) => patch({ page: i === 0 ? undefined : i + 1 }), + onPageSizeChange: (s) => patch({ size: s, page: undefined }), + }} /> - -
- - ) : undefined - } - {...list} - pagination={{ - pageIndex: page - 1, - pageCount: data?.total_pages ?? 1, - pageSize: size, - selectedCount: selectedIds.length, - totalCount: total, - onPageChange: (i) => patch({ page: i === 0 ? undefined : i + 1 }), - onPageSizeChange: (s) => patch({ size: s, page: undefined }), - }} - /> -
@@ -1232,7 +1231,7 @@ export function CustomerPage({ } }} /> -
+ ) } ``` diff --git a/apps/web/src/routes/_auth.tsx b/apps/web/src/routes/_auth.tsx index ae600dcf..8c00ae48 100644 --- a/apps/web/src/routes/_auth.tsx +++ b/apps/web/src/routes/_auth.tsx @@ -129,7 +129,10 @@ function AuthLayout() { {/* 这一层是 div 不是 main —— `SidebarInset` 自己渲染的就是
, 再嵌一个会出现两个 main 地标,读屏「跳到主内容」不知道该去哪个。 - 水平内边距统一在这里给(页面级块一律不加 px-*,见 CLAUDE.md)。 */} + 四边内边距统一在这里给,上下左右同一个 16px(页面级块一律不加 px-* / py-*, + 见 CLAUDE.md;页面最外层走 `pages/_shared/page-shell.tsx`)。 + 原来这里只给 px-4,上下由每页手抄 `py-4 md:py-6` —— 桌面上顶 24 / 左右 16, + 抄漏的页贴着标签条、抄成 py-3 的是 12(#209)。 */} {/* data-slot 是给这一层的**地标**(和 sidebar-inset / table-container 同一套做法), 排障时能一眼定位「外壳的内容列」。 ⚠️ 曾经拿它挂过 `scrollbar-gutter: stable`,已经撤了 —— @@ -137,7 +140,7 @@ function AuthLayout() { 10px 并让表格右边缘比页头短一截。理由见 globals.css 里那段。 */}
diff --git a/packages/platform/src/pages/AGENTS.md b/packages/platform/src/pages/AGENTS.md index 41762446..bbdea9d5 100644 --- a/packages/platform/src/pages/AGENTS.md +++ b/packages/platform/src/pages/AGENTS.md @@ -25,6 +25,7 @@ pages/xxx/ | 文件 | 提供 | |---|---| +| `_shared/page-shell.tsx` | `PageShell`:每个页面最外面那几层 div(`fill="always"` 列表页 / `"lg"` 主从页 / 默认按自然高度)。**页面里不再手写外壳、不写上下 padding**,见下一节 | | `_shared/status.tsx` | `TONE_CLASS` 色板 · `StatusPill` · `StatusBadge`(正常/停用)· `STATUS_FILTER_ITEMS` / `STATUS_FORM_ITEMS` · `YesNoBadge` | | `_shared/filters.tsx` | `TextFilter`(回车/失焦才写 URL)· `SelectFilter` · `StatusFilter` · `ResetButton` · `BulkBar` · `RefreshButton`(见下「刷新」一节) | | `_shared/select-column.tsx` | `buildSelectColumn(col, { canSelect })` —— 表格首列的复选框(含半选态) | @@ -37,6 +38,39 @@ pages/xxx/ | `_shared/use-query-search.ts` | `QueryBar` ↔ URL 的胶水:从地址栏恢复条件 · 本地编辑 · 搜索时写回 URL + 拼接口入参 + **跳回第一页**。两个日志页 + 用户管理页在用,见 [查询区分册](../../../ui/src/components/query-bar/AGENTS.md) | | `_shared/data-io/` | 导出按钮 · 导入抽屉 · 两者的接口层。所有资源共用一份,见 [`data-io` 分册](_shared/data-io/AGENTS.md) | +### 🔴 页面四边 16px 只有一个来源:外壳内容列的 `p-4`(#209) + +**症状**:短信日志 / 投递记录 / AI 调用日志三页的查询区**直接贴着标签条**; +别的列表页桌面上顶部 24px、左右 16px(看着像内容被往下推了一截); +ai-provider 等五页又是 12px。全站三种顶距,没有任何测试会因此变红。 + +**根因**:外壳是三层 div、每页手抄,`py-4 md:py-6` 写在第三层 +(从 shadcn dashboard 模板带来的,那个模板没有标签条)。那三页少抄了一层, +而且是互相照着抄的 —— 新功能照「同一批的兄弟页」抄,错一个传一串。 +之前修过两次(`32985c1` 任务调度两页、ai-provider 补 `py-3`),每次只修当时那一页。 + +**修法**: + +- 内容列(`routes/_auth.tsx` 的 `data-slot="content-scroll"`)从 `px-4` 改成 **`p-4`**,四边同一个值、同一个地方给 +- 页面最外层一律 ``(`_shared/page-shell.tsx`),它**不带**上下 padding;页面根元素也不写 `p-` / `py-` / `pt-` / `pb-` +- `pnpm arch:check` 三条闸门:`page-shell-handwritten`(页面里出现 `@container/main`)· + `page-root-padding`(`*Page` 的根元素带竖向 padding / 上下 margin)· `page-root-unscanned`(认不出根就报错)。 + 「根元素」怎么认、哪些写法放行,见 [scripts 分册](../../../../scripts/AGENTS.md) +- 整屏居中的空态(`pages/forbidden.tsx` 的 `py-24`)不算对齐边距,不在闸门范围 +- 🔴 **内容区滚动模式下,sticky 的基准是滚动容器 padding 的内沿,不是它的上边。** 外壳加了 `pt-4` 之后, + 全站在内容区里吸顶的元素都多了 16px:`top-0` 的条带吸在距顶 16px,上面那条缝里看得见滚过去的行; + `top-2` / `top-4` 的左栏**静止时**就被往下推(个人中心左导航实测落在 24、审批中心左栏 32)。 + 修法统一用 `content-scroll:` 只改内容区模式(整页滚动的基准是视口,原值不动): + 盖住内容的条带用 `content-scroll:-top-4`(参数配置顶栏 · 权限矩阵工具条), + 左栏 / 面板用 `content-scroll:*:top-0`(设置屏导航 · 审批中心 · 流程设计器 · 组件沙箱)。新写 sticky 照这两种挑 +- 参数配置的粘性顶栏要**出血**:`-mx-4 -mt-4` 吃掉外壳的 padding、`px-4 pt-4` 补回来。第一版改成和内容同宽,吸顶时两侧 16px 的槽里露出滚过去的行(代码评审抓到的);主体那层原来的 `py-5` 也只留 `pt-5`,否则底部是 20 + 16 + +参数配置实测:顶栏静止和吸顶时都是 top 0、左右铺满,搜索框都在 16;左栏静止 85、吸顶 80(顶栏 64 + 16)。 + +**实测**(Playwright,1920 和 390 两档,「第一个画出东西的元素」距内容列):用户 / 短信日志 / 投递记录 / AI 调用日志 / +参数配置 / 个人中心 / 工作台 / 模型供应商 / 角色 / 服务器监控 / 文件 / 审批中心等 15 页,上 = 左 = 16(文字行是 17,行高的半行距); +右边 1920 下也是 16。⚠️ 量的时候 `background-image`(个人中心的渐变横幅)也要算「画了东西」,只看 `background-color` 会漏掉它、误报 24。 + ⚠️ 这里**没有**「只读列表工厂」了。`_shared` 下那个 `createListPage(cfg)` 删掉了 —— 它写好之后一直**零调用方**(两个日志页长出 统计条 / 导出 / 详情抽屉之后就搬出去手写了),而休眠代码的代价是实测出来的: diff --git a/packages/platform/src/pages/_shared/page-shell.tsx b/packages/platform/src/pages/_shared/page-shell.tsx new file mode 100644 index 00000000..0dd2aa5d --- /dev/null +++ b/packages/platform/src/pages/_shared/page-shell.tsx @@ -0,0 +1,52 @@ +import type * as React from 'react' + +import { cn } from '@admin/ui/lib/utils' + +/** + * 页面外壳:所有业务页最外面那几层 div,收在这一个地方(#209)。 + * + * ⚠️ **这里不给上下 padding,页面里也不给。** 四边 16px 统一由外壳的内容列 + * (`apps/web/src/routes/_auth.tsx` 上 `data-slot="content-scroll"` 那层的 `p-4`) + * 一次给齐 —— 和「页面级块一律不加 `px-*`」是同一条纪律,补上了竖直方向。 + * 原来每页手抄 `py-4 md:py-6`(shadcn dashboard 模板带来的,那个模板没有标签条), + * 抄漏的页查询区直接贴标签条、抄成 `py-3` 的又是 12px,全站三种顶距。 + * `pnpm arch:check` 的 `page-shell` 规则守着这条。 + * + * `fill` 决定「内容区滚动」模式下这一块要不要撑满可用高度: + * + * - `always` —— 列表页。撑满之后里面的表格框变成定高视区:筛选栏 / 表头 / 分页条钉住, + * 只有行滚。整页滚动模式下祖先高度是 auto,这两条是空操作(见 ui/data-table.tsx 的注释) + * - `lg` —— 主从页。宽屏两栏各自成定高视区,窄屏上下堆叠时照旧整块滚 + * - `none` —— 监控 / 卡片 / 演示页,按自然高度排 + * + * 三层缺一不可:最外层接住外壳的 flex 收缩链;中间层是 `@container/main` + * (仪表盘等按**容器**宽度断点,不按视口);最里层才是页面块之间的 gap。 + */ +export function PageShell({ + fill = 'none', + className, + children, +}: { + fill?: 'always' | 'lg' | 'none' + /** 加在最里层(页面块所在那层)上,比如 `flex-1` 让内容撑满、或改块间距 */ + className?: string + children: React.ReactNode +}) { + return ( +
+
+
+ {children} +
+
+
+ ) +} diff --git a/packages/platform/src/pages/_shared/settings-shell.tsx b/packages/platform/src/pages/_shared/settings-shell.tsx index 6e688ed3..1b600012 100644 --- a/packages/platform/src/pages/_shared/settings-shell.tsx +++ b/packages/platform/src/pages/_shared/settings-shell.tsx @@ -84,7 +84,7 @@ export function SettingsShell({
{/* ── 左:竖导航 ── */}