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:
Tunglies
2025-06-06 21:11:14 +08:00
parent 689042df60
commit 09969d95de
89 changed files with 2630 additions and 2008 deletions

View File

@@ -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">

View File

@@ -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>

View File

@@ -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>
);
};
};

View File

@@ -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>

View File

@@ -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";

View File

@@ -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 }}>

View File

@@ -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} />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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");