feat(ui): 实现页面头部组件 PageHead
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
- 新增 PageHead 组件:页面标题、面包屑导航、操作按钮区域 - 面包屑根据路由路径自动生成,支持手动覆盖 - 响应式适配:移动端隐藏面包屑,操作区换行 - 复用 global.css 中 .xx-page-head 基础样式 - 集成到所有页面:Dashboard、素材库、配音库、标题库、 任务历史、成品库、模板库、一键生成、订阅页
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> = {
|
||||
"/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<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 !== "/" &&
|
||||
location.pathname !== "/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;
|
||||
@@ -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 (
|
||||
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 24,
|
||||
flexWrap: "wrap",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
素材库
|
||||
</Title>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
新建素材库
|
||||
</Button>
|
||||
</div>
|
||||
<PageHead
|
||||
title="素材库"
|
||||
actions={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setCreateModalOpen(true)}
|
||||
>
|
||||
新建素材库
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{libsLoading ? (
|
||||
<div style={{ textAlign: "center", padding: 60 }}>
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
|
||||
<Title level={3} style={{ marginBottom: 24 }}>
|
||||
概览
|
||||
</Title>
|
||||
<PageHead title="概览" />
|
||||
|
||||
{/* 用量统计卡片 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
|
||||
<Title level={3} style={{ marginBottom: 24 }}>
|
||||
一键生成
|
||||
</Title>
|
||||
<PageHead title="一键生成" />
|
||||
|
||||
<Steps
|
||||
current={currentStep}
|
||||
|
||||
@@ -6,7 +6,6 @@ import React, { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Table,
|
||||
Tag,
|
||||
Empty,
|
||||
@@ -25,8 +24,7 @@ import {
|
||||
} from "@ant-design/icons";
|
||||
import { getUserTasks, retryTask, type TaskItem } from "@/api/tasks";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
|
||||
const { Title } = Typography;
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
|
||||
/** 任务状态标签 */
|
||||
const StatusTag: React.FC<{ status: string }> = ({ status }) => {
|
||||
@@ -152,33 +150,24 @@ const TaskHistory: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 24,
|
||||
flexWrap: "wrap",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
任务历史
|
||||
</Title>
|
||||
<Select
|
||||
placeholder="按状态筛选"
|
||||
value={statusFilter || undefined}
|
||||
onChange={setStatusFilter}
|
||||
allowClear
|
||||
style={{ width: 150 }}
|
||||
options={[
|
||||
{ value: "pending", label: "等待中" },
|
||||
{ value: "processing", label: "处理中" },
|
||||
{ value: "completed", label: "已完成" },
|
||||
{ value: "failed", label: "失败" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<PageHead
|
||||
title="任务历史"
|
||||
actions={
|
||||
<Select
|
||||
placeholder="按状态筛选"
|
||||
value={statusFilter || undefined}
|
||||
onChange={setStatusFilter}
|
||||
allowClear
|
||||
style={{ width: 150 }}
|
||||
options={[
|
||||
{ value: "pending", label: "等待中" },
|
||||
{ value: "processing", label: "处理中" },
|
||||
{ value: "completed", label: "已完成" },
|
||||
{ value: "failed", label: "失败" },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: "center", padding: 60 }}>
|
||||
|
||||
@@ -32,8 +32,9 @@ import {
|
||||
getProductDownloadUrl,
|
||||
type ProductItem,
|
||||
} from "@/api/products";
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Text } = Typography;
|
||||
|
||||
/** 状态标签 */
|
||||
const StatusTag: React.FC<{ status: string }> = ({ status }) => {
|
||||
@@ -84,9 +85,7 @@ const ProductLibrary: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
|
||||
<Title level={3} style={{ marginBottom: 24 }}>
|
||||
成品库
|
||||
</Title>
|
||||
<PageHead title="成品库" />
|
||||
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: "center", padding: 60 }}>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import React, { useState } from "react";
|
||||
import { message } from "antd";
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
import "./Plans.css";
|
||||
|
||||
const PLANS = [
|
||||
@@ -76,10 +77,10 @@ const Plans: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="xx-plans-page">
|
||||
<div className="xx-page-head">
|
||||
<h2>选择适合您的方案</h2>
|
||||
<p>灵活定价,满足不同规模的创作需求</p>
|
||||
</div>
|
||||
<PageHead
|
||||
title="选择适合您的方案"
|
||||
description="灵活定价,满足不同规模的创作需求"
|
||||
/>
|
||||
|
||||
<div className="xx-plans-grid">
|
||||
{PLANS.map((plan) => (
|
||||
|
||||
@@ -31,8 +31,9 @@ import {
|
||||
toggleFavoriteTemplate,
|
||||
type TemplateItem,
|
||||
} from "@/api/templates";
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Paragraph } = Typography;
|
||||
|
||||
const TemplateLibrary: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -78,9 +79,7 @@ const TemplateLibrary: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
|
||||
<Title level={3} style={{ marginBottom: 24 }}>
|
||||
模板库
|
||||
</Title>
|
||||
<PageHead title="模板库" />
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
|
||||
@@ -6,7 +6,6 @@ import React, { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
Space,
|
||||
Table,
|
||||
Empty,
|
||||
@@ -33,8 +32,8 @@ import {
|
||||
type TitleItem,
|
||||
} from "@/api/titles";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
|
||||
const { Title } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
const TitleLibrary: React.FC = () => {
|
||||
@@ -203,31 +202,22 @@ const TitleLibrary: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 24,
|
||||
flexWrap: "wrap",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
标题库
|
||||
</Title>
|
||||
<Space wrap>
|
||||
<Button
|
||||
icon={<ImportOutlined />}
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
>
|
||||
批量导入
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建标题
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<PageHead
|
||||
title="标题库"
|
||||
actions={
|
||||
<Space wrap>
|
||||
<Button
|
||||
icon={<ImportOutlined />}
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
>
|
||||
批量导入
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建标题
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 搜索 */}
|
||||
<Input
|
||||
|
||||
@@ -34,8 +34,9 @@ import {
|
||||
type VoiceItem,
|
||||
} from "@/api/voices";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import PageHead from "@/components/layout/PageHead";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Text } = Typography;
|
||||
|
||||
/** 配音状态标签 */
|
||||
const StatusTag: React.FC<{ status?: string }> = ({ status }) => {
|
||||
@@ -230,28 +231,19 @@ const VoiceLibrary: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", maxWidth: 1200, margin: "0 auto" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 24,
|
||||
flexWrap: "wrap",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Title level={3} style={{ margin: 0 }}>
|
||||
配音库
|
||||
</Title>
|
||||
<Space wrap>
|
||||
<Button icon={<RobotOutlined />} onClick={() => setAiModalOpen(true)}>
|
||||
AI 生成
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建配音
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<PageHead
|
||||
title="配音库"
|
||||
actions={
|
||||
<Space wrap>
|
||||
<Button icon={<RobotOutlined />} onClick={() => setAiModalOpen(true)}>
|
||||
AI 生成
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新建配音
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: "center", padding: 60 }}>
|
||||
|
||||
Reference in New Issue
Block a user