feat(ui): V21 design system overhaul #9

Merged
xiaoxia merged 1 commits from feature/v21-design-system into main 2026-06-26 18:09:37 +08:00
18 changed files with 1403 additions and 515 deletions
@@ -1,10 +1,12 @@
/* V21 主容器 */
/* V21 主布局样式 */
.xx-app-shell {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--bg, #f8fafc);
color: var(--slate, #0f172a);
position: relative;
z-index: 1;
}
.xx-app-content {
@@ -15,7 +17,7 @@
padding: 38px 24px 76px;
}
/* 根 CSS 变量 */
/* V21 颜色变量 */
:root {
--indigo: #4f46e5;
--slate: #0f172a;
@@ -28,5 +30,5 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
}
+20 -10
View File
@@ -1,12 +1,27 @@
/**
* V21 全局基础样式
*/
:root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
line-height: 1.5;
line-height: 1.6;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
/* V21 颜色系统 */
--indigo: #4f46e5;
--indigo-dark: #4338ca;
--indigo-soft: #eef2ff;
--slate: #0f172a;
--muted: #64748b;
--line: #e2e8f0;
--bg: #f8fafc;
--green: #10b981;
--amber: #f59e0b;
--red: #ef4444;
color-scheme: light;
color: var(--slate);
background-color: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
@@ -21,16 +36,11 @@
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
#root {
width: 100%;
margin: 0 auto;
padding: 2rem;
text-align: center;
min-height: 100vh;
}
+10 -6
View File
@@ -1,5 +1,5 @@
/**
* 更新主入口,引入全局样式
* V21 设计系统入口
*/
import React from 'react';
import ReactDOM from 'react-dom/client';
@@ -17,17 +17,21 @@ const queryClient = new QueryClient({
queries: {
retry: 1,
refetchOnWindowFocus: false,
staleTime: 5 * 60 * 1000, // 5 分钟
gcTime: 10 * 60 * 1000, // 10 分钟
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
},
},
});
// Ant Design 主题配置
// V21 Ant Design 主题配置
const theme = {
token: {
colorPrimary: '#1890ff',
borderRadius: 4,
// V21 Indigo 主色
colorPrimary: '#4f46e5',
// V21 大圆角
borderRadius: 14,
borderRadiusLG: 22,
borderRadiusSM: 12,
},
};
+147 -34
View File
@@ -1,11 +1,38 @@
/* V21 订阅计划 */
/* V21 定价页面 */
.xx-plans-page {
max-width: 1200px;
margin: 0 auto;
padding: 60px 24px;
}
/* 页面头部 */
.xx-page-head {
text-align: center;
margin-bottom: 60px;
}
.xx-page-head h2 {
font-size: 42px;
font-weight: 900;
color: var(--slate, #0f172a);
margin: 0 0 16px;
letter-spacing: -0.02em;
}
.xx-page-head p {
font-size: 18px;
color: var(--muted, #64748b);
margin: 0;
max-width: 560px;
margin-left: auto;
margin-right: auto;
}
/* 定价卡片网格 - 4列 */
.xx-plans-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-top: 30px;
}
@@ -18,86 +45,172 @@
@media (max-width: 768px) {
.xx-plans-grid {
grid-template-columns: 1fr;
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
}
/* V21 定价卡片 */
.xx-plan-card {
background: rgba(255, 255, 255, 0.94);
border: 1px solid #e2e8f0;
background: rgba(255,255,255,0.94);
border: 1px solid rgba(226,232,240,0.95);
border-radius: 28px;
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09);
padding: 28px;
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
padding: 32px 28px;
position: relative;
transition: all 0.2s;
transition: all 0.3s;
display: flex;
flex-direction: column;
}
.xx-plan-card:hover {
border-color: #4f46e5;
box-shadow: 0 32px 90px rgba(79, 70, 229, 0.15);
border-color: var(--indigo, #4f46e5);
box-shadow: 0 32px 90px rgba(79,70,229,0.15);
transform: translateY(-4px);
}
.xx-plan-card.featured {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.12), 0 32px 90px rgba(79, 70, 229, 0.15);
border: 2px solid var(--indigo, #4f46e5);
box-shadow: 0 0 0 4px rgba(79,70,229,0.1), 0 32px 90px rgba(79,70,229,0.15);
transform: scale(1.02);
}
.xx-plan-card.featured:hover {
transform: scale(1.02) translateY(-4px);
}
/* 推荐标签 */
.xx-badge {
position: absolute;
top: 0;
right: 24px;
transform: translateY(-50%);
background: #4f46e5;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: white;
padding: 4px 12px;
padding: 6px 16px;
border-radius: 999px;
font-size: 12px;
font-weight: 850;
white-space: nowrap;
}
/* 企业版定制标签 */
.xx-badge.enterprise {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
/* 套餐名称 */
.xx-plan-card h3 {
font-size: 24px;
margin: 0 0 16px;
color: #0f172a;
font-size: 22px;
font-weight: 900;
color: var(--slate, #0f172a);
margin: 0 0 20px;
text-align: center;
}
/* 价格 */
.xx-plan-price {
font-size: 42px;
text-align: center;
margin-bottom: 12px;
}
.xx-plan-price .currency {
font-size: 18px;
font-weight: 700;
color: var(--muted, #64748b);
vertical-align: top;
}
.xx-plan-price .amount {
font-size: 48px;
font-weight: 900;
color: #4f46e5;
margin-bottom: 8px;
color: var(--indigo, #4f46e5);
line-height: 1;
}
.xx-plan-price span {
.xx-plan-price .period {
font-size: 16px;
color: #64748b;
font-weight: 750;
color: var(--muted, #64748b);
font-weight: 500;
}
/* 企业版价格 */
.xx-plan-price.custom .amount {
font-size: 32px;
}
/* 描述 */
.xx-plan-description {
color: #64748b;
color: var(--muted, #64748b);
font-size: 14px;
text-align: center;
margin-bottom: 24px;
padding-bottom: 24px;
border-bottom: 1px solid var(--line, #e2e8f0);
}
/* 功能列表 */
.xx-features {
margin-top: 24px;
margin-top: 8px;
padding-top: 24px;
border-top: 1px solid #e2e8f0;
flex: 1;
}
.xx-feature {
display: flex;
gap: 10px;
gap: 12px;
align-items: flex-start;
color: #64748b;
color: var(--muted, #64748b);
font-size: 14px;
margin-bottom: 12px;
margin-bottom: 16px;
line-height: 1.5;
}
.xx-feature span {
color: #10b981;
.xx-feature span:first-child {
color: var(--green, #10b981);
font-weight: 900;
flex-shrink: 0;
}
/* 订阅按钮 */
.xx-subscribe-btn {
width: 100%;
margin-top: 24px;
padding: 14px 24px !important;
font-size: 15px !important;
font-weight: 850 !important;
border-radius: 14px !important;
transition: all 0.2s !important;
}
.xx-subscribe-btn.primary {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
color: white !important;
border: none !important;
box-shadow: 0 14px 26px rgba(79,70,229,0.22) !important;
}
.xx-subscribe-btn.primary:hover {
box-shadow: 0 18px 34px rgba(79,70,229,0.28) !important;
transform: translateY(-1px);
}
.xx-subscribe-btn.ghost {
background: white !important;
border: 1px solid var(--line, #e2e8f0) !important;
color: var(--slate, #0f172a) !important;
}
.xx-subscribe-btn.ghost:hover {
border-color: var(--indigo, #4f46e5) !important;
color: var(--indigo, #4f46e5) !important;
}
/* 企业版按钮 */
.xx-subscribe-btn.enterprise {
background: linear-gradient(135deg, #f59e0b, #d97706) !important;
color: white !important;
border: none !important;
box-shadow: 0 14px 26px rgba(245,158,11,0.22) !important;
}
+93 -23
View File
@@ -1,14 +1,68 @@
/**
* 订阅/计划页面 - V21 完全对标
* 定价页面 - V21 UI 四级定价
*/
import React, { useState } from 'react';
import { message } from 'antd';
import './Plans.css';
const PLANS = [
{ id: 'free', name: 'Free', price: 0, description: '免费试用', recommended: false, features: ['10个项目', '基础视频编辑', '社区支持'] },
{ id: 'pro', name: 'Pro', price: 99, description: '专业用户', recommended: true, features: ['无限项目', '高级编辑', '优先支持', 'API 访问'] },
{ id: 'enterprise', name: 'Enterprise', price: 999, description: '企业级', recommended: false, features: ['定制方案', '专属支持', '白标服务', 'SLA 保证'] },
{
id: 'free',
name: '体验版',
price: 0,
description: '适合个人体验',
recommended: false,
features: [
'3个视频成片',
'基础标题模板',
'基础配音音色',
'720p导出'
]
},
{
id: 'standard',
name: '标准版',
price: 99,
description: '适合内容创作者',
recommended: false,
features: [
'无限视频成片',
'高级标题模板',
'全部配音音色',
'1080p导出',
'API接口'
]
},
{
id: 'pro',
name: '专业版',
price: 299,
description: '适合专业团队',
recommended: true,
features: [
'无限视频成片',
'自定义标题',
'全部配音音色',
'4K导出',
'优先渲染队列',
'专属客服'
]
},
{
id: 'enterprise',
name: '企业版',
price: null,
description: '适合企业用户',
recommended: false,
features: [
'私有化部署',
'自定义品牌形象',
' SLA服务保障',
'技术支持',
'团队协作',
'数据报表'
]
},
];
const Plans: React.FC = () => {
@@ -28,41 +82,57 @@ const Plans: React.FC = () => {
return (
<div className="xx-plans-page">
<div className="xx-page-head">
<div>
<h2></h2>
<p></p>
</div>
<h2></h2>
<p></p>
</div>
<div className="xx-plans-grid">
{PLANS.map((plan) => (
<div key={plan.id} className={`xx-plan-card ${plan.recommended ? 'featured' : ''}`}>
<div
key={plan.id}
className={`xx-plan-card ${plan.recommended ? 'featured' : ''}`}
>
{plan.recommended && <div className="xx-badge"></div>}
{plan.id === 'enterprise' && <div className="xx-badge enterprise"></div>}
<h3>{plan.name}</h3>
<div className="xx-plan-price">
¥{plan.price}
<span>/</span>
<div className={`xx-plan-price ${plan.price === null ? 'custom' : ''}`}>
{plan.price !== null ? (
<>
<span className="currency">¥</span>
<span className="amount">{plan.price}</span>
<span className="period">/</span>
</>
) : (
<span className="amount"></span>
)}
</div>
<p className="xx-plan-description">{plan.description}</p>
<button
className={`btn ${plan.recommended ? 'primary' : 'ghost'}`}
onClick={() => handleSubscribe(plan.id)}
disabled={subscribing}
style={{ width: '100%' }}
>
{subscribing ? '处理中...' : '选择此计划'}
</button>
<div className="xx-features">
{plan.features.map((feature: string, idx: number) => (
{plan.features.map((feature, idx) => (
<div key={idx} className="xx-feature">
<span></span>
{feature}
<span>{feature}</span>
</div>
))}
</div>
<button
className={`xx-subscribe-btn ${
plan.id === 'enterprise'
? 'enterprise'
: plan.recommended
? 'primary'
: 'ghost'
}`}
onClick={() => handleSubscribe(plan.id)}
disabled={subscribing}
>
{subscribing ? '处理中...' : plan.price === null ? '联系我们' : '立即订阅'}
</button>
</div>
))}
</div>
+128 -120
View File
@@ -1,200 +1,208 @@
/* V21 素材库 */
/* V21 素材库页面 */
.xx-project-assets {
max-width: 1200px;
margin: 0 auto;
}
/* 页面头部 */
.xx-page-head {
margin-bottom: 32px;
}
.xx-page-head h2 {
font-size: 28px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 8px;
}
.xx-page-head p {
font-size: 14px;
color: var(--muted, #64748b);
margin: 0;
}
/* V21 大卡片 */
.xx-card {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(226, 232, 240, 0.95);
background: rgba(255,255,255,0.94);
border: 1px solid rgba(226,232,240,0.95);
border-radius: 28px;
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.09);
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
padding: 28px;
margin-bottom: 20px;
margin-bottom: 24px;
transition: all 0.25s;
}
.xx-card:hover {
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
transform: translateY(-2px);
}
.xx-card h3 {
font-size: 21px;
margin: 0 0 16px;
color: #0f172a;
font-size: 18px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 16px;
}
.xx-loading {
padding: 60px 24px;
text-align: center;
color: #64748b;
}
/* 上传区域 */
.xx-upload-zone {
border: 2px dashed #e2e8f0;
border: 2px dashed var(--line, #e2e8f0);
border-radius: 24px;
padding: 40px;
padding: 48px;
text-align: center;
background: #f8fafc;
background: var(--bg, #f8fafc);
cursor: pointer;
transition: all 0.2s;
}
.xx-upload-zone:hover {
border-color: #4f46e5;
background: #eef2ff;
border-color: var(--indigo, #4f46e5);
background: var(--indigo-soft, #eef2ff);
}
.xx-upload-zone .icon {
font-size: 48px;
margin-bottom: 16px;
}
.xx-upload-zone p {
color: #64748b;
color: var(--muted, #64748b);
margin: 12px 0;
font-size: 14px;
}
.xx-video-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
/* V21 上传按钮 */
.xx-upload-btn {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
color: white !important;
border: none !important;
border-radius: 14px !important;
padding: 12px 24px !important;
font-weight: 850 !important;
box-shadow: 0 14px 26px rgba(79,70,229,0.22) !important;
transition: all 0.2s !important;
cursor: pointer;
}
@media (max-width: 1200px) {
.xx-video-grid {
.xx-upload-btn:hover {
box-shadow: 0 18px 34px rgba(79,70,229,0.28) !important;
transform: translateY(-1px);
}
/* 素材网格 - 9:16 竖屏卡片 */
.xx-assets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 20px;
}
@media (max-width: 768px) {
.xx-assets-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.xx-video-grid {
@media (max-width: 480px) {
.xx-assets-grid {
grid-template-columns: 1fr;
}
}
.xx-video-card {
border: 1px solid #e2e8f0;
/* V21 素材卡片 - 大圆角 */
.xx-asset-card {
background: white;
border: 1px solid var(--line, #e2e8f0);
border-radius: 22px;
padding: 12px;
transition: all 0.2s;
transition: all 0.25s;
cursor: pointer;
}
.xx-video-card:hover {
border-color: #4f46e5;
box-shadow: 0 12px 36px rgba(79, 70, 229, 0.12);
.xx-asset-card:hover {
border-color: var(--indigo, #4f46e5);
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
transform: translateY(-4px);
}
.xx-video-thumb {
aspect-ratio: 16 / 9;
/* 缩略图 - 9:16 竖屏 */
.xx-asset-thumb {
aspect-ratio: 9 / 16;
border-radius: 16px;
background: linear-gradient(135deg, #0ea5e9, #312e81);
background: linear-gradient(135deg, #6366f1, #0ea5e9, #10b981);
position: relative;
overflow: hidden;
display: grid;
place-items: center;
color: white;
margin-bottom: 10px;
font-size: 32px;
margin-bottom: 12px;
}
.mini-play {
width: 46px;
height: 46px;
border-radius: 50%;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.22);
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(8px);
font-size: 18px;
}
.xx-video-card b {
display: block;
color: #0f172a;
font-weight: 850;
}
.xx-video-card p {
margin: 5px 0 0;
color: #64748b;
font-size: 12px;
}
.row {
display: flex;
justify-content: space-between;
/* 状态标签 - Pill 样式 */
.xx-asset-status {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.pill {
gap: 4px;
font-size: 11px;
padding: 3px 7px;
border-radius: 999px;
font-weight: 850;
padding: 3px 10px;
border-radius: 999px;
white-space: nowrap;
}
.pill.ok {
.xx-asset-status.ready {
background: #dcfce7;
color: #15803d;
}
.pill.warn {
.xx-asset-status.processing {
background: #fef3c7;
color: #b45309;
}
.pill.bad {
background: #ffe4e6;
color: #be123c;
.xx-asset-status.failed {
background: #fee2e2;
color: #b91c1c;
}
.assist-actions {
display: grid;
gap: 10px;
margin-top: 16px;
grid-template-columns: 1fr 1fr;
}
.btn {
border: 0;
border-radius: 14px;
padding: 12px 17px;
font-weight: 850;
cursor: pointer;
.xx-asset-card b {
display: block;
font-size: 14px;
transition: all 0.2s;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 8px 0 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.btn.primary {
background: #4f46e5;
color: white;
box-shadow: 0 16px 28px rgba(79, 70, 229, 0.24);
}
.btn.primary:hover {
background: #4338ca;
box-shadow: 0 20px 36px rgba(79, 70, 229, 0.32);
}
.btn.primary:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn.ghost {
background: white;
border: 1px solid #e2e8f0;
color: #475569;
}
.btn.ghost:hover {
background: #f8fafc;
border-color: #4f46e5;
color: #4f46e5;
.xx-asset-card p {
font-size: 12px;
color: var(--muted, #64748b);
margin: 0;
}
/* 空状态 */
.xx-empty-state {
text-align: center;
padding: 40px 24px;
color: #64748b;
padding: 60px 24px;
color: var(--muted, #64748b);
}
.xx-empty-state p {
font-size: 16px;
margin: 0;
margin: 12px 0;
}
/* 加载状态 */
.xx-loading {
padding: 60px 24px;
text-align: center;
color: var(--muted, #64748b);
}
/* 隐藏文件输入 */
input[type="file"] {
display: none;
}
+24 -23
View File
@@ -1,5 +1,5 @@
/**
* 素材库页面 - V21 完全对标
* 素材库页面 - V21 UI
*/
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -26,15 +26,13 @@ const ProjectAssets: React.FC = () => {
return (
<div className="xx-project-assets">
<div className="xx-page-head">
<div>
<h2></h2>
<p></p>
</div>
<h2></h2>
<p></p>
</div>
{/* 上传区 */}
{/* 上传区域 - V21 设计 */}
<div className="xx-card">
<h3></h3>
<h3>📤 </h3>
<div
className="xx-upload-zone"
onDragOver={(e) => e.preventDefault()}
@@ -43,43 +41,46 @@ const ProjectAssets: React.FC = () => {
if (e.dataTransfer.files) handleFileUpload(e.dataTransfer.files);
}}
>
<div style={{ fontSize: '32px', marginBottom: '12px' }}>📁</div>
<p></p>
<div className="icon">🎬</div>
<p></p>
<p> MP4MOVAVI 500MB</p>
<input
type="file"
multiple
style={{ display: 'none' }}
accept="video/*"
id="file-input"
onChange={(e) => e.target.files && handleFileUpload(e.target.files)}
/>
<button
className="btn primary"
className="xx-upload-btn"
onClick={() => document.getElementById('file-input')?.click()}
disabled={uploading}
>
{uploading ? '上传中...' : '选择文件'}
{uploading ? '上传中...' : '选择文件上传'}
</button>
</div>
</div>
{/* 素材列表 */}
{/* 素材库 - V21 竖屏卡片网格 */}
<div className="xx-card">
<h3></h3>
<h3>📁 </h3>
{assets.length === 0 ? (
<div className="xx-empty-state">
<p></p>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📂</div>
<p></p>
</div>
) : (
<div className="xx-video-grid">
<div className="xx-assets-grid">
{assets.map((asset) => (
<div key={asset.id} className="xx-video-card">
<div className="xx-video-thumb">
<div className="mini-play"></div>
<div key={asset.id} className="xx-asset-card">
<div className="xx-asset-thumb">
<span></span>
</div>
<div className="row">
<b>{asset.name}</b>
</div>
<p></p>
<span className={`xx-asset-status ${asset.status || 'ready'}`}>
{asset.status === 'processing' ? '⏳ 处理中' : '✓ 就绪'}
</span>
<b>{asset.name}</b>
<p>{(asset.size / 1024 / 1024).toFixed(2)} MB</p>
</div>
))}
</div>
@@ -1,11 +1,77 @@
/* V21 视频剪辑 */
/* V21 剪辑计划页面 */
.xx-project-generation {
max-width: 1200px;
margin: 0 auto;
}
/* 页面头部 */
.xx-page-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 18px;
margin-bottom: 32px;
}
.xx-page-head h2 {
font-size: 28px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 8px;
}
.xx-page-head p {
font-size: 14px;
color: var(--muted, #64748b);
margin: 0;
}
/* V21 按钮 */
.xx-primary-btn {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
color: white !important;
border: none !important;
border-radius: 14px !important;
padding: 12px 24px !important;
font-weight: 850 !important;
box-shadow: 0 14px 26px rgba(79,70,229,0.22) !important;
transition: all 0.2s !important;
cursor: pointer;
}
.xx-primary-btn:hover {
box-shadow: 0 18px 34px rgba(79,70,229,0.28) !important;
transform: translateY(-1px);
}
/* V21 卡片 */
.xx-card {
background: rgba(255,255,255,0.94);
border: 1px solid rgba(226,232,240,0.95);
border-radius: 28px;
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
padding: 28px;
margin-bottom: 24px;
transition: all 0.25s;
}
.xx-card:hover {
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
transform: translateY(-2px);
}
.xx-card h3 {
font-size: 18px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 20px;
}
/* 模板网格 */
.xx-template-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
gap: 16px;
}
@media (max-width: 1200px) {
@@ -20,59 +86,141 @@
}
}
.xx-choice {
/* V21 模板卡片 */
.xx-choice-card {
position: relative;
border: 2px solid #e2e8f0;
border-radius: 18px;
padding: 14px;
border: 2px solid var(--line, #e2e8f0);
border-radius: 20px;
padding: 0;
background: white;
cursor: pointer;
transition: all 0.2s;
transition: all 0.25s;
overflow: hidden;
}
.xx-choice:hover {
border-color: #4f46e5;
.xx-choice-card:hover {
border-color: var(--indigo, #4f46e5);
box-shadow: 0 12px 36px rgba(79,70,229,0.15);
transform: translateY(-2px);
}
.xx-choice.selected {
border-color: #4f46e5;
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.16);
.xx-choice-card.selected {
border-color: var(--indigo, #4f46e5);
box-shadow: 0 0 0 4px rgba(79,70,229,0.1), 0 12px 36px rgba(79,70,229,0.2);
}
.check {
/* 模板封面 */
.xx-template-cover {
height: 100px;
border-radius: 18px 18px 0 0;
display: grid;
place-items: center;
color: white;
font-size: 32px;
margin: 8px 8px 0;
}
.xx-template-cover.gradient-1 {
background: linear-gradient(135deg, #6366f1, #4f46e5);
}
.xx-template-cover.gradient-2 {
background: linear-gradient(135deg, #f97316, #ef4444);
}
.xx-template-cover.gradient-3 {
background: linear-gradient(135deg, #10b981, #0284c7);
}
.xx-template-cover.gradient-4 {
background: linear-gradient(135deg, #8b5cf6, #ec4899);
}
/* 选中标记 */
.xx-choice-card.selected .xx-template-cover::after {
content: '✓';
position: absolute;
right: -7px;
top: -7px;
width: 26px;
height: 26px;
top: 12px;
right: 12px;
width: 28px;
height: 28px;
border-radius: 50%;
background: #4f46e5;
background: var(--indigo, #4f46e5);
color: white;
display: grid;
place-items: center;
font-weight: 900;
font-size: 14px;
}
.cover {
height: 78px;
border-radius: 14px;
display: grid;
place-items: center;
color: white;
font-size: 28px;
margin-bottom: 10px;
.xx-choice-card .xx-template-info {
padding: 12px 16px 16px;
}
.xx-choice b {
.xx-choice-card b {
display: block;
font-size: 15px;
font-weight: 850;
color: #0f172a;
display: block;
color: var(--slate, #0f172a);
margin-bottom: 4px;
}
.xx-choice p {
margin: 5px 0 0;
color: #64748b;
.xx-choice-card p {
font-size: 12px;
color: var(--muted, #64748b);
margin: 0;
line-height: 1.4;
}
/* 分栏布局 */
.xx-generation-layout {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.xx-generation-layout {
grid-template-columns: 1fr;
}
}
/* 计划预览 */
.xx-preview-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.xx-preview-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--bg, #f8fafc);
border-radius: 16px;
}
.xx-preview-item .icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #6366f1, #4f46e5);
display: grid;
place-items: center;
font-size: 20px;
color: white;
}
.xx-preview-item h4 {
font-size: 14px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 4px;
}
.xx-preview-item p {
font-size: 12px;
color: var(--muted, #64748b);
margin: 0;
}
@@ -1,15 +1,16 @@
/**
* 视频剪辑/生成页面 - V21 完全对标
* 剪辑计划页面 - V21 UI
*/
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { Select, Modal, Form, message } from 'antd';
import './ProjectGeneration.css';
const TITLE_OPTIONS = [
{ id: '1', name: '产品介绍', content: '完整介绍产品功能和优势' },
{ id: '2', name: '用户评价', content: '展示用户真实反馈和评分' },
{ id: '3', name: '功能演示', content: '详细演示产品各项功能' },
const TEMPLATE_OPTIONS = [
{ id: '1', name: '知识科普', content: '适合科普、教育类内容' },
{ id: '2', name: '产品展示', content: '适合电商、产品介绍' },
{ id: '3', name: '生活分享', content: '适合日常Vlog、生活记录' },
{ id: '4', name: '新闻资讯', content: '适合新闻、热点解读' },
];
const ProjectGeneration: React.FC = () => {
@@ -21,12 +22,12 @@ const ProjectGeneration: React.FC = () => {
const handleGenerate = async () => {
const values = await form.validateFields();
try {
message.success('成片生成任务已创建');
message.success('开始生成成片');
setGenerateOpen(false);
form.resetFields();
setSelectedTitle('');
} catch (error: any) {
message.error('创建生成任务失败');
message.error('生成失败');
}
};
@@ -34,44 +35,71 @@ const ProjectGeneration: React.FC = () => {
<div className="xx-project-generation">
<div className="xx-page-head">
<div>
<h2></h2>
<p></p>
<h2></h2>
<p></p>
</div>
<button className="btn primary" onClick={() => setGenerateOpen(true)}>
<button className="xx-primary-btn" onClick={() => setGenerateOpen(true)}>
+
</button>
</div>
<div className="xx-card">
<h3></h3>
<div className="xx-template-grid">
{TITLE_OPTIONS.map((title, idx) => {
const colors = [
'linear-gradient(135deg,#4f46e5,#818cf8)',
'linear-gradient(135deg,#f97316,#ef4444)',
'linear-gradient(135deg,#10b981,#0284c7)',
'linear-gradient(135deg,#64748b,#334155)',
];
return (
<div
key={title.id}
className={`xx-choice ${selectedTitle === title.id ? 'selected' : ''}`}
onClick={() => setSelectedTitle(title.id)}
>
<div className="cover" style={{ background: colors[idx % 4] }}>
📹
<div className="xx-generation-layout">
{/* 模板选择 */}
<div className="xx-card">
<h3>🎨 </h3>
<div className="xx-template-grid">
{TEMPLATE_OPTIONS.map((title, idx) => {
const gradients = ['gradient-1', 'gradient-2', 'gradient-3', 'gradient-4'];
return (
<div
key={title.id}
className={`xx-choice-card ${selectedTitle === title.id ? 'selected' : ''}`}
onClick={() => setSelectedTitle(title.id)}
>
<div className={`xx-template-cover ${gradients[idx % 4]}`}>
{idx + 1}
</div>
<div className="xx-template-info">
<b>{title.name}</b>
<p>{title.content}</p>
</div>
</div>
{selectedTitle === title.id && <div className="check"></div>}
<b>{title.name}</b>
<p>{title.content.substring(0, 50)}...</p>
);
})}
</div>
</div>
{/* 计划预览 */}
<div className="xx-preview-section">
<div className="xx-card">
<h3>📋 </h3>
<div className="xx-preview-item">
<div className="icon">📹</div>
<div>
<h4></h4>
<p> 0 </p>
</div>
);
})}
</div>
<div className="xx-preview-item">
<div className="icon"></div>
<div>
<h4></h4>
<p>{selectedTitle ? TEMPLATE_OPTIONS.find(t => t.id === selectedTitle)?.name : '未选择'}</p>
</div>
</div>
<div className="xx-preview-item">
<div className="icon">🎙</div>
<div>
<h4></h4>
<p></p>
</div>
</div>
</div>
</div>
</div>
<Modal
title="生成成片"
title="开始生成成片"
open={generateOpen}
okText="开始生成"
cancelText="取消"
@@ -84,9 +112,9 @@ const ProjectGeneration: React.FC = () => {
label="选择模板"
rules={[{ required: true, message: '请选择模板' }]}
>
<Select placeholder="选择剪辑模板">
<Select placeholder="选择模板">
<Select.Option value="basic"></Select.Option>
<Select.Option value="product"></Select.Option>
<Select.Option value="product"></Select.Option>
<Select.Option value="lifestyle"></Select.Option>
</Select>
</Form.Item>
@@ -95,7 +123,7 @@ const ProjectGeneration: React.FC = () => {
label="视频时长"
rules={[{ required: true, message: '请选择时长' }]}
>
<Select placeholder="选择时长">
<Select placeholder="选择时长">
<Select.Option value="15">15</Select.Option>
<Select.Option value="30">30</Select.Option>
<Select.Option value="60">60</Select.Option>
+111 -39
View File
@@ -1,103 +1,175 @@
/* V21 成片库 */
/* V21 成片库页面 */
.xx-project-results {
max-width: 1200px;
margin: 0 auto;
}
/* 页面头部 */
.xx-page-head {
margin-bottom: 32px;
}
.xx-page-head h2 {
font-size: 28px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 8px;
}
.xx-page-head p {
font-size: 14px;
color: var(--muted, #64748b);
margin: 0;
}
/* V21 竖屏成片网格 */
.xx-vertical-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.xx-vertical-grid.compact {
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
@media (max-width: 1200px) {
.xx-vertical-grid {
grid-template-columns: repeat(3, 1fr);
}
.xx-vertical-grid.compact {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.xx-vertical-grid {
grid-template-columns: repeat(2, 1fr);
}
.xx-vertical-grid.compact {
}
@media (max-width: 480px) {
.xx-vertical-grid {
grid-template-columns: 1fr;
max-width: 300px;
margin: 0 auto;
}
}
/* V21 竖屏成片卡片 */
.xx-vertical-card {
border: 1px solid #e2e8f0;
background: white;
border: 1px solid var(--line, #e2e8f0);
border-radius: 24px;
padding: 12px;
transition: all 0.2s;
transition: all 0.3s;
cursor: pointer;
}
.xx-vertical-card:hover {
border-color: #4f46e5;
box-shadow: 0 12px 36px rgba(79, 70, 229, 0.12);
border-color: var(--indigo, #4f46e5);
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
transform: translateY(-4px);
}
/* 竖屏缩略图 */
.xx-vertical-thumb {
aspect-ratio: 9 / 16;
border-radius: 18px;
background: linear-gradient(135deg, #0ea5e9, #312e81);
position: relative;
overflow: hidden;
display: grid;
place-items: center;
color: white;
margin-bottom: 10px;
cursor: pointer;
font-size: 32px;
margin-bottom: 12px;
}
.xx-vertical-thumb.orange {
background: linear-gradient(135deg, #f97316, #ef4444);
}
.xx-vertical-thumb.gray {
background: linear-gradient(135deg, #64748b, #334155);
.xx-vertical-thumb.indigo {
background: linear-gradient(135deg, #6366f1, #4f46e5);
}
.xx-vertical-thumb.green {
background: linear-gradient(135deg, #10b981, #0284c7);
}
.xx-vertical-thumb video {
width: 100%;
height: 100%;
object-fit: cover;
.xx-vertical-thumb.purple {
background: linear-gradient(135deg, #8b5cf6, #ec4899);
}
.xx-vertical-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
/* 播放按钮 */
.xx-play-btn {
width: 52px;
height: 52px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
backdrop-filter: blur(4px);
border: 2px solid rgba(255,255,255,0.3);
display: grid;
place-items: center;
font-size: 20px;
color: white;
transition: all 0.2s;
}
.xx-vertical-card:hover .xx-play-btn {
background: rgba(255,255,255,0.3);
transform: scale(1.1);
}
/* 视频信息 */
.xx-vertical-card b {
display: block;
color: #0f172a;
font-weight: 850;
font-size: 14px;
font-weight: 850;
color: var(--slate, #0f172a);
margin-bottom: 6px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.xx-vertical-card p {
margin: 5px 0 0;
color: #64748b;
.xx-vertical-card .meta {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: var(--muted, #64748b);
}
.assist-actions {
display: grid;
gap: 8px;
margin-top: 12px;
grid-template-columns: 1fr 1fr;
.xx-vertical-card .duration {
font-weight: 600;
}
.xx-vertical-card .size {
color: var(--green, #10b981);
}
/* 状态标签 */
.xx-status-pill {
display: inline-block;
font-size: 11px;
font-weight: 850;
padding: 4px 10px;
border-radius: 999px;
margin-bottom: 8px;
}
.xx-status-pill.ready {
background: #dcfce7;
color: #15803d;
}
.xx-status-pill.processing {
background: #fef3c7;
color: #b45309;
}
/* 空状态 */
.xx-empty-state {
text-align: center;
padding: 80px 24px;
color: var(--muted, #64748b);
}
.xx-empty-state p {
font-size: 16px;
margin: 12px 0;
}
+25 -22
View File
@@ -1,5 +1,5 @@
/**
* 成片库页面 - V21 完全对标
* 成片库页面 - V21 UI
*/
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -9,37 +9,40 @@ const ProjectResults: React.FC = () => {
const { id: projectId } = useParams<{ id: string }>();
const [videos] = useState<any[]>([]);
const colors = ['orange', 'indigo', 'green', 'purple'];
return (
<div className="xx-project-results">
<div className="xx-page-head">
<div>
<h2></h2>
<p></p>
</div>
<h2></h2>
<p></p>
</div>
{videos.length === 0 ? (
<div className="xx-empty-state">
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🎬</div>
<p></p>
<div style={{ fontSize: '64px', marginBottom: '16px' }}>🎬</div>
<p></p>
<p style={{ fontSize: '14px', color: '#94a3b8' }}>
</p>
</div>
) : (
<div className="xx-card">
<h3></h3>
<div className="xx-vertical-grid compact">
{videos.map((video: any) => (
<div key={video.id} className="xx-vertical-card">
<div className="xx-vertical-thumb">
<button className="mini-play"></button>
</div>
<div className="row">
<b>{video.name}</b>
<span className="pill ok"></span>
</div>
<p>{video.duration || 0}s · 9:16 · {Math.round(video.file_size / 1024 / 1024)}MB</p>
<div className="xx-vertical-grid">
{videos.map((video, idx) => (
<div key={video.id} className="xx-vertical-card">
<div className={`xx-vertical-thumb ${colors[idx % 4]}`}>
<div className="xx-play-btn"></div>
</div>
))}
</div>
<span className={`xx-status-pill ${video.status || 'ready'}`}>
{video.status === 'processing' ? '⏳ 生成中' : '✓ 就绪'}
</span>
<b>{video.name}</b>
<div className="meta">
<span className="duration">{video.duration || 0}s</span>
<span className="size">{(video.file_size / 1024 / 1024).toFixed(1)} MB</span>
</div>
</div>
))}
</div>
)}
</div>
+145 -16
View File
@@ -1,49 +1,178 @@
/* V21 标题库 */
/* V21 标题库页面 */
.xx-project-titles {
max-width: 1200px;
margin: 0 auto;
}
/* 页面头部 */
.xx-page-head {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 18px;
margin-bottom: 32px;
}
.xx-page-head h2 {
font-size: 28px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 8px;
}
.xx-page-head p {
font-size: 14px;
color: var(--muted, #64748b);
margin: 0;
}
/* V21 按钮 */
.xx-primary-btn {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
color: white !important;
border: none !important;
border-radius: 14px !important;
padding: 12px 24px !important;
font-weight: 850 !important;
box-shadow: 0 14px 26px rgba(79,70,229,0.22) !important;
transition: all 0.2s !important;
cursor: pointer;
}
.xx-primary-btn:hover {
box-shadow: 0 18px 34px rgba(79,70,229,0.28) !important;
transform: translateY(-1px);
}
/* V21 卡片 */
.xx-card {
background: rgba(255,255,255,0.94);
border: 1px solid rgba(226,232,240,0.95);
border-radius: 28px;
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
padding: 28px;
margin-bottom: 24px;
transition: all 0.25s;
}
.xx-card:hover {
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
transform: translateY(-2px);
}
/* 搜索栏 - V21 风格 */
.xx-search-bar {
margin-bottom: 16px;
margin-bottom: 24px;
}
.xx-title-list {
.xx-search-input {
width: 100%;
padding: 14px 20px;
border: 2px solid var(--line, #e2e8f0);
border-radius: 16px;
font-size: 15px;
background: white;
transition: all 0.2s;
outline: none;
}
.xx-search-input:focus {
border-color: var(--indigo, #4f46e5);
box-shadow: 0 0 0 4px rgba(79,70,229,0.1);
}
.xx-search-input::placeholder {
color: var(--muted, #64748b);
}
/* 标题列表网格 */
.xx-title-grid {
display: grid;
gap: 10px;
gap: 16px;
}
/* V21 标题卡片 */
.xx-title-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
border: 1px solid #e2e8f0;
gap: 16px;
padding: 20px;
border: 1px solid var(--line, #e2e8f0);
background: white;
border-radius: 18px;
padding: 13px;
transition: all 0.2s;
border-radius: 20px;
transition: all 0.25s;
cursor: pointer;
}
.xx-title-row:hover {
border-color: #4f46e5;
box-shadow: 0 8px 24px rgba(79, 70, 229, 0.08);
border-color: var(--indigo, #4f46e5);
box-shadow: 0 12px 36px rgba(79,70,229,0.1);
transform: translateX(4px);
}
.xx-title-content {
flex: 1;
}
.xx-title-row b {
display: block;
color: #0f172a;
font-weight: 850;
font-size: 16px;
font-weight: 850;
color: var(--slate, #0f172a);
margin-bottom: 6px;
}
.xx-title-row p {
margin: 5px 0 0;
color: #64748b;
font-size: 13px;
font-size: 14px;
color: var(--muted, #64748b);
margin: 0;
line-height: 1.5;
}
/* 操作按钮 */
.xx-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.xx-action-btn {
width: 40px;
height: 40px;
border-radius: 12px;
background: var(--bg, #f8fafc);
border: 1px solid var(--line, #e2e8f0);
cursor: pointer;
display: grid;
place-items: center;
font-size: 16px;
transition: all 0.2s;
}
.xx-action-btn:hover {
background: var(--indigo-soft, #eef2ff);
border-color: var(--indigo, #4f46e5);
}
.xx-action-btn.copy:hover {
color: var(--indigo, #4f46e5);
}
.xx-action-btn.delete:hover {
color: var(--red, #ef4444);
background: #fee2e2;
border-color: #fecaca;
}
/* 空状态 */
.xx-empty-state {
text-align: center;
padding: 60px 24px;
color: var(--muted, #64748b);
}
.xx-empty-state p {
font-size: 16px;
margin: 12px 0;
}
+21 -14
View File
@@ -1,5 +1,5 @@
/**
* 标题库页面 - V21 完全对标
* 标题库页面 - V21 UI
*/
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
@@ -16,7 +16,7 @@ const ProjectTitles: React.FC = () => {
const handleCreate = async () => {
const values = await form.validateFields();
try {
message.success('标题创建成功');
message.success('创建标题成功');
setCreateOpen(false);
form.resetFields();
} catch (error: any) {
@@ -34,35 +34,42 @@ const ProjectTitles: React.FC = () => {
<div className="xx-page-head">
<div>
<h2></h2>
<p></p>
<p></p>
</div>
<button className="btn primary" onClick={() => setCreateOpen(true)}>
<button className="xx-primary-btn" onClick={() => setCreateOpen(true)}>
+
</button>
</div>
<div className="xx-card">
{/* 搜索栏 */}
<div className="xx-search-bar">
<Input
<input
type="text"
className="xx-search-input"
placeholder="搜索标题..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ padding: '10px 14px', borderRadius: '12px' }}
/>
</div>
{filtered.length === 0 ? (
<div className="xx-empty-state">
<p>{searchText ? '没有找到匹配的标题' : '还没有创建任何标题'}</p>
<div style={{ fontSize: '48px', marginBottom: '16px' }}></div>
<p>{searchText ? '没有找到匹配的标题' : '还没有创建标题'}</p>
</div>
) : (
<div className="xx-title-list">
<div className="xx-title-grid">
{filtered.map((title: any) => (
<div key={title.id} className="xx-title-row">
<div>
<div className="xx-title-content">
<b>{title.name}</b>
<p>{title.content}</p>
</div>
<div className="xx-actions">
<button className="xx-action-btn copy" title="复制">📋</button>
<button className="xx-action-btn delete" title="删除">🗑</button>
</div>
</div>
))}
</div>
@@ -70,7 +77,7 @@ const ProjectTitles: React.FC = () => {
</div>
<Modal
title="创建标题"
title="创建标题"
open={createOpen}
okText="创建"
cancelText="取消"
@@ -80,10 +87,10 @@ const ProjectTitles: React.FC = () => {
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="标题"
rules={[{ required: true, message: '请输入标题' }]}
label="标题名称"
rules={[{ required: true, message: '请输入标题名称' }]}
>
<Input placeholder="例如:产品介绍" />
<Input placeholder="请输入标题名称" />
</Form.Item>
<Form.Item
name="content"
@@ -91,7 +98,7 @@ const ProjectTitles: React.FC = () => {
rules={[{ required: true, message: '请输入标题内容' }]}
>
<Input.TextArea
placeholder="例如:完整介绍产品功能和优势"
placeholder="请输入标题内容"
rows={4}
/>
</Form.Item>
+128 -66
View File
@@ -1,105 +1,167 @@
/* V21 配音库 */
/* V21 配音库页面 */
.xx-project-voices {
max-width: 1200px;
margin: 0 auto;
}
/* 页面头部 */
.xx-page-head {
margin-bottom: 32px;
}
.xx-page-head h2 {
font-size: 28px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 8px;
}
.xx-page-head p {
font-size: 14px;
color: var(--muted, #64748b);
margin: 0;
}
/* V21 卡片 */
.xx-card {
background: rgba(255,255,255,0.94);
border: 1px solid rgba(226,232,240,0.95);
border-radius: 28px;
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
padding: 28px;
margin-bottom: 24px;
transition: all 0.25s;
}
.xx-card h3 {
font-size: 18px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 20px;
}
/* 配音网格 - 双列 */
.xx-voice-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
}
@media (max-width: 1200px) {
.xx-voice-grid {
grid-template-columns: repeat(4, 1fr);
}
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
@media (max-width: 768px) {
.xx-voice-grid {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: 1fr;
}
}
.xx-voice {
text-align: center;
border: 2px solid #e2e8f0;
border-radius: 18px;
/* V21 配音卡片 */
.xx-voice-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
border: 2px solid var(--line, #e2e8f0);
border-radius: 20px;
background: white;
padding: 14px;
cursor: pointer;
transition: all 0.2s;
transition: all 0.25s;
position: relative;
}
.xx-voice:hover {
border-color: #4f46e5;
.xx-voice-card:hover {
border-color: var(--indigo, #4f46e5);
box-shadow: 0 12px 36px rgba(79,70,229,0.12);
transform: translateY(-2px);
}
.xx-voice.selected {
border-color: #4f46e5;
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.14);
/* 选中态 */
.xx-voice-card.selected {
border-color: var(--indigo, #4f46e5);
background: var(--indigo-soft, #eef2ff);
box-shadow: 0 0 0 4px rgba(79,70,229,0.1);
}
.xx-voice.selected::after {
/* 选中标记 */
.xx-voice-card.selected::after {
content: '✓';
position: absolute;
right: -7px;
top: -7px;
width: 26px;
height: 26px;
right: -8px;
top: -8px;
width: 28px;
height: 28px;
border-radius: 50%;
background: #4f46e5;
background: var(--indigo, #4f46e5);
color: white;
display: grid;
place-items: center;
font-weight: 900;
font-size: 14px;
}
.voice-icon {
width: 54px;
height: 54px;
margin: 0 auto 9px;
/* 波形图占位 */
.xx-voice-waveform {
width: 80px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, #6366f1, #4f46e5);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.xx-voice-waveform span {
color: white;
font-size: 20px;
}
/* 音色信息 */
.xx-voice-info {
flex: 1;
}
.xx-voice-info h4 {
font-size: 16px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 6px;
}
.xx-voice-info p {
font-size: 13px;
color: var(--muted, #64748b);
margin: 0;
}
/* 播放按钮 */
.xx-voice-play {
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(79,70,229,0.1);
border: none;
cursor: pointer;
display: grid;
place-items: center;
font-size: 18px;
color: var(--indigo, #4f46e5);
transition: all 0.2s;
flex-shrink: 0;
}
.xx-voice-play:hover {
background: var(--indigo, #4f46e5);
color: white;
font-size: 24px;
background: linear-gradient(135deg, #4f46e5, #6366f1);
transform: scale(1.1);
}
.xx-voice div {
font-weight: 850;
font-size: 14px;
color: #0f172a;
}
.xx-voice small {
display: block;
color: #64748b;
margin-top: 4px;
/* 标签 */
.xx-voice-tag {
display: inline-block;
font-size: 11px;
}
.xx-voice-usage-list {
display: grid;
gap: 10px;
}
.xx-voice-usage-row {
border: 1px solid #e2e8f0;
background: white;
border-radius: 18px;
padding: 13px;
}
.xx-voice-usage-row b {
display: block;
color: #0f172a;
font-weight: 850;
}
.xx-voice-usage-row p {
margin: 5px 0 0;
color: #64748b;
font-size: 12px;
padding: 4px 10px;
border-radius: 999px;
background: var(--indigo-soft, #eef2ff);
color: var(--indigo-dark, #4338ca);
margin-top: 8px;
}
+20 -16
View File
@@ -1,17 +1,17 @@
/**
* 配音库页面 - V21 完全对标
* 配音库页面 - V21 UI
*/
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import './ProjectVoices.css';
const VOICE_OPTIONS = [
{ id: 'female-1', name: '女声1', icon: '👩' },
{ id: 'female-2', name: '女声2', icon: '👩' },
{ id: 'male-1', name: '男声1', icon: '👨' },
{ id: 'male-2', name: '男声2', icon: '👨' },
{ id: 'neutral-1', name: '中性声1', icon: '🎤' },
{ id: 'neutral-2', name: '中性声2', icon: '🎤' },
{ id: 'female-youth', name: '元气少女', desc: '年轻女性,活泼开朗', tag: '热门' },
{ id: 'female-mature', name: '知性姐姐', desc: '成熟女性,温柔亲切', tag: '推荐' },
{ id: 'male-young', name: '阳光男孩', desc: '年轻男性,清新自然', tag: '热门' },
{ id: 'male-mature', name: '磁性大叔', desc: '成熟男性,深沉有力', tag: '' },
{ id: 'neutral-youth', name: '活力童声', desc: '童声配音,活泼可爱', tag: '新品' },
{ id: 'neutral-mature', name: '专业播音', desc: '标准播音腔,端庄大气', tag: '' },
];
const ProjectVoices: React.FC = () => {
@@ -21,24 +21,28 @@ const ProjectVoices: React.FC = () => {
return (
<div className="xx-project-voices">
<div className="xx-page-head">
<div>
<h2></h2>
<p></p>
</div>
<h2></h2>
<p></p>
</div>
<div className="xx-card">
<h3></h3>
<h3>🎙 </h3>
<div className="xx-voice-grid">
{VOICE_OPTIONS.map((voice) => (
<div
key={voice.id}
className={`xx-voice ${selectedVoice === voice.id ? 'selected' : ''}`}
className={`xx-voice-card ${selectedVoice === voice.id ? 'selected' : ''}`}
onClick={() => setSelectedVoice(voice.id)}
>
<div className="voice-icon">{voice.icon}</div>
<div>{voice.name}</div>
<small></small>
<div className="xx-voice-waveform">
<span>🔊</span>
</div>
<div className="xx-voice-info">
<h4>{voice.name}</h4>
<p>{voice.desc}</p>
{voice.tag && <span className="xx-voice-tag">{voice.tag}</span>}
</div>
<button className="xx-voice-play"></button>
</div>
))}
</div>
+118 -14
View File
@@ -1,7 +1,110 @@
/* V21 工作空间列表 */
/* V21 工作空间列表页面 */
.xx-workspace-list {
max-width: 1200px;
margin: 0 auto;
}
/* Hero 区域 - V21 设计 */
.xx-hero {
text-align: center;
padding: 60px 24px 80px;
position: relative;
}
.xx-hero::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(79,70,229,0.15), transparent 70%);
border-radius: 50%;
filter: blur(40px);
}
.xx-hero h1 {
font-size: 42px;
font-weight: 900;
color: var(--slate, #0f172a);
margin: 0 0 16px;
letter-spacing: -0.02em;
position: relative;
}
.xx-hero p {
font-size: 18px;
color: var(--muted, #64748b);
margin: 0 0 32px;
max-width: 560px;
margin-left: auto;
margin-right: auto;
line-height: 1.6;
}
/* 功能卡片网格 - V21 */
.xx-features-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 60px;
}
@media (max-width: 1200px) {
.xx-features-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.xx-features-grid {
grid-template-columns: 1fr;
}
}
.xx-feature-card {
background: rgba(255,255,255,0.94);
border: 1px solid rgba(226,232,240,0.95);
border-radius: 24px;
padding: 28px;
text-align: center;
transition: all 0.25s;
cursor: pointer;
}
.xx-feature-card:hover {
transform: translateY(-4px);
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
border-color: var(--indigo, #4f46e5);
}
.xx-feature-icon {
width: 56px;
height: 56px;
margin: 0 auto 16px;
background: linear-gradient(135deg, #6366f1, #4f46e5);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.xx-feature-card h3 {
font-size: 16px;
font-weight: 850;
color: var(--slate, #0f172a);
margin: 0 0 8px;
}
.xx-feature-card p {
font-size: 13px;
color: var(--muted, #64748b);
margin: 0;
}
/* 页面头部 */
.xx-page-head {
display: flex;
justify-content: space-between;
@@ -13,19 +116,19 @@
.xx-page-head h2 {
font-size: 28px;
margin: 0 0 6px;
color: #0f172a;
color: var(--slate, #0f172a);
}
.xx-page-head p {
margin: 0;
color: #64748b;
color: var(--muted, #64748b);
font-size: 14px;
}
.xx-grid4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
gap: 16px;
}
@media (max-width: 1200px) {
@@ -47,23 +150,24 @@
}
.xx-workspace-card {
border: 1px solid #e2e8f0;
border: 1px solid var(--line, #e2e8f0);
background: rgba(255, 255, 255, 0.94);
border-radius: 24px;
padding: 18px;
cursor: pointer;
transition: all 0.2s;
transition: all 0.25s;
}
.xx-workspace-card:hover {
border-color: #4f46e5;
box-shadow: 0 12px 36px rgba(79, 70, 229, 0.12);
border-color: var(--indigo, #4f46e5);
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
transform: translateY(-2px);
}
.xx-workspace-name {
font-size: 16px;
font-weight: 850;
color: #0f172a;
color: var(--slate, #0f172a);
margin-bottom: 12px;
overflow: hidden;
text-overflow: ellipsis;
@@ -74,10 +178,10 @@
display: inline-block;
font-size: 12px;
font-weight: 850;
padding: 4px 8px;
border-radius: 8px;
background: #eef2ff;
color: #4338ca;
padding: 4px 10px;
border-radius: 10px;
background: var(--indigo-soft, #eef2ff);
color: var(--indigo-dark, #4338ca);
margin-bottom: 12px;
}
@@ -89,7 +193,7 @@
.xx-empty-state {
text-align: center;
padding: 60px 24px;
color: #64748b;
color: var(--muted, #64748b);
}
.xx-empty-state p {
+38 -12
View File
@@ -1,5 +1,5 @@
/**
* 工作空间列表页面 - V21 严格对标
* 工作空间列表页面 - V21 UI
*/
import React from 'react';
import { Button, Modal, Form, Input, message } from 'antd';
@@ -20,27 +20,53 @@ const WorkspaceList: React.FC = () => {
const values = await form.validateFields();
try {
const workspace = await createWorkspaceMutation.mutateAsync({ name: values.name });
message.success('工作空间创建成功');
message.success('创建工作空间成功');
setCreateOpen(false);
form.resetFields();
navigate(`/workspaces/${workspace.id}`);
} catch (createError: any) {
message.error(createError.response?.data?.detail || '工作空间创建失败');
message.error(createError.response?.data?.detail || '创建失败');
}
};
// 功能卡片数据
const features = [
{ id: 'assets', icon: '🎬', title: '素材管理', desc: '上传管理视频素材' },
{ id: 'titles', icon: '✍️', title: '标题库', desc: '创作吸引眼球的标题' },
{ id: 'voices', icon: '🎙️', title: '配音库', desc: '选择优质配音音色' },
{ id: 'generation', icon: '✨', title: '剪辑计划', desc: 'AI智能生成成片' },
];
return (
<div className="xx-workspace-list">
<div className="xx-page-head">
<div>
<h2></h2>
<p></p>
</div>
{/* Hero 区域 - V21 设计 */}
<div className="xx-hero">
<h1> SaaS</h1>
<p>AI视频剪辑平台</p>
<Button type="primary" size="large" onClick={openCreateModal}>
+
</Button>
</div>
{/* 功能卡片 - V21 设计 */}
<div className="xx-features-grid">
{features.map((feature) => (
<div key={feature.id} className="xx-feature-card">
<div className="xx-feature-icon">{feature.icon}</div>
<h3>{feature.title}</h3>
<p>{feature.desc}</p>
</div>
))}
</div>
{/* 工作空间列表 */}
<div className="xx-page-head">
<div>
<h2></h2>
<p></p>
</div>
</div>
<div className="xx-grid4">
{workspaces?.map((workspace) => (
<div
@@ -61,10 +87,10 @@ const WorkspaceList: React.FC = () => {
{!isLoading && workspaces?.length === 0 && (
<div className="xx-empty-state">
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📦</div>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🚀</div>
<p></p>
<Button type="primary" onClick={openCreateModal}>
+
</Button>
</div>
)}
@@ -84,10 +110,10 @@ const WorkspaceList: React.FC = () => {
label="工作空间名称"
rules={[
{ required: true, message: '请输入工作空间名称' },
{ max: 50, message: '最多 50 个字符' },
{ max: 50, message: '名称不能超过50个字符' },
]}
>
<Input placeholder="例如:我的剪辑工作室" autoFocus />
<Input placeholder="请输入工作空间名称" autoFocus />
</Form.Item>
</Form>
</Modal>
+125 -28
View File
@@ -1,29 +1,43 @@
/**
* 全局样式变量
* V21 设计系统 - 全局样式
*/
:root {
/* 主色调 */
--primary-color: #1890ff;
--primary-hover: #40a9ff;
--primary-active: #096dd9;
--primary-color: #4f46e5;
--primary-hover: #6366f1;
--primary-active: #4338ca;
--primary-soft: #eef2ff;
/* 状态色 */
--success-color: #52c41a;
--warning-color: #faad14;
--error-color: #ff4d4f;
--info-color: #1890ff;
--success-color: #10b981;
--warning-color: #f59e0b;
--error-color: #ef4444;
--info-color: #4f46e5;
/* 文字色 */
--text-primary: #262626;
--text-secondary: #595959;
/* 文字色 */
--text-primary: #0f172a;
--text-secondary: #64748b;
--text-disabled: #bfbfbf;
/* 背景色 */
--bg-primary: #ffffff;
--bg-primary: #f8fafc;
--bg-secondary: #fafafa;
--bg-tertiary: #f5f5f5;
/* 边框和分割线 */
/* V21 设计令牌 */
--indigo: #4f46e5;
--indigo-dark: #4338ca;
--indigo-soft: #eef2ff;
--gradient-primary: linear-gradient(135deg, #6366f1, #4f46e5);
--slate: #0f172a;
--muted: #64748b;
--line: #e2e8f0;
--bg: #f8fafc;
--green: #10b981;
--amber: #f59e0b;
--red: #ef4444;
/* 边框色 */
--border-color: #d9d9d9;
--divider-color: #f0f0f0;
@@ -34,18 +48,102 @@
--space-lg: 24px;
--space-xl: 32px;
/* 圆角 */
--radius-sm: 2px;
--radius-md: 4px;
--radius-lg: 8px;
/* 圆角 - V21 大圆角 */
--radius-sm: 12px;
--radius-md: 16px;
--radius-lg: 22px;
--radius-xl: 28px;
/* 阴影 */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08);
--shadow-md: 0 4px 8px rgba(0, 0, 0, 0.12);
--shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.16);
/* 阴影 - V21 柔和大阴影 */
--shadow-sm: 0 10px 30px rgba(15,23,42,0.06);
--shadow-md: 0 20px 58px rgba(15,23,42,0.10);
--shadow-lg: 0 24px 70px rgba(15,23,42,0.09);
--shadow-hover: 0 26px 64px rgba(15,23,42,0.14);
}
/* 全局样式重置 */
/**
* V21 按钮样式
*/
.btn.primary,
.ant-btn-primary {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
color: white !important;
border-radius: 14px !important;
font-weight: 850 !important;
box-shadow: 0 14px 26px rgba(79,70,229,0.22) !important;
border: none !important;
padding: 12px 22px !important;
transition: all 0.2s !important;
}
.btn.primary:hover,
.ant-btn-primary:hover {
box-shadow: 0 18px 34px rgba(79,70,229,0.28) !important;
transform: translateY(-1px);
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
}
.btn.ghost,
.ant-btn-default {
background: white !important;
border: 1px solid #dbe3ef !important;
border-radius: 14px !important;
font-weight: 850 !important;
padding: 12px 22px !important;
transition: all 0.2s !important;
}
.btn.ghost:hover,
.ant-btn-default:hover {
border-color: #4f46e5 !important;
color: #4f46e5 !important;
}
/**
* V21 卡片样式
*/
.xx-card {
background: rgba(255,255,255,0.94);
border: 1px solid rgba(226,232,240,0.95);
border-radius: 28px;
box-shadow: 0 24px 70px rgba(15,23,42,0.09);
padding: 28px;
transition: all 0.25s;
}
.xx-card:hover {
box-shadow: 0 26px 64px rgba(15,23,42,0.14);
transform: translateY(-2px);
}
/**
* V21 背景装饰
*/
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background:
radial-gradient(circle at 18% 20%, rgba(79,70,229,0.11), transparent 30%),
radial-gradient(circle at 86% 14%, rgba(16,185,129,0.08), transparent 28%),
radial-gradient(circle at 48% 80%, rgba(99,102,241,0.08), transparent 34%),
#f8fafc;
}
body::after {
content: '';
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background-image: radial-gradient(circle, rgba(79,70,229,0.1) 1px, transparent 1px);
background-size: 38px 38px;
opacity: 0.32;
}
/* 全局重置 */
* {
margin: 0;
padding: 0;
@@ -53,13 +151,12 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 滚动条样式 */
/* 滚动条 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
@@ -74,7 +171,7 @@ body {
background: rgba(0, 0, 0, 0.3);
}
/* 通用工具类 */
/* 工具类 */
.text-center {
text-align: center;
}
@@ -107,7 +204,7 @@ body {
margin-bottom: var(--space-lg);
}
/* 卡片hover效果 */
/* 卡片悬浮效果 */
.card-hover {
transition: all 0.3s;
cursor: pointer;
@@ -118,7 +215,7 @@ body {
box-shadow: var(--shadow-lg);
}
/* 选中的套餐卡片 */
/* 选中 */
.selected-plan {
border: 2px solid var(--primary-color) !important;
box-shadow: var(--shadow-md);