Refactor dashboard and components, remove echarts
Some checks are pending
Build NapCat Artifacts / Build-Framework (push) Waiting to run
Build NapCat Artifacts / Build-Shell (push) Waiting to run

Replaces echarts-based usage pie chart with a custom SVG implementation, removing the echarts dependency. Improves caching for version and system info requests, simplifies page background to static elements, and switches dashboard state to use localStorage for persistence. Also removes polling from hitokoto and updates button styling in system info.
This commit is contained in:
手瓜一十雪 2025-12-24 13:56:34 +08:00
parent e56b912bbd
commit fa3a229827
6 changed files with 113 additions and 158 deletions

View File

@ -59,7 +59,6 @@
"axios": "^1.7.9",
"clsx": "^2.1.1",
"crypto-js": "^4.2.0",
"echarts": "^5.5.1",
"event-source-polyfill": "^1.0.31",
"framer-motion": "^12.0.6",
"monaco-editor": "^0.52.2",
@ -124,4 +123,4 @@
"react-dom": "$react-dom"
}
}
}
}

View File

@ -19,7 +19,6 @@ export default function Hitokoto () {
loading,
run,
} = useRequest(() => request.get<IHitokoto>('https://hitokoto.152710.xyz/'), {
pollingInterval: 10000,
throttleWait: 1000,
});
const backupData = {

View File

@ -1,34 +1,15 @@
import { motion } from 'motion/react';
const PageBackground = () => {
return (
<div className='fixed inset-0 w-full h-full -z-10 overflow-hidden bg-gradient-to-br from-indigo-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900'>
{/* 动态呼吸光斑 - ACG风格 */}
<motion.div
animate={{
scale: [1, 1.2, 1],
rotate: [0, 90, 0],
opacity: [0.3, 0.5, 0.3]
}}
transition={{ duration: 15, repeat: Infinity, ease: "easeInOut" }}
{/* 静态光斑 - ACG风格 */}
<div
className='absolute top-[-10%] left-[-10%] w-[500px] h-[500px] rounded-full bg-primary-200/40 blur-[100px]'
/>
<motion.div
animate={{
scale: [1, 1.3, 1],
x: [0, 100, 0],
opacity: [0.3, 0.6, 0.3]
}}
transition={{ duration: 18, repeat: Infinity, ease: "easeInOut", delay: 2 }}
<div
className='absolute top-[20%] right-[-10%] w-[400px] h-[400px] rounded-full bg-secondary-200/40 blur-[90px]'
/>
<motion.div
animate={{
scale: [1, 1.1, 1],
y: [0, -50, 0],
opacity: [0.2, 0.4, 0.2]
}}
transition={{ duration: 12, repeat: Infinity, ease: "easeInOut", delay: 5 }}
<div
className='absolute bottom-[-10%] left-[20%] w-[600px] h-[600px] rounded-full bg-pink-200/30 blur-[110px]'
/>
</div>

View File

@ -293,7 +293,11 @@ const UpdateDialogContent: React.FC<{
const NewVersionTip = (props: NewVersionTipProps) => {
const { currentVersion } = props;
const dialog = useDialog();
const { data: latestVersion, error } = useRequest(WebUIManager.getLatestTag);
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');
if (error || !latestVersion || !currentVersion || latestVersion === currentVersion) {
@ -362,9 +366,7 @@ const NewVersionTip = (props: NewVersionTipProps) => {
<Button
isIconOnly
radius='full'
color='primary'
variant='shadow'
className='!w-5 !h-5 !min-w-0 text-small shadow-md'
className='!w-5 !h-5 !min-w-0 text-[10px] shadow-lg shadow-pink-500/40 bg-gradient-to-tr from-[#D33FF0] to-[#FF709F] text-white'
isLoading={updateStatus === 'updating'}
onPress={showUpdateDialog}
>
@ -383,7 +385,11 @@ const NapCatVersion: React.FC<NapCatVersionProps> = ({ hasBackground = false })
data: packageData,
loading: packageLoading,
error: packageError,
} = useRequest(WebUIManager.GetNapCatVersion);
} = useRequest(WebUIManager.GetNapCatVersion, {
cacheKey: 'napcat-version',
staleTime: 60 * 60 * 1000,
cacheTime: 24 * 60 * 60 * 1000,
});
const currentVersion = packageData?.version;
@ -419,7 +425,11 @@ const SystemInfo: React.FC<SystemInfoProps> = (props) => {
data: qqVersionData,
loading: qqVersionLoading,
error: qqVersionError,
} = useRequest(WebUIManager.getQQVersion);
} = useRequest(WebUIManager.getQQVersion, {
cacheKey: 'qq-version',
staleTime: 60 * 60 * 1000,
cacheTime: 24 * 60 * 60 * 1000,
});
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
const hasBackground = !!backgroundImage;

View File

@ -1,143 +1,109 @@
import * as echarts from 'echarts';
import React, { useEffect, useRef } from 'react';
import React, { useMemo } from 'react';
import { useTheme } from '@/hooks/use-theme';
interface UsagePieProps {
systemUsage: number
processUsage: number
title?: string
systemUsage: number;
processUsage: number;
title?: string;
}
const defaultOption: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: '<center>{b}<br/><b>{d}%</b></center>',
borderRadius: 10,
extraCssText: 'backdrop-filter: blur(10px);',
},
series: [
{
name: '系统占用',
type: 'pie',
radius: ['70%', '90%'],
avoidLabelOverlap: false,
label: {
show: true,
position: 'center',
formatter: '系统占用',
fontSize: 14,
},
itemStyle: {
borderWidth: 1,
borderRadius: 10,
},
labelLine: {
show: false,
},
data: [
{
value: 100,
name: '系统总量',
},
],
},
],
};
const UsagePie: React.FC<UsagePieProps> = ({
systemUsage,
processUsage,
title,
}) => {
const chartRef = useRef<HTMLDivElement>(null);
const chartInstance = useRef<echarts.ECharts | null>(null);
const { theme } = useTheme();
useEffect(() => {
if (chartRef.current) {
chartInstance.current = echarts.init(chartRef.current);
const option = defaultOption;
chartInstance.current.setOption(option);
const observer = new ResizeObserver(() => {
chartInstance.current?.resize();
});
observer.observe(chartRef.current);
return () => {
chartInstance.current?.dispose();
observer.disconnect();
};
}
}, []);
// Ensure values are clean
const cleanSystem = Math.min(Math.max(systemUsage || 0, 0), 100);
const cleanProcess = Math.min(Math.max(processUsage || 0, 0), cleanSystem);
useEffect(() => {
if (chartInstance.current) {
chartInstance.current.setOption({
series: [
{
label: {
formatter: title,
},
},
],
});
}
}, [title]);
// SVG Config
const size = 100;
const strokeWidth = 10;
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const center = size / 2;
useEffect(() => {
if (chartInstance.current) {
chartInstance.current.setOption({
darkMode: theme === 'dark',
tooltip: {
backgroundColor:
theme === 'dark'
? 'rgba(0, 0, 0, 0.8)'
: 'rgba(255, 255, 255, 0.8)',
textStyle: {
color: theme === 'dark' ? '#fff' : '#333',
},
},
color:
theme === 'dark'
? ['#D33FF0', '#EF8664', '#E25180']
: ['#D33FF0', '#EA7D9B', '#FFC107'],
series: [
{
itemStyle: {
borderColor: theme === 'dark' ? '#333' : '#F0A9A7',
},
},
],
});
}
}, [theme]);
// Colors
const colors = {
qq: '#D33FF0',
other: theme === 'dark' ? '#EF8664' : '#EA7D9B',
track: theme === 'dark' ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)',
};
useEffect(() => {
if (chartInstance.current) {
chartInstance.current.setOption({
series: [
{
data: [
{
value: processUsage,
name: 'QQ占用',
},
{
value: systemUsage - processUsage,
name: '其他进程占用',
},
{
value: 100 - systemUsage,
name: '剩余系统总量',
},
],
},
],
});
}
}, [systemUsage, processUsage]);
// Dash Arrays
// 1. Total System Usage (QQ + Others)
const systemDash = useMemo(() => {
return `${(cleanSystem / 100) * circumference} ${circumference}`;
}, [cleanSystem, circumference]);
return <div ref={chartRef} className='w-36 h-36 flex-shrink-0' />;
// 2. QQ Usage (Subset of System)
const processDash = useMemo(() => {
return `${(cleanProcess / 100) * circumference} ${circumference}`;
}, [cleanProcess, circumference]);
return (
<div className="relative w-36 h-36 flex items-center justify-center">
<svg
className="w-full h-full -rotate-90"
viewBox={`0 0 ${size} ${size}`}
>
{/* Track / Free Space */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke={colors.track}
strokeWidth={strokeWidth}
strokeLinecap="round"
/>
{/* System Usage (Background for QQ) - effectively "Others" + "QQ" */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke={colors.other}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={systemDash}
className="transition-all duration-700 ease-out"
/>
{/* QQ Usage - Layered on top */}
<circle
cx={center}
cy={center}
r={radius}
fill="none"
stroke={colors.qq}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={processDash}
className="transition-all duration-700 ease-out"
/>
</svg>
{/* Center Content */}
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none select-none">
{title && (
<span className="text-[10px] text-default-500 font-medium mb-0.5 opacity-80 uppercase tracking-widest scale-90">
{title}
</span>
)}
<div className="flex items-baseline gap-0.5">
<span className="text-2xl font-bold font-mono tracking-tight text-default-900 dark:text-gray-100">
{Math.round(cleanSystem)}
</span>
<span className="text-xs text-default-400 font-bold">%</span>
</div>
</div>
</div>
);
};
export default UsagePie;

View File

@ -2,7 +2,7 @@ import { Card, CardBody } from '@heroui/card';
import { useLocalStorage } from '@uidotdev/usehooks';
import { useRequest } from 'ahooks';
import clsx from 'clsx';
import { useCallback, useEffect, useState, useRef } from 'react';
import { useCallback, useEffect, useRef } from 'react';
import key from '@/const/key';
import toast from 'react-hot-toast';
@ -65,7 +65,7 @@ export interface SystemStatusCardProps {
setArchInfo: (arch: string | undefined) => void;
}
const SystemStatusCard: React.FC<SystemStatusCardProps> = ({ setArchInfo }) => {
const [systemStatus, setSystemStatus] = useState<SystemStatus>();
const [systemStatus, setSystemStatus] = useLocalStorage<SystemStatus | undefined>('napcat_system_status_cache', undefined);
const isSetted = useRef(false);
const getStatus = useCallback(() => {
try {
@ -94,7 +94,7 @@ const SystemStatusCard: React.FC<SystemStatusCardProps> = ({ setArchInfo }) => {
};
const DashboardIndexPage: React.FC = () => {
const [archInfo, setArchInfo] = useState<string>();
const [archInfo, setArchInfo] = useLocalStorage<string | undefined>('napcat_arch_info_cache', undefined);
// @ts-ignore
const [backgroundImage] = useLocalStorage<string>(key.backgroundImage, '');
const hasBackground = !!backgroundImage;