增加检查更新和下载功能,优化待看名单添加逻辑
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;
|
||||||
+2
-9
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
<!-- 添加当前页面按钮 -->
|
<!-- 添加当前页面按钮 -->
|
||||||
<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,18 +453,32 @@ 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 {
|
||||||
const success = await watchlistStore.addItem({ url: currentUrl });
|
// 检查是否有相同作者的项目需要更新
|
||||||
if (success) {
|
if (hasSameAuthorDifferentPage.value && sameAuthorItem.value) {
|
||||||
console.log('页面已添加到待看名单');
|
// 更新现有项目的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 {
|
} 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;
|
||||||
|
|||||||
@@ -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;
|
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
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user