| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446 |
- <template>
- <view class="page">
- <!-- 顶部导航栏 -->
- <view class="nav-bar">
- <view class="nav-content">
- <text class="page-title">生成音频</text>
- </view>
- </view>
- <!-- 主内容区 -->
- <view class="main-content">
- <!-- 文本输入区 -->
- <view class="card input-card">
- <view class="card-header">
- <text class="card-title">输入文本</text>
- <text class="word-count" :class="{ warning: text.length > 1800, error: text.length >= 2000 }">
- {{ text.length }}/2000 字
- </text>
- </view>
- <textarea
- v-model="text"
- class="text-input"
- placeholder="请输入要转换的文本内容..."
- :maxlength="2000"
- auto-height
- />
- <view class="input-actions">
- <button class="action-btn ai-generate-btn" @click="goToAIGenerate">
- <text>🤖 AI 生成内容</text>
- </button>
- <button class="action-btn" @click="clearText">清空</button>
- <button class="action-btn" @click="pasteText">粘贴</button>
- </view>
- <view class="input-hint">
- <text>支持长按粘贴文本内容</text>
- </view>
- </view>
- <!-- 音色选择 - 网格布局 -->
- <view class="card voice-card">
- <text class="card-title">选择音色</text>
- <view class="voice-grid">
- <view
- v-for="voice in audioStore.voices"
- :key="voice.id"
- class="voice-item"
- :class="{ active: selectedVoice === voice.id }"
- @click="selectedVoice = voice.id"
- >
- <view class="voice-icon">
- <text>{{ voice.gender === 'female' ? '👩' : '👨' }}</text>
- </view>
- <text class="voice-name">{{ voice.name }}</text>
- <view class="voice-preview-btn" @click.stop="previewVoice(voice.id)">
- <text v-if="previewingVoice !== voice.id">▶</text>
- <text v-else>⏹</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 参数调节 -->
- <view class="card params-card">
- <text class="card-title">参数调节</text>
- <view class="param-item">
- <view class="param-header">
- <text class="param-label">语速</text>
- <view class="param-value">{{ voiceParams.speed.toFixed(1) }}x</view>
- </view>
- <slider
- :value="(voiceParams.speed - 0.5) * 100 / 1.5"
- :min="0"
- :max="100"
- @changing="(e: any) => voiceParams.speed = Number((0.5 + (e.detail.value / 100) * 1.5).toFixed(1))"
- activeColor="#4F46E5"
- backgroundColor="#e5e7eb"
- block-size="20"
- />
- <view class="param-range">
- <text>0.5x</text>
- <text>2.0x</text>
- </view>
- </view>
- <view class="param-item">
- <view class="param-header">
- <text class="param-label">音调</text>
- <view class="param-value">{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</view>
- </view>
- <slider
- :value="(voiceParams.pitch + 500) / 10"
- :min="0"
- :max="100"
- @changing="(e: any) => voiceParams.pitch = Math.round(e.detail.value * 10 - 500)"
- activeColor="#4F46E5"
- backgroundColor="#e5e7eb"
- block-size="20"
- />
- <view class="param-range">
- <text>-500</text>
- <text>+500</text>
- </view>
- </view>
- <view class="param-item">
- <view class="param-header">
- <text class="param-label">音量</text>
- <view class="param-value">{{ voiceParams.volume }}%</view>
- </view>
- <slider
- :value="voiceParams.volume"
- :min="0"
- :max="100"
- @changing="(e: any) => voiceParams.volume = e.detail.value"
- activeColor="#4F46E5"
- backgroundColor="#e5e7eb"
- block-size="20"
- />
- <view class="param-range">
- <text>0%</text>
- <text>100%</text>
- </view>
- </view>
- </view>
- <!-- 专辑选择 -->
- <view class="card album-card">
- <view class="card-header-row">
- <text class="card-title">📚 所属专辑</text>
- <text class="album-add-btn" @click="showAlbumPanel = true">+ 新建专辑</text>
- </view>
- <view class="album-selector" @click="showAlbumListPanel = true">
- <text v-if="selectedAlbum" class="album-name">{{ selectedAlbum.title }}</text>
- <text v-else class="album-placeholder">点击选择专辑(可选)</text>
- <text class="album-arrow">›</text>
- </view>
- </view>
- <!-- 生成按钮 -->
- <button
- class="generate-btn"
- :disabled="!canGenerate"
- @click="handleGenerate"
- @touchstart="btnPressed = true"
- @touchend="btnPressed = false"
- :class="{ pressed: btnPressed }"
- >
- <text class="btn-text">{{ generating ? '生成中...' : '🎵 生成音频' }}</text>
- </button>
- <!-- 视频生成入口 -->
- <view class="video-entry-card" @click="goToVideoGenerator">
- <view class="video-entry-content">
- <text class="video-entry-icon">🎬</text>
- <view class="video-entry-text">
- <text class="video-entry-title">视频生成</text>
- <text class="video-entry-desc">图片 + 音频 = 精美视频</text>
- </view>
- </view>
- <text class="video-entry-arrow">→</text>
- </view>
- </view>
- <!-- 生成成功弹窗 -->
- <view v-if="showSuccessModal" class="success-modal" @click="closeSuccessModal">
- <view class="success-content" @click.stop>
- <!-- 成功动画 -->
- <view class="success-animation">
- <view class="success-circle">
- <text class="success-checkmark">✓</text>
- </view>
- <view class="success-sparkles">
- <text class="sparkle">✨</text>
- <text class="sparkle">🌟</text>
- <text class="sparkle">✨</text>
- </view>
- </view>
- <text class="success-title">生成成功!</text>
- <text class="success-desc">音频已准备就绪</text>
- <!-- 操作按钮 -->
- <view class="success-actions">
- <button class="success-btn play-btn" @click="goToPlayer">
- <text>▶ 立即播放</text>
- </button>
- <button class="success-btn share-btn" @click="shareAudio">
- <text>📤 分享给好友</text>
- </button>
- <button class="success-btn close-btn" @click="closeSuccessModal">
- <text>继续生成</text>
- </button>
- </view>
- </view>
- </view>
- <!-- 新手引导 -->
- <view v-if="showGuide" class="guide-overlay">
- <view class="guide-content">
- <view class="guide-header">
- <text class="guide-step">步骤 {{ guideStep }} / {{ guideSteps.length }}</text>
- </view>
- <view class="guide-body">
- <text class="guide-title">{{ guideSteps[guideStep - 1]?.title }}</text>
- <text class="guide-desc">{{ guideSteps[guideStep - 1]?.desc }}</text>
- </view>
- <view class="guide-dots">
- <view
- v-for="i in guideSteps.length"
- :key="i"
- class="guide-dot"
- :class="{ active: i === guideStep }"
- />
- </view>
- <view class="guide-actions">
- <button v-if="guideStep > 1" class="guide-btn skip" @click="closeGuide">
- <text>跳过</text>
- </button>
- <button v-if="guideStep < guideSteps.length" class="guide-btn next" @click="nextGuideStep">
- <text>下一步</text>
- </button>
- <button v-else class="guide-btn finish" @click="closeGuide">
- <text>开始使用</text>
- </button>
- </view>
- </view>
- </view>
- <!-- 额度用尽弹窗 -->
- <view v-if="showQuotaModal" class="quota-modal" @click="showQuotaModal = false">
- <view class="quota-content" @click.stop>
- <view class="quota-icon">👑</view>
- <text class="quota-title">今日免费额度已用完</text>
- <text class="quota-desc">开通会员解锁更多生成次数</text>
- <view class="quota-actions">
- <button class="quota-btn primary" @click="goToMember">
- <text>开通会员</text>
- </button>
- <button class="quota-btn secondary" @click="showQuotaModal = false">
- <text>稍后再说</text>
- </button>
- </view>
- </view>
- </view>
- <!-- 专辑列表面板 -->
- <view v-if="showAlbumListPanel" class="album-panel" @click="showAlbumListPanel = false">
- <view class="album-content" @click.stop>
- <view class="album-header">
- <text class="album-title">选择专辑</text>
- <text class="album-close" @click="showAlbumListPanel = false">✕</text>
- </view>
- <scroll-view scroll-y class="album-list">
- <view
- v-for="album in albums"
- :key="album.id"
- class="album-item"
- :class="{ active: selectedAlbum?.id === album.id }"
- @click="selectAlbum(album)"
- >
- <view class="album-item-content">
- <text class="album-item-title">{{ album.title }}</text>
- <text class="album-item-desc">{{ album.totalChapters }} 个章节</text>
- </view>
- <text v-if="selectedAlbum?.id === album.id" class="album-check">✓</text>
- </view>
- <view v-if="albums.length === 0" class="album-empty">
- <text>暂无专辑</text>
- </view>
- </scroll-view>
- <button class="album-create-btn" @click="showAlbumListPanel = false; showAlbumPanel = true">
- <text>+ 新建专辑</text>
- </button>
- </view>
- </view>
- <!-- 新建专辑面板 -->
- <view v-if="showAlbumPanel" class="album-panel" @click="showAlbumPanel = false">
- <view class="album-content" @click.stop>
- <view class="album-header">
- <text class="album-title">新建专辑</text>
- <text class="album-close" @click="showAlbumPanel = false">✕</text>
- </view>
- <view class="album-form">
- <view class="form-item">
- <text class="form-label">专辑名称 *</text>
- <input
- v-model="newAlbumTitle"
- class="form-input"
- placeholder="例如:我的有声书"
- maxlength="50"
- />
- </view>
- <view class="form-item">
- <text class="form-label">描述(可选)</text>
- <textarea
- v-model="newAlbumDescription"
- class="form-textarea"
- placeholder="简单描述这个专辑..."
- maxlength="200"
- />
- </view>
- </view>
- <button class="album-submit-btn" :disabled="!newAlbumTitle.trim()" @click="createAlbum">
- <text>创建专辑</text>
- </button>
- </view>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- import { ref, computed, onMounted } from 'vue';
- import { onShow } from '@dcloudio/uni-app';
- import { useUserStore } from '../../store/user';
- import { useAudioStore } from '../../store/audio';
- import { get, post } from '../../utils/request';
- import type { VoiceParams } from '../../types';
- const userStore = useUserStore();
- const audioStore = useAudioStore();
- // 状态
- const text = ref('');
- const selectedVoice = ref('cherry');
- const voiceParams = ref<VoiceParams>({
- speed: 1.0,
- pitch: 0,
- volume: 50,
- });
- const generating = ref(false);
- const btnPressed = ref(false);
- const previewingVoice = ref<string | null>(null);
- let previewAudio: HTMLAudioElement | null = null;
- // 专辑相关状态
- const selectedAlbum = ref<{ id: string; title: string } | null>(null);
- const albums = ref<any[]>([]);
- const showAlbumListPanel = ref(false);
- const showAlbumPanel = ref(false);
- const newAlbumTitle = ref('');
- const newAlbumDescription = ref('');
- // 成功弹窗状态
- const showSuccessModal = ref(false);
- const generatedAudioId = ref('');
- // 新手引导状态
- const showGuide = ref(false);
- const guideStep = ref(1);
- const guideSteps = [
- { step: 1, title: '输入文本', desc: '在文本框中输入要转换的文本内容', target: '.input-card' },
- { step: 2, title: '选择音色', desc: '选择你喜欢的声音音色', target: '.voice-card' },
- { step: 3, title: '调整参数', desc: '可以调整语速、音调、音量', target: '.params-card' },
- { step: 4, title: '生成音频', desc: '点击按钮开始生成', target: '.generate-btn' },
- ];
- // 额度用尽弹窗
- const showQuotaModal = ref(false);
- // 计算属性
- const canGenerate = computed(() => {
- return text.value.trim().length > 0 && !generating.value;
- });
- // 专辑相关函数
- async function fetchAlbums() {
- try {
- const result = await get<{ data: { albums: any[] } }>('/book-generator/albums');
- albums.value = result.data?.albums || [];
- } catch (error) {
- console.error('获取专辑列表失败:', error);
- }
- }
- function selectAlbum(album: any) {
- selectedAlbum.value = { id: album.id, title: album.title };
- showAlbumListPanel.value = false;
- }
- async function createAlbum() {
- if (!newAlbumTitle.value.trim()) return;
- try {
- const result = await post<{ data: { id: string; title: string } }>('/book-generator/albums', {
- title: newAlbumTitle.value.trim(),
- description: newAlbumDescription.value.trim(),
- });
- if (result.code === 0 && result.data) {
- selectedAlbum.value = { id: String(result.data.id), title: result.data.title };
- await fetchAlbums(); // 刷新列表
- showAlbumPanel.value = false;
- newAlbumTitle.value = '';
- newAlbumDescription.value = '';
- uni.showToast({ title: '专辑创建成功', icon: 'success' });
- }
- } catch (error) {
- console.error('创建专辑失败:', error);
- uni.showToast({ title: '创建失败', icon: 'none' });
- }
- }
- // 初始化
- onMounted(async () => {
- await audioStore.fetchVoices();
- await fetchAlbums(); // 获取专辑列表
- // 检查是否显示新手引导(只显示一次)
- const hasSeenGuide = uni.getStorageSync('hasSeenCreateGuide');
- if (!hasSeenGuide) {
- setTimeout(() => {
- showGuide.value = true;
- }, 500);
- }
- });
- // 跳转到视频生成页面
- function goToVideoGenerator() {
- uni.navigateTo({ url: '/pages/video-generator/index' });
- }
- // 跳转到 AI 生成内容页面
- function goToAIGenerate() {
- uni.navigateTo({ url: '/pages/ai-generate/index' });
- }
- // 页面显示时检查是否有 AI 生成的文本
- onShow(() => {
- const aiText = uni.getStorageSync('ai_generated_text');
- if (aiText) {
- text.value = aiText;
- uni.removeStorageSync('ai_generated_text');
- uni.showToast({ title: '已填充 AI 生成的文本', icon: 'success' });
- }
- });
- // 清空文本
- function clearText() {
- text.value = '';
- }
- // 粘贴文本
- async function pasteText() {
- const content = await uni.getClipboardData();
- if (content.data) {
- text.value = content.data;
- }
- }
- // 试听音色
- async function previewVoice(voiceId: string) {
- // 如果正在试听当前音色,停止试听
- if (previewingVoice.value === voiceId) {
- stopPreview();
- return;
- }
- // 停止之前的试听
- stopPreview();
- try {
- previewingVoice.value = voiceId;
- uni.showLoading({ title: '生成试听音频...' });
- // 调用API生成预览音频
- const res = await post<{ audioUrl: string }>('/tts/preview', {
- voiceId,
- voiceParams: voiceParams.value,
- });
- uni.hideLoading();
- if (res.audioUrl) {
- // #ifdef H5
- previewAudio = new Audio('http://localhost:3000' + res.audioUrl);
- previewAudio.play();
- previewAudio.onended = () => {
- previewingVoice.value = null;
- previewAudio = null;
- };
- // #endif
- // #ifndef H5
- // 非H5环境直接停止
- uni.showToast({ title: '试听已播放', icon: 'success' });
- setTimeout(() => {
- previewingVoice.value = null;
- }, 2000);
- // #endif
- }
- } catch (error: any) {
- uni.hideLoading();
- uni.showToast({ title: '试听生成失败', icon: 'none' });
- previewingVoice.value = null;
- }
- }
- // 停止试听
- function stopPreview() {
- if (previewAudio) {
- previewAudio.pause();
- previewAudio = null;
- }
- previewingVoice.value = null;
- }
- // 生成音频
- async function handleGenerate() {
- if (!canGenerate.value) return;
- // 检查额度(模拟)
- const quotaExhausted = !userStore.memberStatus || userStore.memberStatus.quota.dailyRemaining <= 0;
- if (quotaExhausted) {
- showQuotaModal.value = true;
- return;
- }
- generating.value = true;
- try {
- const result = await audioStore.generateAudio(
- text.value,
- selectedVoice.value,
- voiceParams.value,
- {
- bookId: selectedAlbum.value?.id,
- }
- );
- // 保存生成的音频ID
- generatedAudioId.value = result.audioId;
- // 显示成功弹窗
- showSuccessModal.value = true;
- // 清空文本,方便下次输入
- text.value = '';
- } catch (error: any) {
- console.error('生成失败:', error);
- // 检查是否是额度用尽错误
- if (error.message?.includes('额度') || error.message?.includes('quota')) {
- showQuotaModal.value = true;
- } else {
- uni.showToast({
- title: error.message || '生成失败,请重试',
- icon: 'none'
- });
- }
- } finally {
- generating.value = false;
- }
- }
- // 关闭成功弹窗
- function closeSuccessModal() {
- showSuccessModal.value = false;
- generatedAudioId.value = '';
- }
- // 跳转到播放器
- function goToPlayer() {
- if (generatedAudioId.value) {
- uni.navigateTo({
- url: `/pages/player/index?id=${generatedAudioId.value}`,
- });
- }
- closeSuccessModal();
- }
- // 分享音频
- function shareAudio() {
- if (generatedAudioId.value) {
- // #ifdef H5
- const shareUrl = `${window.location.origin}/#/pages/player/index?id=${generatedAudioId.value}`;
- uni.setClipboardData({
- data: shareUrl,
- success: () => {
- uni.showToast({ title: '链接已复制', icon: 'success' });
- }
- });
- // #endif
- // #ifndef H5
- uni.showToast({ title: '分享功能开发中', icon: 'none' });
- // #endif
- }
- }
- // 新手引导 - 下一步
- function nextGuideStep() {
- if (guideStep.value < guideSteps.length) {
- guideStep.value++;
- }
- }
- // 新手引导 - 关闭
- function closeGuide() {
- showGuide.value = false;
- uni.setStorageSync('hasSeenCreateGuide', true);
- }
- // 跳转到会员页
- function goToMember() {
- showQuotaModal.value = false;
- uni.navigateTo({ url: '/pages/member/index' });
- }
- </script>
- <style scoped>
- .page {
- min-height: 100vh;
- background: #f5f5f5;
- }
- .nav-bar {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- height: 88rpx;
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
- z-index: 100;
- padding-top: 44rpx;
- }
- .nav-content {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 32rpx;
- height: 88rpx;
- }
- .page-title {
- font-size: 34rpx;
- font-weight: 600;
- color: #ffffff;
- }
- .main-content {
- padding: 132rpx 32rpx 180rpx;
- }
- .card {
- background: #ffffff;
- border-radius: 24rpx;
- padding: 32rpx;
- margin-bottom: 24rpx;
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
- }
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24rpx;
- }
- .card-title {
- font-size: 32rpx;
- font-weight: 600;
- color: #1f2937;
- }
- .word-count {
- font-size: 24rpx;
- color: #6b7280;
- }
- .word-count.warning {
- color: #f59e0b;
- }
- .word-count.error {
- color: #ef4444;
- font-weight: 600;
- }
- .text-input {
- width: 100%;
- height: 300rpx;
- font-size: 28rpx;
- color: #1f2937;
- line-height: 1.6;
- overflow-y: auto;
- }
- .input-actions {
- display: flex;
- justify-content: flex-end;
- gap: 16rpx;
- margin-top: 16rpx;
- }
- .input-hint {
- margin-top: 12rpx;
- text-align: right;
- }
- .input-hint text {
- font-size: 22rpx;
- color: #9ca3af;
- }
- .action-btn {
- font-size: 24rpx;
- color: #6b7280;
- background: #f3f4f6;
- padding: 12rpx 24rpx;
- border-radius: 12rpx;
- border: none;
- transition: all 0.2s;
- }
- .action-btn:active {
- opacity: 0.7;
- transform: scale(0.98);
- }
- .action-btn::after {
- border: none;
- }
- .voice-list {
- white-space: nowrap;
- margin-top: 24rpx;
- }
- /* 音色网格布局 - 2列 */
- .voice-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 16rpx;
- margin-top: 24rpx;
- }
- .voice-item {
- position: relative;
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 24rpx 16rpx;
- border-radius: 16rpx;
- background: #f9fafb;
- border: 2rpx solid transparent;
- transition: all 0.3s;
- }
- .voice-item.active {
- background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
- border-color: #4f46e5;
- }
- .voice-item.active .voice-name {
- color: #ffffff;
- }
- .voice-item:active {
- transform: scale(0.95);
- }
- .voice-icon {
- font-size: 48rpx;
- margin-bottom: 12rpx;
- }
- .voice-name {
- font-size: 26rpx;
- font-weight: 500;
- color: #1f2937;
- margin-bottom: 4rpx;
- }
- .voice-preview-btn {
- position: absolute;
- top: 8rpx;
- right: 8rpx;
- width: 40rpx;
- height: 40rpx;
- background: rgba(79, 70, 229, 0.9);
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 20rpx;
- color: #ffffff;
- }
- .voice-item.active .voice-preview-btn {
- background: rgba(255, 255, 255, 0.9);
- color: #4f46e5;
- }
- .voice-desc {
- font-size: 20rpx;
- color: #6b7280;
- }
- .param-item {
- margin-top: 24rpx;
- }
- .param-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12rpx;
- }
- .param-label {
- font-size: 26rpx;
- color: #6b7280;
- }
- .param-value {
- font-size: 28rpx;
- font-weight: 600;
- color: #4F46E5;
- background: #EEF2FF;
- padding: 4rpx 16rpx;
- border-radius: 8rpx;
- }
- .param-range {
- display: flex;
- justify-content: space-between;
- margin-top: 8rpx;
- }
- .param-range text {
- font-size: 22rpx;
- color: #9ca3af;
- }
- /* 专辑选择 */
- .album-card {
- background: #ffffff;
- border-radius: 24rpx;
- padding: 32rpx;
- margin-bottom: 24rpx;
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
- }
- .card-header-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20rpx;
- }
- .album-add-btn {
- font-size: 28rpx;
- color: #4f46e5;
- font-weight: 500;
- }
- .album-selector {
- display: flex;
- justify-content: space-between;
- align-items: center;
- background: #f9fafb;
- border-radius: 16rpx;
- padding: 24rpx;
- }
- .album-name {
- font-size: 28rpx;
- color: #1f2937;
- font-weight: 500;
- }
- .album-placeholder {
- font-size: 28rpx;
- color: #9ca3af;
- }
- .album-arrow {
- font-size: 40rpx;
- color: #9ca3af;
- }
- /* 专辑面板 */
- .album-panel {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.5);
- z-index: 1000;
- display: flex;
- align-items: flex-end;
- }
- .album-content {
- width: 100%;
- max-height: 80vh;
- background: #ffffff;
- border-radius: 32rpx 32rpx 0 0;
- padding: 32rpx;
- display: flex;
- flex-direction: column;
- }
- .album-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 24rpx;
- }
- .album-title {
- font-size: 34rpx;
- font-weight: 600;
- color: #1f2937;
- }
- .album-close {
- font-size: 40rpx;
- color: #9ca3af;
- padding: 8rpx;
- }
- .album-list {
- flex: 1;
- max-height: 50vh;
- }
- .album-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 24rpx;
- border-radius: 16rpx;
- background: #f9fafb;
- margin-bottom: 16rpx;
- }
- .album-item.active {
- background: #eef2ff;
- border: 2px solid #4f46e5;
- }
- .album-item-content {
- flex: 1;
- }
- .album-item-title {
- display: block;
- font-size: 28rpx;
- font-weight: 600;
- color: #1f2937;
- margin-bottom: 8rpx;
- }
- .album-item-desc {
- display: block;
- font-size: 24rpx;
- color: #6b7280;
- }
- .album-check {
- font-size: 32rpx;
- color: #4f46e5;
- margin-left: 16rpx;
- }
- .album-empty {
- text-align: center;
- padding: 60rpx 0;
- color: #9ca3af;
- font-size: 28rpx;
- }
- .album-create-btn,
- .album-submit-btn {
- width: 100%;
- height: 100rpx;
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
- border-radius: 24rpx;
- border: none;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 32rpx;
- font-weight: 600;
- color: #ffffff;
- margin-top: 24rpx;
- }
- .album-create-btn::after,
- .album-submit-btn::after {
- border: none;
- }
- /* 专辑表单 */
- .album-form {
- margin: 24rpx 0;
- }
- .form-item {
- margin-bottom: 24rpx;
- }
- .form-label {
- display: block;
- font-size: 28rpx;
- color: #1f2937;
- font-weight: 500;
- margin-bottom: 12rpx;
- }
- .form-input {
- width: 100%;
- height: 88rpx;
- background: #f9fafb;
- border-radius: 16rpx;
- padding: 0 24rpx;
- font-size: 28rpx;
- color: #1f2937;
- }
- .form-textarea {
- width: 100%;
- min-height: 160rpx;
- background: #f9fafb;
- border-radius: 16rpx;
- padding: 24rpx;
- font-size: 28rpx;
- color: #1f2937;
- line-height: 1.6;
- }
- .generate-btn {
- width: 100%;
- height: 100rpx;
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
- border-radius: 24rpx;
- border: none;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 8rpx 24rpx rgba(79, 70, 229, 0.4);
- transition: all 0.3s ease;
- }
- .generate-btn::after {
- border: none;
- }
- .generate-btn[disabled] {
- background: #e5e7eb;
- box-shadow: none;
- }
- .generate-btn.pressed {
- transform: scale(0.96);
- box-shadow: 0 4rpx 12rpx rgba(79, 70, 229, 0.3);
- }
- /* 视频生成入口卡片 */
- .video-entry-card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- border-radius: 16rpx;
- padding: 30rpx;
- margin-top: 20rpx;
- }
- .video-entry-content {
- display: flex;
- align-items: center;
- gap: 20rpx;
- }
- .video-entry-icon {
- font-size: 60rpx;
- }
- .video-entry-text {
- display: flex;
- flex-direction: column;
- gap: 6rpx;
- }
- .video-entry-title {
- font-size: 32rpx;
- font-weight: 600;
- color: #ffffff;
- }
- .video-entry-desc {
- font-size: 24rpx;
- color: rgba(255, 255, 255, 0.8);
- }
- .video-entry-arrow {
- font-size: 40rpx;
- color: #ffffff;
- }
- .btn-text {
- font-size: 34rpx;
- font-weight: 600;
- color: #ffffff;
- letter-spacing: 2rpx;
- }
- .ai-btn {
- display: flex;
- align-items: center;
- gap: 8rpx;
- }
- /* 成功弹窗 */
- .success-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.6);
- z-index: 2000;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .success-content {
- width: 600rpx;
- background: #ffffff;
- border-radius: 32rpx;
- padding: 60rpx 40rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .success-animation {
- position: relative;
- margin-bottom: 40rpx;
- }
- .success-circle {
- width: 160rpx;
- height: 160rpx;
- border-radius: 50%;
- background: linear-gradient(135deg, #10b981 0%, #34d399 100%);
- display: flex;
- align-items: center;
- justify-content: center;
- animation: success-pop 0.5s ease;
- }
- .success-checkmark {
- font-size: 80rpx;
- color: #ffffff;
- font-weight: bold;
- }
- .success-sparkles {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 200rpx;
- height: 200rpx;
- }
- .sparkle {
- position: absolute;
- font-size: 32rpx;
- animation: sparkle 1.5s ease-in-out infinite;
- }
- .sparkle:nth-child(1) {
- top: 0;
- left: 50%;
- animation-delay: 0s;
- }
- .sparkle:nth-child(2) {
- top: 50%;
- right: 0;
- animation-delay: 0.5s;
- }
- .sparkle:nth-child(3) {
- bottom: 0;
- left: 50%;
- animation-delay: 1s;
- }
- @keyframes success-pop {
- 0% {
- transform: scale(0);
- }
- 50% {
- transform: scale(1.1);
- }
- 100% {
- transform: scale(1);
- }
- }
- @keyframes sparkle {
- 0%, 100% {
- opacity: 0;
- transform: scale(0.5);
- }
- 50% {
- opacity: 1;
- transform: scale(1);
- }
- }
- .success-title {
- font-size: 40rpx;
- font-weight: 600;
- color: #1f2937;
- margin-bottom: 12rpx;
- }
- .success-desc {
- font-size: 28rpx;
- color: #6b7280;
- margin-bottom: 40rpx;
- }
- .success-actions {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- }
- .success-btn {
- width: 100%;
- height: 88rpx;
- border-radius: 44rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 30rpx;
- border: none;
- }
- .success-btn::after {
- border: none;
- }
- .play-btn {
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
- color: #ffffff;
- }
- .share-btn {
- background: #f3f4f6;
- color: #4b5563;
- }
- .close-btn {
- background: transparent;
- color: #9ca3af;
- }
- /* 新手引导 */
- .guide-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.8);
- z-index: 3000;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .guide-content {
- width: 600rpx;
- background: #ffffff;
- border-radius: 32rpx;
- padding: 48rpx 40rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .guide-header {
- margin-bottom: 24rpx;
- }
- .guide-step {
- font-size: 24rpx;
- color: #9ca3af;
- }
- .guide-body {
- text-align: center;
- margin-bottom: 32rpx;
- }
- .guide-title {
- font-size: 36rpx;
- font-weight: 600;
- color: #1f2937;
- display: block;
- margin-bottom: 16rpx;
- }
- .guide-desc {
- font-size: 28rpx;
- color: #6b7280;
- display: block;
- line-height: 1.6;
- }
- .guide-dots {
- display: flex;
- gap: 12rpx;
- margin-bottom: 32rpx;
- }
- .guide-dot {
- width: 12rpx;
- height: 12rpx;
- border-radius: 50%;
- background: #e5e7eb;
- }
- .guide-dot.active {
- background: #4f46e5;
- width: 32rpx;
- border-radius: 6rpx;
- }
- .guide-actions {
- display: flex;
- gap: 20rpx;
- width: 100%;
- }
- .guide-btn {
- flex: 1;
- height: 88rpx;
- border-radius: 44rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 30rpx;
- border: none;
- }
- .guide-btn::after {
- border: none;
- }
- .guide-btn.skip {
- background: #f3f4f6;
- color: #6b7280;
- }
- .guide-btn.next,
- .guide-btn.finish {
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
- color: #ffffff;
- }
- /* 额度用尽弹窗 */
- .quota-modal {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.6);
- z-index: 3000;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .quota-content {
- width: 600rpx;
- background: #ffffff;
- border-radius: 32rpx;
- padding: 60rpx 40rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .quota-icon {
- font-size: 100rpx;
- margin-bottom: 24rpx;
- }
- .quota-title {
- font-size: 36rpx;
- font-weight: 600;
- color: #1f2937;
- margin-bottom: 12rpx;
- }
- .quota-desc {
- font-size: 28rpx;
- color: #6b7280;
- margin-bottom: 40rpx;
- }
- .quota-actions {
- width: 100%;
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- }
- .quota-btn {
- width: 100%;
- height: 88rpx;
- border-radius: 44rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 30rpx;
- border: none;
- }
- .quota-btn::after {
- border: none;
- }
- .quota-btn.primary {
- background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
- color: #ffffff;
- }
- .quota-btn.secondary {
- background: #f3f4f6;
- color: #6b7280;
- }
- </style>
|