diff --git a/apps/web/src/components/layout/PageHead.tsx b/apps/web/src/components/layout/PageHead.tsx deleted file mode 100644 index 46b71d2c1..000000000 --- a/apps/web/src/components/layout/PageHead.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/** - * PageHead - 页面头部组件(Task 1.4) - * - * 功能: - * - 页面标题展示 - * - 面包屑导航(自动根据路由生成,也支持手动传入) - * - 右侧操作按钮区(slot,由页面自行填充) - * - 响应式:移动端简化布局(隐藏面包屑,缩小标题) - * - * 复用 global.css 中已有的 .xx-page-head 基础样式, - * 补充面包屑、操作区等扩展样式。 - */ -import React from "react" -import { useLocation, useNavigate, Link } from "react-router-dom" -import { RightOutlined, HomeOutlined } from "@ant-design/icons" -import "./PageHead.css" - -/* ── 类型定义 ─────────────────────────────────────────────── */ - -/** 面包屑项 */ -export interface BreadcrumbItem { - /** 显示文字 */ - label: string - /** 路由路径,不传则为当前页(不可点击) */ - path?: string -} - -/** PageHead 组件属性 */ -export interface PageHeadProps { - /** 页面标题 */ - title: string - /** 页面描述(可选,显示在标题下方) */ - description?: React.ReactNode - /** 面包屑项(可选,不传则自动根据路由生成) */ - breadcrumb?: BreadcrumbItem[] - /** 右侧操作区内容(按钮等) */ - actions?: React.ReactNode - /** 是否隐藏面包屑 */ - hideBreadcrumb?: boolean -} - -/* ── 路由 → 标题映射(用于自动生成面包屑) ────────────────── */ - -const ROUTE_TITLE_MAP: Record = { - "/app/dashboard": "首页", - "/app/generate": "智能剪辑", - "/app/assets": "视频库", - "/app/voices": "配音库", - "/app/titles": "标题库", - "/app/products": "成片库", - "/app/templates": "模板库", - "/app/history": "任务历史", - "/app/admin": "控制台", - "/app/admin/users": "用户管理", - "/app/admin/analytics": "数据分析", - "/app/admin/monitor": "系统监控", - "/app/admin/logs": "系统日志", - "/app/subscription": "订阅管理", - "/app/subscription/upgrade": "升级订阅", - "/app/subscription/billing": "账单管理", - "/app/profile": "个人设置", - "/app/editing-planner": "模板制作", - "/app/my-templates": "我的模板", - "/app/voice-clone": "我的音色", - "/app/voice-materials": "配音库", - "/app/accounts": "账号管理", - "/app/duplication": "查重", - "/app/duplication/results": "查重结果", -} - -/* ── 自动生成面包屑 ─────────────────────────────────────── */ - -/** 根据当前路径生成面包屑 */ -const generateBreadcrumb = (pathname: string): BreadcrumbItem[] => { - const items: BreadcrumbItem[] = [{ label: "首页", path: "/app/dashboard" }] - - // 首页本身不需要面包屑 - if (pathname === "/app" || pathname === "/app/dashboard") { - return items - } - - // 逐级拆分路径,生成中间层级 - const segments = pathname.split("/").filter(Boolean) - let currentPath = "" - - for (let i = 0; i < segments.length; i++) { - currentPath += `/${segments[i]}` - const title = ROUTE_TITLE_MAP[currentPath] - - if (title) { - // 最后一级不带 path(当前页面,不可点击) - const isLast = i === segments.length - 1 - items.push({ - label: title, - path: isLast ? undefined : currentPath, - }) - } else { - // 动态路由段(如 :id),用路径片段做 label - const isLast = i === segments.length - 1 - items.push({ - label: segments[i], - path: isLast ? undefined : currentPath, - }) - } - } - - return items -} - -/* ── 组件 ───────────────────────────────────────────────── */ - -const PageHead: React.FC = ({ - title, - description, - breadcrumb, - actions, - hideBreadcrumb = false, -}) => { - const location = useLocation() - const navigate = useNavigate() - - // 使用传入的面包屑或自动生成 - const breadcrumbItems = breadcrumb ?? generateBreadcrumb(location.pathname) - - // 首页不显示面包屑 - const showBreadcrumb = - !hideBreadcrumb && - breadcrumbItems.length > 1 && - location.pathname !== "/app" && - location.pathname !== "/app/dashboard" - - return ( -
-
- {/* 面包屑导航 */} - {showBreadcrumb && ( - - )} - - {/* 标题 + 描述 */} -
-

{title}

- {description &&

{description}

} -
-
- - {/* 右侧操作区 */} - {actions &&
{actions}
} -
- ) -} - -export default PageHead diff --git a/apps/web/src/components/layout/PageHead.css b/apps/web/src/components/layout/PageHead/PageHead.css similarity index 99% rename from apps/web/src/components/layout/PageHead.css rename to apps/web/src/components/layout/PageHead/PageHead.css index 9789a9183..dbbc3dbcf 100644 --- a/apps/web/src/components/layout/PageHead.css +++ b/apps/web/src/components/layout/PageHead/PageHead.css @@ -159,3 +159,4 @@ flex-wrap: wrap; } } + diff --git a/apps/web/src/components/layout/PageHead/constants.ts b/apps/web/src/components/layout/PageHead/constants.ts new file mode 100644 index 000000000..1082d633b --- /dev/null +++ b/apps/web/src/components/layout/PageHead/constants.ts @@ -0,0 +1,27 @@ +/** 路由 → 标题映射(用于自动生成面包屑) */ +export const ROUTE_TITLE_MAP: Record = { + "/app/dashboard": "首页", + "/app/generate": "智能剪辑", + "/app/assets": "视频库", + "/app/voices": "配音库", + "/app/titles": "标题库", + "/app/products": "成片库", + "/app/templates": "模板库", + "/app/history": "任务历史", + "/app/admin": "控制台", + "/app/admin/users": "用户管理", + "/app/admin/analytics": "数据分析", + "/app/admin/monitor": "系统监控", + "/app/admin/logs": "系统日志", + "/app/subscription": "订阅管理", + "/app/subscription/upgrade": "升级订阅", + "/app/subscription/billing": "账单管理", + "/app/profile": "个人设置", + "/app/editing-planner": "模板制作", + "/app/my-templates": "我的模板", + "/app/voice-clone": "我的音色", + "/app/voice-materials": "配音库", + "/app/accounts": "账号管理", + "/app/duplication": "查重", + "/app/duplication/results": "查重结果", +} diff --git a/apps/web/src/components/layout/PageHead/index.tsx b/apps/web/src/components/layout/PageHead/index.tsx new file mode 100644 index 000000000..925a91dce --- /dev/null +++ b/apps/web/src/components/layout/PageHead/index.tsx @@ -0,0 +1,91 @@ +/** + * PageHead - 页面头部组件(Task 1.4) + * + * 功能: + * - 页面标题展示 + * - 面包屑导航(自动根据路由生成,也支持手动传入) + * - 右侧操作按钮区(slot,由页面自行填充) + * - 响应式:移动端简化布局(隐藏面包屑,缩小标题) + * + * 复用 global.css 中已有的 .xx-page-head 基础样式, + * 补充面包屑、操作区等扩展样式。 + */ +import React from "react" +import { useLocation, useNavigate, Link } from "react-router-dom" +import { RightOutlined, HomeOutlined } from "@ant-design/icons" +import type { PageHeadProps } from "./types" +import { generateBreadcrumb } from "./utils" +import "./PageHead.css" + +const PageHead: React.FC = ({ + title, + description, + breadcrumb, + actions, + hideBreadcrumb = false, +}) => { + const location = useLocation() + const navigate = useNavigate() + + // 使用传入的面包屑或自动生成 + const breadcrumbItems = breadcrumb ?? generateBreadcrumb(location.pathname) + + // 首页不显示面包屑 + const showBreadcrumb = + !hideBreadcrumb && + breadcrumbItems.length > 1 && + location.pathname !== "/app" && + location.pathname !== "/app/dashboard" + + return ( +
+
+ {/* 面包屑导航 */} + {showBreadcrumb && ( + + )} + + {/* 标题 + 描述 */} +
+

{title}

+ {description &&

{description}

} +
+
+ + {/* 右侧操作区 */} + {actions &&
{actions}
} +
+ ) +} + +export default PageHead +export type { BreadcrumbItem, PageHeadProps } from "./types" diff --git a/apps/web/src/components/layout/PageHead/types.ts b/apps/web/src/components/layout/PageHead/types.ts new file mode 100644 index 000000000..f034dfe61 --- /dev/null +++ b/apps/web/src/components/layout/PageHead/types.ts @@ -0,0 +1,23 @@ +import type React from "react" + +/** 面包屑项 */ +export interface BreadcrumbItem { + /** 显示文字 */ + label: string + /** 路由路径,不传则为当前页(不可点击) */ + path?: string +} + +/** PageHead 组件属性 */ +export interface PageHeadProps { + /** 页面标题 */ + title: string + /** 页面描述(可选,显示在标题下方) */ + description?: React.ReactNode + /** 面包屑项(可选,不传则自动根据路由生成) */ + breadcrumb?: BreadcrumbItem[] + /** 右侧操作区内容(按钮等) */ + actions?: React.ReactNode + /** 是否隐藏面包屑 */ + hideBreadcrumb?: boolean +} diff --git a/apps/web/src/components/layout/PageHead/utils.ts b/apps/web/src/components/layout/PageHead/utils.ts new file mode 100644 index 000000000..cda324406 --- /dev/null +++ b/apps/web/src/components/layout/PageHead/utils.ts @@ -0,0 +1,39 @@ +import { ROUTE_TITLE_MAP } from "./constants" +import type { BreadcrumbItem } from "./types" + +/** 根据当前路径生成面包屑 */ +export const generateBreadcrumb = (pathname: string): BreadcrumbItem[] => { + const items: BreadcrumbItem[] = [{ label: "首页", path: "/app/dashboard" }] + + // 首页本身不需要面包屑 + if (pathname === "/app" || pathname === "/app/dashboard") { + return items + } + + // 逐级拆分路径,生成中间层级 + const segments = pathname.split("/").filter(Boolean) + let currentPath = "" + + for (let i = 0; i < segments.length; i++) { + currentPath += `/${segments[i]}` + const title = ROUTE_TITLE_MAP[currentPath] + + if (title) { + // 最后一级不带 path(当前页面,不可点击) + const isLast = i === segments.length - 1 + items.push({ + label: title, + path: isLast ? undefined : currentPath, + }) + } else { + // 动态路由段(如 :id),用路径片段做 label + const isLast = i === segments.length - 1 + items.push({ + label: segments[i], + path: isLast ? undefined : currentPath, + }) + } + } + + return items +} diff --git a/apps/web/src/test/components/layout/PageHead.test.tsx b/apps/web/src/test/components/layout/PageHead/index.test.tsx similarity index 100% rename from apps/web/src/test/components/layout/PageHead.test.tsx rename to apps/web/src/test/components/layout/PageHead/index.test.tsx