summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorLarvan2 <[email protected]>2026-04-18 14:07:20 +0800
committerLarvan2 <[email protected]>2026-04-18 14:07:20 +0800
commit56758999537ca0790837f446984379eebca3a44d (patch)
treec1151d88422b0492dedd19eb39f56f6360b840c3 /src/components
parentedf6c5cb65cbf103fb1848760595f5c381dac723 (diff)
chore: modify chart
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Collapsible.tsx23
-rw-r--r--src/components/Sparkline.tsx24
-rw-r--r--src/components/TrafficNow.module.scss32
-rw-r--r--src/components/TrafficNow.tsx82
-rw-r--r--src/components/proxies/ProxyProvider.tsx28
-rw-r--r--src/components/shared/FeatherIcons.ts91
-rw-r--r--src/components/shared/ThemeSwitcher.tsx150
7 files changed, 226 insertions, 204 deletions
diff --git a/src/components/Collapsible.tsx b/src/components/Collapsible.tsx
index f43dbd6..6948cef 100644
--- a/src/components/Collapsible.tsx
+++ b/src/components/Collapsible.tsx
@@ -1,7 +1,6 @@
+import { LazyMotion, domAnimation, m } from 'framer-motion';
import React from 'react';
-import { framerMotionResouce } from '../misc/motion';
-
const { memo } = React;
const variantsCollpapsibleWrap = {
@@ -27,17 +26,17 @@ const variantsCollpapsibleWrap = {
};
const Collapsible = memo(({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) => {
- const module = framerMotionResouce.read();
- const motion = module.motion;
return (
- <motion.div
- initial={isOpen ? 'initialOpen' : 'closed'}
- animate={isOpen ? 'open' : 'closed'}
- variants={variantsCollpapsibleWrap}
- style={{ overflow: 'hidden' }}
- >
- {children}
- </motion.div>
+ <LazyMotion features={domAnimation}>
+ <m.div
+ initial={isOpen ? 'initialOpen' : 'closed'}
+ animate={isOpen ? 'open' : 'closed'}
+ variants={variantsCollpapsibleWrap}
+ style={{ overflow: 'hidden' }}
+ >
+ {children}
+ </m.div>
+ </LazyMotion>
);
});
diff --git a/src/components/Sparkline.tsx b/src/components/Sparkline.tsx
index b7f35a9..79f8bf5 100644
--- a/src/components/Sparkline.tsx
+++ b/src/components/Sparkline.tsx
@@ -63,9 +63,19 @@ const extraChartOptions: any = {
export default function Sparkline({ data: dataArray, labels, type, styleIndex = 0 }) {
chartJSResource.read();
+ const isMemory = type === 'inuse';
+
const options = useMemo(() => {
return {
...extraChartOptions,
+ scales: {
+ ...extraChartOptions.scales,
+ y: {
+ display: false,
+ // 内存值稳定,不从零开始,让 Y 轴自动适应数据范围以显示波动
+ beginAtZero: !isMemory,
+ },
+ },
plugins: {
...extraChartOptions.plugins,
tooltip: {
@@ -75,9 +85,9 @@ export default function Sparkline({ data: dataArray, labels, type, styleIndex =
title: () => '',
label(context) {
if (context.parsed.y !== null) {
- const suffix = type === 'inuse' ? '' : '/s';
- // 还原 log1p 变换后的真实值
- return prettyBytes(Math.expm1(context.parsed.y)) + suffix;
+ const suffix = isMemory ? '' : '/s';
+ const raw = isMemory ? context.parsed.y : Math.expm1(context.parsed.y);
+ return prettyBytes(raw) + suffix;
}
return '';
},
@@ -85,7 +95,7 @@ export default function Sparkline({ data: dataArray, labels, type, styleIndex =
},
},
};
- }, [type]);
+ }, [type, isMemory]);
const data = useMemo(
() => ({
@@ -93,13 +103,13 @@ export default function Sparkline({ data: dataArray, labels, type, styleIndex =
{
...commonDataSetProps,
...chartStyles[styleIndex][type],
- // log1p 变换:压缩大尖刺,让小流量也可见;log1p(0)=0 不会出现 -Infinity
- data: dataArray.map((v, i) => ({ x: labels[i], y: Math.log1p(v) })),
+ // 内存用原始值(变化幅度小,不需要压缩);流量用 log1p 压缩尖刺
+ data: dataArray.map((v, i) => ({ x: labels[i], y: isMemory ? v : Math.log1p(v) })),
fill: true,
},
],
}),
- [dataArray, labels, type, styleIndex]
+ [dataArray, labels, type, styleIndex, isMemory],
);
return (
diff --git a/src/components/TrafficNow.module.scss b/src/components/TrafficNow.module.scss
index 85281e2..2b0bcdf 100644
--- a/src/components/TrafficNow.module.scss
+++ b/src/components/TrafficNow.module.scss
@@ -1,11 +1,12 @@
.TrafficNow {
color: var(--color-text);
display: flex;
- flex-direction: column; grid-gap: 20px;
+ flex-direction: column;
+ grid-gap: 20px;
gap: 20px;
padding: 10px 0;
-.overview {
+ .overview {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
@@ -23,19 +24,40 @@
}
}
+ .chartsRow {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+
+ @media (min-width: 768px) {
+ flex-direction: row;
+
+ & > .sec {
+ flex: 1;
+ min-width: 0;
+ }
+ }
+ }
+
.sec {
padding: 20px;
background-color: var(--color-bg-card);
border-radius: 12px;
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
- transition: transform 0.2s ease, box-shadow 0.2s ease;
+ box-shadow:
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 140px;
&:hover {
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ box-shadow:
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.header {
diff --git a/src/components/TrafficNow.tsx b/src/components/TrafficNow.tsx
index e1b0f6d..5dc601b 100644
--- a/src/components/TrafficNow.tsx
+++ b/src/components/TrafficNow.tsx
@@ -1,5 +1,12 @@
import * as React from 'react';
-import { Download, ArrowDown, ArrowUp, Cpu, Link as LinkIcon, Upload } from '~/components/shared/FeatherIcons';
+import {
+ Download,
+ ArrowDown,
+ ArrowUp,
+ Cpu,
+ Link as LinkIcon,
+ Upload,
+} from '~/components/shared/FeatherIcons';
import { useTranslation } from 'react-i18next';
import useMemory from '../hooks/useMemory';
@@ -51,45 +58,46 @@ export default function TrafficNow({ apiConfig, selectedChartStyleIndex }: Props
</div>
</div>
- <div className={s0.sec}>
- <div className={s0.header}>
- <ArrowDown size={16} />
- <span>{t('Download')}</span>
+ <div className={s0.chartsRow}>
+ <div className={s0.sec}>
+ <div className={s0.header}>
+ <ArrowDown size={16} />
+ <span>{t('Download')}</span>
+ </div>
+ <div className={s0.value}>{downStr}</div>
+ <Sparkline
+ data={traffic.down}
+ labels={traffic.labels}
+ type="down"
+ styleIndex={selectedChartStyleIndex}
+ />
</div>
- <div className={s0.value}>{downStr}</div>
- <Sparkline
- data={traffic.down}
- labels={traffic.labels}
- type="down"
- styleIndex={selectedChartStyleIndex}
- />
- </div>
- <div className={s0.sec}>
- <div className={s0.header}>
- <ArrowUp size={16} />
- <span>{t('Upload')}</span>
+ <div className={s0.sec}>
+ <div className={s0.header}>
+ <ArrowUp size={16} />
+ <span>{t('Upload')}</span>
+ </div>
+ <div className={s0.value}>{upStr}</div>
+ <Sparkline
+ data={traffic.up}
+ labels={traffic.labels}
+ type="up"
+ styleIndex={selectedChartStyleIndex}
+ />
</div>
- <div className={s0.value}>{upStr}</div>
- <Sparkline
- data={traffic.up}
- labels={traffic.labels}
- type="up"
- styleIndex={selectedChartStyleIndex}
- />
- </div>
-
- <div className={s0.sec}>
- <div className={s0.header}>
- <Cpu size={16} />
- <span>{t('Memory Usage')}</span>
+ <div className={s0.sec}>
+ <div className={s0.header}>
+ <Cpu size={16} />
+ <span>{t('Memory Usage')}</span>
+ </div>
+ <div className={s0.value}>{mUsage}</div>
+ <Sparkline
+ data={memory.inuse}
+ labels={memory.labels}
+ type="inuse"
+ styleIndex={selectedChartStyleIndex}
+ />
</div>
- <div className={s0.value}>{mUsage}</div>
- <Sparkline
- data={memory.inuse}
- labels={memory.labels}
- type="inuse"
- styleIndex={selectedChartStyleIndex}
- />{' '}
</div>
</div>
);
diff --git a/src/components/proxies/ProxyProvider.tsx b/src/components/proxies/ProxyProvider.tsx
index bd2b4fa..47ccb8b 100644
--- a/src/components/proxies/ProxyProvider.tsx
+++ b/src/components/proxies/ProxyProvider.tsx
@@ -8,7 +8,7 @@ import Collapsible from '~/components/Collapsible';
import CollapsibleSectionHeader from '~/components/CollapsibleSectionHeader';
import s0 from '~/components/proxies/ProxyGroup.module.scss';
import { useStoreActions } from '~/components/StateProvider';
-import { framerMotionResouce } from '~/misc/motion';
+import { LazyMotion, domAnimation, m } from 'framer-motion';
import { useFilteredAndSorted, useUpdateProviderItem } from '~/modules/proxies/hooks';
import { healthcheckProviderByName } from '~/store/proxies';
import { DelayMapping, DispatchFn, ProxiesMapping, SubscriptionInfo } from '~/store/types';
@@ -190,19 +190,19 @@ function formatBytes(bytes, decimals = 2) {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}
function Refresh() {
- const module = framerMotionResouce.read();
- const motion = module.motion;
return (
- <motion.div
- className={s.refresh}
- variants={button}
- initial="rest"
- whileHover="hover"
- whileTap="pressed"
- >
- <motion.div className="flexCenter" variants={arrow}>
- <RotateCw size={16} />
- </motion.div>
- </motion.div>
+ <LazyMotion features={domAnimation}>
+ <m.div
+ className={s.refresh}
+ variants={button}
+ initial="rest"
+ whileHover="hover"
+ whileTap="pressed"
+ >
+ <m.div className="flexCenter" variants={arrow}>
+ <RotateCw size={16} />
+ </m.div>
+ </m.div>
+ </LazyMotion>
);
}
diff --git a/src/components/shared/FeatherIcons.ts b/src/components/shared/FeatherIcons.ts
index 745baa1..ee1f410 100644
--- a/src/components/shared/FeatherIcons.ts
+++ b/src/components/shared/FeatherIcons.ts
@@ -1,53 +1,38 @@
-import * as React from 'react';
-import * as Feather from 'react-feather';
-
-type FeatherCompatProps = {
- color?: string;
- size?: string | number;
- className?: string;
- style?: React.CSSProperties;
- [key: string]: unknown;
-};
-
-type IconComponent = React.ComponentType<FeatherCompatProps>;
-
-function asIcon(name: keyof typeof Feather): IconComponent {
- return Feather[name] as unknown as IconComponent;
-}
-
-export const Activity = asIcon('Activity');
-export const ArrowDown = asIcon('ArrowDown');
-export const ArrowDownCircle = asIcon('ArrowDownCircle');
-export const ArrowUp = asIcon('ArrowUp');
-export const ChevronDown = asIcon('ChevronDown');
-export const ChevronUp = asIcon('ChevronUp');
-export const Cpu = asIcon('Cpu');
-export const Database = asIcon('Database');
-export const Download = asIcon('Download');
-export const DownloadCloud = asIcon('DownloadCloud');
-export const Eye = asIcon('Eye');
-export const EyeOff = asIcon('EyeOff');
-export const FileText = asIcon('FileText');
-export const GitHub = asIcon('GitHub');
-export const Globe = asIcon('Globe');
-export const Hash = asIcon('Hash');
-export const Info = asIcon('Info');
-export const Link = asIcon('Link');
-export const LogOut = asIcon('LogOut');
-export const Menu = asIcon('Menu');
-export const Monitor = asIcon('Monitor');
-export const Pause = asIcon('Pause');
-export const Play = asIcon('Play');
-export const RefreshCcw = asIcon('RefreshCcw');
-export const RefreshCw = asIcon('RefreshCw');
-export const RotateCw = asIcon('RotateCw');
-export const Settings = asIcon('Settings');
-export const Shield = asIcon('Shield');
-export const Sliders = asIcon('Sliders');
-export const Tag = asIcon('Tag');
-export const Tool = asIcon('Tool');
-export const Trash2 = asIcon('Trash2');
-export const Upload = asIcon('Upload');
-export const X = asIcon('X');
-export const XCircle = asIcon('XCircle');
-export const Zap = asIcon('Zap');
+export {
+ Activity,
+ ArrowDown,
+ ArrowDownCircle,
+ ArrowUp,
+ ChevronDown,
+ ChevronUp,
+ Cpu,
+ Database,
+ Download,
+ DownloadCloud,
+ Eye,
+ EyeOff,
+ FileText,
+ GitHub,
+ Globe,
+ Hash,
+ Info,
+ Link,
+ LogOut,
+ Menu,
+ Monitor,
+ Pause,
+ Play,
+ RefreshCcw,
+ RefreshCw,
+ RotateCw,
+ Settings,
+ Shield,
+ Sliders,
+ Tag,
+ Tool,
+ Trash2,
+ Upload,
+ X,
+ XCircle,
+ Zap,
+} from 'react-feather';
diff --git a/src/components/shared/ThemeSwitcher.tsx b/src/components/shared/ThemeSwitcher.tsx
index 363d422..dfe248d 100644
--- a/src/components/shared/ThemeSwitcher.tsx
+++ b/src/components/shared/ThemeSwitcher.tsx
@@ -3,7 +3,7 @@ import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from '~/components/StateProvider';
-import { framerMotionResouce } from '~/misc/motion';
+import { LazyMotion, domAnimation, m } from 'framer-motion';
import { getTheme, switchTheme } from '~/store/app';
import { State } from '~/store/types';
@@ -28,7 +28,7 @@ export function ThemeSwitcherImpl({ theme, dispatch }) {
const onChange = React.useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => dispatch(switchTheme(e.target.value)),
- [dispatch]
+ [dispatch],
);
return (
@@ -46,91 +46,89 @@ export function ThemeSwitcherImpl({ theme, dispatch }) {
}
function MoonA() {
- const module = framerMotionResouce.read();
- const motion = module.motion;
return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="20"
- height="20"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <motion.path
- d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
- initial={{ rotate: -30 }}
- animate={{ rotate: 0 }}
- transition={{ duration: 0.7 }}
- />
- </svg>
+ <LazyMotion features={domAnimation}>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <m.path
+ d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
+ initial={{ rotate: -30 }}
+ animate={{ rotate: 0 }}
+ transition={{ duration: 0.7 }}
+ />
+ </svg>
+ </LazyMotion>
);
}
function Sun() {
- const module = framerMotionResouce.read();
- const motion = module.motion;
-
return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="20"
- height="20"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <circle cx="12" cy="12" r="5"></circle>
- <motion.g initial={{ scale: 0.7 }} animate={{ scale: 1 }} transition={{ duration: 0.5 }}>
- <line x1="12" y1="1" x2="12" y2="3"></line>
- <line x1="12" y1="21" x2="12" y2="23"></line>
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
- <line x1="1" y1="12" x2="3" y2="12"></line>
- <line x1="21" y1="12" x2="23" y2="12"></line>
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
- </motion.g>
- </svg>
+ <LazyMotion features={domAnimation}>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <circle cx="12" cy="12" r="5"></circle>
+ <m.g initial={{ scale: 0.7 }} animate={{ scale: 1 }} transition={{ duration: 0.5 }}>
+ <line x1="12" y1="1" x2="12" y2="3"></line>
+ <line x1="12" y1="21" x2="12" y2="23"></line>
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
+ <line x1="1" y1="12" x2="3" y2="12"></line>
+ <line x1="21" y1="12" x2="23" y2="12"></line>
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
+ </m.g>
+ </svg>
+ </LazyMotion>
);
}
function Auto() {
- const module = framerMotionResouce.read();
- const motion = module.motion;
-
return (
- <svg
- xmlns="http://www.w3.org/2000/svg"
- width="20"
- height="20"
- viewBox="0 0 24 24"
- fill="none"
- stroke="currentColor"
- strokeWidth="2"
- strokeLinecap="round"
- strokeLinejoin="round"
- >
- <circle cx="12" cy="12" r="11" />
- <clipPath id="cut-off-bottom">
- <motion.rect
- x="12"
- y="0"
- width="12"
- height="24"
- initial={{ rotate: -30 }}
- animate={{ rotate: 0 }}
- transition={{ duration: 0.7 }}
- />
- </clipPath>
- <circle cx="12" cy="12" r="6" clipPath="url(#cut-off-bottom)" fill="currentColor" />
- </svg>
+ <LazyMotion features={domAnimation}>
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width="20"
+ height="20"
+ viewBox="0 0 24 24"
+ fill="none"
+ stroke="currentColor"
+ strokeWidth="2"
+ strokeLinecap="round"
+ strokeLinejoin="round"
+ >
+ <circle cx="12" cy="12" r="11" />
+ <clipPath id="cut-off-bottom">
+ <m.rect
+ x="12"
+ y="0"
+ width="12"
+ height="24"
+ initial={{ rotate: -30 }}
+ animate={{ rotate: 0 }}
+ transition={{ duration: 0.7 }}
+ />
+ </clipPath>
+ <circle cx="12" cy="12" r="6" clipPath="url(#cut-off-bottom)" fill="currentColor" />
+ </svg>
+ </LazyMotion>
);
}