NapCatQQ/packages/napcat-webui-frontend/src/components/system_info.tsx
手瓜一十雪 6e8adad7ca Improve layout and styling of NewVersionTip component
Added flexbox classes to center the update tip, adjusted Chip component styles for better alignment, and set a minimum width. Spinner size and alignment were also refined for consistency.
2026-01-22 13:41:01 +08:00

849 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Card, CardBody, CardHeader } from '@heroui/card';
import { Chip } from '@heroui/chip';
import { Spinner } from '@heroui/spinner';
import { Tooltip } from '@heroui/tooltip';
import { Select, SelectItem } from '@heroui/select';
import { Switch } from '@heroui/switch';
import { Pagination } from '@heroui/pagination';
import { Tabs, Tab } from '@heroui/tabs';
import { Input } from '@heroui/input';
import { useLocalStorage, useDebounce } from '@uidotdev/usehooks';
import { useRequest } from 'ahooks';
import clsx from 'clsx';
import { FaCircleInfo, FaQq } from 'react-icons/fa6';
import { IoLogoChrome, IoLogoOctocat, IoSearch } from 'react-icons/io5';
import { RiMacFill } from 'react-icons/ri';
import { useState, useCallback } from 'react';
import key from '@/const/key';
import WebUIManager from '@/controllers/webui_manager';
import useDialog from '@/hooks/use-dialog';
import Modal from '@/components/modal';
import { hasNewVersion, compareVersion } from '@/utils/version';
export interface SystemInfoItemProps {
title: string;
icon?: React.ReactNode;
value?: React.ReactNode;
endContent?: React.ReactNode;
hasBackground?: boolean;
onClick?: () => void;
clickable?: boolean;
}
const SystemInfoItem: React.FC<SystemInfoItemProps> = ({
title,
value = '--',
icon,
endContent,
hasBackground = false,
onClick,
clickable = false,
}) => {
return (
<div
className={clsx(
'flex text-sm gap-3 py-2 items-baseline transition-colors',
hasBackground
? 'text-white/90'
: 'text-default-600 dark:text-gray-300',
clickable && 'cursor-pointer hover:bg-default-100/50 dark:hover:bg-default-800/30 rounded-lg -mx-2 px-2'
)}
onClick={onClick}
>
<div className="text-lg opacity-70 self-center">{icon}</div>
<div className='w-24 font-medium'>{title}</div>
<div className={clsx(
'text-xs font-mono flex-1',
hasBackground ? 'text-white/80' : 'text-default-500'
)}>{value}</div>
<div className="self-center">{endContent}</div>
</div>
);
};
export interface NewVersionTipProps {
currentVersion?: string;
}
// 更新状态类型
type UpdateStatus = 'idle' | 'updating' | 'success' | 'error';
// 更新对话框内容组件
const UpdateDialogContent: React.FC<{
currentVersion: string;
latestVersion: string;
status: UpdateStatus;
errorMessage?: string;
}> = ({ currentVersion, latestVersion, status, errorMessage }) => {
return (
<div className='space-y-6'>
{/* 版本对比 */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 px-6 py-8 bg-default-50 dark:bg-default-100/5 rounded-xl border border-default-100 dark:border-default-100/10">
<div className="flex flex-col items-center gap-2 min-w-0 w-full sm:w-auto">
<span className="text-xs text-default-500 font-medium uppercase tracking-wider"></span>
<Tooltip content={`v${currentVersion}`}>
<Chip size="md" variant="flat" color="default" classNames={{ content: "font-mono font-bold text-sm truncate max-w-[120px] sm:max-w-[160px]" }}>
v{currentVersion}
</Chip>
</Tooltip>
</div>
<div className="flex flex-col items-center text-primary-500 px-4 shrink-0">
<div className="p-2 rounded-full bg-primary-50 dark:bg-primary-900/20">
<svg className="w-6 h-6 animate-pulse rotate-90 sm:rotate-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</div>
</div>
<div className="flex flex-col items-center gap-2 min-w-0 w-full sm:w-auto">
<span className="text-xs text-primary-500 font-medium uppercase tracking-wider"></span>
<Tooltip content={`v${latestVersion}`}>
<Chip size="md" color="primary" variant="shadow" classNames={{ content: "font-mono font-bold text-sm truncate max-w-[120px] sm:max-w-[160px]" }}>
v{latestVersion}
</Chip>
</Tooltip>
</div>
</div>
{/* 更新状态显示 */}
{status === 'updating' && (
<div className='flex flex-col items-center justify-center gap-3 py-4 px-4 rounded-lg bg-primary-50/50 dark:bg-primary-900/20 border border-primary-200/50 dark:border-primary-700/30'>
<Spinner size='md' color='primary' />
<div className='text-center'>
<p className='text-sm font-medium text-primary-600 dark:text-primary-400'>
...
</p>
<p className='text-xs text-default-500 mt-1'>
</p>
</div>
</div>
)}
{status === 'success' && (
<div className='flex flex-col items-center justify-center gap-3 py-4 px-4 rounded-lg bg-success-50/50 dark:bg-success-900/20 border border-success-200/50 dark:border-success-700/30'>
<div className='w-12 h-12 rounded-full bg-success-100 dark:bg-success-900/40 flex items-center justify-center'>
<svg className='w-6 h-6 text-success-600 dark:text-success-400' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M5 13l4 4L19 7' />
</svg>
</div>
<div className='text-center'>
<p className='text-sm font-medium text-success-600 dark:text-success-400'>
</p>
<p className='text-xs text-default-500 mt-1'>
NapCat
</p>
</div>
<div className='mt-2 p-3 rounded-lg bg-warning-50/50 dark:bg-warning-900/20 border border-warning-200/50 dark:border-warning-700/30'>
<p className='text-xs text-warning-700 dark:text-warning-400 flex items-center gap-1 justify-center'>
<svg className='w-4 h-4' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z' />
</svg>
<span> NapCat </span>
</p>
</div>
<div className='flex gap-3 justify-center mt-2 w-full'>
<button
className='px-4 py-2 text-sm rounded-lg bg-primary-500 hover:bg-primary-600 text-white shadow-sm transition-colors shadow-primary-500/20 w-full'
onClick={() => WebUIManager.restart()}
>
</button>
</div>
</div>
)}
{status === 'error' && (
<div className='flex flex-col items-center justify-center gap-3 py-4 px-4 rounded-lg bg-danger-50/50 dark:bg-danger-900/20 border border-danger-200/50 dark:border-danger-700/30'>
<div className='w-12 h-12 rounded-full bg-danger-100 dark:bg-danger-900/40 flex items-center justify-center'>
<svg className='w-6 h-6 text-danger-600 dark:text-danger-400' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M6 18L18 6M6 6l12 12' />
</svg>
</div>
<div className='text-center'>
<p className='text-sm font-medium text-danger-600 dark:text-danger-400'>
</p>
<p className='text-xs text-default-500 mt-1'>
{errorMessage || '请稍后重试或手动更新'}
</p>
</div>
</div>
)}
</div>
);
};
const NewVersionTip = (props: NewVersionTipProps) => {
const { currentVersion } = props;
const dialog = useDialog();
const { data: latestVersion, error } = useRequest(WebUIManager.getLatestTag, {
cacheKey: 'napcat-latest-tag',
staleTime: 10 * 60 * 1000,
cacheTime: 30 * 60 * 1000,
});
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('idle');
// 使用 SemVer 规范比较版本号
if (error || !latestVersion || !currentVersion || !hasNewVersion(currentVersion, latestVersion)) {
return null;
}
const handleUpdate = async () => {
setUpdateStatus('updating');
try {
await WebUIManager.UpdateNapCat();
setUpdateStatus('success');
// 显示更新成功对话框
dialog.alert({
title: '更新完成',
content: (
<UpdateDialogContent
currentVersion={currentVersion}
latestVersion={latestVersion}
status='success'
/>
),
confirmText: '我知道了',
size: 'md',
});
} catch (err) {
console.error('Update failed:', err);
const errMessage = err instanceof Error ? err.message : '未知错误';
setUpdateStatus('error');
// 显示更新失败对话框
dialog.alert({
title: '更新失败',
content: (
<UpdateDialogContent
currentVersion={currentVersion}
latestVersion={latestVersion}
status='error'
errorMessage={errMessage}
/>
),
confirmText: '确定',
size: 'md',
});
}
};
const showUpdateDialog = () => {
dialog.confirm({
title: '发现新版本',
content: (
<UpdateDialogContent
currentVersion={currentVersion}
latestVersion={latestVersion}
status='idle'
/>
),
confirmText: '立即更新',
cancelText: '稍后更新',
size: 'md',
onConfirm: handleUpdate,
});
};
return (
<Tooltip content='有新版本可用'>
<div className="cursor-pointer flex items-center justify-center" onClick={updateStatus === 'updating' ? undefined : showUpdateDialog}>
<Chip
size="sm"
color="danger"
variant="flat"
classNames={{
content: "font-bold text-[10px] px-1 flex items-center justify-center",
base: "h-5 min-h-5 min-w-[42px]"
}}
>
{updateStatus === 'updating' ? <Spinner size="sm" color="danger" classNames={{ wrapper: "w-3 h-3" }} /> : 'New'}
</Chip>
</div>
</Tooltip>
);
};
// 版本信息类型
interface VersionInfo {
tag: string;
type: 'release' | 'prerelease' | 'action';
artifactId?: number;
artifactName?: string;
createdAt?: string;
expiresAt?: string;
size?: number;
workflowRunId?: number;
headSha?: string;
workflowTitle?: string;
}
// 版本选择对话框内容
interface VersionSelectDialogProps {
currentVersion: string;
onClose: () => void;
}
const VersionSelectDialogContent: React.FC<VersionSelectDialogProps> = ({
currentVersion,
onClose,
}) => {
const dialog = useDialog();
const [selectedVersion, setSelectedVersion] = useState<VersionInfo | null>(null);
const [forceUpdate, setForceUpdate] = useState(false);
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('idle');
const [errorMessage, setErrorMessage] = useState<string>('');
const [currentPage, setCurrentPage] = useState(1);
const [activeTab, setActiveTab] = useState<'release' | 'action'>('release');
const [searchQuery, setSearchQuery] = useState('');
const debouncedSearch = useDebounce(searchQuery, 300);
const [selectedMirror, setSelectedMirror] = useState<string | undefined>(undefined);
const { data: mirrorsData } = useRequest(WebUIManager.getMirrors, {
cacheKey: 'napcat-mirrors',
staleTime: 60 * 60 * 1000,
});
const mirrors = mirrorsData?.mirrors || [];
const pageSize = 15;
// 获取所有可用版本(带分页、过滤和搜索)
// 懒加载:根据 activeTab 只获取对应类型的版本
const { data: releasesData, loading: releasesLoading, error: releasesError } = useRequest(
() => WebUIManager.getAllReleases({
page: currentPage,
pageSize,
type: activeTab,
search: debouncedSearch,
mirror: selectedMirror
}),
{
refreshDeps: [currentPage, activeTab, debouncedSearch, selectedMirror],
}
);
// 版本列表已在后端过滤,直接使用
const filteredVersions = (releasesData?.versions || []) as VersionInfo[];
// 检查是否是降级(使用语义化版本比较)
const isDowngrade = useCallback((targetTag: string): boolean => {
if (!currentVersion || !targetTag) return false;
// Action 版本不算降级
if (targetTag.startsWith('action-')) return false;
return compareVersion(targetTag, currentVersion) < 0;
}, [currentVersion]);
const selectedVersionTag = selectedVersion?.tag || '';
const isSelectedDowngrade = isDowngrade(selectedVersionTag);
const performUpdate = async (force: boolean) => {
if (!selectedVersion) return;
setUpdateStatus('updating');
setErrorMessage('');
try {
await WebUIManager.UpdateNapCatToVersion(selectedVersionTag, force, selectedMirror);
setUpdateStatus('success');
} catch (err) {
console.error('Update failed:', err);
const errMsg = err instanceof Error ? err.message : '未知错误';
setErrorMessage(errMsg);
setUpdateStatus('error');
}
};
const handleUpdate = async () => {
if (!selectedVersion) return;
if (isSelectedDowngrade && !forceUpdate) {
dialog.confirm({
title: '确认降级',
content: (
<div className='space-y-2'>
<p className='text-warning-600'>
<strong>v{currentVersion}</strong> <strong>{selectedVersionTag}</strong>
</p>
<p className='text-sm text-default-500'>
</p>
</div>
),
confirmText: '确认降级',
cancelText: '取消',
onConfirm: () => performUpdate(true),
});
return;
}
await performUpdate(forceUpdate);
};
// 处理分页变化
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
if (updateStatus === 'success') {
return (
<div className='flex flex-col items-center justify-center gap-3 py-4'>
<div className='w-12 h-12 rounded-full bg-success-100 dark:bg-success-900/40 flex items-center justify-center'>
<svg className='w-6 h-6 text-success-600 dark:text-success-400' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M5 13l4 4L19 7' />
</svg>
</div>
<div className='text-center w-full'>
<p className='text-sm font-medium text-success-600 dark:text-success-400'>
{selectedVersionTag}
</p>
<p className='text-xs text-default-500 mt-1 mb-6'>
NapCat
</p>
<div className='flex gap-3 justify-center'>
<button
className='px-4 py-2 text-sm rounded-lg bg-default-100 hover:bg-default-200 transition-colors text-default-700'
onClick={onClose}
>
</button>
<button
className='px-4 py-2 text-sm rounded-lg bg-primary-500 hover:bg-primary-600 text-white shadow-sm transition-colors shadow-primary-500/20'
onClick={async () => {
await WebUIManager.restart();
onClose();
}}
>
</button>
</div>
</div>
</div>
);
}
if (updateStatus === 'error') {
return (
<div className='flex flex-col items-center justify-center gap-3 py-4'>
<div className='w-12 h-12 rounded-full bg-danger-100 dark:bg-danger-900/40 flex items-center justify-center'>
<svg className='w-6 h-6 text-danger-600 dark:text-danger-400' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M6 18L18 6M6 6l12 12' />
</svg>
</div>
<div className='text-center'>
<p className='text-sm font-medium text-danger-600 dark:text-danger-400'>
</p>
<p className='text-xs text-default-500 mt-1'>
{errorMessage || '请稍后重试'}
</p>
</div>
</div>
);
}
if (updateStatus === 'updating') {
return (
<div className='flex flex-col items-center justify-center gap-3 py-6'>
<Spinner size='lg' color='primary' />
<div className='text-center'>
<p className='text-sm font-medium text-primary-600 dark:text-primary-400'>
{selectedVersionTag}...
</p>
<p className='text-xs text-default-500 mt-1'>
</p>
</div>
</div>
);
}
const pagination = releasesData?.pagination;
return (
<div className='space-y-4'>
{/* 当前版本 */}
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<span className='text-sm text-default-600'>:</span>
<Chip color='primary' variant='flat' size='sm'>
v{currentVersion}
</Chip>
</div>
{releasesData?.mirror && (
<div className='text-xs text-default-400 flex items-center gap-1'>
<span className='w-2 h-2 rounded-full bg-success-500'></span>
: {releasesData.mirror}
</div>
)}
</div>
{/* 版本类型切换 */}
<Tabs
selectedKey={activeTab}
onSelectionChange={(key) => {
setActiveTab(key as 'release' | 'action');
setCurrentPage(1);
setSelectedVersion(null);
setSearchQuery('');
}}
size='sm'
color='primary'
variant='underlined'
classNames={{
tabList: 'gap-4',
}}
>
<Tab key='release' title='正式版本' />
<Tab key='action' title='临时版本 (Action)' />
</Tabs>
<div className="flex gap-2">
{/* 搜索框 */}
<Input
placeholder='搜索版本号...'
size='sm'
value={searchQuery}
onValueChange={(value) => {
setSearchQuery(value);
setCurrentPage(1);
setSelectedVersion(null);
}}
startContent={<IoSearch className='text-default-400' />}
isClearable
onClear={() => setSearchQuery('')}
classNames={{
inputWrapper: 'h-9',
base: 'flex-1'
}}
/>
{/* 镜像选择 */}
<Select
placeholder="自动选择 (默认)"
selectedKeys={selectedMirror ? [selectedMirror] : ['default']}
onSelectionChange={(keys) => {
const m = Array.from(keys)[0] as string;
setSelectedMirror(m === 'default' ? undefined : m);
}}
size="sm"
className="w-48"
classNames={{ trigger: 'h-9 min-h-9' }}
aria-label="选择镜像源"
>
{['default', ...mirrors].map(m => (
<SelectItem key={m} textValue={m === 'default' ? '自动选择' : m}>
{m === 'default' ? '自动选择 (默认)' : m}
</SelectItem>
))}
</Select>
</div>
{/* 版本选择 */}
<div className='space-y-2'>
<div className='flex items-center justify-between'>
<label className='text-sm font-medium text-default-700'></label>
{releasesData?.pagination && (
<span className='text-xs text-default-400'>
{releasesData.pagination.total}
</span>
)}
</div>
{releasesLoading ? (
<div className='flex items-center gap-2 py-2'>
<Spinner size='sm' />
<span className='text-sm text-default-500'>...</span>
</div>
) : releasesError ? (
<div className='text-sm text-danger-500'>
: {releasesError.message}
</div>
) : filteredVersions.length === 0 ? (
<div className='text-sm text-default-500 py-4 text-center'>
{searchQuery ? `未找到匹配 "${searchQuery}" 的版本` : '暂无可用版本'}
</div>
) : (
<Select
label='选择版本'
placeholder='请选择要更新的版本'
selectedKeys={selectedVersion ? [selectedVersionTag] : []}
onSelectionChange={(keys) => {
const selectedTag = Array.from(keys)[0] as string;
const version = filteredVersions.find(v => v.tag === selectedTag);
setSelectedVersion(version || null);
}}
classNames={{
trigger: 'h-auto min-h-10',
}}
>
{filteredVersions.map((version) => {
const isCurrent = version.tag.replace(/^v/, '') === currentVersion;
const downgrade = isDowngrade(version.tag);
return (
<SelectItem
key={version.tag}
textValue={version.tag}
>
<div className='flex flex-col gap-0.5'>
<div className='flex items-center gap-2'>
<span className="truncate max-w-[300px]">
{version.type === 'action'
? (version.workflowTitle || version.artifactName || version.tag)
: version.tag
}
</span>
{version.type === 'prerelease' && (
<Chip size='sm' color='secondary' variant='flat'></Chip>
)}
{version.type === 'action' && (
<Chip size='sm' color='default' variant='flat'></Chip>
)}
{isCurrent && (
<Chip size='sm' color='success' variant='flat'></Chip>
)}
{downgrade && !isCurrent && version.type !== 'action' && (
<Chip size='sm' color='warning' variant='flat'></Chip>
)}
</div>
{version.type === 'action' && (
<div className='text-xs text-default-400 flex items-center gap-2'>
<span className='font-mono bg-default-100 dark:bg-default-100/10 px-1 rounded'>{version.tag}</span>
{version.headSha && <span className='font-mono' title={version.headSha}>{version.headSha.slice(0, 7)}</span>}
{version.createdAt && <span>{new Date(version.createdAt).toLocaleString()}</span>}
{version.size && <span>{(version.size / 1024 / 1024).toFixed(1)} MB</span>}
</div>
)}
</div>
</SelectItem>
);
})}
</Select>
)}
</div>
{/* Action 版本提示 */}
{activeTab === 'action' && (
<div className='p-3 rounded-lg bg-default-50 dark:bg-default-100/10 border border-default-200/50'>
<p className='text-xs text-default-500'>
GitHub Actions
{selectedVersion?.expiresAt && (
<span className='block mt-1 text-warning-600'>
{new Date(selectedVersion.expiresAt).toLocaleDateString()}
</span>
)}
</p>
</div>
)}
{/* 降级警告 */}
{selectedVersion && isSelectedDowngrade && (
<div className='p-3 rounded-lg bg-warning-50/50 dark:bg-warning-900/20 border border-warning-200/50 dark:border-warning-700/30'>
<div className='flex items-start gap-2'>
<svg className='w-5 h-5 text-warning-600 dark:text-warning-400 flex-shrink-0 mt-0.5' fill='none' viewBox='0 0 24 24' stroke='currentColor'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z' />
</svg>
<div>
<p className='text-sm font-medium text-warning-700 dark:text-warning-400'>
</p>
<p className='text-xs text-warning-600/80 dark:text-warning-500 mt-1'>
</p>
</div>
</div>
<div className='mt-3 flex items-center gap-2'>
<Switch
size='sm'
isSelected={forceUpdate}
onValueChange={setForceUpdate}
/>
<span className='text-xs text-warning-700 dark:text-warning-400'>
</span>
</div>
</div>
)}
{/* 分页 */}
{pagination && pagination.totalPages > 1 && (
<div className='flex justify-center'>
<Pagination
total={pagination.totalPages}
page={currentPage}
onChange={handlePageChange}
size='sm'
showControls
/>
</div>
)}
{/* 操作按钮 */}
<div className='flex justify-end gap-2 pt-4 border-t border-default-100 dark:border-default-100/10'>
<button
className='px-4 py-2 text-sm rounded-lg bg-default-100 hover:bg-default-200 transition-colors'
onClick={onClose}
>
</button>
<button
className={clsx(
'px-4 py-2 text-sm rounded-lg transition-colors text-white shadow-sm',
selectedVersion && (!isSelectedDowngrade || forceUpdate)
? 'bg-primary-500 hover:bg-primary-600 shadow-primary-500/20'
: 'bg-default-300 cursor-not-allowed'
)}
disabled={!selectedVersion || (isSelectedDowngrade && !forceUpdate)}
onClick={handleUpdate}
>
{isSelectedDowngrade ? '确认降级更新' : '更新到此版本'}
</button>
</div>
</div>
);
};
interface NapCatVersionProps {
hasBackground?: boolean;
}
const NapCatVersion: React.FC<NapCatVersionProps> = ({ hasBackground = false }) => {
const [isVersionModalOpen, setIsVersionModalOpen] = useState(false);
const {
data: packageData,
loading: packageLoading,
error: packageError,
} = useRequest(WebUIManager.GetNapCatVersion, {
cacheKey: 'napcat-version',
staleTime: 60 * 60 * 1000,
cacheTime: 24 * 60 * 60 * 1000,
});
const currentVersion = packageData?.version;
// 点击版本号时显示版本选择对话框
const handleVersionClick = useCallback(() => {
if (!currentVersion) return;
setIsVersionModalOpen(true);
}, [currentVersion]);
return (
<>
<SystemInfoItem
title='NapCat 版本'
icon={<IoLogoOctocat className='text-xl' />}
hasBackground={hasBackground}
value={
packageError
? (
`错误:${packageError.message}`
)
: packageLoading
? (
<Spinner size='sm' />
)
: (
<Tooltip content='点击管理版本'>
<span
className='cursor-pointer hover:text-primary-500 transition-colors underline decoration-dashed underline-offset-2'
onClick={handleVersionClick}
>
{currentVersion}
</span>
</Tooltip>
)
}
endContent={<NewVersionTip currentVersion={currentVersion} />}
/>
{isVersionModalOpen && (
<Modal
title='版本管理'
size='lg'
hideFooter={true}
onClose={() => setIsVersionModalOpen(false)}
content={
<VersionSelectDialogContent
currentVersion={currentVersion || ''}
onClose={() => setIsVersionModalOpen(false)}
/>
}
/>
)}
</>
);
};
export interface SystemInfoProps {
archInfo?: string;
}
const SystemInfo: React.FC<SystemInfoProps> = (props) => {
const { archInfo } = props;
const {
data: qqVersionData,
loading: qqVersionLoading,
error: qqVersionError,
} = useRequest(WebUIManager.getQQVersion, {
cacheKey: 'qq-version',
staleTime: 60 * 60 * 1000,
cacheTime: 24 * 60 * 60 * 1000,
});
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
const hasBackground = !!backgroundImage;
return (
<Card className={clsx(
'backdrop-blur-sm border border-white/40 dark:border-white/10 shadow-sm overflow-visible flex-1',
hasBackground ? 'bg-white/10 dark:bg-black/10' : 'bg-white/60 dark:bg-black/40'
)}>
<CardHeader className={clsx(
'pb-0 items-center gap-2 font-bold px-4 pt-4',
hasBackground ? 'text-white drop-shadow-sm' : 'text-default-700 dark:text-white'
)}>
<FaCircleInfo className='text-lg opacity-80' />
<span></span>
</CardHeader>
<CardBody className='flex-1'>
<div className='flex flex-col gap-2 justify-between h-full'>
<NapCatVersion hasBackground={hasBackground} />
<SystemInfoItem
title='QQ 版本'
icon={<FaQq className='text-lg' />}
hasBackground={hasBackground}
value={
qqVersionError
? (
`错误:${qqVersionError.message}`
)
: qqVersionLoading
? (
<Spinner size='sm' />
)
: (
qqVersionData
)
}
/>
<SystemInfoItem
title='WebUI 版本'
icon={<IoLogoChrome className='text-xl' />}
value='Next'
hasBackground={hasBackground}
/>
<SystemInfoItem
title='系统版本'
icon={<RiMacFill className='text-xl' />}
value={archInfo}
hasBackground={hasBackground}
/>
</div>
</CardBody>
</Card>
);
};
export default SystemInfo;