feat(admin): 增加版本更新检查

- 在关于页检查 GitHub 最新发布版本。
- 发现新版本时提示并提供跳转查看链接。
This commit is contained in:
LanQin_
2026-06-23 14:07:20 +08:00
parent 1373479973
commit b309368225
+60 -7
View File
@@ -2,7 +2,7 @@ import * as React from "react"
import DOMPurify from "dompurify" import DOMPurify from "dompurify"
import { useSearchParams } from "react-router-dom" import { useSearchParams } from "react-router-dom"
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { ArrowRight, BookOpen, CheckCircle2, Circle, Copy, GitBranch, Github, Globe2, Mailbox, MoreHorizontal, Plus, RefreshCcw, Scale, Search, ShieldCheck, Star, Trash2, Users } from "lucide-react" import { ArrowRight, BookOpen, CheckCircle2, Circle, Copy, ExternalLink, GitBranch, Github, Globe2, Mailbox, MoreHorizontal, Plus, RefreshCcw, Scale, Search, ShieldCheck, Star, Trash2, Users } from "lucide-react"
import { api, AdminUser, Alias, DNSRecord, Domain, Mailbox as MailboxType, MailMessage, MailTemplate, PermissionGroup, PermissionInfo, PermissionLimits, SystemSettings } from "@/lib/api" import { api, AdminUser, Alias, DNSRecord, Domain, Mailbox as MailboxType, MailMessage, MailTemplate, PermissionGroup, PermissionInfo, PermissionLimits, SystemSettings } from "@/lib/api"
import { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils" import { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -1081,7 +1081,47 @@ function SystemSettingsSection({ settings, domains }: { settings?: SystemSetting
) )
} }
function parseSemver(tag: string): number[] {
return (tag.startsWith("v") ? tag.slice(1) : tag).split(".").map(Number)
}
function AboutProjectCard() { function AboutProjectCard() {
const { toast } = useToast()
const latestRelease = useQuery({
queryKey: ["github", "latest-release"],
queryFn: async () => {
const res = await fetch("https://api.github.com/repos/LanQin996/LanQin-Email/releases/latest")
if (!res.ok) throw new Error("rate limited or unavailable")
return res.json() as Promise<{ tag_name: string; html_url: string }>
},
enabled: !!projectTag,
staleTime: 1000 * 60 * 60, // 1 hour
retry: 1,
})
const updateAvailable = React.useMemo(() => {
if (!projectTag || !latestRelease.data) return false
const current = parseSemver(projectTag)
const latest = parseSemver(latestRelease.data.tag_name)
for (let i = 0; i < Math.max(current.length, latest.length); i++) {
const a = current[i] ?? 0
const b = latest[i] ?? 0
if (b > a) return true
if (a > b) return false
}
return false
}, [projectTag, latestRelease.data])
React.useEffect(() => {
if (updateAvailable && latestRelease.data) {
toast({
title: "发现新版本",
description: `${latestRelease.data.tag_name} 已可用,点击版本号查看详情。`,
})
}
// Only toast once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [updateAvailable])
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
@@ -1090,12 +1130,25 @@ function AboutProjectCard() {
<CardContent className="space-y-4 text-sm"> <CardContent className="space-y-4 text-sm">
<AboutRow label="版本"> <AboutRow label="版本">
{projectTag ? ( {projectTag ? (
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild> <div className="flex flex-wrap items-center gap-2">
<a href={projectReleaseUrl} target="_blank" rel="noreferrer"> <Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" asChild>
<GitBranch className="h-5 w-5 text-primary" /> <a href={projectReleaseUrl} target="_blank" rel="noreferrer">
{projectTag} <GitBranch className="h-5 w-5 text-primary" />
</a> {projectTag}
</Button> </a>
</Button>
{updateAvailable && latestRelease.data && (
<Button type="button" variant="default" className="h-11 px-4 text-base font-normal" asChild>
<a href={latestRelease.data.html_url} target="_blank" rel="noreferrer">
<ExternalLink className="h-5 w-5" />
{latestRelease.data.tag_name}
</a>
</Button>
)}
{latestRelease.isLoading && (
<span className="text-xs text-muted-foreground">...</span>
)}
</div>
) : ( ) : (
<Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" disabled> <Button type="button" variant="outline" className="h-11 justify-start px-4 text-base font-normal" disabled>
<GitBranch className="h-5 w-5 text-muted-foreground" /> <GitBranch className="h-5 w-5 text-muted-foreground" />