Files
xiaoxia-saas/apps/web/src/components/layout/PageHead.tsx
T
xiaoxia fb9f5cc1c3
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 / Staging API Integration Tests (push) Failing after 1411h54m44s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 1411h54m45s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 1411h54m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1411h55m19s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1411h55m17s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1411h55m19s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1411h55m20s
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Failing after 1412h27m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 1412h27m26s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1412h26m51s
feat(P1): #557 剪辑编辑器改为纯模板编辑器 (#607)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-07-19 19:34:07 +08:00

183 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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