From 5b2756fea80134ab9df912f68e7f7bcba0756153 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 28 Sep 2026 14:24:49 +0800 Subject: [PATCH 1/4] fix(web): spacing for monitoring account tags and square larger avatar --- web/src/components/accounts/MonitoringAccountList.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/src/components/accounts/MonitoringAccountList.tsx b/web/src/components/accounts/MonitoringAccountList.tsx index 6d5810e..a09c1c8 100644 --- a/web/src/components/accounts/MonitoringAccountList.tsx +++ b/web/src/components/accounts/MonitoringAccountList.tsx @@ -130,8 +130,8 @@ export default function MonitoringAccountList() { title: '账号', dataIndex: 'nickname', render: (_, account) => ( - - {(account.nickname || '账').slice(0, 1).toUpperCase()} + + {(account.nickname || '账').slice(0, 1).toUpperCase()} history.push(`/accounts/monitoring/${account.id}`)}> {account.nickname || '未命名账号'} @@ -174,7 +174,7 @@ export default function MonitoringAccountList() { { title: '标签', dataIndex: 'tags', - render: (tags?: string[]) => tags?.length ? tags.map((tag) => {tag}) : 未设置, + render: (tags?: string[]) => tags?.length ? {tags.map((tag) => {tag})} : 未设置, }, { title: '操作', From 630a4feab1bbe78591b630a137aca70a852c8e50 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 28 Sep 2026 14:33:54 +0800 Subject: [PATCH 2/4] fix(web): use text buttons for table row actions --- .../components/accounts/AccountManagementList.tsx | 6 +++--- .../components/accounts/MonitoringAccountList.tsx | 8 ++++---- web/src/pages/accounts/import.tsx | 2 +- web/src/pages/browsers/index.tsx | 5 +++-- web/src/pages/gateways/index.tsx | 4 ++-- web/src/pages/network-exits/index.tsx | 12 ++++++------ web/src/pages/tasks/index.tsx | 2 +- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/web/src/components/accounts/AccountManagementList.tsx b/web/src/components/accounts/AccountManagementList.tsx index ffda90a..eae7efa 100644 --- a/web/src/components/accounts/AccountManagementList.tsx +++ b/web/src/components/accounts/AccountManagementList.tsx @@ -131,10 +131,10 @@ export default function AccountManagementList() { align: 'right', render: (_, account) => ( - - deleteAccount(account)} disabled={actionBusy === account.id} > - diff --git a/web/src/components/accounts/MonitoringAccountList.tsx b/web/src/components/accounts/MonitoringAccountList.tsx index a09c1c8..93d3ba7 100644 --- a/web/src/components/accounts/MonitoringAccountList.tsx +++ b/web/src/components/accounts/MonitoringAccountList.tsx @@ -182,9 +182,9 @@ export default function MonitoringAccountList() { align: 'right', render: (_, account) => ( - - - + + deleteAccount(account)} disabled={actionBusy === account.id} > - + ), diff --git a/web/src/pages/accounts/import.tsx b/web/src/pages/accounts/import.tsx index ecbe91c..71db2c7 100644 --- a/web/src/pages/accounts/import.tsx +++ b/web/src/pages/accounts/import.tsx @@ -160,7 +160,7 @@ export default function Page() { align: 'right', render: (_, job) => job.status === 'failed' ? ( - ) : ( diff --git a/web/src/pages/browsers/index.tsx b/web/src/pages/browsers/index.tsx index da12a0f..58435f9 100644 --- a/web/src/pages/browsers/index.tsx +++ b/web/src/pages/browsers/index.tsx @@ -132,6 +132,7 @@ export default function Page() { return ( - handleRecycle(runtime)}> - diff --git a/web/src/pages/gateways/index.tsx b/web/src/pages/gateways/index.tsx index 76297be..5b5b638 100644 --- a/web/src/pages/gateways/index.tsx +++ b/web/src/pages/gateways/index.tsx @@ -154,11 +154,11 @@ export default function Page() { align: 'right', render: (_, gateway) => ( - removeGateway(gateway)}> - diff --git a/web/src/pages/network-exits/index.tsx b/web/src/pages/network-exits/index.tsx index e3c686b..7469a04 100644 --- a/web/src/pages/network-exits/index.tsx +++ b/web/src/pages/network-exits/index.tsx @@ -245,25 +245,25 @@ export default function Page() { align: 'right', render: (_, exit) => ( - {exit.health_status === 'disabled' ? ( - ) : ( runAction(exit, 'disable')}> - )} - deleteExit(exit)}> - @@ -289,7 +289,7 @@ export default function Page() { style={{ marginBottom: 16 }} title={`环境不可用${bindingsError.status ? `(${bindingsError.status})` : ''}:无法读取账号绑定,状态暂时未知。`} action={ - } diff --git a/web/src/pages/tasks/index.tsx b/web/src/pages/tasks/index.tsx index d1f4293..426f3af 100644 --- a/web/src/pages/tasks/index.tsx +++ b/web/src/pages/tasks/index.tsx @@ -130,7 +130,7 @@ export default function Page() { key: 'actions', align: 'right', render: (_, task) => ( - ), From 7ed542df4f78af3f7040a975947dbe2d0782972c Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 28 Sep 2026 15:20:55 +0800 Subject: [PATCH 3/4] fix(web): unify list tables with fixed header, fixed actions and pagination --- .../accounts/AccountManagementList.tsx | 8 +++-- .../accounts/MonitoringAccountList.tsx | 7 ++-- web/src/pages/accounts/import.tsx | 4 ++- web/src/pages/browsers/index.tsx | 6 +++- web/src/pages/gateways/index.tsx | 4 ++- web/src/pages/network-exits/index.tsx | 6 +++- web/src/pages/tasks/index.tsx | 6 +++- web/src/utils/table.ts | 24 +++++++++++++ web/tests/table-fixed-header.test.cjs | 34 +++++++++++++++++++ 9 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 web/src/utils/table.ts create mode 100644 web/tests/table-fixed-header.test.cjs diff --git a/web/src/components/accounts/AccountManagementList.tsx b/web/src/components/accounts/AccountManagementList.tsx index eae7efa..6112cc6 100644 --- a/web/src/components/accounts/AccountManagementList.tsx +++ b/web/src/components/accounts/AccountManagementList.tsx @@ -8,6 +8,7 @@ import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { remove, creatorUpdate, creatorGet } from '@/services/api'; import { conflictMessage, dateTime } from '@/utils/helpers'; +import { fixedRight, tablePagination, tableScroll } from '@/utils/table'; // 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。 interface Row { @@ -113,7 +114,7 @@ export default function AccountManagementList() { { title: '标签', dataIndex: 'tags', - render: (tags?: string[]) => (tags?.length ? tags.map((tag) => {tag}) : 未设置) }, + render: (tags?: string[]) => (tags?.length ? {tags.map((tag) => {tag})} : 未设置) }, { title: '状态', dataIndex: 'status', @@ -129,6 +130,7 @@ export default function AccountManagementList() { title: '操作', key: 'actions', align: 'right', + ...fixedRight({}), render: (_, account) => ( @@ -218,9 +220,10 @@ export default function MonitoringAccountList() { columns={columns} dataSource={rows} loading={pending} - pagination={false} + sticky + pagination={tablePagination()} locale={{ emptyText: '暂无监控账号。' }} - scroll={{ x: 1100 }} + scroll={tableScroll(1100)} /> ({}), render: (_, job) => job.status === 'failed' ? ( } /> ) : null} - +
diff --git a/web/src/pages/browsers/index.tsx b/web/src/pages/browsers/index.tsx index 58435f9..8b523e2 100644 --- a/web/src/pages/browsers/index.tsx +++ b/web/src/pages/browsers/index.tsx @@ -6,6 +6,7 @@ import { Alert, App, Button, Card, Flex, Popconfirm, Space, Table, Tag, Typograp import { CopyOutlined, PlusOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { browserAction, getList } from '@/services/api'; +import { fixedRight, tablePagination, tableScroll } from '@/utils/table'; const stateText: Record = { created: '已创建', @@ -124,6 +125,7 @@ export default function Page() { title: '操作', key: 'actions', align: 'right', + ...fixedRight({}), render: (_, runtime) => { const running = runtime.state === 'running'; const stopping = ['running', 'degraded', 'starting', 'failed', 'cleanup_pending'].includes(runtime.state); @@ -169,7 +171,9 @@ export default function Page() { columns={columns} dataSource={runtimes} loading={pending} - pagination={false} + sticky + pagination={tablePagination()} + scroll={tableScroll(1000)} locale={{ emptyText: '暂无运行环境,创建第一个隔离 Profile。' }} /> diff --git a/web/src/pages/gateways/index.tsx b/web/src/pages/gateways/index.tsx index 5b5b638..93bf7f9 100644 --- a/web/src/pages/gateways/index.tsx +++ b/web/src/pages/gateways/index.tsx @@ -5,6 +5,7 @@ import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined, PlusOutlined } from '@ import type { ColumnsType } from 'antd/es/table'; import { create, getList, remove, update } from '@/services/api'; import { conflictMessage } from '@/utils/helpers'; +import { fixedRight, tablePagination, tableScroll } from '@/utils/table'; const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{15,127}$/; @@ -152,6 +153,7 @@ export default function Page() { title: '操作', key: 'actions', align: 'right', + ...fixedRight({}), render: (_, gateway) => ( } /> ) : null} -
+
= { unchecked: '未检测', @@ -243,6 +244,7 @@ export default function Page() { title: '操作', key: 'actions', align: 'right', + ...fixedRight({}), render: (_, exit) => ( } /> ) : null} -
+
diff --git a/web/src/pages/browsers/index.tsx b/web/src/pages/browsers/index.tsx index 8b523e2..6585bf4 100644 --- a/web/src/pages/browsers/index.tsx +++ b/web/src/pages/browsers/index.tsx @@ -6,7 +6,7 @@ import { Alert, App, Button, Card, Flex, Popconfirm, Space, Table, Tag, Typograp import { CopyOutlined, PlusOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { browserAction, getList } from '@/services/api'; -import { fixedRight, tablePagination, tableScroll } from '@/utils/table'; +import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table'; const stateText: Record = { created: '已创建', @@ -37,6 +37,7 @@ export default function Page() { const [error, setError] = useState(null); const [busy, setBusy] = useState(''); const { message: messageApi } = App.useApp(); + const { vertical, horizontal } = useOverflowGrid(runtimes.length); const load = useCallback(async () => { setPending(true); @@ -75,6 +76,7 @@ export default function Page() { { title: '环境', dataIndex: 'name', + ...fixedLeft({}), render: (_, runtime) => (
history.push(`/browsers/${encodeURIComponent(runtime.alias)}`)}> {runtime.name} @@ -173,7 +175,7 @@ export default function Page() { loading={pending} sticky pagination={tablePagination()} - scroll={tableScroll(1000)} + scroll={tableScroll(horizontal ? 1000 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)} locale={{ emptyText: '暂无运行环境,创建第一个隔离 Profile。' }} /> diff --git a/web/src/pages/gateways/index.tsx b/web/src/pages/gateways/index.tsx index 93bf7f9..7411619 100644 --- a/web/src/pages/gateways/index.tsx +++ b/web/src/pages/gateways/index.tsx @@ -5,7 +5,7 @@ import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined, PlusOutlined } from '@ import type { ColumnsType } from 'antd/es/table'; import { create, getList, remove, update } from '@/services/api'; import { conflictMessage } from '@/utils/helpers'; -import { fixedRight, tablePagination, tableScroll } from '@/utils/table'; +import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table'; const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{15,127}$/; @@ -80,6 +80,7 @@ export default function Page() { const [busy, setBusy] = useState(''); const [created, setCreated] = useState(null); const { message: messageApi } = App.useApp(); + const { vertical, horizontal } = useOverflowGrid(gateways.length); const load = useCallback(async () => { setPending(true); @@ -143,7 +144,7 @@ export default function Page() { } const columns: ColumnsType = [ - { title: '名称', dataIndex: 'name', render: (value: string) => {value} }, + { title: '名称', dataIndex: 'name', ...fixedLeft({}), render: (value: string) => {value} }, { title: 'Endpoint', dataIndex: 'endpoint', render: (value: string) => {value} }, { title: '令牌', @@ -202,7 +203,7 @@ export default function Page() { {error ? ( 重试} /> ) : null} -
+
= { unchecked: '未检测', @@ -113,6 +113,7 @@ export default function Page() { const [editing, setEditing] = useState(null); const [busy, setBusy] = useState(''); const { message: messageApi } = App.useApp(); + const { vertical, horizontal } = useOverflowGrid(exits.length); const load = useCallback(async () => { setPending(true); @@ -216,6 +217,7 @@ export default function Page() { { title: '出口', dataIndex: 'id', + ...fixedLeft({}), render: (_, exit) => (
history.push(`/network-exits/${encodeURIComponent(exit.id)}`)}>{exit.id} @@ -304,7 +306,7 @@ export default function Page() { loading={pending} sticky pagination={tablePagination()} - scroll={tableScroll(1100)} + scroll={tableScroll(horizontal ? 1100 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)} locale={{ emptyText: '暂无网络出口。创建并检测健康后,才能绑定运行环境。' }} /> = { queued: '已排队', @@ -56,6 +56,7 @@ export default function Page() { const [accountsPending, setAccountsPending] = useState(false); const [pending, setPending] = useState(true); const [error, setError] = useState(null); + const { vertical, horizontal } = useOverflowGrid(rows.length); const load = useCallback(async () => { setPending(true); @@ -97,6 +98,7 @@ export default function Page() { { title: '任务', dataIndex: 'id', + ...fixedLeft({}), render: (value: string) => ( history.push(`/tasks/${value}`)}>{value} @@ -175,7 +177,7 @@ export default function Page() { loading={pending} sticky pagination={tablePagination()} - scroll={tableScroll(960)} + scroll={tableScroll(horizontal ? 960 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)} locale={{ emptyText: accountFilter || stateFilter ? '没有符合筛选条件的任务' : '尚无任务' }} />
diff --git a/web/src/utils/table.ts b/web/src/utils/table.ts index 1f0fdda..aa75ea0 100644 --- a/web/src/utils/table.ts +++ b/web/src/utils/table.ts @@ -1,22 +1,39 @@ -// 列表页表格的统一形态:固定表头(表内滚动)+ 横向滚动防挤压 + 分页。 -// - scroll.y 用 calc(100vh - offset) 让表格高度贴合一屏,表头表尾常驻,数据在表内滚动; -// offset 是页头(菜单行+PageContainer 标题/页头槽+卡片边框)与表格头行之外的高度估算值, -// 估不准只会留下少量页面滚动,配合 sticky 兜底表头仍可见。 -// - sticky:当 offset 估算偏差或行高变化导致页面仍有滚动时,表头吸顶兜底。 -// - scroll.x + 操作列 fixed: 'right':窗口窄时横向滚动,列内容不再被压缩折叠。 +// 列表页表格的统一形态:固定表头(表内滚动)+ 固定首列/操作列 + 横向滚动 + 分页。 +// 仅用 antd/rc-table 原生能力,不自定义滚动组件: +// - scroll.y(表内滚动,固定表头)+ sticky(页面余量滚动时表头吸顶兜底); +// antd 源码在 fixHeader 时固定 overflowY: 'scroll'(滚动条常驻,官方未提供按需开关), +// 因此仅在「数据行超过一屏」时才开启 scroll.y,用 overflowGrid 判断: +// 纵向 ≤ md(阈值 768,Grid.useBreakpoint 官方响应式 hook)→ 小屏数据必超一屏 → scroll.y + 首列固定; +// 横向 ≥ xl(阈值 1280,布局含侧栏后仍够宽)→ 无挤压,不加 scroll.x/fixed 列/scroll.y; +// 其余中等宽度 → scroll.x + fixed 首列/操作列,不设 scroll.y(无固定表头时滚动条自然按需出现)。 +// - scroll.x + 固定首列(fixed: 'left')与操作列(fixed: 'right')防挤压折叠。 // - pageSize 20 + hideOnSinglePage:数据多时翻页,单页时隐藏分页条保持原视觉。 +import { Grid } from 'antd'; import type { ColumnsType, ColumnType } from 'antd/es/table'; export const TABLE_PAGE_SIZE = 20; -export function tableScroll(x: number, offset = 300) { - return { x, y: `calc(100vh - ${offset}px)` }; +// antd Table 内容超出视口高度时,浏览器才需要纵向滚动; +// 这里用「数据行数 × 行高」与视口比较,决定是否需要表内滚动(固定表头)。 +export function useOverflowGrid(rowCount: number) { + const screens = Grid.useBreakpoint(); + const vertical = rowCount * 48 + 120 >= window.innerHeight - 300 || !screens.md; + const horizontal = !screens.xl; + return { vertical, horizontal }; +} + +export function tableScroll(x: number | undefined, y: string | undefined) { + return { x, y }; } export function tablePagination() { return { pageSize: TABLE_PAGE_SIZE, hideOnSinglePage: true }; } +export function fixedLeft(column: ColumnType): ColumnType { + return { ...column, fixed: 'left' as const }; +} + export function fixedRight(column: ColumnType): ColumnType { return { ...column, fixed: 'right' as const }; } diff --git a/web/tests/table-fixed-header.test.cjs b/web/tests/table-fixed-header.test.cjs index 2b55fcc..aaabf74 100644 --- a/web/tests/table-fixed-header.test.cjs +++ b/web/tests/table-fixed-header.test.cjs @@ -25,10 +25,11 @@ for (const file of files) { const code = source.getFullText(); test(`${file} 表格使用统一固定表头与分页形态`, () => { - assert.match(code, /tableScroll\(\d+\)/, '应设置 scroll.x/y(tableScroll)'); assert.match(code, /sticky/, '应开启表头吸顶兜底'); assert.match(code, /pagination=\{tablePagination\(\)\}/, '应恢复分页(每页 20,单页隐藏)'); assert.doesNotMatch(code, /pagination=\{false\}/, '不应再保留 pagination={false}'); + assert.match(code, /\.\.\.fixedLeft