feat: add rustfmt configuration and CI workflow for code formatting
refactor: streamline formatting workflow by removing unused taplo steps and clarifying directory change refactor: remove unnecessary directory change step in formatting workflow
This commit is contained in:
@@ -25,7 +25,7 @@ export const ClashInfoCard = () => {
|
||||
// 使用备忘录组件内容,减少重新渲染
|
||||
const cardContent = useMemo(() => {
|
||||
if (!clashConfig) return null;
|
||||
|
||||
|
||||
return (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
|
||||
@@ -24,11 +24,14 @@ export const ClashModeCard = () => {
|
||||
const currentMode = clashConfig?.mode?.toLowerCase();
|
||||
|
||||
// 模式图标映射
|
||||
const modeIcons = useMemo(() => ({
|
||||
rule: <MultipleStopRounded fontSize="small" />,
|
||||
global: <LanguageRounded fontSize="small" />,
|
||||
direct: <DirectionsRounded fontSize="small" />
|
||||
}), []);
|
||||
const modeIcons = useMemo(
|
||||
() => ({
|
||||
rule: <MultipleStopRounded fontSize="small" />,
|
||||
global: <LanguageRounded fontSize="small" />,
|
||||
direct: <DirectionsRounded fontSize="small" />,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// 切换模式的处理函数
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
@@ -68,18 +71,19 @@ export const ClashModeCard = () => {
|
||||
"&:active": {
|
||||
transform: "translateY(1px)",
|
||||
},
|
||||
"&::after": mode === currentMode
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -16,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 16,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
"&::after":
|
||||
mode === currentMode
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -16,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 16,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
});
|
||||
|
||||
// 描述样式
|
||||
@@ -143,12 +147,10 @@ export const ClashModeCard = () => {
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={descriptionStyles}
|
||||
>
|
||||
{t(`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`)}
|
||||
<Typography variant="caption" component="div" sx={descriptionStyles}>
|
||||
{t(
|
||||
`${currentMode?.charAt(0).toUpperCase()}${currentMode?.slice(1)} Mode Description`,
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -105,7 +105,7 @@ export const CurrentProxyCard = () => {
|
||||
// 添加排序类型状态
|
||||
const [sortType, setSortType] = useState<ProxySortType>(() => {
|
||||
const savedSortType = localStorage.getItem(STORAGE_KEY_SORT_TYPE);
|
||||
return savedSortType ? Number(savedSortType) as ProxySortType : 0;
|
||||
return savedSortType ? (Number(savedSortType) as ProxySortType) : 0;
|
||||
});
|
||||
|
||||
// 定义状态类型
|
||||
@@ -156,7 +156,8 @@ export const CurrentProxyCard = () => {
|
||||
primaryKeywords.some((keyword) =>
|
||||
group.name.toLowerCase().includes(keyword.toLowerCase()),
|
||||
),
|
||||
) || proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
|
||||
) ||
|
||||
proxies.groups.filter((g: { name: string }) => g.name !== "GLOBAL")[0];
|
||||
|
||||
return primaryGroup?.name || "";
|
||||
};
|
||||
@@ -200,11 +201,13 @@ export const CurrentProxyCard = () => {
|
||||
// 只保留 Selector 类型的组用于选择
|
||||
const filteredGroups = proxies.groups
|
||||
.filter((g: { name: string; type?: string }) => g.type === "Selector")
|
||||
.map((g: { name: string; now: string; all: Array<{ name: string }> }) => ({
|
||||
name: g.name,
|
||||
now: g.now || "",
|
||||
all: g.all.map((p: { name: string }) => p.name),
|
||||
}));
|
||||
.map(
|
||||
(g: { name: string; now: string; all: Array<{ name: string }> }) => ({
|
||||
name: g.name,
|
||||
now: g.now || "",
|
||||
all: g.all.map((p: { name: string }) => p.name),
|
||||
}),
|
||||
);
|
||||
|
||||
let newProxy = "";
|
||||
let newDisplayProxy = null;
|
||||
@@ -230,12 +233,12 @@ export const CurrentProxyCard = () => {
|
||||
if (selectorGroup) {
|
||||
newGroup = selectorGroup.name;
|
||||
newProxy = selectorGroup.now || selectorGroup.all[0] || "";
|
||||
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||
newDisplayProxy = proxies.records?.[newProxy] || null;
|
||||
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
if (newProxy) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
if (newProxy) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,7 +283,9 @@ export const CurrentProxyCard = () => {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
|
||||
setState((prev) => {
|
||||
const group = prev.proxyData.groups.find((g: { name: string }) => g.name === newGroup);
|
||||
const group = prev.proxyData.groups.find(
|
||||
(g: { name: string }) => g.name === newGroup,
|
||||
);
|
||||
if (group) {
|
||||
return {
|
||||
...prev,
|
||||
@@ -368,14 +373,16 @@ export const CurrentProxyCard = () => {
|
||||
}, [state.displayProxy]);
|
||||
|
||||
// 获取当前节点的延迟(增加非空校验)
|
||||
const currentDelay = currentProxy && state.selection.group
|
||||
? delayManager.getDelayFix(currentProxy, state.selection.group)
|
||||
: -1;
|
||||
const currentDelay =
|
||||
currentProxy && state.selection.group
|
||||
? delayManager.getDelayFix(currentProxy, state.selection.group)
|
||||
: -1;
|
||||
|
||||
// 信号图标(增加非空校验)
|
||||
const signalInfo = currentProxy && state.selection.group
|
||||
? getSignalIcon(currentDelay)
|
||||
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
|
||||
const signalInfo =
|
||||
currentProxy && state.selection.group
|
||||
? getSignalIcon(currentDelay)
|
||||
: { icon: <SignalNone />, text: "未初始化", color: "text.secondary" };
|
||||
|
||||
// 自定义渲染选择框中的值
|
||||
const renderProxyValue = useCallback(
|
||||
@@ -384,7 +391,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
state.proxyData.records[selected],
|
||||
state.selection.group
|
||||
state.selection.group,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -441,7 +448,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
return list;
|
||||
},
|
||||
[sortType, state.proxyData.records, state.selection.group]
|
||||
[sortType, state.proxyData.records, state.selection.group],
|
||||
);
|
||||
|
||||
// 计算要显示的代理选项(增加非空校验)
|
||||
@@ -452,11 +459,11 @@ export const CurrentProxyCard = () => {
|
||||
if (isGlobalMode && proxies?.global) {
|
||||
const options = proxies.global.all
|
||||
.filter((p: any) => {
|
||||
const name = typeof p === 'string' ? p : p.name;
|
||||
const name = typeof p === "string" ? p : p.name;
|
||||
return name !== "DIRECT" && name !== "REJECT";
|
||||
})
|
||||
.map((p: any) => ({
|
||||
name: typeof p === 'string' ? p : p.name
|
||||
name: typeof p === "string" ? p : p.name,
|
||||
}));
|
||||
|
||||
return sortProxies(options);
|
||||
@@ -464,7 +471,7 @@ export const CurrentProxyCard = () => {
|
||||
|
||||
// 规则模式
|
||||
const group = state.selection.group
|
||||
? state.proxyData.groups.find(g => g.name === state.selection.group)
|
||||
? state.proxyData.groups.find((g) => g.name === state.selection.group)
|
||||
: null;
|
||||
|
||||
if (group) {
|
||||
@@ -473,7 +480,14 @@ export const CurrentProxyCard = () => {
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [isDirectMode, isGlobalMode, proxies, state.proxyData, state.selection.group, sortProxies]);
|
||||
}, [
|
||||
isDirectMode,
|
||||
isGlobalMode,
|
||||
proxies,
|
||||
state.proxyData,
|
||||
state.selection.group,
|
||||
sortProxies,
|
||||
]);
|
||||
|
||||
// 获取排序图标
|
||||
const getSortIcon = () => {
|
||||
@@ -660,12 +674,14 @@ export const CurrentProxyCard = () => {
|
||||
{isDirectMode
|
||||
? null
|
||||
: proxyOptions.map((proxy, index) => {
|
||||
const delayValue = state.proxyData.records[proxy.name] && state.selection.group
|
||||
? delayManager.getDelayFix(
|
||||
state.proxyData.records[proxy.name],
|
||||
state.selection.group,
|
||||
)
|
||||
: -1;
|
||||
const delayValue =
|
||||
state.proxyData.records[proxy.name] &&
|
||||
state.selection.group
|
||||
? delayManager.getDelayFix(
|
||||
state.proxyData.records[proxy.name],
|
||||
state.selection.group,
|
||||
)
|
||||
: -1;
|
||||
return (
|
||||
<MenuItem
|
||||
key={`${proxy.name}-${index}`}
|
||||
@@ -706,4 +722,4 @@ export const CurrentProxyCard = () => {
|
||||
)}
|
||||
</EnhancedCard>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,7 +38,7 @@ export const EnhancedCard = ({
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
display: "block"
|
||||
display: "block",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -62,13 +62,15 @@ export const EnhancedCard = ({
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
overflow: "hidden"
|
||||
}}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
@@ -87,9 +89,9 @@ export const EnhancedCard = ({
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
{typeof title === "string" ? (
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight="medium"
|
||||
<Typography
|
||||
variant="h6"
|
||||
fontWeight="medium"
|
||||
fontSize={18}
|
||||
sx={titleTruncateStyle}
|
||||
title={title}
|
||||
@@ -97,9 +99,7 @@ export const EnhancedCard = ({
|
||||
{title}
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={titleTruncateStyle}>
|
||||
{title}
|
||||
</Box>
|
||||
<Box sx={titleTruncateStyle}>{title}</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -30,7 +30,7 @@ ChartJS.register(
|
||||
PointElement,
|
||||
LineElement,
|
||||
Tooltip,
|
||||
Filler
|
||||
Filler,
|
||||
);
|
||||
|
||||
// 流量数据项接口
|
||||
@@ -54,8 +54,8 @@ type DataPoint = ITrafficItem & { name: string; timestamp: number };
|
||||
/**
|
||||
* 增强型流量图表组件
|
||||
*/
|
||||
export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
(props, ref) => {
|
||||
export const EnhancedTrafficGraph = memo(
|
||||
forwardRef<EnhancedTrafficGraphRef>((props, ref) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -63,20 +63,20 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>(10);
|
||||
const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
|
||||
const [displayData, setDisplayData] = useState<DataPoint[]>([]);
|
||||
|
||||
|
||||
// 数据缓冲区
|
||||
const dataBufferRef = useRef<DataPoint[]>([]);
|
||||
|
||||
// 根据时间范围计算保留的数据点数量
|
||||
const getMaxPointsByTimeRange = useCallback(
|
||||
(minutes: TimeRange): number => minutes * 60,
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
// 最大数据点数量
|
||||
const MAX_BUFFER_SIZE = useMemo(
|
||||
() => getMaxPointsByTimeRange(10),
|
||||
[getMaxPointsByTimeRange]
|
||||
[getMaxPointsByTimeRange],
|
||||
);
|
||||
|
||||
// 颜色配置
|
||||
@@ -89,23 +89,28 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
text: theme.palette.text.primary,
|
||||
tooltipBorder: theme.palette.divider,
|
||||
}),
|
||||
[theme]
|
||||
[theme],
|
||||
);
|
||||
|
||||
// 切换时间范围
|
||||
const handleTimeRangeClick = useCallback((event: React.MouseEvent<SVGTextElement>) => {
|
||||
event.stopPropagation();
|
||||
setTimeRange((prevRange) => {
|
||||
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 点击图表主体或图例时切换样式
|
||||
const handleToggleStyleClick = useCallback((event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
|
||||
}, []);
|
||||
const handleTimeRangeClick = useCallback(
|
||||
(event: React.MouseEvent<SVGTextElement>) => {
|
||||
event.stopPropagation();
|
||||
setTimeRange((prevRange) => {
|
||||
return prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 点击图表主体或图例时切换样式
|
||||
const handleToggleStyleClick = useCallback(
|
||||
(event: React.MouseEvent<SVGTextElement | HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// 初始化数据缓冲区
|
||||
useEffect(() => {
|
||||
@@ -121,7 +126,9 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
let nameValue: string;
|
||||
try {
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn(`Initial data generation: Invalid date for timestamp ${pointTime}`);
|
||||
console.warn(
|
||||
`Initial data generation: Invalid date for timestamp ${pointTime}`,
|
||||
);
|
||||
nameValue = "??:??:??";
|
||||
} else {
|
||||
nameValue = date.toLocaleTimeString("en-US", {
|
||||
@@ -132,7 +139,14 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error in toLocaleTimeString during initial data gen:", e, "Date:", date, "Timestamp:", pointTime);
|
||||
console.error(
|
||||
"Error in toLocaleTimeString during initial data gen:",
|
||||
e,
|
||||
"Date:",
|
||||
date,
|
||||
"Timestamp:",
|
||||
pointTime,
|
||||
);
|
||||
nameValue = "Err:Time";
|
||||
}
|
||||
|
||||
@@ -142,55 +156,66 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
timestamp: pointTime,
|
||||
name: nameValue,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
dataBufferRef.current = initialBuffer;
|
||||
|
||||
|
||||
// 更新显示数据
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(initialBuffer.slice(-pointsToShow));
|
||||
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange]);
|
||||
// 添加数据点方法
|
||||
const appendData = useCallback((data: ITrafficItem) => {
|
||||
const safeData = {
|
||||
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
|
||||
down: typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
|
||||
};
|
||||
const appendData = useCallback(
|
||||
(data: ITrafficItem) => {
|
||||
const safeData = {
|
||||
up: typeof data.up === "number" && !isNaN(data.up) ? data.up : 0,
|
||||
down:
|
||||
typeof data.down === "number" && !isNaN(data.down) ? data.down : 0,
|
||||
};
|
||||
|
||||
const timestamp = data.timestamp || Date.now();
|
||||
const date = new Date(timestamp);
|
||||
const timestamp = data.timestamp || Date.now();
|
||||
const date = new Date(timestamp);
|
||||
|
||||
let nameValue: string;
|
||||
try {
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
|
||||
nameValue = "??:??:??";
|
||||
} else {
|
||||
nameValue = date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
let nameValue: string;
|
||||
try {
|
||||
if (isNaN(date.getTime())) {
|
||||
console.warn(`appendData: Invalid date for timestamp ${timestamp}`);
|
||||
nameValue = "??:??:??";
|
||||
} else {
|
||||
nameValue = date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Error in toLocaleTimeString in appendData:",
|
||||
e,
|
||||
"Date:",
|
||||
date,
|
||||
"Timestamp:",
|
||||
timestamp,
|
||||
);
|
||||
nameValue = "Err:Time";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error in toLocaleTimeString in appendData:", e, "Date:", date, "Timestamp:", timestamp);
|
||||
nameValue = "Err:Time";
|
||||
}
|
||||
// 带时间标签的新数据点
|
||||
const newPoint: DataPoint = {
|
||||
...safeData,
|
||||
name: nameValue,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
// 带时间标签的新数据点
|
||||
const newPoint: DataPoint = {
|
||||
...safeData,
|
||||
name: nameValue,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
|
||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||
dataBufferRef.current = newBuffer;
|
||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||
dataBufferRef.current = newBuffer;
|
||||
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(newBuffer.slice(-pointsToShow));
|
||||
}, [timeRange, getMaxPointsByTimeRange]);
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(newBuffer.slice(-pointsToShow));
|
||||
},
|
||||
[timeRange, getMaxPointsByTimeRange],
|
||||
);
|
||||
|
||||
// 监听时间范围变化
|
||||
useEffect(() => {
|
||||
@@ -202,7 +227,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
|
||||
// 切换图表样式
|
||||
const toggleStyle = useCallback(() => {
|
||||
setChartStyle((prev) => prev === "line" ? "area" : "line");
|
||||
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
|
||||
}, []);
|
||||
|
||||
// 暴露方法给父组件
|
||||
@@ -212,30 +237,31 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
appendData,
|
||||
toggleStyle,
|
||||
}),
|
||||
[appendData, toggleStyle]
|
||||
[appendData, toggleStyle],
|
||||
);
|
||||
|
||||
|
||||
const formatYAxis = useCallback((value: number | string): string => {
|
||||
if (typeof value !== 'number') return String(value);
|
||||
if (typeof value !== "number") return String(value);
|
||||
const [num, unit] = parseTraffic(value);
|
||||
return `${num}${unit}`;
|
||||
}, []);
|
||||
|
||||
const formatXLabel = useCallback((tickValue: string | number, index: number, ticks: any[]) => {
|
||||
const dataPoint = displayData[index as number];
|
||||
if (dataPoint && dataPoint.name) {
|
||||
const parts = dataPoint.name.split(":");
|
||||
return `${parts[0]}:${parts[1]}`;
|
||||
}
|
||||
if(typeof tickValue === 'string') {
|
||||
const parts = tickValue.split(":");
|
||||
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
|
||||
return tickValue;
|
||||
}
|
||||
return '';
|
||||
}, [displayData]);
|
||||
|
||||
const formatXLabel = useCallback(
|
||||
(tickValue: string | number, index: number, ticks: any[]) => {
|
||||
const dataPoint = displayData[index as number];
|
||||
if (dataPoint && dataPoint.name) {
|
||||
const parts = dataPoint.name.split(":");
|
||||
return `${parts[0]}:${parts[1]}`;
|
||||
}
|
||||
if (typeof tickValue === "string") {
|
||||
const parts = tickValue.split(":");
|
||||
if (parts.length >= 2) return `${parts[0]}:${parts[1]}`;
|
||||
return tickValue;
|
||||
}
|
||||
return "";
|
||||
},
|
||||
[displayData],
|
||||
);
|
||||
|
||||
// 获取当前时间范围文本
|
||||
const getTimeRangeText = useCallback(() => {
|
||||
@@ -243,13 +269,13 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
}, [timeRange, t]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
const labels = displayData.map(d => d.name);
|
||||
const labels = displayData.map((d) => d.name);
|
||||
return {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
label: t("Upload"),
|
||||
data: displayData.map(d => d.up),
|
||||
data: displayData.map((d) => d.up),
|
||||
borderColor: colors.up,
|
||||
backgroundColor: chartStyle === "area" ? colors.up : colors.up,
|
||||
fill: chartStyle === "area",
|
||||
@@ -260,7 +286,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
},
|
||||
{
|
||||
label: t("Download"),
|
||||
data: displayData.map(d => d.down),
|
||||
data: displayData.map((d) => d.down),
|
||||
borderColor: colors.down,
|
||||
backgroundColor: chartStyle === "area" ? colors.down : colors.down,
|
||||
fill: chartStyle === "area",
|
||||
@@ -268,113 +294,130 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 4,
|
||||
borderWidth: 2,
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [displayData, colors.up, colors.down, t, chartStyle]);
|
||||
|
||||
const chartOptions = useMemo(() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false as false,
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
type: 'category' as const,
|
||||
labels: displayData.map(d => d.name),
|
||||
ticks: {
|
||||
const chartOptions = useMemo(
|
||||
() => ({
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: false as false,
|
||||
scales: {
|
||||
x: {
|
||||
display: true,
|
||||
color: colors.text,
|
||||
font: { size: 10 },
|
||||
callback: function(this: Scale, tickValue: string | number, index: number, ticks: Tick[]): string | undefined {
|
||||
let labelToFormat: string | undefined = undefined;
|
||||
type: "category" as const,
|
||||
labels: displayData.map((d) => d.name),
|
||||
ticks: {
|
||||
display: true,
|
||||
color: colors.text,
|
||||
font: { size: 10 },
|
||||
callback: function (
|
||||
this: Scale,
|
||||
tickValue: string | number,
|
||||
index: number,
|
||||
ticks: Tick[],
|
||||
): string | undefined {
|
||||
let labelToFormat: string | undefined = undefined;
|
||||
|
||||
const currentDisplayTick = ticks[index];
|
||||
if (currentDisplayTick && typeof currentDisplayTick.label === 'string') {
|
||||
labelToFormat = currentDisplayTick.label;
|
||||
} else {
|
||||
const sourceLabels = displayData.map(d => d.name);
|
||||
if (typeof tickValue === 'number' && tickValue >= 0 && tickValue < sourceLabels.length) {
|
||||
labelToFormat = sourceLabels[tickValue];
|
||||
} else if (typeof tickValue === 'string') {
|
||||
labelToFormat = tickValue;
|
||||
const currentDisplayTick = ticks[index];
|
||||
if (
|
||||
currentDisplayTick &&
|
||||
typeof currentDisplayTick.label === "string"
|
||||
) {
|
||||
labelToFormat = currentDisplayTick.label;
|
||||
} else {
|
||||
const sourceLabels = displayData.map((d) => d.name);
|
||||
if (
|
||||
typeof tickValue === "number" &&
|
||||
tickValue >= 0 &&
|
||||
tickValue < sourceLabels.length
|
||||
) {
|
||||
labelToFormat = sourceLabels[tickValue];
|
||||
} else if (typeof tickValue === "string") {
|
||||
labelToFormat = tickValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof labelToFormat !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof labelToFormat !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const parts: string[] = labelToFormat.split(':');
|
||||
return parts.length >= 2 ? `${parts[0]}:${parts[1]}` : labelToFormat;
|
||||
const parts: string[] = labelToFormat.split(":");
|
||||
return parts.length >= 2
|
||||
? `${parts[0]}:${parts[1]}`
|
||||
: labelToFormat;
|
||||
},
|
||||
autoSkip: true,
|
||||
maxTicksLimit: Math.max(
|
||||
5,
|
||||
Math.floor(displayData.length / (timeRange * 2)),
|
||||
),
|
||||
minRotation: 0,
|
||||
maxRotation: 0,
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
drawOnChartArea: false,
|
||||
drawTicks: true,
|
||||
tickLength: 2,
|
||||
color: colors.text,
|
||||
},
|
||||
autoSkip: true,
|
||||
maxTicksLimit: Math.max(5, Math.floor(displayData.length / (timeRange * 2))),
|
||||
minRotation: 0,
|
||||
maxRotation: 0,
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
drawOnChartArea: false,
|
||||
drawTicks: true,
|
||||
tickLength: 2,
|
||||
color: colors.text,
|
||||
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: colors.text,
|
||||
font: { size: 10 },
|
||||
callback: formatYAxis,
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
drawTicks: true,
|
||||
tickLength: 3,
|
||||
color: colors.grid,
|
||||
},
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: colors.text,
|
||||
font: { size: 10 },
|
||||
callback: formatYAxis,
|
||||
},
|
||||
grid: {
|
||||
display: true,
|
||||
drawTicks: true,
|
||||
tickLength: 3,
|
||||
color: colors.grid,
|
||||
|
||||
},
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
mode: 'index' as const,
|
||||
intersect: false,
|
||||
backgroundColor: colors.tooltipBg,
|
||||
titleColor: colors.text,
|
||||
bodyColor: colors.text,
|
||||
borderColor: colors.tooltipBorder,
|
||||
borderWidth: 1,
|
||||
cornerRadius: 4,
|
||||
padding: 8,
|
||||
callbacks: {
|
||||
title: (tooltipItems: any[]) => {
|
||||
return `${t("Time")}: ${tooltipItems[0].label}`;
|
||||
plugins: {
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
mode: "index" as const,
|
||||
intersect: false,
|
||||
backgroundColor: colors.tooltipBg,
|
||||
titleColor: colors.text,
|
||||
bodyColor: colors.text,
|
||||
borderColor: colors.tooltipBorder,
|
||||
borderWidth: 1,
|
||||
cornerRadius: 4,
|
||||
padding: 8,
|
||||
callbacks: {
|
||||
title: (tooltipItems: any[]) => {
|
||||
return `${t("Time")}: ${tooltipItems[0].label}`;
|
||||
},
|
||||
label: (context: any): string => {
|
||||
const label = context.dataset.label || "";
|
||||
const value = context.parsed.y;
|
||||
const [num, unit] = parseTraffic(value);
|
||||
return `${label}: ${num} ${unit}/s`;
|
||||
},
|
||||
},
|
||||
label: (context: any): string => {
|
||||
const label = context.dataset.label || '';
|
||||
const value = context.parsed.y;
|
||||
const [num, unit] = parseTraffic(value);
|
||||
return `${label}: ${num} ${unit}/s`;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
right: 7,
|
||||
left: 3,
|
||||
}
|
||||
}
|
||||
}), [colors, t, formatYAxis, timeRange, displayData]);
|
||||
|
||||
layout: {
|
||||
padding: {
|
||||
top: 16,
|
||||
right: 7,
|
||||
left: 3,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[colors, t, formatYAxis, timeRange, displayData],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -392,8 +435,17 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
{displayData.length > 0 && (
|
||||
<ChartJsLine data={chartData} options={chartOptions} />
|
||||
)}
|
||||
|
||||
<svg width="100%" height="100%" style={{ position: 'absolute', top: 0, left: 0, pointerEvents: 'none' }}>
|
||||
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<text
|
||||
x="3.5%"
|
||||
y="10%"
|
||||
@@ -402,11 +454,11 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
onClick={handleTimeRangeClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
style={{ cursor: "pointer", pointerEvents: "all" }}
|
||||
>
|
||||
{getTimeRangeText()}
|
||||
</text>
|
||||
|
||||
|
||||
<text
|
||||
x="99%"
|
||||
y="10%"
|
||||
@@ -415,7 +467,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
onClick={handleToggleStyleClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
style={{ cursor: "pointer", pointerEvents: "all" }}
|
||||
>
|
||||
{t("Upload")}
|
||||
</text>
|
||||
@@ -428,7 +480,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
onClick={handleToggleStyleClick}
|
||||
style={{ cursor: "pointer", pointerEvents: 'all' }}
|
||||
style={{ cursor: "pointer", pointerEvents: "all" }}
|
||||
>
|
||||
{t("Download")}
|
||||
</text>
|
||||
@@ -436,7 +488,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
));
|
||||
}),
|
||||
);
|
||||
|
||||
EnhancedTrafficGraph.displayName = "EnhancedTrafficGraph";
|
||||
|
||||
@@ -66,85 +66,90 @@ const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
|
||||
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
|
||||
|
||||
// 统计卡片组件 - 使用memo优化
|
||||
const CompactStatCard = memo(({
|
||||
icon,
|
||||
title,
|
||||
value,
|
||||
unit,
|
||||
color,
|
||||
onClick,
|
||||
}: StatCardProps) => {
|
||||
const theme = useTheme();
|
||||
const CompactStatCard = memo(
|
||||
({ icon, title, value, unit, color, onClick }: StatCardProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// 获取调色板颜色 - 使用useMemo避免重复计算
|
||||
const colorValue = useMemo(() => {
|
||||
const palette = theme.palette;
|
||||
if (
|
||||
color in palette &&
|
||||
palette[color as keyof typeof palette] &&
|
||||
"main" in (palette[color as keyof typeof palette] as PaletteColor)
|
||||
) {
|
||||
return (palette[color as keyof typeof palette] as PaletteColor).main;
|
||||
}
|
||||
return palette.primary.main;
|
||||
}, [theme.palette, color]);
|
||||
// 获取调色板颜色 - 使用useMemo避免重复计算
|
||||
const colorValue = useMemo(() => {
|
||||
const palette = theme.palette;
|
||||
if (
|
||||
color in palette &&
|
||||
palette[color as keyof typeof palette] &&
|
||||
"main" in (palette[color as keyof typeof palette] as PaletteColor)
|
||||
) {
|
||||
return (palette[color as keyof typeof palette] as PaletteColor).main;
|
||||
}
|
||||
return palette.primary.main;
|
||||
}, [theme.palette, color]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: 2,
|
||||
bgcolor: alpha(colorValue, 0.05),
|
||||
border: `1px solid ${alpha(colorValue, 0.15)}`,
|
||||
padding: "8px",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
cursor: onClick ? "pointer" : "default",
|
||||
"&:hover": onClick ? {
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
border: `1px solid ${alpha(colorValue, 0.3)}`,
|
||||
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
|
||||
} : {},
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* 图标容器 */}
|
||||
<Grid
|
||||
component="div"
|
||||
return (
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
mr: 1,
|
||||
ml: "2px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "50%",
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
color: colorValue,
|
||||
borderRadius: 2,
|
||||
bgcolor: alpha(colorValue, 0.05),
|
||||
border: `1px solid ${alpha(colorValue, 0.15)}`,
|
||||
padding: "8px",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
cursor: onClick ? "pointer" : "default",
|
||||
"&:hover": onClick
|
||||
? {
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
border: `1px solid ${alpha(colorValue, 0.3)}`,
|
||||
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
</Grid>
|
||||
|
||||
{/* 文本内容 */}
|
||||
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{title}
|
||||
</Typography>
|
||||
<Grid component="div" sx={{ display: "flex", alignItems: "baseline" }}>
|
||||
<Typography variant="body1" fontWeight="bold" noWrap sx={{ mr: 0.5 }}>
|
||||
{value}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{unit}
|
||||
</Typography>
|
||||
{/* 图标容器 */}
|
||||
<Grid
|
||||
component="div"
|
||||
sx={{
|
||||
mr: 1,
|
||||
ml: "2px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "50%",
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
color: colorValue,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
|
||||
{/* 文本内容 */}
|
||||
<Grid component="div" sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{title}
|
||||
</Typography>
|
||||
<Grid
|
||||
component="div"
|
||||
sx={{ display: "flex", alignItems: "baseline" }}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
noWrap
|
||||
sx={{ mr: 0.5 }}
|
||||
>
|
||||
{value}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{unit}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 添加显示名称
|
||||
CompactStatCard.displayName = "CompactStatCard";
|
||||
@@ -205,25 +210,25 @@ export const EnhancedTrafficStats = () => {
|
||||
down: data.down,
|
||||
timestamp: now,
|
||||
});
|
||||
} catch { }
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
lastUpdateRef.current.traffic = now;
|
||||
const safeUp = isNaN(data.up) ? 0 : data.up;
|
||||
const safeDown = isNaN(data.down) ? 0 : data.down;
|
||||
try {
|
||||
setStats(prev => ({
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
traffic: { up: safeUp, down: safeDown }
|
||||
traffic: { up: safeUp, down: safeDown },
|
||||
}));
|
||||
} catch { }
|
||||
} catch {}
|
||||
try {
|
||||
trafficRef.current?.appendData({
|
||||
up: safeUp,
|
||||
down: safeDown,
|
||||
timestamp: now,
|
||||
});
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Traffic] 解析数据错误:", err, event.data);
|
||||
@@ -235,12 +240,12 @@ export const EnhancedTrafficStats = () => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as MemoryUsage;
|
||||
if (data && typeof data.inuse === "number") {
|
||||
setStats(prev => ({
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
memory: {
|
||||
inuse: isNaN(data.inuse) ? 0 : data.inuse,
|
||||
oslimit: data.oslimit,
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -257,7 +262,7 @@ export const EnhancedTrafficStats = () => {
|
||||
|
||||
// 清理现有连接的函数
|
||||
const cleanupSockets = () => {
|
||||
Object.values(socketRefs.current).forEach(socket => {
|
||||
Object.values(socketRefs.current).forEach((socket) => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
@@ -269,40 +274,78 @@ export const EnhancedTrafficStats = () => {
|
||||
cleanupSockets();
|
||||
|
||||
// 创建新连接
|
||||
console.log(`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`);
|
||||
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, {
|
||||
onmessage: handleTrafficUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event);
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] 正在连接: ${server}/traffic`,
|
||||
);
|
||||
socketRefs.current.traffic = createAuthSockette(
|
||||
`${server}/traffic`,
|
||||
secret,
|
||||
{
|
||||
onmessage: handleTrafficUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
|
||||
event,
|
||||
);
|
||||
},
|
||||
onerror: (event) => {
|
||||
console.error(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
|
||||
event,
|
||||
);
|
||||
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
},
|
||||
onclose: (event) => {
|
||||
console.log(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
|
||||
event.code,
|
||||
event.reason,
|
||||
);
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.warn(
|
||||
`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
|
||||
);
|
||||
setStats((prev) => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
}
|
||||
},
|
||||
},
|
||||
onerror: (event) => {
|
||||
console.error(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
|
||||
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
},
|
||||
onclose: (event) => {
|
||||
console.log(`[Traffic][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.warn(`[Traffic][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
|
||||
setStats(prev => ({ ...prev, traffic: { up: 0, down: 0 } }));
|
||||
}
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
console.log(`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`);
|
||||
console.log(
|
||||
`[Memory][${EnhancedTrafficStats.name}] 正在连接: ${server}/memory`,
|
||||
);
|
||||
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
|
||||
onmessage: handleMemoryUpdate,
|
||||
onopen: (event) => {
|
||||
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`, event);
|
||||
console.log(
|
||||
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接已建立`,
|
||||
event,
|
||||
);
|
||||
},
|
||||
onerror: (event) => {
|
||||
console.error(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`, event);
|
||||
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } }));
|
||||
console.error(
|
||||
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接错误或达到最大重试次数`,
|
||||
event,
|
||||
);
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
memory: { inuse: 0, oslimit: undefined },
|
||||
}));
|
||||
},
|
||||
onclose: (event) => {
|
||||
console.log(`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`, event.code, event.reason);
|
||||
console.log(
|
||||
`[Memory][${EnhancedTrafficStats.name}] WebSocket 连接关闭`,
|
||||
event.code,
|
||||
event.reason,
|
||||
);
|
||||
if (event.code !== 1000 && event.code !== 1001) {
|
||||
console.warn(`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`);
|
||||
setStats(prev => ({ ...prev, memory: { inuse: 0, oslimit: undefined } }));
|
||||
console.warn(
|
||||
`[Memory][${EnhancedTrafficStats.name}] 连接非正常关闭,重置状态`,
|
||||
);
|
||||
setStats((prev) => ({
|
||||
...prev,
|
||||
memory: { inuse: 0, oslimit: undefined },
|
||||
}));
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -314,11 +357,11 @@ export const EnhancedTrafficStats = () => {
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
Object.values(socketRefs.current).forEach(socket => {
|
||||
Object.values(socketRefs.current).forEach((socket) => {
|
||||
if (socket) socket.close();
|
||||
});
|
||||
socketRefs.current = { traffic: null, memory: null };
|
||||
} catch { }
|
||||
} catch {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -339,13 +382,25 @@ export const EnhancedTrafficStats = () => {
|
||||
const [up, upUnit] = parseTraffic(stats.traffic.up);
|
||||
const [down, downUnit] = parseTraffic(stats.traffic.down);
|
||||
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
|
||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(connections.uploadTotal);
|
||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(connections.downloadTotal);
|
||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(
|
||||
connections.uploadTotal,
|
||||
);
|
||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(
|
||||
connections.downloadTotal,
|
||||
);
|
||||
|
||||
return {
|
||||
up, upUnit, down, downUnit, inuse, inuseUnit,
|
||||
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit,
|
||||
connectionsCount: connections.count
|
||||
up,
|
||||
upUnit,
|
||||
down,
|
||||
downUnit,
|
||||
inuse,
|
||||
inuseUnit,
|
||||
uploadTotal,
|
||||
uploadTotalUnit,
|
||||
downloadTotal,
|
||||
downloadTotalUnit,
|
||||
connectionsCount: connections.count,
|
||||
};
|
||||
}, [stats, connections]);
|
||||
|
||||
@@ -392,51 +447,54 @@ export const EnhancedTrafficStats = () => {
|
||||
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
|
||||
|
||||
// 使用useMemo计算统计卡片配置
|
||||
const statCards = useMemo(() => [
|
||||
{
|
||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||
title: t("Upload Speed"),
|
||||
value: parsedData.up,
|
||||
unit: `${parsedData.upUnit}/s`,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||
title: t("Download Speed"),
|
||||
value: parsedData.down,
|
||||
unit: `${parsedData.downUnit}/s`,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <LinkRounded fontSize="small" />,
|
||||
title: t("Active Connections"),
|
||||
value: parsedData.connectionsCount,
|
||||
unit: "",
|
||||
color: "success" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudUploadRounded fontSize="small" />,
|
||||
title: t("Uploaded"),
|
||||
value: parsedData.uploadTotal,
|
||||
unit: parsedData.uploadTotalUnit,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudDownloadRounded fontSize="small" />,
|
||||
title: t("Downloaded"),
|
||||
value: parsedData.downloadTotal,
|
||||
unit: parsedData.downloadTotalUnit,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <MemoryRounded fontSize="small" />,
|
||||
title: t("Memory Usage"),
|
||||
value: parsedData.inuse,
|
||||
unit: parsedData.inuseUnit,
|
||||
color: "error" as const,
|
||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||
},
|
||||
], [t, parsedData, isDebug, handleGarbageCollection]);
|
||||
const statCards = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||
title: t("Upload Speed"),
|
||||
value: parsedData.up,
|
||||
unit: `${parsedData.upUnit}/s`,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||
title: t("Download Speed"),
|
||||
value: parsedData.down,
|
||||
unit: `${parsedData.downUnit}/s`,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <LinkRounded fontSize="small" />,
|
||||
title: t("Active Connections"),
|
||||
value: parsedData.connectionsCount,
|
||||
unit: "",
|
||||
color: "success" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudUploadRounded fontSize="small" />,
|
||||
title: t("Uploaded"),
|
||||
value: parsedData.uploadTotal,
|
||||
unit: parsedData.uploadTotalUnit,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudDownloadRounded fontSize="small" />,
|
||||
title: t("Downloaded"),
|
||||
value: parsedData.downloadTotal,
|
||||
unit: parsedData.downloadTotalUnit,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <MemoryRounded fontSize="small" />,
|
||||
title: t("Memory Usage"),
|
||||
value: parsedData.inuse,
|
||||
unit: parsedData.inuseUnit,
|
||||
color: "error" as const,
|
||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||
},
|
||||
],
|
||||
[t, parsedData, isDebug, handleGarbageCollection],
|
||||
);
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||
|
||||
@@ -78,12 +78,16 @@ const truncateStyle = {
|
||||
maxWidth: "calc(100% - 28px)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap"
|
||||
whiteSpace: "nowrap",
|
||||
};
|
||||
|
||||
// 提取独立组件减少主组件复杂度
|
||||
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
current: ProfileItem;
|
||||
const ProfileDetails = ({
|
||||
current,
|
||||
onUpdateProfile,
|
||||
updating,
|
||||
}: {
|
||||
current: ProfileItem;
|
||||
onUpdateProfile: () => void;
|
||||
updating: boolean;
|
||||
}) => {
|
||||
@@ -99,7 +103,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
if (!current.extra || !current.extra.total) return 1;
|
||||
return Math.min(
|
||||
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
|
||||
100
|
||||
100,
|
||||
);
|
||||
}, [current.extra, usedTraffic]);
|
||||
|
||||
@@ -109,19 +113,24 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
{current.url && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<DnsOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary" noWrap sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
noWrap
|
||||
sx={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<span style={{ flexShrink: 0 }}>{t("From")}: </span>
|
||||
{current.home ? (
|
||||
<Link
|
||||
component="button"
|
||||
fontWeight="medium"
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
sx={{
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
minWidth: 0,
|
||||
maxWidth: "calc(100% - 40px)",
|
||||
ml: 0.5
|
||||
ml: 0.5,
|
||||
}}
|
||||
title={parseUrl(current.url)}
|
||||
>
|
||||
@@ -132,14 +141,19 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
minWidth: 0,
|
||||
flex: 1
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{parseUrl(current.url)}
|
||||
</Typography>
|
||||
<LaunchOutlined
|
||||
fontSize="inherit"
|
||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }}
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
fontSize: "0.8rem",
|
||||
opacity: 0.7,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
@@ -152,7 +166,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
whiteSpace: "nowrap",
|
||||
minWidth: 0,
|
||||
flex: 1,
|
||||
ml: 0.5
|
||||
ml: 0.5,
|
||||
}}
|
||||
title={parseUrl(current.url)}
|
||||
>
|
||||
@@ -195,7 +209,8 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Used / Total")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)}
|
||||
{parseTraffic(usedTraffic)} /{" "}
|
||||
{parseTraffic(current.extra.total)}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -240,7 +255,7 @@ const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
// 提取空配置组件
|
||||
const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -268,27 +283,30 @@ const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
|
||||
export const HomeProfileCard = ({
|
||||
current,
|
||||
onProfileUpdated,
|
||||
}: HomeProfileCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { refreshAll } = useAppData();
|
||||
|
||||
// 更新当前订阅
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
|
||||
const onUpdateProfile = useLockFn(async () => {
|
||||
if (!current?.uid) return;
|
||||
|
||||
setUpdating(true);
|
||||
try {
|
||||
await updateProfile(current.uid, current.option);
|
||||
showNotice('success', t("Update subscription successfully"), 1000);
|
||||
showNotice("success", t("Update subscription successfully"), 1000);
|
||||
onProfileUpdated?.();
|
||||
|
||||
|
||||
// 刷新首页数据
|
||||
refreshAll();
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString(), 3000);
|
||||
showNotice("error", err.message || err.toString(), 3000);
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
@@ -302,9 +320,9 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
// 卡片标题
|
||||
const cardTitle = useMemo(() => {
|
||||
if (!current) return t("Profiles");
|
||||
|
||||
|
||||
if (!current.home) return current.name;
|
||||
|
||||
|
||||
return (
|
||||
<Link
|
||||
component="button"
|
||||
@@ -323,19 +341,19 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
flex: 1
|
||||
}
|
||||
flex: 1,
|
||||
},
|
||||
}}
|
||||
title={current.name}
|
||||
>
|
||||
<span>{current.name}</span>
|
||||
<LaunchOutlined
|
||||
fontSize="inherit"
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
fontSize: "0.8rem",
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
fontSize: "0.8rem",
|
||||
opacity: 0.7,
|
||||
flexShrink: 0
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
@@ -345,7 +363,7 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
// 卡片操作按钮
|
||||
const cardAction = useMemo(() => {
|
||||
if (!current) return null;
|
||||
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
@@ -367,10 +385,10 @@ export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardPr
|
||||
action={cardAction}
|
||||
>
|
||||
{current ? (
|
||||
<ProfileDetails
|
||||
current={current}
|
||||
onUpdateProfile={onUpdateProfile}
|
||||
updating={updating}
|
||||
<ProfileDetails
|
||||
current={current}
|
||||
onUpdateProfile={onUpdateProfile}
|
||||
updating={updating}
|
||||
/>
|
||||
) : (
|
||||
<EmptyProfile onClick={goToProfiles} />
|
||||
|
||||
@@ -83,28 +83,28 @@ export const IpInfoCard = () => {
|
||||
// 组件加载时获取IP信息
|
||||
useEffect(() => {
|
||||
fetchIpInfo();
|
||||
|
||||
|
||||
// 倒计时实现优化,减少不必要的重渲染
|
||||
let timer: number | null = null;
|
||||
let currentCount = IP_REFRESH_SECONDS;
|
||||
|
||||
|
||||
// 只在必要时更新状态,减少重渲染次数
|
||||
const startCountdown = () => {
|
||||
timer = window.setInterval(() => {
|
||||
currentCount -= 1;
|
||||
|
||||
|
||||
if (currentCount <= 0) {
|
||||
fetchIpInfo();
|
||||
currentCount = IP_REFRESH_SECONDS;
|
||||
}
|
||||
|
||||
|
||||
// 每5秒或倒计时结束时才更新UI
|
||||
if (currentCount % 5 === 0 || currentCount <= 0) {
|
||||
setCountdown(currentCount);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
|
||||
startCountdown();
|
||||
return () => {
|
||||
if (timer) clearInterval(timer);
|
||||
@@ -112,7 +112,7 @@ export const IpInfoCard = () => {
|
||||
}, [fetchIpInfo]);
|
||||
|
||||
const toggleShowIp = useCallback(() => {
|
||||
setShowIp(prev => !prev);
|
||||
setShowIp((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
// 渲染加载状态
|
||||
@@ -282,9 +282,7 @@ export const IpInfoCard = () => {
|
||||
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
||||
<InfoItem
|
||||
label={t("Location")}
|
||||
value={[ipInfo?.city, ipInfo?.region]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
value={[ipInfo?.city, ipInfo?.region].filter(Boolean).join(", ")}
|
||||
/>
|
||||
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
||||
</Box>
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography, Stack, Divider, Chip, IconButton, Tooltip } from "@mui/material";
|
||||
import {
|
||||
InfoOutlined,
|
||||
SettingsOutlined,
|
||||
WarningOutlined,
|
||||
import {
|
||||
Typography,
|
||||
Stack,
|
||||
Divider,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
InfoOutlined,
|
||||
SettingsOutlined,
|
||||
WarningOutlined,
|
||||
AdminPanelSettingsOutlined,
|
||||
DnsOutlined,
|
||||
ExtensionOutlined
|
||||
ExtensionOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
getSystemInfo,
|
||||
} from "@/services/cmds";
|
||||
import { getSystemInfo } from "@/services/cmds";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { version as appVersion } from "@root/package.json";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -30,32 +35,35 @@ export const SystemInfoCard = () => {
|
||||
const { isAdminMode, isSidecarMode, mutateRunningMode } = useSystemState();
|
||||
const { installServiceAndRestartCore } = useServiceInstaller();
|
||||
|
||||
// 系统信息状态
|
||||
const [systemState, setSystemState] = useState({
|
||||
osInfo: "",
|
||||
lastCheckUpdate: "-",
|
||||
});
|
||||
// 系统信息状态
|
||||
const [systemState, setSystemState] = useState({
|
||||
osInfo: "",
|
||||
lastCheckUpdate: "-",
|
||||
});
|
||||
|
||||
// 初始化系统信息
|
||||
useEffect(() => {
|
||||
getSystemInfo()
|
||||
.then((info) => {
|
||||
const lines = info.split("\n");
|
||||
if (lines.length > 0) {
|
||||
const sysName = lines[0].split(": ")[1] || "";
|
||||
let sysVersion = lines[1].split(": ")[1] || "";
|
||||
// 初始化系统信息
|
||||
useEffect(() => {
|
||||
getSystemInfo()
|
||||
.then((info) => {
|
||||
const lines = info.split("\n");
|
||||
if (lines.length > 0) {
|
||||
const sysName = lines[0].split(": ")[1] || "";
|
||||
let sysVersion = lines[1].split(": ")[1] || "";
|
||||
|
||||
if (sysName && sysVersion.toLowerCase().startsWith(sysName.toLowerCase())) {
|
||||
sysVersion = sysVersion.substring(sysName.length).trim();
|
||||
if (
|
||||
sysName &&
|
||||
sysVersion.toLowerCase().startsWith(sysName.toLowerCase())
|
||||
) {
|
||||
sysVersion = sysVersion.substring(sysName.length).trim();
|
||||
}
|
||||
|
||||
setSystemState((prev) => ({
|
||||
...prev,
|
||||
osInfo: `${sysName} ${sysVersion}`,
|
||||
}));
|
||||
}
|
||||
|
||||
setSystemState((prev) => ({
|
||||
...prev,
|
||||
osInfo: `${sysName} ${sysVersion}`,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
// 获取最后检查更新时间
|
||||
const lastCheck = localStorage.getItem("last_check_update");
|
||||
@@ -122,7 +130,6 @@ useEffect(() => {
|
||||
}
|
||||
}, [verge, patchVerge]);
|
||||
|
||||
|
||||
// 点击运行模式处理,Sidecar或纯管理员模式允许安装服务
|
||||
const handleRunningModeClick = useCallback(() => {
|
||||
if (isSidecarMode || (isAdminMode && isSidecarMode)) {
|
||||
@@ -135,13 +142,13 @@ useEffect(() => {
|
||||
try {
|
||||
const info = await checkUpdate();
|
||||
if (!info?.available) {
|
||||
showNotice('success', t("Currently on the Latest Version"));
|
||||
showNotice("success", t("Currently on the Latest Version"));
|
||||
} else {
|
||||
showNotice('info', t("Update Available"), 2000);
|
||||
showNotice("info", t("Update Available"), 2000);
|
||||
goToSettings();
|
||||
}
|
||||
} catch (err: any) {
|
||||
showNotice('error', err.message || err.toString());
|
||||
showNotice("error", err.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -155,13 +162,15 @@ useEffect(() => {
|
||||
const runningModeStyle = useMemo(
|
||||
() => ({
|
||||
// Sidecar或纯管理员模式允许安装服务
|
||||
cursor: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "pointer" : "default",
|
||||
textDecoration: (isSidecarMode || (isAdminMode && isSidecarMode)) ? "underline" : "none",
|
||||
cursor:
|
||||
isSidecarMode || (isAdminMode && isSidecarMode) ? "pointer" : "default",
|
||||
textDecoration:
|
||||
isSidecarMode || (isAdminMode && isSidecarMode) ? "underline" : "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 0.5,
|
||||
"&:hover": {
|
||||
opacity: (isSidecarMode || (isAdminMode && isSidecarMode)) ? 0.7 : 1,
|
||||
opacity: isSidecarMode || (isAdminMode && isSidecarMode) ? 0.7 : 1,
|
||||
},
|
||||
}),
|
||||
[isSidecarMode, isAdminMode],
|
||||
@@ -174,34 +183,34 @@ useEffect(() => {
|
||||
if (!isSidecarMode) {
|
||||
return (
|
||||
<>
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
titleAccess={t("Administrator Mode")}
|
||||
/>
|
||||
<DnsOutlined
|
||||
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
|
||||
<DnsOutlined
|
||||
sx={{ color: "success.main", fontSize: 16, ml: 0.5 }}
|
||||
titleAccess={t("Service Mode")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
<AdminPanelSettingsOutlined
|
||||
sx={{ color: "primary.main", fontSize: 16 }}
|
||||
titleAccess={t("Administrator Mode")}
|
||||
/>
|
||||
);
|
||||
} else if (isSidecarMode) {
|
||||
return (
|
||||
<ExtensionOutlined
|
||||
sx={{ color: "info.main", fontSize: 16 }}
|
||||
<ExtensionOutlined
|
||||
sx={{ color: "info.main", fontSize: 16 }}
|
||||
titleAccess={t("Sidecar Mode")}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DnsOutlined
|
||||
sx={{ color: "success.main", fontSize: 16 }}
|
||||
<DnsOutlined
|
||||
sx={{ color: "success.main", fontSize: 16 }}
|
||||
titleAccess={t("Service Mode")}
|
||||
/>
|
||||
);
|
||||
@@ -247,13 +256,19 @@ useEffect(() => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Auto Launch")}
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
{isAdminMode && (
|
||||
<Tooltip title={t("Administrator mode may not support auto launch")}>
|
||||
<Tooltip
|
||||
title={t("Administrator mode may not support auto launch")}
|
||||
>
|
||||
<WarningOutlined sx={{ color: "warning.main", fontSize: 20 }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -268,7 +283,11 @@ useEffect(() => {
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Running Mode")}
|
||||
</Typography>
|
||||
|
||||
@@ -87,12 +87,12 @@ export const TestCard = () => {
|
||||
}
|
||||
|
||||
const newList = testList.map((x) =>
|
||||
x.uid === uid ? { ...x, ...patch } : x
|
||||
x.uid === uid ? { ...x, ...patch } : x,
|
||||
);
|
||||
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
},
|
||||
[testList, verge, mutateVerge]
|
||||
[testList, verge, mutateVerge],
|
||||
);
|
||||
|
||||
const onDeleteTestListItem = useCallback(
|
||||
@@ -101,7 +101,7 @@ export const TestCard = () => {
|
||||
patchVerge({ test_list: newList });
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
},
|
||||
[testList, verge, patchVerge, mutateVerge]
|
||||
[testList, verge, patchVerge, mutateVerge],
|
||||
);
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
@@ -122,7 +122,7 @@ export const TestCard = () => {
|
||||
const patchFn = () => {
|
||||
try {
|
||||
patchVerge({ test_list: newList });
|
||||
} catch { }
|
||||
} catch {}
|
||||
};
|
||||
if (window.requestIdleCallback) {
|
||||
window.requestIdleCallback(patchFn);
|
||||
@@ -131,7 +131,7 @@ export const TestCard = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
[testList, verge, mutateVerge, patchVerge]
|
||||
[testList, verge, mutateVerge, patchVerge],
|
||||
);
|
||||
|
||||
// 仅在verge首次加载时初始化测试列表
|
||||
@@ -142,22 +142,25 @@ export const TestCard = () => {
|
||||
}, [verge, patchVerge]);
|
||||
|
||||
// 使用useMemo优化UI内容,减少渲染计算
|
||||
const renderTestItems = useMemo(() => (
|
||||
<Grid container spacing={1} columns={12}>
|
||||
<SortableContext items={testList.map((x) => x.uid)}>
|
||||
{testList.map((item) => (
|
||||
<Grid key={item.uid} size={3}>
|
||||
<TestItem
|
||||
id={item.uid}
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
), [testList, onDeleteTestListItem]);
|
||||
const renderTestItems = useMemo(
|
||||
() => (
|
||||
<Grid container spacing={1} columns={12}>
|
||||
<SortableContext items={testList.map((x) => x.uid)}>
|
||||
{testList.map((item) => (
|
||||
<Grid key={item.uid} size={3}>
|
||||
<TestItem
|
||||
id={item.uid}
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
),
|
||||
[testList, onDeleteTestListItem],
|
||||
);
|
||||
|
||||
const handleTestAll = useCallback(() => {
|
||||
emit("verge://test-all");
|
||||
|
||||
Reference in New Issue
Block a user