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
2917 lines
79 KiB
Markdown
2917 lines
79 KiB
Markdown
---
|
||
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/WAV,10秒以上)
|
||
</p>
|
||
<p style={{ margin: '8px 0 0', fontSize: '11px', color: 'var(--muted)' }}>
|
||
支持格式:MP3、WAV · 建议时长: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' }}>支持 MP3、WAV 格式</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 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。
|