feat(app): 添加版本更新弹窗功能

- 在顶部导航栏和用户状态组件中集成版本更新弹窗
- 实现版本检查钩子,支持检查最新版本和更新日志
- 解析 CHANGELOG.md 文件并展示版本更新时间线
- 添加版本更新弹窗 UI 组件,显示当前版本和最新版本
- 在 Dockerfile 中更新 bun 版本依赖
- 调整 Next.js 配置以解析版本信息并注入环境变量
- 更新待测试文档中的版本更新功能描述
This commit is contained in:
HouYunFei
2026-05-20 15:25:57 +08:00
parent 2ab499bc54
commit 7a27684e3c
11 changed files with 241 additions and 8 deletions
+74
View File
@@ -0,0 +1,74 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { parseChangelog, type ReleaseInfo } from "@/lib/release-info";
const latestVersionUrl = "https://raw.githubusercontent.com/basketikun/infinite-canvas/main/VERSION";
const latestChangelogUrl = "https://raw.githubusercontent.com/basketikun/infinite-canvas/main/CHANGELOG.md";
function readLocalReleases(): ReleaseInfo[] {
try {
return JSON.parse(process.env.NEXT_PUBLIC_APP_RELEASES || "[]");
} catch {
return [];
}
}
function toVersionParts(version: string) {
const match = version.trim().match(/^v?(\d+)\.(\d+)\.(\d+)/);
return match ? match.slice(1).map(Number) : null;
}
function isNewerVersion(latestVersion: string, currentVersion: string) {
const latest = toVersionParts(latestVersion);
const current = toVersionParts(currentVersion);
if (!latest || !current) return false;
return latest.some((value, index) => value > current[index] && latest.slice(0, index).every((part, prevIndex) => part === current[prevIndex]));
}
export function useVersionCheck(currentVersion: string) {
const localReleases = useMemo(readLocalReleases, []);
const [latestVersion, setLatestVersion] = useState(currentVersion);
const [releases, setReleases] = useState<ReleaseInfo[]>(localReleases);
const [checking, setChecking] = useState(false);
const hasNewVersion = isNewerVersion(latestVersion, currentVersion);
const checkLatestVersion = useCallback(async () => {
try {
const response = await fetch(latestVersionUrl);
if (!response.ok) return false;
const version = await response.text();
setLatestVersion(version.trim() || currentVersion);
return true;
} catch {
return false;
}
}, [currentVersion]);
const checkLatestRelease = useCallback(async () => {
setChecking(true);
try {
const [versionResponse, changelogResponse] = await Promise.all([
fetch(latestVersionUrl),
fetch(latestChangelogUrl),
]);
if (!versionResponse.ok) throw new Error("版本读取失败");
if (!changelogResponse.ok) throw new Error("更新日志读取失败");
const [version, changelog] = await Promise.all([versionResponse.text(), changelogResponse.text()]);
setLatestVersion(version.trim() || currentVersion);
if (changelog.trim()) setReleases(parseChangelog(changelog));
return true;
} catch {
setLatestVersion(currentVersion);
setReleases(localReleases);
return false;
} finally {
setChecking(false);
}
}, [currentVersion, localReleases]);
useEffect(() => {
void checkLatestVersion();
}, [checkLatestVersion]);
return { latestVersion, releases, checking, hasNewVersion, checkLatestRelease };
}