chore: merge feature/account-bound-runtime web ui fixes
douyin-release-gate / verify (push) Failing after 3m30s
douyin-release-gate / verify (push) Failing after 3m30s
This commit is contained in:
@@ -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 { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
|
||||
|
||||
// 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。
|
||||
interface Row {
|
||||
@@ -30,6 +31,7 @@ export default function AccountManagementList() {
|
||||
const [tagBusy, setTagBusy] = useState(false);
|
||||
const [actionBusy, setActionBusy] = useState('');
|
||||
const { message: messageApi } = App.useApp();
|
||||
const { vertical, horizontal } = useOverflowGrid(rows.length);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -91,6 +93,7 @@ export default function AccountManagementList() {
|
||||
{
|
||||
title: '账号',
|
||||
dataIndex: 'name',
|
||||
...fixedLeft<Row>({}),
|
||||
render: (_, account) => (
|
||||
<div>
|
||||
<Typography.Link onClick={() => history.push(`/accounts/${account.id}`)}>{account.name}</Typography.Link>
|
||||
@@ -113,7 +116,7 @@ export default function AccountManagementList() {
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tags',
|
||||
render: (tags?: string[]) => (tags?.length ? tags.map((tag) => <Tag key={tag}>{tag}</Tag>) : <Typography.Text type="secondary">未设置</Typography.Text>) },
|
||||
render: (tags?: string[]) => (tags?.length ? <Flex wrap="wrap" gap={4}>{tags.map((tag) => <Tag key={tag}>{tag}</Tag>)}</Flex> : <Typography.Text type="secondary">未设置</Typography.Text>) },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -129,12 +132,13 @@ export default function AccountManagementList() {
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'right',
|
||||
...fixedRight<Row>({}),
|
||||
render: (_, account) => (
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => openTags(account)}>
|
||||
<Button type="text" size="small" onClick={() => openTags(account)}>
|
||||
编辑标签
|
||||
</Button>
|
||||
<Button size="small" onClick={() => history.push(`/accounts/${account.id}/edit`)}>
|
||||
<Button type="text" size="small" onClick={() => history.push(`/accounts/${account.id}/edit`)}>
|
||||
编辑账号
|
||||
</Button>
|
||||
<Popconfirm
|
||||
@@ -142,7 +146,7 @@ export default function AccountManagementList() {
|
||||
onConfirm={() => deleteAccount(account)}
|
||||
disabled={actionBusy === account.id}
|
||||
>
|
||||
<Button size="small" danger disabled={actionBusy === account.id}>
|
||||
<Button type="text" size="small" danger disabled={actionBusy === account.id}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -169,7 +173,9 @@ export default function AccountManagementList() {
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
loading={pending}
|
||||
pagination={false}
|
||||
sticky
|
||||
pagination={tablePagination()}
|
||||
scroll={tableScroll(horizontal ? 960 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)}
|
||||
locale={{ emptyText: '暂无自有账号。' }}
|
||||
/>
|
||||
<Modal
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import { CloudUploadOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { getList, remove, creatorAction, creatorUpdate } from '@/services/api';
|
||||
import { conflictMessage, dateTime } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
|
||||
|
||||
interface CompetitorRow {
|
||||
id: string;
|
||||
@@ -44,6 +45,7 @@ export default function MonitoringAccountList() {
|
||||
const [tagBusy, setTagBusy] = useState(false);
|
||||
const [actionBusy, setActionBusy] = useState('');
|
||||
const { message: messageApi } = App.useApp();
|
||||
const { vertical, horizontal } = useOverflowGrid(rows.length);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -129,9 +131,10 @@ export default function MonitoringAccountList() {
|
||||
{
|
||||
title: '账号',
|
||||
dataIndex: 'nickname',
|
||||
...fixedLeft<CompetitorRow>({}),
|
||||
render: (_, account) => (
|
||||
<Flex align="center" gap={8}>
|
||||
<Avatar src={account.avatar_url || undefined}>{(account.nickname || '账').slice(0, 1).toUpperCase()}</Avatar>
|
||||
<Flex align="center" gap={12}>
|
||||
<Avatar size={40} shape="square" src={account.avatar_url || undefined}>{(account.nickname || '账').slice(0, 1).toUpperCase()}</Avatar>
|
||||
<Typography.Link onClick={() => history.push(`/accounts/monitoring/${account.id}`)}>
|
||||
{account.nickname || '未命名账号'}
|
||||
</Typography.Link>
|
||||
@@ -174,17 +177,18 @@ export default function MonitoringAccountList() {
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tags',
|
||||
render: (tags?: string[]) => tags?.length ? tags.map((tag) => <Tag key={tag}>{tag}</Tag>) : <Typography.Text type="secondary">未设置</Typography.Text>,
|
||||
render: (tags?: string[]) => tags?.length ? <Flex wrap="wrap" gap={4}>{tags.map((tag) => <Tag key={tag}>{tag}</Tag>)}</Flex> : <Typography.Text type="secondary">未设置</Typography.Text>,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'right',
|
||||
...fixedRight<CompetitorRow>({}),
|
||||
render: (_, account) => (
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => openTags(account)}>编辑标签</Button>
|
||||
<Button size="small" loading={actionBusy === account.id} disabled={actionBusy === account.id} onClick={() => syncCompetitor(account)}>强制同步</Button>
|
||||
<Button size="small" disabled={actionBusy === account.id} onClick={() => toggleCompetitor(account)}>
|
||||
<Button type="text" size="small" onClick={() => openTags(account)}>编辑标签</Button>
|
||||
<Button type="text" size="small" loading={actionBusy === account.id} disabled={actionBusy === account.id} onClick={() => syncCompetitor(account)}>强制同步</Button>
|
||||
<Button type="text" size="small" disabled={actionBusy === account.id} onClick={() => toggleCompetitor(account)}>
|
||||
{account.enabled ? '暂停' : '恢复'}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
@@ -192,7 +196,7 @@ export default function MonitoringAccountList() {
|
||||
onConfirm={() => deleteAccount(account)}
|
||||
disabled={actionBusy === account.id}
|
||||
>
|
||||
<Button size="small" danger disabled={actionBusy === account.id}>删除</Button>
|
||||
<Button type="text" size="small" danger disabled={actionBusy === account.id}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
@@ -218,9 +222,10 @@ export default function MonitoringAccountList() {
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
loading={pending}
|
||||
pagination={false}
|
||||
sticky
|
||||
pagination={tablePagination()}
|
||||
locale={{ emptyText: '暂无监控账号。' }}
|
||||
scroll={{ x: 1100 }}
|
||||
scroll={tableScroll(horizontal ? 1100 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)}
|
||||
/>
|
||||
<Modal
|
||||
open={Boolean(tagTarget)}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PlusOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { create, getList, creatorAction } from '@/services/api';
|
||||
import { conflictMessage, dateTime, extractShareURL, platformForShareURL, platformLabel, shareJobStatus } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
|
||||
|
||||
interface Job {
|
||||
id: string;
|
||||
@@ -33,6 +34,7 @@ export default function Page() {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [retryID, setRetryID] = useState<string | null>(null);
|
||||
const { message: messageApi } = App.useApp();
|
||||
const { vertical, horizontal } = useOverflowGrid(jobs.length);
|
||||
|
||||
const loadShareJobs = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -106,6 +108,7 @@ export default function Page() {
|
||||
{
|
||||
title: '账号名称',
|
||||
dataIndex: 'author_name',
|
||||
...fixedLeft<Job>({}),
|
||||
render: (value?: string, job?: Job) => {
|
||||
if (!value) {
|
||||
return (
|
||||
@@ -158,9 +161,10 @@ export default function Page() {
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'right',
|
||||
...fixedRight<Job>({}),
|
||||
render: (_, job) =>
|
||||
job.status === 'failed' ? (
|
||||
<Button size="small" disabled={retryID === job.id} onClick={() => retryShareJob(job.id)}>
|
||||
<Button type="text" size="small" disabled={retryID === job.id} onClick={() => retryShareJob(job.id)}>
|
||||
{retryID === job.id ? '入队中…' : '重新入队'}
|
||||
</Button>
|
||||
) : (
|
||||
@@ -182,7 +186,7 @@ export default function Page() {
|
||||
{error ? (
|
||||
<Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={loadShareJobs}>重试</Button>} />
|
||||
) : null}
|
||||
<Table rowKey="id" columns={columns} dataSource={jobs} loading={pending} pagination={false} locale={{ emptyText: '暂无分享链接解析任务。' }} />
|
||||
<Table rowKey="id" columns={columns} dataSource={jobs} loading={pending} sticky pagination={tablePagination()} scroll={tableScroll(horizontal ? 1000 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)} locale={{ emptyText: '暂无分享链接解析任务。' }} />
|
||||
<Modal open={createOpen} title="创建账号导入任务" onCancel={closeCreate} onOk={createJob} confirmLoading={busy} okText={busy ? '加入中…' : '加入解析队列'} width={560}>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="shareText" label="分享内容或链接" required extra="可直接粘贴平台分享文案,系统会自动提取其中的链接并识别作者。">
|
||||
|
||||
@@ -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 { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
|
||||
|
||||
const stateText: Record<string, string> = {
|
||||
created: '已创建',
|
||||
@@ -36,6 +37,7 @@ export default function Page() {
|
||||
const [error, setError] = useState<any>(null);
|
||||
const [busy, setBusy] = useState('');
|
||||
const { message: messageApi } = App.useApp();
|
||||
const { vertical, horizontal } = useOverflowGrid(runtimes.length);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -74,6 +76,7 @@ export default function Page() {
|
||||
{
|
||||
title: '环境',
|
||||
dataIndex: 'name',
|
||||
...fixedLeft<any>({}),
|
||||
render: (_, runtime) => (
|
||||
<div style={{ cursor: 'pointer' }} onClick={() => history.push(`/browsers/${encodeURIComponent(runtime.alias)}`)}>
|
||||
<Typography.Text strong>{runtime.name}</Typography.Text>
|
||||
@@ -124,6 +127,7 @@ export default function Page() {
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'right',
|
||||
...fixedRight<any>({}),
|
||||
render: (_, runtime) => {
|
||||
const running = runtime.state === 'running';
|
||||
const stopping = ['running', 'degraded', 'starting', 'failed', 'cleanup_pending'].includes(runtime.state);
|
||||
@@ -132,6 +136,7 @@ export default function Page() {
|
||||
return (
|
||||
<Space wrap style={{ justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
title={`启动 ${runtime.name}${block ? `(${block})` : ''}`}
|
||||
disabled={busy === runtime.alias || running || gatewayUnavailable || block !== ''}
|
||||
@@ -139,11 +144,11 @@ export default function Page() {
|
||||
>
|
||||
启动
|
||||
</Button>
|
||||
<Button size="small" disabled={busy === runtime.alias || !stopping || gatewayUnavailable} onClick={() => runAction(runtime, 'stop')}>
|
||||
<Button type="text" size="small" disabled={busy === runtime.alias || !stopping || gatewayUnavailable} onClick={() => runAction(runtime, 'stop')}>
|
||||
停止
|
||||
</Button>
|
||||
<Popconfirm title={`回收 ${runtime.name}(${runtime.alias})?运行资源将被回收,Profile 目录保留。`} onConfirm={() => handleRecycle(runtime)}>
|
||||
<Button size="small" danger disabled={busy === runtime.alias || gatewayUnavailable}>
|
||||
<Button type="text" size="small" danger disabled={busy === runtime.alias || gatewayUnavailable}>
|
||||
回收
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -168,7 +173,9 @@ export default function Page() {
|
||||
columns={columns}
|
||||
dataSource={runtimes}
|
||||
loading={pending}
|
||||
pagination={false}
|
||||
sticky
|
||||
pagination={tablePagination()}
|
||||
scroll={tableScroll(horizontal ? 1000 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)}
|
||||
locale={{ emptyText: '暂无运行环境,创建第一个隔离 Profile。' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -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 { 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}$/;
|
||||
@@ -79,6 +80,7 @@ export default function Page() {
|
||||
const [busy, setBusy] = useState('');
|
||||
const [created, setCreated] = useState<any>(null);
|
||||
const { message: messageApi } = App.useApp();
|
||||
const { vertical, horizontal } = useOverflowGrid(gateways.length);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -142,7 +144,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: '名称', dataIndex: 'name', render: (value: string) => <Typography.Text strong>{value}</Typography.Text> },
|
||||
{ title: '名称', dataIndex: 'name', ...fixedLeft<any>({}), render: (value: string) => <Typography.Text strong>{value}</Typography.Text> },
|
||||
{ title: 'Endpoint', dataIndex: 'endpoint', render: (value: string) => <Typography.Text code style={{ fontSize: 12 }} ellipsis>{value}</Typography.Text> },
|
||||
{
|
||||
title: '令牌',
|
||||
@@ -152,13 +154,14 @@ export default function Page() {
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'right',
|
||||
...fixedRight<any>({}),
|
||||
render: (_, gateway) => (
|
||||
<Space>
|
||||
<Button size="small" disabled={busy === gateway.name || busy === 'edit'} onClick={() => setEditing(gateway)}>
|
||||
<Button type="text" size="small" disabled={busy === gateway.name || busy === 'edit'} onClick={() => setEditing(gateway)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title={`删除网关 ${gateway.name}?`} onConfirm={() => removeGateway(gateway)}>
|
||||
<Button size="small" danger disabled={busy === gateway.name || busy === 'edit'}>
|
||||
<Button type="text" size="small" danger disabled={busy === gateway.name || busy === 'edit'}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -200,7 +203,7 @@ export default function Page() {
|
||||
{error ? (
|
||||
<Alert type="error" showIcon style={{ marginBottom: 16 }} title={error.message} action={<Button size="small" onClick={load}>重试</Button>} />
|
||||
) : null}
|
||||
<Table rowKey="name" columns={columns} dataSource={gateways} loading={pending} pagination={false} locale={{ emptyText: '暂无网关;注册后环境即可调度到该网关。' }} />
|
||||
<Table rowKey="name" columns={columns} dataSource={gateways} loading={pending} sticky pagination={tablePagination()} scroll={tableScroll(horizontal ? 960 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)} locale={{ emptyText: '暂无网关;注册后环境即可调度到该网关。' }} />
|
||||
<GatewayFormModal
|
||||
open={createOpen}
|
||||
initial={null}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { PlusOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { create, getOne, getList, networkExitAction, remove, update } from '@/services/api';
|
||||
import { conflictMessage, dateTime } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
|
||||
|
||||
const healthText: Record<string, string> = {
|
||||
unchecked: '未检测',
|
||||
@@ -112,6 +113,7 @@ export default function Page() {
|
||||
const [editing, setEditing] = useState<any | null>(null);
|
||||
const [busy, setBusy] = useState('');
|
||||
const { message: messageApi } = App.useApp();
|
||||
const { vertical, horizontal } = useOverflowGrid(exits.length);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -215,6 +217,7 @@ export default function Page() {
|
||||
{
|
||||
title: '出口',
|
||||
dataIndex: 'id',
|
||||
...fixedLeft<any>({}),
|
||||
render: (_, exit) => (
|
||||
<div>
|
||||
<Typography.Link onClick={() => history.push(`/network-exits/${encodeURIComponent(exit.id)}`)}>{exit.id}</Typography.Link>
|
||||
@@ -243,27 +246,28 @@ export default function Page() {
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'right',
|
||||
...fixedRight<any>({}),
|
||||
render: (_, exit) => (
|
||||
<Space wrap style={{ justifyContent: 'flex-end' }}>
|
||||
<Button size="small" disabled={busy === exit.id} onClick={() => runAction(exit, 'check')}>
|
||||
<Button type="text" size="small" disabled={busy === exit.id} onClick={() => runAction(exit, 'check')}>
|
||||
检测
|
||||
</Button>
|
||||
{exit.health_status === 'disabled' ? (
|
||||
<Button size="small" disabled={busy === exit.id} onClick={() => runAction(exit, 'enable')}>
|
||||
<Button type="text" size="small" disabled={busy === exit.id} onClick={() => runAction(exit, 'enable')}>
|
||||
启用
|
||||
</Button>
|
||||
) : (
|
||||
<Popconfirm title={`停用网络出口 ${exit.id}?绑定账号将暂停,排队任务进入 hold。`} onConfirm={() => runAction(exit, 'disable')}>
|
||||
<Button size="small" danger disabled={busy === exit.id}>
|
||||
<Button type="text" size="small" danger disabled={busy === exit.id}>
|
||||
停用
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<Button size="small" disabled={busy === exit.id} onClick={() => setEditing(exit)}>
|
||||
<Button type="text" size="small" disabled={busy === exit.id} onClick={() => setEditing(exit)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm title={`删除网络出口 ${exit.id}?有绑定或运行实例时会拒绝。`} onConfirm={() => deleteExit(exit)}>
|
||||
<Button size="small" danger disabled={busy === exit.id}>
|
||||
<Button type="text" size="small" danger disabled={busy === exit.id}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -289,7 +293,7 @@ export default function Page() {
|
||||
style={{ marginBottom: 16 }}
|
||||
title={`环境不可用${bindingsError.status ? `(${bindingsError.status})` : ''}:无法读取账号绑定,状态暂时未知。`}
|
||||
action={
|
||||
<Button size="small" onClick={loadBindings}>
|
||||
<Button type="text" size="small" onClick={loadBindings}>
|
||||
重试环境状态
|
||||
</Button>
|
||||
}
|
||||
@@ -300,7 +304,9 @@ export default function Page() {
|
||||
columns={columns}
|
||||
dataSource={exits}
|
||||
loading={pending}
|
||||
pagination={false}
|
||||
sticky
|
||||
pagination={tablePagination()}
|
||||
scroll={tableScroll(horizontal ? 1100 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)}
|
||||
locale={{ emptyText: '暂无网络出口。创建并检测健康后,才能绑定运行环境。' }}
|
||||
/>
|
||||
<ExitFormModal
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Alert, Button, Card, Flex, Select, Space, Table, Tag, Typography } from
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { getList } from '@/services/api';
|
||||
import { dateTime } from '@/utils/helpers';
|
||||
import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
|
||||
|
||||
const stateText: Record<string, string> = {
|
||||
queued: '已排队',
|
||||
@@ -55,6 +56,7 @@ export default function Page() {
|
||||
const [accountsPending, setAccountsPending] = useState(false);
|
||||
const [pending, setPending] = useState(true);
|
||||
const [error, setError] = useState<any>(null);
|
||||
const { vertical, horizontal } = useOverflowGrid(rows.length);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setPending(true);
|
||||
@@ -96,6 +98,7 @@ export default function Page() {
|
||||
{
|
||||
title: '任务',
|
||||
dataIndex: 'id',
|
||||
...fixedLeft<any>({}),
|
||||
render: (value: string) => (
|
||||
<Space size={4}>
|
||||
<Typography.Link onClick={() => history.push(`/tasks/${value}`)}>{value}</Typography.Link>
|
||||
@@ -129,8 +132,9 @@ export default function Page() {
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
align: 'right',
|
||||
...fixedRight<any>({}),
|
||||
render: (_, task) => (
|
||||
<Button size="small" onClick={() => history.push(`/tasks/${task.id}`)}>
|
||||
<Button type="text" size="small" onClick={() => history.push(`/tasks/${task.id}`)}>
|
||||
查看
|
||||
</Button>
|
||||
),
|
||||
@@ -171,7 +175,9 @@ export default function Page() {
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
loading={pending}
|
||||
pagination={false}
|
||||
sticky
|
||||
pagination={tablePagination()}
|
||||
scroll={tableScroll(horizontal ? 960 : undefined, vertical ? 'calc(100vh - 300px)' : undefined)}
|
||||
locale={{ emptyText: accountFilter || stateFilter ? '没有符合筛选条件的任务' : '尚无任务' }} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// 列表页表格的统一形态:固定表头(表内滚动)+ 固定首列/操作列 + 横向滚动 + 分页。
|
||||
// 仅用 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;
|
||||
|
||||
// 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<RecordType>(column: ColumnType<RecordType>): ColumnType<RecordType> {
|
||||
return { ...column, fixed: 'left' as const };
|
||||
}
|
||||
|
||||
export function fixedRight<RecordType>(column: ColumnType<RecordType>): ColumnType<RecordType> {
|
||||
return { ...column, fixed: 'right' as const };
|
||||
}
|
||||
|
||||
export type { ColumnsType };
|
||||
@@ -0,0 +1,35 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { readFileSync } = require('node:fs');
|
||||
const { test } = require('node:test');
|
||||
const ts = require('typescript');
|
||||
|
||||
// 表格统一形态:固定表头(scroll.y)+ sticky 兜底 + 横向滚动 + 操作列固定右侧 + 分页。
|
||||
const files = [
|
||||
'src/components/accounts/MonitoringAccountList.tsx',
|
||||
'src/components/accounts/AccountManagementList.tsx',
|
||||
'src/pages/accounts/import.tsx',
|
||||
'src/pages/tasks/index.tsx',
|
||||
'src/pages/browsers/index.tsx',
|
||||
'src/pages/network-exits/index.tsx',
|
||||
'src/pages/gateways/index.tsx',
|
||||
];
|
||||
|
||||
for (const file of files) {
|
||||
const source = ts.createSourceFile(
|
||||
file,
|
||||
readFileSync(`${__dirname}/../${file}`, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
ts.ScriptKind.TSX,
|
||||
);
|
||||
const code = source.getFullText();
|
||||
|
||||
test(`${file} 表格使用统一固定表头与分页形态`, () => {
|
||||
assert.match(code, /sticky/, '应开启表头吸顶兜底');
|
||||
assert.match(code, /pagination=\{tablePagination\(\)\}/, '应恢复分页(每页 20,单页隐藏)');
|
||||
assert.doesNotMatch(code, /pagination=\{false\}/, '不应再保留 pagination={false}');
|
||||
assert.match(code, /\.\.\.fixedLeft</, '首列应固定在左侧');
|
||||
assert.match(code, /\.\.\.fixedRight</, '操作列应固定在右侧');
|
||||
assert.match(code, /useOverflowGrid\(/, '应按数据量按需开启表内滚动/横向滚动');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user