增加检查更新和下载功能,优化待看名单添加逻辑

This commit is contained in:
2025-09-12 18:55:56 +08:00
parent e87fd2e332
commit 7f4fb9c0ea
8 changed files with 1037 additions and 24 deletions
+120
View File
@@ -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;
+2 -9
View File
@@ -15,6 +15,7 @@ const proxyRoutes = require('./routes/proxy');
const repositoryRoutes = require('./routes/repository'); const repositoryRoutes = require('./routes/repository');
const rankingRoutes = require('./routes/ranking'); const rankingRoutes = require('./routes/ranking');
const watchlistRoutes = require('./routes/watchlist'); const watchlistRoutes = require('./routes/watchlist');
const updateRoutes = require('./routes/update');
// 导入中间件 - 临时注释掉来定位问题 // 导入中间件 - 临时注释掉来定位问题
const { errorHandler } = require('./middleware/errorHandler'); const { errorHandler } = require('./middleware/errorHandler');
@@ -115,15 +116,6 @@ function customLogger(req, res, next) {
methodIcon = '❓'; 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`); 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/repository', repositoryRoutes); // 仓库管理,不需要认证
this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证 this.app.use('/api/proxy', proxyRoutes); // 图片代理,不需要认证
this.app.use('/api/watchlist', authMiddleware, watchlistRoutes); // 待看名单,需要认证 this.app.use('/api/watchlist', authMiddleware, watchlistRoutes); // 待看名单,需要认证
this.app.use('/api/update', updateRoutes); // 更新检查,不需要认证
// 404 处理 // 404 处理
this.app.use((req, res) => { this.app.use((req, res) => {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "pixiv-backend", "name": "pixiv-backend",
"version": "1.0.0", "version": "1.0.4",
"description": "Pixiv 下载浏览管理器", "description": "Pixiv 下载浏览管理器",
"main": "backend/start.js", "main": "backend/start.js",
"bin": "backend/start.js", "bin": "backend/start.js",
+13 -1
View File
@@ -4,13 +4,16 @@ import { computed, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useDownloadStore } from '@/stores/download' import { useDownloadStore } from '@/stores/download'
import { useUpdateStore } from '@/stores/update'
import SettingsWidget from '@/components/common/SettingsWidget.vue' import SettingsWidget from '@/components/common/SettingsWidget.vue'
import DownloadProgressWidget from '@/components/common/DownloadProgressWidget.vue' import DownloadProgressWidget from '@/components/common/DownloadProgressWidget.vue'
import WatchlistWidget from '@/components/common/WatchlistWidget.vue' import WatchlistWidget from '@/components/common/WatchlistWidget.vue'
import UpdateChecker from '@/components/common/UpdateChecker.vue'
const route = useRoute() const route = useRoute()
const authStore = useAuthStore() const authStore = useAuthStore()
const downloadStore = useDownloadStore() const downloadStore = useDownloadStore()
const updateStore = useUpdateStore()
const isLoggedIn = computed(() => authStore.isLoggedIn) const isLoggedIn = computed(() => authStore.isLoggedIn)
const username = computed(() => authStore.username) const username = computed(() => authStore.username)
@@ -23,11 +26,16 @@ const showDownloadWidget = computed(() => {
onMounted(async () => { onMounted(async () => {
await authStore.fetchLoginStatus() await authStore.fetchLoginStatus()
// 如果已登录,初始化下载store // 如果已登录,初始化下载store和检查更新
if (authStore.isLoggedIn) { if (authStore.isLoggedIn) {
await downloadStore.fetchTasks() await downloadStore.fetchTasks()
// 启动定期刷新 // 启动定期刷新
downloadStore.startRefreshInterval() downloadStore.startRefreshInterval()
// 自动检查更新(静默)
setTimeout(() => {
updateStore.autoCheckUpdate()
}, 2000) // 延迟2秒,避免影响登录流程
} }
}) })
</script> </script>
@@ -64,6 +72,9 @@ onMounted(async () => {
</div> </div>
<RouterLink v-else to="/login" class="btn btn-primary">登录</RouterLink> <RouterLink v-else to="/login" class="btn btn-primary">登录</RouterLink>
<!-- 更新检查器 -->
<UpdateChecker />
<!-- GitHub 链接 --> <!-- GitHub 链接 -->
<a href="https://github.com/kjqwer/pixiv-D" target="_blank" rel="noopener noreferrer" class="github-link" <a href="https://github.com/kjqwer/pixiv-D" target="_blank" rel="noopener noreferrer" class="github-link"
title="查看项目源码"> title="查看项目源码">
@@ -176,6 +187,7 @@ onMounted(async () => {
.nav-auth { .nav-auth {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem;
} }
.user-info { .user-info {
+631
View File
@@ -0,0 +1,631 @@
<template>
<div class="update-checker">
<!-- 检查更新按钮 -->
<button @click="checkUpdate" :disabled="isChecking" class="update-btn"
:class="{ 'has-update': hasUpdate, 'checking': isChecking }" :title="hasUpdate ? '发现新版本!' : '检查更新'">
<!-- 更新提示小红点 -->
<span v-if="hasUpdate" class="update-dot"></span>
<svg v-if="!isChecking" viewBox="0 0 24 24" fill="currentColor" class="update-icon">
<path
d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" />
</svg>
<svg v-else viewBox="0 0 24 24" fill="currentColor" class="update-icon spinning">
<path d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
<span v-if="!hasUpdate">{{ isChecking ? '检查中...' : '检查更新' }}</span>
<span v-else class="update-available">有新版本</span>
</button>
<!-- 更新弹窗 -->
<teleport to="body">
<div v-if="showModal" class="modal-overlay" @click="closeModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3 class="modal-title">
<svg viewBox="0 0 24 24" fill="currentColor" class="modal-icon">
<path
d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" />
</svg>
版本检查结果
</h3>
<button @click="closeModal" class="modal-close">
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" />
</svg>
</button>
</div>
<div class="modal-body">
<div v-if="updateInfo" class="version-info">
<div class="version-row">
<span class="label">当前版本:</span>
<span class="version current">v{{ updateInfo.current }}</span>
</div>
<div class="version-row">
<span class="label">最新版本:</span>
<span class="version latest" :class="{ 'newer': updateInfo.hasUpdate }">
v{{ updateInfo.latest }}
</span>
</div>
</div>
<div v-if="updateInfo?.hasUpdate" class="update-available-section">
<div class="update-status">
<svg viewBox="0 0 24 24" fill="currentColor" class="status-icon success">
<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>
<span class="status-text">发现新版本</span>
</div>
<div v-if="updateInfo.releaseInfo" class="release-info">
<h4>{{ updateInfo.releaseInfo.name }}</h4>
<div class="release-date">
发布时间: {{ formatDate(updateInfo.releaseInfo.publishedAt) }}
</div>
<div v-if="updateInfo.releaseInfo.body" class="release-notes">
<h5>更新说明:</h5>
<div class="release-body" v-html="formatReleaseNotes(updateInfo.releaseInfo.body)">
</div>
</div>
<!-- 更新方法提示 -->
<div class="update-instructions">
<h5>📋 更新方法:</h5>
<div class="instructions-content">
<div class="instruction-step">
<span class="step-number">1</span>
<span class="step-text">下载新版本的 <code>pixiv-manager-portable.rar</code>
文件</span>
</div>
<div class="instruction-step">
<span class="step-number">2</span>
<span class="step-text">解压到当前程序目录选择覆盖所有文件</span>
</div>
<div class="instruction-step">
<span class="step-number">3</span>
<span class="step-text"> <strong>重要</strong>重新检查 <code>start.bat</code>
中的代理端口和启动端口配置</span>
</div>
<div class="instruction-step">
<span class="step-number">4</span>
<span class="step-text">重新启动程序即可</span>
</div>
</div>
</div>
</div>
</div>
<div v-else-if="updateInfo && !updateInfo.hasUpdate" class="no-update-section">
<div class="update-status">
<svg viewBox="0 0 24 24" fill="currentColor" class="status-icon info">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" />
</svg>
<span class="status-text">您使用的已是最新版本</span>
</div>
</div>
<div v-if="error" class="error-section">
<div class="update-status">
<svg viewBox="0 0 24 24" fill="currentColor" class="status-icon error">
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
</svg>
<span class="status-text">检查更新失败</span>
</div>
<div class="error-message">{{ error }}</div>
</div>
</div>
<div class="modal-footer">
<button @click="closeModal" class="btn btn-secondary">关闭</button>
<button v-if="updateInfo?.hasUpdate && updateInfo.releaseInfo?.downloadUrl"
@click="downloadUpdate" class="btn btn-primary">
下载更新
</button>
<button v-if="updateInfo?.hasUpdate && updateInfo.releaseInfo?.htmlUrl" @click="viewRelease"
class="btn btn-outline">
查看详情
</button>
</div>
</div>
</div>
</teleport>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useUpdateStore } from '@/stores/update'
const updateStore = useUpdateStore()
const showModal = ref(false)
const error = ref<string | null>(null)
const hasUpdate = computed(() => updateStore.updateInfo?.hasUpdate ?? false)
const updateInfo = computed(() => updateStore.updateInfo)
const isChecking = computed(() => updateStore.isChecking)
const checkUpdate = async () => {
error.value = null
try {
const result = await updateStore.checkUpdate(false)
if (result) {
showModal.value = true
} else {
error.value = '检查更新失败'
showModal.value = true
}
} catch (err: any) {
error.value = err.message || '网络连接失败'
showModal.value = true
}
}
const closeModal = () => {
showModal.value = false
}
const downloadUpdate = () => {
if (updateInfo.value?.releaseInfo?.downloadUrl) {
window.open(updateInfo.value.releaseInfo.downloadUrl, '_blank')
}
}
const viewRelease = () => {
if (updateInfo.value?.releaseInfo?.htmlUrl) {
window.open(updateInfo.value.releaseInfo.htmlUrl, '_blank')
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString('zh-CN')
}
const formatReleaseNotes = (body: string) => {
// 简单的Markdown到HTML转换
return body
.replace(/\n/g, '<br>')
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/`(.*?)`/g, '<code>$1</code>')
}
// 暴露给父组件的方法
defineExpose({
checkUpdate
})
</script>
<style scoped>
.update-checker {
position: relative;
}
.update-dot {
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
background: #ef4444;
border: 2px solid white;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.8;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.update-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background: white;
color: #6b7280;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.update-btn:hover:not(:disabled) {
background: #f9fafb;
border-color: #9ca3af;
color: #374151;
}
.update-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.update-btn.has-update {
background: #fef3c7;
border-color: #f59e0b;
color: #92400e;
}
.update-btn.has-update:hover:not(:disabled) {
background: #fde68a;
}
.update-btn.checking {
background: #dbeafe;
border-color: #3b82f6;
color: #1d4ed8;
}
.update-icon {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.update-available {
font-weight: 600;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.modal-content {
background: white;
border-radius: 0.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
width: 100%;
max-width: 32rem;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
}
.modal-title {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: #111827;
}
.modal-icon {
width: 1.25rem;
height: 1.25rem;
color: #3b82f6;
}
.modal-close {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: none;
border-radius: 0.25rem;
background: none;
color: #6b7280;
cursor: pointer;
transition: all 0.2s;
}
.modal-close:hover {
background: #f3f4f6;
color: #374151;
}
.modal-close svg {
width: 1rem;
height: 1rem;
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
flex: 1;
}
.version-info {
margin-bottom: 1.5rem;
}
.version-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #f3f4f6;
}
.version-row:last-child {
border-bottom: none;
}
.label {
font-weight: 500;
color: #374151;
}
.version {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background: #f3f4f6;
color: #374151;
}
.version.newer {
background: #dcfce7;
color: #166534;
}
.update-available-section,
.no-update-section,
.error-section {
margin-top: 1rem;
}
.update-status {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.status-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}
.status-icon.success {
color: #10b981;
}
.status-icon.info {
color: #3b82f6;
}
.status-icon.error {
color: #ef4444;
}
.status-text {
font-weight: 600;
color: #111827;
}
.release-info h4 {
margin: 0 0 0.5rem 0;
font-size: 1.125rem;
font-weight: 600;
color: #111827;
}
.release-date {
color: #6b7280;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.release-notes h5 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
font-weight: 600;
color: #374151;
}
.release-body {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 0.375rem;
padding: 1rem;
font-size: 0.875rem;
line-height: 1.5;
color: #374151;
max-height: 12rem;
overflow-y: auto;
}
.update-instructions {
margin-top: 1.5rem;
padding: 1rem;
background: #fef7cd;
border: 1px solid #fbbf24;
border-radius: 0.375rem;
}
.update-instructions h5 {
margin: 0 0 1rem 0;
font-size: 1rem;
font-weight: 600;
color: #92400e;
}
.instructions-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.instruction-step {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
min-width: 1.5rem;
height: 1.5rem;
background: #f59e0b;
color: white;
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
flex-shrink: 0;
}
.step-text {
font-size: 0.875rem;
line-height: 1.5;
color: #92400e;
}
.step-text code {
background: #fde68a;
border: 1px solid #f59e0b;
border-radius: 0.25rem;
padding: 0.125rem 0.25rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.75rem;
color: #92400e;
}
.error-message {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 0.375rem;
padding: 0.75rem;
color: #991b1b;
font-size: 0.875rem;
}
.modal-footer {
display: flex;
gap: 0.75rem;
justify-content: flex-end;
padding: 1.5rem;
border-top: 1px solid #e5e7eb;
background: #f9fafb;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
text-decoration: none;
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.btn-outline {
background: white;
color: #374151;
border-color: #d1d5db;
}
.btn-outline:hover {
background: #f9fafb;
border-color: #9ca3af;
}
@media (max-width: 640px) {
.modal-content {
margin: 1rem;
max-width: calc(100vw - 2rem);
}
.modal-header,
.modal-body,
.modal-footer {
padding: 1rem;
}
.version-row {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.modal-footer {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
+122 -8
View File
@@ -10,11 +10,19 @@
</button> </button>
<!-- 添加当前页面按钮 --> <!-- 添加当前页面按钮 -->
<button @click="addCurrentPage" class="add-current-toggle" <button @click="addOrUpdateCurrentPage" class="add-current-toggle" :class="{
:class="{ added: isCurrentPageAdded, loading: addLoading }" :disabled="addLoading" title="添加当前页面到待看名单"> added: isCurrentPageAdded,
loading: addLoading,
update: hasSameAuthorDifferentPage && !isCurrentPageAdded
}" :disabled="addLoading" :title="getAddButtonTitle()">
<svg v-if="!addLoading" viewBox="0 0 24 24" fill="currentColor" class="add-icon"> <svg v-if="!addLoading" viewBox="0 0 24 24" fill="currentColor" class="add-icon">
<path v-if="!isCurrentPageAdded" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> <!-- 已添加显示勾选图标 -->
<path v-else d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" /> <path v-if="isCurrentPageAdded" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" />
<!-- 更新模式显示更新图标 -->
<path v-else-if="hasSameAuthorDifferentPage"
d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z" />
<!-- 添加模式显示加号图标 -->
<path v-else d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
</svg> </svg>
<svg v-else viewBox="0 0 24 24" fill="currentColor" class="loading-icon"> <svg v-else viewBox="0 0 24 24" fill="currentColor" class="loading-icon">
<path <path
@@ -102,10 +110,15 @@
</div> </div>
<div v-else class="items-list"> <div v-else class="items-list">
<div v-for="item in filteredAndSortedItems" :key="item.id" class="watchlist-item" <div v-for="item in filteredAndSortedItems" :key="item.id" class="watchlist-item" :class="{
:class="{ current: isCurrentUrl(item.url) }"> current: isCurrentUrl(item.url),
duplicate: isDuplicateAuthor(item)
}">
<div class="item-main" @click="navigateToItem(item)"> <div class="item-main" @click="navigateToItem(item)">
<div class="item-title" :title="item.title">{{ item.title }}</div> <div class="item-title" :title="item.title">
{{ item.title }}
<span v-if="isDuplicateAuthor(item)" class="duplicate-badge" title="该作者有多个页面">重复</span>
</div>
<div class="item-url" :title="item.url">{{ formatUrl(item.url) }}</div> <div class="item-url" :title="item.url">{{ formatUrl(item.url) }}</div>
<div class="item-time">{{ formatTime(item.createdAt) }}</div> <div class="item-time">{{ formatTime(item.createdAt) }}</div>
</div> </div>
@@ -327,6 +340,32 @@ const isCurrentPageAdded = computed(() => {
return watchlistStore.hasUrl(currentPageUrl.value); 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 // 检查是否为当前URL
const isCurrentUrl = (url: string) => { const isCurrentUrl = (url: string) => {
const currentUrl = currentPageUrl.value; const currentUrl = currentPageUrl.value;
@@ -414,19 +453,33 @@ const toggleWatchlist = () => {
const fetchItems = async () => { const fetchItems = async () => {
await watchlistStore.fetchItems(); await watchlistStore.fetchItems();
// 数据加载完成后检查重复作者
checkDuplicateAuthors();
}; };
const addCurrentPage = async () => { const addOrUpdateCurrentPage = async () => {
if (addLoading.value || isCurrentPageAdded.value) return; if (addLoading.value || isCurrentPageAdded.value) return;
addLoading.value = true; addLoading.value = true;
const currentUrl = currentPageUrl.value; const currentUrl = currentPageUrl.value;
try { try {
// 检查是否有相同作者的项目需要更新
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 }); const success = await watchlistStore.addItem({ url: currentUrl });
if (success) { if (success) {
console.log('页面已添加到待看名单'); console.log('页面已添加到待看名单');
} }
}
} finally { } finally {
addLoading.value = false; addLoading.value = false;
} }
@@ -628,6 +681,44 @@ onMounted(() => {
fetchItems(); fetchItems();
}); });
// 检查重复作者的方法
const checkDuplicateAuthors = () => {
const authorMap = new Map<string, WatchlistItem[]>();
// 按作者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 // 监听路由变化,更新当前页面URL
watch(() => route.fullPath, () => { watch(() => route.fullPath, () => {
// 路由变化时更新当前页面URL // 路由变化时更新当前页面URL
@@ -679,6 +770,11 @@ watch(() => route.fullPath, () => {
color: #10b981; color: #10b981;
} }
.add-current-toggle.update {
border-color: #f59e0b;
color: #f59e0b;
}
.add-current-toggle.loading { .add-current-toggle.loading {
border-color: #f59e0b; border-color: #f59e0b;
color: #f59e0b; color: #f59e0b;
@@ -965,6 +1061,13 @@ watch(() => route.fullPath, () => {
border-color: #3b82f6; border-color: #3b82f6;
} }
.watchlist-item.duplicate {
background: #fef3c7;
/* 浅黄色背景 */
border-color: #f59e0b;
/* 橙色边框 */
}
.item-main { .item-main {
flex: 1; flex: 1;
cursor: pointer; cursor: pointer;
@@ -980,6 +1083,17 @@ watch(() => route.fullPath, () => {
text-overflow: ellipsis; 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 { .item-url {
font-size: 0.75rem; font-size: 0.75rem;
color: #6b7280; color: #6b7280;
+92
View File
@@ -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<UpdateInfo | null>(null)
const isChecking = ref(false)
const lastCheckTime = ref<Date | null>(null)
// 检查更新
const checkUpdate = async (silent = false): Promise<UpdateInfo | null> => {
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
}
})
+52 -1
View File
@@ -119,6 +119,52 @@ export const useWatchlistStore = defineStore('watchlist', () => {
error.value = null; 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 { return {
// 状态 // 状态
items, items,
@@ -134,6 +180,11 @@ export const useWatchlistStore = defineStore('watchlist', () => {
deleteItem, deleteItem,
hasUrl, hasUrl,
findByUrl, findByUrl,
clearError clearError,
// 新增的作者相关方法
extractAuthorId,
findSameAuthor,
hasSameAuthor,
findItemsByAuthor
}; };
}); });