From e7886215971218704595f2c72713fd236a366ca0 Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Tue, 26 Aug 2025 06:36:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8B=E4=B8=80=E9=A1=B5?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E4=B8=8D=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E7=BC=93=E5=AD=98=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/src/App.vue | 4 + ui/src/components/common/SettingsWidget.vue | 633 ++++++++++++++++++++ ui/src/router/index.ts | 19 +- ui/src/services/cache.ts | 72 +++ ui/src/views/ArtistView.vue | 4 +- ui/src/views/ArtworkView.vue | 37 +- ui/src/views/RankingView.vue | 198 +++++- 7 files changed, 954 insertions(+), 13 deletions(-) create mode 100644 ui/src/components/common/SettingsWidget.vue create mode 100644 ui/src/services/cache.ts diff --git a/ui/src/App.vue b/ui/src/App.vue index cdeda5b..ef37623 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -2,6 +2,7 @@ import { RouterLink, RouterView } from 'vue-router' import { computed, onMounted } from 'vue' import { useAuthStore } from '@/stores/auth' +import SettingsWidget from '@/components/common/SettingsWidget.vue' const authStore = useAuthStore() @@ -68,6 +69,9 @@ onMounted(async () => {

© 2025 Pixiv Manager. 仅供学习和个人使用。

+ + + diff --git a/ui/src/components/common/SettingsWidget.vue b/ui/src/components/common/SettingsWidget.vue new file mode 100644 index 0000000..fab26cd --- /dev/null +++ b/ui/src/components/common/SettingsWidget.vue @@ -0,0 +1,633 @@ + + + + + \ No newline at end of file diff --git a/ui/src/router/index.ts b/ui/src/router/index.ts index 3292b0d..317e219 100644 --- a/ui/src/router/index.ts +++ b/ui/src/router/index.ts @@ -63,7 +63,24 @@ const router = createRouter({ component: () => import('@/views/BookmarksView.vue'), meta: { requiresAuth: true } } - ] + ], + scrollBehavior(to, from, savedPosition) { + // 如果有保存的位置(浏览器前进/后退),则恢复到该位置 + if (savedPosition) { + return savedPosition + } + + // 如果有锚点,则滚动到锚点位置 + if (to.hash) { + return { + el: to.hash, + behavior: 'smooth' + } + } + + // 否则滚动到页面顶部 + return { top: 0 } + } }) // 路由守卫 diff --git a/ui/src/services/cache.ts b/ui/src/services/cache.ts new file mode 100644 index 0000000..edcf313 --- /dev/null +++ b/ui/src/services/cache.ts @@ -0,0 +1,72 @@ +import apiService from './api'; +import type { ApiResponse } from '@/types'; + +export interface CacheConfig { + maxAge: number; + maxSize: number; + cleanupInterval: number; + enabled: boolean; + proxy: { + enabled: boolean; + timeout: number; + retryCount: number; + retryDelay: number; + }; + allowedExtensions: string[]; + lastUpdated: string; +} + +export interface CacheStats { + fileCount: number; + totalSize: number; + maxSize: number; + maxAge: number; + usagePercentage: number; +} + +class CacheService { + /** + * 获取缓存统计信息 + */ + async getCacheStats(): Promise> { + return apiService.get('/api/proxy/cache/stats'); + } + + /** + * 获取缓存配置 + */ + async getCacheConfig(): Promise> { + return apiService.get('/api/proxy/cache/config'); + } + + /** + * 更新缓存配置 + */ + async updateCacheConfig(config: Partial): Promise> { + return apiService.put('/api/proxy/cache/config', config); + } + + /** + * 重置缓存配置为默认值 + */ + async resetCacheConfig(): Promise> { + return apiService.post('/api/proxy/cache/config/reset'); + } + + /** + * 清理所有缓存 + */ + async clearAllCache(): Promise> { + return apiService.delete<{ message: string }>('/api/proxy/cache'); + } + + /** + * 清理过期缓存 + */ + async clearExpiredCache(): Promise> { + return apiService.delete<{ message: string }>('/api/proxy/cache/expired'); + } +} + +export const cacheService = new CacheService(); +export default cacheService; \ No newline at end of file diff --git a/ui/src/views/ArtistView.vue b/ui/src/views/ArtistView.vue index e15dc3b..4926540 100644 --- a/ui/src/views/ArtistView.vue +++ b/ui/src/views/ArtistView.vue @@ -100,7 +100,7 @@ @@ -324,7 +324,7 @@ const fetchArtworks = async (page = 1, isJumpToPage = false) => { } } catch (err) { console.error('获取作品列表失败:', err); - + // 只有在跳转到指定页面失败时才显示错误 if (isJumpToPage) { error.value = `跳转失败:无法跳转到第 ${page} 页`; diff --git a/ui/src/views/ArtworkView.vue b/ui/src/views/ArtworkView.vue index fc97a92..f3ef136 100644 --- a/ui/src/views/ArtworkView.vue +++ b/ui/src/views/ArtworkView.vue @@ -253,8 +253,9 @@ const fetchArtworkDetail = async () => { loading.value = true; error.value = null; - // 清理之前的任务状态 + // 立即清理所有下载相关状态 currentTask.value = null; + downloading.value = false; stopTaskStreaming(); const response = await artworkService.getArtworkDetail(artworkId); @@ -307,7 +308,10 @@ const handleDownload = async () => { if (!artwork.value) return; try { + // 清理之前的任务状态 + currentTask.value = null; downloading.value = true; + // 如果已经下载过,则强制重新下载(跳过现有文件检查) const skipExisting = !isDownloaded.value; const response = await downloadService.downloadArtwork(artwork.value.id, { @@ -383,9 +387,12 @@ const startTaskStreaming = (taskId: string) => { // 延迟检查下载状态,确保文件写入完成 setTimeout(async () => { - await checkDownloadStatus(artwork.value!.id); - // 清理任务状态,显示下载完成状态 - currentTask.value = null; + // 检查当前页面是否还是同一个作品,避免页面切换后的状态更新 + if (artwork.value && artwork.value.id === task.artwork_id) { + await checkDownloadStatus(artwork.value.id); + // 清理任务状态,显示下载完成状态 + currentTask.value = null; + } }, 1000); } }, @@ -404,6 +411,9 @@ const stopTaskStreaming = () => { sseConnection.value(); sseConnection.value = null; } + // 确保清理任务状态 + currentTask.value = null; + downloading.value = false; }; // 更新任务状态 @@ -501,6 +511,11 @@ const fetchArtistArtworks = async () => { // 导航到上一个作品 const navigateToPrevious = () => { if (previousArtwork.value && !loading.value) { + // 立即清理下载任务状态 + currentTask.value = null; + downloading.value = false; + stopTaskStreaming(); + // 立即设置加载状态 loading.value = true; @@ -519,6 +534,11 @@ const navigateToPrevious = () => { // 导航到下一个作品 const navigateToNext = () => { if (nextArtwork.value && !loading.value) { + // 立即清理下载任务状态 + currentTask.value = null; + downloading.value = false; + stopTaskStreaming(); + // 立即设置加载状态 loading.value = true; @@ -630,8 +650,12 @@ watch(() => route.params.id, (newId, oldId) => { // 如果是同一个ID,不重复加载 if (newId === oldId) return; - // 清理之前的任务状态 + // 确保页面滚动到顶部 + window.scrollTo(0, 0); + + // 立即清理所有下载相关状态 currentTask.value = null; + downloading.value = false; stopTaskStreaming(); // 重新获取作品详情 @@ -660,6 +684,9 @@ const handleKeydown = (event: KeyboardEvent) => { }; onMounted(() => { + // 确保页面滚动到顶部 + window.scrollTo(0, 0); + fetchArtworkDetail(); if (showNavigation.value) { fetchArtistArtworks(); diff --git a/ui/src/views/RankingView.vue b/ui/src/views/RankingView.vue index a803446..d357ced 100644 --- a/ui/src/views/RankingView.vue +++ b/ui/src/views/RankingView.vue @@ -46,6 +46,18 @@ + +
+
+ + + +
+
+
第 {{ currentPage }} 页,共 {{ totalPages }} 页 @@ -89,6 +101,10 @@ const pageSize = ref(30); const totalCount = ref(0); const totalPages = ref(0); +// 跳转到指定页面相关 +const jumpPageInput = ref(''); +const jumping = ref(false); + // 缓存相关 const cache = ref>(new Map()); const cacheTimeout = ref>(new Map()); @@ -227,6 +243,16 @@ const fetchRankingData = async (page = 1) => { const handleModeChange = (mode: 'day' | 'week' | 'month') => { currentMode.value = mode; currentPage.value = 1; + + // 更新URL参数 + router.push({ + query: { + mode: mode, + type: currentType.value, + page: undefined + } + }); + fetchRankingData(1); }; @@ -234,12 +260,32 @@ const handleModeChange = (mode: 'day' | 'week' | 'month') => { const handleTypeChange = (type: 'art' | 'manga' | 'novel') => { currentType.value = type; currentPage.value = 1; + + // 更新URL参数 + router.push({ + query: { + mode: currentMode.value, + type: type, + page: undefined + } + }); + fetchRankingData(1); }; // 跳转到指定页面 const goToPage = (page: number) => { if (page < 1 || page > totalPages.value || page === currentPage.value) return; + + // 更新URL参数 + router.push({ + query: { + mode: currentMode.value, + type: currentType.value, + page: page.toString() + } + }); + fetchRankingData(page); }; @@ -275,13 +321,64 @@ const handleDownloadError = (errorMessage: string) => { error.value = errorMessage; }; +// 跳转到指定页面 +const handleJumpToPage = async () => { + const page = parseInt(jumpPageInput.value as string); + if (isNaN(page) || page < 1) { + error.value = '请输入有效的页码'; + return; + } + + jumping.value = true; + jumpPageInput.value = ''; // 清空输入框 + + // 更新URL参数 + router.push({ + query: { + mode: currentMode.value, + type: currentType.value, + page: page.toString() + } + }); + + try { + await fetchRankingData(page); + } finally { + jumping.value = false; + } +}; + // 监听路由变化 watch(() => route.query, () => { - // 检查是否有返回的页面信息 - const returnPage = parseInt(route.query.page as string); - if (returnPage && returnPage > 0) { + // 恢复模式、类型和页码状态 + const urlMode = route.query.mode as string; + const urlType = route.query.type as string; + const urlPage = route.query.page as string; + + let hasChanges = false; + + // 恢复模式 + if (urlMode && ['day', 'week', 'month'].includes(urlMode) && urlMode !== currentMode.value) { + currentMode.value = urlMode as 'day' | 'week' | 'month'; + hasChanges = true; + } + + // 恢复类型 + if (urlType && ['art', 'manga', 'novel'].includes(urlType) && urlType !== currentType.value) { + currentType.value = urlType as 'art' | 'manga' | 'novel'; + hasChanges = true; + } + + // 恢复页码 + const returnPage = parseInt(urlPage); + if (returnPage && returnPage > 0 && returnPage !== currentPage.value) { currentPage.value = returnPage; - fetchRankingData(returnPage); + hasChanges = true; + } + + // 如果有变化,重新获取数据 + if (hasChanges) { + fetchRankingData(currentPage.value); } }); @@ -291,7 +388,29 @@ onUnmounted(() => { }); onMounted(async () => { - await fetchRankingData(1); + // 检查URL参数并恢复状态 + const urlMode = route.query.mode as string; + const urlType = route.query.type as string; + const urlPage = route.query.page as string; + + // 恢复模式 + if (urlMode && ['day', 'week', 'month'].includes(urlMode)) { + currentMode.value = urlMode as 'day' | 'week' | 'month'; + } + + // 恢复类型 + if (urlType && ['art', 'manga', 'novel'].includes(urlType)) { + currentType.value = urlType as 'art' | 'manga' | 'novel'; + } + + // 恢复页码 + const returnPage = parseInt(urlPage); + if (returnPage && returnPage > 0) { + currentPage.value = returnPage; + await fetchRankingData(returnPage); + } else { + await fetchRankingData(1); + } }); @@ -387,6 +506,57 @@ onMounted(async () => { font-size: 0.875rem; } +.jump-to-page { + display: flex; + justify-content: center; + margin-top: 1rem; +} + +.jump-input-group { + display: flex; + align-items: center; + gap: 0.5rem; + background: #f3f4f6; + border: 1px solid #d1d5db; + border-radius: 0.5rem; + padding: 0.5rem 1rem; + width: fit-content; +} + +.jump-input { + border: none; + background: transparent; + padding: 0.5rem 0.25rem; + font-size: 0.875rem; + width: 50px; + text-align: center; +} + +.jump-input:focus { + outline: none; +} + +.jump-btn { + background: #4f46e5; + color: white; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + border: none; + cursor: pointer; + font-size: 0.875rem; + transition: background-color 0.2s ease; +} + +.jump-btn:hover:not(:disabled) { + background: #4338ca; +} + +.jump-btn:disabled { + background: #9ca3af; + cursor: not-allowed; + color: #6b7280; +} + @media (max-width: 768px) { .container { padding: 0 1rem; @@ -401,5 +571,23 @@ onMounted(async () => { gap: 0.5rem; text-align: center; } + + .jump-to-page { + flex-direction: column; + gap: 0.5rem; + } + + .jump-input-group { + flex-direction: column; + align-items: flex-start; + } + + .jump-input { + width: 100%; + } + + .jump-btn { + width: 100%; + } } \ No newline at end of file