diff --git a/frontend/package.json b/frontend/package.json index 27443b0..80842f6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "1.0.1", + "version": "1.0.2", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/AppFooter.tsx b/frontend/src/components/AppFooter.tsx index b944bfe..600b115 100644 --- a/frontend/src/components/AppFooter.tsx +++ b/frontend/src/components/AppFooter.tsx @@ -1,11 +1,56 @@ -import { Typography, Space, Divider } from 'antd'; +import { useState, useEffect } from 'react'; +import { Typography, Space, Divider, Badge, Tooltip } from 'antd'; import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined } from '@ant-design/icons'; import { VERSION_INFO, getVersionString } from '../config/version'; +import { checkLatestVersion } from '../services/versionService'; const { Text, Link } = Typography; export default function AppFooter() { const isMobile = window.innerWidth <= 768; + const [hasUpdate, setHasUpdate] = useState(false); + const [latestVersion, setLatestVersion] = useState(''); + const [releaseUrl, setReleaseUrl] = useState(''); + + useEffect(() => { + // 检查版本更新(每次都重新检查) + const checkVersion = async () => { + console.log('[页脚组件] 开始版本检查流程...'); + console.log('[页脚组件] 开始从 GitHub 检查版本...'); + + try { + const result = await checkLatestVersion(); + console.log('[页脚组件] 版本检查结果:', result); + console.log('[页脚组件] 是否显示红点:', result.hasUpdate); + + setHasUpdate(result.hasUpdate); + setLatestVersion(result.latestVersion); + setReleaseUrl(result.releaseUrl); + } catch (error) { + console.error('[页脚组件] 版本检查失败:', error); + } + }; + + // 延迟3秒后检查,避免影响首次加载 + console.log('[页脚组件] 将在3秒后开始版本检查'); + const timer = setTimeout(checkVersion, 3000); + return () => { + console.log('[页脚组件] 清理定时器'); + clearTimeout(timer); + }; + }, []); + + // 点击版本号查看更新 + const handleVersionClick = () => { + console.log('[页脚组件] 版本号被点击, hasUpdate:', hasUpdate, 'releaseUrl:', releaseUrl); + + if (hasUpdate && releaseUrl) { + console.log('[页脚组件] 打开发布页面:', releaseUrl); + window.open(releaseUrl, '_blank'); + } else { + console.log('[页脚组件] 无更新或无发布链接,不执行跳转'); + } + }; return (
- - {VERSION_INFO.projectName} - {getVersionString()} - + + + + {VERSION_INFO.projectName} + {getVersionString()} + + + {/* 版本信息 */} - - {VERSION_INFO.projectName} - {getVersionString()} - + + + { + if (hasUpdate) { + e.currentTarget.style.transform = 'scale(1.05)'; + } + }} + onMouseLeave={(e) => { + if (hasUpdate) { + e.currentTarget.style.transform = 'scale(1)'; + } + }} + > + {VERSION_INFO.projectName} + {getVersionString()} + + + {/* GitHub 链接 */} v2 + */ +function compareVersion(v1: string, v2: string): number { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const num1 = parts1[i] || 0; + const num2 = parts2[i] || 0; + + if (num1 < num2) return -1; + if (num1 > num2) return 1; + } + + return 0; +} + +/** + * 使用 shields.io Badge API 获取最新版本 + * 优点:无 CORS 问题,自动从 GitHub 获取,无需维护 + */ +export async function checkLatestVersion(): Promise { + console.log('[版本检查] 开始检查版本更新...'); + console.log('[版本检查] 当前版本:', VERSION_INFO.version); + + try { + // 使用 shields.io 的 GitHub release badge API + const badgeUrl = 'https://img.shields.io/github/v/release/xiamuceer-j/MuMuAINovel'; + console.log('[版本检查] 请求 Badge API:', badgeUrl); + + const response = await fetch(badgeUrl, { + method: 'GET', + cache: 'no-cache', + }); + + console.log('[版本检查] 响应状态:', response.status, response.statusText); + + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } + + // shields.io 返回的是 SVG 格式 + const svgText = await response.text(); + console.log('[版本检查] 获取 SVG 成功,长度:', svgText.length); + + // 从 SVG 中提取版本号 + // SVG 中版本号通常在 标签内,格式如: v1.0.0 或 1.0.0 + const versionRegex = /v?([\d.]+)/g; + const matches = svgText.match(versionRegex); + + console.log('[版本检查] 正则匹配结果:', matches); + + if (matches && matches.length > 0) { + // 通常最后一个匹配是版本号(前面的可能是标签文本) + const versionMatch = matches[matches.length - 1]; + const latestVersion = versionMatch.replace('v', ''); + + console.log('[版本检查] 提取的版本号:', latestVersion); + + // 验证版本号格式 (x.x.x) + if (/^\d+\.\d+(\.\d+)?$/.test(latestVersion)) { + const hasUpdate = compareVersion(VERSION_INFO.version, latestVersion) < 0; + + console.log('[版本检查] 最新版本:', latestVersion); + console.log('[版本检查] 版本比较: 当前', VERSION_INFO.version, 'vs 最新', latestVersion); + console.log('[版本检查] 是否有更新:', hasUpdate); + + const result = { + hasUpdate, + latestVersion, + releaseUrl: `https://github.com/xiamuceer-j/MuMuAINovel/releases/tag/v${latestVersion}`, + }; + + console.log('[版本检查] 检查完成,结果:', result); + return result; + } else { + console.warn('[版本检查] 版本号格式不正确:', latestVersion); + } + } + + // 打印 SVG 内容用于调试 + console.log('[版本检查] SVG 内容片段:', svgText.substring(0, 500)); + + throw new Error('无法从 Badge API 解析版本信息'); + } catch (error) { + console.error('[版本检查] 检查失败:', error); + console.error('[版本检查] 错误详情:', error instanceof Error ? error.message : String(error)); + + // 失败时返回无更新 + return { + hasUpdate: false, + latestVersion: VERSION_INFO.version, + releaseUrl: VERSION_INFO.githubUrl, + }; + } +} + +/** + * 检查是否应该执行版本检查(避免频繁请求) + */ +export function shouldCheckVersion(): boolean { + const lastCheck = localStorage.getItem('version_last_check'); + + if (!lastCheck) { + return true; + } + + const lastCheckTime = new Date(lastCheck).getTime(); + const now = Date.now(); + const sixHoursMs = 6 * 60 * 60 * 1000; // 6小时 + + return now - lastCheckTime >= sixHoursMs; +} + +/** + * 记录版本检查时间 + */ +export function markVersionChecked(): void { + localStorage.setItem('version_last_check', new Date().toISOString()); +} + +/** + * 获取缓存的版本信息 + */ +export function getCachedVersionInfo(): VersionCheckResult | null { + const cached = localStorage.getItem('version_check_result'); + if (cached) { + try { + return JSON.parse(cached); + } catch { + return null; + } + } + return null; +} + +/** + * 缓存版本信息 + */ +export function cacheVersionInfo(info: VersionCheckResult): void { + localStorage.setItem('version_check_result', JSON.stringify(info)); +} + +/** + * 用户已查看更新提示 + */ +export function markUpdateViewed(version: string): void { + console.log('[版本检查] 标记版本已查看:', version); + localStorage.setItem('version_viewed', version); +} + +/** + * 检查用户是否已查看此版本的更新提示 + */ +export function hasViewedUpdate(version: string): boolean { + const viewedVersion = localStorage.getItem('version_viewed'); + console.log('[版本检查] 检查是否已查看, 最新版本:', version, ', 已查看版本:', viewedVersion); + + // 如果已查看的版本低于最新版本,应该显示红点 + if (viewedVersion && version) { + const parts1 = viewedVersion.split('.').map(Number); + const parts2 = version.split('.').map(Number); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const num1 = parts1[i] || 0; + const num2 = parts2[i] || 0; + + if (num1 < num2) { + console.log('[版本检查] 已查看版本低于最新版本,应显示红点'); + return false; // 已查看的版本低于最新版本,需要显示红点 + } + if (num1 > num2) { + console.log('[版本检查] 已查看版本高于最新版本,不显示红点'); + return true; // 已查看的版本高于最新版本 + } + } + } + + const result = viewedVersion === version; + console.log('[版本检查] 版本比较结果:', result); + return result; +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ee261c4..756da25 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,11 +1,19 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import { readFileSync } from 'fs' +import { resolve } from 'path' + +// 读取 package.json 获取版本号 +const packageJson = JSON.parse( + readFileSync(resolve(__dirname, 'package.json'), 'utf-8') +) // https://vite.dev/config/ export default defineConfig({ plugins: [react()], // 定义全局常量,在构建时注入 define: { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version), 'import.meta.env.VITE_BUILD_TIME': JSON.stringify( new Date().toISOString().split('T')[0] ),