mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2025-12-25 01:30:07 +08:00
Refactor dashboard and components, remove echarts
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:
parent
e56b912bbd
commit
fa3a229827
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -19,7 +19,6 @@ export default function Hitokoto () {
|
||||
loading,
|
||||
run,
|
||||
} = useRequest(() => request.get<IHitokoto>('https://hitokoto.152710.xyz/'), {
|
||||
pollingInterval: 10000,
|
||||
throttleWait: 1000,
|
||||
});
|
||||
const backupData = {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user