diff --git a/README.md b/README.md index 8f1d8a1..4a08184 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Pixiv 下载浏览管理器是一个基于 Web 的应用程序,提供以下功 - 📥 作品下载管理 - 👤 作者搜索 - 🆔 作品ID搜索 +- 📁 本地仓库管理 +- 🖼️ 画廊模式浏览 +- 🔍 作品检索和分类 ## 🚀 快速开始 @@ -72,11 +75,11 @@ Pixiv 下载浏览管理器是一个基于 Web 的应用程序,提供以下功 ## 📱 功能展示 -### 主界面 +### 脚本启动 ![脚本启动](./pic/脚本启动.png) ### 搜索功能 -- **作品搜索** +- **作品搜索(下载过的会有提示)** ![搜索作品](./pic/搜索作品.png) - **作者搜索** @@ -85,9 +88,22 @@ Pixiv 下载浏览管理器是一个基于 Web 的应用程序,提供以下功 - **作品ID搜索** ![作品id搜索](./pic/作品id搜索.png) +### 列表轮换 +- **通过作者搜索进入作品可以上下一个切换** +- ![列表轮换](./pic/列表轮换.png) + ### 下载管理 ![下载管理](./pic/下载管理.png) +### 仓库管理 +![仓库管理](./pic/仓库管理.png) + +### 作品检索 +![作品检索](./pic/作品检索.png) + +### 画廊模式 +![画廊模式](./pic/画廊模式.png) + ## 🛠️ 开发说明 本项目刚刚建立,很多功能还不够完善,欢迎大家一起参与开发! diff --git a/backend/routes/artist.js b/backend/routes/artist.js index 6799e7d..3fc2d68 100644 --- a/backend/routes/artist.js +++ b/backend/routes/artist.js @@ -2,6 +2,62 @@ const express = require('express'); const router = express.Router(); const ArtistService = require('../services/artist'); +/** + * 搜索作者 + * GET /api/artist/search + */ +router.get('/search', async (req, res) => { + try { + const { keyword, offset = 0, limit = 30 } = req.query; + + if (!keyword || keyword.trim() === '') { + return res.status(400).json({ + success: false, + error: '搜索关键词不能为空' + }); + } + + const artistService = new ArtistService(req.backend.getAuth()); + const result = await artistService.searchArtists({ + keyword: keyword.trim(), + offset: parseInt(offset), + limit: parseInt(limit) + }); + + if (result.success) { + // 转换数据格式以匹配前端期望 + const artists = (result.data.users || []).map(user => ({ + id: user.user.id, + name: user.user.name, + account: user.user.account, + profile_image_urls: user.user.profile_image_urls, + total_illusts: 0, + total_manga: 0, + total_followers: 0, + is_followed: user.user.is_followed || false + })); + + res.json({ + success: true, + data: { + artists, + total: artists.length + } + }); + } else { + res.status(404).json({ + success: false, + error: result.error + }); + } + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + /** * 获取当前用户关注的作者列表 * GET /api/artist/following diff --git a/pic/仓库管理.png b/pic/仓库管理.png new file mode 100644 index 0000000..7ab86ef Binary files /dev/null and b/pic/仓库管理.png differ diff --git a/pic/作品id搜索.png b/pic/作品id搜索.png index 3dff95c..ad7d05d 100644 Binary files a/pic/作品id搜索.png and b/pic/作品id搜索.png differ diff --git a/pic/作品检索.png b/pic/作品检索.png new file mode 100644 index 0000000..cb333c3 Binary files /dev/null and b/pic/作品检索.png differ diff --git a/pic/列表轮换.png b/pic/列表轮换.png new file mode 100644 index 0000000..40ea647 Binary files /dev/null and b/pic/列表轮换.png differ diff --git a/pic/画廊模式.png b/pic/画廊模式.png new file mode 100644 index 0000000..953faf8 Binary files /dev/null and b/pic/画廊模式.png differ diff --git a/ui/dist.zip b/ui/dist.zip index 4d35298..1780338 100644 Binary files a/ui/dist.zip and b/ui/dist.zip differ diff --git a/ui/src/services/artist.ts b/ui/src/services/artist.ts index 35b66c2..5e13c14 100644 --- a/ui/src/services/artist.ts +++ b/ui/src/services/artist.ts @@ -19,6 +19,12 @@ export interface ArtistFollowersOptions { limit?: number; } +export interface SearchArtistsOptions { + keyword: string; + offset?: number; + limit?: number; +} + class ArtistService { /** * 获取作者信息 @@ -88,6 +94,20 @@ class ArtistService { const url = query ? `/api/artist/following?${query}` : '/api/artist/following'; return apiService.get<{ artists: Artist[]; total: number }>(url); } + + /** + * 搜索作者 + */ + async searchArtists(options: SearchArtistsOptions): Promise> { + const params = new URLSearchParams(); + params.append('keyword', options.keyword); + if (options.offset !== undefined) params.append('offset', options.offset.toString()); + if (options.limit !== undefined) params.append('limit', options.limit.toString()); + + const query = params.toString(); + const url = `/api/artist/search?${query}`; + return apiService.get<{ artists: Artist[]; total: number }>(url); + } } export const artistService = new ArtistService(); diff --git a/ui/src/stores/artist.ts b/ui/src/stores/artist.ts new file mode 100644 index 0000000..ab23fba --- /dev/null +++ b/ui/src/stores/artist.ts @@ -0,0 +1,184 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; +import artistService from '@/services/artist'; +import type { Artist } from '@/types'; + +export const useArtistStore = defineStore('artist', () => { + // 状态 + const followingArtists = ref([]); + const searchResults = ref([]); + const loading = ref(false); + const error = ref(null); + const lastFetchTime = ref(0); + const cacheExpiry = 5 * 60 * 1000; // 5分钟缓存 + + // 计算属性 + const isDataStale = computed(() => { + return Date.now() - lastFetchTime.value > cacheExpiry; + }); + + const hasFollowingArtists = computed(() => { + return followingArtists.value.length > 0; + }); + + const hasSearchResults = computed(() => { + return searchResults.value.length > 0; + }); + + // 获取关注的作者 + const fetchFollowingArtists = async (forceRefresh = false) => { + // 如果数据不是过期的且不是强制刷新,直接返回缓存的数据 + if (!forceRefresh && !isDataStale.value && hasFollowingArtists.value) { + return { + success: true, + data: { artists: followingArtists.value } + }; + } + + try { + loading.value = true; + error.value = null; + + const response = await artistService.getFollowingArtists(); + if (response.success && response.data) { + followingArtists.value = response.data.artists; + lastFetchTime.value = Date.now(); + } else { + throw new Error(response.error || '获取关注列表失败'); + } + + return response; + } catch (err) { + error.value = err instanceof Error ? err.message : '获取关注列表失败'; + console.error('获取关注列表失败:', err); + throw err; + } finally { + loading.value = false; + } + }; + + // 搜索作者 + const searchArtists = async (keyword: string) => { + if (!keyword.trim()) { + searchResults.value = []; + return { success: true, data: { artists: [] } }; + } + + try { + const response = await artistService.searchArtists({ keyword }); + if (response.success && response.data) { + searchResults.value = response.data.artists; + return response; + } else { + throw new Error(response.error || '搜索失败'); + } + } catch (err) { + error.value = err instanceof Error ? err.message : '搜索失败'; + console.error('搜索失败:', err); + throw err; + } + }; + + // 关注作者 + const followArtist = async (artistId: number) => { + try { + const response = await artistService.followArtist(artistId, 'follow'); + + if (response.success) { + // 更新搜索结果的关注状态 + const artist = searchResults.value.find(a => a.id === artistId); + if (artist) { + artist.is_followed = true; + } + + // 添加到关注列表 + const artistToAdd = searchResults.value.find(a => a.id === artistId); + if (artistToAdd && !followingArtists.value.find(a => a.id === artistId)) { + followingArtists.value.push(artistToAdd); + } + } else { + throw new Error(response.error || '关注失败'); + } + + return response; + } catch (err) { + error.value = err instanceof Error ? err.message : '关注失败'; + console.error('关注失败:', err); + throw err; + } + }; + + // 取消关注 + const unfollowArtist = async (artistId: number) => { + try { + const response = await artistService.followArtist(artistId, 'unfollow'); + + if (response.success) { + // 从关注列表中移除 + followingArtists.value = followingArtists.value.filter(a => a.id !== artistId); + + // 更新搜索结果的关注状态 + const artist = searchResults.value.find(a => a.id === artistId); + if (artist) { + artist.is_followed = false; + } + } else { + throw new Error(response.error || '取消关注失败'); + } + + return response; + } catch (err) { + error.value = err instanceof Error ? err.message : '取消关注失败'; + console.error('取消关注失败:', err); + throw err; + } + }; + + // 清除搜索结果 + const clearSearchResults = () => { + searchResults.value = []; + }; + + // 清除错误 + const clearError = () => { + error.value = null; + }; + + // 强制刷新数据 + const refreshData = async () => { + return await fetchFollowingArtists(true); + }; + + // 重置状态 + const reset = () => { + followingArtists.value = []; + searchResults.value = []; + loading.value = false; + error.value = null; + lastFetchTime.value = 0; + }; + + return { + // 状态 + followingArtists, + searchResults, + loading, + error, + lastFetchTime, + + // 计算属性 + isDataStale, + hasFollowingArtists, + hasSearchResults, + + // 方法 + fetchFollowingArtists, + searchArtists, + followArtist, + unfollowArtist, + clearSearchResults, + clearError, + refreshData, + reset + }; +}); \ No newline at end of file diff --git a/ui/src/utils/formatters.ts b/ui/src/utils/formatters.ts new file mode 100644 index 0000000..4d89f2c --- /dev/null +++ b/ui/src/utils/formatters.ts @@ -0,0 +1,21 @@ +/** + * 格式化文件大小 + * @param bytes 字节数 + * @returns 格式化后的文件大小字符串 + */ +export const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +/** + * 生成文件预览URL + * @param filePath 文件路径 + * @returns 预览URL + */ +export const getPreviewUrl = (filePath: string): string => { + return `/api/repository/preview?path=${encodeURIComponent(filePath)}` +} \ No newline at end of file diff --git a/ui/src/views/ArtistsView.vue b/ui/src/views/ArtistsView.vue index 2726f34..49d8402 100644 --- a/ui/src/views/ArtistsView.vue +++ b/ui/src/views/ArtistsView.vue @@ -18,25 +18,47 @@ + -
- +
+
-
- +
+
-

