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]
),