diff --git a/apps/web/src/pages/admin.tsx b/apps/web/src/pages/admin.tsx index 3e0769d..56c4496 100644 --- a/apps/web/src/pages/admin.tsx +++ b/apps/web/src/pages/admin.tsx @@ -2,7 +2,7 @@ import * as React from "react" import DOMPurify from "dompurify" import { useSearchParams } from "react-router-dom" 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 { cn, decodeMimeHeader, formatBytes, formatDate } from "@/lib/utils" 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() { + 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 ( @@ -1090,12 +1130,25 @@ function AboutProjectCard() { {projectTag ? ( - +
+ + {updateAvailable && latestRelease.data && ( + + )} + {latestRelease.isLoading && ( + 检查更新中... + )} +
) : (