NapCatQQ source snapshot
Build NapCat Artifacts / Type Check & Test (push) Waiting to run
Release Tag / validate-version (push) Waiting to run

This commit is contained in:
TanTan committed 2026-11-13 09:01:47 +08:00
commit f954252a34
1111 files changed
+139867

No files matched your search

@@ -0,0 +1,227 @@
import { BreadcrumbItem, Breadcrumbs } from '@heroui/breadcrumbs';
import { Button } from '@heroui/button';
import { useLocalStorage } from '@uidotdev/usehooks';
import clsx from 'clsx';
import { AnimatePresence, motion } from 'motion/react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { MdMenu, MdMenuOpen } from 'react-icons/md';
import { useLocation, useNavigate } from 'react-router-dom';
import key from '@/const/key';
import errorFallbackRender from '@/components/error_fallback';
import PageLoading from '@/components/page_loading';
import SideBar from '@/components/sidebar';
import useAuth from '@/hooks/auth';
import useDialog from '@/hooks/use-dialog';
import type { MenuItem } from '@/config/site';
import { siteConfig } from '@/config/site';
import QQManager from '@/controllers/qq_manager';
import ProcessManager from '@/controllers/process_manager';
import { waitForBackendReady } from '@/utils/process_utils';
const menus: MenuItem[] = siteConfig.navItems;
const findTitle = (menus: MenuItem[], pathname: string): string[] => {
const paths: string[] = [];
if (pathname) {
for (const item of menus) {
if (item.href === pathname) {
paths.push(item.label);
} else if (item.items) {
const title = findTitle(item.items, pathname);
if (title.length > 0) {
paths.push(item.label);
paths.push(...title);
}
}
}
}
return paths;
};
const Layout: React.FC<{ children: React.ReactNode; }> = ({ children }) => {
const location = useLocation();
const contentRef = useRef<HTMLDivElement>(null);
const [openSideBar, setOpenSideBar] = useLocalStorage(key.sideBarOpen, true);
const [b64img] = useLocalStorage(key.backgroundImage, '');
const navigate = useNavigate();
const { isAuth, revokeAuth } = useAuth();
const dialog = useDialog();
const isOnlineRef = useRef(true);
const [isRestarting, setIsRestarting] = useState(false);
// 定期检查 QQ 在线状态,掉线时弹窗提示
useEffect(() => {
if (!isAuth) return;
const checkOnlineStatus = async () => {
const currentPath = location.pathname;
if (currentPath === '/qq_login' || currentPath === '/web_login') return;
try {
const info = await QQManager.getQQLoginInfo();
if (info?.online === false && isOnlineRef.current === true) {
isOnlineRef.current = false;
dialog.confirm({
title: '账号已离线',
content: '您的 QQ 账号已掉线,是否重启进程以重新登录?',
confirmText: '重启进程',
cancelText: '退出账户',
onConfirm: async () => {
setIsRestarting(true);
try {
await ProcessManager.restartProcess();
} catch (_e) {
// 忽略错误,因为后端正在重启关闭连接
}
// 轮询探测后端是否恢复
await waitForBackendReady(
15000, // 15秒超时
() => {
setIsRestarting(false);
window.location.reload();
},
() => {
setIsRestarting(false);
dialog.alert({
title: '启动超时',
content: '后端在 15 秒内未响应,请检查 NapCat 运行日志或手动重启。',
});
}
);
},
onCancel: () => {
revokeAuth();
navigate('/web_login');
},
});
} else if (info?.online === true) {
isOnlineRef.current = true;
}
} catch (_e) {
// 忽略请求错误
}
};
const timer = setInterval(checkOnlineStatus, 5000);
checkOnlineStatus();
return () => clearInterval(timer);
}, [isAuth, location.pathname]);
const checkIsQQLogin = async () => {
try {
const result = await QQManager.checkQQLoginStatus();
// 掉线状态由 checkOnlineStatus 定期检测并弹窗处理,这里只处理未登录
if (result.isOffline) {
// 已登录但掉线,标记状态,等待 checkOnlineStatus 弹窗
isOnlineRef.current = false;
} else if (!result.isLogin) {
// 未登录状态:跳转到登录页面
if (isAuth) {
navigate('/qq_login', { replace: true });
} else {
navigate('/web_login', { replace: true });
}
}
} catch (_error) {
navigate('/web_login', { replace: true });
}
};
useEffect(() => {
contentRef?.current?.scrollTo?.({
top: 0,
behavior: 'smooth',
});
}, [location.pathname]);
useEffect(() => {
if (isAuth) {
checkIsQQLogin();
}
}, [isAuth]);
const title = useMemo(() => {
return findTitle(menus, location.pathname);
}, [location.pathname]);
return (
<div
className='h-screen relative flex items-stretch overflow-hidden'
style={{
backgroundImage: b64img ? `url(${b64img})` : undefined,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<PageLoading loading={isRestarting} />
<SideBar
items={menus}
open={openSideBar}
onClose={() => setOpenSideBar(false)}
/>
<motion.div
layout
ref={contentRef}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4 }}
className={clsx(
'flex-1 overflow-y-auto',
'transition-all duration-300 ease-in-out',
openSideBar ? 'ml-0' : 'ml-0',
'pb-10 md:pb-0'
)}
>
<div
className={clsx(
'h-10 flex items-center font-bold text-xl backdrop-blur-lg rounded-full',
'dark:bg-background dark:shadow-primary-100',
'bg-background !bg-opacity-50',
'shadow-sm shadow-primary-50',
'z-30 m-2 mb-0 sticky top-2 left-0'
)}
>
<div
className={clsx(
'mr-1 ease-in-out ml-0 md:relative z-50 md:z-auto',
openSideBar && 'pl-2',
'md:!ml-0 md:pl-0'
)}
>
<Button
isIconOnly
radius='full'
variant='light'
onPress={() => setOpenSideBar(!openSideBar)}
>
{openSideBar ? <MdMenuOpen size={24} /> : <MdMenu size={24} />}
</Button>
</div>
<Breadcrumbs isDisabled size='lg'>
{title?.map((item, index) => (
<BreadcrumbItem key={index}>
<AnimatePresence mode='wait'>
<motion.div
key={item}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.3 }}
>
{item}
</motion.div>
</AnimatePresence>
</BreadcrumbItem>
))}
</Breadcrumbs>
</div>
<ErrorBoundary fallbackRender={errorFallbackRender}>
{children}
</ErrorBoundary>
</motion.div>
</div>
);
};
export default Layout;
@@ -0,0 +1,25 @@
import { Link } from '@heroui/link';
import { Tooltip } from '@heroui/tooltip';
import { GithubIcon } from '@/components/icons';
export default function PureLayout ({
children,
}: {
children: React.ReactNode
}) {
return (
<div className='relative flex flex-col h-screen'>
<div className='absolute right-0 top-0 p-2'>
<Tooltip content='查看WebUI源码' placement='left' showArrow>
<Link isExternal href='https://github.com/bietiaop/NextNapCatWebUI'>
<GithubIcon className='text-default-900 hover:text-default-600 w-10 h-10 hover:drop-shadow-lg transition-all' />
</Link>
</Tooltip>
</div>
<main className='flex-grow w-full flex flex-col justify-center items-center'>
{children}
</main>
</div>
);
}