关注的作者

+
+

关注的作者

+
+ + + + + 数据已过期 + + + + + + 数据已缓存 + +
+
-
+
@@ -91,16 +113,19 @@

暂无关注的作者

关注喜欢的作者,在这里管理他们

+
+ 💡 提示:数据已缓存,点击刷新按钮获取最新数据 +
-
+

搜索结果

@@ -158,63 +183,40 @@ \ No newline at end of file diff --git a/ui/src/views/repository/ArtworkModal.vue b/ui/src/views/repository/ArtworkModal.vue new file mode 100644 index 0000000..0521746 --- /dev/null +++ b/ui/src/views/repository/ArtworkModal.vue @@ -0,0 +1,190 @@ + + + + + \ No newline at end of file diff --git a/ui/src/views/repository/RepositoryBrowse.vue b/ui/src/views/repository/RepositoryBrowse.vue new file mode 100644 index 0000000..0df3db9 --- /dev/null +++ b/ui/src/views/repository/RepositoryBrowse.vue @@ -0,0 +1,1445 @@ + + + + + \ No newline at end of file diff --git a/ui/src/views/repository/RepositoryConfig.vue b/ui/src/views/repository/RepositoryConfig.vue new file mode 100644 index 0000000..f54d7b2 --- /dev/null +++ b/ui/src/views/repository/RepositoryConfig.vue @@ -0,0 +1,389 @@ + + + + + \ No newline at end of file diff --git a/ui/src/views/repository/RepositoryMigration.vue b/ui/src/views/repository/RepositoryMigration.vue new file mode 100644 index 0000000..a7384a7 --- /dev/null +++ b/ui/src/views/repository/RepositoryMigration.vue @@ -0,0 +1,244 @@ + + + + + \ No newline at end of file diff --git a/ui/src/views/repository/RepositoryStats.vue b/ui/src/views/repository/RepositoryStats.vue new file mode 100644 index 0000000..d3a8f1b --- /dev/null +++ b/ui/src/views/repository/RepositoryStats.vue @@ -0,0 +1,78 @@ + + + + + \ No newline at end of file