32e78f31e2
CI/CD Pipeline / Deploy Staging (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (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 / Frontend Lint (push) Failing after 51h43m2s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 51h43m2s
- 新建 VoiceMaterialLibrary.tsx:卡片/列表双布局、上传、试听、编辑、删除 - 元信息:名称、音色描述、性别(男/女/童声/中性)、风格标签 - V21 设计系统样式(voice-materials.css),含性别色带、响应式适配 - 路由 /app/voice-materials,导航 + PageHead 面包屑配置 - 当前阶段:静态页面 + Mock 数据,等后端 API 就绪后对接 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
195 lines
6.4 KiB
TypeScript
195 lines
6.4 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;
|