fb9f5cc1c3
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 22s
CI/CD Pipeline / Build Staging API Image (push) Failing after 26s
CI/CD Pipeline / Build Staging Web Image (push) Failing after 33s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 3m28s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 3m56s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 4m2s
CI/CD Pipeline / Unit Tests (push) Successful in 4m7s
CI/CD Pipeline / Integration Tests (push) Successful in 1m26s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
183 lines
6.0 KiB
TypeScript
183 lines
6.0 KiB
TypeScript
/**
|
||
* 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<string, string> = {
|
||
"/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<PageHeadProps> = ({
|
||
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 (
|
||
<header className="xx-page-head">
|
||
<div className="xx-page-head-left">
|
||
{/* 面包屑导航 */}
|
||
{showBreadcrumb && (
|
||
<nav className="xx-page-breadcrumb" aria-label="面包屑导航">
|
||
<ol>
|
||
{breadcrumbItems.map((item, index) => {
|
||
const isLast = index === breadcrumbItems.length - 1
|
||
return (
|
||
<li key={`${item.label}-${index}`} className="xx-page-breadcrumb-item">
|
||
{index > 0 && <RightOutlined className="xx-page-breadcrumb-separator" />}
|
||
{item.path && !isLast ? (
|
||
<Link
|
||
to={item.path}
|
||
className="xx-page-breadcrumb-link"
|
||
onClick={(e) => {
|
||
e.preventDefault()
|
||
navigate(item.path!)
|
||
}}
|
||
>
|
||
{index === 0 ? <HomeOutlined className="xx-page-breadcrumb-home" /> : null}
|
||
<span>{item.label}</span>
|
||
</Link>
|
||
) : (
|
||
<span className="xx-page-breadcrumb-current" aria-current="page">
|
||
{index === 0 ? <HomeOutlined className="xx-page-breadcrumb-home" /> : null}
|
||
<span>{item.label}</span>
|
||
</span>
|
||
)}
|
||
</li>
|
||
)
|
||
})}
|
||
</ol>
|
||
</nav>
|
||
)}
|
||
|
||
{/* 标题 + 描述 */}
|
||
<div className="xx-page-head-title">
|
||
<h2>{title}</h2>
|
||
{description && <p>{description}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 右侧操作区 */}
|
||
{actions && <div className="xx-page-head-actions">{actions}</div>}
|
||
</header>
|
||
)
|
||
}
|
||
|
||
export default PageHead
|