增加首页推荐
This commit is contained in:
@@ -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>
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user