Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 60 additions & 61 deletions apps/docs/src/content/docs/en/guides/first-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -1131,68 +1132,66 @@ export function CustomerPage({
const hasFilter = countActive(q.applied, FIELDS) > 0

return (
<div className="flex flex-1 flex-col content-scroll:min-h-0">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6 content-scroll:min-h-0 content-scroll:flex-1">
<PageHeader title={t('客户管理')} description={t('维护客户档案;能看到哪些客户由数据权限决定。')} />

<div className="flex flex-col gap-4 content-scroll:min-h-0 content-scroll:flex-1">
<QueryBar
fields={FIELDS}
value={q.value}
onChange={q.setValue}
onSearch={submitQuery}
onReset={clearFilters}
applied={q.applied}
loading={isFetching}
viewsStorageKey="qb:customer"
actions={
<>
<Can perm="crm:customer:add">
<Button
size="sm"
onClick={() => {
setEditing(null)
setSheetOpen(true)
}}
>
<IconPlus className="size-4" />
{t('新增客户')}
</Button>
</Can>
<RefreshButton busy={isFetching} onClick={list.onRetry} />
<DataTableColumnVisibility table={table} columnLabels={COLUMN_LABELS} />
<Can perm="crm:customer:del">
<BulkBar count={selectedIds.length} pending={del.isPending} onDelete={() => setBulkOpen(true)} />
</Can>
</>
<PageShell fill="always">
<PageHeader title={t('客户管理')} description={t('维护客户档案;能看到哪些客户由数据权限决定。')} />

<div className="flex flex-col gap-4 content-scroll:min-h-0 content-scroll:flex-1">
<QueryBar
fields={FIELDS}
value={q.value}
onChange={q.setValue}
onSearch={submitQuery}
onReset={clearFilters}
applied={q.applied}
loading={isFetching}
viewsStorageKey="qb:customer"
actions={
<>
<Can perm="crm:customer:add">
<Button
size="sm"
onClick={() => {
setEditing(null)
setSheetOpen(true)
}}
>
<IconPlus className="size-4" />
{t('新增客户')}
</Button>
</Can>
<RefreshButton busy={isFetching} onClick={list.onRetry} />
<DataTableColumnVisibility table={table} columnLabels={COLUMN_LABELS} />
<Can perm="crm:customer:del">
<BulkBar count={selectedIds.length} pending={del.isPending} onDelete={() => setBulkOpen(true)} />
</Can>
</>
}
/>

<div className="content-scroll:flex content-scroll:min-h-0 content-scroll:flex-1 content-scroll:flex-col">
<DataTable
table={table}
showColumnVisibility={false}
rows={table.getRowModel().rows}
columnCount={columns.length}
columnLabels={COLUMN_LABELS}
emptyMessage={t('还没有客户')}
emptyAction={
hasFilter ? (
<ResetButton variant="outline" label={t('清除筛选')} onClick={clearFilters} />
) : 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 }),
}}
/>

<div className="content-scroll:flex content-scroll:min-h-0 content-scroll:flex-1 content-scroll:flex-col">
<DataTable
table={table}
showColumnVisibility={false}
rows={table.getRowModel().rows}
columnCount={columns.length}
columnLabels={COLUMN_LABELS}
emptyMessage={t('还没有客户')}
emptyAction={
hasFilter ? (
<ResetButton variant="outline" label={t('清除筛选')} onClick={clearFilters} />
) : 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 }),
}}
/>
</div>
</div>
</div>

