增加首页推荐

This commit is contained in:
2025-09-04 11:21:28 +08:00
parent 77382de53b
commit 2cc36fa8b9
2 changed files with 361 additions and 0 deletions
@@ -0,0 +1,350 @@
<template>
<div class="random-recommendations">
<div class="container">
<div class="section-header">
<h2 class="section-title">为你推荐</h2>
<div class="header-actions">
<button @click="refreshRecommendations" class="btn btn-secondary btn-small" :disabled="loading">
<svg v-if="!loading" viewBox="0 0 24 24" fill="currentColor" class="refresh-icon">
<path
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" />
</svg>
<LoadingSpinner v-else text="" />
刷新推荐
</button>
</div>
</div>
<div v-if="loading && recommendations.length === 0" class="loading-section">
<LoadingSpinner text="正在获取推荐..." />
</div>
<div v-else-if="error" class="error-section">
<ErrorMessage :error="error" @dismiss="clearError" />
</div>
<div v-else-if="recommendations.length === 0" class="empty-section">
<div class="empty-content">
<svg viewBox="0 0 24 24" fill="currentColor" class="empty-icon">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" />
</svg>
<p class="empty-text">暂无推荐内容</p>
<p class="empty-subtext">关注一些作者后这里会显示他们的最新作品</p>
</div>
</div>
<div v-else class="recommendations-grid">
<ArtworkCard v-for="artwork in recommendations" :key="artwork.id" :artwork="artwork"
@click="handleArtworkClick" />
</div>
<div v-if="recommendations.length > 0" class="section-footer">
<p class="footer-text">
基于你关注的 {{ selectedArtists.length }} 位作者的最新作品推荐
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { useArtistStore } from '@/stores/artist';
import artistService from '@/services/artist';
import type { Artist, Artwork } from '@/types';
import ArtworkCard from '@/components/artwork/ArtworkCard.vue';
import LoadingSpinner from '@/components/common/LoadingSpinner.vue';
import ErrorMessage from '@/components/common/ErrorMessage.vue';
const router = useRouter();
const artistStore = useArtistStore();
// 状态
const loading = ref(false);
const error = ref<string | null>(null);
const recommendations = ref<Artwork[]>([]);
const selectedArtists = ref<Artist[]>([]);
// 计算属性
const hasFollowingArtists = computed(() => artistStore.followingArtists.length > 0);
// 随机选择作者
const selectRandomArtists = (artists: Artist[], count: number): Artist[] => {
if (artists.length <= count) return artists;
const shuffled = [...artists].sort(() => 0.5 - Math.random());
return shuffled.slice(0, count);
};
// 获取作者最新作品
const fetchArtistArtworks = async (artist: Artist): Promise<Artwork[]> => {
try {
const response = await artistService.getArtistArtworks(artist.id, {
type: 'art',
limit: 3 // 每个作者获取3个最新作品
});
if (response.success && response.data) {
return response.data.artworks || [];
}
return [];
} catch (err) {
console.error(`获取作者 ${artist.name} 作品失败:`, err);
return [];
}
};
// 生成推荐
const generateRecommendations = async () => {
if (!hasFollowingArtists.value) {
recommendations.value = [];
selectedArtists.value = [];
return;
}
try {
loading.value = true;
error.value = null;
// 随机选择3-4个作者
const artistCount = Math.min(4, Math.max(3, Math.floor(artistStore.followingArtists.length * 0.3)));
const randomArtists = selectRandomArtists(artistStore.followingArtists, artistCount);
selectedArtists.value = randomArtists;
// 获取所有作者的最新作品
const allArtworks: Artwork[] = [];
for (const artist of randomArtists) {
const artworks = await fetchArtistArtworks(artist);
allArtworks.push(...artworks);
}
// 按创建时间排序,取最新的作品
const sortedArtworks = allArtworks
.sort((a, b) => new Date(b.create_date).getTime() - new Date(a.create_date).getTime())
.slice(0, 12); // 最多显示12个作品
recommendations.value = sortedArtworks;
} catch (err) {
error.value = err instanceof Error ? err.message : '获取推荐失败';
console.error('生成推荐失败:', err);
} finally {
loading.value = false;
}
};
// 刷新推荐
const refreshRecommendations = async () => {
await generateRecommendations();
};
// 处理作品点击
const handleArtworkClick = (artwork: Artwork) => {
router.push(`/artwork/${artwork.id}`);
};
// 清除错误
const clearError = () => {
error.value = null;
};
onMounted(async () => {
// 如果还没有关注作者数据,先获取
if (artistStore.followingArtists.length === 0) {
try {
await artistStore.fetchFollowingArtists();
} catch (err) {
console.error('获取关注作者失败:', err);
}
}
// 生成推荐
await generateRecommendations();
});
// 监听关注作者列表变化
watch(() => hasFollowingArtists.value, (newValue) => {
if (newValue && recommendations.value.length === 0) {
generateRecommendations();
}
});
</script>
<style scoped>
.random-recommendations {
margin: 2rem 0;
padding: 0 2rem;
}
.random-recommendations .container {
max-width: 1200px;
margin: 0 auto;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.section-title {
font-size: 2rem;
font-weight: 700;
color: #1f2937;
margin: 0;
}
.header-actions {
display: flex;
gap: 1rem;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 0.875rem;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-secondary:hover:not(:disabled) {
background: #e5e7eb;
}
.btn-small {
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
}
.refresh-icon {
width: 1rem;
height: 1rem;
}
.loading-section {
display: flex;
justify-content: center;
padding: 3rem 0;
}
.error-section {
margin-bottom: 2rem;
}
.empty-section {
text-align: center;
padding: 4rem 0;
}
.empty-content {
max-width: 400px;
margin: 0 auto;
}
.empty-icon {
width: 4rem;
height: 4rem;
color: #9ca3af;
margin-bottom: 1rem;
}
.empty-text {
font-size: 1.25rem;
font-weight: 600;
color: #6b7280;
margin: 0 0 0.5rem 0;
}
.empty-subtext {
color: #9ca3af;
margin: 0;
line-height: 1.5;
}
.recommendations-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
max-width: 100%;
}
/* 确保网格能够更好地填充空间 */
.recommendations-grid:has(:nth-child(4n)) {
grid-template-columns: repeat(4, 1fr);
}
.recommendations-grid:has(:nth-child(3n)):not(:has(:nth-child(4n))) {
grid-template-columns: repeat(3, 1fr);
}
.recommendations-grid:has(:nth-child(2n)):not(:has(:nth-child(3n))) {
grid-template-columns: repeat(2, 1fr);
}
.section-footer {
text-align: center;
padding: 1rem 0;
border-top: 1px solid #e5e7eb;
}
.footer-text {
color: #6b7280;
font-size: 0.875rem;
margin: 0;
}
@media (max-width: 1200px) {
.recommendations-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
@media (max-width: 768px) {
.random-recommendations {
padding: 0 1rem;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.recommendations-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.section-title {
font-size: 1.5rem;
}
}
@media (max-width: 480px) {
.recommendations-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
</style>
+11
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted } from 'vue'; import { computed, onMounted } from 'vue';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import RandomRecommendations from '@/components/home/RandomRecommendations.vue';
const authStore = useAuthStore(); const authStore = useAuthStore();
@@ -35,6 +36,11 @@ onMounted(async () => {
</div> </div>
</div> </div>
<!-- 随机推荐区域 -->
<div class="recommendations-section">
<RandomRecommendations v-if="isLoggedIn" />
</div>
<div class="features-section"> <div class="features-section">
<div class="container"> <div class="container">
<h2 class="section-title">主要功能</h2> <h2 class="section-title">主要功能</h2>
@@ -207,6 +213,11 @@ onMounted(async () => {
padding: 0 2rem; padding: 0 2rem;
} }
.recommendations-section {
padding: 2rem 0;
background: white;
}
.features-section { .features-section {
padding: 4rem 0; padding: 4rem 0;
background: #f8fafc; background: #f8fafc;