From 7f4fb9c0eac19aa93f559e4e40c44c53ad77d010 Mon Sep 17 00:00:00 2001 From: kjqwer <2990346238@qq.com> Date: Fri, 12 Sep 2025 18:55:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=A3=80=E6=9F=A5=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=92=8C=E4=B8=8B=E8=BD=BD=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BE=85=E7=9C=8B=E5=90=8D=E5=8D=95=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/update.js | 120 ++++ backend/server.js | 13 +- package.json | 2 +- ui/src/App.vue | 14 +- ui/src/components/common/UpdateChecker.vue | 631 +++++++++++++++++++ ui/src/components/common/WatchlistWidget.vue | 136 +++- ui/src/stores/update.ts | 92 +++ ui/src/stores/watchlist.ts | 53 +- 8 files changed, 1037 insertions(+), 24 deletions(-) create mode 100644 backend/routes/update.js create mode 100644 ui/src/components/common/UpdateChecker.vue create mode 100644 ui/src/stores/update.ts diff --git a/backend/routes/update.js b/backend/routes/update.js new file mode 100644 index 0000000..2e7c0e7 --- /dev/null +++ b/backend/routes/update.js @@ -0,0 +1,120 @@ +const express = require('express'); +const axios = require('axios'); +const { defaultLogger } = require('../utils/logger'); +const packageInfo = require('../../package.json'); + +const router = express.Router(); +const logger = defaultLogger.child('UpdateRoute'); + +/** + * 获取当前版本信息 + */ +router.get('/current-version', (req, res) => { + try { + res.json({ + success: true, + data: { + version: packageInfo.version, + name: packageInfo.name, + description: packageInfo.description + } + }); + } catch (error) { + logger.error('获取当前版本失败', error); + res.status(500).json({ + success: false, + error: '获取当前版本失败', + message: error.message + }); + } +}); + +/** + * 检查最新版本 + */ +router.get('/check-latest', async (req, res) => { + try { + logger.info('检查最新版本...'); + + // 获取GitHub发行版信息 + const response = await axios.get('https://api.github.com/repos/kjqwer/pixiv-D/releases/latest', { + timeout: 10000, + headers: { + 'User-Agent': 'Pixiv-Manager-Update-Checker' + } + }); + + const latestRelease = response.data; + const currentVersion = packageInfo.version; + const latestVersion = latestRelease.tag_name.replace(/^v/, ''); // 移除v前缀 + + // 版本比较 + const hasUpdate = compareVersions(latestVersion, currentVersion) > 0; + + const result = { + current: currentVersion, + latest: latestVersion, + hasUpdate, + releaseInfo: { + name: latestRelease.name, + body: latestRelease.body, + publishedAt: latestRelease.published_at, + htmlUrl: latestRelease.html_url, + downloadUrl: latestRelease.assets.find(asset => + asset.name.includes('pixiv-manager-portable.rar') + )?.browser_download_url || latestRelease.html_url + } + }; + + logger.info(`版本检查完成: 当前版本 ${currentVersion}, 最新版本 ${latestVersion}, 有更新: ${hasUpdate}`); + + res.json({ + success: true, + data: result + }); + + } catch (error) { + logger.error('检查最新版本失败', error); + + // 如果是网络错误,返回友好的错误信息 + let errorMessage = '检查更新失败'; + if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') { + errorMessage = '无法连接到GitHub,请检查网络连接'; + } else if (error.response?.status === 404) { + errorMessage = '未找到发行版信息'; + } else if (error.response?.status === 403) { + errorMessage = 'GitHub API访问限制,请稍后再试'; + } + + res.status(500).json({ + success: false, + error: errorMessage, + message: error.message + }); + } +}); + +/** + * 版本比较函数 + * @param {string} version1 版本1 + * @param {string} version2 版本2 + * @returns {number} 1: version1 > version2, 0: 相等, -1: version1 < version2 + */ +function compareVersions(version1, version2) { + const v1Parts = version1.split('.').map(Number); + const v2Parts = version2.split('.').map(Number); + + const maxLength = Math.max(v1Parts.length, v2Parts.length); + + for (let i = 0; i < maxLength; i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; + + if (v1Part > v2Part) return 1; + if (v1Part < v2Part) return -1; + } + + return 0; +} + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 428a2e8..b4411a6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -15,6 +15,7 @@ const proxyRoutes = require('./routes/proxy'); const repositoryRoutes = require('./routes/repository'); const rankingRoutes = require('./routes/ranking'); const watchlistRoutes = require('./routes/watchlist'); +const updateRoutes = require('./routes/update'); // 导入中间件 - 临时注释掉来定位问题 const { errorHandler } = require('./middleware/errorHandler'); @@ -114,16 +115,7 @@ function customLogger(req, res, next) { default: methodIcon = '❓'; } - - // 格式化时间 - const now = new Date(); - const timeStr = now.toLocaleTimeString('zh-CN', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - }); - + // 输出日志 logger.info(`${statusIcon} ${methodIcon} ${method} ${url} ${statusCode} ${duration}ms`); @@ -227,6 +219,7 @@ class PixivServer { this.app.use('/api/repository', repositoryRoutes); // 仓库管理,不需要认证 this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证 this.app.use('/api/watchlist', authMiddleware, watchlistRoutes); // 待看名单,需要认证 + this.app.use('/api/update', updateRoutes); // 更新检查,不需要认证 // 404 处理 this.app.use((req, res) => { diff --git a/package.json b/package.json index 15dbd04..7e15f4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pixiv-backend", - "version": "1.0.0", + "version": "1.0.4", "description": "Pixiv 下载浏览管理器", "main": "backend/start.js", "bin": "backend/start.js", diff --git a/ui/src/App.vue b/ui/src/App.vue index 300ec26..0591f5f 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -4,13 +4,16 @@ import { computed, onMounted } from 'vue' import { useRoute } from 'vue-router' import { useAuthStore } from '@/stores/auth' import { useDownloadStore } from '@/stores/download' +import { useUpdateStore } from '@/stores/update' import SettingsWidget from '@/components/common/SettingsWidget.vue' import DownloadProgressWidget from '@/components/common/DownloadProgressWidget.vue' import WatchlistWidget from '@/components/common/WatchlistWidget.vue' +import UpdateChecker from '@/components/common/UpdateChecker.vue' const route = useRoute() const authStore = useAuthStore() const downloadStore = useDownloadStore() +const updateStore = useUpdateStore() const isLoggedIn = computed(() => authStore.isLoggedIn) const username = computed(() => authStore.username) @@ -23,11 +26,16 @@ const showDownloadWidget = computed(() => { onMounted(async () => { await authStore.fetchLoginStatus() - // 如果已登录,初始化下载store + // 如果已登录,初始化下载store和检查更新 if (authStore.isLoggedIn) { await downloadStore.fetchTasks() // 启动定期刷新 downloadStore.startRefreshInterval() + + // 自动检查更新(静默) + setTimeout(() => { + updateStore.autoCheckUpdate() + }, 2000) // 延迟2秒,避免影响登录流程 } }) @@ -64,6 +72,9 @@ onMounted(async () => { 登录 + + + @@ -176,6 +187,7 @@ onMounted(async () => { .nav-auth { display: flex; align-items: center; + gap: 0.75rem; } .user-info { diff --git a/ui/src/components/common/UpdateChecker.vue b/ui/src/components/common/UpdateChecker.vue new file mode 100644 index 0000000..0d0620d --- /dev/null +++ b/ui/src/components/common/UpdateChecker.vue @@ -0,0 +1,631 @@ + + + + + + + + + + + + + {{ isChecking ? '检查中...' : '检查更新' }} + 有新版本 + + + + + + + + + + + + 版本检查结果 + + + + + + + + + + + + 当前版本: + v{{ updateInfo.current }} + + + 最新版本: + + v{{ updateInfo.latest }} + + + + + + + + + + 发现新版本! + + + + {{ updateInfo.releaseInfo.name }} + + 发布时间: {{ formatDate(updateInfo.releaseInfo.publishedAt) }} + + + 更新说明: + + + + + + + 📋 更新方法: + + + 1 + 下载新版本的 pixiv-manager-portable.rar + 文件 + + + 2 + 解压到当前程序目录,选择覆盖所有文件 + + + 3 + ⚠️ 重要:重新检查 start.bat + 中的代理端口和启动端口配置 + + + 4 + 重新启动程序即可 + + + + + + + + + + + + 您使用的已是最新版本 + + + + + + + + + 检查更新失败 + + {{ error }} + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/src/components/common/WatchlistWidget.vue b/ui/src/components/common/WatchlistWidget.vue index 2815258..65b52c7 100644 --- a/ui/src/components/common/WatchlistWidget.vue +++ b/ui/src/components/common/WatchlistWidget.vue @@ -10,11 +10,19 @@ - + - - + + + + + + - + - {{ item.title }} + + {{ item.title }} + 重复 + {{ formatUrl(item.url) }} {{ formatTime(item.createdAt) }} @@ -327,6 +340,32 @@ const isCurrentPageAdded = computed(() => { return watchlistStore.hasUrl(currentPageUrl.value); }); +// 检查是否有相同作者但不同页面的项目 +const hasSameAuthorDifferentPage = computed(() => { + if (isCurrentPageAdded.value) return false; + + const currentUrl = currentPageUrl.value; + const sameAuthorItem = watchlistStore.findSameAuthor(currentUrl); + return !!sameAuthorItem; +}); + +// 获取相同作者的项目 +const sameAuthorItem = computed(() => { + if (isCurrentPageAdded.value) return null; + return watchlistStore.findSameAuthor(currentPageUrl.value); +}); + +// 获取添加按钮的标题 +const getAddButtonTitle = () => { + if (isCurrentPageAdded.value) { + return '当前页面已在待看名单中'; + } else if (hasSameAuthorDifferentPage.value) { + return '更新相同作者的页面'; + } else { + return '添加当前页面到待看名单'; + } +}; + // 检查是否为当前URL const isCurrentUrl = (url: string) => { const currentUrl = currentPageUrl.value; @@ -414,18 +453,32 @@ const toggleWatchlist = () => { const fetchItems = async () => { await watchlistStore.fetchItems(); + // 数据加载完成后检查重复作者 + checkDuplicateAuthors(); }; -const addCurrentPage = async () => { +const addOrUpdateCurrentPage = async () => { if (addLoading.value || isCurrentPageAdded.value) return; addLoading.value = true; const currentUrl = currentPageUrl.value; try { - const success = await watchlistStore.addItem({ url: currentUrl }); - if (success) { - console.log('页面已添加到待看名单'); + // 检查是否有相同作者的项目需要更新 + if (hasSameAuthorDifferentPage.value && sameAuthorItem.value) { + // 更新现有项目的URL为当前页面 + const success = await watchlistStore.updateItem(sameAuthorItem.value.id, { + url: currentUrl + }); + if (success) { + console.log('已更新相同作者的页面到当前页面'); + } + } else { + // 添加新项目 + const success = await watchlistStore.addItem({ url: currentUrl }); + if (success) { + console.log('页面已添加到待看名单'); + } } } finally { addLoading.value = false; @@ -628,6 +681,44 @@ onMounted(() => { fetchItems(); }); +// 检查重复作者的方法 +const checkDuplicateAuthors = () => { + const authorMap = new Map(); + + // 按作者ID分组 + items.value.forEach(item => { + const authorId = watchlistStore.extractAuthorId(item.url); + if (authorId) { + if (!authorMap.has(authorId)) { + authorMap.set(authorId, []); + } + authorMap.get(authorId)!.push(item); + } + }); + + // 找出有重复的作者 + const duplicateAuthors: string[] = []; + authorMap.forEach((items, authorId) => { + if (items.length > 1) { + duplicateAuthors.push(authorId); + console.warn(`检测到作者 ${authorId} 有 ${items.length} 个重复项目:`, items.map(item => item.url)); + } + }); + + if (duplicateAuthors.length > 0) { + console.log(`发现 ${duplicateAuthors.length} 个作者有重复项目,建议清理`); + } +}; + +// 检查是否为重复作者 +const isDuplicateAuthor = (item: WatchlistItem) => { + const authorId = watchlistStore.extractAuthorId(item.url); + if (!authorId) return false; + + const itemsByAuthor = watchlistStore.findItemsByAuthor(authorId); + return itemsByAuthor.length > 1; +}; + // 监听路由变化,更新当前页面URL watch(() => route.fullPath, () => { // 路由变化时更新当前页面URL @@ -679,6 +770,11 @@ watch(() => route.fullPath, () => { color: #10b981; } +.add-current-toggle.update { + border-color: #f59e0b; + color: #f59e0b; +} + .add-current-toggle.loading { border-color: #f59e0b; color: #f59e0b; @@ -965,6 +1061,13 @@ watch(() => route.fullPath, () => { border-color: #3b82f6; } +.watchlist-item.duplicate { + background: #fef3c7; + /* 浅黄色背景 */ + border-color: #f59e0b; + /* 橙色边框 */ +} + .item-main { flex: 1; cursor: pointer; @@ -980,6 +1083,17 @@ watch(() => route.fullPath, () => { text-overflow: ellipsis; } +.item-title .duplicate-badge { + margin-left: 0.5rem; + background-color: #f59e0b; + color: white; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; +} + .item-url { font-size: 0.75rem; color: #6b7280; diff --git a/ui/src/stores/update.ts b/ui/src/stores/update.ts new file mode 100644 index 0000000..8845181 --- /dev/null +++ b/ui/src/stores/update.ts @@ -0,0 +1,92 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +interface ReleaseInfo { + name: string + body: string + publishedAt: string + htmlUrl: string + downloadUrl?: string +} + +interface UpdateInfo { + current: string + latest: string + hasUpdate: boolean + releaseInfo?: ReleaseInfo +} + +export const useUpdateStore = defineStore('update', () => { + const updateInfo = ref(null) + const isChecking = ref(false) + const lastCheckTime = ref(null) + + // 检查更新 + const checkUpdate = async (silent = false): Promise => { + if (isChecking.value) return null + + isChecking.value = true + + try { + const response = await fetch('/api/update/check-latest') + const result = await response.json() + + if (result.success) { + updateInfo.value = result.data + lastCheckTime.value = new Date() + return result.data + } else { + if (!silent) { + console.error('检查更新失败:', result.error) + } + return null + } + } catch (error) { + if (!silent) { + console.error('检查更新网络错误:', error) + } + return null + } finally { + isChecking.value = false + } + } + + // 自动检查更新(登录后调用) + const autoCheckUpdate = async () => { + // 如果距离上次检查不足1小时,跳过 + if (lastCheckTime.value && Date.now() - lastCheckTime.value.getTime() < 60 * 60 * 1000) { + return + } + + const result = await checkUpdate(true) // 静默检查 + + // 如果有更新,显示一个简单的控制台提示 + if (result?.hasUpdate) { + console.log(`🎉 发现新版本 v${result.latest},当前版本 v${result.current}`) + } + } + + // 获取当前版本 + const getCurrentVersion = async () => { + try { + const response = await fetch('/api/update/current-version') + const result = await response.json() + + if (result.success) { + return result.data.version + } + } catch (error) { + console.error('获取当前版本失败:', error) + } + return null + } + + return { + updateInfo, + isChecking, + lastCheckTime, + checkUpdate, + autoCheckUpdate, + getCurrentVersion + } +}) \ No newline at end of file diff --git a/ui/src/stores/watchlist.ts b/ui/src/stores/watchlist.ts index 25f6a2d..122f089 100644 --- a/ui/src/stores/watchlist.ts +++ b/ui/src/stores/watchlist.ts @@ -119,6 +119,52 @@ export const useWatchlistStore = defineStore('watchlist', () => { error.value = null; }; + // 提取作者ID的工具函数 + const extractAuthorId = (url: string) => { + try { + let path = ''; + + // 处理完整URL + if (url.startsWith('http://') || url.startsWith('https://')) { + const urlObj = new URL(url); + path = urlObj.pathname; + } else { + // 处理相对路径 + path = url.startsWith('/') ? url : '/' + url; + } + + // 匹配 /artist/数字 的模式 + const match = path.match(/\/artist\/(\d+)/); + return match ? match[1] : null; + } catch { + return null; + } + }; + + // 检查是否有相同作者但不同页面的项目 + const findSameAuthor = (url: string) => { + const authorId = extractAuthorId(url); + if (!authorId) return null; + + return items.value.find(item => { + const itemAuthorId = extractAuthorId(item.url); + return itemAuthorId === authorId && item.url !== url; + }); + }; + + // 检查当前URL是否与已存在的作者页面相同(忽略页面参数) + const hasSameAuthor = (url: string) => { + return findSameAuthor(url) !== null; + }; + + // 根据作者ID查找所有项目 + const findItemsByAuthor = (authorId: string) => { + return items.value.filter(item => { + const itemAuthorId = extractAuthorId(item.url); + return itemAuthorId === authorId; + }); + }; + return { // 状态 items, @@ -134,6 +180,11 @@ export const useWatchlistStore = defineStore('watchlist', () => { deleteItem, hasUrl, findByUrl, - clearError + clearError, + // 新增的作者相关方法 + extractAuthorId, + findSameAuthor, + hasSameAuthor, + findItemsByAuthor }; }); \ No newline at end of file
pixiv-manager-portable.rar
start.bat