Files
xiaoxia-saas/apps/web/src/components/layout/PageHead.tsx
T
灵应 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
feat: 新增配音素材管理页面 VoiceMaterialLibrary
- 新建 VoiceMaterialLibrary.tsx:卡片/列表双布局、上传、试听、编辑、删除
- 元信息:名称、音色描述、性别(男/女/童声/中性)、风格标签
- V21 设计系统样式(voice-materials.css),含性别色带、响应式适配
- 路由 /app/voice-materials,导航 + PageHead 面包屑配置
- 当前阶段:静态页面 + Mock 数据,等后端 API 就绪后对接

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 09:11:17 +08:00

195 lines
6.4 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;