diff --git a/apps/web/src/components/layout/PageHead.css b/apps/web/src/components/layout/PageHead.css new file mode 100644 index 000000000..9789a9183 --- /dev/null +++ b/apps/web/src/components/layout/PageHead.css @@ -0,0 +1,161 @@ +/* ============================================================ + PageHead.css - 页面头部扩展样式(Task 1.4) + + 基础样式复用 global.css 中的 .xx-page-head, + 本文件补充面包屑、标题区、操作区等扩展样式。 + + 结构: + .xx-page-head 头部容器(global.css 定义) + ├── .xx-page-head-left 左侧区域 + │ ├── .xx-page-breadcrumb 面包屑导航 + │ │ └── .xx-page-breadcrumb-item 面包屑项 + │ │ ├── .xx-page-breadcrumb-separator 分隔符 + │ │ ├── .xx-page-breadcrumb-link 可点击链接 + │ │ ├── .xx-page-breadcrumb-current 当前页(不可点击) + │ │ └── .xx-page-breadcrumb-home 首页图标 + │ └── .xx-page-head-title 标题 + 描述 + └── .xx-page-head-actions 右侧操作按钮区 + ============================================================ */ + +/* ── 左侧区域 ─────────────────────────────────────────────── */ +.xx-page-head-left { + display: flex; + flex-direction: column; + gap: var(--space-xs); + min-width: 0; /* 允许内容收缩,防止溢出 */ +} + +/* ── 面包屑导航 ───────────────────────────────────────────── */ +.xx-page-breadcrumb { + margin: 0; + padding: 0; +} + +.xx-page-breadcrumb ol { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-xxs); + margin: 0; + padding: 0; + list-style: none; +} + +.xx-page-breadcrumb-item { + display: flex; + align-items: center; + gap: var(--space-xxs); + font-size: var(--font-size-sm); + color: var(--text-tertiary); + line-height: var(--line-height-tight); +} + +/* 分隔符 */ +.xx-page-breadcrumb-separator { + font-size: 10px; + color: var(--text-quaternary); + flex-shrink: 0; +} + +/* 可点击链接 */ +.xx-page-breadcrumb-link { + display: inline-flex; + align-items: center; + gap: var(--space-xxs); + color: var(--text-secondary); + text-decoration: none; + transition: var(--transition-colors); + border-radius: var(--radius-xs); + padding: 2px 4px; + margin: -2px -4px; +} + +.xx-page-breadcrumb-link:hover { + color: var(--primary-color); + background: var(--primary-soft); +} + +/* 当前页(不可点击) */ +.xx-page-breadcrumb-current { + display: inline-flex; + align-items: center; + gap: var(--space-xxs); + color: var(--text-primary); + font-weight: var(--font-weight-medium); +} + +/* 首页图标 */ +.xx-page-breadcrumb-home { + font-size: 12px; +} + +/* ── 标题区 ───────────────────────────────────────────────── */ +.xx-page-head-title h2 { + margin: 0; +} + +.xx-page-head-title p { + margin: var(--space-xxs) 0 0; +} + +/* ── 右侧操作区 ───────────────────────────────────────────── */ +.xx-page-head-actions { + display: flex; + align-items: center; + gap: var(--space-sm); + flex-shrink: 0; + margin-left: var(--space-lg); +} + +/* ============================================================ + 响应式 —— 平板(≤1024px) + 操作区按钮紧凑排列 + ============================================================ */ +@media (max-width: 1024px) { + .xx-page-head-actions { + gap: var(--space-xs); + margin-left: var(--space-md); + } +} + +/* ============================================================ + 响应式 —— 移动端(≤768px) + 隐藏面包屑,简化布局 + ============================================================ */ +@media (max-width: 768px) { + /* 隐藏面包屑 */ + .xx-page-breadcrumb { + display: none; + } + + /* 标题区占满宽度 */ + .xx-page-head-left { + flex: 1; + } + + /* 操作区换行显示 */ + .xx-page-head { + flex-wrap: wrap; + gap: var(--space-sm); + } + + .xx-page-head-actions { + margin-left: 0; + width: 100%; + justify-content: flex-start; + } +} + +/* ============================================================ + 响应式 —— 小屏手机(≤480px) + 标题缩小,操作区堆叠 + ============================================================ */ +@media (max-width: 480px) { + .xx-page-head-title h2 { + font-size: 20px; + } + + .xx-page-head-actions { + flex-wrap: wrap; + } +} diff --git a/apps/web/src/components/layout/PageHead.tsx b/apps/web/src/components/layout/PageHead.tsx new file mode 100644 index 000000000..191e4e685 --- /dev/null +++ b/apps/web/src/components/layout/PageHead.tsx @@ -0,0 +1,185 @@ +/** + * 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?: string; + /** 面包屑项(可选,不传则自动根据路由生成) */ + breadcrumb?: BreadcrumbItem[]; + /** 右侧操作区内容(按钮等) */ + actions?: React.ReactNode; + /** 是否隐藏面包屑 */ + hideBreadcrumb?: boolean; +} + +/* ── 路由 → 标题映射(用于自动生成面包屑) ────────────────── */ + +const ROUTE_TITLE_MAP: Record = { + "/dashboard": "首页", + "/generate": "一键生成", + "/assets": "素材库", + "/voices": "配音库", + "/titles": "标题库", + "/products": "成片库", + "/templates": "模板库", + "/history": "任务历史", + "/admin": "控制台", + "/admin/users": "用户管理", + "/admin/analytics": "数据分析", + "/admin/monitor": "系统监控", + "/admin/logs": "系统日志", + "/subscription": "订阅管理", + "/subscription/upgrade": "升级订阅", + "/subscription/billing": "账单管理", + "/profile": "个人设置", + "/editing-planner": "剪辑规划", + "/my-templates": "我的模板", + "/duplication": "查重", + "/duplication/results": "查重结果", +}; + +/* ── 自动生成面包屑 ─────────────────────────────────────── */ + +/** 根据当前路径生成面包屑 */ +const generateBreadcrumb = (pathname: string): BreadcrumbItem[] => { + const items: BreadcrumbItem[] = [{ label: "首页", path: "/dashboard" }]; + + // 首页本身不需要面包屑 + if (pathname === "/" || pathname === "/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 !== "/" && + location.pathname !== "/dashboard"; + + return ( +
+
+ {/* 面包屑导航 */} + {showBreadcrumb && ( + + )} + + {/* 标题 + 描述 */} +
+

{title}

+ {description &&

{description}

} +
+
+ + {/* 右侧操作区 */} + {actions &&
{actions}
} +
+ ); +}; + +export default PageHead; diff --git a/apps/web/src/pages/assets/AssetLibrary.tsx b/apps/web/src/pages/assets/AssetLibrary.tsx index 4d41d916c..9b8da2a68 100644 --- a/apps/web/src/pages/assets/AssetLibrary.tsx +++ b/apps/web/src/pages/assets/AssetLibrary.tsx @@ -40,8 +40,9 @@ import { uploadAssetDirect, } from "@/api/assets"; import { getOrCreateDefaultProject } from "@/api/projects"; +import PageHead from "@/components/layout/PageHead"; -const { Title, Text } = Typography; +const { Text } = Typography; const { Dragger } = Upload; /** 最大文件大小:2048MB(与后端 OSS_DIRECT_UPLOAD_MAX_MB=2000 对齐,留少量余量) */ @@ -193,27 +194,18 @@ const AssetLibrary: React.FC = () => { return (
-
- - 素材库 - - -
+ } + onClick={() => setCreateModalOpen(true)} + > + 新建素材库 + + } + /> {libsLoading ? (
diff --git a/apps/web/src/pages/dashboard/Dashboard.tsx b/apps/web/src/pages/dashboard/Dashboard.tsx index c3cafcac1..7bddd5d4b 100644 --- a/apps/web/src/pages/dashboard/Dashboard.tsx +++ b/apps/web/src/pages/dashboard/Dashboard.tsx @@ -11,7 +11,6 @@ import { Statistic, Table, Tag, - Typography, Spin, Button, Progress, @@ -31,8 +30,7 @@ import { import { useNavigate } from "react-router-dom"; import { getDashboardOverview } from "@/api/dashboard"; import type { ColumnsType } from "antd/es/table"; - -const { Title } = Typography; +import PageHead from "@/components/layout/PageHead"; /** 格式化文件大小 */ const formatFileSize = (bytes: number): string => { @@ -126,9 +124,7 @@ const Dashboard: React.FC = () => { return (
- - 概览 - + {/* 用量统计卡片 */} diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 8e8edb2a7..d655967cf 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -32,8 +32,9 @@ import { getAssetLibraries, getAssets, type AssetItem } from "@/api/assets"; import { getTitles } from "@/api/titles"; import { getVoices } from "@/api/voices"; import { createGenerationTask } from "@/api/tasks"; +import PageHead from "@/components/layout/PageHead"; -const { Title, Text } = Typography; +const { Text } = Typography; const GeneratePage: React.FC = () => { const navigate = useNavigate(); @@ -325,9 +326,7 @@ const GeneratePage: React.FC = () => { return (
- - 一键生成 - + = ({ status }) => { @@ -152,33 +150,24 @@ const TaskHistory: React.FC = () => { return (
-
- - 任务历史 - - = ({ status }) => { @@ -230,28 +231,19 @@ const VoiceLibrary: React.FC = () => { return (
-
- - 配音库 - - - - - -
+ + + + + } + /> {isLoading ? (