fix(ui): 修正一键生成页面偏差,补充视频预览、时间线、克隆声音区域
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Validate Code Quality And Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
Deploy / Deploy Staging (push) Has been cancelled
Deploy / Build Production Runtime Images (push) Has been cancelled
Deploy / Deploy Production (push) Has been cancelled
Deploy / Production Browser E2E (push) Has been cancelled
This commit is contained in:
@@ -5,8 +5,6 @@
|
||||
*/
|
||||
import React, { useState, useRef, useCallback } from "react";
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Typography,
|
||||
Collapse,
|
||||
Progress,
|
||||
@@ -28,7 +26,6 @@ import {
|
||||
PauseCircleOutlined,
|
||||
DownloadOutlined,
|
||||
ShareAltOutlined,
|
||||
ReloadOutlined,
|
||||
InboxOutlined,
|
||||
SoundOutlined,
|
||||
FormOutlined,
|
||||
@@ -147,6 +144,37 @@ const MOCK_VOICES: VoiceItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/* ── 克隆声音 Mock ── */
|
||||
interface ClonedVoice {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
const MOCK_CLONED_VOICES: ClonedVoice[] = [
|
||||
{ id: "cv1", name: "我的声音 v1", createdAt: "2026-06-20", duration: "45s" },
|
||||
{ id: "cv2", name: "我的声音 v2", createdAt: "2026-06-25", duration: "30s" },
|
||||
];
|
||||
|
||||
/* ── 时间线 Mock ── */
|
||||
interface TimelineScene {
|
||||
scene: string;
|
||||
time: string;
|
||||
duration: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const MOCK_TIMELINE: TimelineScene[] = [
|
||||
{ scene: "开场 - 品牌 Logo 展示", time: "0:00 - 0:05", duration: 5, color: "#6366f1" },
|
||||
{ scene: "产品特写 - 细节展示", time: "0:05 - 0:15", duration: 10, color: "#818cf8" },
|
||||
{ scene: "使用场景 - 生活化演绎", time: "0:15 - 0:30", duration: 15, color: "#10b981" },
|
||||
{ scene: "卖点字幕 - 核心优势", time: "0:30 - 0:40", duration: 10, color: "#f59e0b" },
|
||||
{ scene: "结尾 CTA - 引导关注", time: "0:40 - 0:50", duration: 10, color: "#ef4444" },
|
||||
];
|
||||
|
||||
const TOTAL_DURATION = MOCK_TIMELINE.reduce((sum, t) => sum + t.duration, 0);
|
||||
|
||||
/* ── 常量 ── */
|
||||
|
||||
interface RatioOption {
|
||||
@@ -218,9 +246,14 @@ const GeneratePage: React.FC = () => {
|
||||
/* ── 配音 ── */
|
||||
const [selectedVoice, setSelectedVoice] = useState<string>("");
|
||||
const [playingVoice, setPlayingVoice] = useState<string | null>(null);
|
||||
const [voiceMode, setVoiceMode] = useState<"preset" | "custom">("preset");
|
||||
const [voiceMode, setVoiceMode] = useState<"preset" | "custom" | "clone">("preset");
|
||||
const [customVoiceText, setCustomVoiceText] = useState("");
|
||||
|
||||
/* ── 克隆声音 ── */
|
||||
const [cloneFileList, setCloneFileList] = useState<UploadFile[]>([]);
|
||||
const [selectedClonedVoice, setSelectedClonedVoice] = useState<string>("");
|
||||
const [cloning, setCloning] = useState(false);
|
||||
|
||||
/* ── 标题/文案 ── */
|
||||
const [titleMode, setTitleMode] = useState<"ai" | "manual">("ai");
|
||||
const [manualCopy, setManualCopy] = useState("");
|
||||
@@ -473,6 +506,7 @@ const GeneratePage: React.FC = () => {
|
||||
>
|
||||
<Radio.Button value="preset">系统预设</Radio.Button>
|
||||
<Radio.Button value="custom">自定义录制</Radio.Button>
|
||||
<Radio.Button value="clone">克隆我的声音</Radio.Button>
|
||||
</Radio.Group>
|
||||
|
||||
{voiceMode === "preset" ? (
|
||||
@@ -528,7 +562,7 @@ const GeneratePage: React.FC = () => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
) : voiceMode === "custom" ? (
|
||||
<div>
|
||||
<TextArea
|
||||
placeholder="输入配音文案,系统将自动合成语音…"
|
||||
@@ -544,6 +578,104 @@ const GeneratePage: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* ── 克隆我的声音 ── */
|
||||
<div className="xx-clone-voice-section">
|
||||
<Typography.Title level={5} className="xx-clone-voice-title">
|
||||
克隆我的声音
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph className="xx-clone-voice-desc">
|
||||
上传一段您的语音样本,AI 将克隆您的声音用于视频配音
|
||||
</Typography.Paragraph>
|
||||
|
||||
{/* 上传区域 */}
|
||||
<Upload.Dragger
|
||||
accept=".wav,.mp3,audio/*"
|
||||
fileList={cloneFileList}
|
||||
onChange={({ fileList: fl }) => setCloneFileList(fl)}
|
||||
beforeUpload={() => false}
|
||||
maxCount={1}
|
||||
className="xx-clone-upload-area"
|
||||
>
|
||||
<div className="xx-clone-upload-icon">
|
||||
<AudioOutlined />
|
||||
</div>
|
||||
<Typography.Paragraph className="xx-clone-upload-text">
|
||||
点击或拖拽音频文件到此区域
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="xx-clone-upload-hint">
|
||||
支持 WAV / MP3 格式,建议时长 30 秒以上
|
||||
</Typography.Paragraph>
|
||||
</Upload.Dragger>
|
||||
|
||||
{/* 开始克隆按钮 */}
|
||||
<div style={{ marginTop: 16, textAlign: "center" }}>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
loading={cloning}
|
||||
onClick={() => {
|
||||
if (cloneFileList.length === 0) {
|
||||
message.warning("请先上传音频文件");
|
||||
return;
|
||||
}
|
||||
setCloning(true);
|
||||
setTimeout(() => {
|
||||
setCloning(false);
|
||||
message.success("声音克隆完成!");
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
开始克隆
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 已克隆声音列表 */}
|
||||
{MOCK_CLONED_VOICES.length > 0 && (
|
||||
<div className="xx-clone-voices-list">
|
||||
<Typography.Paragraph className="xx-clone-voices-label">
|
||||
已克隆的声音
|
||||
</Typography.Paragraph>
|
||||
<div className="xx-voice-grid">
|
||||
{MOCK_CLONED_VOICES.map((cv) => (
|
||||
<div
|
||||
key={cv.id}
|
||||
className={`xx-voice-card ${selectedClonedVoice === cv.id ? "xx-voice-card-selected" : ""}`}
|
||||
onClick={() => setSelectedClonedVoice(cv.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={selectedClonedVoice === cv.id}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
setSelectedClonedVoice(cv.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="xx-voice-header">
|
||||
<div className="xx-voice-avatar">
|
||||
<AudioOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<Typography.Paragraph className="xx-voice-name">
|
||||
{cv.name}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="xx-voice-desc">
|
||||
{cv.createdAt} · {cv.duration}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提示 */}
|
||||
<Typography.Paragraph className="xx-clone-tip">
|
||||
💡 提示:录音环境越安静,克隆效果越好。建议使用专业麦克风录制。
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -773,91 +905,127 @@ const GeneratePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ════════════ 右侧:结果面板 ════════════ */}
|
||||
{/* ════════════ 右侧:预览面板 ════════════ */}
|
||||
<div>
|
||||
<Card className="xx-generate-section" style={{ marginBottom: 0 }}>
|
||||
<Card className="xx-generate-section" style={{ marginBottom: 0, position: "sticky", top: 24 }}>
|
||||
<div
|
||||
className="xx-section-header"
|
||||
style={{ padding: "16px 24px" }}
|
||||
>
|
||||
<Typography.Title level={5} className="xx-section-title">
|
||||
生成结果
|
||||
预览
|
||||
</Typography.Title>
|
||||
{generated && <Tag variant="success">已完成</Tag>}
|
||||
</div>
|
||||
<div className="xx-section-body">
|
||||
{generated ? (
|
||||
<>
|
||||
<div className="xx-result-preview">
|
||||
<PlayCircleOutlined />
|
||||
{/* ── 视频预览播放器 ── */}
|
||||
<div className="xx-video-preview">
|
||||
<div className="xx-video-preview-overlay" />
|
||||
{generated ? (
|
||||
<video
|
||||
src=""
|
||||
controls
|
||||
preload="none"
|
||||
poster=""
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
/>
|
||||
) : (
|
||||
<div className="xx-video-preview-placeholder">
|
||||
<button className="xx-video-play-btn" type="button">
|
||||
<PlayCircleOutlined />
|
||||
</button>
|
||||
<Text style={{ color: "rgba(255,255,255,0.6)", fontSize: "var(--font-size-sm)" }}>
|
||||
预览视频
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Row gutter={[8, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col span={12}>
|
||||
<Text type="secondary">标题</Text>
|
||||
<br />
|
||||
<Text strong>{title || "未设置"}</Text>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text type="secondary">比例</Text>
|
||||
<br />
|
||||
<Text strong>{videoRatio}</Text>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text type="secondary">时长</Text>
|
||||
<br />
|
||||
<Text strong>{duration}s</Text>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text type="secondary">素材</Text>
|
||||
<br />
|
||||
<Text strong>{selectedMaterials.length} 个</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* ── 标题预览 ── */}
|
||||
<div
|
||||
style={{
|
||||
background: "var(--bg-secondary)",
|
||||
color: "var(--text-primary)",
|
||||
borderRadius: "var(--radius-sm)",
|
||||
padding: "10px",
|
||||
textAlign: "center",
|
||||
fontWeight: 600,
|
||||
fontSize: "var(--font-size-base)",
|
||||
marginBottom: 14,
|
||||
}}
|
||||
>
|
||||
{title || "视频标题预览"}
|
||||
</div>
|
||||
|
||||
<div className="xx-result-actions">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
block
|
||||
>
|
||||
下载视频
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
icon={<ShareAltOutlined />}
|
||||
block
|
||||
>
|
||||
分享
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="text"
|
||||
icon={<ReloadOutlined />}
|
||||
block
|
||||
onClick={() => {
|
||||
setGenerated(false);
|
||||
setProgress(0);
|
||||
{/* ── 时间线预览 ── */}
|
||||
<div className="xx-timeline">
|
||||
<div className="xx-timeline-header">
|
||||
<Typography.Paragraph className="xx-timeline-title">
|
||||
剪辑计划预览
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="xx-timeline-duration">
|
||||
总时长 {TOTAL_DURATION}s
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 时间线轨道 */}
|
||||
<div className="xx-timeline-track">
|
||||
{MOCK_TIMELINE.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="xx-timeline-clip"
|
||||
style={{
|
||||
width: `${(item.duration / TOTAL_DURATION) * 100}%`,
|
||||
background: item.color,
|
||||
}}
|
||||
>
|
||||
重新生成
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: "center",
|
||||
padding: "40px 0",
|
||||
color: "var(--text-tertiary)",
|
||||
}}
|
||||
{item.duration >= 10 ? `${item.duration}s` : ""}
|
||||
</div>
|
||||
))}
|
||||
{/* 播放头 */}
|
||||
<div className="xx-timeline-playhead" style={{ left: "20%" }} />
|
||||
</div>
|
||||
|
||||
{/* 场景列表 */}
|
||||
<div className="xx-timeline-list">
|
||||
{MOCK_TIMELINE.map((item, idx) => (
|
||||
<div key={idx} className="xx-timeline-item">
|
||||
<div className="xx-timeline-item-index">{idx + 1}</div>
|
||||
<span className="xx-timeline-item-scene">{item.scene}</span>
|
||||
<span className="xx-timeline-item-time">{item.time}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 操作按钮 ── */}
|
||||
<div className="xx-result-actions">
|
||||
<Button
|
||||
buttonType="primary"
|
||||
icon={<ThunderboltOutlined />}
|
||||
block
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
>
|
||||
<ThunderboltOutlined
|
||||
style={{ fontSize: 48, marginBottom: 16, opacity: 0.4 }}
|
||||
/>
|
||||
<br />
|
||||
<Text type="secondary">
|
||||
{generating ? "视频生成中…" : "配置参数后点击生成"}
|
||||
</Text>
|
||||
{generated ? "重新生成" : "开始生成"}
|
||||
</Button>
|
||||
</div>
|
||||
{generated && (
|
||||
<div className="xx-result-actions">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
icon={<DownloadOutlined />}
|
||||
block
|
||||
>
|
||||
下载视频
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
icon={<ShareAltOutlined />}
|
||||
block
|
||||
>
|
||||
分享
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
============================================================ */
|
||||
.xx-generate-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 380px;
|
||||
grid-template-columns: 1fr 320px;
|
||||
gap: var(--space-xl);
|
||||
align-items: start;
|
||||
}
|
||||
@@ -359,24 +359,264 @@
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
结果区域
|
||||
视频预览播放器
|
||||
============================================================ */
|
||||
.xx-result-preview {
|
||||
border-radius: var(--radius-lg);
|
||||
.xx-video-preview {
|
||||
aspect-ratio: 9 / 16;
|
||||
max-height: 400px;
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, #111827, #312e81);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.xx-video-preview video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.xx-video-preview-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 72% 28%, rgba(255, 255, 255, 0.2), transparent 40%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xx-video-preview-placeholder {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.xx-video-play-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: var(--radius-full);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(8px);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
font-size: var(--font-size-2xl);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-all);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.xx-video-play-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
时间线预览
|
||||
============================================================ */
|
||||
.xx-timeline {
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.xx-timeline-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.xx-timeline-title {
|
||||
font-size: var(--font-size-sm) !important;
|
||||
font-weight: var(--font-weight-semibold) !important;
|
||||
color: var(--text-primary) !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.xx-timeline-duration {
|
||||
font-size: var(--font-size-xs) !important;
|
||||
color: var(--text-tertiary) !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.xx-timeline-track {
|
||||
display: flex;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
background: var(--bg-tertiary);
|
||||
height: 200px;
|
||||
margin-bottom: var(--space-sm);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xx-timeline-clip {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-4xl);
|
||||
margin-bottom: var(--space-md);
|
||||
font-size: var(--font-size-xs);
|
||||
color: #fff;
|
||||
font-weight: var(--font-weight-medium);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 4px;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.xx-timeline-clip:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.xx-timeline-playhead {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background: #ef4444;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.xx-timeline-playhead::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: var(--radius-full);
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.xx-timeline-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.xx-timeline-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.xx-timeline-item-index {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: var(--radius-xs);
|
||||
background: var(--primary-soft);
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-xs);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.xx-timeline-item-scene {
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xx-timeline-item-time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
克隆声音区域
|
||||
============================================================ */
|
||||
.xx-clone-voice-section {
|
||||
margin-top: var(--space-md);
|
||||
padding: var(--space-lg);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.xx-clone-voice-title {
|
||||
font-size: var(--font-size-base) !important;
|
||||
font-weight: var(--font-weight-semibold) !important;
|
||||
color: var(--text-primary) !important;
|
||||
margin: 0 0 var(--space-sm) !important;
|
||||
}
|
||||
|
||||
.xx-clone-voice-desc {
|
||||
font-size: var(--font-size-sm) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
margin: 0 0 var(--space-md) !important;
|
||||
}
|
||||
|
||||
.xx-clone-upload-area {
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
text-align: center;
|
||||
background: var(--bg-secondary);
|
||||
cursor: pointer;
|
||||
transition: var(--transition-all);
|
||||
}
|
||||
|
||||
.xx-clone-upload-area:hover {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--primary-soft);
|
||||
}
|
||||
|
||||
.xx-clone-upload-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.xx-clone-upload-text {
|
||||
font-size: var(--font-size-sm) !important;
|
||||
color: var(--text-primary) !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.xx-clone-upload-hint {
|
||||
font-size: var(--font-size-xs) !important;
|
||||
color: var(--text-tertiary) !important;
|
||||
margin: var(--space-xs) 0 0 !important;
|
||||
}
|
||||
|
||||
.xx-clone-tip {
|
||||
font-size: var(--font-size-xs) !important;
|
||||
color: var(--text-tertiary) !important;
|
||||
background: var(--bg-secondary);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-sm);
|
||||
margin: var(--space-md) 0 0 !important;
|
||||
}
|
||||
|
||||
.xx-clone-voices-list {
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
.xx-clone-voices-label {
|
||||
font-size: var(--font-size-sm) !important;
|
||||
font-weight: var(--font-weight-semibold) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
margin: 0 0 var(--space-sm) !important;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
结果区域
|
||||
============================================================ */
|
||||
.xx-result-actions {
|
||||
display: flex;
|
||||
gap: var(--space-sm);
|
||||
margin-top: var(--space-md);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
Reference in New Issue
Block a user