diff --git a/web/src/components/PageActions.tsx b/web/src/components/PageActions.tsx index a750d54..32ba187 100644 --- a/web/src/components/PageActions.tsx +++ b/web/src/components/PageActions.tsx @@ -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 }; diff --git a/web/src/layouts/index.tsx b/web/src/layouts/index.tsx index ec21af3..0e7f755 100644 --- a/web/src/layouts/index.tsx +++ b/web/src/layouts/index.tsx @@ -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(null); const [pageActions, setPageActions] = useState(null); if (location.pathname === '/login') return ; const meta = pageMetadata(location.pathname); return ( - + - {/* 页面标题在卡片外(内容区左上角),动作按钮单独一行渲染在标题下方(content 槽,右侧对齐);卡片内不再重复渲染标题。 */} + {/* 页面标题在卡片外(内容区左上角),content 行左侧可放实体标识/关键状态、右侧放操作按钮;卡片内不再重复渲染标题。 */} {/* App 包裹:App.useApp() 的 message 等实例需要此上下文(component=false 不增加 DOM,避免破坏 ProLayout 布局)。 */} {pageActions} : undefined} + // 标题与 content 行各占一行:content 行左标识/右按钮,两侧都有时 space-between, + // 仅一侧时按该侧对齐;右对齐用 Flex 组件 API,不写自定义对齐样式。 + content={ + pageInfo || pageActions ? ( + + {pageInfo} + {pageActions && {pageActions}} + + ) : undefined + } > - + ); } diff --git a/web/src/pages/accounts/$id/index.tsx b/web/src/pages/accounts/$id/index.tsx index f6d4da3..c8f1aa9 100644 --- a/web/src/pages/accounts/$id/index.tsx +++ b/web/src/pages/accounts/$id/index.tsx @@ -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 ? ( + + {readiness.label} + + ) : null; + + // 页头 content 行左侧:实体标识(账号名 + 状态 + 平台明细),对标 Advanced Profile 的「状态:待审批」信息行。 + usePageInfo( + account ? ( + + + {account.name} + + {statusTag} + + {account.platform_account_key} · {platformLabel(account.platform)} + + + ) : null, + [account?.id, account?.name, account?.runtime_status, binding, browsersError], + ); + + // 页头 content 行右侧:返回/编辑注册到 Layout 页头槽,不在卡片内重复渲染。 usePageActions( account ? ( @@ -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 ; if (error || !account) return ; - const statusTag = ( - - {readiness?.label} - + // 账号状态卡局部操作:暂停/恢复留在状态卡右上角(Card extra),不进页头槽。 + const pauseResume = ( + + + + ); - const items = [ - { - key: 'status', - label: '账号状态', - children: ( + return ( + + {contextHolder} + - - {statusTag} - - - - - - + {account.authorization_status === 'authorized' ? '已授权' : '已撤销'} {`${account.runtime_status === 'active' ? '启用' : '暂停'} · 版本 ${account.version}`} {account.tags?.join('、') || '无'} - ), - }, - { - key: 'binding', - label: '固定资源', - children: browsersError ? ( - - 重试环境状态 - - } - /> - ) : binding ? ( -
- - {`${binding.name}(${binding.alias})`} - - {binding.network_exit_id ? `${binding.network_exit_id} · ${binding.network_exit_health || '未知状态'}` : '当前机器直连'} - - {binding.binding_version} - - {binding.schedule_block_reason || '无'} - - - history.push('/browsers')} style={{ display: 'inline-block', marginTop: 12 }}> - 前往运行环境 - -
- ) : ( -
- 尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。 - history.push('/browsers')}>前往运行环境 -
- ), - }, - { - key: 'drafts', - label: '文本草稿', - children: ( +
+ + {browsersError ? ( + + 重试环境状态 + + } + /> + ) : binding ? ( + + + {`${binding.name}(${binding.alias})`} + + {binding.network_exit_id ? `${binding.network_exit_id} · ${binding.network_exit_health || '未知状态'}` : '当前机器直连'} + + {binding.binding_version} + + {binding.schedule_block_reason || '无'} + + + history.push('/browsers')}>前往运行环境 + + ) : ( + + + 尚未绑定运行环境。账号暂停且资源就绪后,可在运行环境页创建绑定。 + + history.push('/browsers')}>前往运行环境 + + )} + + 仅支持单用户 Phase A Mock 文本;内部 ID 与版本由系统生成。
@@ -228,23 +239,7 @@ export default function Page() { ))} - ), - }, - ]; - - return ( -
- {contextHolder} - {/* 实体标识行:账号名 + 平台明细(动态数据,非页面标题);操作按钮在页头槽。 */} -
- - {account.name} - - - {account.platform_account_key} · {platformLabel(account.platform)} - -
- +

{`暂停账号 ${account.name}?待领取任务将进入 hold,恢复账号不会自动恢复既有 hold。`}

-
+ ); }