增加检查更新和下载功能,优化待看名单添加逻辑
This commit is contained in:
@@ -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;
|
||||
+3
-10
@@ -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) => {
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+13
-1
@@ -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秒,避免影响登录流程
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -64,6 +72,9 @@ onMounted(async () => {
|
||||
</div>
|
||||
<RouterLink v-else to="/login" class="btn btn-primary">登录</RouterLink>
|
||||
|
||||
<!-- 更新检查器 -->
|
||||
<UpdateChecker />
|
||||
|
||||
<!-- GitHub 链接 -->
|
||||
<a href="https://github.com/kjqwer/pixiv-D" target="_blank" rel="noopener noreferrer" class="github-link"
|
||||
title="查看项目源码">
|
||||
@@ -176,6 +187,7 @@ onMounted(async () => {
|
||||
.nav-auth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
|
||||
@@ -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>
|
||||
@@ -10,11 +10,19 @@
|
||||
</button>
|
||||
|
||||
<!-- 添加当前页面按钮 -->
|
||||
<button @click="addCurrentPage" class="add-current-toggle"
|
||||
:class="{ added: isCurrentPageAdded, loading: addLoading }" :disabled="addLoading" title="添加当前页面到待看名单">
|
||||
<button @click="addOrUpdateCurrentPage" class="add-current-toggle" :class="{
|
||||
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">
|
||||
<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 v-else viewBox="0 0 24 24" fill="currentColor" class="loading-icon">
|
||||
<path
|
||||
@@ -102,10 +110,15 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="items-list">
|
||||
<div v-for="item in filteredAndSortedItems" :key="item.id" class="watchlist-item"
|
||||
:class="{ current: isCurrentUrl(item.url) }">
|
||||
<div v-for="item in filteredAndSortedItems" :key="item.id" class="watchlist-item" :class="{
|
||||
current: isCurrentUrl(item.url),
|
||||
duplicate: isDuplicateAuthor(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-time">{{ formatTime(item.createdAt) }}</div>
|
||||
</div>
|
||||
@@ -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<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
|
||||
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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user