NapCatQQ/packages/napcat-webui-frontend/src/components/system_info.tsx
手瓜一十雪 173a165c4b Add latest version check and update prompt to UI
Introduces backend and frontend logic to fetch the latest NapCat version tag from multiple sources, exposes a new API endpoint, and adds a UI prompt to notify users of new versions with an update button. Also includes minor code style improvements in dialog context.
2025-11-22 13:52:49 +08:00

348 lines
10 KiB
TypeScript

import { Card, CardBody, CardHeader } from '@heroui/card';
import { Button } from '@heroui/button';
import { Chip } from '@heroui/chip';
import { Spinner } from '@heroui/spinner';
import { Tooltip } from '@heroui/tooltip';
import { useRequest } from 'ahooks';
import { FaCircleInfo, FaInfo, FaQq } from 'react-icons/fa6';
import { IoLogoChrome, IoLogoOctocat } from 'react-icons/io';
import { RiMacFill } from 'react-icons/ri';
import { useState } from 'react';
import toast from 'react-hot-toast';
import WebUIManager from '@/controllers/webui_manager';
import useDialog from '@/hooks/use-dialog';
export interface SystemInfoItemProps {
title: string;
icon?: React.ReactNode;
value?: React.ReactNode;
endContent?: React.ReactNode;
}
const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
title,
value = '--',
icon,
endContent,
}) => {
return (
<div className='flex text-sm gap-1 p-2 items-center shadow-sm shadow-primary-100 dark:shadow-primary-100 rounded text-primary-400'>
{icon}
<div className='w-24'>{title}</div>
<div className='text-primary-200'>{value}</div>
<div className='ml-auto'>{endContent}</div>
</div>
);
};
export interface NewVersionTipProps {
currentVersion?: string;
}
// const NewVersionTip = (props: NewVersionTipProps) => {
// const { currentVersion } = props;
// const dialog = useDialog();
// const { data: releaseData, error } = useRequest(() =>
// request.get<GithubRelease[]>(
// 'https://api.github.com/repos/NapNeko/NapCatQQ/releases'
// )
// );
// if (error) {
// return (
// <Tooltip content='检查新版本失败'>
// <Button
// isIconOnly
// radius='full'
// color='primary'
// variant='shadow'
// className='!w-5 !h-5 !min-w-0 text-small shadow-md'
// onPress={() => {
// dialog.alert({
// title: '检查新版本失败',
// content: error.message,
// });
// }}
// >
// <FaInfo />
// </Button>
// </Tooltip>
// );
// }
// const latestVersion = releaseData?.data?.[0]?.tag_name;
// if (!latestVersion || !currentVersion) {
// return null;
// }
// if (compareVersion(latestVersion, currentVersion) <= 0) {
// return null;
// }
// const middleVersions: GithubRelease[] = [];
// for (let i = 0; i < releaseData.data.length; i++) {
// const versionInfo = releaseData.data[i];
// if (compareVersion(versionInfo.tag_name, currentVersion) > 0) {
// middleVersions.push(versionInfo);
// } else {
// break;
// }
// }
// const AISummaryComponent = () => {
// const {
// data: aiSummaryData,
// loading: aiSummaryLoading,
// error: aiSummaryError,
// run: runAiSummary,
// } = useRequest(
// (version) =>
// request.get<ServerResponse<string | null>>(
// `https://release.nc.152710.xyz/?version=${version}`,
// {
// timeout: 30000,
// }
// ),
// {
// manual: true,
// }
// );
// useEffect(() => {
// runAiSummary(currentVersion);
// }, [currentVersion, runAiSummary]);
// if (aiSummaryLoading) {
// return (
// <div className='flex justify-center py-1'>
// <Spinner size='sm' />
// </div>
// );
// }
// if (aiSummaryError) {
// return <div className='text-center text-primary-500'>AI 摘要获取失败</div>;
// }
// return <span className='text-default-700'>{aiSummaryData?.data.data}</span>;
// };
// return (
// <Tooltip content='有新版本可用'>
// <Button
// isIconOnly
// radius='full'
// color='primary'
// variant='shadow'
// className='!w-5 !h-5 !min-w-0 text-small shadow-md'
// onPress={() => {
// dialog.confirm({
// title: '有新版本可用',
// content: (
// <div className='space-y-2'>
// <div className='text-sm space-x-2'>
// <span>当前版本</span>
// <Chip color='primary' variant='flat'>
// v{currentVersion}
// </Chip>
// </div>
// <div className='text-sm space-x-2'>
// <span>最新版本</span>
// <Chip color='primary'>{latestVersion}</Chip>
// </div>
// <div className='p-2 rounded-md bg-content2 text-sm'>
// <div className='text-primary-400 font-bold flex items-center gap-1 mb-1'>
// <BsStars />
// <span>AI总结</span>
// </div>
// <AISummaryComponent />
// </div>
// <div className='text-sm space-y-2 !mt-4'>
// {middleVersions.map((versionInfo) => (
// <div
// key={versionInfo.tag_name}
// className='p-4 bg-content1 rounded-md shadow-small'
// >
// <TailwindMarkdown content={versionInfo.body} />
// </div>
// ))}
// </div>
// </div>
// ),
// scrollBehavior: 'inside',
// size: '3xl',
// confirmText: '前往下载',
// onConfirm () {
// window.open(
// 'https://github.com/NapNeko/NapCatQQ/releases',
// '_blank',
// 'noopener'
// );
// },
// });
// }}
// >
// <FaInfo />
// </Button>
// </Tooltip>
// );
// };
const NewVersionTip = (props: NewVersionTipProps) => {
const { currentVersion } = props;
const dialog = useDialog();
const { data: latestVersion, error } = useRequest(WebUIManager.getLatestTag);
const [updating, setUpdating] = useState(false);
if (error || !latestVersion || !currentVersion || latestVersion === currentVersion) {
return null;
}
return (
<Tooltip content='有新版本可用'>
<Button
isIconOnly
radius='full'
color='primary'
variant='shadow'
className='!w-5 !h-5 !min-w-0 text-small shadow-md'
onPress={() => {
dialog.confirm({
title: '有新版本可用',
content: (
<div className='space-y-2'>
<div className='text-sm space-x-2'>
<span></span>
<Chip color='primary' variant='flat'>
{currentVersion}
</Chip>
</div>
<div className='text-sm space-x-2'>
<span></span>
<Chip color='primary'>{latestVersion}</Chip>
</div>
{updating && (
<div className='flex justify-center'>
<Spinner size='sm' />
</div>
)}
</div>
),
confirmText: updating ? '更新中...' : '更新',
onConfirm: async () => {
setUpdating(true);
toast('更新中,预计需要几分钟,请耐心等待', {
duration: 3000,
});
try {
await WebUIManager.UpdateNapCat();
toast.success('更新完成,重启生效', {
duration: 5000,
});
} catch (error) {
console.error('Update failed:', error);
toast.success('更新异常', {
duration: 5000,
});
} finally {
setUpdating(false);
}
},
});
}}
>
<FaInfo />
</Button>
</Tooltip>
);
};
const NapCatVersion = () => {
const {
data: packageData,
loading: packageLoading,
error: packageError,
} = useRequest(WebUIManager.GetNapCatVersion);
const currentVersion = packageData?.version;
return (
<SystemInfoItem
title='NapCat 版本'
icon={<IoLogoOctocat className='text-xl' />}
value={
packageError
? (
`错误:${packageError.message}`
)
: packageLoading
? (
<Spinner size='sm' />
)
: (
currentVersion
)
}
endContent={<NewVersionTip currentVersion={currentVersion} />}
/>
);
};
export interface SystemInfoProps {
archInfo?: string;
}
const SystemInfo: React.FC<SystemInfoProps> = (props) => {
const { archInfo } = props;
const {
data: qqVersionData,
loading: qqVersionLoading,
error: qqVersionError,
} = useRequest(WebUIManager.getQQVersion);
return (
<Card className='bg-opacity-60 shadow-sm shadow-primary-100 dark:shadow-primary-100 overflow-visible flex-1'>
<CardHeader className='pb-0 items-center gap-1 text-primary-500 font-extrabold'>
<FaCircleInfo className='text-lg' />
<span></span>
</CardHeader>
<CardBody className='flex-1'>
<div className='flex flex-col justify-between h-full'>
<NapCatVersion />
<SystemInfoItem
title='QQ 版本'
icon={<FaQq className='text-lg' />}
value={
qqVersionError
? (
`错误:${qqVersionError.message}`
)
: qqVersionLoading
? (
<Spinner size='sm' />
)
: (
qqVersionData
)
}
/>
<SystemInfoItem
title='WebUI 版本'
icon={<IoLogoChrome className='text-xl' />}
value='Next'
/>
<SystemInfoItem
title='系统版本'
icon={<RiMacFill className='text-xl' />}
value={archInfo}
/>
</div>
</CardBody>
</Card>
);
};
export default SystemInfo;