Expand Down Expand Up @@ -1232,7 +1231,7 @@ export function CustomerPage({
}
}}
/>
</div>
</PageShell>
)
}
```
Expand Down
121 changes: 60 additions & 61 deletions apps/docs/src/content/docs/guides/first-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -1131,68 +1132,66 @@ export function CustomerPage({
const hasFilter = countActive(q.applied, FIELDS) > 0

return (
<div className="flex flex-1 flex-col content-scroll:min-h-0">
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6 content-scroll:min-h-0 content-scroll:flex-1">
<PageHeader title={t('客户管理')} description={t('维护客户档案;能看到哪些客户由数据权限决定。')} />

<div className="flex flex-col gap-4 content-scroll:min-h-0 content-scroll:flex-1">
<QueryBar
fields={FIELDS}
value={q.value}
onChange={q.setValue}
onSearch={submitQuery}
onReset={clearFilters}
applied={q.applied}
loading={isFetching}
viewsStorageKey="qb:customer"
actions={
<>
<Can perm="crm:customer:add">
<Button
size="sm"
onClick={() => {
setEditing(null)
setSheetOpen(true)
}}
>
<IconPlus className="size-4" />
{t('新增客户')}
</Button>
</Can>
<RefreshButton busy={isFetching} onClick={list.onRetry} />
<DataTableColumnVisibility table={table} columnLabels={COLUMN_LABELS} />
<Can perm="crm:customer:del">
<BulkBar count={selectedIds.length} pending={del.isPending} onDelete={() => setBulkOpen(true)} />
</Can>
</>
<PageShell fill="always">
<PageHeader title={t('客户管理')} description={t('维护客户档案;能看到哪些客户由数据权限决定。')} />

<div className="flex flex-col gap-4 content-scroll:min-h-0 content-scroll:flex-1">
<QueryBar
fields={FIELDS}
value={q.value}
onChange={q.setValue}
onSearch={submitQuery}
onReset={clearFilters}
applied={q.applied}
loading={isFetching}
viewsStorageKey="qb:customer"
actions={
<>
<Can perm="crm:customer:add">
<Button
size="sm"
onClick={() => {
setEditing(null)
setSheetOpen(true)
}}
>
<IconPlus className="size-4" />
{t('新增客户')}
</Button>
</Can>
<RefreshButton busy={isFetching} onClick={list.onRetry} />
<DataTableColumnVisibility table={table} columnLabels={COLUMN_LABELS} />
<Can perm="crm:customer:del">
<BulkBar count={selectedIds.length} pending={del.isPending} onDelete={() => setBulkOpen(true)} />
</Can>
</>
}
/>

<div className="content-scroll:flex content-scroll:min-h-0 content-scroll:flex-1 content-scroll:flex-col">
<DataTable
table={table}
showColumnVisibility={false}
rows={table.getRowModel().rows}
columnCount={columns.length}
columnLabels={COLUMN_LABELS}
emptyMessage={t('还没有客户')}
emptyAction={
hasFilter ? (
<ResetButton variant="outline" label={t('清除筛选')} onClick={clearFilters} />
) : 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 }),
}}
/>

<div className="content-scroll:flex content-scroll:min-h-0 content-scroll:flex-1 content-scroll:flex-col">
<DataTable
table={table}
showColumnVisibility={false}
rows={table.getRowModel().rows}
columnCount={columns.length}
columnLabels={COLUMN_LABELS}
emptyMessage={t('还没有客户')}
emptyAction={
hasFilter ? (
<ResetButton variant="outline" label={t('清除筛选')} onClick={clearFilters} />
) : 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 }),
}}
/>
</div>
</div>
</div>

Expand Down Expand Up @@ -1232,7 +1231,7 @@ export function CustomerPage({
}
}}
/>
</div>
</PageShell>
)
}
```
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/routes/_auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,18 @@ function AuthLayout() {
<AiDrawer />
{/* 这一层是 div 不是 main —— `SidebarInset` 自己渲染的就是 <main>,
再嵌一个会出现两个 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`,已经撤了 ——
表格页真正会滚的是 `table-container`,这一层根本不滚,留槽位只会白占
10px 并让表格右边缘比页头短一截。理由见 globals.css 里那段。 */}
<div
data-slot="content-scroll"
className="flex flex-1 flex-col px-4 content-scroll:min-h-0 content-scroll:overflow-y-auto content-scroll:overflow-x-hidden"
className="flex flex-1 flex-col p-4 content-scroll:min-h-0 content-scroll:overflow-y-auto content-scroll:overflow-x-hidden"
>
<TabOutlet registry={pageRegistry} />
</div>
Expand Down
34 changes: 34 additions & 0 deletions packages/platform/src/pages/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 })` —— 表格首列的复选框(含半选态) |
Expand All @@ -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`**,四边同一个值、同一个地方给
- 页面最外层一律 `<PageShell>`(`_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)`
删掉了 —— 它写好之后一直**零调用方**(两个日志页长出
统计条 / 导出 / 详情抽屉之后就搬出去手写了),而休眠代码的代价是实测出来的:
Expand Down
Loading
Loading