feat(web): 账号详情页对齐 Pro Advanced Profile——三卡卡片流、实体标识进页头 content 行左侧
douyin-release-gate / verify (push) Failing after 2m47s
douyin-release-gate / verify (push) Failing after 2m47s
This commit is contained in:
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user