diff --git a/backend/README.md b/backend/README.md index 6d3e77f..a75ac8a 100644 --- a/backend/README.md +++ b/backend/README.md @@ -51,6 +51,8 @@ backend/ - `GET /api/artwork/:id/preview` - 获取作品预览 - `GET /api/artwork/:id/images` - 获取作品图片URL - 参数: `size` (small/medium/large/original) +- `GET /api/artwork/:id/related` - 获取相关推荐作品 + - 参数: `offset`, `limit` ### 排行榜相关 diff --git a/backend/routes/artwork.js b/backend/routes/artwork.js index ddf008f..281d581 100644 --- a/backend/routes/artwork.js +++ b/backend/routes/artwork.js @@ -266,4 +266,45 @@ router.post('/:id/bookmark', async (req, res) => { } }); +/** + * 获取相关推荐作品 + * GET /api/artwork/:id/related + */ +router.get('/:id/related', async (req, res) => { + try { + const { id } = req.params; + const { offset = 0, limit = 30 } = req.query; + + if (!id || isNaN(parseInt(id))) { + return res.status(400).json({ + success: false, + error: 'Invalid artwork ID' + }); + } + + const artworkService = new ArtworkService(req.backend.getAuth()); + const result = await artworkService.getRelatedArtworks(parseInt(id), { + offset: parseInt(offset), + limit: parseInt(limit) + }); + + if (result.success) { + res.json({ + success: true, + data: result.data + }); + } else { + res.status(404).json({ + success: false, + error: result.error + }); + } + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/services/artwork.js b/backend/services/artwork.js index b507181..3758cd8 100644 --- a/backend/services/artwork.js +++ b/backend/services/artwork.js @@ -366,6 +366,48 @@ class ArtworkService { } } + /** + * 获取相关推荐作品 + */ + async getRelatedArtworks(artworkId, options = {}) { + try { + const { offset = 0, limit = 30 } = options; + + if (!artworkId || isNaN(parseInt(artworkId))) { + return { + success: false, + error: 'Invalid artwork ID' + }; + } + + const params = { + illust_id: parseInt(artworkId), + offset: parseInt(offset), + filter: 'for_ios' + }; + + const response = await this.makeRequest('GET', '/v2/illust/related', params); + + return { + success: true, + data: { + artworks: response.illusts || [], + next_url: response.next_url, + total: response.illusts ? response.illusts.length : 0, + source_artwork_id: artworkId + } + }; + } catch (error) { + logger.error('Get related artworks error:', error.message); + logger.error('Get related artworks error details:', error.response?.data); + + return { + success: false, + error: error.message || 'Failed to get related artworks' + }; + } + } + /** * 发送API请求 */ diff --git a/ui/package.json b/ui/package.json index ee7b163..cc36e77 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { - "name": "pixivdownload", - "version": "0.0.0", + "name": "pixivmanager", + "version": "1.0.4", "private": true, "type": "module", "engines": { diff --git a/ui/src/components/artwork/ArtworkRecommendations.vue b/ui/src/components/artwork/ArtworkRecommendations.vue new file mode 100644 index 0000000..b8739b6 --- /dev/null +++ b/ui/src/components/artwork/ArtworkRecommendations.vue @@ -0,0 +1,317 @@ + + + + + \ No newline at end of file diff --git a/ui/src/services/artwork.ts b/ui/src/services/artwork.ts index 103fc65..c59c584 100644 --- a/ui/src/services/artwork.ts +++ b/ui/src/services/artwork.ts @@ -97,6 +97,18 @@ class ArtworkService { return apiService.get<{ artworks: Artwork[]; next_url?: string; total: number }>(`/api/artwork/bookmarks?${queryParams.toString()}`); } + + /** + * 获取相关推荐作品 + */ + async getRelatedArtworks(artworkId: number, params: { offset?: number; limit?: number } = {}): Promise> { + const queryParams = new URLSearchParams(); + + if (params.offset !== undefined) queryParams.append('offset', params.offset.toString()); + if (params.limit !== undefined) queryParams.append('limit', params.limit.toString()); + + return apiService.get<{ artworks: Artwork[]; next_url?: string; total: number; source_artwork_id: number }>(`/api/artwork/${artworkId}/related?${queryParams.toString()}`); + } } export const artworkService = new ArtworkService(); diff --git a/ui/src/views/ArtworkView.vue b/ui/src/views/ArtworkView.vue index 5e8497a..89cf25f 100644 --- a/ui/src/views/ArtworkView.vue +++ b/ui/src/views/ArtworkView.vue @@ -25,10 +25,25 @@ + :canNavigateNext="canNavigateToNext" :selected-tags="selectedTags" @download="handleDownload" + @bookmark="handleBookmark" @go-back="goBackToArtist" @navigate-previous="navigateToPrevious" + @navigate-next="navigateToNext" @tag-click="handleTagClick" /> + + + +
+
+ +
+ + +
+ +
@@ -51,6 +66,7 @@ import LoadingSpinner from '@/components/common/LoadingSpinner.vue'; import DownloadProgress from '@/components/download/DownloadProgress.vue'; import ArtworkGallery from '@/components/artwork/ArtworkGallery.vue'; import ArtworkInfoPanel from '@/components/artwork/ArtworkInfoPanel.vue'; +import ArtworkRecommendations from '@/components/artwork/ArtworkRecommendations.vue'; const route = useRoute(); const router = useRouter(); @@ -85,6 +101,22 @@ const hasPreviousPages = ref(false); // 是否还有上一页 const isLoadingMore = ref(false); // 是否正在加载更多页面 const isLoadingPrevious = ref(false); // 是否正在加载上一页 +// 推荐作品开关状态 +const showRecommendations = ref(true); + +// 初始化推荐开关状态(从localStorage读取) +const initializeRecommendationsState = () => { + const saved = localStorage.getItem('artwork-show-recommendations'); + if (saved !== null) { + showRecommendations.value = JSON.parse(saved); + } +}; + +// 监听推荐开关状态变化并保存到localStorage +watch(showRecommendations, (newValue) => { + localStorage.setItem('artwork-show-recommendations', JSON.stringify(newValue)); +}); + // 导航相关计算属性 const showNavigation = computed(() => { return !!(route.query.artistId && route.query.artworkType); @@ -95,12 +127,12 @@ const previousArtwork = computed(() => { if (currentArtworkIndex.value === 0 && hasPreviousPages.value) { return null; // 返回null,但hasPreviousPages会控制按钮状态 } - + // 如果当前作品不在第一个位置,返回前一个作品 if (currentArtworkIndex.value > 0) { return artistArtworks.value[currentArtworkIndex.value - 1]; } - + return null; }); @@ -370,7 +402,7 @@ const fetchArtistArtworks = async (page = 1, append = false, prepend = false) => hasMorePages.value = response.data.artworks.length === pageSize; // 检查是否还有上一页 hasPreviousPages.value = page > 1; - + // 更新当前导航页码 if (!append && !prepend) { navigationCurrentPage.value = page; @@ -412,7 +444,7 @@ const fetchArtistArtworks = async (page = 1, append = false, prepend = false) => // 加载下一页作品 const loadNextPage = async () => { if (!hasMorePages.value || isLoadingMore.value) return; - + const nextPage = navigationCurrentPage.value + 1; await fetchArtistArtworks(nextPage, true, false); // 更新当前导航页码 @@ -422,7 +454,7 @@ const loadNextPage = async () => { // 加载上一页作品 const loadPreviousPage = async () => { if (!hasPreviousPages.value || isLoadingPrevious.value) return; - + const previousPage = navigationCurrentPage.value - 1; await fetchArtistArtworks(previousPage, false, true); // 更新当前导航页码 @@ -432,7 +464,7 @@ const loadPreviousPage = async () => { // 辅助函数:更新returnUrl中的页码 const updateReturnUrlPage = (returnUrl: string, newPage: number): string => { if (!returnUrl) return returnUrl; - + // 如果returnUrl包含页码,更新它 if (returnUrl.includes('page=')) { return returnUrl.replace(/page=\d+/, `page=${newPage}`); @@ -462,7 +494,7 @@ const navigateToNext = async () => { // 计算返回链接的页码和当前导航页码 let returnPage = parseInt(route.query.page as string) || 1; let currentNavPage = navigationCurrentPage.value; - + if (currentArtworkIndex.value === artistArtworks.value.length - 1) { // 如果跳转到下一页的作品,返回链接应该指向当前页,当前导航页码递增 returnPage = currentNavPage; // 返回时应该在第x页 @@ -507,7 +539,7 @@ const navigateToPrevious = async () => { // 计算返回链接的页码和当前导航页码 let returnPage = parseInt(route.query.page as string) || 1; let currentNavPage = navigationCurrentPage.value; - + if (currentArtworkIndex.value === 0) { // 如果跳转到上一页的作品,返回链接应该指向当前页,当前导航页码递减 returnPage = currentNavPage; // 返回时应该在第x页 @@ -537,18 +569,18 @@ const navigateToPrevious = async () => { const goBackToArtist = () => { const returnUrl = route.query.returnUrl as string; let targetPath = returnUrl || `/artist/${route.query.artistId}`; - + // 如果返回链接不包含页码,添加当前页码 if (targetPath && !targetPath.includes('page=')) { const separator = targetPath.includes('?') ? '&' : '?'; targetPath += `${separator}page=${navigationCurrentPage.value}`; } - + if (targetPath) { // 获取当前保存的滚动位置(如果有的话) const savedScrollKey = `scroll_${targetPath}`; const savedPosition = sessionStorage.getItem(savedScrollKey); - + // 如果没有保存的滚动位置,设置一个默认位置(通常是之前访问时的位置) if (!savedPosition && route.query.scrollTop) { const scrollPosition = { @@ -557,7 +589,7 @@ const goBackToArtist = () => { }; saveScrollPositionForPath(targetPath, scrollPosition); } - + router.push(targetPath); } }; @@ -710,6 +742,9 @@ onMounted(() => { document.addEventListener('keydown', handleKeydown); document.addEventListener('keydown', handleKeyDown); document.addEventListener('keyup', handleKeyUp); + + // 初始化推荐开关状态 + initializeRecommendationsState(); }); // 组件卸载时移除事件监听 @@ -754,7 +789,70 @@ onUnmounted(() => { pointer-events: none; } +.recommendations-section { + margin-top: 3rem; + padding: 2rem; + background-color: #f0f2f5; + border-radius: 10px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} +.recommendations-toggle { + display: flex; + align-items: center; + margin-bottom: 1.5rem; + padding-left: 0.5rem; +} + +.toggle-label { + display: flex; + align-items: center; + cursor: pointer; + font-size: 0.9rem; + color: #333; +} + +.toggle-checkbox { + display: none; + /* Hide the default checkbox */ +} + +.toggle-switch { + position: relative; + width: 40px; + height: 20px; + background-color: #ccc; + border-radius: 10px; + margin: 0 10px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.toggle-switch::before { + content: ""; + position: absolute; + width: 16px; + height: 16px; + border-radius: 50%; + background-color: white; + top: 2px; + left: 2px; + transition: transform 0.3s ease; +} + +.toggle-checkbox:checked+.toggle-switch { + background-color: #4caf50; + /* Green color for checked */ +} + +.toggle-checkbox:checked+.toggle-switch::before { + transform: translateX(20px); +} + +.toggle-text { + font-size: 0.9rem; + color: #333; +} @media (max-width: 1024px) { .artwork-content { diff --git a/ui/src/views/SearchView.vue b/ui/src/views/SearchView.vue index 5d23eb7..5fb831a 100644 --- a/ui/src/views/SearchView.vue +++ b/ui/src/views/SearchView.vue @@ -95,7 +95,7 @@