library-picturebook-activity/frontend/src/views/public/mine/Favorites.vue
aid 66827c0199 Day5: 公众端响应式修复 + 点赞收藏功能 + 报名作品合并 + 菜单同步
- 公众端桌面端新增顶部导航菜单,修复横屏模式菜单消失问题
- 实现点赞/收藏 toggle API(含批量状态查询、我的收藏列表)
- 作品详情页新增互动栏(点赞/收藏按钮,乐观更新+动效)
- 广场卡片支持点赞交互
- 报名列表合并展示参赛作品,移除独立的「我的作品」页面
- 个人中心新增「我的收藏」入口
- menus.json 与数据库完整同步,更新初始化脚本租户分配逻辑
- Vite 开启局域网访问

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 13:56:20 +08:00

139 lines
3.9 KiB
Vue

<template>
<div class="favorites-page">
<div class="page-header">
<h2>我的收藏</h2>
</div>
<div v-if="loading" class="loading-wrap"><a-spin /></div>
<div v-else-if="list.length === 0" class="empty-wrap">
<a-empty description="还没有收藏任何作品">
<a-button type="primary" shape="round" @click="$router.push('/p/gallery')">
去发现作品
</a-button>
</a-empty>
</div>
<div v-else class="works-grid">
<div
v-for="item in list"
:key="item.id"
class="work-card"
@click="$router.push(`/p/works/${item.work.id}`)"
>
<div class="card-cover">
<img v-if="item.work.coverUrl" :src="item.work.coverUrl" :alt="item.work.title" />
<div v-else class="cover-placeholder">
<picture-outlined />
</div>
</div>
<div class="card-body">
<h3>{{ item.work.title }}</h3>
<div class="card-author">
<a-avatar :size="20" :src="item.work.creator?.avatar">
{{ item.work.creator?.nickname?.charAt(0) }}
</a-avatar>
<span>{{ item.work.creator?.nickname }}</span>
</div>
<div class="card-stats">
<span><heart-outlined /> {{ item.work.likeCount || 0 }}</span>
<span><eye-outlined /> {{ item.work.viewCount || 0 }}</span>
</div>
</div>
</div>
</div>
<div v-if="total > pageSize" class="pagination-wrap">
<a-pagination
v-model:current="page"
:total="total"
:page-size="pageSize"
simple
@change="fetchList"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PictureOutlined, HeartOutlined, EyeOutlined } from '@ant-design/icons-vue'
import { publicInteractionApi } from '@/api/public'
const list = ref<any[]>([])
const loading = ref(true)
const page = ref(1)
const pageSize = 12
const total = ref(0)
const fetchList = async () => {
loading.value = true
try {
const res = await publicInteractionApi.myFavorites({ page: page.value, pageSize })
list.value = res.list
total.value = res.total
} catch {
message.error('获取收藏列表失败')
} finally {
loading.value = false
}
}
onMounted(fetchList)
</script>
<style scoped lang="scss">
$primary: #6366f1;
.favorites-page { max-width: 700px; margin: 0 auto; }
.page-header {
margin-bottom: 16px;
h2 { font-size: 20px; font-weight: 700; color: #1e1b4b; margin: 0; }
}
.loading-wrap, .empty-wrap { padding: 60px 0; display: flex; justify-content: center; }
.works-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
@media (min-width: 640px) { grid-template-columns: repeat(3, 1fr); }
}
.work-card {
background: #fff;
border-radius: 14px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
border: 1px solid rgba($primary, 0.04);
&:hover { box-shadow: 0 4px 20px rgba($primary, 0.1); transform: translateY(-2px); }
.card-cover {
aspect-ratio: 3/4;
background: #f5f3ff;
img { width: 100%; height: 100%; object-fit: cover; }
.cover-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; font-size: 28px; color: #d1d5db; }
}
.card-body {
padding: 10px 12px;
h3 { font-size: 13px; font-weight: 600; color: #1e1b4b; margin: 0 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card-author {
display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
span { font-size: 11px; color: #6b7280; }
}
.card-stats {
display: flex; gap: 12px;
span { font-size: 11px; color: #9ca3af; display: flex; align-items: center; gap: 3px; }
}
}
}
.pagination-wrap { display: flex; justify-content: center; padding: 24px 0; }
</style>