feat(web): 账号详情页对齐 Pro Advanced Profile——三卡卡片流、实体标识进页头 content 行左侧
douyin-release-gate / verify (push) Failing after 2m47s

This commit is contained in:
2026-09-24 17:39:13 +08:00
parent c4efe14e94
commit 1a0640f54a
3 changed files with 130 additions and 108 deletions
+22 -8
View File
@@ -1,17 +1,31 @@
import { createContext, useContext, useEffect } from 'react';
import type { ReactNode } from 'react';
// 页头动作槽:页面挂载时把自己的操作按钮注册到布局 PageContainer 的 content(标题下方独立操作行,右侧对齐),
// 卸载时自动清除。Layout 提供 Provider,页面只消费 usePageActions。
const PageActionsContext = createContext<(node: ReactNode) => void>(() => {});
// 页头槽:页面挂载时把自己的内容注册到布局 PageContainer 的 content 行——
// info 在左(实体标识/关键状态),actions 在右(操作按钮,右侧对齐),卸载时自动清除。
// Layout 提供 Provider,页面只消费 usePageInfo / usePageActions。
type SetSlot = (node: ReactNode) => void;
const PageHeaderSlotsContext = createContext<{ setInfo: SetSlot; setActions: SetSlot }>({
setInfo: () => {},
setActions: () => {},
});
export function usePageActions(node: ReactNode, deps: unknown[] = []) {
const setPageActions = useContext(PageActionsContext);
function useSlot(setSlot: SetSlot, node: ReactNode, deps: unknown[]) {
useEffect(() => {
setPageActions(node);
return () => setPageActions(null);
setSlot(node);
return () => setSlot(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
}
export { PageActionsContext };
export function usePageInfo(node: ReactNode, deps: unknown[] = []) {
const { setInfo } = useContext(PageHeaderSlotsContext);
useSlot(setInfo, node, deps);
}
export function usePageActions(node: ReactNode, deps: unknown[] = []) {
const { setActions } = useContext(PageHeaderSlotsContext);
useSlot(setActions, node, deps);
}
export { PageHeaderSlotsContext };
+21 -8
View File
@@ -4,7 +4,7 @@ import { history, Outlet, useLocation } from '@umijs/max';
import { PageContainer, ProLayout } from '@ant-design/pro-components';
import { App, Dropdown, Flex } from 'antd';
import { LogoutOutlined } from '@ant-design/icons';
import { PageActionsContext } from '@/components/PageActions';
import { PageHeaderSlotsContext, } from '@/components/PageActions';
import { menu, menuSelectedKey, pageMetadata } from '@/utils/metadata';
// 菜单结构对齐 web.archived/src/app/Layout.jsx(运营 / 资源 两组);侧栏仅保留模块链接菜单,标题由内容区页头呈现。
@@ -12,14 +12,15 @@ import { menu, menuSelectedKey, pageMetadata } from '@/utils/metadata';
// (渲染器不传 children prop,用 children 会导致整页空白且无任何报错)。
export default function Layout() {
const location = useLocation();
// 页头动作槽:列表页在挂载时把自己的添加类按钮注册进来,渲染在标题下方的独立操作行(右侧)。
// 页头槽:内容行左侧为页面注册的实体标识/关键状态(info),右侧为操作按钮(actions)。
const [pageInfo, setPageInfo] = useState<ReactNode>(null);
const [pageActions, setPageActions] = useState<ReactNode>(null);
if (location.pathname === '/login') return <Outlet />;
const meta = pageMetadata(location.pathname);
return (
<PageActionsContext.Provider value={setPageActions}>
<PageHeaderSlotsContext.Provider value={{ setInfo: setPageInfo, setActions: setPageActions }}>
<ProLayout
title="CreatorHub"
logo={false}
@@ -58,7 +59,7 @@ export default function Layout() {
),
}}
>
{/* 页面标题在卡片外(内容区左上角),动作按钮单独一行渲染在标题下方(content 槽,右侧对齐);卡片内不再重复渲染标题。 */}
{/* 页面标题在卡片外(内容区左上角),content 行左侧可放实体标识/关键状态、右侧放操作按钮;卡片内不再重复渲染标题。 */}
{/* App 包裹:App.useApp() 的 message 等实例需要此上下文(component=false 不增加 DOM,避免破坏 ProLayout 布局)。 */}
<App component={false}>
<PageContainer
@@ -67,14 +68,26 @@ export default function Layout() {
subTitle={meta.subtitle}
// 全站不展示面包屑导航(官方关闭方式:breadcrumbRender={false})。
breadcrumbRender={false}
// 标题与操作按钮各占一行:标题走 PageContainer 自带 title/subTitle,按钮走 content 槽,
// 右对齐用 Flex 组件 API,不写自定义对齐样式。
content={pageActions ? <Flex justify="flex-end">{pageActions}</Flex> : undefined}
// 标题与 content 行各占一行:content 行左标识/右按钮,两侧都有时 space-between,
// 仅一侧时按该侧对齐;右对齐用 Flex 组件 API,不写自定义对齐样式。
content={
pageInfo || pageActions ? (
<Flex
justify={pageInfo && pageActions ? 'space-between' : pageActions ? 'flex-end' : 'flex-start'}
align="center"
gap={16}
wrap="wrap"
>
{pageInfo}
{pageActions && <Flex justify="flex-end" wrap="wrap">{pageActions}</Flex>}
</Flex>
) : undefined
}
>
<Outlet />
</PageContainer>
</App>
</ProLayout>
</PageActionsContext.Provider>
</PageHeaderSlotsContext.Provider>
);
}
+87 -92
View File
@@ -1,5 +1,6 @@
// 账号详情:语义对齐 web.archived AccountDetail。
// 三卡布局改用 antd Tabs(账号状态 / 固定资源 / 文本草稿)——antd 默认组件无三列等宽卡片栅格约束,Tabs 是标准信息组织方式。
// 账号详情:样式对齐 Pro Advanced Profile(卡片流)——
// 主体为三张带标题 Card 纵向堆叠(账号状态 / 固定资源 / 文本草稿);
// 实体标识(账号名 + 状态 Tag + 平台明细)经 usePageInfo 进页头 content 行左侧,操作按钮在页头右侧。
import { useCallback, useEffect, useState } from 'react';
import { history, useParams } from '@umijs/max';
import {
@@ -12,14 +13,13 @@ import {
Input,
Modal,
Space,
Tabs,
Tag,
Typography,
message,
} from 'antd';
import { accountAction, createDraft, getOne, getList } from '@/services/api';
import { accountReadiness, conflictMessage, dateTime, platformLabel } from '@/utils/helpers';
import { usePageActions } from '@/components/PageActions';
import { accountReadiness, conflictMessage, platformLabel } from '@/utils/helpers';
import { usePageActions, usePageInfo } from '@/components/PageActions';
export default function Page() {
const { id = '' } = useParams<{ id: string }>();
@@ -35,7 +35,34 @@ export default function Page() {
const [draftBusy, setDraftBusy] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
// 页头动作槽:编辑/返回注册到 Layout 页头 content 槽(标题下方右侧),不在卡片内重复渲染。
const readiness = account ? accountReadiness(account, binding, browsersError) : null;
const statusTag =
account && readiness ? (
<Tag
color={readiness.ready ? 'success' : account.runtime_status === 'paused' && readiness.canResume ? 'warning' : 'error'}
>
{readiness.label}
</Tag>
) : null;
// 页头 content 行左侧:实体标识(账号名 + 状态 + 平台明细),对标 Advanced Profile 的「状态:待审批」信息行。
usePageInfo(
account ? (
<Flex align="center" gap={8} wrap="wrap">
<Typography.Title level={5} style={{ marginBottom: 0 }} ellipsis>
{account.name}
</Typography.Title>
{statusTag}
<Typography.Text type="secondary">
{account.platform_account_key} · {platformLabel(account.platform)}
</Typography.Text>
</Flex>
) : null,
[account?.id, account?.name, account?.runtime_status, binding, browsersError],
);
// 页头 content 行右侧:返回/编辑注册到 Layout 页头槽,不在卡片内重复渲染。
usePageActions(
account ? (
<Space>
@@ -46,8 +73,6 @@ export default function Page() {
[account?.id],
);
const readiness = account ? accountReadiness(account, binding, browsersError) : null;
const loadAll = useCallback(async () => {
setPending(true);
setError(null);
@@ -118,81 +143,67 @@ export default function Page() {
if (pending) return <Card loading />;
if (error || !account) return <Alert type="error" showIcon title={error?.message || '账号不存在'} />;
const statusTag = (
<Tag
color={readiness?.ready ? 'success' : account.runtime_status === 'paused' && readiness?.canResume ? 'warning' : 'error'}
>
{readiness?.label}
</Tag>
// 账号状态卡局部操作:暂停/恢复留在状态卡右上角(Card extra),不进页头槽。
const pauseResume = (
<Space wrap>
<Button danger disabled={actionBusy || account.runtime_status === 'paused'} onClick={() => setPauseOpen(true)}>
暂停账号
</Button>
<Button type="primary" disabled={actionBusy || !readiness?.canResume} loading={actionBusy} onClick={() => runAction('resume')}>
恢复账号
</Button>
</Space>
);
const items = [
{
key: 'status',
label: '账号状态',
children: (
return (
<Flex vertical gap={16}>
{contextHolder}
<Card title="账号状态" extra={pauseResume}>
<Flex vertical gap={16}>
<Flex justify="space-between" align="center" gap={16}>
{statusTag}
<Space wrap>
<Button danger disabled={actionBusy || account.runtime_status === 'paused'} onClick={() => setPauseOpen(true)}>
暂停账号
</Button>
<Button type="primary" disabled={actionBusy || !readiness?.canResume} loading={actionBusy} onClick={() => runAction('resume')}>
恢复账号
</Button>
</Space>
</Flex>
<Descriptions column={1} bordered size="small">
<Descriptions column={1} size="small">
<Descriptions.Item label="授权">{account.authorization_status === 'authorized' ? '已授权' : '已撤销'}</Descriptions.Item>
<Descriptions.Item label="运行">{`${account.runtime_status === 'active' ? '启用' : '暂停'} · 版本 ${account.version}`}</Descriptions.Item>
<Descriptions.Item label="标签">{account.tags?.join('、') || '无'}</Descriptions.Item>
</Descriptions>
<Alert type="info" showIcon title="暂停会把待领取任务置为 hold;恢复账号只恢复账号可用性,不会自动恢复既有 hold。" />
</Flex>
),
},
{
key: 'binding',
label: '固定资源',
children: browsersError ? (
<Alert
type="error"
showIcon
title="环境不可用:运行环境与网络出口状态未知,依赖资源状态的操作暂不可用。"
action={
<Button size="small" onClick={loadAll}>
重试环境状态
</Button>
}
/>
) : binding ? (
<div>
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="运行环境">{`${binding.name}(${binding.alias})`}</Descriptions.Item>
<Descriptions.Item label="网络出口">
{binding.network_exit_id ? `${binding.network_exit_id} · ${binding.network_exit_health || '未知状态'}` : '当前机器直连'}
</Descriptions.Item>
<Descriptions.Item label="绑定版本">{binding.binding_version}</Descriptions.Item>
<Descriptions.Item label="不可调度原因">
{binding.schedule_block_reason || '无'}
</Descriptions.Item>
</Descriptions>
<Typography.Link onClick={() => history.push('/browsers')} style={{ display: 'inline-block', marginTop: 12 }}>
前往运行环境
</Typography.Link>
</div>
) : (
<div>
<Typography.Paragraph type="secondary">尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。</Typography.Paragraph>
<Typography.Link onClick={() => history.push('/browsers')}>前往运行环境</Typography.Link>
</div>
),
},
{
key: 'drafts',
label: '文本草稿',
children: (
</Card>
<Card title="固定资源">
{browsersError ? (
<Alert
type="error"
showIcon
title="环境不可用:运行环境与网络出口状态未知,依赖资源状态的操作暂不可用。"
action={
<Button size="small" onClick={loadAll}>
重试环境状态
</Button>
}
/>
) : binding ? (
<Flex vertical gap={12}>
<Descriptions column={1} size="small">
<Descriptions.Item label="运行环境">{`${binding.name}(${binding.alias})`}</Descriptions.Item>
<Descriptions.Item label="网络出口">
{binding.network_exit_id ? `${binding.network_exit_id} · ${binding.network_exit_health || '未知状态'}` : '当前机器直连'}
</Descriptions.Item>
<Descriptions.Item label="绑定版本">{binding.binding_version}</Descriptions.Item>
<Descriptions.Item label="不可调度原因">
{binding.schedule_block_reason || '无'}
</Descriptions.Item>
</Descriptions>
<Typography.Link onClick={() => history.push('/browsers')}>前往运行环境</Typography.Link>
</Flex>
) : (
<Flex vertical gap={12}>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。
</Typography.Paragraph>
<Typography.Link onClick={() => history.push('/browsers')}>前往运行环境</Typography.Link>
</Flex>
)}
</Card>
<Card title="文本草稿">
<Flex vertical gap={16}>
<Typography.Text type="secondary">仅支持单用户 Phase A Mock 文本;内部 ID 与版本由系统生成。</Typography.Text>
<Form form={draftForm} layout="vertical">
@@ -228,23 +239,7 @@ export default function Page() {
</Card>
))}
</Flex>
),
},
];
return (
<div>
{contextHolder}
{/* 实体标识行:账号名 + 平台明细(动态数据,非页面标题);操作按钮在页头槽。 */}
<div style={{ marginBottom: 16 }}>
<Typography.Title level={4} style={{ marginBottom: 4 }} ellipsis>
{account.name}
</Typography.Title>
<Typography.Text type="secondary">
{account.platform_account_key} · {platformLabel(account.platform)}
</Typography.Text>
</div>
<Tabs defaultActiveKey="status" items={items} />
</Card>
<Modal
open={pauseOpen}
title="暂停账号"
@@ -258,6 +253,6 @@ export default function Page() {
>
<p>{`暂停账号 ${account.name}?待领取任务将进入 hold,恢复账号不会自动恢复既有 hold。`}</p>
</Modal>
</div>
</Flex>
);
}