diff --git a/src/components/connection/connection-detail.tsx b/src/components/connection/connection-detail.tsx index 9eb0f563..fe563c0e 100644 --- a/src/components/connection/connection-detail.tsx +++ b/src/components/connection/connection-detail.tsx @@ -2,7 +2,7 @@ import { Box, Button, Snackbar, useTheme } from "@mui/material"; import { useLockFn } from "ahooks"; import dayjs from "dayjs"; import { t } from "i18next"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { deleteConnection } from "@/services/cmds"; import parseTraffic from "@/utils/parse-traffic"; @@ -11,45 +11,43 @@ export interface ConnectionDetailRef { open: (detail: IConnectionsItem) => void; } -export const ConnectionDetail = forwardRef( - (props, ref) => { - const [open, setOpen] = useState(false); - const [detail, setDetail] = useState(null!); - const theme = useTheme(); +export const ConnectionDetail = ({ ref, ...props }) => { + const [open, setOpen] = useState(false); + const [detail, setDetail] = useState(null!); + const theme = useTheme(); - useImperativeHandle(ref, () => ({ - open: (detail: IConnectionsItem) => { - if (open) return; - setOpen(true); - setDetail(detail); - }, - })); + useImperativeHandle(ref, () => ({ + open: (detail: IConnectionsItem) => { + if (open) return; + setOpen(true); + setDetail(detail); + }, + })); - const onClose = () => setOpen(false); + const onClose = () => setOpen(false); - return ( - - ) : null - } - /> - ); - }, -); + return ( + + ) : null + } + /> + ); +}; interface InnerProps { data: IConnectionsItem; diff --git a/src/components/home/enhanced-canvas-traffic-graph.tsx b/src/components/home/enhanced-canvas-traffic-graph.tsx index 2f8a4719..9ccd5cc9 100644 --- a/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -1,6 +1,5 @@ import { Box, useTheme } from "@mui/material"; import { - forwardRef, useImperativeHandle, useState, useEffect, @@ -81,578 +80,525 @@ const GRAPH_CONFIG = { * 稳定版Canvas流量图表组件 * 修复闪烁问题,添加时间轴显示 */ -export const EnhancedCanvasTrafficGraph = memo( - forwardRef((props, ref) => { - const theme = useTheme(); - const { t } = useTranslation(); +export const EnhancedCanvasTrafficGraph = memo(({ ref, ...props }) => { + const theme = useTheme(); + const { t } = useTranslation(); - // 使用增强版全局流量数据管理 - const { dataPoints, getDataForTimeRange, isDataFresh, samplerStats } = - useTrafficGraphDataEnhanced(); + // 使用增强版全局流量数据管理 + const { dataPoints, getDataForTimeRange, isDataFresh, samplerStats } = + useTrafficGraphDataEnhanced(); - // 基础状态 - const [timeRange, setTimeRange] = useState(10); - const [chartStyle, setChartStyle] = useState<"bezier" | "line">("bezier"); + // 基础状态 + const [timeRange, setTimeRange] = useState(10); + const [chartStyle, setChartStyle] = useState<"bezier" | "line">("bezier"); - // 悬浮提示状态 - const [tooltipData, setTooltipData] = useState({ - x: 0, - y: 0, - upSpeed: "", - downSpeed: "", - timestamp: "", - visible: false, - dataIndex: -1, - highlightY: 0, - }); + // 悬浮提示状态 + const [tooltipData, setTooltipData] = useState({ + x: 0, + y: 0, + upSpeed: "", + downSpeed: "", + timestamp: "", + visible: false, + dataIndex: -1, + highlightY: 0, + }); - // Canvas引用和渲染状态 - const canvasRef = useRef(null); - const animationFrameRef = useRef(undefined); - const lastRenderTimeRef = useRef(0); - const isInitializedRef = useRef(false); + // Canvas引用和渲染状态 + const canvasRef = useRef(null); + const animationFrameRef = useRef(undefined); + const lastRenderTimeRef = useRef(0); + const isInitializedRef = useRef(false); - // 当前显示的数据缓存 - const [displayData, setDisplayData] = useState([]); + // 当前显示的数据缓存 + const [displayData, setDisplayData] = useState([]); - // 主题颜色配置 - const colors = useMemo( - () => ({ - up: theme.palette.secondary.main, - down: theme.palette.primary.main, - grid: theme.palette.divider, - text: theme.palette.text.secondary, - background: theme.palette.background.paper, - }), - [theme], - ); + // 主题颜色配置 + const colors = useMemo( + () => ({ + up: theme.palette.secondary.main, + down: theme.palette.primary.main, + grid: theme.palette.divider, + text: theme.palette.text.secondary, + background: theme.palette.background.paper, + }), + [theme], + ); - // 更新显示数据(防抖处理) - const updateDisplayDataDebounced = useMemo(() => { - let timeoutId: number; - return (newData: ITrafficDataPoint[]) => { - clearTimeout(timeoutId); - timeoutId = window.setTimeout(() => { - setDisplayData(newData); - }, 50); // 50ms防抖 - }; - }, []); + // 更新显示数据(防抖处理) + const updateDisplayDataDebounced = useMemo(() => { + let timeoutId: number; + return (newData: ITrafficDataPoint[]) => { + clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + setDisplayData(newData); + }, 50); // 50ms防抖 + }; + }, []); - // 监听数据变化 - useEffect(() => { - const timeRangeData = getDataForTimeRange(timeRange); - updateDisplayDataDebounced(timeRangeData); - }, [ - dataPoints, - timeRange, - getDataForTimeRange, - updateDisplayDataDebounced, - ]); + // 监听数据变化 + useEffect(() => { + const timeRangeData = getDataForTimeRange(timeRange); + updateDisplayDataDebounced(timeRangeData); + }, [dataPoints, timeRange, getDataForTimeRange, updateDisplayDataDebounced]); - // Y轴坐标计算 - 基于刻度范围的线性映射 - const calculateY = useCallback( - (value: number, height: number, data: ITrafficDataPoint[]): number => { - const padding = GRAPH_CONFIG.padding; - const topY = padding.top + 10; // 与刻度系统保持一致 - const bottomY = height - padding.bottom - 5; + // Y轴坐标计算 - 基于刻度范围的线性映射 + const calculateY = useCallback( + (value: number, height: number, data: ITrafficDataPoint[]): number => { + const padding = GRAPH_CONFIG.padding; + const topY = padding.top + 10; // 与刻度系统保持一致 + const bottomY = height - padding.bottom - 5; - if (data.length === 0) return bottomY; + if (data.length === 0) return bottomY; - // 获取当前的刻度范围 - const allValues = [ - ...data.map((d) => d.up), - ...data.map((d) => d.down), - ]; - const maxValue = Math.max(...allValues); - const minValue = Math.min(...allValues); + // 获取当前的刻度范围 + const allValues = [...data.map((d) => d.up), ...data.map((d) => d.down)]; + const maxValue = Math.max(...allValues); + const minValue = Math.min(...allValues); - let topValue, bottomValue; + let topValue, bottomValue; - if (maxValue === 0) { - topValue = 1024; + if (maxValue === 0) { + topValue = 1024; + bottomValue = 0; + } else { + const range = maxValue - minValue; + const padding_percent = range > 0 ? 0.1 : 0.5; + + if (range === 0) { bottomValue = 0; + topValue = maxValue * 1.2; } else { - const range = maxValue - minValue; - const padding_percent = range > 0 ? 0.1 : 0.5; - - if (range === 0) { - bottomValue = 0; - topValue = maxValue * 1.2; - } else { - bottomValue = Math.max(0, minValue - range * padding_percent); - topValue = maxValue + range * padding_percent; - } + bottomValue = Math.max(0, minValue - range * padding_percent); + topValue = maxValue + range * padding_percent; } + } - // 线性映射到Y坐标 - if (topValue === bottomValue) return bottomY; + // 线性映射到Y坐标 + if (topValue === bottomValue) return bottomY; - const ratio = (value - bottomValue) / (topValue - bottomValue); - return bottomY - ratio * (bottomY - topY); - }, - [], - ); + const ratio = (value - bottomValue) / (topValue - bottomValue); + return bottomY - ratio * (bottomY - topY); + }, + [], + ); - // 鼠标悬浮处理 - 计算最近的数据点 - const handleMouseMove = useCallback( - (event: React.MouseEvent) => { - const canvas = canvasRef.current; - if (!canvas || displayData.length === 0) return; + // 鼠标悬浮处理 - 计算最近的数据点 + const handleMouseMove = useCallback( + (event: React.MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas || displayData.length === 0) return; - const rect = canvas.getBoundingClientRect(); - const mouseX = event.clientX - rect.left; - const mouseY = event.clientY - rect.top; + const rect = canvas.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; - const padding = GRAPH_CONFIG.padding; - const effectiveWidth = rect.width - padding.left - padding.right; + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = rect.width - padding.left - padding.right; - // 计算最接近的数据点索引 - const relativeMouseX = mouseX - padding.left; - const ratio = Math.max(0, Math.min(1, relativeMouseX / effectiveWidth)); - const dataIndex = Math.round(ratio * (displayData.length - 1)); + // 计算最接近的数据点索引 + const relativeMouseX = mouseX - padding.left; + const ratio = Math.max(0, Math.min(1, relativeMouseX / effectiveWidth)); + const dataIndex = Math.round(ratio * (displayData.length - 1)); - if (dataIndex >= 0 && dataIndex < displayData.length) { - const dataPoint = displayData[dataIndex]; + if (dataIndex >= 0 && dataIndex < displayData.length) { + const dataPoint = displayData[dataIndex]; - // 格式化流量数据 - const [upValue, upUnit] = parseTraffic(dataPoint.up); - const [downValue, downUnit] = parseTraffic(dataPoint.down); + // 格式化流量数据 + const [upValue, upUnit] = parseTraffic(dataPoint.up); + const [downValue, downUnit] = parseTraffic(dataPoint.down); - // 格式化时间戳 - const timeStr = dataPoint.timestamp - ? new Date(dataPoint.timestamp).toLocaleTimeString("zh-CN", { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }) - : "未知时间"; + // 格式化时间戳 + const timeStr = dataPoint.timestamp + ? new Date(dataPoint.timestamp).toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }) + : "未知时间"; - // 计算数据点对应的Y坐标位置(用于高亮) - const upY = calculateY(dataPoint.up, rect.height, displayData); - const downY = calculateY(dataPoint.down, rect.height, displayData); - const highlightY = - Math.max(dataPoint.up, dataPoint.down) === dataPoint.up - ? upY - : downY; + // 计算数据点对应的Y坐标位置(用于高亮) + const upY = calculateY(dataPoint.up, rect.height, displayData); + const downY = calculateY(dataPoint.down, rect.height, displayData); + const highlightY = + Math.max(dataPoint.up, dataPoint.down) === dataPoint.up ? upY : downY; - setTooltipData({ - x: mouseX, - y: mouseY, - upSpeed: `${upValue}${upUnit}/s`, - downSpeed: `${downValue}${downUnit}/s`, - timestamp: timeStr, - visible: true, - dataIndex, - highlightY, - }); - } - }, - [displayData, calculateY], - ); - - // 鼠标离开处理 - const handleMouseLeave = useCallback(() => { - setTooltipData((prev) => ({ ...prev, visible: false })); - }, []); - - // 获取智能Y轴刻度(三刻度系统:最小值、中间值、最大值) - const getYAxisTicks = useCallback( - (data: ITrafficDataPoint[], height: number) => { - if (data.length === 0) return []; - - // 找到数据的最大值和最小值 - const allValues = [ - ...data.map((d) => d.up), - ...data.map((d) => d.down), - ]; - const maxValue = Math.max(...allValues); - const minValue = Math.min(...allValues); - - // 格式化流量数值 - const formatTrafficValue = (bytes: number): string => { - if (bytes === 0) return "0"; - if (bytes < 1024) return `${Math.round(bytes)}B`; - if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`; - return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; - }; - - const padding = GRAPH_CONFIG.padding; - - // 强制显示三个刻度:底部、中间、顶部 - const topY = padding.top + 10; // 避免与顶部时间范围按钮重叠 - const bottomY = height - padding.bottom - 5; // 避免与底部时间轴重叠 - const middleY = (topY + bottomY) / 2; - - // 计算对应的值 - let topValue, middleValue, bottomValue; - - if (maxValue === 0) { - // 如果没有流量,显示0到一个小值的范围 - topValue = 1024; // 1KB - middleValue = 512; // 512B - bottomValue = 0; - } else { - // 根据数据范围计算合适的刻度值 - const range = maxValue - minValue; - const padding_percent = range > 0 ? 0.1 : 0.5; // 如果范围为0,使用更大的边距 - - if (range === 0) { - // 所有值相同的情况 - bottomValue = 0; - middleValue = maxValue * 0.5; - topValue = maxValue * 1.2; - } else { - // 正常情况 - bottomValue = Math.max(0, minValue - range * padding_percent); - topValue = maxValue + range * padding_percent; - middleValue = (bottomValue + topValue) / 2; - } - } - - // 创建三个固定位置的刻度 - const ticks = [ - { - value: bottomValue, - label: formatTrafficValue(bottomValue), - y: bottomY, - }, - { - value: middleValue, - label: formatTrafficValue(middleValue), - y: middleY, - }, - { - value: topValue, - label: formatTrafficValue(topValue), - y: topY, - }, - ]; - - return ticks; - }, - [], - ); - - // 绘制Y轴刻度线和标签 - const drawYAxis = useCallback( - ( - ctx: CanvasRenderingContext2D, - width: number, - height: number, - data: ITrafficDataPoint[], - ) => { - const padding = GRAPH_CONFIG.padding; - const ticks = getYAxisTicks(data, height); - - if (ticks.length === 0) return; - - ctx.save(); - - ticks.forEach((tick, index) => { - const isBottomTick = index === 0; // 最底部的刻度 - const isTopTick = index === ticks.length - 1; // 最顶部的刻度 - - // 绘制水平刻度线,只绘制关键刻度线 - if (isBottomTick || isTopTick) { - ctx.strokeStyle = colors.grid; - ctx.lineWidth = isBottomTick ? 0.8 : 0.4; // 底部刻度线稍粗 - ctx.globalAlpha = isBottomTick ? 0.25 : 0.15; - - ctx.beginPath(); - ctx.moveTo(padding.left, tick.y); - ctx.lineTo(width - padding.right, tick.y); - ctx.stroke(); - } - - // 绘制Y轴标签 - ctx.fillStyle = colors.text; - ctx.font = - "8px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif"; - ctx.globalAlpha = 0.9; - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - - // 为标签添加更清晰的背景(仅在必要时) - if (tick.label !== "0") { - const labelWidth = ctx.measureText(tick.label).width; - ctx.globalAlpha = 0.15; - ctx.fillStyle = colors.background; - ctx.fillRect( - padding.left - labelWidth - 8, - tick.y - 5, - labelWidth + 4, - 10, - ); - } - - // 绘制标签文字 - ctx.globalAlpha = 0.9; - ctx.fillStyle = colors.text; - ctx.fillText(tick.label, padding.left - 4, tick.y); + setTooltipData({ + x: mouseX, + y: mouseY, + upSpeed: `${upValue}${upUnit}/s`, + downSpeed: `${downValue}${downUnit}/s`, + timestamp: timeStr, + visible: true, + dataIndex, + highlightY, }); + } + }, + [displayData, calculateY], + ); - ctx.restore(); - }, - [colors.grid, colors.text, colors.background, getYAxisTicks], - ); + // 鼠标离开处理 + const handleMouseLeave = useCallback(() => { + setTooltipData((prev) => ({ ...prev, visible: false })); + }, []); - // 获取时间范围对应的最佳时间显示策略 - const getTimeDisplayStrategy = useCallback( - (timeRangeMinutes: TimeRange) => { - switch (timeRangeMinutes) { - case 1: // 1分钟:更密集的时间标签,显示 MM:SS - return { - maxLabels: 6, // 减少到6个,更适合短时间 - formatTime: (timestamp: number) => { - const date = new Date(timestamp); - const minutes = date.getMinutes().toString().padStart(2, "0"); - const seconds = date.getSeconds().toString().padStart(2, "0"); - return `${minutes}:${seconds}`; // 显示 MM:SS - }, - intervalSeconds: 10, // 每10秒一个标签,更合理 - minPixelDistance: 35, // 减少间距,允许更多标签 - }; - case 5: // 5分钟:中等密度,显示 HH:MM - return { - maxLabels: 6, // 6个标签比较合适 - formatTime: (timestamp: number) => { - const date = new Date(timestamp); - return date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - }); // 显示 HH:MM - }, - intervalSeconds: 30, // 约30秒间隔 - minPixelDistance: 38, // 减少间距,允许更多标签 - }; - case 10: // 10分钟:标准密度,显示 HH:MM - default: - return { - maxLabels: 8, // 保持8个 - formatTime: (timestamp: number) => { - const date = new Date(timestamp); - return date.toLocaleTimeString("en-US", { - hour12: false, - hour: "2-digit", - minute: "2-digit", - }); // 显示 HH:MM - }, - intervalSeconds: 60, // 1分钟间隔 - minPixelDistance: 40, // 减少间距,允许更多标签 - }; + // 获取智能Y轴刻度(三刻度系统:最小值、中间值、最大值) + const getYAxisTicks = useCallback( + (data: ITrafficDataPoint[], height: number) => { + if (data.length === 0) return []; + + // 找到数据的最大值和最小值 + const allValues = [...data.map((d) => d.up), ...data.map((d) => d.down)]; + const maxValue = Math.max(...allValues); + const minValue = Math.min(...allValues); + + // 格式化流量数值 + const formatTrafficValue = (bytes: number): string => { + if (bytes === 0) return "0"; + if (bytes < 1024) return `${Math.round(bytes)}B`; + if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; + }; + + const padding = GRAPH_CONFIG.padding; + + // 强制显示三个刻度:底部、中间、顶部 + const topY = padding.top + 10; // 避免与顶部时间范围按钮重叠 + const bottomY = height - padding.bottom - 5; // 避免与底部时间轴重叠 + const middleY = (topY + bottomY) / 2; + + // 计算对应的值 + let topValue, middleValue, bottomValue; + + if (maxValue === 0) { + // 如果没有流量,显示0到一个小值的范围 + topValue = 1024; // 1KB + middleValue = 512; // 512B + bottomValue = 0; + } else { + // 根据数据范围计算合适的刻度值 + const range = maxValue - minValue; + const padding_percent = range > 0 ? 0.1 : 0.5; // 如果范围为0,使用更大的边距 + + if (range === 0) { + // 所有值相同的情况 + bottomValue = 0; + middleValue = maxValue * 0.5; + topValue = maxValue * 1.2; + } else { + // 正常情况 + bottomValue = Math.max(0, minValue - range * padding_percent); + topValue = maxValue + range * padding_percent; + middleValue = (bottomValue + topValue) / 2; } - }, - [], - ); + } - // 绘制时间轴 - const drawTimeAxis = useCallback( - ( - ctx: CanvasRenderingContext2D, - width: number, - height: number, - data: ITrafficDataPoint[], - ) => { - if (data.length === 0) return; + // 创建三个固定位置的刻度 + const ticks = [ + { + value: bottomValue, + label: formatTrafficValue(bottomValue), + y: bottomY, + }, + { + value: middleValue, + label: formatTrafficValue(middleValue), + y: middleY, + }, + { + value: topValue, + label: formatTrafficValue(topValue), + y: topY, + }, + ]; - const padding = GRAPH_CONFIG.padding; - const effectiveWidth = width - padding.left - padding.right; - const timeAxisY = height - padding.bottom + 14; + return ticks; + }, + [], + ); - const strategy = getTimeDisplayStrategy(timeRange); + // 绘制Y轴刻度线和标签 + const drawYAxis = useCallback( + ( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + data: ITrafficDataPoint[], + ) => { + const padding = GRAPH_CONFIG.padding; + const ticks = getYAxisTicks(data, height); - ctx.save(); + if (ticks.length === 0) return; + + ctx.save(); + + ticks.forEach((tick, index) => { + const isBottomTick = index === 0; // 最底部的刻度 + const isTopTick = index === ticks.length - 1; // 最顶部的刻度 + + // 绘制水平刻度线,只绘制关键刻度线 + if (isBottomTick || isTopTick) { + ctx.strokeStyle = colors.grid; + ctx.lineWidth = isBottomTick ? 0.8 : 0.4; // 底部刻度线稍粗 + ctx.globalAlpha = isBottomTick ? 0.25 : 0.15; + + ctx.beginPath(); + ctx.moveTo(padding.left, tick.y); + ctx.lineTo(width - padding.right, tick.y); + ctx.stroke(); + } + + // 绘制Y轴标签 ctx.fillStyle = colors.text; ctx.font = - "10px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif"; - ctx.globalAlpha = 0.7; + "8px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif"; + ctx.globalAlpha = 0.9; + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; - // 根据数据长度和时间范围智能选择显示间隔 - const targetLabels = Math.min(strategy.maxLabels, data.length); - const step = Math.max(1, Math.floor(data.length / (targetLabels - 1))); - - // 使用策略中定义的最小像素间距 - const minPixelDistance = strategy.minPixelDistance || 45; - const actualStep = Math.max( - step, - Math.ceil((data.length * minPixelDistance) / effectiveWidth), - ); - - // 收集要显示的时间点 - const timePoints: Array<{ index: number; x: number; label: string }> = - []; - - // 添加第一个时间点 - if (data.length > 0 && data[0].timestamp) { - timePoints.push({ - index: 0, - x: padding.left, - label: strategy.formatTime(data[0].timestamp), - }); + // 为标签添加更清晰的背景(仅在必要时) + if (tick.label !== "0") { + const labelWidth = ctx.measureText(tick.label).width; + ctx.globalAlpha = 0.15; + ctx.fillStyle = colors.background; + ctx.fillRect( + padding.left - labelWidth - 8, + tick.y - 5, + labelWidth + 4, + 10, + ); } - // 添加中间的时间点 - for ( - let i = actualStep; - i < data.length - actualStep; - i += actualStep - ) { - const point = data[i]; - if (!point.timestamp) continue; + // 绘制标签文字 + ctx.globalAlpha = 0.9; + ctx.fillStyle = colors.text; + ctx.fillText(tick.label, padding.left - 4, tick.y); + }); - const x = padding.left + (i / (data.length - 1)) * effectiveWidth; - timePoints.push({ - index: i, - x, - label: strategy.formatTime(point.timestamp), - }); - } + ctx.restore(); + }, + [colors.grid, colors.text, colors.background, getYAxisTicks], + ); - // 添加最后一个时间点(如果不会与前面的重叠) - if (data.length > 1 && data[data.length - 1].timestamp) { - const lastX = width - padding.right; - const lastPoint = timePoints[timePoints.length - 1]; + // 获取时间范围对应的最佳时间显示策略 + const getTimeDisplayStrategy = useCallback((timeRangeMinutes: TimeRange) => { + switch (timeRangeMinutes) { + case 1: // 1分钟:更密集的时间标签,显示 MM:SS + return { + maxLabels: 6, // 减少到6个,更适合短时间 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + const minutes = date.getMinutes().toString().padStart(2, "0"); + const seconds = date.getSeconds().toString().padStart(2, "0"); + return `${minutes}:${seconds}`; // 显示 MM:SS + }, + intervalSeconds: 10, // 每10秒一个标签,更合理 + minPixelDistance: 35, // 减少间距,允许更多标签 + }; + case 5: // 5分钟:中等密度,显示 HH:MM + return { + maxLabels: 6, // 6个标签比较合适 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + }); // 显示 HH:MM + }, + intervalSeconds: 30, // 约30秒间隔 + minPixelDistance: 38, // 减少间距,允许更多标签 + }; + case 10: // 10分钟:标准密度,显示 HH:MM + default: + return { + maxLabels: 8, // 保持8个 + formatTime: (timestamp: number) => { + const date = new Date(timestamp); + return date.toLocaleTimeString("en-US", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + }); // 显示 HH:MM + }, + intervalSeconds: 60, // 1分钟间隔 + minPixelDistance: 40, // 减少间距,允许更多标签 + }; + } + }, []); - // 确保最后一个标签与前一个标签有足够间距 - if (!lastPoint || lastX - lastPoint.x >= minPixelDistance) { - timePoints.push({ - index: data.length - 1, - x: lastX, - label: strategy.formatTime(data[data.length - 1].timestamp), - }); - } - } + // 绘制时间轴 + const drawTimeAxis = useCallback( + ( + ctx: CanvasRenderingContext2D, + width: number, + height: number, + data: ITrafficDataPoint[], + ) => { + if (data.length === 0) return; - // 绘制时间标签 - timePoints.forEach((point, index) => { - if (index === 0) { - // 第一个标签左对齐 - ctx.textAlign = "left"; - } else if (index === timePoints.length - 1) { - // 最后一个标签右对齐 - ctx.textAlign = "right"; - } else { - // 中间标签居中对齐 - ctx.textAlign = "center"; - } + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = width - padding.left - padding.right; + const timeAxisY = height - padding.bottom + 14; - ctx.fillText(point.label, point.x, timeAxisY); + const strategy = getTimeDisplayStrategy(timeRange); + + ctx.save(); + ctx.fillStyle = colors.text; + ctx.font = + "10px -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif"; + ctx.globalAlpha = 0.7; + + // 根据数据长度和时间范围智能选择显示间隔 + const targetLabels = Math.min(strategy.maxLabels, data.length); + const step = Math.max(1, Math.floor(data.length / (targetLabels - 1))); + + // 使用策略中定义的最小像素间距 + const minPixelDistance = strategy.minPixelDistance || 45; + const actualStep = Math.max( + step, + Math.ceil((data.length * minPixelDistance) / effectiveWidth), + ); + + // 收集要显示的时间点 + const timePoints: Array<{ index: number; x: number; label: string }> = []; + + // 添加第一个时间点 + if (data.length > 0 && data[0].timestamp) { + timePoints.push({ + index: 0, + x: padding.left, + label: strategy.formatTime(data[0].timestamp), }); + } - ctx.restore(); - }, - [colors.text, timeRange, getTimeDisplayStrategy], - ); + // 添加中间的时间点 + for (let i = actualStep; i < data.length - actualStep; i += actualStep) { + const point = data[i]; + if (!point.timestamp) continue; - // 绘制网格线 - const drawGrid = useCallback( - (ctx: CanvasRenderingContext2D, width: number, height: number) => { - const padding = GRAPH_CONFIG.padding; - const effectiveWidth = width - padding.left - padding.right; - const effectiveHeight = height - padding.top - padding.bottom; + const x = padding.left + (i / (data.length - 1)) * effectiveWidth; + timePoints.push({ + index: i, + x, + label: strategy.formatTime(point.timestamp), + }); + } - ctx.save(); - ctx.strokeStyle = colors.grid; - ctx.lineWidth = GRAPH_CONFIG.lineWidth.grid; - ctx.globalAlpha = 0.7; + // 添加最后一个时间点(如果不会与前面的重叠) + if (data.length > 1 && data[data.length - 1].timestamp) { + const lastX = width - padding.right; + const lastPoint = timePoints[timePoints.length - 1]; - // 水平网格线 - const horizontalLines = 4; - for (let i = 1; i <= horizontalLines; i++) { - const y = padding.top + (effectiveHeight / (horizontalLines + 1)) * i; - ctx.beginPath(); - ctx.moveTo(padding.left, y); - ctx.lineTo(width - padding.right, y); - ctx.stroke(); + // 确保最后一个标签与前一个标签有足够间距 + if (!lastPoint || lastX - lastPoint.x >= minPixelDistance) { + timePoints.push({ + index: data.length - 1, + x: lastX, + label: strategy.formatTime(data[data.length - 1].timestamp), + }); + } + } + + // 绘制时间标签 + timePoints.forEach((point, index) => { + if (index === 0) { + // 第一个标签左对齐 + ctx.textAlign = "left"; + } else if (index === timePoints.length - 1) { + // 最后一个标签右对齐 + ctx.textAlign = "right"; + } else { + // 中间标签居中对齐 + ctx.textAlign = "center"; } - // 垂直网格线 - const verticalLines = 6; - for (let i = 1; i <= verticalLines; i++) { - const x = padding.left + (effectiveWidth / (verticalLines + 1)) * i; - ctx.beginPath(); - ctx.moveTo(x, padding.top); - ctx.lineTo(x, height - padding.bottom); - ctx.stroke(); - } + ctx.fillText(point.label, point.x, timeAxisY); + }); - ctx.restore(); - }, - [colors.grid], - ); + ctx.restore(); + }, + [colors.text, timeRange, getTimeDisplayStrategy], + ); - // 绘制流量线条 - const drawTrafficLine = useCallback( - ( - ctx: CanvasRenderingContext2D, - values: number[], - width: number, - height: number, - color: string, - withGradient = false, - data: ITrafficDataPoint[], - ) => { - if (values.length < 2) return; + // 绘制网格线 + const drawGrid = useCallback( + (ctx: CanvasRenderingContext2D, width: number, height: number) => { + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = width - padding.left - padding.right; + const effectiveHeight = height - padding.top - padding.bottom; - const padding = GRAPH_CONFIG.padding; - const effectiveWidth = width - padding.left - padding.right; + ctx.save(); + ctx.strokeStyle = colors.grid; + ctx.lineWidth = GRAPH_CONFIG.lineWidth.grid; + ctx.globalAlpha = 0.7; - const points = values.map((value, index) => [ - padding.left + (index / (values.length - 1)) * effectiveWidth, - calculateY(value, height, data), - ]); - - ctx.save(); - - // 绘制渐变填充 - if (withGradient && chartStyle === "bezier") { - const gradient = ctx.createLinearGradient( - 0, - padding.top, - 0, - height - padding.bottom, - ); - gradient.addColorStop( - 0, - `${color}${Math.round(GRAPH_CONFIG.alpha.gradient * 255) - .toString(16) - .padStart(2, "0")}`, - ); - gradient.addColorStop(1, `${color}00`); - - ctx.beginPath(); - ctx.moveTo(points[0][0], points[0][1]); - - if (chartStyle === "bezier") { - for (let i = 1; i < points.length; i++) { - const current = points[i]; - const next = points[i + 1] || current; - const controlX = (current[0] + next[0]) / 2; - const controlY = (current[1] + next[1]) / 2; - ctx.quadraticCurveTo(current[0], current[1], controlX, controlY); - } - } else { - for (let i = 1; i < points.length; i++) { - ctx.lineTo(points[i][0], points[i][1]); - } - } - - ctx.lineTo(points[points.length - 1][0], height - padding.bottom); - ctx.lineTo(points[0][0], height - padding.bottom); - ctx.closePath(); - ctx.fillStyle = gradient; - ctx.fill(); - } - - // 绘制主线条 + // 水平网格线 + const horizontalLines = 4; + for (let i = 1; i <= horizontalLines; i++) { + const y = padding.top + (effectiveHeight / (horizontalLines + 1)) * i; ctx.beginPath(); - ctx.strokeStyle = color; - ctx.lineWidth = GRAPH_CONFIG.lineWidth.up; - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.globalAlpha = GRAPH_CONFIG.alpha.line; + ctx.moveTo(padding.left, y); + ctx.lineTo(width - padding.right, y); + ctx.stroke(); + } + // 垂直网格线 + const verticalLines = 6; + for (let i = 1; i <= verticalLines; i++) { + const x = padding.left + (effectiveWidth / (verticalLines + 1)) * i; + ctx.beginPath(); + ctx.moveTo(x, padding.top); + ctx.lineTo(x, height - padding.bottom); + ctx.stroke(); + } + + ctx.restore(); + }, + [colors.grid], + ); + + // 绘制流量线条 + const drawTrafficLine = useCallback( + ( + ctx: CanvasRenderingContext2D, + values: number[], + width: number, + height: number, + color: string, + withGradient = false, + data: ITrafficDataPoint[], + ) => { + if (values.length < 2) return; + + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = width - padding.left - padding.right; + + const points = values.map((value, index) => [ + padding.left + (index / (values.length - 1)) * effectiveWidth, + calculateY(value, height, data), + ]); + + ctx.save(); + + // 绘制渐变填充 + if (withGradient && chartStyle === "bezier") { + const gradient = ctx.createLinearGradient( + 0, + padding.top, + 0, + height - padding.bottom, + ); + gradient.addColorStop( + 0, + `${color}${Math.round(GRAPH_CONFIG.alpha.gradient * 255) + .toString(16) + .padStart(2, "0")}`, + ); + gradient.addColorStop(1, `${color}00`); + + ctx.beginPath(); ctx.moveTo(points[0][0], points[0][1]); if (chartStyle === "bezier") { @@ -669,337 +615,359 @@ export const EnhancedCanvasTrafficGraph = memo( } } - ctx.stroke(); - ctx.restore(); - }, - [calculateY, chartStyle], - ); - - // 主绘制函数 - const drawGraph = useCallback(() => { - const canvas = canvasRef.current; - if (!canvas || displayData.length === 0) return; - - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - // Canvas尺寸设置 - const rect = canvas.getBoundingClientRect(); - const dpr = window.devicePixelRatio || 1; - const width = rect.width; - const height = rect.height; - - // 只在尺寸变化时重新设置Canvas - if (canvas.width !== width * dpr || canvas.height !== height * dpr) { - canvas.width = width * dpr; - canvas.height = height * dpr; - ctx.scale(dpr, dpr); - canvas.style.width = width + "px"; - canvas.style.height = height + "px"; + ctx.lineTo(points[points.length - 1][0], height - padding.bottom); + ctx.lineTo(points[0][0], height - padding.bottom); + ctx.closePath(); + ctx.fillStyle = gradient; + ctx.fill(); } - // 清空画布 - ctx.clearRect(0, 0, width, height); + // 绘制主线条 + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = GRAPH_CONFIG.lineWidth.up; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.globalAlpha = GRAPH_CONFIG.alpha.line; - // 绘制Y轴刻度线(背景层) - drawYAxis(ctx, width, height, displayData); + ctx.moveTo(points[0][0], points[0][1]); - // 绘制网格 - drawGrid(ctx, width, height); - - // 绘制时间轴 - drawTimeAxis(ctx, width, height, displayData); - - // 提取流量数据 - const upValues = displayData.map((d) => d.up); - const downValues = displayData.map((d) => d.down); - - // 绘制下载线(背景层) - drawTrafficLine( - ctx, - downValues, - width, - height, - colors.down, - true, - displayData, - ); - - // 绘制上传线(前景层) - drawTrafficLine( - ctx, - upValues, - width, - height, - colors.up, - true, - displayData, - ); - - // 绘制悬浮高亮线 - if (tooltipData.visible && tooltipData.dataIndex >= 0) { - const padding = GRAPH_CONFIG.padding; - const effectiveWidth = width - padding.left - padding.right; - const dataX = - padding.left + - (tooltipData.dataIndex / (displayData.length - 1)) * effectiveWidth; - - ctx.save(); - ctx.strokeStyle = colors.text; - ctx.lineWidth = 1; - ctx.globalAlpha = 0.6; - ctx.setLineDash([4, 4]); // 虚线效果 - - // 绘制垂直指示线 - ctx.beginPath(); - ctx.moveTo(dataX, padding.top); - ctx.lineTo(dataX, height - padding.bottom); - ctx.stroke(); - - // 绘制水平指示线(高亮Y轴位置) - ctx.beginPath(); - ctx.moveTo(padding.left, tooltipData.highlightY); - ctx.lineTo(width - padding.right, tooltipData.highlightY); - ctx.stroke(); - - ctx.restore(); + if (chartStyle === "bezier") { + for (let i = 1; i < points.length; i++) { + const current = points[i]; + const next = points[i + 1] || current; + const controlX = (current[0] + next[0]) / 2; + const controlY = (current[1] + next[1]) / 2; + ctx.quadraticCurveTo(current[0], current[1], controlX, controlY); + } + } else { + for (let i = 1; i < points.length; i++) { + ctx.lineTo(points[i][0], points[i][1]); + } } - isInitializedRef.current = true; - }, [ + ctx.stroke(); + ctx.restore(); + }, + [calculateY, chartStyle], + ); + + // 主绘制函数 + const drawGraph = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas || displayData.length === 0) return; + + const ctx = canvas.getContext("2d"); + if (!ctx) return; + + // Canvas尺寸设置 + const rect = canvas.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + const width = rect.width; + const height = rect.height; + + // 只在尺寸变化时重新设置Canvas + if (canvas.width !== width * dpr || canvas.height !== height * dpr) { + canvas.width = width * dpr; + canvas.height = height * dpr; + ctx.scale(dpr, dpr); + canvas.style.width = width + "px"; + canvas.style.height = height + "px"; + } + + // 清空画布 + ctx.clearRect(0, 0, width, height); + + // 绘制Y轴刻度线(背景层) + drawYAxis(ctx, width, height, displayData); + + // 绘制网格 + drawGrid(ctx, width, height); + + // 绘制时间轴 + drawTimeAxis(ctx, width, height, displayData); + + // 提取流量数据 + const upValues = displayData.map((d) => d.up); + const downValues = displayData.map((d) => d.down); + + // 绘制下载线(背景层) + drawTrafficLine( + ctx, + downValues, + width, + height, + colors.down, + true, displayData, - colors, - drawYAxis, - drawGrid, - drawTimeAxis, - drawTrafficLine, - tooltipData, - ]); - - // 受控的动画循环 - useEffect(() => { - const animate = (currentTime: number) => { - // 控制帧率,减少不必要的重绘 - if ( - currentTime - lastRenderTimeRef.current >= - 1000 / GRAPH_CONFIG.targetFPS - ) { - drawGraph(); - lastRenderTimeRef.current = currentTime; - } - animationFrameRef.current = requestAnimationFrame(animate); - }; - - // 只有在有数据时才开始动画 - if (displayData.length > 0) { - animationFrameRef.current = requestAnimationFrame(animate); - } - - return () => { - if (animationFrameRef.current) { - cancelAnimationFrame(animationFrameRef.current); - } - }; - }, [drawGraph, displayData.length]); - - // 切换时间范围 - const handleTimeRangeClick = useCallback((event: React.MouseEvent) => { - event.stopPropagation(); - setTimeRange((prev) => { - return prev === 1 ? 5 : prev === 5 ? 10 : 1; - }); - }, []); - - // 切换图表样式 - const toggleStyle = useCallback(() => { - setChartStyle((prev) => (prev === "bezier" ? "line" : "bezier")); - }, []); - - // 兼容性方法 - const appendData = useCallback((data: ITrafficItem) => { - console.log( - "[EnhancedCanvasTrafficGraphV2] appendData called (using global data):", - data, - ); - }, []); - - // 暴露方法给父组件 - useImperativeHandle( - ref, - () => ({ - appendData, - toggleStyle, - }), - [appendData, toggleStyle], ); - // 获取时间范围文本 - const getTimeRangeText = useCallback(() => { - return t("{{time}} Minutes", { time: timeRange }); - }, [timeRange, t]); + // 绘制上传线(前景层) + drawTrafficLine(ctx, upValues, width, height, colors.up, true, displayData); - return ( - = 0) { + const padding = GRAPH_CONFIG.padding; + const effectiveWidth = width - padding.left - padding.right; + const dataX = + padding.left + + (tooltipData.dataIndex / (displayData.length - 1)) * effectiveWidth; + + ctx.save(); + ctx.strokeStyle = colors.text; + ctx.lineWidth = 1; + ctx.globalAlpha = 0.6; + ctx.setLineDash([4, 4]); // 虚线效果 + + // 绘制垂直指示线 + ctx.beginPath(); + ctx.moveTo(dataX, padding.top); + ctx.lineTo(dataX, height - padding.bottom); + ctx.stroke(); + + // 绘制水平指示线(高亮Y轴位置) + ctx.beginPath(); + ctx.moveTo(padding.left, tooltipData.highlightY); + ctx.lineTo(width - padding.right, tooltipData.highlightY); + ctx.stroke(); + + ctx.restore(); + } + + isInitializedRef.current = true; + }, [ + displayData, + colors, + drawYAxis, + drawGrid, + drawTimeAxis, + drawTrafficLine, + tooltipData, + ]); + + // 受控的动画循环 + useEffect(() => { + const animate = (currentTime: number) => { + // 控制帧率,减少不必要的重绘 + if ( + currentTime - lastRenderTimeRef.current >= + 1000 / GRAPH_CONFIG.targetFPS + ) { + drawGraph(); + lastRenderTimeRef.current = currentTime; + } + animationFrameRef.current = requestAnimationFrame(animate); + }; + + // 只有在有数据时才开始动画 + if (displayData.length > 0) { + animationFrameRef.current = requestAnimationFrame(animate); + } + + return () => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + }; + }, [drawGraph, displayData.length]); + + // 切换时间范围 + const handleTimeRangeClick = useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + setTimeRange((prev) => { + return prev === 1 ? 5 : prev === 5 ? 10 : 1; + }); + }, []); + + // 切换图表样式 + const toggleStyle = useCallback(() => { + setChartStyle((prev) => (prev === "bezier" ? "line" : "bezier")); + }, []); + + // 兼容性方法 + const appendData = useCallback((data: ITrafficItem) => { + console.log( + "[EnhancedCanvasTrafficGraphV2] appendData called (using global data):", + data, + ); + }, []); + + // 暴露方法给父组件 + useImperativeHandle( + ref, + () => ({ + appendData, + toggleStyle, + }), + [appendData, toggleStyle], + ); + + // 获取时间范围文本 + const getTimeRangeText = useCallback(() => { + return t("{{time}} Minutes", { time: timeRange }); + }, [timeRange, t]); + + return ( + + - + onMouseMove={handleMouseMove} + onMouseLeave={handleMouseLeave} + /> - {/* 控制层覆盖 */} + {/* 控制层覆盖 */} + + {/* 时间范围按钮 */} + + {getTimeRangeText()} + + + {/* 图例 */} - {/* 时间范围按钮 */} + {t("Upload")} + + + {t("Download")} + + + + {/* 样式指示器 */} + + {chartStyle === "bezier" ? "Smooth" : "Linear"} + + + {/* 数据统计指示器(左下角) */} + + Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} | + Compressed: {samplerStats.compressedBufferSize} + + + {/* 悬浮提示框 */} + {tooltipData.visible && ( + - {getTimeRangeText()} - - - {/* 图例 */} - - - {t("Upload")} - - - {t("Download")} - - - - {/* 样式指示器 */} - - {chartStyle === "bezier" ? "Smooth" : "Linear"} - - - {/* 数据统计指示器(左下角) */} - 200 ? "translateX(-100%)" : "translateX(0)", + boxShadow: "0 4px 12px rgba(0,0,0,0.15)", + backdropFilter: "none", + opacity: 1, }} > - Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} | - Compressed: {samplerStats.compressedBufferSize} - - - {/* 悬浮提示框 */} - {tooltipData.visible && ( - 200 ? "translateX(-100%)" : "translateX(0)", - boxShadow: "0 4px 12px rgba(0,0,0,0.15)", - backdropFilter: "none", - opacity: 1, - }} - > - - {tooltipData.timestamp} - - - ↑ {tooltipData.upSpeed} - - - ↓ {tooltipData.downSpeed} - + + {tooltipData.timestamp} - )} - + + ↑ {tooltipData.upSpeed} + + + ↓ {tooltipData.downSpeed} + + + )} - ); - }), -); + + ); +}); EnhancedCanvasTrafficGraph.displayName = "EnhancedCanvasTrafficGraph"; diff --git a/src/components/layout/traffic-graph.tsx b/src/components/layout/traffic-graph.tsx index 9fd6e2a7..82d011b5 100644 --- a/src/components/layout/traffic-graph.tsx +++ b/src/components/layout/traffic-graph.tsx @@ -1,5 +1,5 @@ import { useTheme } from "@mui/material"; -import { forwardRef, useEffect, useImperativeHandle, useRef } from "react"; +import { useEffect, useImperativeHandle, useRef } from "react"; const maxPoint = 30; @@ -24,7 +24,7 @@ export interface TrafficRef { /** * draw the traffic graph */ -export const TrafficGraph = forwardRef((props, ref) => { +export const TrafficGraph = ({ ref, ...props }) => { const countRef = useRef(0); const styleRef = useRef(true); const listRef = useRef(defaultList); @@ -196,4 +196,4 @@ export const TrafficGraph = forwardRef((props, ref) => { }, [palette]); return ; -}); +}; diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index d6c4bbc7..0bafb44b 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -9,13 +9,7 @@ import { TextField, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { - forwardRef, - useEffect, - useImperativeHandle, - useRef, - useState, -} from "react"; +import { useEffect, useImperativeHandle, useRef, useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -38,304 +32,280 @@ export interface ProfileViewerRef { // create or edit the profile // remote / local -export const ProfileViewer = forwardRef( - (props, ref) => { - const { t } = useTranslation(); - const [open, setOpen] = useState(false); - const [openType, setOpenType] = useState<"new" | "edit">("new"); - const [loading, setLoading] = useState(false); - const { profiles } = useProfiles(); +export const ProfileViewer = ({ + ref, + ...props +}: Props & { ref?: React.RefObject }) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const [openType, setOpenType] = useState<"new" | "edit">("new"); + const [loading, setLoading] = useState(false); + const { profiles } = useProfiles(); - // file input - const fileDataRef = useRef(null); + // file input + const fileDataRef = useRef(null); - const { - control, - watch, - register: _register, - ...formIns - } = useForm({ - defaultValues: { - type: "remote", - name: "", - desc: "", - url: "", - option: { - with_proxy: false, - self_proxy: false, - }, + const { + control, + watch, + register: _register, + ...formIns + } = useForm({ + defaultValues: { + type: "remote", + name: "", + desc: "", + url: "", + option: { + with_proxy: false, + self_proxy: false, }, - }); + }, + }); - useImperativeHandle(ref, () => ({ - create: () => { - setOpenType("new"); - setOpen(true); - }, - edit: (item) => { - if (item) { - Object.entries(item).forEach(([key, value]) => { - formIns.setValue(key as any, value); - }); - } - setOpenType("edit"); - setOpen(true); - }, - })); + useImperativeHandle(ref, () => ({ + create: () => { + setOpenType("new"); + setOpen(true); + }, + edit: (item) => { + if (item) { + Object.entries(item).forEach(([key, value]) => { + formIns.setValue(key as any, value); + }); + } + setOpenType("edit"); + setOpen(true); + }, + })); - const selfProxy = watch("option.self_proxy"); - const withProxy = watch("option.with_proxy"); + const selfProxy = watch("option.self_proxy"); + const withProxy = watch("option.with_proxy"); - useEffect(() => { - if (selfProxy) formIns.setValue("option.with_proxy", false); - }, [selfProxy]); + useEffect(() => { + if (selfProxy) formIns.setValue("option.with_proxy", false); + }, [selfProxy]); - useEffect(() => { - if (withProxy) formIns.setValue("option.self_proxy", false); - }, [withProxy]); + useEffect(() => { + if (withProxy) formIns.setValue("option.self_proxy", false); + }, [withProxy]); - const handleOk = useLockFn( - formIns.handleSubmit(async (form) => { - if (form.option?.timeout_seconds) { - form.option.timeout_seconds = +form.option.timeout_seconds; + const handleOk = useLockFn( + formIns.handleSubmit(async (form) => { + if (form.option?.timeout_seconds) { + form.option.timeout_seconds = +form.option.timeout_seconds; + } + + setLoading(true); + try { + // 基本验证 + if (!form.type) throw new Error("`Type` should not be null"); + if (form.type === "remote" && !form.url) { + throw new Error("The URL should not be null"); } - setLoading(true); - try { - // 基本验证 - if (!form.type) throw new Error("`Type` should not be null"); - if (form.type === "remote" && !form.url) { - throw new Error("The URL should not be null"); - } + // 处理表单数据 + if (form.option?.update_interval) { + form.option.update_interval = +form.option.update_interval; + } else { + delete form.option?.update_interval; + } + if (form.option?.user_agent === "") { + delete form.option.user_agent; + } - // 处理表单数据 - if (form.option?.update_interval) { - form.option.update_interval = +form.option.update_interval; + const name = form.name || `${form.type} file`; + const item = { ...form, name }; + const isRemote = form.type === "remote"; + const isUpdate = openType === "edit"; + + // 判断是否是当前激活的配置 + const isActivating = isUpdate && form.uid === (profiles?.current ?? ""); + + // 保存原始代理设置以便回退成功后恢复 + const originalOptions = { + with_proxy: form.option?.with_proxy, + self_proxy: form.option?.self_proxy, + }; + + // 执行创建或更新操作,本地配置不需要回退机制 + if (!isRemote) { + if (openType === "new") { + await createProfile(item, fileDataRef.current); } else { - delete form.option?.update_interval; + if (!form.uid) throw new Error("UID not found"); + await patchProfile(form.uid, item); } - if (form.option?.user_agent === "") { - delete form.option.user_agent; - } - - const name = form.name || `${form.type} file`; - const item = { ...form, name }; - const isRemote = form.type === "remote"; - const isUpdate = openType === "edit"; - - // 判断是否是当前激活的配置 - const isActivating = - isUpdate && form.uid === (profiles?.current ?? ""); - - // 保存原始代理设置以便回退成功后恢复 - const originalOptions = { - with_proxy: form.option?.with_proxy, - self_proxy: form.option?.self_proxy, - }; - - // 执行创建或更新操作,本地配置不需要回退机制 - if (!isRemote) { + } else { + // 远程配置使用回退机制 + try { + // 尝试正常操作 if (openType === "new") { await createProfile(item, fileDataRef.current); } else { if (!form.uid) throw new Error("UID not found"); await patchProfile(form.uid, item); } - } else { - // 远程配置使用回退机制 - try { - // 尝试正常操作 - if (openType === "new") { - await createProfile(item, fileDataRef.current); - } else { - if (!form.uid) throw new Error("UID not found"); - await patchProfile(form.uid, item); - } - } catch { - // 首次创建/更新失败,尝试使用自身代理 - showNotice( - "info", - t("Profile creation failed, retrying with Clash proxy..."), - ); + } catch { + // 首次创建/更新失败,尝试使用自身代理 + showNotice( + "info", + t("Profile creation failed, retrying with Clash proxy..."), + ); - // 使用自身代理的配置 - const retryItem = { - ...item, - option: { - ...item.option, - with_proxy: false, - self_proxy: true, - }, - }; + // 使用自身代理的配置 + const retryItem = { + ...item, + option: { + ...item.option, + with_proxy: false, + self_proxy: true, + }, + }; - // 使用自身代理再次尝试 - if (openType === "new") { - await createProfile(retryItem, fileDataRef.current); - } else { - if (!form.uid) throw new Error("UID not found"); - await patchProfile(form.uid, retryItem); + // 使用自身代理再次尝试 + if (openType === "new") { + await createProfile(retryItem, fileDataRef.current); + } else { + if (!form.uid) throw new Error("UID not found"); + await patchProfile(form.uid, retryItem); - // 编辑模式下恢复原始代理设置 - await patchProfile(form.uid, { option: originalOptions }); - } - - showNotice( - "success", - t("Profile creation succeeded with Clash proxy"), - ); + // 编辑模式下恢复原始代理设置 + await patchProfile(form.uid, { option: originalOptions }); } + + showNotice( + "success", + t("Profile creation succeeded with Clash proxy"), + ); } - - // 成功后的操作 - setOpen(false); - setTimeout(() => formIns.reset(), 500); - fileDataRef.current = null; - - // 优化:UI先关闭,异步通知父组件 - setTimeout(() => { - props.onChange(isActivating); - }, 0); - } catch (err: any) { - showNotice("error", err.message || err.toString()); - } finally { - setLoading(false); } - }), - ); - const handleClose = () => { - try { + // 成功后的操作 setOpen(false); - fileDataRef.current = null; setTimeout(() => formIns.reset(), 500); - } catch (e) { - console.warn("[ProfileViewer] handleClose error:", e); + fileDataRef.current = null; + + // 优化:UI先关闭,异步通知父组件 + setTimeout(() => { + props.onChange(isActivating); + }, 0); + } catch (err: any) { + showNotice("error", err.message || err.toString()); + } finally { + setLoading(false); } - }; + }), + ); - const text = { - fullWidth: true, - size: "small", - margin: "normal", - variant: "outlined", - autoComplete: "off", - autoCorrect: "off", - } as const; + const handleClose = () => { + try { + setOpen(false); + fileDataRef.current = null; + setTimeout(() => formIns.reset(), 500); + } catch (e) { + console.warn("[ProfileViewer] handleClose error:", e); + } + }; - const formType = watch("type"); - const isRemote = formType === "remote"; - const isLocal = formType === "local"; + const text = { + fullWidth: true, + size: "small", + margin: "normal", + variant: "outlined", + autoComplete: "off", + autoCorrect: "off", + } as const; - return ( - - ( - - {t("Type")} - - - )} - /> + const formType = watch("type"); + const isRemote = formType === "remote"; + const isLocal = formType === "local"; - ( - - )} - /> - - ( - - )} - /> - - {isRemote && ( - <> - ( - - )} - /> - - ( - - )} - /> - - ( - - {t("seconds")} - - ), - }, - }} - /> - )} - /> - + return ( + + ( + + {t("Type")} + + )} + /> - {(isRemote || isLocal) && ( + ( + + )} + /> + + ( + + )} + /> + + {isRemote && ( + <> ( + + )} + /> + + ( + + )} + /> + + ( - {t("mins")} + {t("seconds")} ), }, @@ -343,57 +313,79 @@ export const ProfileViewer = forwardRef( /> )} /> - )} + + )} - {isLocal && openType === "new" && ( - { - formIns.setValue("name", formIns.getValues("name") || file.name); - fileDataRef.current = val; - }} + {(isRemote || isLocal) && ( + ( + {t("mins")} + ), + }, + }} + /> + )} + /> + )} + + {isLocal && openType === "new" && ( + { + formIns.setValue("name", formIns.getValues("name") || file.name); + fileDataRef.current = val; + }} + /> + )} + + {isRemote && ( + <> + ( + + {t("Use System Proxy")} + + + )} /> - )} - {isRemote && ( - <> - ( - - {t("Use System Proxy")} - - - )} - /> + ( + + {t("Use Clash Proxy")} + + + )} + /> - ( - - {t("Use Clash Proxy")} - - - )} - /> - - ( - - {t("Accept Invalid Certs (Danger)")} - - - )} - /> - - )} - - ); - }, -); + ( + + {t("Accept Invalid Certs (Danger)")} + + + )} + /> + + )} + + ); +}; const StyledBox = styled(Box)(() => ({ margin: "8px 0 8px 8px", diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index 476f5f90..33e6b553 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -1,23 +1,19 @@ +import { ExpandMoreRounded } from "@mui/icons-material"; import { Box, Snackbar, Alert, Chip, - Stack, Typography, IconButton, - Collapse, Menu, MenuItem, - Divider, - Button, } from "@mui/material"; -import { ArchiveOutlined, ExpandMoreRounded } from "@mui/icons-material"; import { useLockFn } from "ahooks"; import { useRef, useState, useEffect, useCallback, useMemo } from "react"; -import useSWR from "swr"; import { useTranslation } from "react-i18next"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; +import useSWR from "swr"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; @@ -34,8 +30,8 @@ import { BaseEmpty } from "../base"; import { ScrollTopButton } from "../layout/scroll-top-button"; import { ProxyChain } from "./proxy-chain"; -import { ProxyRender } from "./proxy-render"; import { ProxyGroupNavigator } from "./proxy-group-navigator"; +import { ProxyRender } from "./proxy-render"; import { useRenderList } from "./use-render-list"; interface Props { diff --git a/src/components/setting/mods/backup-viewer.tsx b/src/components/setting/mods/backup-viewer.tsx index 804f3541..8c03e4c7 100644 --- a/src/components/setting/mods/backup-viewer.tsx +++ b/src/components/setting/mods/backup-viewer.tsx @@ -1,16 +1,10 @@ import { Box, Paper, Divider } from "@mui/material"; import dayjs from "dayjs"; import customParseFormat from "dayjs/plugin/customParseFormat"; -import { - forwardRef, - useImperativeHandle, - useState, - useCallback, - useMemo, -} from "react"; +import { useImperativeHandle, useState, useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef } from "@/components/base"; +import { BaseDialog } from "@/components/base"; import { BaseLoadingOverlay } from "@/components/base"; import { listWebDavBackup } from "@/services/cmds"; @@ -25,7 +19,7 @@ dayjs.extend(customParseFormat); const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss"; const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/; -export const BackupViewer = forwardRef((props, ref) => { +export const BackupViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -131,4 +125,4 @@ export const BackupViewer = forwardRef((props, ref) => { ); -}); +}; diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx index 869b5784..f41626de 100644 --- a/src/components/setting/mods/clash-core-viewer.tsx +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -12,11 +12,11 @@ import { ListItemText, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import { mutate } from "swr"; -import { BaseDialog, DialogRef } from "@/components/base"; +import { BaseDialog } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; import { changeClashCore, restartCore } from "@/services/cmds"; import { @@ -31,7 +31,7 @@ const VALID_CORE = [ { name: "Mihomo Alpha", core: "verge-mihomo-alpha", chip: "Alpha Version" }, ]; -export const ClashCoreViewer = forwardRef((props, ref) => { +export const ClashCoreViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const { verge, mutateVerge } = useVerge(); @@ -169,4 +169,4 @@ export const ClashCoreViewer = forwardRef((props, ref) => { ); -}); +}; diff --git a/src/components/setting/mods/clash-port-viewer.tsx b/src/components/setting/mods/clash-port-viewer.tsx index ec495804..e0db3184 100644 --- a/src/components/setting/mods/clash-port-viewer.tsx +++ b/src/components/setting/mods/clash-port-viewer.tsx @@ -9,7 +9,7 @@ import { TextField, } from "@mui/material"; import { useLockFn, useRequest } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, Switch } from "@/components/base"; @@ -30,10 +30,12 @@ interface ClashPortViewerRef { const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025; -export const ClashPortViewer = forwardRef< - ClashPortViewerRef, - ClashPortViewerProps ->((props, ref) => { +export const ClashPortViewer = ({ + ref, + ...props +}: ClashPortViewerProps & { + ref?: React.RefObject; +}) => { const { t } = useTranslation(); const { clashInfo, patchInfo } = useClashInfo(); const { verge, patchVerge } = useVerge(); @@ -348,4 +350,4 @@ export const ClashPortViewer = forwardRef< ); -}); +}; diff --git a/src/components/setting/mods/config-viewer.tsx b/src/components/setting/mods/config-viewer.tsx index 26609caa..e68030f9 100644 --- a/src/components/setting/mods/config-viewer.tsx +++ b/src/components/setting/mods/config-viewer.tsx @@ -1,12 +1,11 @@ import { Box, Chip } from "@mui/material"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { DialogRef } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { getRuntimeYaml } from "@/services/cmds"; -export const ConfigViewer = forwardRef((_, ref) => { +export const ConfigViewer = ({ ref, ..._ }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [runtimeConfig, setRuntimeConfig] = useState(""); @@ -38,4 +37,4 @@ export const ConfigViewer = forwardRef((_, ref) => { onClose={() => setOpen(false)} /> ); -}); +}; diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx index f39b928b..154ff523 100644 --- a/src/components/setting/mods/controller-viewer.tsx +++ b/src/components/setting/mods/controller-viewer.tsx @@ -12,15 +12,15 @@ import { Tooltip, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; import { showNotice } from "@/services/noticeService"; -export const ControllerViewer = forwardRef((props, ref) => { +export const ControllerViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [copySuccess, setCopySuccess] = useState(null); @@ -217,4 +217,4 @@ export const ControllerViewer = forwardRef((props, ref) => { ); -}); +}; diff --git a/src/components/setting/mods/dns-viewer.tsx b/src/components/setting/mods/dns-viewer.tsx index f076dbc0..6f3cf071 100644 --- a/src/components/setting/mods/dns-viewer.tsx +++ b/src/components/setting/mods/dns-viewer.tsx @@ -15,11 +15,11 @@ import { import { invoke } from "@tauri-apps/api/core"; import { useLockFn } from "ahooks"; import yaml from "js-yaml"; -import { forwardRef, useImperativeHandle, useState, useEffect } from "react"; +import { useImperativeHandle, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import MonacoEditor from "react-monaco-editor"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; import { showNotice } from "@/services/noticeService"; import { useThemeMode } from "@/services/states"; @@ -87,7 +87,7 @@ const DEFAULT_DNS_CONFIG = { }, }; -export const DnsViewer = forwardRef((props, ref) => { +export const DnsViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const { clash, mutateClash } = useClash(); const themeMode = useThemeMode(); @@ -1034,4 +1034,4 @@ export const DnsViewer = forwardRef((props, ref) => { )} ); -}); +}; diff --git a/src/components/setting/mods/external-controller-cors.tsx b/src/components/setting/mods/external-controller-cors.tsx index d2522f35..355e316e 100644 --- a/src/components/setting/mods/external-controller-cors.tsx +++ b/src/components/setting/mods/external-controller-cors.tsx @@ -1,7 +1,7 @@ import { Delete as DeleteIcon } from "@mui/icons-material"; import { Box, Button, Divider, List, ListItem, TextField } from "@mui/material"; import { useLockFn, useRequest } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import { BaseDialog, Switch } from "@/components/base"; @@ -71,201 +71,194 @@ interface ClashHeaderConfigingRef { close: () => void; } -export const HeaderConfiguration = forwardRef( - (props, ref) => { - const { t } = useTranslation(); - const { clash, mutateClash, patchClash } = useClash(); - const [open, setOpen] = useState(false); +export const HeaderConfiguration = ({ ref, ...props }) => { + const { t } = useTranslation(); + const { clash, mutateClash, patchClash } = useClash(); + const [open, setOpen] = useState(false); - // CORS配置状态管理 - const [corsConfig, setCorsConfig] = useState<{ - allowPrivateNetwork: boolean; - allowOrigins: string[]; - }>(() => { + // CORS配置状态管理 + const [corsConfig, setCorsConfig] = useState<{ + allowPrivateNetwork: boolean; + allowOrigins: string[]; + }>(() => { + const cors = clash?.["external-controller-cors"]; + const origins = cors?.["allow-origins"] ?? []; + return { + allowPrivateNetwork: cors?.["allow-private-network"] ?? true, + allowOrigins: filterBaseOriginsForUI(origins), + }; + }); + + // 处理CORS配置变更 + const handleCorsConfigChange = ( + key: "allowPrivateNetwork" | "allowOrigins", + value: boolean | string[], + ) => { + setCorsConfig((prev) => ({ + ...prev, + [key]: value, + })); + }; + + // 添加新的允许来源 + const handleAddOrigin = () => { + handleCorsConfigChange("allowOrigins", [...corsConfig.allowOrigins, ""]); + }; + + // 更新允许来源列表中的某一项 + const handleUpdateOrigin = (index: number, value: string) => { + const newOrigins = [...corsConfig.allowOrigins]; + newOrigins[index] = value; + handleCorsConfigChange("allowOrigins", newOrigins); + }; + + // 删除允许来源列表中的某一项 + const handleDeleteOrigin = (index: number) => { + const newOrigins = [...corsConfig.allowOrigins]; + newOrigins.splice(index, 1); + handleCorsConfigChange("allowOrigins", newOrigins); + }; + + // 保存配置请求 + const { loading, run: saveConfig } = useRequest( + async () => { + // 保存时使用完整的源列表(包括开发URL) + const fullOrigins = getFullOrigins(corsConfig.allowOrigins); + + await patchClash({ + "external-controller-cors": { + "allow-private-network": corsConfig.allowPrivateNetwork, + "allow-origins": fullOrigins.filter( + (origin: string) => origin.trim() !== "", + ), + }, + }); + await mutateClash(); + }, + { + manual: true, + onSuccess: () => { + setOpen(false); + showNotice("success", t("Configuration saved successfully")); + }, + onError: () => { + showNotice("error", t("Failed to save configuration")); + }, + }, + ); + + useImperativeHandle(ref, () => ({ + open: () => { const cors = clash?.["external-controller-cors"]; const origins = cors?.["allow-origins"] ?? []; - return { + setCorsConfig({ allowPrivateNetwork: cors?.["allow-private-network"] ?? true, allowOrigins: filterBaseOriginsForUI(origins), - }; - }); + }); + setOpen(true); + }, + close: () => setOpen(false), + })); - // 处理CORS配置变更 - const handleCorsConfigChange = ( - key: "allowPrivateNetwork" | "allowOrigins", - value: boolean | string[], - ) => { - setCorsConfig((prev) => ({ - ...prev, - [key]: value, - })); - }; + const handleSave = useLockFn(async () => { + await saveConfig(); + }); - // 添加新的允许来源 - const handleAddOrigin = () => { - handleCorsConfigChange("allowOrigins", [...corsConfig.allowOrigins, ""]); - }; + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + onOk={handleSave} + > + + + + + {t("Allow private network access")} + + + handleCorsConfigChange("allowPrivateNetwork", e.target.checked) + } + /> + + - // 更新允许来源列表中的某一项 - const handleUpdateOrigin = (index: number, value: string) => { - const newOrigins = [...corsConfig.allowOrigins]; - newOrigins[index] = value; - handleCorsConfigChange("allowOrigins", newOrigins); - }; - - // 删除允许来源列表中的某一项 - const handleDeleteOrigin = (index: number) => { - const newOrigins = [...corsConfig.allowOrigins]; - newOrigins.splice(index, 1); - handleCorsConfigChange("allowOrigins", newOrigins); - }; - - // 保存配置请求 - const { loading, run: saveConfig } = useRequest( - async () => { - // 保存时使用完整的源列表(包括开发URL) - const fullOrigins = getFullOrigins(corsConfig.allowOrigins); - - await patchClash({ - "external-controller-cors": { - "allow-private-network": corsConfig.allowPrivateNetwork, - "allow-origins": fullOrigins.filter( - (origin: string) => origin.trim() !== "", - ), - }, - }); - await mutateClash(); - }, - { - manual: true, - onSuccess: () => { - setOpen(false); - showNotice("success", t("Configuration saved successfully")); - }, - onError: () => { - showNotice("error", t("Failed to save configuration")); - }, - }, - ); - - useImperativeHandle(ref, () => ({ - open: () => { - const cors = clash?.["external-controller-cors"]; - const origins = cors?.["allow-origins"] ?? []; - setCorsConfig({ - allowPrivateNetwork: cors?.["allow-private-network"] ?? true, - allowOrigins: filterBaseOriginsForUI(origins), - }); - setOpen(true); - }, - close: () => setOpen(false), - })); - - const handleSave = useLockFn(async () => { - await saveConfig(); - }); - - return ( - setOpen(false)} - onCancel={() => setOpen(false)} - onOk={handleSave} - > - - - - - {t("Allow private network access")} - - - handleCorsConfigChange( - "allowPrivateNetwork", - e.target.checked, - ) - } - /> - - - - - - -
-
- {t("Allowed Origins")} -
- {corsConfig.allowOrigins.map((origin, index) => ( -
- handleUpdateOrigin(index, e.target.value)} - placeholder={t("Please enter a valid url")} - inputProps={{ style: { fontSize: 14 } }} - /> - -
- ))} - + + +
+
+ {t("Allowed Origins")} +
+ {corsConfig.allowOrigins.map((origin, index) => (
-
handleUpdateOrigin(index, e.target.value)} + placeholder={t("Please enter a valid url")} + inputProps={{ style: { fontSize: 14 } }} + /> +
+ + +
+ ))} + + +
+
+ {t("Always included origins: {{urls}}", { + urls: DEV_URLS.join(", "), + })}
- - - - ); - }, -); +
+
+ + + ); +}; diff --git a/src/components/setting/mods/hotkey-viewer.tsx b/src/components/setting/mods/hotkey-viewer.tsx index cd09e244..8ea690fa 100644 --- a/src/components/setting/mods/hotkey-viewer.tsx +++ b/src/components/setting/mods/hotkey-viewer.tsx @@ -1,9 +1,9 @@ import { styled, Typography } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; import { showNotice } from "@/services/noticeService"; @@ -26,7 +26,7 @@ const HOTKEY_FUNC = [ "entry_lightweight_mode", ]; -export const HotkeyViewer = forwardRef((props, ref) => { +export const HotkeyViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -117,4 +117,4 @@ export const HotkeyViewer = forwardRef((props, ref) => { ))} ); -}); +}; diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 5e0f1e25..80951552 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -12,10 +12,10 @@ import { convertFileSrc } from "@tauri-apps/api/core"; import { join } from "@tauri-apps/api/path"; import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { exists } from "@tauri-apps/plugin-fs"; -import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; +import { useEffect, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useVerge } from "@/hooks/use-verge"; import { copyIconFile, getAppDir } from "@/services/cmds"; @@ -38,7 +38,7 @@ const getIcons = async (icon_dir: string, name: string) => { }; }; -export const LayoutViewer = forwardRef((props, ref) => { +export const LayoutViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const { verge, patchVerge, mutateVerge } = useVerge(); @@ -387,7 +387,7 @@ export const LayoutViewer = forwardRef((props, ref) => { ); -}); +}; const Item = styled(ListItem)(() => ({ padding: "5px 2px", diff --git a/src/components/setting/mods/lite-mode-viewer.tsx b/src/components/setting/mods/lite-mode-viewer.tsx index 6c243667..30f534a2 100644 --- a/src/components/setting/mods/lite-mode-viewer.tsx +++ b/src/components/setting/mods/lite-mode-viewer.tsx @@ -7,16 +7,16 @@ import { InputAdornment, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useVerge } from "@/hooks/use-verge"; import { entry_lightweight_mode } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; -export const LiteModeViewer = forwardRef((props, ref) => { +export const LiteModeViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const { verge, patchVerge } = useVerge(); @@ -143,4 +143,4 @@ export const LiteModeViewer = forwardRef((props, ref) => { ); -}); +}; diff --git a/src/components/setting/mods/misc-viewer.tsx b/src/components/setting/mods/misc-viewer.tsx index b0048fd8..cee02882 100644 --- a/src/components/setting/mods/misc-viewer.tsx +++ b/src/components/setting/mods/misc-viewer.tsx @@ -8,15 +8,15 @@ import { TextField, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useVerge } from "@/hooks/use-verge"; import { showNotice } from "@/services/noticeService"; -export const MiscViewer = forwardRef((props, ref) => { +export const MiscViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const { verge, patchVerge } = useVerge(); @@ -319,4 +319,4 @@ export const MiscViewer = forwardRef((props, ref) => { ); -}); +}; diff --git a/src/components/setting/mods/network-interface-viewer.tsx b/src/components/setting/mods/network-interface-viewer.tsx index 912529eb..5b7d0422 100644 --- a/src/components/setting/mods/network-interface-viewer.tsx +++ b/src/components/setting/mods/network-interface-viewer.tsx @@ -1,15 +1,15 @@ import { ContentCopyRounded } from "@mui/icons-material"; import { alpha, Box, Button, IconButton } from "@mui/material"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; -import { BaseDialog, DialogRef } from "@/components/base"; +import { BaseDialog } from "@/components/base"; import { getNetworkInterfacesInfo } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; -export const NetworkInterfaceViewer = forwardRef((props, ref) => { +export const NetworkInterfaceViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [isV4, setIsV4] = useState(true); @@ -99,7 +99,7 @@ export const NetworkInterfaceViewer = forwardRef((props, ref) => { ))} ); -}); +}; const AddressDisplay = (props: { label: string; content: string }) => { const { t } = useTranslation(); diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index 329d857e..9649a3f6 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -11,25 +11,19 @@ import { Typography, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { - forwardRef, - useEffect, - useImperativeHandle, - useMemo, - useState, -} from "react"; +import { useEffect, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import useSWR, { mutate } from "swr"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { BaseFieldset } from "@/components/base/base-fieldset"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-provider"; -import { getClashConfig } from "@/services/cmds"; import { getAutotemProxy, + getClashConfig, getNetworkInterfacesInfo, getSystemHostname, getSystemProxy, @@ -75,7 +69,7 @@ const getValidReg = (isWindows: boolean) => { return new RegExp(rValid); }; -export const SysproxyViewer = forwardRef((props, ref) => { +export const SysproxyViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const isWindows = getSystem() === "windows"; const validReg = useMemo(() => getValidReg(isWindows), [isWindows]); @@ -619,7 +613,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { ); -}); +}; const FlexBox = styled("div")` display: flex; diff --git a/src/components/setting/mods/theme-viewer.tsx b/src/components/setting/mods/theme-viewer.tsx index e3f764c5..f59fae53 100644 --- a/src/components/setting/mods/theme-viewer.tsx +++ b/src/components/setting/mods/theme-viewer.tsx @@ -9,16 +9,16 @@ import { useTheme, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef } from "@/components/base"; +import { BaseDialog } from "@/components/base"; import { EditorViewer } from "@/components/profile/editor-viewer"; import { useVerge } from "@/hooks/use-verge"; import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; import { showNotice } from "@/services/noticeService"; -export const ThemeViewer = forwardRef((props, ref) => { +export const ThemeViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -144,7 +144,7 @@ export const ThemeViewer = forwardRef((props, ref) => { ); -}); +}; const Item = styled(ListItem)(() => ({ padding: "5px 2px", diff --git a/src/components/setting/mods/tun-viewer.tsx b/src/components/setting/mods/tun-viewer.tsx index cd361cc9..d3667573 100644 --- a/src/components/setting/mods/tun-viewer.tsx +++ b/src/components/setting/mods/tun-viewer.tsx @@ -8,10 +8,10 @@ import { TextField, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, DialogRef, Switch } from "@/components/base"; +import { BaseDialog, Switch } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; import { enhanceProfiles } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; @@ -21,7 +21,7 @@ import { StackModeSwitch } from "./stack-mode-switch"; const OS = getSystem(); -export const TunViewer = forwardRef((props, ref) => { +export const TunViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const { clash, mutateClash, patchClash } = useClash(); @@ -238,4 +238,4 @@ export const TunViewer = forwardRef((props, ref) => { ); -}); +}; diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index 3b67e175..a2fa367e 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -4,24 +4,18 @@ import { relaunch } from "@tauri-apps/plugin-process"; import { open as openUrl } from "@tauri-apps/plugin-shell"; import { check as checkUpdate } from "@tauri-apps/plugin-updater"; import { useLockFn } from "ahooks"; -import { - forwardRef, - useImperativeHandle, - useState, - useMemo, - useEffect, -} from "react"; +import { useImperativeHandle, useState, useMemo, useEffect } from "react"; import { useTranslation } from "react-i18next"; import ReactMarkdown from "react-markdown"; import useSWR from "swr"; -import { BaseDialog, DialogRef } from "@/components/base"; +import { BaseDialog } from "@/components/base"; import { useListen } from "@/hooks/use-listen"; import { portableFlag } from "@/pages/_layout"; import { showNotice } from "@/services/noticeService"; import { useUpdateState, useSetUpdateState } from "@/services/states"; -export const UpdateViewer = forwardRef((props, ref) => { +export const UpdateViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); @@ -167,4 +161,4 @@ export const UpdateViewer = forwardRef((props, ref) => { )} ); -}); +}; diff --git a/src/components/setting/mods/web-ui-viewer.tsx b/src/components/setting/mods/web-ui-viewer.tsx index e82035fc..655be1e9 100644 --- a/src/components/setting/mods/web-ui-viewer.tsx +++ b/src/components/setting/mods/web-ui-viewer.tsx @@ -1,9 +1,9 @@ import { Button, Box, Typography } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; -import { BaseDialog, BaseEmpty, DialogRef } from "@/components/base"; +import { BaseDialog, BaseEmpty } from "@/components/base"; import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; import { openWebUrl } from "@/services/cmds"; @@ -11,7 +11,7 @@ import { showNotice } from "@/services/noticeService"; import { WebUIItem } from "./web-ui-item"; -export const WebUIViewer = forwardRef((props, ref) => { +export const WebUIViewer = ({ ref, ...props }) => { const { t } = useTranslation(); const { clashInfo } = useClashInfo(); @@ -139,4 +139,4 @@ export const WebUIViewer = forwardRef((props, ref) => { )} ); -}); +}; diff --git a/src/components/test/test-viewer.tsx b/src/components/test/test-viewer.tsx index 0962c424..56453bb9 100644 --- a/src/components/test/test-viewer.tsx +++ b/src/components/test/test-viewer.tsx @@ -1,7 +1,7 @@ import { TextField } from "@mui/material"; import { useLockFn } from "ahooks"; import { nanoid } from "nanoid"; -import { forwardRef, useImperativeHandle, useState } from "react"; +import { useImperativeHandle, useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { useTranslation } from "react-i18next"; @@ -19,7 +19,10 @@ export interface TestViewerRef { } // create or edit the test item -export const TestViewer = forwardRef((props, ref) => { +export const TestViewer = ({ + ref, + ...props +}: Props & { ref?: React.RefObject }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [openType, setOpenType] = useState<"new" | "edit">("new"); @@ -173,4 +176,4 @@ export const TestViewer = forwardRef((props, ref) => { /> ); -}); +}; diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 16ad3962..8abd1380 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -1,11 +1,5 @@ import { listen } from "@tauri-apps/api/event"; -import React, { - createContext, - useContext, - useEffect, - useMemo, - useRef, -} from "react"; +import React, { createContext, use, useEffect, useMemo, useRef } from "react"; import useSWR from "swr"; import { useClashInfo } from "@/hooks/use-clash"; @@ -589,14 +583,12 @@ export const AppDataProvider = ({ refreshAll, ]); - return ( - {children} - ); + return {children}; }; // 自定义Hook访问全局数据 export const useAppData = () => { - const context = useContext(AppDataContext); + const context = use(AppDataContext); if (!context) { throw new Error("useAppData必须在AppDataProvider内使用"); diff --git a/src/providers/chain-proxy-provider.tsx b/src/providers/chain-proxy-provider.tsx index 2b834f89..c8ccce1d 100644 --- a/src/providers/chain-proxy-provider.tsx +++ b/src/providers/chain-proxy-provider.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useCallback, useContext, useState } from "react"; +import React, { createContext, useCallback, use, useState } from "react"; interface ChainProxyContextType { isChainMode: boolean; @@ -26,7 +26,7 @@ export const ChainProxyProvider = ({ }, []); return ( - {children} - + ); }; export const useChainProxy = () => { - const context = useContext(ChainProxyContext); + const context = use(ChainProxyContext); if (!context) { throw new Error("useChainProxy must be used within a ChainProxyProvider"); }