|
|
@@ -1,201 +1,211 @@
|
|
|
<template>
|
|
|
<view class="page">
|
|
|
- <!-- 顶部导航栏 -->
|
|
|
- <view class="nav-bar">
|
|
|
- <view class="nav-content">
|
|
|
- <text class="page-title">生成音频</text>
|
|
|
+ <!-- 顶部搜索栏 -->
|
|
|
+ <view class="search-bar">
|
|
|
+ <view class="search-inner">
|
|
|
+ <text class="search-icon">🔍</text>
|
|
|
+ <text class="search-placeholder">搜索历史记录...</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">{{ text.length }} 字</text>
|
|
|
- </view>
|
|
|
- <textarea
|
|
|
- v-model="text"
|
|
|
- class="text-input"
|
|
|
- placeholder="请输入要转换的文本内容..."
|
|
|
- :maxlength="50000"
|
|
|
- auto-height
|
|
|
- />
|
|
|
- <view class="input-actions">
|
|
|
- <button class="action-btn ai-btn" @click="goToAIPage">
|
|
|
- <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 class="tab-bar">
|
|
|
+ <scroll-view class="tab-scroll" scroll-x="true" :show-scrollbar="false">
|
|
|
+ <view class="tab-list">
|
|
|
+ <view
|
|
|
+ v-for="tab in tabs"
|
|
|
+ :key="tab.id"
|
|
|
+ class="tab-item"
|
|
|
+ :class="{ active: selectedTab === tab.id }"
|
|
|
+ @click="selectTab(tab.id)"
|
|
|
+ >
|
|
|
+ <text>{{ tab.name }}</text>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 音频列表 -->
|
|
|
+ <scroll-view scroll-y class="audio-list" @scrolltolower="loadMore">
|
|
|
+ <view v-if="audioList.length === 0 && !loading" class="empty">
|
|
|
+ <text class="empty-icon">📚</text>
|
|
|
+ <text class="empty-text">暂无历史记录</text>
|
|
|
+ <text class="empty-hint">快去生成第一个音频吧</text>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 音色选择 -->
|
|
|
- <view class="card voice-card">
|
|
|
- <text class="card-title">选择音色</text>
|
|
|
- <scroll-view scroll-x class="voice-list">
|
|
|
- <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 v-else class="audio-grid">
|
|
|
+ <view
|
|
|
+ v-for="item in audioList"
|
|
|
+ :key="item._id"
|
|
|
+ class="audio-card"
|
|
|
+ @click="playAudio(item)"
|
|
|
+ >
|
|
|
+ <view class="audio-cover" :style="{ background: getCoverGradient(item.voiceId) }">
|
|
|
+ <text class="cover-icon">🎵</text>
|
|
|
+ <view class="play-overlay">
|
|
|
+ <text class="play-icon">▶</text>
|
|
|
</view>
|
|
|
- <text class="voice-name">{{ voice.name }}</text>
|
|
|
- <text class="voice-desc">{{ voice.description }}</text>
|
|
|
+ <text class="audio-duration-badge">{{ formatDuration(item.audioDuration) }}</text>
|
|
|
</view>
|
|
|
- </scroll-view>
|
|
|
+ <view class="audio-info">
|
|
|
+ <text class="audio-title">{{ item.title }}</text>
|
|
|
+ <text class="audio-meta">{{ item.wordCount }}字 · {{ formatDate(item.createdAt) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 参数调节 -->
|
|
|
- <view class="card params-card">
|
|
|
- <text class="card-title">参数调节</text>
|
|
|
- <view class="param-item">
|
|
|
- <text class="param-label">语速:{{ voiceParams.speed.toFixed(1) }}x</text>
|
|
|
- <slider
|
|
|
- :value="(voiceParams.speed - 0.5) * 100 / 1.5"
|
|
|
- :min="0"
|
|
|
- :max="100"
|
|
|
- @change="(e: any) => voiceParams.speed = Number((0.5 + (e.detail.value / 100) * 1.5).toFixed(1))"
|
|
|
- activeColor="#4F46E5"
|
|
|
- backgroundColor="#e5e7eb"
|
|
|
- block-size="20"
|
|
|
- />
|
|
|
- </view>
|
|
|
- <view class="param-item">
|
|
|
- <text class="param-label">音调:{{ voiceParams.pitch > 0 ? '+' : '' }}{{ voiceParams.pitch }}</text>
|
|
|
- <slider
|
|
|
- :value="(voiceParams.pitch + 500) / 10"
|
|
|
- :min="0"
|
|
|
- :max="100"
|
|
|
- @change="(e: any) => voiceParams.pitch = Math.round(e.detail.value * 10 - 500)"
|
|
|
- activeColor="#4F46E5"
|
|
|
- backgroundColor="#e5e7eb"
|
|
|
- block-size="20"
|
|
|
- />
|
|
|
- </view>
|
|
|
- <view class="param-item">
|
|
|
- <text class="param-label">音量:{{ voiceParams.volume }}%</text>
|
|
|
- <slider
|
|
|
- :value="voiceParams.volume"
|
|
|
- :min="0"
|
|
|
- :max="100"
|
|
|
- @change="(e: any) => voiceParams.volume = e.detail.value"
|
|
|
- activeColor="#4F46E5"
|
|
|
- backgroundColor="#e5e7eb"
|
|
|
- block-size="20"
|
|
|
- />
|
|
|
- </view>
|
|
|
+ <view v-if="loading" class="loading">
|
|
|
+ <text>加载中...</text>
|
|
|
</view>
|
|
|
|
|
|
- <!-- 生成按钮 -->
|
|
|
- <button
|
|
|
- class="generate-btn"
|
|
|
- :disabled="!canGenerate"
|
|
|
- @click="handleGenerate"
|
|
|
- >
|
|
|
- <text class="btn-text">{{ generating ? '生成中...' : '生成音频' }}</text>
|
|
|
- </button>
|
|
|
- </view>
|
|
|
+ <view v-if="!hasMore && audioList.length > 0" class="no-more">
|
|
|
+ <text>没有更多了</text>
|
|
|
+ </view>
|
|
|
+ </scroll-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 { ref, onMounted } from 'vue';
|
|
|
import { useAudioStore } from '../../store/audio';
|
|
|
-import type { VoiceParams } from '../../types';
|
|
|
+import { get } from '../../utils/request';
|
|
|
+import type { AudioItem } 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 canGenerate = computed(() => {
|
|
|
- return text.value.trim().length > 0 && !generating.value;
|
|
|
-});
|
|
|
-
|
|
|
-// 初始化
|
|
|
-onMounted(async () => {
|
|
|
- await audioStore.fetchVoices();
|
|
|
-});
|
|
|
-
|
|
|
-// 页面显示时检查是否有 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' });
|
|
|
+// 标签状态
|
|
|
+const tabs = ref([
|
|
|
+ { id: 1, name: '全部' },
|
|
|
+ { id: 2, name: '今天' },
|
|
|
+ { id: 3, name: '本周' },
|
|
|
+ { id: 4, name: '本月' },
|
|
|
+]);
|
|
|
+const selectedTab = ref(1);
|
|
|
+
|
|
|
+// 音频列表状态
|
|
|
+const audioList = ref<AudioItem[]>([]);
|
|
|
+const page = ref(1);
|
|
|
+const pageSize = 20;
|
|
|
+const total = ref(0);
|
|
|
+const loading = ref(false);
|
|
|
+const hasMore = ref(true);
|
|
|
+
|
|
|
+// 音色封面颜色映射
|
|
|
+const voiceColors: Record<string, string[]> = {
|
|
|
+ cherry: ['#667eea', '#764ba2'],
|
|
|
+ ethan: ['#f093fb', '#f5576c'],
|
|
|
+ serena: ['#4facfe', '#00f2fe'],
|
|
|
+ chelsie: ['#43e97b', '#38f9d7'],
|
|
|
+ momo: ['#fa709a', '#fee140'],
|
|
|
+ vivian: ['#a8edea', '#fed6e3'],
|
|
|
+ moon: ['#5ee7df', '#b490ca'],
|
|
|
+ maia: ['#d299c2', '#fef9d7'],
|
|
|
+ kai: ['#89f7fe', '#66a6ff'],
|
|
|
+ nofish: ['#cd9cf2', '#f6f3ff'],
|
|
|
+};
|
|
|
+
|
|
|
+// 获取封面渐变色
|
|
|
+function getCoverGradient(voiceId: string): string {
|
|
|
+ const colors = voiceColors[voiceId] || ['#667eea', '#764ba2'];
|
|
|
+ return `linear-gradient(135deg, ${colors[0]} 0%, ${colors[1]} 100%)`;
|
|
|
+}
|
|
|
+
|
|
|
+// 选择标签
|
|
|
+async function selectTab(tabId: number) {
|
|
|
+ selectedTab.value = tabId;
|
|
|
+ page.value = 1;
|
|
|
+ await fetchHistoryList();
|
|
|
+}
|
|
|
+
|
|
|
+// 获取历史列表
|
|
|
+async function fetchHistoryList(isLoadMore = false) {
|
|
|
+ if (loading.value) return;
|
|
|
+ if (!isLoadMore) {
|
|
|
+ page.value = 1;
|
|
|
}
|
|
|
-});
|
|
|
|
|
|
-// 清空文本
|
|
|
-function clearText() {
|
|
|
- text.value = '';
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ // 按时间过滤
|
|
|
+ let startDate = '';
|
|
|
+ const now = new Date();
|
|
|
+
|
|
|
+ switch (selectedTab.value) {
|
|
|
+ case 2: // 今天
|
|
|
+ startDate = new Date(now.setHours(0, 0, 0, 0)).toISOString();
|
|
|
+ break;
|
|
|
+ case 3: // 本周
|
|
|
+ const weekAgo = new Date(now);
|
|
|
+ weekAgo.setDate(weekAgo.getDate() - 7);
|
|
|
+ startDate = weekAgo.toISOString();
|
|
|
+ break;
|
|
|
+ case 4: // 本月
|
|
|
+ const monthAgo = new Date(now);
|
|
|
+ monthAgo.setMonth(monthAgo.getMonth() - 1);
|
|
|
+ startDate = monthAgo.toISOString();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ const params: any = { page: page.value, pageSize };
|
|
|
+ if (startDate) {
|
|
|
+ params.startDate = startDate;
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await get<{ list: AudioItem[]; total: number; totalPages: number }>('/history', params);
|
|
|
+
|
|
|
+ if (isLoadMore) {
|
|
|
+ audioList.value.push(...result.list);
|
|
|
+ } else {
|
|
|
+ audioList.value = result.list;
|
|
|
+ }
|
|
|
+ total.value = result.total;
|
|
|
+ hasMore.value = page.value < result.totalPages;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取历史记录失败:', error);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-// 粘贴文本
|
|
|
-async function pasteText() {
|
|
|
- const content = await uni.getClipboardData();
|
|
|
- if (content.data) {
|
|
|
- text.value = content.data;
|
|
|
+// 加载更多
|
|
|
+function loadMore() {
|
|
|
+ if (hasMore.value && !loading.value) {
|
|
|
+ page.value++;
|
|
|
+ fetchHistoryList(true);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 跳转到 AI 生成页面
|
|
|
-function goToAIPage() {
|
|
|
- uni.navigateTo({ url: '/pages/ai/index' });
|
|
|
+// 播放音频
|
|
|
+function playAudio(item: AudioItem) {
|
|
|
+ audioStore.play(item);
|
|
|
+ uni.navigateTo({ url: `/pages/player/index?id=${item._id}` });
|
|
|
}
|
|
|
|
|
|
-// 生成音频
|
|
|
-async function handleGenerate() {
|
|
|
- if (!canGenerate.value) return;
|
|
|
+// 格式化时长
|
|
|
+function formatDuration(seconds: number): string {
|
|
|
+ const mins = Math.floor(seconds / 60);
|
|
|
+ const secs = Math.floor(seconds % 60);
|
|
|
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
|
+}
|
|
|
|
|
|
- generating.value = true;
|
|
|
- try {
|
|
|
- const result = await audioStore.generateAudio(
|
|
|
- text.value,
|
|
|
- selectedVoice.value,
|
|
|
- voiceParams.value
|
|
|
- );
|
|
|
-
|
|
|
- uni.showToast({ title: '生成成功', icon: 'success' });
|
|
|
-
|
|
|
- // 清空文本,方便下次输入
|
|
|
- text.value = '';
|
|
|
-
|
|
|
- // 跳转到播放器
|
|
|
- uni.navigateTo({
|
|
|
- url: `/pages/player/index?id=${result.audioId}`,
|
|
|
- });
|
|
|
- } catch (error: any) {
|
|
|
- console.error('生成失败:', error);
|
|
|
- uni.showToast({
|
|
|
- title: error.message || '生成失败,请重试',
|
|
|
- icon: 'none'
|
|
|
- });
|
|
|
- } finally {
|
|
|
- generating.value = false;
|
|
|
- }
|
|
|
+// 格式化日期
|
|
|
+function formatDate(dateStr: string): string {
|
|
|
+ const date = new Date(dateStr);
|
|
|
+ const now = new Date();
|
|
|
+ const diff = now.getTime() - date.getTime();
|
|
|
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
|
|
+
|
|
|
+ if (days === 0) return '今天';
|
|
|
+ if (days === 1) return '昨天';
|
|
|
+ if (days < 7) return `${days}天前`;
|
|
|
+
|
|
|
+ return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
|
}
|
|
|
+
|
|
|
+// 初始化
|
|
|
+onMounted(async () => {
|
|
|
+ await fetchHistoryList();
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -204,190 +214,216 @@ async function handleGenerate() {
|
|
|
background: #f5f5f5;
|
|
|
}
|
|
|
|
|
|
-.nav-bar {
|
|
|
+.search-bar {
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
left: 0;
|
|
|
right: 0;
|
|
|
- height: 88rpx;
|
|
|
- background: #ffffff;
|
|
|
+ height: 100rpx;
|
|
|
+ padding-top: env(safe-area-inset-top);
|
|
|
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding-left: 24rpx;
|
|
|
+ padding-right: 24rpx;
|
|
|
z-index: 100;
|
|
|
- padding-top: 44rpx;
|
|
|
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
|
|
}
|
|
|
|
|
|
-.nav-content {
|
|
|
+.search-inner {
|
|
|
+ flex: 1;
|
|
|
+ height: 64rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ border-radius: 32rpx;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
- justify-content: center;
|
|
|
- padding: 0 32rpx;
|
|
|
- height: 88rpx;
|
|
|
+ padding: 0 24rpx;
|
|
|
}
|
|
|
|
|
|
-.page-title {
|
|
|
- font-size: 34rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #1f2937;
|
|
|
+.search-icon {
|
|
|
+ font-size: 28rpx;
|
|
|
+ margin-right: 12rpx;
|
|
|
}
|
|
|
|
|
|
-.main-content {
|
|
|
- padding: 132rpx 32rpx 180rpx;
|
|
|
+.search-placeholder {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
}
|
|
|
|
|
|
-.card {
|
|
|
+.tab-bar {
|
|
|
+ position: fixed;
|
|
|
+ top: 100rpx;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
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;
|
|
|
+ z-index: 99;
|
|
|
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
|
|
}
|
|
|
|
|
|
-.text-input {
|
|
|
+.tab-scroll {
|
|
|
width: 100%;
|
|
|
- min-height: 300rpx;
|
|
|
- font-size: 28rpx;
|
|
|
- color: #1f2937;
|
|
|
- line-height: 1.6;
|
|
|
+ white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
-.input-actions {
|
|
|
- display: flex;
|
|
|
- justify-content: flex-end;
|
|
|
+.tab-list {
|
|
|
+ display: inline-flex;
|
|
|
gap: 16rpx;
|
|
|
- margin-top: 16rpx;
|
|
|
+ padding: 16rpx 24rpx;
|
|
|
}
|
|
|
|
|
|
-.input-hint {
|
|
|
- margin-top: 12rpx;
|
|
|
- text-align: right;
|
|
|
+.tab-item {
|
|
|
+ padding: 12rpx 32rpx;
|
|
|
+ font-size: 28rpx;
|
|
|
+ color: #666;
|
|
|
+ flex-shrink: 0;
|
|
|
+ position: relative;
|
|
|
}
|
|
|
|
|
|
-.input-hint text {
|
|
|
- font-size: 22rpx;
|
|
|
- color: #9ca3af;
|
|
|
+.tab-item.active {
|
|
|
+ color: #4f46e5;
|
|
|
+ font-weight: 600;
|
|
|
}
|
|
|
|
|
|
-.action-btn {
|
|
|
- font-size: 24rpx;
|
|
|
- color: #6b7280;
|
|
|
- background: #f3f4f6;
|
|
|
- padding: 12rpx 24rpx;
|
|
|
- border-radius: 12rpx;
|
|
|
- border: none;
|
|
|
- transition: all 0.2s;
|
|
|
+.tab-item.active::after {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ width: 40rpx;
|
|
|
+ height: 6rpx;
|
|
|
+ background: #4f46e5;
|
|
|
+ border-radius: 3rpx;
|
|
|
}
|
|
|
|
|
|
-.action-btn:active {
|
|
|
- opacity: 0.7;
|
|
|
- transform: scale(0.98);
|
|
|
+.audio-list {
|
|
|
+ padding-top: 160rpx;
|
|
|
+ height: calc(100vh - 160rpx);
|
|
|
}
|
|
|
|
|
|
-.action-btn::after {
|
|
|
- border: none;
|
|
|
+.empty {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 200rpx 0;
|
|
|
}
|
|
|
|
|
|
-.voice-list {
|
|
|
- white-space: nowrap;
|
|
|
- margin-top: 24rpx;
|
|
|
+.empty-icon {
|
|
|
+ font-size: 120rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
}
|
|
|
|
|
|
-.voice-item {
|
|
|
- display: inline-flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- width: 160rpx;
|
|
|
- padding: 24rpx 16rpx;
|
|
|
- margin-right: 16rpx;
|
|
|
- border-radius: 16rpx;
|
|
|
- background: #f9fafb;
|
|
|
- border: 2rpx solid transparent;
|
|
|
- transition: all 0.3s;
|
|
|
+.empty-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #6b7280;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
}
|
|
|
|
|
|
-.voice-item.active {
|
|
|
- background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
|
|
|
- border-color: #4f46e5;
|
|
|
+.empty-hint {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #9ca3af;
|
|
|
}
|
|
|
|
|
|
-.voice-item.active .voice-name,
|
|
|
-.voice-item.active .voice-desc {
|
|
|
- color: #ffffff;
|
|
|
+.audio-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ padding: 24rpx;
|
|
|
+ gap: 20rpx;
|
|
|
}
|
|
|
|
|
|
-.voice-icon {
|
|
|
- font-size: 48rpx;
|
|
|
- margin-bottom: 12rpx;
|
|
|
+.audio-card {
|
|
|
+ width: calc(50% - 10rpx);
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
|
|
|
}
|
|
|
|
|
|
-.voice-name {
|
|
|
- font-size: 26rpx;
|
|
|
- font-weight: 500;
|
|
|
- color: #1f2937;
|
|
|
- margin-bottom: 4rpx;
|
|
|
+.audio-cover {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 240rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
}
|
|
|
|
|
|
-.voice-desc {
|
|
|
- font-size: 20rpx;
|
|
|
- color: #6b7280;
|
|
|
+.cover-icon {
|
|
|
+ font-size: 80rpx;
|
|
|
+ opacity: 0.6;
|
|
|
}
|
|
|
|
|
|
-.param-item {
|
|
|
- margin-top: 24rpx;
|
|
|
+.play-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ bottom: 0;
|
|
|
+ background: rgba(0, 0, 0, 0.2);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.3s;
|
|
|
}
|
|
|
|
|
|
-.param-label {
|
|
|
- font-size: 26rpx;
|
|
|
- color: #6b7280;
|
|
|
- margin-bottom: 12rpx;
|
|
|
- display: block;
|
|
|
+.audio-card:active .play-overlay {
|
|
|
+ opacity: 1;
|
|
|
}
|
|
|
|
|
|
-.generate-btn {
|
|
|
- width: 100%;
|
|
|
- height: 96rpx;
|
|
|
- background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
|
|
|
- border-radius: 24rpx;
|
|
|
- border: none;
|
|
|
+.play-icon {
|
|
|
+ width: 80rpx;
|
|
|
+ height: 80rpx;
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ border-radius: 50%;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #4f46e5;
|
|
|
+ padding-left: 8rpx;
|
|
|
}
|
|
|
|
|
|
-.generate-btn::after {
|
|
|
- border: none;
|
|
|
+.audio-duration-badge {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 12rpx;
|
|
|
+ right: 12rpx;
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
+ color: #ffffff;
|
|
|
+ font-size: 22rpx;
|
|
|
+ padding: 4rpx 12rpx;
|
|
|
+ border-radius: 8rpx;
|
|
|
}
|
|
|
|
|
|
-.generate-btn[disabled] {
|
|
|
- background: #e5e7eb;
|
|
|
+.audio-info {
|
|
|
+ padding: 20rpx;
|
|
|
}
|
|
|
|
|
|
-.btn-text {
|
|
|
- font-size: 32rpx;
|
|
|
- font-weight: 600;
|
|
|
- color: #ffffff;
|
|
|
+.audio-title {
|
|
|
+ font-size: 28rpx;
|
|
|
+ font-weight: 500;
|
|
|
+ color: #1f2937;
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
}
|
|
|
|
|
|
-.ai-btn {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8rpx;
|
|
|
+.audio-meta {
|
|
|
+ font-size: 22rpx;
|
|
|
+ color: #9ca3af;
|
|
|
+}
|
|
|
+
|
|
|
+.loading,
|
|
|
+.no-more {
|
|
|
+ text-align: center;
|
|
|
+ padding: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.loading text,
|
|
|
+.no-more text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #9ca3af;
|
|
|
}
|
|
|
</style>
|