Files
xiaoxia-saas/docs/frontend-v21-ui-development-plan.md
灵应 bbb4878f58
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
docs: 添加 V21 UI 开发文档和任务拆解文档
2026-06-30 22:17:39 +08:00

2917 lines
79 KiB
Markdown
Raw Permalink 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.
---
AIGC:
Label: "1"
ContentProducer: 001191110102MACQD9K64018705
ProduceID: 15868733686388_0/project_7655981463858544923-files/docs/frontend-v21-ui-development-plan.md
ReservedCode1: ""
ContentPropagator: 001191110102MACQD9K64028705
PropagateID: 15868733686388#1782829027273
ReservedCode2: ""
---
# V21 UI 前端开发指令文档
> 版本:V21 UI 1:1 还原开发指南
> 更新时间:2024年
> 技术栈:React 18 + TypeScript + Ant Design 5 + Tailwind CSS + React Query
---
## 目录
1. [全局设计系统规范](#1-全局设计系统规范)
2. [全局布局规范](#2-全局布局规范)
3. [逐页开发指令](#3-逐页开发指令)
4. [新增页面开发指令](#4-新增页面开发指令)
5. [基础组件封装规范](#5-基础组件封装规范)
6. [执行顺序建议](#6-执行顺序建议)
---
## 1. 全局设计系统规范
### 1.1 CSS 变量定义(global.css
从原型中提取的完整 CSS 变量系统:
```css
/* ========== global.css ========== */
:root {
/* ==================== 颜色系统 ==================== */
/* 主色调 */
--primary: #4f46e5; /* 主色 */
--primary-dark: #4338ca; /* 主色深 */
--primary-soft: #eef2ff; /* 主色浅(背景) */
--primary-light: #818cf8; /* 主色亮 */
/* 中性色 */
--slate: #0f172a; /* 标题/重要文字 */
--muted: #64748b; /* 次要文字/占位符 */
--line: #e2e8f0; /* 边框/分隔线 */
--bg: #f8fafc; /* 页面背景 */
/* 功能色 */
--green: #10b981; /* 成功/通过 */
--amber: #f59e0b; /* 警告/进行中 */
--red: #ef4444; /* 错误/高风险 */
/* ==================== 圆角系统 ==================== */
--radius-sm: 14px; /* 小圆角(按钮、内边距) */
--radius-md: 18px; /* 中圆角(卡片、模态框) */
--radius-lg: 24px; /* 大圆角(容器) */
--radius-xl: 28px; /* 超大圆角(主内容卡片) */
/* ==================== 阴影系统 ==================== */
--shadow-sm: 0 10px 30px rgba(15, 23, 42, 0.06); /* 小阴影 */
--shadow-md: 0 24px 70px rgba(15, 23, 42, 0.09); /* 中阴影(卡片默认) */
/* ==================== 间距系统(已对齐) ==================== */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 12px;
--space-lg: 16px;
--space-xl: 20px;
--space-2xl: 24px;
--space-3xl: 32px;
/* ==================== 布局尺寸 ==================== */
--sidebar-width: 240px;
--header-height: 68px;
--max-content-width: 1440px;
/* ==================== 字体系统 ==================== */
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif;
--font-size-xs: 11px;
--font-size-sm: 12px;
--font-size-base: 14px;
--font-size-md: 15px;
--font-size-lg: 16px;
--font-size-xl: 18px;
--font-size-2xl: 22px;
--font-size-3xl: 26px;
--font-size-4xl: 28px;
--font-size-hero: 44px;
/* ==================== 动效 ==================== */
--transition-fast: 0.15s ease;
--transition-base: 0.18s ease;
--transition-slow: 0.3s ease;
}
```
### 1.2 颜色使用规范
| 颜色名 | 色值 | 使用场景 |
|--------|------|----------|
| `--primary` | #4f46e5 | 主按钮、选中状态、强调文字 |
| `--primary-soft` | #eef2ff | 选中/激活背景、标签背景 |
| `--primary-light` | #818cf8 | 图标、渐变色 |
| `--slate` | #0f172a | 标题、主要文字 |
| `--muted` | #64748b | 次要文字、时间戳 |
| `--line` | #e2e8f0 | 边框、分隔线 |
| `--bg` | #f8fafc | 页面背景 |
| `--green` | #10b981 | 成功、通过、可发布 |
| `--amber` | #f59e0b | 警告、进行中、待复核 |
| `--red` | #ef4444 | 错误、高风险、删除 |
### 1.3 标签(Tag/Pill)样式规范
```css
/* 标签通用样式 */
.tag {
display: inline-flex;
padding: 6px 12px;
border-radius: 999px;
border: 1px solid #c7d2fe;
background: #eef2ff;
color: #4338ca;
font-weight: 700;
font-size: 13px;
}
/* 状态胶囊样式 */
.pill {
font-size: 11px;
padding: 4px 8px;
border-radius: 999px;
font-weight: 700;
display: inline-block;
}
.pill.ok { /* 成功/已通过 */
background: #dcfce7;
color: #166534;
}
.pill.warn { /* 警告/待复核 */
background: #ffedd5;
color: #c2410c;
}
.pill.bad { /* 错误/高风险 */
background: #ffe4e6;
color: #be123c;
}
.pill.info { /* 信息/视频类型 */
background: #dbeafe;
color: #1d4ed8;
}
.pill.muted { /* 禁用/品牌类 */
background: #f1f5f9;
color: #64748b;
}
```
### 1.4 按钮样式规范
```css
/* 按钮基础样式 */
.btn {
border: 0;
border-radius: var(--radius-sm); /* 14px */
padding: 11px 18px;
font-weight: 700;
cursor: pointer;
transition: var(--transition-base);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
font-size: var(--font-size-base);
}
/* 主要按钮(渐变) */
.btn.primary {
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: #fff;
box-shadow: 0 14px 26px rgba(79, 70, 229, 0.22);
}
.btn.primary:hover {
transform: translateY(-2px);
box-shadow: 0 18px 34px rgba(79, 70, 229, 0.28);
}
/* 次要按钮(幽灵) */
.btn.ghost {
background: #fff;
border: 1px solid var(--line);
color: #475569;
}
.btn.ghost:hover {
border-color: #c7d2fe;
color: #4338ca;
background: #f8faff;
}
/* 小按钮 */
.btn.sm {
padding: 8px 14px;
font-size: var(--font-size-sm);
}
/* 激活态 */
.btn:active {
transform: translateY(0);
}
/* 禁用态 */
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
```
### 1.5 卡片样式规范
```css
/* 通用卡片 */
.card {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
border-radius: var(--radius-xl); /* 28px */
box-shadow: var(--shadow-md);
padding: 28px;
}
/* 网格卡片(Hover效果) */
.card-hover {
transition: var(--transition-base);
}
.card-hover:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-sm);
}
```
---
## 2. 全局布局规范
### 2.1 主应用布局(AppLayout
```
┌─────────────────────────────────────────────────────────────────┐
│ Header (height: 68px, sticky) │
├──────────────┬──────────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Main Content │
│ (240px) │ (flex: 1) │
│ │ │
│ - Logo │ ┌─────────────────────────────────────────┐ │
│ - Nav │ │ Page Head (flex space-between) │ │
│ - Divider │ │ - Title + Description │ │
│ - Bottom │ │ - Action Buttons │ │
│ │ └─────────────────────────────────────────┘ │
│ │ │
│ │ ┌─────────────────────────────────────────┐ │
│ │ │ Content Area │ │
│ │ │ (根据页面类型不同布局) │ │
│ │ └─────────────────────────────────────────┘ │
│ │ │
└──────────────┴──────────────────────────────────────────────────┘
```
**布局实现代码:**
```tsx
// components/layout/AppLayout.tsx
import React from 'react';
import { Outlet } from 'react-router-dom';
import Sidebar from './Sidebar';
import Header from './Header';
const AppLayout: React.FC = () => {
return (
<div style={{ minHeight: '100vh' }}>
{/* 全局背景 */}
<div className="bg" />
<div className="dots" />
<Header />
<div className="app-layout" style={{
display: 'grid',
gridTemplateColumns: '240px 1fr',
gap: '24px',
maxWidth: '1440px',
margin: '0 auto',
padding: '0 24px 60px'
}}>
<Sidebar />
<main className="main-content">
<Outlet />
</main>
</div>
</div>
);
};
export default AppLayout;
```
### 2.2 侧边栏导航结构(Sidebar
```tsx
// components/layout/Sidebar.tsx
const navItems = [
{ icon: '📊', label: '控制台', path: '/dashboard' },
{ icon: '📝', label: '标题库', path: '/titles' },
{ icon: '📦', label: '素材库', path: '/assets' },
{ icon: '🎙️', label: '配音库', path: '/voices' },
{ icon: '🎨', label: '模板库', path: '/templates' },
];
const navItems2 = [
{ icon: '✨', label: '一键生成', path: '/generate', highlight: true },
{ icon: '🎬', label: '成片库', path: '/products' },
{ icon: '📋', label: '任务历史', path: '/history' },
{ icon: '🔍', label: '查重检测', path: '/duplication' },
{ icon: '✂️', label: '剪辑计划', path: '/editing-planner' },
{ icon: '🎤', label: '我的音色', path: '/voice-clone' },
{ icon: '🔑', label: '账号管理', path: '/accounts' },
];
const bottomItems = [
{ icon: '💎', label: '订阅管理', path: '/subscription' },
];
```
**侧边栏样式(来自原型):**
```tsx
// Sidebar CSS
const sidebarStyle = {
background: 'rgba(255, 255, 255, 0.88)',
backdropFilter: 'blur(18px)',
borderRight: '1px solid rgba(226, 232, 240, 0.9)',
padding: '24px 16px',
position: 'sticky' as const,
top: '68px',
height: 'calc(100vh - 68px)',
overflowY: 'auto' as const,
};
const sidebarItemStyle = {
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '12px 14px',
borderRadius: 'var(--radius-sm)',
color: 'var(--muted)',
cursor: 'pointer',
fontWeight: 600,
fontSize: '14px',
transition: '0.15s ease',
};
const sidebarItemActiveStyle = {
...sidebarItemStyle,
background: 'var(--primary-soft)',
color: 'var(--primary)',
fontWeight: 700,
};
const iconStyle = {
width: '36px',
height: '36px',
borderRadius: '10px',
display: 'grid',
placeItems: 'center',
fontSize: '18px',
background: '#f1f5f9',
};
const iconActiveStyle = {
...iconStyle,
background: 'var(--primary)',
color: '#fff',
};
```
### 2.3 页面头部(PageHead
```tsx
// components/common/PageHead.tsx
interface PageHeadProps {
title: string;
description?: string;
tag?: string;
actions?: React.ReactNode;
}
const PageHead: React.FC<PageHeadProps> = ({
title,
description,
tag,
actions
}) => {
return (
<div className="page-head" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '18px',
marginBottom: '22px',
paddingTop: '32px',
}}>
<div>
{tag && <span className="tag">{tag}</span>}
<h2 style={{
fontSize: '26px',
margin: tag ? '6px 0 0' : '0 0 6px',
letterSpacing: '-0.02em',
}}>{title}</h2>
{description && (
<p style={{ margin: '4px 0 0', color: 'var(--muted)', lineHeight: 1.7 }}>
{description}
</p>
)}
</div>
{actions && <div style={{ display: 'flex', gap: '10px' }}>{actions}</div>}
</div>
);
};
```
### 2.4 页面容器(Wrap
```tsx
// 全局页面包装器
const Wrap: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div style={{
maxWidth: '1440px',
margin: '0 auto',
padding: '0 24px 60px',
}}>
{children}
</div>
);
```
---
## 3. 逐页开发指令
### 3.1 控制台 (Dashboard)
**文件路径:** `pages/dashboard/Dashboard.tsx`
**路由:** `/dashboard`
**当前状态:**
- ✅ API 调用逻辑已实现
- ✅ 统计数据获取逻辑已实现
- ⚠️ KPI 卡片使用 Ant Design 样式
- ⚠️ 最近任务使用 Table 布局,需改为卡片式
- ⚠️ 缺少快速入口卡片网格(4列布局)
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| KPI 卡片样式 | Ant Design Card | 渐变背景 + 大号数字 + 趋势 | P2 |
| 最近任务布局 | Table | 卡片式列表 | P2 |
| 快速入口网格 | 无 | 4列 feature-grid | P2 |
| 页面头部欢迎语 | - | 带用户名的欢迎语 + 日期 | P2 |
**具体修改指令:**
#### 3.1.1 KPI 卡片网格
**改什么:** 将现有的 KPI 卡片改为原型设计的渐变背景卡片
**改成什么样子:**
```
┌────────────┬────────────┬────────────┬────────────┐
│ 12 │ 486 │ 156 │ 2.4GB │
│ 项目总数 │ 素材总数 │ 本月生成数 │ 存储空间 │
│ ↑2本月新增 │ ↑38本月上传 │ ↑23% vs上月│ 已用24% │
└────────────┴────────────┴────────────┴────────────┘
```
**使用 Ant Design/Tailwind 组件:**
```tsx
// 使用 Tailwind + 自定义样式
<div className="kpi-grid" style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '16px',
marginBottom: '24px',
}}>
{kpiData.map((item) => (
<div
key={item.key}
className="kpi"
style={{
background: 'linear-gradient(180deg, #fff, #f8fafc)',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-lg)',
padding: '20px',
}}
>
<div style={{
fontSize: '30px',
fontWeight: 800,
color: 'var(--slate)',
}}>{item.value}</div>
<div style={{
color: 'var(--muted)',
fontSize: '13px',
marginTop: '4px',
}}>{item.label}</div>
<div style={{
fontSize: '12px',
marginTop: '8px',
color: item.trend.includes('↑') ? 'var(--green)' : 'var(--muted)',
}}>{item.trend}</div>
</div>
))}
</div>
```
#### 3.1.2 最近任务卡片列表
**改什么:** 将 Table 布局改为卡片式列表
**改成什么样子:**
```tsx
<div className="task-list" style={{
display: 'flex',
flexDirection: 'column',
gap: '10px',
}}>
{recentTasks.map((task) => (
<div
key={task.id}
className="task-item"
style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-md)',
padding: '16px',
display: 'grid',
gridTemplateColumns: '1fr auto auto auto',
gap: '16px',
alignItems: 'center',
}}
>
{/* 任务信息 */}
<div className="task-info">
<h4 style={{ margin: '0 0 4px', fontSize: '14px', fontWeight: 600 }}>
{task.name}
</h4>
<div style={{ fontSize: '12px', color: 'var(--muted)' }}>
{task.type} · 模板:{task.template}
</div>
</div>
{/* 状态标签 */}
<StatusBadge status={task.status} />
{/* 时间 */}
<div style={{ textAlign: 'right', fontSize: '12px', color: 'var(--muted)' }}>
<span style={{ display: 'block', marginBottom: '2px' }}>{task.date}</span>
{task.status === 'completed' ? `耗时 ${task.duration}` : '进行中'}
</div>
{/* 操作按钮 */}
<button className="btn ghost sm">查看</button>
</div>
))}
</div>
```
#### 3.1.3 快速入口卡片网格
**改什么:** 新增快速入口区域,使用 4 列 feature-grid
**改成什么样子:**
```tsx
<div className="section">
<div className="section-title">
<h3>快速入口</h3>
</div>
<div className="feature-grid" style={{ margin: 0 }}>
{quickEntries.map((entry) => (
<div
key={entry.id}
className="feature"
style={{ cursor: 'pointer' }}
onClick={() => navigate(entry.path)}
>
<div
className="icon"
style={{
background: entry.iconGradient,
marginBottom: '14px',
}}
>
{entry.icon}
</div>
<h3 style={{ margin: '0 0 8px', fontSize: '16px' }}>{entry.title}</h3>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '13px' }}>
{entry.description}
</p>
</div>
))}
</div>
</div>
```
**新增数据:**
```tsx
const quickEntries = [
{ id: 'titles', icon: '📝', iconGradient: 'linear-gradient(135deg, #6366f1, #4f46e5)', title: '标题库', description: '24条标题 · 5个分类', path: '/titles' },
{ id: 'assets', icon: '📦', iconGradient: 'linear-gradient(135deg, #0ea5e9, #0284c7)', title: '素材库', description: '486个素材 · 3个素材库', path: '/assets' },
{ id: 'generate', icon: '✨', iconGradient: 'linear-gradient(135deg, #10b981, #059669)', title: '一键生成', description: '开始创作新视频', path: '/generate' },
{ id: 'products', icon: '🎬', iconGradient: 'linear-gradient(135deg, #f59e0b, #d97706)', title: '成片库', description: '89个成片 · 3个待复核', path: '/products' },
];
```
**优先级:** P2
---
### 3.2 一键生成 (Generate)
**文件路径:** `pages/generate/GeneratePage.tsx`
**路由:** `/generate`
**当前状态:**
- ✅ API 调用逻辑已实现
- ❌ 缺失克隆声音展开区域
- ❌ 缺失时间线预览区域
- ❌ 缺失视频预览区域
- ❌ 缺失重新生成按钮
- ⚠️ 步骤条样式需调整
- ⚠️ 表单卡片式选择需调整
- ⚠️ 左右分栏布局需调整
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| 克隆声音展开区 | 无 | 展开/收起音频上传区 | P1 |
| 时间线预览 | 无 | 右侧 sticky 预览 | P1 |
| 视频预览区 | 无 | 9:16 竖版预览 | P1 |
| 重新生成按钮 | 无 | 预览区左下角 | P1 |
| 步骤条 | - | 居中 4 步骤 | P2 |
| 选择卡片 | - | 选中带对勾图标 | P2 |
| 左右分栏 | - | 1fr:320px | P2 |
**具体修改指令:**
#### 3.2.1 整体布局改造
**改什么:** 改为左右分栏布局(1fr : 320px)
```tsx
<div className="generate-layout" style={{
display: 'grid',
gridTemplateColumns: '1fr 320px',
gap: '24px',
}}>
{/* 左侧表单区 */}
<div className="generate-form">{/* ... */}</div>
{/* 右侧预览区 */}
<div className="generate-preview">{/* ... */}</div>
</div>
```
#### 3.2.2 步骤条组件
**改什么:** 实现居中 4 步骤条
```tsx
<div className="steps-bar" style={{
display: 'flex',
justifyContent: 'center',
gap: '8px',
marginBottom: '28px',
}}>
{[
{ num: 1, label: '选择模板', active: true },
{ num: 2, label: '选择素材', active: false },
{ num: 3, label: '选择标题', active: false },
{ num: 4, label: '选择配音', active: false },
].map((step, idx) => (
<React.Fragment key={step.num}>
{idx > 0 && <span style={{ color: '#cbd5e1' }}></span>}
<div
className={`step-item ${step.active ? 'active' : ''}`}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '10px 16px',
borderRadius: 'var(--radius-sm)',
color: step.active ? 'var(--primary)' : 'var(--muted)',
fontWeight: 600,
fontSize: '14px',
background: step.active ? 'var(--primary-soft)' : 'transparent',
}}
>
<div style={{
width: '26px',
height: '26px',
borderRadius: '50%',
background: step.active ? 'var(--primary)' : '#f1f5f9',
color: step.active ? '#fff' : 'inherit',
display: 'grid',
placeItems: 'center',
fontSize: '12px',
fontWeight: 700,
}}>{step.num}</div>
<span>{step.label}</span>
</div>
</React.Fragment>
))}
</div>
```
#### 3.2.3 选择卡片组件
**改什么:** 模板/配音选择使用卡片式,带选中状态和勾选图标
```tsx
<div className="choice-list" style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '12px',
}}>
{options.map((opt) => (
<div
key={opt.id}
className={`choice-item ${selectedId === opt.id ? 'selected' : ''}`}
onClick={() => setSelectedId(opt.id)}
style={{
background: '#fff',
border: `2px solid ${selectedId === opt.id ? 'var(--primary)' : 'var(--line)'}`,
borderRadius: 'var(--radius-md)',
padding: '14px',
cursor: 'pointer',
position: 'relative',
textAlign: 'center',
transition: 'var(--transition-base)',
}}
>
{/* 选中对勾 */}
{selectedId === opt.id && (
<div style={{
position: 'absolute',
right: '-6px',
top: '-6px',
width: '22px',
height: '22px',
borderRadius: '50%',
background: 'var(--primary)',
color: '#fff',
display: 'grid',
placeItems: 'center',
fontSize: '12px',
}}></div>
)}
{/* 缩略图 */}
<div style={{
height: '60px',
borderRadius: 'var(--radius-sm)',
display: 'grid',
placeItems: 'center',
color: '#fff',
fontSize: '24px',
marginBottom: '10px',
background: opt.gradient,
}}>{opt.thumb}</div>
<h4 style={{ margin: '0 0 4px', fontSize: '13px', fontWeight: 600 }}>
{opt.title}
</h4>
<p style={{ margin: 0, fontSize: '11px', color: 'var(--muted)' }}>
{opt.desc}
</p>
</div>
))}
</div>
```
#### 3.2.4 克隆声音展开区域
**改什么:** 在选择配音的第4步,点击"克隆我的声音"选项后展开音频上传区域
```tsx
{/* 克隆声音选项被选中时显示 */}
{selectedVoice === 'clone' && (
<div id="clone-voice-expand" style={{
marginTop: '16px',
padding: '20px',
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-md)',
}}>
<h4 style={{ margin: '0 0 12px', fontSize: '14px' }}>🎤 克隆我的声音</h4>
<p style={{ margin: '0 0 12px', fontSize: '12px', color: 'var(--muted)' }}>
上传你的声音,AI将用你的音色生成配音
</p>
{/* 上传区域 */}
<div style={{
border: '2px dashed var(--line)',
borderRadius: 'var(--radius-sm)',
padding: '30px',
textAlign: 'center',
background: '#f8fafc',
cursor: 'pointer',
}}>
<div style={{ fontSize: '28px', marginBottom: '8px' }}>🎵</div>
<p style={{ margin: 0, fontSize: '13px' }}>
拖拽或点击上传音频文件(MP3/WAV10秒以上)
</p>
<p style={{ margin: '8px 0 0', fontSize: '11px', color: 'var(--muted)' }}>
支持格式:MP3WAV · 建议时长:10~3分钟
</p>
</div>
{/* 录音按钮 */}
<div style={{ display: 'flex', gap: '10px', marginTop: '12px' }}>
<button className="btn ghost" style={{ flex: 1 }}>🎙️ 直接录制</button>
</div>
{/* 提示 */}
<p style={{
margin: '12px 0 0',
fontSize: '11px',
color: 'var(--muted)',
background: '#f8fafc',
padding: '8px 12px',
borderRadius: '8px',
}}>
💡 建议上传10~3分钟的清晰语音,环境安静、语速均匀效果最佳
</p>
{/* 已克隆音色选择 */}
<div style={{ marginTop: '16px' }}>
<p style={{ margin: '0 0 8px', fontSize: '12px', fontWeight: 600, color: 'var(--muted)' }}>
或选择已克隆的音色:
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{clonedVoices.map((voice) => (
<VoiceSelectItem key={voice.id} voice={voice} />
))}
</div>
</div>
{/* 开始克隆按钮 */}
<button className="btn primary" style={{ width: '100%', marginTop: '16px' }}>
🎤 开始克隆
</button>
</div>
)}
```
#### 3.2.5 右侧预览区域
**改什么:** 实现 sticky 定位的视频预览、时间线预览
```tsx
<div className="generate-preview" style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-md)',
padding: '20px',
position: 'sticky',
top: '92px',
}}>
{/* 视频预览 */}
<div style={{
aspectRatio: '9/16',
maxHeight: '400px',
borderRadius: 'var(--radius-md)',
background: 'linear-gradient(135deg, #111827, #312e81)',
display: 'grid',
placeItems: 'center',
color: '#fff',
fontSize: '36px',
position: 'relative',
overflow: 'hidden',
marginBottom: '16px',
}}>
{/* 渐变叠加 */}
<div style={{
position: 'absolute',
inset: 0,
background: 'radial-gradient(circle at 72% 28%, rgba(255,255,255,0.2), transparent 40%)',
}} />
{/* 播放按钮 */}
<div style={{
position: 'relative',
zIndex: 1,
width: '60px',
height: '60px',
borderRadius: '50%',
background: 'rgba(255,255,255,0.2)',
display: 'grid',
placeItems: 'center',
backdropFilter: 'blur(8px)',
}}></div>
</div>
{/* 标题预览 */}
<div style={{
background: 'rgba(255,255,255,0.95)',
color: 'var(--slate)',
borderRadius: '10px',
padding: '10px',
textAlign: 'center',
fontWeight: 600,
fontSize: '14px',
marginBottom: '14px',
}}>
{selectedTitle}
</div>
{/* 剪辑计划标题 */}
<div style={{ fontWeight: 600, marginBottom: '10px' }}>剪辑计划预览</div>
{/* 时间线 */}
<div className="preview-timeline" style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{timeline.map((item, idx) => (
<div key={idx} style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px',
background: '#f8fafc',
borderRadius: '10px',
fontSize: '13px',
}}>
<div style={{
width: '24px',
height: '24px',
borderRadius: '8px',
background: 'var(--primary-soft)',
color: 'var(--primary)',
display: 'grid',
placeItems: 'center',
fontWeight: 700,
fontSize: '11px',
}}>{idx + 1}</div>
<span>{item.scene}</span>
<span style={{ color: 'var(--muted)', marginLeft: 'auto', fontSize: '12px' }}>
{item.time}
</span>
</div>
))}
</div>
{/* 操作按钮 */}
<div style={{ display: 'flex', gap: '10px', marginTop: '16px' }}>
<button className="btn ghost">重新生成</button>
<button className="btn primary" style={{ flex: 1 }}> 确认生成</button>
</div>
</div>
```
**优先级:** P1
---
### 3.3 标题库 (Title Library)
**文件路径:** `pages/titles/TitleLibrary.tsx`
**路由:** `/titles`
**当前状态:**
- ✅ CRUD + 批量导入已实现
- ❌ 缺失左侧分类列表(需两栏布局)
- ❌ 缺失标题卡片网格布局
- ❌ 缺失收藏功能(★/☆切换)
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| 左侧分类列表 | 无 | 两栏布局左侧 | P1 |
| 标题卡片网格 | Table | 3列卡片网格 | P1 |
| 收藏功能 | 无 | ★/☆切换 | P1 |
| 页面头部 | - | 批量导入 + 新增按钮 | P2 |
**具体修改指令:**
#### 3.3.1 两栏布局
```tsx
<div className="titles-layout" style={{
display: 'grid',
gridTemplateColumns: '220px 1fr',
gap: '20px',
}}>
{/* 左侧分类列表 */}
<div className="category-list" style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}>
{categories.map((cat) => (
<div
key={cat.id}
className={`category-item ${selectedCategory === cat.id ? 'active' : ''}`}
onClick={() => setSelectedCategory(cat.id)}
style={{
border: `1px solid ${selectedCategory === cat.id ? 'var(--primary)' : 'var(--line)'}`,
background: selectedCategory === cat.id ? 'var(--primary-soft)' : '#fff',
borderRadius: 'var(--radius-sm)',
padding: '14px',
cursor: 'pointer',
transition: '0.15s ease',
}}
>
<h4 style={{ margin: '0 0 4px', fontSize: '14px' }}>{cat.name}</h4>
<span style={{ fontSize: '12px', color: 'var(--muted)' }}>{cat.count} </span>
</div>
))}
</div>
{/* 右侧标题卡片网格 */}
<div className="titles-grid" style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '14px',
}}>
{titles.map((title) => (
<TitleCard key={title.id} title={title} />
))}
</div>
</div>
```
#### 3.3.2 标题卡片组件
```tsx
interface TitleCardProps {
title: {
id: string;
content: string;
category: string;
usageCount: number;
isFavorited: boolean;
};
}
const TitleCard: React.FC<TitleCardProps> = ({ title }) => {
const [isFavorited, setIsFavorited] = useState(title.isFavorited);
return (
<div
className="title-card"
style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-md)',
padding: '16px',
cursor: 'pointer',
transition: 'var(--transition-base)',
}}
>
<div style={{
fontSize: '14px',
fontWeight: 600,
marginBottom: '10px',
lineHeight: 1.5,
}}>
{title.content}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<span className="pill info" style={{ marginRight: '6px' }}>
{title.category}
</span>
<span style={{ fontSize: '12px', color: 'var(--muted)' }}>
使用 {title.usageCount}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<button className="btn ghost sm">✏️ 编辑</button>
<button className="btn ghost sm" style={{ color: 'var(--red)' }}>🗑️</button>
<span
onClick={(e) => {
e.stopPropagation();
setIsFavorited(!isFavorited);
}}
style={{
color: isFavorited ? 'var(--amber)' : 'var(--muted)',
fontSize: '16px',
cursor: 'pointer',
}}
>
{isFavorited ? '★' : '☆'}
</span>
</div>
</div>
</div>
);
};
```
**优先级:** P1
---
### 3.4 素材库 (Asset Library)
**文件路径:** `pages/assets/AssetLibrary.tsx`
**路由:** `/assets`
**当前状态:**
- ✅ 上传功能、素材库管理已实现
- ❌ 缺失左侧素材库列表(需两栏布局)
- ⚠️ 素材卡片样式、状态标签、诊断按钮需对齐
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| 左侧素材库列表 | 无 | 260px 宽列表 | P1 |
| 素材卡片样式 | - | 9:16 竖版缩略图 | P2 |
| 状态标签 | - | ok/warn/bad/info | P2 |
| 诊断按钮 | - | 卡片底部 | P2 |
**具体修改指令:**
#### 3.4.1 两栏布局
```tsx
<div className="assets-layout" style={{
display: 'grid',
gridTemplateColumns: '260px 1fr',
gap: '20px',
}}>
{/* 左侧素材库列表 */}
<div className="asset-library-list">
{assetLibraries.map((lib) => (
<div
key={lib.id}
className={`asset-library-item ${selectedLib === lib.id ? 'active' : ''}`}
style={{
border: `1px solid ${selectedLib === lib.id ? 'var(--primary)' : 'var(--line)'}`,
background: selectedLib === lib.id ? 'var(--primary-soft)' : '#fff',
borderRadius: 'var(--radius-sm)',
padding: '14px',
cursor: 'pointer',
}}
>
<h4 style={{ margin: '0 0 4px', fontSize: '14px' }}>{lib.name}</h4>
<span style={{ fontSize: '12px', color: 'var(--muted)' }}>
{lib.count} 个素材 · {lib.size}
</span>
</div>
))}
</div>
{/* 右侧素材网格 */}
<div className="asset-grid" style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '14px',
}}>
{assets.map((asset) => (
<AssetCard key={asset.id} asset={asset} />
))}
</div>
</div>
```
#### 3.4.2 素材卡片组件
```tsx
const AssetCard: React.FC<{ asset: Asset }> = ({ asset }) => (
<div
className="asset-card"
style={{
border: '1px solid var(--line)',
background: '#fff',
borderRadius: 'var(--radius-md)',
overflow: 'hidden',
cursor: 'pointer',
transition: 'var(--transition-base)',
}}
>
{/* 缩略图 */}
<div
className="asset-thumb"
style={{
aspectRatio: '9/16',
borderRadius: 'var(--radius-sm) var(--radius-sm) 0 0',
position: 'relative',
overflow: 'hidden',
display: 'grid',
placeItems: 'center',
color: '#fff',
background: asset.gradient,
}}
>
<div
className="play"
style={{
position: 'absolute',
width: '40px',
height: '40px',
borderRadius: '50%',
background: 'rgba(255,255,255,0.25)',
display: 'grid',
placeItems: 'center',
backdropFilter: 'blur(4px)',
}}
>
</div>
</div>
{/* 信息 */}
<div style={{ padding: '12px' }}>
<h4 style={{
margin: '0 0 6px',
fontSize: '13px',
fontWeight: 600,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
{asset.name}
</h4>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '11px', color: 'var(--muted)' }}>
<StatusPill status={asset.status} />
<span>{asset.duration}</span>
</div>
<button
className="btn ghost sm"
style={{ width: '100%', marginTop: '8px', padding: '6px', fontSize: '12px' }}
>
🔍 诊断
</button>
</div>
</div>
);
```
**优先级:** P1
---
### 3.5 配音库 (Voice Library)
**文件路径:** `pages/voices/VoiceLibrary.tsx`
**路由:** `/voices`
**当前状态:**
- ✅ AI生成、CRUD 功能已实现
- ❌ 缺失左侧配音库分类列表
- ❌ 缺失配音卡片布局(voice-card + voice-wave 波形)
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| 左侧分类列表 | 无 | 220px 宽 | P1 |
| 配音卡片 | 表格 | voice-card 布局 | P1 |
| 波形可视化 | 无 | 灰色波形条 | P1 |
| 页面头部 | - | 新建配音库+AI生成+上传 | P2 |
**具体修改指令:**
#### 3.5.1 配音库两栏布局
```tsx
<div className="voices-layout" style={{
display: 'grid',
gridTemplateColumns: '220px 1fr',
gap: '20px',
}}>
{/* 左侧配音库分类 */}
<div className="voice-library-list">
{voiceLibraries.map((lib) => (
<div
key={lib.id}
className={`asset-library-item ${selectedLib === lib.id ? 'active' : ''}`}
style={{ /* 同素材库样式 */ }}
>
<h4>{lib.name}</h4>
<span>{lib.count} 条配音</span>
</div>
))}
</div>
{/* 右侧配音卡片列表 */}
<div className="voice-list" style={{
display: 'flex',
flexDirection: 'column',
gap: '10px',
}}>
{voices.map((voice) => (
<VoiceCard key={voice.id} voice={voice} />
))}
</div>
</div>
```
#### 3.5.2 配音卡片组件
```tsx
const VoiceCard: React.FC<{ voice: Voice }> = ({ voice }) => (
<div
className="voice-card"
style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-md)',
padding: '16px',
display: 'grid',
gridTemplateColumns: '50px 1fr auto',
gap: '14px',
alignItems: 'center',
transition: 'var(--transition-base)',
}}
>
{/* 头像 */}
<div
style={{
width: '50px',
height: '50px',
borderRadius: '50%',
display: 'grid',
placeItems: 'center',
color: '#fff',
fontSize: '22px',
background: voice.gradient,
}}
>
🎙️
</div>
{/* 信息 */}
<div>
<h4 style={{ margin: '0 0 4px', fontSize: '14px', fontWeight: 600 }}>
{voice.name}
</h4>
<div style={{ fontSize: '12px', color: 'var(--muted)' }}>
{voice.library} · {voice.duration} · {voice.gender}
</div>
</div>
{/* 波形 */}
<div
style={{
height: '24px',
width: '120px',
borderRadius: '999px',
background: 'repeating-linear-gradient(90deg, #c7d2fe 0 5px, transparent 5px 10px)',
}}
/>
{/* 操作 */}
<div style={{ display: 'flex', gap: '8px' }}>
<button className="btn ghost sm"> 试听</button>
</div>
</div>
);
```
**优先级:** P1
---
### 3.6 成片库 (Product Library)
**文件路径:** `pages/products/ProductLibrary.tsx`
**路由:** `/products`
**当前状态:**
- ✅ 下载/预览/删除/查重率展示已实现
- ❌ 缺失批量选择功能
- ❌ 缺失批量发布/批量下载按钮
- ❌ 缺失已发布状态角标
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| 批量选择 | 无 | 复选框 + 全选 | P1 |
| 批量操作按钮 | 无 | 发布 + 下载 | P1 |
| 已发布角标 | 无 | 右上角绿色角标 | P1 |
| 选中效果 | 无 | 边框高亮 + 阴影 | P1 |
**具体修改指令:**
#### 3.6.1 页面头部批量操作
```tsx
<div className="page-head">
<div>
<h2>成片库</h2>
<p>查看和管理生成的视频成品</p>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
{/* 全选复选框 */}
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontSize: '14px', fontWeight: 600, color: 'var(--muted)' }}>
<input
type="checkbox"
checked={selectedAll}
onChange={handleSelectAll}
style={{ width: '16px', height: '16px', accentColor: 'var(--primary)' }}
/>
全选
</label>
{/* 已选计数 */}
<span style={{ fontSize: '13px', color: 'var(--muted)' }}>
已选 <span style={{ color: 'var(--primary)', fontWeight: 700 }}>{selectedCount}</span> 个视频
</span>
{/* 批量发布按钮 */}
<button
className="btn primary"
disabled={selectedCount === 0}
style={{
opacity: selectedCount === 0 ? 0.5 : 1,
cursor: selectedCount === 0 ? 'not-allowed' : 'pointer',
}}
>
📢 批量发布
</button>
{/* 批量下载 */}
<button className="btn ghost" disabled={selectedCount === 0}>
批量下载
</button>
</div>
</div>
```
#### 3.6.2 成片卡片(含批量选择)
```tsx
const ProductCard: React.FC<{ product: Product }> = ({ product }) => {
const [isSelected, setIsSelected] = useState(false);
return (
<div
className={`product-card ${isSelected ? 'selected' : ''}`}
style={{
background: '#fff',
border: `1px solid ${isSelected ? 'var(--primary)' : 'var(--line)'}`,
borderRadius: 'var(--radius-md)',
overflow: 'hidden',
cursor: 'pointer',
transition: 'var(--transition-base)',
boxShadow: isSelected ? '0 0 0 2px var(--primary-soft)' : 'none',
opacity: product.isPublished ? 0.65 : 1,
}}
>
{/* 选择复选框 */}
<div style={{ position: 'absolute', top: '12px', left: '12px', zIndex: 10 }}>
<input
type="checkbox"
checked={isSelected}
disabled={product.isPublished}
onChange={() => setIsSelected(!isSelected)}
style={{
width: '18px',
height: '18px',
accentColor: 'var(--primary)',
opacity: product.isPublished ? 0.4 : 1,
}}
/>
</div>
{/* 已发布角标 */}
{product.isPublished && (
<div style={{
position: 'absolute',
top: '12px',
right: '12px',
background: 'var(--green)',
color: '#fff',
fontSize: '11px',
fontWeight: 700,
padding: '3px 8px',
borderRadius: '6px',
zIndex: 10,
}}>
已发布
</div>
)}
{/* 缩略图 */}
<div
className="product-thumb"
style={{
aspectRatio: '9/16',
position: 'relative',
overflow: 'hidden',
display: 'grid',
placeItems: 'center',
color: '#fff',
background: product.gradient,
}}
>
<div className="play"></div>
<span className="duration">{product.duration}</span>
</div>
{/* 信息 */}
<div style={{ padding: '14px' }}>
<h4 style={{ margin: '0 0 8px', fontSize: '14px', fontWeight: 600 }}>
{product.name}
</h4>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<StatusPill status={product.status} />
<span style={{ fontSize: '12px', color: 'var(--muted)' }}>{product.date}</span>
</div>
<div style={{ display: 'flex', gap: '6px', marginTop: '8px' }}>
<button className="btn ghost sm" style={{ flex: 1, padding: '6px 4px', fontSize: '12px' }}>
📥 下载
</button>
{product.isPublished ? (
<span className="pill ok" style={{ flex: 1, padding: '6px 4px', fontSize: '12px', textAlign: 'center' }}>
已发布
</span>
) : (
<button className="btn primary sm" style={{ flex: 1, padding: '6px 4px', fontSize: '12px' }}>
📢 发布
</button>
)}
</div>
</div>
</div>
);
};
```
**优先级:** P1
---
### 3.7 任务历史 (Task History)
**文件路径:** `pages/history/TaskHistory.tsx`
**路由:** `/history`
**当前状态:**
- ✅ 状态筛选逻辑、重试功能已实现
- ⚠️ 状态筛选 Tab 样式需调整
- ⚠️ 任务列表需改为卡片布局
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| 状态 Tab | - | 圆角胶囊样式 | P2 |
| 任务列表 | Table | 卡片式布局 | P2 |
**具体修改指令:**
#### 3.7.1 Tab 切换样式
```tsx
<div className="tabs" style={{
display: 'flex',
gap: '6px',
marginBottom: '20px',
borderBottom: '1px solid var(--line)',
paddingBottom: '12px',
}}>
{[
{ key: 'all', label: '全部', count: 156 },
{ key: 'running', label: '进行中', count: 3 },
{ key: 'done', label: '已完成', count: 150 },
{ key: 'failed', label: '失败', count: 3 },
].map((tab) => (
<div
key={tab.key}
className={`tab ${activeTab === tab.key ? 'active' : ''}`}
onClick={() => setActiveTab(tab.key)}
style={{
padding: '8px 16px',
borderRadius: 'var(--radius-sm)',
cursor: 'pointer',
fontWeight: 600,
fontSize: '14px',
color: activeTab === tab.key ? 'var(--primary)' : 'var(--muted)',
background: activeTab === tab.key ? 'var(--primary-soft)' : 'transparent',
transition: '0.15s ease',
}}
>
{tab.label} ({tab.count})
</div>
))}
</div>
```
#### 3.7.2 任务卡片列表
```tsx
<div className="task-list" style={{
display: 'flex',
flexDirection: 'column',
gap: '10px',
}}>
{tasks.map((task) => (
<div
key={task.id}
className="task-item"
style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-md)',
padding: '16px',
display: 'grid',
gridTemplateColumns: '1fr auto auto auto',
gap: '16px',
alignItems: 'center',
transition: '0.15s ease',
}}
>
{/* 任务信息 */}
<div>
<h4 style={{ margin: '0 0 4px', fontSize: '14px', fontWeight: 600 }}>
{task.name}
</h4>
<div style={{ fontSize: '12px', color: 'var(--muted)' }}>
{task.type} · 模板:{task.template}
</div>
</div>
{/* 状态 */}
<StatusPill status={task.status} />
{/* 时间 */}
<div style={{ textAlign: 'right', fontSize: '12px', color: 'var(--muted)' }}>
<span style={{ display: 'block', marginBottom: '2px' }}>{task.date}</span>
{task.status === 'failed' ? '失败' : task.status === 'running' ? '进行中' : `耗时 ${task.duration}`}
</div>
{/* 操作 */}
<button className="btn ghost sm">
{task.status === 'failed' ? '重试' : '查看'}
</button>
</div>
))}
</div>
```
**优先级:** P2
---
### 3.8 模板库 (Template Library)
**文件路径:** `pages/templates/TemplateLibrary.tsx`
**路由:** `/templates`
**当前状态:**
- ✅ 搜索/筛选/收藏逻辑已实现
- ⚠️ 分类筛选改为按钮组
- ⚠️ 卡片 4 列布局
- ⚠️ 创建模板按钮位置
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| 分类筛选 | 下拉框 | 胶囊按钮组 | P2 |
| 模板卡片 | - | 4列网格 | P2 |
| 卡片样式 | - | 16:9 缩略图 | P2 |
| 页面头部 | - | 标题+创建模板按钮 | P2 |
**具体修改指令:**
#### 3.8.1 工具栏(搜索 + 分类按钮组)
```tsx
<div className="template-toolbar" style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
gap: '16px',
marginBottom: '20px',
}}>
{/* 搜索框 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-sm)',
padding: '0 14px',
flex: 1,
maxWidth: '400px',
}}>
<span>🔍</span>
<input
type="text"
placeholder="搜索模板..."
style={{
border: 0,
outline: 0,
padding: '12px 0',
fontSize: '14px',
width: '100%',
}}
/>
</div>
{/* 分类按钮组 */}
<div style={{ display: 'flex', gap: '8px' }}>
{['全部', '口播', '种草', '产品', '品牌'].map((cat) => (
<button
key={cat}
className={`btn ghost sm ${activeCategory === cat ? 'active' : ''}`}
style={{
background: activeCategory === cat ? 'var(--primary-soft)' : '#fff',
color: activeCategory === cat ? 'var(--primary)' : 'var(--muted)',
border: activeCategory === cat ? '1px solid var(--primary)' : '1px solid var(--line)',
}}
>
{cat}
</button>
))}
</div>
</div>
```
#### 3.8.2 模板卡片
```tsx
<div className="template-grid" style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gap: '16px',
}}>
{templates.map((tmpl) => (
<div
key={tmpl.id}
className="template-card"
style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-md)',
overflow: 'hidden',
cursor: 'pointer',
transition: 'var(--transition-base)',
}}
>
{/* 缩略图 */}
<div
style={{
aspectRatio: '16/9',
display: 'grid',
placeItems: 'center',
color: '#fff',
fontSize: '28px',
fontWeight: 700,
position: 'relative',
background: tmpl.gradient,
}}
>
<span style={{ position: 'relative', zIndex: 1 }}>{tmpl.code}</span>
<div style={{
position: 'absolute',
inset: 0,
background: 'linear-gradient(0deg, rgba(0,0,0,0.3), transparent)',
}} />
</div>
{/* 信息 */}
<div style={{ padding: '14px' }}>
<h4 style={{ margin: '0 0 6px', fontSize: '14px', fontWeight: 600 }}>
{tmpl.name}
</h4>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '12px', color: 'var(--muted)' }}>
<CategoryPill category={tmpl.category} />
<span>使用 {tmpl.usageCount} </span>
</div>
<button
className="btn primary sm"
style={{ width: '100%', marginTop: '10px' }}
>
使用此模板
</button>
</div>
</div>
))}
</div>
```
**优先级:** P2
---
### 3.9 订阅管理 (Subscription)
**文件路径:** `pages/subscription/SubscriptionPage.tsx`
**路由:** `/subscription`
**当前状态:**
- ✅ 订阅逻辑已实现
- ⚠️ 定价卡片样式、特性列表、价格展示需对齐
- ⚠️ 当前套餐信息卡片样式
- ❌ 账单记录表格
**UI 差异清单:**
| 差异项 | 当前状态 | 原型状态 | 优先级 |
|--------|----------|----------|--------|
| 定价卡片样式 | - | 原型卡片样式 | P2 |
| 特性列表 | - | 带勾选列表 | P2 |
| 当前套餐卡片 | - | 标签+按钮组 | P2 |
| 账单记录表格 | 无 | 完整表格 | P1 |
**具体修改指令:**
#### 3.9.1 账单记录表格
```tsx
<div className="section">
<div className="section-title">
<h3>账单记录</h3>
</div>
<div style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-lg)',
overflow: 'hidden',
}}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#f8fafc' }}>
{['订单号', '套餐', '金额', '支付方式', '时间', '状态'].map((h) => (
<th key={h} style={{
padding: '14px 16px',
textAlign: 'left',
fontSize: '13px',
color: 'var(--muted)',
}}>{h}</th>
))}
</tr>
</thead>
<tbody>
{bills.map((bill) => (
<tr key={bill.id} style={{ borderTop: '1px solid var(--line)' }}>
<td style={{ padding: '14px 16px', fontSize: '13px', color: 'var(--muted)' }}>
{bill.orderNo}
</td>
<td style={{ padding: '14px 16px', fontSize: '13px' }}>
{bill.plan}
</td>
<td style={{ padding: '14px 16px', fontSize: '13px', fontWeight: 600 }}>
¥{bill.amount.toLocaleString()}
</td>
<td style={{ padding: '14px 16px', fontSize: '13px' }}>
{bill.paymentMethod}
</td>
<td style={{ padding: '14px 16px', fontSize: '13px', color: 'var(--muted)' }}>
{bill.date}
</td>
<td style={{ padding: '14px 16px' }}>
<StatusPill status="paid" />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
```
**优先级:** P1(账单表格)、P2(其他样式)
---
### 3.10 剪辑计划编辑器 (Editing Planner)
**文件路径:** `pages/editing-planner/EditingPlanner.tsx`
**路由:** `/editing-planner`
**当前状态:**
- ✅ 核心功能已实现
- ⚠️ 模式切换按钮样式
- ⚠️ 面板样式对齐
**优先级:** P2
---
## 4. 新增页面开发指令
### 4.1 查重检测页面组
**目录:** `pages/duplication/`
**涉及文件:**
- `DuplicationUpload.tsx` - 上传页面
- `DuplicationResults.tsx` - 结果页面
- `DuplicationDetail.tsx` - 详情页面
**路由配置:**
```tsx
{
path: '/duplication',
element: <AppLayout />,
children: [
{ path: '', element: <DuplicationUpload /> },
{ path: 'results/:id', element: <DuplicationResults /> },
{ path: 'detail/:id', element: <DuplicationDetail /> },
]
}
```
#### 4.1.1 DuplicationUpload 页面规格
**页面布局:** 左右分栏(1fr : 1fr
```tsx
// pages/duplication/DuplicationUpload.tsx
const DuplicationUpload: React.FC = () => {
return (
<PageHead
title="视频查重检测"
description="上传视频文件,检测与现有视频的相似度"
/>
<div className="duplication-layout" style={{
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '24px',
}}>
{/* 左侧上传区 */}
<div>
<div
className="upload-zone"
style={{
border: '2px dashed var(--line)',
borderRadius: 'var(--radius-lg)',
padding: '60px',
textAlign: 'center',
background: 'linear-gradient(180deg, #fff, #f8fafc)',
transition: 'var(--transition-base)',
cursor: 'pointer',
}}
>
<div
className="icon"
style={{
width: '64px',
height: '64px',
borderRadius: '20px',
background: 'var(--primary-soft)',
margin: '0 auto 16px',
display: 'grid',
placeItems: 'center',
fontSize: '28px',
color: 'var(--primary)',
}}
>
📤
</div>
<h3 style={{ margin: '0 0 8px', fontSize: '18px' }}>
拖拽上传视频文件
</h3>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '13px' }}>
或点击选择文件上传
</p>
<div style={{ marginTop: '12px', fontSize: '12px', color: 'var(--muted)' }}>
支持格式:MP4 / AVI / MOV / MKV,单文件 2GB
</div>
</div>
<div style={{ marginTop: '16px', display: 'flex', gap: '10px' }}>
<button className="btn primary" style={{ flex: 1 }}>
📤 选择文件
</button>
<button className="btn ghost">
📷 摄像头录制
</button>
</div>
</div>
{/* 右侧结果区 */}
<ResultZone />
</div>
);
};
```
#### 4.1.2 DuplicationResults 页面规格
```tsx
// pages/duplication/DuplicationResults.tsx
const DuplicationResults: React.FC = () => {
const { id } = useParams();
return (
<PageHead
title="查重结果"
description={`检测 ID: ${id}`}
actions={
<button className="btn ghost">📥 下载报告</button>
}
/>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
{/* 相似度圆环 */}
<div className="card">
<div className="result-score" style={{ textAlign: 'center', padding: '30px' }}>
<div
className="result-circle"
style={{
width: '140px',
height: '140px',
borderRadius: '50%',
margin: '0 auto 16px',
display: 'grid',
placeItems: 'center',
flexDirection: 'column',
background: 'linear-gradient(135deg, #fef3c7, #fde68a)',
color: '#92400e',
}}
>
<div style={{ fontSize: '36px', fontWeight: 800 }}>12%</div>
<div style={{ fontSize: '14px', opacity: 0.8 }}>相似度</div>
</div>
<p style={{ color: 'var(--muted)', fontSize: '14px', marginTop: '12px' }}>
该视频相似度较低,原创性良好
</p>
</div>
</div>
{/* 匹配列表 */}
<div className="card">
<h3 style={{ margin: '0 0 16px' }}>匹配的视频片段</h3>
<div className="result-list">
{matches.map((match) => (
<div
key={match.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '14px',
padding: '14px',
background: '#f8fafc',
borderRadius: 'var(--radius-sm)',
marginBottom: '10px',
}}
>
<div
style={{
width: '60px',
height: '80px',
borderRadius: '8px',
background: 'linear-gradient(135deg, #64748b, #334155)',
display: 'grid',
placeItems: 'center',
color: '#fff',
}}
>
</div>
<div style={{ flex: 1 }}>
<h4 style={{ margin: '0 0 4px', fontSize: '14px', fontWeight: 600 }}>
{match.videoName}
</h4>
<p style={{ margin: 0, fontSize: '12px', color: 'var(--muted)' }}>
相似片段:{match.segment}
</p>
</div>
<div style={{ fontWeight: 700, color: 'var(--amber)' }}>
{match.similarity}%
</div>
</div>
))}
</div>
</div>
</div>
);
};
```
**优先级:** P1
---
### 4.2 我的音色 (Voice Clone)
**目录:** `pages/voice-clone/`
**文件:** `VoiceClone.tsx`
**路由:** `/voice-clone`
#### 4.2.1 页面规格
```tsx
// pages/voice-clone/VoiceClone.tsx
const VoiceClone: React.FC = () => {
return (
<PageHead
title="🎤 我的音色库"
description="克隆和管理你的专属音色,用AI生成个性化配音"
actions={
<button className="btn primary" onClick={openCloneModal}>
克隆新音色
</button>
}
/>
{/* 音色卡片网格 */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
gap: '16px',
marginTop: '20px',
}}>
{clonedVoices.map((voice) => (
<VoiceCloneCard key={voice.id} voice={voice} />
))}
</div>
{/* 空状态 */}
{clonedVoices.length === 0 && (
<div style={{
marginTop: '40px',
textAlign: 'center',
padding: '48px',
}}>
<div style={{ fontSize: '64px', marginBottom: '16px' }}>🎤</div>
<h3 style={{ margin: '0 0 8px', fontSize: '18px' }}>
还没有克隆音色
</h3>
<p style={{ margin: '0 0 20px', color: 'var(--muted)' }}>
上传你的声音,AI将克隆你的专属音色
</p>
<button className="btn primary" onClick={openCloneModal}>
立即克隆
</button>
</div>
)}
);
};
```
#### 4.2.2 音色克隆卡片组件
```tsx
interface VoiceCloneCardProps {
voice: {
id: string;
name: string;
duration: string;
createdAt: string;
status: 'ready' | 'processing';
};
}
const VoiceCloneCard: React.FC<VoiceCloneCardProps> = ({ voice }) => (
<div
style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-lg)',
padding: '20px',
position: 'relative',
}}
>
{/* 操作按钮 */}
<div style={{ position: 'absolute', top: '16px', right: '16px', display: 'flex', gap: '4px' }}>
<button className="btn ghost sm" style={{ padding: '4px 8px', fontSize: '11px' }}>🗑️</button>
<button className="btn ghost sm" style={{ padding: '4px 8px', fontSize: '11px' }}>✏️</button>
</div>
{/* 头部信息 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '14px', marginBottom: '14px' }}>
<div
style={{
width: '56px',
height: '56px',
borderRadius: '50%',
background: 'linear-gradient(135deg, #f59e0b, #d97706)',
display: 'grid',
placeItems: 'center',
color: '#fff',
fontSize: '24px',
}}
>
🎤
</div>
<div>
<h4 style={{ margin: '0 0 4px', fontSize: '15px', fontWeight: 600 }}>
{voice.name}
</h4>
<StatusPill status={voice.status === 'ready' ? 'ok' : 'warn'} />
</div>
</div>
{/* 元信息 */}
<div style={{ fontSize: '13px', color: 'var(--muted)', marginBottom: '12px' }}>
<div style={{ marginBottom: '4px' }}>🎵 时长:{voice.duration}</div>
<div>📅 创建于:{voice.createdAt}</div>
</div>
{/* 操作区 */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '10px',
background: '#f8fafc',
borderRadius: '10px',
}}>
<button className="btn ghost sm" style={{ flex: 1 }}> 试听</button>
<button className="btn primary sm" style={{ flex: 1 }}> 使用此音色</button>
</div>
</div>
);
```
#### 4.2.3 克隆音色 Modal
```tsx
// components/modals/CloneVoiceModal.tsx
const CloneVoiceModal: React.FC<{ visible: boolean; onClose: () => void }> = ({
visible,
onClose,
}) => {
const [step, setStep] = useState<'input' | 'uploading' | 'success'>('input');
return (
<Modal
visible={visible}
onClose={onClose}
title="🎤 克隆新音色"
width={520}
>
{/* 音色名称 */}
<Form.Item label="音色名称">
<Input placeholder="输入音色名称" defaultValue="我的声音 1" />
</Form.Item>
{/* 上传区域 */}
<Form.Item label="上传音频">
<div
style={{
border: '2px dashed var(--line)',
borderRadius: 'var(--radius-md)',
padding: '30px',
textAlign: 'center',
background: '#f8fafc',
cursor: 'pointer',
}}
>
<div style={{ fontSize: '36px', marginBottom: '10px' }}>🎵</div>
<p style={{ fontWeight: 600 }}>拖拽音频文件到此处</p>
<p style={{ color: 'var(--muted)', fontSize: '13px' }}>支持 MP3WAV 格式</p>
</div>
</Form.Item>
{/* 或分隔 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', margin: '20px 0' }}>
<div style={{ flex: 1, height: '1px', background: 'var(--line)' }} />
<span style={{ fontSize: '13px', color: 'var(--muted)' }}></span>
<div style={{ flex: 1, height: '1px', background: 'var(--line)' }} />
</div>
{/* 录音 */}
<Form.Item label="直接录制">
<div style={{ border: '1px solid var(--line)', borderRadius: 'var(--radius-md)', padding: '24px', textAlign: 'center' }}>
<p style={{ color: 'var(--muted)', marginBottom: '12px' }}>点击按钮开始录制你的声音</p>
<button
className="btn primary"
style={{
width: '80px',
height: '80px',
borderRadius: '50%',
fontSize: '32px',
padding: 0,
background: 'linear-gradient(135deg, #ef4444, #dc2626)',
}}
>
🎙️
</button>
</div>
</Form.Item>
{/* 提示 */}
<div style={{
padding: '12px 16px',
background: '#fef3c7',
borderRadius: 'var(--radius-sm)',
marginBottom: '20px',
fontSize: '13px',
color: '#92400e',
}}>
💡 建议上传10~3分钟的清晰语音,环境安静、语速均匀效果最佳
</div>
{/* 按钮 */}
<div style={{ display: 'flex', gap: '12px' }}>
<button className="btn ghost" onClick={onClose} style={{ flex: 1 }}>
取消
</button>
<button className="btn primary" style={{ flex: 1 }}>
🎤 开始克隆
</button>
</div>
</Modal>
);
};
```
**优先级:** P1
---
### 4.3 账号管理 (Accounts)
**目录:** `pages/accounts/`
**文件:** `Accounts.tsx`
**路由:** `/accounts`
#### 4.3.1 页面规格
```tsx
// pages/accounts/Accounts.tsx
const Accounts: React.FC = () => {
const platforms = [
{ id: 'douyin', name: '抖音', icon: '📱', gradient: 'linear-gradient(135deg, #fe2c55, #25f4ee)' },
{ id: 'kuaishou', name: '快手', icon: '🎬', gradient: 'linear-gradient(135deg, #ff4906, #ffba00)' },
{ id: 'xiaohongshu', name: '小红书', icon: '📕', gradient: 'linear-gradient(135deg, #ff2442, #ff6b6b)' },
{ id: 'wechat', name: '微信视频号', icon: '💬', gradient: 'linear-gradient(135deg, #07c160, #4cd964)' },
];
return (
<PageHead
title="🔑 账号管理"
description="绑定您的社交平台账号,用于视频一键发布到各平台"
/>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '20px' }}>
{platforms.map((platform) => (
<PlatformCard key={platform.id} platform={platform} />
))}
</div>
{/* 底部统计 */}
<div style={{
marginTop: '24px',
padding: '16px 20px',
background: '#f8fafc',
borderRadius: 'var(--radius-md)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
}}>
<span style={{ fontSize: '16px' }}>📊</span>
<span style={{ fontWeight: 600 }}>
已绑定 <span style={{ color: 'var(--primary)' }}>4</span> 个账号 /
支持 <span style={{ color: 'var(--primary)' }}>4</span> 个平台
</span>
</div>
);
};
```
#### 4.3.2 平台账号卡片组件
```tsx
interface PlatformCardProps {
platform: {
id: string;
name: string;
subName: string;
icon: string;
gradient: string;
};
}
const PlatformCard: React.FC<PlatformCardProps> = ({ platform }) => {
const accounts = useAccounts(platform.id);
return (
<div
style={{
background: '#fff',
border: '1px solid var(--line)',
borderRadius: 'var(--radius-lg)',
padding: '24px',
}}
>
{/* 平台头部 */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '20px' }}>
<span style={{ fontSize: '28px' }}>{platform.icon}</span>
<div>
<h3 style={{ margin: 0, fontSize: '18px' }}>{platform.name}</h3>
<p style={{ margin: '4px 0 0', fontSize: '13px', color: 'var(--muted)' }}>
{platform.subName}
</p>
</div>
</div>
{/* 账号列表 */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '16px' }}>
{accounts.length > 0 ? (
accounts.map((account) => (
<div
key={account.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '12px',
background: '#f8fafc',
borderRadius: '12px',
}}
>
<div
style={{
width: '36px',
height: '36px',
borderRadius: '50%',
background: platform.gradient,
display: 'grid',
placeItems: 'center',
color: '#fff',
fontWeight: 700,
fontSize: '14px',
}}
>
{account.avatar || platform.icon}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: '14px' }}>{account.name}</div>
<StatusPill status="ok" />
</div>
<button className="btn ghost sm" style={{ padding: '6px 10px', fontSize: '12px' }}>
解绑
</button>
</div>
))
) : (
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--muted)', fontSize: '14px' }}>
<div style={{ fontSize: '32px', marginBottom: '8px' }}>🔓</div>
<p style={{ margin: 0 }}>暂未绑定账号</p>
</div>
)}
</div>
{/* 绑定按钮 */}
<button className="btn ghost" style={{ width: '100%' }}>
+ 绑定新账号
</button>
</div>
);
};
```
**优先级:** P1
---
### 4.4 首页 (Landing Page)
**目录:** `pages/home/`
**文件:** `HomePage.tsx`
**路由:** `/`
#### 4.4.1 页面规格
```tsx
// pages/home/HomePage.tsx
const HomePage: React.FC = () => {
return (
<div>
{/* Hero 区域 */}
<HeroSection />
{/* 功能卡片 */}
<FeatureSection />
{/* 工作流程 */}
<WorkflowSection />
{/* 定价预览 */}
<PricingSection />
{/* CTA */}
<CTASection />
</div>
);
};
```
#### 4.4.2 Hero 区域组件
```tsx
const HeroSection: React.FC = () => (
<div className="hero" style={{
display: 'grid',
gridTemplateColumns: '1fr 420px',
gap: '48px',
alignItems: 'center',
padding: '60px 0 50px',
maxWidth: '1440px',
margin: '0 auto',
paddingLeft: '24px',
paddingRight: '24px',
}}>
<div>
<span className="tag">🦐 小虾智剪 · AI智能视频创作平台</span>
<h1 style={{
fontSize: '44px',
lineHeight: 1.1,
margin: '16px 0 14px',
letterSpacing: '-0.04em',
}}>
上传素材,AI自动剪辑<br/>
一键生成短视频
</h1>
<p style={{
fontSize: '17px',
lineHeight: 1.8,
color: 'var(--muted)',
margin: '0 0 22px',
}}>
面向口播、产品介绍、直播切片和企业宣传的智能视频创作平台。
</p>
<div className="hero-actions" style={{ display: 'flex', gap: '12px', marginTop: '8px' }}>
<button className="btn primary" style={{ padding: '14px 24px', fontSize: '15px' }}>
立即免费开始
</button>
<button className="btn ghost" style={{ padding: '14px 24px', fontSize: '15px' }}>
查看定价方案
</button>
</div>
</div>
<div className="hero-visual">
<div className="hero-video">
<div className="play-btn"></div>
</div>
<div className="hero-info">
<div className="hero-badge">
<span className="demo-badge"> AI智能剪辑</span>
<span className="demo-badge"> 30秒生成</span>
</div>
<span className="pill ok">可发布</span>
</div>
</div>
</div>
);
```
**优先级:** P3
---
## 5. 基础组件封装规范
### 5.1 StatusPill 状态标签
```tsx
// components/common/StatusPill.tsx
type StatusType = 'ok' | 'warn' | 'bad' | 'info' | 'muted';
interface StatusPillProps {
status: StatusType;
label?: string;
}
const statusConfig: Record<StatusType, { bg: string; color: string; defaultLabel: string }> = {
ok: { bg: '#dcfce7', color: '#166534', defaultLabel: '已通过' },
warn: { bg: '#ffedd5', color: '#c2410c', defaultLabel: '待复核' },
bad: { bg: '#ffe4e6', color: '#be123c', defaultLabel: '高风险' },
info: { bg: '#dbeafe', color: '#1d4ed8', defaultLabel: '视频' },
muted: { bg: '#f1f5f9', color: '#64748b', defaultLabel: '品牌' },
};
export const StatusPill: React.FC<StatusPillProps> = ({ status, label }) => {
const config = statusConfig[status];
return (
<span
className="pill"
style={{
background: config.bg,
color: config.color,
fontSize: '11px',
padding: '4px 8px',
borderRadius: '999px',
fontWeight: 700,
}}
>
{label || config.defaultLabel}
</span>
);
};
```
### 5.2 CategoryPill 分类标签
```tsx
// components/common/CategoryPill.tsx
const categoryConfig: Record<string, { bg: string; color: string }> = {
口播: { bg: '#dbeafe', color: '#1d4ed8' },
种草: { bg: '#dcfce7', color: '#166534' },
产品: { bg: '#ffedd5', color: '#c2410c' },
品牌: { bg: '#f1f5f9', color: '#64748b' },
直播: { bg: '#fef3c7', color: '#92400e' },
教程: { bg: '#e0e7ff', color: '#4338ca' },
};
interface CategoryPillProps {
category: string;
}
export const CategoryPill: React.FC<CategoryPillProps> = ({ category }) => {
const config = categoryConfig[category] || categoryConfig['口播'];
return (
<span
className="pill"
style={{
background: config.bg,
color: config.color,
fontSize: '11px',
padding: '4px 8px',
borderRadius: '999px',
fontWeight: 700,
}}
>
{category}
</span>
);
};
```
### 5.3 IconBox 图标盒子
```tsx
// components/common/IconBox.tsx
interface IconBoxProps {
icon: string;
gradient?: string;
size?: number;
}
export const IconBox: React.FC<IconBoxProps> = ({
icon,
gradient = 'linear-gradient(135deg, #6366f1, #4f46e5)',
size = 44,
}) => (
<div
style={{
width: `${size}px`,
height: `${size}px`,
borderRadius: `${Math.floor(size * 0.36)}px`,
display: 'grid',
placeItems: 'center',
fontSize: `${size * 0.45}px`,
background: gradient,
color: '#fff',
}}
>
{icon}
</div>
);
```
### 5.4 UploadZone 上传区域
```tsx
// components/common/UploadZone.tsx
interface UploadZoneProps {
accept?: string;
maxSize?: number; // MB
onUpload: (file: File) => void;
}
export const UploadZone: React.FC<UploadZoneProps> = ({
accept = '*',
maxSize = 2048,
onUpload,
}) => {
const [isDragging, setIsDragging] = useState(false);
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
setIsDragging(false);
const file = e.dataTransfer.files[0];
if (file && file.size <= maxSize * 1024 * 1024) {
onUpload(file);
}
};
return (
<div
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
style={{
border: `2px dashed ${isDragging ? 'var(--primary-light)' : 'var(--line)'}`,
borderRadius: 'var(--radius-lg)',
padding: '60px',
textAlign: 'center',
background: isDragging ? 'var(--primary-soft)' : 'linear-gradient(180deg, #fff, #f8fafc)',
transition: 'var(--transition-base)',
cursor: 'pointer',
}}
>
<IconBox icon="📤" gradient="var(--primary-soft)" />
<h3 style={{ margin: '16px 0 8px', fontSize: '18px' }}>拖拽上传文件</h3>
<p style={{ color: 'var(--muted)', fontSize: '13px' }}>或点击选择文件</p>
</div>
);
};
```
### 5.5 EmptyState 空状态
```tsx
// components/common/EmptyState.tsx
interface EmptyStateProps {
icon?: string;
title: string;
description?: string;
action?: {
label: string;
onClick: () => void;
type?: 'primary' | 'ghost';
};
}
export const EmptyState: React.FC<EmptyStateProps> = ({
icon = '📭',
title,
description,
action,
}) => (
<div
style={{
border: '1px dashed #cbd5e1',
background: 'linear-gradient(180deg, #fff, #f8fafc)',
borderRadius: 'var(--radius-lg)',
padding: '48px',
textAlign: 'center',
color: 'var(--muted)',
}}
>
<IconBox icon={icon} size={60} />
<h3 style={{ color: 'var(--slate)', margin: '16px 0 8px', fontSize: '16px' }}>
{title}
</h3>
{description && (
<p style={{ margin: '0 0 16px', fontSize: '14px' }}>{description}</p>
)}
{action && (
<button
className={`btn ${action.type || 'primary'}`}
onClick={action.onClick}
>
{action.label}
</button>
)}
</div>
);
```
---
## 6. 执行顺序建议
### Phase 1: 基础对齐(5-7 人天)
| 序号 | 任务 | 文件 | 优先级 | 预估人天 |
|------|------|------|--------|----------|
| 1.1 | 设计系统 CSS 变量统一 | `global.css` | P0 | 0.5 |
| 1.2 | 主布局组件 AppLayout | `components/layout/AppLayout.tsx` | P0 | 1 |
| 1.3 | 侧边栏组件 Sidebar | `components/layout/Sidebar.tsx` | P0 | 1 |
| 1.4 | 页面头部组件 PageHead | `components/common/PageHead.tsx` | P0 | 0.5 |
| 1.5 | 基础组件封装 | `components/common/*` | P1 | 2 |
### Phase 2: 核心页面改造(8-10 人天)
| 序号 | 任务 | 文件 | 优先级 | 预估人天 |
|------|------|------|--------|----------|
| 2.1 | 一键生成页面 | `pages/generate/GeneratePage.tsx` | P1 | 2 |
| 2.2 | 素材库页面 | `pages/assets/AssetLibrary.tsx` | P1 | 1.5 |
| 2.3 | 配音库页面 | `pages/voices/VoiceLibrary.tsx` | P1 | 1.5 |
| 2.4 | 标题库页面 | `pages/titles/TitleLibrary.tsx` | P1 | 1.5 |
| 2.5 | 成片库页面 | `pages/products/ProductLibrary.tsx` | P1 | 2 |
### Phase 3: 次要页面改造(5-7 人天)
| 序号 | 任务 | 文件 | 优先级 | 预估人天 |
|------|------|------|--------|----------|
| 3.1 | 控制台页面 | `pages/dashboard/Dashboard.tsx` | P2 | 1.5 |
| 3.2 | 模板库页面 | `pages/templates/TemplateLibrary.tsx` | P2 | 1 |
| 3.3 | 任务历史页面 | `pages/history/TaskHistory.tsx` | P2 | 1 |
| 3.4 | 订阅管理页面 | `pages/subscription/SubscriptionPage.tsx` | P2 | 1.5 |
| 3.5 | 剪辑计划编辑器 | `pages/editing-planner/EditingPlanner.tsx` | P2 | 1 |
### Phase 4: 新增页面(3-5 人天)
| 序号 | 任务 | 文件 | 优先级 | 预估人天 |
|------|------|------|--------|----------|
| 4.1 | 查重检测上传页 | `pages/duplication/DuplicationUpload.tsx` | P1 | 0.5 |
| 4.2 | 查重检测结果页 | `pages/duplication/DuplicationResults.tsx` | P1 | 0.5 |
| 4.3 | 查重检测详情页 | `pages/duplication/DuplicationDetail.tsx` | P1 | 0.5 |
| 4.4 | 我的音色页面 | `pages/voice-clone/VoiceClone.tsx` | P1 | 1 |
| 4.5 | 音色克隆 Modal | `components/modals/CloneVoiceModal.tsx` | P1 | 1 |
| 4.6 | 账号管理页面 | `pages/accounts/Accounts.tsx` | P1 | 1 |
| 4.7 | 首页落地页 | `pages/home/HomePage.tsx` | P3 | 1 |
---
## 附录
### A. 路由配置参考
```tsx
// router/index.tsx
const router = createBrowserRouter([
// 公开页面
{ path: '/', element: <HomePage /> },
{ path: '/login', element: <LoginPage /> },
{ path: '/subscription', element: <SubscriptionPage /> },
// 受保护页面(需要登录)
{
path: '/app',
element: <AppLayout />,
children: [
{ index: true, element: <Navigate to="/app/dashboard" replace /> },
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'titles', element: <TitleLibrary /> },
{ path: 'assets', element: <AssetLibrary /> },
{ path: 'voices', element: <VoiceLibrary /> },
{ path: 'templates', element: <TemplateLibrary /> },
{ path: 'generate', element: <GeneratePage /> },
{ path: 'products', element: <ProductLibrary /> },
{ path: 'history', element: <TaskHistory /> },
{ path: 'editing-planner', element: <EditingPlanner /> },
{ path: 'subscription', element: <SubscriptionPage /> },
{ path: 'duplication/*', element: <DuplicationRoutes /> },
{ path: 'voice-clone', element: <VoiceClone /> },
{ path: 'accounts', element: <Accounts /> },
],
},
]);
```
### B. 样式文件结构
```
src/
├── styles/
│ ├── global.css # CSS 变量、reset、通用样式
│ ├── components.css # 组件样式(可选)
│ └── pages/
│ ├── dashboard.css
│ ├── generate.css
│ └── ...
```
### C. 注意事项
1. **不删除现有功能**:所有已有的 API 调用逻辑、数据获取/提交逻辑必须保持不变
2. **不改变业务逻辑**:只修改 UI 展示层
3. **保持路由一致**:路由配置必须与原型保持一致
4. **响应式适配**:参考原型中的 `@media` 断点进行响应式处理
---
> 文档版本:V21.1
> 最后更新:2024年
---
> 本内容由 Coze AI 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。