diff --git a/src/App.tsx b/src/App.tsx index 7e5202642..20276be8f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,8 @@ import { ConnectConfirmDialog } from "./components/connect"; import { showRegistryLoadError } from "./lib/utils/connectError"; import { useDeepLink } from "./hooks/useDeepLink"; import { useRelayRegistry } from "./hooks/useRelayRegistry"; +import { ComponentDebugProvider } from "./contexts/ComponentDebugContext"; +import { ComponentDebugOverlay } from "./components/dev"; /** * 页面类型定义 @@ -229,24 +231,28 @@ function App() { // 4. 正常主界面 return ( - - - {renderPage()} - {/* ProxyCast Connect 确认弹窗 */} - {/* _Requirements: 5.2_ */} - - + + + + {renderPage()} + {/* ProxyCast Connect 确认弹窗 */} + {/* _Requirements: 5.2_ */} + + {/* 组件视图调试覆盖层 */} + + + ); } diff --git a/src/components/dev/ComponentDebugOverlay.tsx b/src/components/dev/ComponentDebugOverlay.tsx new file mode 100644 index 000000000..c433d3ccb --- /dev/null +++ b/src/components/dev/ComponentDebugOverlay.tsx @@ -0,0 +1,310 @@ +/** + * @file ComponentDebugOverlay.tsx + * @description 组件视图调试覆盖层 - Alt+悬浮显示轮廓,Alt+点击显示组件信息 + */ +import { useEffect, useCallback, useState } from "react"; +import { useComponentDebug, ComponentInfo } from "@/contexts/ComponentDebugContext"; +import { X, Copy, Check, Component, FileCode, Layers, Hash } from "lucide-react"; + +/** + * 从 React Fiber 节点获取组件信息 + */ +function getReactFiberInfo(element: HTMLElement): Partial | null { + const fiberKey = Object.keys(element).find( + (key) => key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$") + ); + + if (!fiberKey) return null; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let fiber = (element as any)[fiberKey]; + if (!fiber) return null; + + let depth = 0; + while (fiber) { + const type = fiber.type; + if (type && typeof type === "function") { + const name = type.displayName || type.name || "Anonymous"; + if (!name.startsWith("_") && name !== "Anonymous") { + let filePath = "未知路径"; + if (fiber._debugSource) { + filePath = `${fiber._debugSource.fileName}:${fiber._debugSource.lineNumber}`; + } else if (type._source) { + filePath = `${type._source.fileName}:${type._source.lineNumber}`; + } + + const props = fiber.memoizedProps || {}; + const safeProps: Record = {}; + for (const key of Object.keys(props)) { + const value = props[key]; + if (typeof value === "function") { + safeProps[key] = "[Function]"; + } else if (typeof value === "object" && value !== null) { + if (Array.isArray(value)) { + safeProps[key] = `[Array(${value.length})]`; + } else if (value.$$typeof) { + safeProps[key] = "[ReactElement]"; + } else { + safeProps[key] = "[Object]"; + } + } else { + safeProps[key] = value; + } + } + + return { name, filePath, props: safeProps, depth }; + } + } + fiber = fiber.return; + depth++; + } + return null; +} + +/** 组件信息弹窗 */ +function ComponentInfoPopup() { + const { componentInfo, hideComponentInfo } = useComponentDebug(); + const [copiedField, setCopiedField] = useState(null); + + if (!componentInfo) return null; + + const handleCopy = async (text: string, field: string) => { + await navigator.clipboard.writeText(text); + setCopiedField(field); + setTimeout(() => setCopiedField(null), 2000); + }; + + const propsEntries = Object.entries(componentInfo.props || {}).filter( + ([key]) => key !== "children" + ); + + return ( +
e.stopPropagation()} + > + {/* 标题栏 */} +
+
+ + 组件信息 +
+ +
+ + {/* 内容区域 */} +
+ {/* 组件名称 */} +
+ +
+
组件名称
+
+ {componentInfo.name} + +
+
+
+ + {/* 文件路径 */} +
+ +
+
文件路径
+
+ {componentInfo.filePath} + +
+
+
+ + {/* HTML 标签 */} +
+ +
+
DOM 元素
+ <{componentInfo.tagName.toLowerCase()}> +
+
+ + {/* 组件层级 */} +
+ +
+
组件层级
+ 第 {componentInfo.depth} 层 +
+
+ + {/* Props */} + {propsEntries.length > 0 && ( +
+
Props
+
+
+ {propsEntries.slice(0, 10).map(([key, value]) => ( +
+ {key}: + + {typeof value === "string" ? `"${value}"` : String(value)} + +
+ ))} + {propsEntries.length > 10 && ( +
... 还有 {propsEntries.length - 10} 个属性
+ )} +
+
+
+ )} +
+ + {/* 底部提示 */} +
+

提示: 按 Esc 或点击其他区域关闭

+
+
+ ); +} + +/** 调试交互处理 */ +function DebugInteractionHandler() { + const { enabled, showComponentInfo, hideComponentInfo } = useComponentDebug(); + const [altPressed, setAltPressed] = useState(false); + const [hoveredElement, setHoveredElement] = useState(null); + + // 监听 Alt 键 + useEffect(() => { + if (!enabled) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Alt") { + setAltPressed(true); + } + if (e.key === "Escape") { + hideComponentInfo(); + } + }; + + const handleKeyUp = (e: KeyboardEvent) => { + if (e.key === "Alt") { + setAltPressed(false); + setHoveredElement(null); + } + }; + + // 窗口失焦时重置状态 + const handleBlur = () => { + setAltPressed(false); + setHoveredElement(null); + }; + + window.addEventListener("keydown", handleKeyDown); + window.addEventListener("keyup", handleKeyUp); + window.addEventListener("blur", handleBlur); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + window.removeEventListener("keyup", handleKeyUp); + window.removeEventListener("blur", handleBlur); + }; + }, [enabled, hideComponentInfo]); + + // 监听鼠标移动(仅在 Alt 按下时) + useEffect(() => { + if (!enabled || !altPressed) { + setHoveredElement(null); + return; + } + + const handleMouseMove = (e: MouseEvent) => { + const target = e.target as HTMLElement; + // 忽略弹窗内的元素 + if (target.closest(".component-debug-popup")) return; + setHoveredElement(target); + }; + + document.addEventListener("mousemove", handleMouseMove); + return () => document.removeEventListener("mousemove", handleMouseMove); + }, [enabled, altPressed]); + + // 监听点击(仅在 Alt 按下时) + useEffect(() => { + if (!enabled) return; + + const handleClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; + + // 点击弹窗外部关闭弹窗 + if (!target.closest(".component-debug-popup")) { + if (!e.altKey) { + hideComponentInfo(); + return; + } + + // Alt + 点击显示组件信息 + e.preventDefault(); + e.stopPropagation(); + + const fiberInfo = getReactFiberInfo(target); + showComponentInfo({ + name: fiberInfo?.name || "DOM Element", + filePath: fiberInfo?.filePath || "非 React 组件", + props: fiberInfo?.props || {}, + depth: fiberInfo?.depth || 0, + tagName: target.tagName, + x: e.clientX + 10, + y: e.clientY + 10, + }); + } + }; + + document.addEventListener("click", handleClick, true); + return () => document.removeEventListener("click", handleClick, true); + }, [enabled, showComponentInfo, hideComponentInfo]); + + // 渲染悬浮高亮框 + if (!altPressed || !hoveredElement) return null; + + const rect = hoveredElement.getBoundingClientRect(); + + return ( +
+ ); +} + +export function ComponentDebugOverlay() { + const { enabled } = useComponentDebug(); + + if (!enabled) return null; + + return ( + <> + +
+ +
+ + ); +} diff --git a/src/components/dev/index.ts b/src/components/dev/index.ts new file mode 100644 index 000000000..02604df1c --- /dev/null +++ b/src/components/dev/index.ts @@ -0,0 +1 @@ +export { ComponentDebugOverlay } from "./ComponentDebugOverlay"; diff --git a/src/components/settings/DeveloperSettings.tsx b/src/components/settings/DeveloperSettings.tsx new file mode 100644 index 000000000..8da4d5a94 --- /dev/null +++ b/src/components/settings/DeveloperSettings.tsx @@ -0,0 +1,50 @@ +/** + * @file DeveloperSettings.tsx + * @description 开发者设置页面 - 组件视图调试等开发工具 + */ +import { Code2, Eye } from "lucide-react"; +import { Switch } from "@/components/ui/switch"; +import { useComponentDebug } from "@/contexts/ComponentDebugContext"; + +export function DeveloperSettings() { + const { enabled, setEnabled } = useComponentDebug(); + + return ( +
+ {/* 标题 */} +
+ +

开发者工具

+
+ + {/* 组件视图调试 */} +
+
+
+
+ +
+
+

组件视图调试

+

+ 显示组件轮廓,Alt+点击查看组件信息 +

+
+
+ +
+ + {enabled && ( +
+

使用说明:

+
    +
  • 所有标记的组件会显示蓝色虚线轮廓
  • +
  • 鼠标悬浮时轮廓高亮显示
  • +
  • 按住 Alt 键点击组件可查看名称和文件路径
  • +
+
+ )} +
+
+ ); +} diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 5bce0e1e9..2e80a7c30 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -7,14 +7,16 @@ import { TlsSettings } from "./TlsSettings"; import { QuotaSettings } from "./QuotaSettings"; import { RemoteManagementSettings } from "./RemoteManagementSettings"; import { ExtensionsSettings } from "./ExtensionsSettings"; +import { DeveloperSettings } from "./DeveloperSettings"; -type SettingsTab = "general" | "security" | "advanced" | "extensions" | "about"; +type SettingsTab = "general" | "security" | "advanced" | "extensions" | "developer" | "about"; const tabs: { id: SettingsTab; label: string; experimental?: boolean }[] = [ { id: "general", label: "通用" }, { id: "security", label: "安全" }, { id: "advanced", label: "高级" }, { id: "extensions", label: "扩展", experimental: true }, + { id: "developer", label: "开发者" }, { id: "about", label: "关于" }, ]; @@ -68,6 +70,7 @@ export function SettingsPage() {
)} {activeTab === "extensions" && } + {activeTab === "developer" && } {activeTab === "about" && } diff --git a/src/components/settings/index.ts b/src/components/settings/index.ts index 375c40edf..78805f052 100644 --- a/src/components/settings/index.ts +++ b/src/components/settings/index.ts @@ -6,3 +6,4 @@ export { AboutSection } from "./AboutSection"; export { TlsSettings } from "./TlsSettings"; export { QuotaSettings } from "./QuotaSettings"; export { RemoteManagementSettings } from "./RemoteManagementSettings"; +export { DeveloperSettings } from "./DeveloperSettings"; diff --git a/src/contexts/ComponentDebugContext.tsx b/src/contexts/ComponentDebugContext.tsx new file mode 100644 index 000000000..ddc7e9ec0 --- /dev/null +++ b/src/contexts/ComponentDebugContext.tsx @@ -0,0 +1,75 @@ +/** + * @file ComponentDebugContext.tsx + * @description 组件视图调试上下文 - 提供全局组件轮廓显示和信息查看功能 + */ +import { createContext, useContext, useState, useCallback, ReactNode } from "react"; + +export interface ComponentInfo { + name: string; + filePath: string; + props: Record; + depth: number; + tagName: string; + x: number; + y: number; +} + +interface ComponentDebugContextType { + /** 是否启用组件视图调试 */ + enabled: boolean; + /** 切换调试模式 */ + setEnabled: (enabled: boolean) => void; + /** 当前显示的组件信息弹窗 */ + componentInfo: ComponentInfo | null; + /** 显示组件信息弹窗 */ + showComponentInfo: (info: ComponentInfo) => void; + /** 隐藏组件信息弹窗 */ + hideComponentInfo: () => void; +} + +const ComponentDebugContext = createContext(null); + +const STORAGE_KEY = "component-debug-enabled"; + +export function ComponentDebugProvider({ children }: { children: ReactNode }) { + const [enabled, setEnabledState] = useState(() => { + const stored = localStorage.getItem(STORAGE_KEY); + return stored === "true"; + }); + const [componentInfo, setComponentInfo] = useState(null); + + const setEnabled = useCallback((value: boolean) => { + setEnabledState(value); + localStorage.setItem(STORAGE_KEY, String(value)); + }, []); + + const showComponentInfo = useCallback((info: ComponentInfo) => { + setComponentInfo(info); + }, []); + + const hideComponentInfo = useCallback(() => { + setComponentInfo(null); + }, []); + + return ( + + {children} + + ); +} + +export function useComponentDebug() { + const context = useContext(ComponentDebugContext); + if (!context) { + throw new Error("useComponentDebug must be used within ComponentDebugProvider"); + } + return context; +}