Explorar el Código

refactor: 重构页面路由结构

- 将 history 页面改为真正的历史记录页面
- 新增 create 页面作为生成音频页面
- 底部导航改为:首页、生成、历史、我的
- 历史页面支持按时间筛选(全部/今天/本周/本月)
- 更新首页跳转逻辑
MyFramework User hace 5 meses
padre
commit
f046aea7bd

+ 11 - 1
my-uniapp-vue3/src/pages.json

@@ -7,6 +7,12 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/create/index",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
     {
       "path": "pages/history/index",
       "style": {
@@ -74,9 +80,13 @@
         "text": "首页"
       },
       {
-        "pagePath": "pages/history/index",
+        "pagePath": "pages/create/index",
         "text": "生成"
       },
+      {
+        "pagePath": "pages/history/index",
+        "text": "历史"
+      },
       {
         "pagePath": "pages/mine/index",
         "text": "我的"

+ 392 - 0
my-uniapp-vue3/src/pages/create/index.vue

@@ -0,0 +1,392 @@
+<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">{{ 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>
+      </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>
+            <text class="voice-name">{{ voice.name }}</text>
+            <text class="voice-desc">{{ voice.description }}</text>
+          </view>
+        </scroll-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>
+
+      <!-- 生成按钮 -->
+      <button
+        class="generate-btn"
+        :disabled="!canGenerate"
+        @click="handleGenerate"
+      >
+        <text class="btn-text">{{ generating ? '生成中...' : '生成音频' }}</text>
+      </button>
+    </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 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 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' });
+  }
+});
+
+// 清空文本
+function clearText() {
+  text.value = '';
+}
+
+// 粘贴文本
+async function pasteText() {
+  const content = await uni.getClipboardData();
+  if (content.data) {
+    text.value = content.data;
+  }
+}
+
+// 跳转到 AI 生成页面
+function goToAIPage() {
+  uni.navigateTo({ url: '/pages/ai/index' });
+}
+
+// 生成音频
+async function handleGenerate() {
+  if (!canGenerate.value) return;
+
+  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;
+  }
+}
+</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;
+}
+
+.text-input {
+  width: 100%;
+  min-height: 300rpx;
+  font-size: 28rpx;
+  color: #1f2937;
+  line-height: 1.6;
+}
+
+.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;
+}
+
+.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;
+}
+
+.voice-item.active {
+  background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
+  border-color: #4f46e5;
+}
+
+.voice-item.active .voice-name,
+.voice-item.active .voice-desc {
+  color: #ffffff;
+}
+
+.voice-icon {
+  font-size: 48rpx;
+  margin-bottom: 12rpx;
+}
+
+.voice-name {
+  font-size: 26rpx;
+  font-weight: 500;
+  color: #1f2937;
+  margin-bottom: 4rpx;
+}
+
+.voice-desc {
+  font-size: 20rpx;
+  color: #6b7280;
+}
+
+.param-item {
+  margin-top: 24rpx;
+}
+
+.param-label {
+  font-size: 26rpx;
+  color: #6b7280;
+  margin-bottom: 12rpx;
+  display: block;
+}
+
+.generate-btn {
+  width: 100%;
+  height: 96rpx;
+  background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
+  border-radius: 24rpx;
+  border: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.generate-btn::after {
+  border: none;
+}
+
+.generate-btn[disabled] {
+  background: #e5e7eb;
+}
+
+.btn-text {
+  font-size: 32rpx;
+  font-weight: 600;
+  color: #ffffff;
+}
+
+.ai-btn {
+  display: flex;
+  align-items: center;
+  gap: 8rpx;
+}
+</style>

+ 323 - 287
my-uniapp-vue3/src/pages/history/index.vue

@@ -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>

+ 2 - 2
my-uniapp-vue3/src/pages/index/index.vue

@@ -219,9 +219,9 @@ function goToSearch() {
   uni.navigateTo({ url: '/pages/search/index' });
 }
 
-// 生成新音频(跳转到历史页面)
+// 生成新音频(跳转到生成页面)
 function handleGenerate() {
-  uni.navigateTo({ url: '/pages/history/index' });
+  uni.switchTab({ url: '/pages/create/index' });
 }
 
 // 初始化