diff --git a/web/src/components/PageActions.tsx b/web/src/components/PageActions.tsx new file mode 100644 index 0000000..c7a6629 --- /dev/null +++ b/web/src/components/PageActions.tsx @@ -0,0 +1,17 @@ +import { createContext, useContext, useEffect } from 'react'; +import type { ReactNode } from 'react'; + +// 页头动作槽:页面挂载时把自己的操作按钮注册到布局 PageContainer 的 extra(与标题同一行), +// 卸载时自动清除。Layout 提供 Provider,页面只消费 usePageActions。 +const PageActionsContext = createContext<(node: ReactNode) => void>(() => {}); + +export function usePageActions(node: ReactNode, deps: unknown[] = []) { + const setPageActions = useContext(PageActionsContext); + useEffect(() => { + setPageActions(node); + return () => setPageActions(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); +} + +export { PageActionsContext }; diff --git a/web/src/components/accounts/AccountManagementList.tsx b/web/src/components/accounts/AccountManagementList.tsx index 6e6609b..eab3e44 100644 --- a/web/src/components/accounts/AccountManagementList.tsx +++ b/web/src/components/accounts/AccountManagementList.tsx @@ -19,6 +19,7 @@ import { PlusOutlined, CloudUploadOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { getList, remove, creatorAction, creatorUpdate } from '@/services/api'; import { conflictMessage, isCollectionAccount, platformLabel } from '@/utils/helpers'; +import { usePageActions } from '@/components/PageActions'; // 语义对齐 web.archived/src/features/accounts/AccountsPage.jsx 的 AccountManagementList(mode: owned/monitoring)。 // 仅用 antd 默认组件:Table/Tag/Modal/Popconfirm/Select(tags)。 @@ -223,23 +224,25 @@ export default function AccountManagementList({ mode }: { mode: 'owned' | 'monit }, ]; + usePageActions( + isMonitoring ? ( + + ) : ( + + ), + [isMonitoring], + ); + return ( {contextHolder} -
- - 共 {rows.length} 个{isMonitoring ? '监控' : '自有'}账号 - - {isMonitoring ? ( - - ) : ( - - )} -
+ + 共 {rows.length} 个{isMonitoring ? '监控' : '自有'}账号 + {error ? ( 重试} /> ) : null} diff --git a/web/src/layouts/index.tsx b/web/src/layouts/index.tsx index ffdc48d..726c47f 100644 --- a/web/src/layouts/index.tsx +++ b/web/src/layouts/index.tsx @@ -1,67 +1,78 @@ +import { useState } from 'react'; +import type { ReactNode } from 'react'; import { history, Outlet, useLocation } from '@umijs/max'; import { PageContainer, ProLayout } from '@ant-design/pro-components'; import { Dropdown, Typography } from 'antd'; import { LogoutOutlined } from '@ant-design/icons'; -import { menu, pageMetadata } from '@/utils/metadata'; +import { PageActionsContext } from '@/components/PageActions'; +import { menu, menuSelectedKey, pageMetadata } from '@/utils/metadata'; -// 菜单结构对齐 web.archived/src/app/Layout.jsx(运营 / 资源 两组);侧栏仅保留模块链接菜单,标题由 TopNav 呈现。 +// 菜单结构对齐 web.archived/src/app/Layout.jsx(运营 / 资源 两组);侧栏仅保留模块链接菜单,标题由内容区页头呈现。 // Umi 4(react-router 6)全局布局:location 用 useLocation(),子路由用 渲染 // (渲染器不传 children prop,用 children 会导致整页空白且无任何报错)。 export default function Layout() { const location = useLocation(); + // 页头动作槽:列表页在挂载时把自己的添加类按钮注册进来,与标题渲染在同一行。 + const [pageActions, setPageActions] = useState(null); if (location.pathname === '/login') return ; + const meta = pageMetadata(location.pathname); + return ( - ( - item.path && history.push(item.path)}>{dom} - )} - avatarProps={{ - icon: , - size: 'small', - render: (_props, dom) => ( - , - label: '退出登录', - onClick: () => { - localStorage.removeItem('creatorhub.auth'); - history.replace('/login'); + + ( + item.path && history.push(item.path)}>{dom} + )} + avatarProps={{ + icon: , + size: 'small', + render: (_props, dom) => ( + , + label: '退出登录', + onClick: () => { + localStorage.removeItem('creatorhub.auth'); + history.replace('/login'); + }, }, - }, - ], - }} - > - {dom} - - ), - }} - > - {/* 页面标题在卡片外面(内容区右上角);卡片内不再重复渲染标题。 */} - - {pageMetadata(location.pathname).title} - {pageMetadata(location.pathname).subtitle} - - } + ], + }} + > + {dom} + + ), + }} > - - - + {/* 页面标题在卡片外(内容区左上角),动作按钮与标题同一行(右侧);卡片内不再重复渲染标题。 */} + + {meta.title} + {meta.subtitle} + + } + subTitle={false} + extra={pageActions} + > + + + + ); } diff --git a/web/src/pages/accounts/import.tsx b/web/src/pages/accounts/import.tsx index e1f5c3d..f12a423 100644 --- a/web/src/pages/accounts/import.tsx +++ b/web/src/pages/accounts/import.tsx @@ -17,6 +17,7 @@ import { import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { create, getList, creatorAction } from '@/services/api'; +import { usePageActions } from '@/components/PageActions'; import { conflictMessage, dateTime, extractShareURL, platformForShareURL, platformLabel, shareJobStatus } from '@/utils/helpers'; interface Job { @@ -157,20 +158,15 @@ export default function Page() { }, ]; + usePageActions( + , + ); + return ( {contextHolder} -
- -
分享链接解析任务 diff --git a/web/src/pages/browsers/index.tsx b/web/src/pages/browsers/index.tsx index 454f677..20b767e 100644 --- a/web/src/pages/browsers/index.tsx +++ b/web/src/pages/browsers/index.tsx @@ -6,6 +6,7 @@ import { Alert, Button, Card, Popconfirm, Space, Table, Tag, Typography, message import { CopyOutlined, PlusOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { browserAction, getList } from '@/services/api'; +import { usePageActions } from '@/components/PageActions'; const stateText: Record = { created: '已创建', @@ -159,15 +160,15 @@ export default function Page() { }, ]; + usePageActions( + , + ); + return ( {contextHolder} -
- 启动、停止并回收隔离的指纹浏览器运行环境 - -
{error ? ( 重试} /> ) : null} diff --git a/web/src/pages/creator/workbench/index.tsx b/web/src/pages/creator/workbench/index.tsx index ec34307..0ff3034 100644 --- a/web/src/pages/creator/workbench/index.tsx +++ b/web/src/pages/creator/workbench/index.tsx @@ -990,7 +990,7 @@ export default function Page() { ]; const operationsTab = ( - + <> - + ); return ( -
+ {contextHolder} 当前私信文案尚未发送,放弃后再切换吗? -
+ ); } diff --git a/web/src/pages/dashboard/index.tsx b/web/src/pages/dashboard/index.tsx index 5830b5b..fdd03b9 100644 --- a/web/src/pages/dashboard/index.tsx +++ b/web/src/pages/dashboard/index.tsx @@ -1,11 +1,12 @@ // 工作台:对标 pro 官方 workplace 预览的占位页。 // 统计卡片与快捷入口暂不接真实数据(后期规划),仅布局占位;不引入兜底数据。 -import { PageContainer } from '@ant-design/pro-components'; import { Card, Col, Empty, Row, Typography } from 'antd'; +// 统计卡片与快捷入口暂不接真实数据(后期规划),仅布局占位;不引入兜底数据。 +// 页面标题由全局布局的 PageContainer 提供,这里只渲染内容区。 export default function Page() { return ( - + <> @@ -28,6 +29,6 @@ export default function Page() { > - + ); } diff --git a/web/src/pages/gateways/index.tsx b/web/src/pages/gateways/index.tsx index a325209..47f98ab 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 { usePageActions } from '@/components/PageActions'; const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/; const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{15,127}$/; @@ -169,15 +170,15 @@ export default function Page() { }, ]; + usePageActions( + , + ); + return ( {contextHolder} -
- 注册宿主机 native browser gateway;令牌须与 gateway 进程的 GATEWAY_TOKEN 一致 - -
{created ? ( = { unchecked: '未检测', @@ -277,15 +278,15 @@ export default function Page() { }, ]; + usePageActions( + , + ); + return ( {contextHolder} -
- 维护固定出口、健康观测与账号绑定 - -
{error ? ( 重试} /> ) : null} diff --git a/web/src/utils/metadata.tsx b/web/src/utils/metadata.tsx index 0e75f31..653b4ef 100644 --- a/web/src/utils/metadata.tsx +++ b/web/src/utils/metadata.tsx @@ -74,3 +74,17 @@ export function pageMetadata(pathname: string): { title: string; subtitle: strin } return { title: 'CreatorHub', subtitle: '运营控制台' }; } + +export function menuSelectedKey(pathname: string): string { + const leafPaths = menu.flatMap((group) => (group.children ?? []).map((item) => item.path ?? '')); + const segments = pathname.split('/').filter(Boolean); + for (let i = segments.length; i >= 1; i--) { + const candidate = `/${segments.slice(0, i).join('/')}`; + if (leafPaths.includes(candidate)) return candidate; + } + return pathname; +} + +// 侧栏选中态:ProLayout 默认按路径前缀高亮(/accounts 是 /accounts/import 的前缀,两项会同时高亮)。 +// 这里改为受控 selectedKeys:从完整路径逐段向前回退,仅精确命中菜单叶路径; +// 详情页(如 /accounts/123、/accounts/new)回落到父级菜单项。