|
|
@@ -0,0 +1,231 @@
|
|
|
+<template>
|
|
|
+ <view class="page">
|
|
|
+ <!-- 顶部导航 -->
|
|
|
+ <view class="nav-bar">
|
|
|
+ <view class="nav-btn" @click="goBack">
|
|
|
+ <text class="nav-icon">←</text>
|
|
|
+ </view>
|
|
|
+ <text class="nav-title">我的收藏</text>
|
|
|
+ <view class="nav-btn" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 加载状态 -->
|
|
|
+ <view v-if="loading" class="loading">
|
|
|
+ <text>加载中...</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <view v-else-if="favorites.length === 0" class="empty">
|
|
|
+ <text class="empty-icon">❤️</text>
|
|
|
+ <text class="empty-text">暂无收藏</text>
|
|
|
+ <text class="empty-hint">去听书页面收藏喜欢的音频吧</text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 收藏列表 -->
|
|
|
+ <view v-else class="list">
|
|
|
+ <view
|
|
|
+ v-for="item in favorites"
|
|
|
+ :key="item.id"
|
|
|
+ class="item"
|
|
|
+ @click="playAudio(item.audio)"
|
|
|
+ >
|
|
|
+ <view class="item-cover">
|
|
|
+ <text class="cover-icon">🎵</text>
|
|
|
+ </view>
|
|
|
+ <view class="item-info">
|
|
|
+ <text class="item-title">{{ item.audio?.title || '未知标题' }}</text>
|
|
|
+ <text class="item-meta">{{ item.audio?.wordCount || 0 }}字</text>
|
|
|
+ </view>
|
|
|
+ <view class="item-action" @click.stop="removeFavorite(item.audioId)">
|
|
|
+ <text class="action-icon">❤️</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, onMounted } from 'vue';
|
|
|
+import { get, del } from '../../utils/request';
|
|
|
+
|
|
|
+interface FavoriteItem {
|
|
|
+ id: number;
|
|
|
+ audioId: number;
|
|
|
+ createdAt: string;
|
|
|
+ audio?: {
|
|
|
+ id: number;
|
|
|
+ title: string;
|
|
|
+ text: string;
|
|
|
+ audioUrl: string;
|
|
|
+ audioDuration: number;
|
|
|
+ wordCount: number;
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+const favorites = ref<FavoriteItem[]>([]);
|
|
|
+const loading = ref(true);
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await fetchFavorites();
|
|
|
+});
|
|
|
+
|
|
|
+async function fetchFavorites() {
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ const result = await get<any>('/favorites');
|
|
|
+ favorites.value = result || [];
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取收藏列表失败:', error);
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function removeFavorite(audioId: number) {
|
|
|
+ try {
|
|
|
+ await del(`/favorites/${audioId}`);
|
|
|
+ uni.showToast({ title: '已取消收藏', icon: 'success' });
|
|
|
+ // 从列表中移除
|
|
|
+ favorites.value = favorites.value.filter(item => item.audioId !== audioId);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('取消收藏失败:', error);
|
|
|
+ uni.showToast({ title: '操作失败', icon: 'none' });
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function playAudio(audio: any) {
|
|
|
+ if (!audio) return;
|
|
|
+
|
|
|
+ uni.navigateTo({
|
|
|
+ url: `/pages/player/index?id=${audio.id}`
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function goBack() {
|
|
|
+ uni.navigateBack();
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.page {
|
|
|
+ min-height: 100vh;
|
|
|
+ background: #f5f5f5;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-bar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 44rpx 32rpx 24rpx;
|
|
|
+ background: #ffffff;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-btn {
|
|
|
+ width: 64rpx;
|
|
|
+ height: 64rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-icon {
|
|
|
+ font-size: 36rpx;
|
|
|
+ color: #333333;
|
|
|
+}
|
|
|
+
|
|
|
+.nav-title {
|
|
|
+ font-size: 32rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333333;
|
|
|
+}
|
|
|
+
|
|
|
+.loading {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 200rpx 0;
|
|
|
+ color: #999999;
|
|
|
+ font-size: 28rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 200rpx 0;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-icon {
|
|
|
+ font-size: 120rpx;
|
|
|
+ margin-bottom: 32rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-text {
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #333333;
|
|
|
+ font-weight: 600;
|
|
|
+ margin-bottom: 16rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.empty-hint {
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #999999;
|
|
|
+}
|
|
|
+
|
|
|
+.list {
|
|
|
+ padding: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #ffffff;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 24rpx;
|
|
|
+ margin-bottom: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.item-cover {
|
|
|
+ width: 100rpx;
|
|
|
+ height: 100rpx;
|
|
|
+ background: linear-gradient(135deg, #4f46e5 0%, #818cf8 100%);
|
|
|
+ border-radius: 12rpx;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-right: 24rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.cover-icon {
|
|
|
+ font-size: 48rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.item-info {
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.item-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ color: #333333;
|
|
|
+ font-weight: 500;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ max-width: 400rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.item-meta {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999999;
|
|
|
+}
|
|
|
+
|
|
|
+.item-action {
|
|
|
+ padding: 20rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.action-icon {
|
|
|
+ font-size: 40rpx;
|
|
|
+}
|
|
|
+</style>
|