import { TanStackDevtools } from "@tanstack/react-devtools";
import type { QueryClient } from "@tanstack/react-query";
import {
createRootRouteWithContext,
HeadContent,
Scripts,
useRouteContext,
} from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import theme from "@theme";
import { ThemeProvider } from "@/components/common/theme-provider";
import { siteConfigQuery } from "@/features/config/queries";
import TanStackQueryDevtools from "@/integrations/tanstack-query/devtools";
import { clientEnv } from "@/lib/env/client.env";
import { getLocale } from "@/paraglide/runtime";
import appCss from "@/styles.css?url";
import { useEffect, useState } from "react";
// 路由上下文类型定义
interface MyRouterContext {
queryClient: QueryClient;
}
export const Route = createRootRouteWithContext<MyRouterContext>()({
beforeLoad: async ({ context }) => {
const siteConfig = await context.queryClient.ensureQueryData(siteConfigQuery);
return { siteConfig };
},
loader: async ({ context }) => {
return { siteConfig: context.siteConfig };
},
head: ({ loaderData }) => {
const env = clientEnv();
return {
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: loaderData?.siteConfig?.title },
{ name: "description", content: loaderData?.siteConfig?.description },
],
links: [
{ rel: "icon", type: "image/svg+xml", href: loaderData?.siteConfig?.icons.faviconSvg },
{ rel: "icon", type: "image/png", href: loaderData?.siteConfig?.icons.favicon96, sizes: "96x96" },
{ rel: "shortcut icon", href: loaderData?.siteConfig?.icons.faviconIco },
{ rel: "apple-touch-icon", type: "image/png", href: loaderData?.siteConfig?.icons.appleTouchIcon, sizes: "180x180" },
{ rel: "manifest", href: "/site.webmanifest" },
{ rel: "stylesheet", href: appCss },
{ rel: "alternate", type: "application/rss+xml", title: "RSS Feed", href: "/rss.xml" },
{ rel: "alternate", type: "application/atom+xml", title: "Atom Feed", href: "/atom.xml" },
{ rel: "alternate", type: "application/feed+json", title: "JSON Feed", href: "/feed.json" },
],
scripts: [],
};
},
shellComponent: RootDocument,
});
// RSS文章类型定义
interface RssItem {
title: string;
link: string;
date: Date;
}
function RootDocument({ children }: { children: React.ReactNode }) {
const { siteConfig } = useRouteContext({ from: "__root__" });
const umamiWebsiteId = clientEnv().VITE_UMAMI_WEBSITE_ID;
const [newArticles, setNewArticles] = useState<RssItem[]>([]);
const [lastVisit, setLastVisit] = useState<Date | null>(null);
const [showPopup, setShowPopup] = useState(false);
const [isFirstVisit, setIsFirstVisit] = useState(true);
// 格式化时间显示
const formatTime = (d: Date) =>
d.toLocaleString("zh-CN", {
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit"
});
// 拉取RSS并筛选新文章(修复后版本)
const fetchAndFilterNewArticles = async (lastTime: Date) => {
try {
const res = await fetch("https://taiyanglee.eu.org/rss.xml");
if (!res.ok) throw new Error("RSS请求失败");
const text = await res.text();
const xml = new DOMParser().parseFromString(text, "application/xml");
const items = xml.querySelectorAll("item");
const filteredArticles: RssItem[] = [];
items.forEach((item) => {
// 兼容pubDate/dc:date/updated三种时间字段
const pubDateText =
item.querySelector("pubDate")?.textContent ||
item.querySelector("dc:date")?.textContent ||
item.querySelector("updated")?.textContent || "";
const articleDate = new Date(pubDateText);
// 校验时间有效性 + 筛选新文章
if (!isNaN(articleDate.getTime()) && articleDate > lastTime) {
filteredArticles.push({
title: item.querySelector("title")?.textContent || "无标题文章",
link: item.querySelector("link")?.textContent || "#",
date: articleDate
});
}
});
setNewArticles(filteredArticles);
setShowPopup(true); // 回访用户显示弹窗
} catch (err) {
console.log("新文章检测失败(不影响页面正常运行):", err);
}
};
// 页面加载时执行核心逻辑
useEffect(() => {
const now = new Date();
const lastVisitStr = localStorage.getItem("last_visit_time");
if (!lastVisitStr) {
// 新用户:仅记录时间,不显示弹窗
localStorage.setItem("last_visit_time", now.toISOString());
setIsFirstVisit(true);
return;
}
// 回访用户:读取历史时间并筛选新文章
const lastTime = new Date(lastVisitStr);
setLastVisit(lastTime);
setIsFirstVisit(false);
fetchAndFilterNewArticles(lastTime);
// 更新本次访问时间
localStorage.setItem("last_visit_time", now.toISOString());
}, []);
return (
<html lang={getLocale()} suppressHydrationWarning style={theme.getDocumentStyle?.(siteConfig)}>
<head><HeadContent /></head>
<body>
<ThemeProvider>{children}</ThemeProvider>
{/* 新文章提醒弹窗(最终优化版) */}
{!isFirstVisit && showPopup && lastVisit && (
<div
style={{
position: "fixed",
bottom: "80px", // 避开页面底部元素
right: "20px",
width: "280px",
background: "var(--background)",
border: "1px solid var(--border)",
borderRadius: "14px",
padding: "14px 16px",
fontSize: "13px",
boxShadow: "0 10px 30px -8px rgba(0,0,0,0.08)",
zIndex: 999,
backdropFilter: "blur(8px)",
transition: "all 0.2s ease",
}}
>
{/* 标题 + 关闭按钮 */}
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: "10px"
}}>
<div style={{
fontWeight: 600,
color: "var(--primary)",
fontSize: "14px"
}}>
自 {formatTime(lastVisit)} 后新增文章
</div>
<button
onClick={() => setShowPopup(false)}
style={{
background: "none",
border: "none",
color: "var(--muted-foreground)",
fontSize: "16px",
cursor: "pointer",
padding: "0 4px"
}}
>
×
</button>
</div>
{/* 文章列表/暂无提示(支持滚动) */}
<div style={{
display: "flex",
flexDirection: "column",
gap: "8px",
maxHeight: "160px", // 内容区最大高度
overflowY: "auto", // 超出自动滚动
paddingRight: "4px"
}}>
{newArticles.length > 0 ? (
newArticles.map((item, index) => (
<a
key={index}
href={item.link}
target="_blank"
rel="noreferrer"
style={{
color: "var(--foreground)",
textDecoration: "none",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
padding: "4px 2px",
transition: "color 0.2s"
}}
onMouseOver={(e) => e.currentTarget.style.color = "var(--primary)"}
onMouseOut={(e) => e.currentTarget.style.color = "var(--foreground)"}
>
• {item.title}
</a>
))
) : (
<div style={{ color: "var(--muted-foreground)" }}>暂无最新文章</div>
)}
</div>
</div>
)}
{/* 开发工具(保留原有功能) */}
<TanStackDevtools
config={{ position: "bottom-right" }}
plugins={[
{ name: "Tanstack Router", render: <TanStackRouterDevtoolsPanel /> },
TanStackQueryDevtools,
]}
/>
<Scripts />
</body>
</html>
);
}