From 50034331bc48590679fa952a9a315ce6a83116b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Fri, 13 Feb 2026 20:38:15 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E6=B6=88=E6=81=AF=E6=9C=BA=E5=88=B6=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84=201=E3=80=81?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=B6=88=E6=81=AF=E7=B3=BB=E7=BB=9F=EF=BC=9A?= =?UTF-8?q?=E5=B0=86=20background=E3=80=81content=20=E5=92=8C=20popup=20?= =?UTF-8?q?=E7=9A=84=E9=80=9A=E4=BF=A1=E6=96=B9=E5=BC=8F=E4=BB=8E=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E7=9A=84=20chrome.runtime=20=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E8=87=B3=20@webext-core/messaging=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BA=86=E6=B6=88=E6=81=AF=E5=9B=9E=E8=B0=83=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=20undefined=20=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82=202?= =?UTF-8?q?=E3=80=81=E4=BC=98=E5=8C=96=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=9A=E5=B0=86=E9=80=9A=E7=94=A8=E7=BB=84=E4=BB=B6=E4=BB=8E?= =?UTF-8?q?=20entrypoints/popup/components=20=E8=BF=81=E7=A7=BB=E8=87=B3?= =?UTF-8?q?=E6=A0=B9=E7=9B=AE=E5=BD=95=20components=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20MUI=20=E9=87=8D=E6=9E=84=E4=BA=86=20Navbar?= =?UTF-8?q?=E3=80=82=203=E3=80=81=E5=90=8C=E6=AD=A5=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=9A=E8=B0=83=E6=95=B4=E4=BA=86=E5=BD=95=E5=88=B6=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=20(useRecorder)=20=E5=92=8C=E5=90=84=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=BB=84=E4=BB=B6=EF=BC=88RecordeReplayPage,=20TestPa?= =?UTF-8?q?ge=E7=AD=89=EF=BC=89=EF=BC=8C=E4=BB=A5=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=96=B0=E7=9A=84=E6=B6=88=E6=81=AF=E5=8D=8F=E8=AE=AE=E5=92=8C?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E8=B7=AF=E5=BE=84=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components => components}/CopyButton.tsx | 0 .../DatetimeToTimestamp.tsx | 0 components/Navbar.tsx | 117 +++++++++++++++ .../RoutePersistence.tsx | 0 .../TimestampExecution.tsx | 0 .../TimestampToDatetime.tsx | 0 entrypoints/background.ts | 135 +++++++----------- entrypoints/content.ts | 83 ++++------- entrypoints/popup/App.tsx | 4 +- entrypoints/popup/components/Navbar.tsx | 97 ------------- entrypoints/popup/pages/RecordeReplayPage.tsx | 44 +++--- entrypoints/popup/pages/TestPage.tsx | 8 +- entrypoints/popup/pages/TimestampPage.tsx | 6 +- tsconfig.json | 3 +- utils/messages.tsx | 18 ++- utils/useRecorder.tsx | 8 +- 16 files changed, 252 insertions(+), 271 deletions(-) rename {entrypoints/popup/components => components}/CopyButton.tsx (100%) rename {entrypoints/popup/components => components}/DatetimeToTimestamp.tsx (100%) create mode 100644 components/Navbar.tsx rename {entrypoints/popup/components => components}/RoutePersistence.tsx (100%) rename {entrypoints/popup/components => components}/TimestampExecution.tsx (100%) rename {entrypoints/popup/components => components}/TimestampToDatetime.tsx (100%) delete mode 100644 entrypoints/popup/components/Navbar.tsx diff --git a/entrypoints/popup/components/CopyButton.tsx b/components/CopyButton.tsx similarity index 100% rename from entrypoints/popup/components/CopyButton.tsx rename to components/CopyButton.tsx diff --git a/entrypoints/popup/components/DatetimeToTimestamp.tsx b/components/DatetimeToTimestamp.tsx similarity index 100% rename from entrypoints/popup/components/DatetimeToTimestamp.tsx rename to components/DatetimeToTimestamp.tsx diff --git a/components/Navbar.tsx b/components/Navbar.tsx new file mode 100644 index 0000000..f306c3f --- /dev/null +++ b/components/Navbar.tsx @@ -0,0 +1,117 @@ +import { NavLink, useLocation } from 'react-router-dom'; +import { useState, ReactNode, MouseEvent } from 'react'; +import { + AppBar, + Box, + IconButton, + Menu, + MenuItem, + Tab, + Tabs, + Toolbar, + useMediaQuery, +} from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; + +interface RouteItem { + path: string; + label: string; + element: ReactNode; +} + +interface NavbarProps { + items?: RouteItem[]; +} + +function Navbar({ items = [] }: NavbarProps) { + const [anchorEl, setAnchorEl] = useState(null); + const isMenuOpen = Boolean(anchorEl); + const location = useLocation(); + + // Replicating the screen size logic from original component + const isLargeScreen = useMediaQuery('(min-width:768px)'); + const isMediumScreen = useMediaQuery('(min-width:480px)'); + + let visibleItemsCount: number; + if (isLargeScreen) { + visibleItemsCount = items.length; + } else if (isMediumScreen) { + visibleItemsCount = Math.min(3, items.length); + } else { + // Small screen + visibleItemsCount = Math.min(2, items.length); + } + + const visibleNavItems = items.slice(0, visibleItemsCount); + const collapsedNavItems = items.slice(visibleItemsCount); + + const handleMenuOpen = (event: MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + + // Find the current active tab index for the Tabs value + // Using startsWith to handle nested routes correctly. + const activeTabIndex = visibleNavItems.findIndex((item) => + location.pathname.startsWith(item.path), + ); + + return ( + + + + {visibleNavItems.map((item) => ( + + ))} + + + {collapsedNavItems.length > 0 && ( + + + + + + {collapsedNavItems.map((item) => ( + + {item.label} + + ))} + + + )} + + + ); +} + +export default Navbar; diff --git a/entrypoints/popup/components/RoutePersistence.tsx b/components/RoutePersistence.tsx similarity index 100% rename from entrypoints/popup/components/RoutePersistence.tsx rename to components/RoutePersistence.tsx diff --git a/entrypoints/popup/components/TimestampExecution.tsx b/components/TimestampExecution.tsx similarity index 100% rename from entrypoints/popup/components/TimestampExecution.tsx rename to components/TimestampExecution.tsx diff --git a/entrypoints/popup/components/TimestampToDatetime.tsx b/components/TimestampToDatetime.tsx similarity index 100% rename from entrypoints/popup/components/TimestampToDatetime.tsx rename to components/TimestampToDatetime.tsx diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 975f94a..2745de5 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -1,6 +1,7 @@ import '../.wxt/types/imports.d.ts'; import { browser } from 'wxt/browser'; import { downloadHtmlInBackground } from '@/utils/recordUtils.tsx'; +import { sendMessage, onMessage } from '@/utils/messages'; const events: unknown[] = []; @@ -51,88 +52,65 @@ export default defineBackground(() => { ); }); - // 监听来自 popup script 的消息 - chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { - if (msg.type === messages.popup.checkStatus) { - console.log('[bg] checkStatus received'); - sendToActiveTab({ type: messages.content.checkStatus }) - .then((res) => { - sendResponse(res); - }) - .catch((error: Error) => { - if (error.message.includes('restricted URL')) { - console.warn('Could not check status: on a restricted page.', error.message); - } else if (error.message.includes('No active tab found')) { - console.warn('Could not check status: no active tab found.'); - } else { - console.error('Failed to check status in content script', error); - } - sendResponse({ ok: false }); - }); - return true; - } + onMessage('popup:check-status', async (message) => { + console.log(`[background] popup:check-status: ${message}`); + const obj = { + active: false, + startTime: -1, + }; - if (msg.type === messages.popup.from.start) { - console.log('[bg] startRecording received'); - sendToActiveTab({ type: messages.content.to.startRecording }) - .then(async () => { - await chrome.runtime.sendMessage({ type: messages.popup.to.started }); - sendResponse({ ok: true }); - }) - .catch((error: Error) => { - if (error.message.includes('restricted URL')) { - console.warn('Could not start recording: on a restricted page.', error.message); - } else if (error.message.includes('No active tab found')) { - console.warn('Could not start recording: no active tab found.'); - } else { - console.error('Failed to start recording in content script', error); - } - sendResponse({ ok: false }); - }); + try { + const tabId = await getActiveTabId(); + const result = await sendMessage('content:check-status', undefined, tabId); + console.log(`[background]111${result}`); + obj.active = result; + obj.startTime = new Date().getTime(); + } catch (err) { + console.error(`[background-err]${err}`); } + return obj; + }); - if (msg.type === messages.popup.from.stop) { - console.log('[bg] stopRecording received'); - sendToActiveTab({ type: messages.content.to.stopRecording }) - .then(async () => { - await chrome.runtime.sendMessage({ type: messages.popup.to.stopped }); - downloadHtmlInBackground(events); - sendResponse({ ok: true }); - }) - .catch((error: Error) => { - if (error.message.includes('restricted URL')) { - console.warn('Could not stop recording: on a restricted page.', error.message); - } else if (error.message.includes('No active tab found')) { - console.warn('Could not stop recording: no active tab found.'); - } else { - console.error('Failed to stop recording in content script', error); - } - sendResponse({ ok: false }); - }); + onMessage('popup:start', async () => { + console.log('[bg] startRecording received'); + try { + const tabId = await getActiveTabId(); + const response = await sendMessage('content:start-recording', undefined, tabId); + if (response.ok) { + await sendMessage('popup:started', undefined); + return { ok: true }; + } + return { ok: false }; + } catch (error) { + console.error('Failed to start recording in content script', error); + return { ok: false }; } + }); + onMessage('popup:stop', async () => { + console.log('[bg] stopRecording received'); + try { + const tabId = await getActiveTabId(); + const response = await sendMessage('content:stop-recording', undefined, tabId); + if (response.ok) { + await sendMessage('popup:stopped', undefined); + downloadHtmlInBackground(events); + return { ok: true }; + } + return { ok: false }; + } catch (error: unknown) { + console.error('Failed to stop recording in content script', error); + return { ok: false }; + } + }); + + onMessage('content:save-tracke-events', (eventsList) => { + console.log('[bg] saveTrackeEvents received:', eventsList); + events.push(eventsList); return true; }); - // 监听来自 content script 的消息 - chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { - if (msg.type === messages.content.from.saveTrackeEvents) { - console.log('[bg] saveTrackeEvents received:', msg.payload); - events.push(msg.payload); - sendResponse({ ok: true }); - } - }); - - onMessage('getStringLength', (message) => { - return message.data.length; - }); - - async function sendToActiveTab(message: { - type: string; - data?: unknown; - activeTabId?: number; - [key: string]: unknown; - }) { + async function getActiveTabId() { const activeTabs = await chrome.tabs.query({ active: true, currentWindow: true, @@ -159,12 +137,7 @@ export default defineBackground(() => { } } - const sendTo = message.activeTabId || activeTab.id; - try { - return await chrome.tabs.sendMessage(sendTo!, message); - } catch (error) { - console.error('Error sending message to active tab:', error); - throw error; - } + const sendTo = activeTab.id; + return sendTo!; } }); diff --git a/entrypoints/content.ts b/entrypoints/content.ts index caab129..df8818d 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -1,11 +1,6 @@ import '../.wxt/types/imports.d.ts'; import { createRecorder } from '@/utils/useRecorder'; - -interface IResponse { - ok: boolean; - error?: string; - data?: unknown; -} +import { onMessage } from '@/utils/messages.tsx'; export default defineContentScript({ // matches: ['*://*.google.com/*'], @@ -15,56 +10,36 @@ export default defineContentScript({ const recorder = createRecorder(); let isRecording = false; - onMessage('getStringLength', (message) => { - console.log(`[content] getStringLength received: ${message.data}`); + onMessage('content:check-status', () => { + console.log(`[content]${isRecording}`); + return isRecording; }); - // 监听来自 background script 的消息 - chrome.runtime.onMessage.addListener( - (msg: { type: string }, _sender, sendResponse: (response: IResponse) => void) => { - const handleAsyncMessage = async () => { - try { - switch (msg.type) { - // 在这里处理异步消息类型 - case messages.content.checkStatus: - return { ok: true, data: { isRecording } }; + onMessage('content:start-recording', async () => { + try { + const started = await recorder.startRecord(); + if (started) { + isRecording = true; + return { ok: true }; + } else { + return { ok: false, error: 'Failed to start recorder' }; + } + } catch (error) { + console.error('Error handling start recording:', error); + return { ok: false }; + } + }); - case messages.content.to.startRecording: { - const started = await recorder.startRecord(messages.content.from.saveTrackeEvents); - - if (started) { - isRecording = true; - return { ok: true }; - } else { - // 如果启动失败,返回错误信息 - return { ok: false, error: 'Failed to start recorder' }; - } - } - - case messages.content.to.stopRecording: - // 停止录制的处理逻辑 - console.log('[content] stopRecording received'); - recorder.stopRecord(); - isRecording = false; - return { ok: true }; - - default: - return { ok: false, error: 'Unknown message type' }; - } - } catch (error) { - console.error('Error handling message:', error); - return { ok: false }; - } - }; - - handleAsyncMessage().then((response) => { - if (sendResponse) { - sendResponse(response); - } - }); - - return true; - }, - ); + onMessage('content:stop-recording', () => { + try { + console.log('[content] stopRecording received'); + recorder.stopRecord(); + isRecording = false; + return { ok: true }; + } catch (error) { + console.error('Error handling stop recording:', error); + return { ok: false }; + } + }); }, }); diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index c7a51f1..000290a 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -3,8 +3,8 @@ import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import TimestampPage from './pages/TimestampPage'; import RecordeReplayPage from './pages/RecordeReplayPage'; import TestPage from './pages/TestPage'; -import Navbar from './components/Navbar'; -import RoutePersistence from './components/RoutePersistence'; +import Navbar from '../../components/Navbar'; +import RoutePersistence from '../../components/RoutePersistence'; import './App.css'; // 路由配置数据 diff --git a/entrypoints/popup/components/Navbar.tsx b/entrypoints/popup/components/Navbar.tsx deleted file mode 100644 index c965751..0000000 --- a/entrypoints/popup/components/Navbar.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { NavLink } from 'react-router-dom'; -import { useState, useEffect, ReactNode } from 'react'; -import { IconButton } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; -import CloseIcon from '@mui/icons-material/Close'; - -interface RouteItem { - path: string; - label: string; - element: ReactNode; -} - -interface NavbarProps { - items?: RouteItem[]; -} - -function Navbar({ items = [] }: NavbarProps) { - const [isMenuOpen, setIsMenuOpen] = useState(false); - const [isMobile, setIsMobile] = useState(false); - const [visibleItems, setVisibleItems] = useState(items.length); - - // 检测屏幕尺寸变化 - useEffect(() => { - const handleResize = () => { - const width = window.innerWidth; - setIsMobile(width < 768); - - // 根据屏幕宽度决定显示多少个导航项 - if (width >= 768) { - setVisibleItems(items.length); // 大屏幕显示所有 - } else if (width >= 480) { - // 中等屏幕:如果导航项超过3个,显示3个,否则显示全部 - setVisibleItems(Math.min(3, items.length)); - } else { - // 小屏幕:如果导航项超过2个,显示2个,否则显示全部 - setVisibleItems(Math.min(2, items.length)); - } - }; - - handleResize(); // 初始调用 - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, [items.length]); // 添加依赖,如果 items 长度变化也需要重新计算 - - // 计算哪些导航项应该显示,哪些应该折叠 - const visibleNavItems = items.slice(0, visibleItems); - const collapsedNavItems = items.slice(visibleItems); - - return ( - - ); -} - -export default Navbar; diff --git a/entrypoints/popup/pages/RecordeReplayPage.tsx b/entrypoints/popup/pages/RecordeReplayPage.tsx index 6d42dc0..7de7e06 100644 --- a/entrypoints/popup/pages/RecordeReplayPage.tsx +++ b/entrypoints/popup/pages/RecordeReplayPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useMemo } from 'react'; import { AppState } from '../types'; -import { messages } from '@/utils/messages'; +import { sendMessage, onMessage } from '@/utils/messages'; import { Button } from '@mui/material'; const RecordeReplayPage = () => { @@ -8,27 +8,22 @@ const RecordeReplayPage = () => { const isRecording = useMemo(() => status === AppState.RECORDING, [status]); useEffect(() => { - const handleMessage = (msg: { type: string }) => { - if (msg.type === messages.popup.ready) { - setStatus(AppState.READ); - } + const unlistenStarted = onMessage('popup:started', () => { + console.log('[popup] Received started message'); + setStatus(AppState.RECORDING); + }); - if (msg.type === messages.popup.to.started) { - console.log('[popup] Received started message'); - setStatus(AppState.RECORDING); - } + const unlistenStopped = onMessage('popup:stopped', () => { + setStatus(AppState.READ); + }); - if (msg.type === messages.popup.to.stopped) { - setStatus(AppState.READ); - } - }; + const unlistenReady = onMessage('popup:ready', () => { + setStatus(AppState.READ); + }); - browser.runtime.onMessage.addListener(handleMessage); - - browser.runtime - .sendMessage({ type: messages.popup.checkStatus }) + sendMessage('popup:check-status', undefined) .then((res) => { - if (res?.data?.isRecording) { + if (res?.active) { setStatus(AppState.RECORDING); } else { setStatus(AppState.READ); @@ -38,13 +33,20 @@ const RecordeReplayPage = () => { console.error(err); }); - return () => browser.runtime.onMessage.removeListener(handleMessage); + return () => { + unlistenStarted(); + unlistenStopped(); + unlistenReady(); + }; }, []); const toggleRecording = async () => { try { - const actionType = isRecording ? messages.popup.from.stop : messages.popup.from.start; - await browser.runtime.sendMessage({ type: actionType }); + if (isRecording) { + await sendMessage('popup:stop', undefined); + } else { + await sendMessage('popup:start', undefined); + } } catch (error) { console.error('Error toggling recording:', error); setStatus(AppState.READ); diff --git a/entrypoints/popup/pages/TestPage.tsx b/entrypoints/popup/pages/TestPage.tsx index a90eb49..6e915fe 100644 --- a/entrypoints/popup/pages/TestPage.tsx +++ b/entrypoints/popup/pages/TestPage.tsx @@ -3,12 +3,8 @@ import { Button } from '@mui/material'; const TestPage = () => { const handleSeedMessage = async () => { - const tabs = await browser.tabs.query({ active: true, currentWindow: true }); - tabs.forEach(async (tab) => { - console.log(tab.id); - const length = await sendMessage('getStringLength', 'hello world', tab.id); - console.log('字符串长度:', length); - }); + const status = await sendMessage('popup:check-status'); + console.log(`[popup]status: ${status}`); }; return (
diff --git a/entrypoints/popup/pages/TimestampPage.tsx b/entrypoints/popup/pages/TimestampPage.tsx index f53057b..d02c555 100644 --- a/entrypoints/popup/pages/TimestampPage.tsx +++ b/entrypoints/popup/pages/TimestampPage.tsx @@ -1,6 +1,6 @@ -import { TimestampToDatetime } from '../components/TimestampToDatetime'; -import { DatetimeToTimestamp } from '../components/DatetimeToTimestamp'; -import { TimestampExecution } from '../components/TimestampExecution'; +import { TimestampToDatetime } from '@/components/TimestampToDatetime'; +import { DatetimeToTimestamp } from '@/components/DatetimeToTimestamp'; +import { TimestampExecution } from '@/components/TimestampExecution'; const TimestampPage = () => { return ( diff --git a/tsconfig.json b/tsconfig.json index 2ac5e7c..263e7b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -48,7 +48,8 @@ "assets/**/*", "hooks/**/*", ".wxt/types/**/*.ts", - ".wxt/types/*.d.ts" + ".wxt/types/*.d.ts", + "components" ], "exclude": ["node_modules", ".wxt", "eslint.config.ts"] } diff --git a/utils/messages.tsx b/utils/messages.tsx index 6869253..c1d8e06 100644 --- a/utils/messages.tsx +++ b/utils/messages.tsx @@ -32,7 +32,23 @@ export const messages = { }; interface ProtocolMap { - getStringLength(data: string): number; + // --- Popup 相关 --- + 'popup:start': () => { ok: boolean }; + 'popup:stop': () => { ok: boolean }; + 'popup:started': () => void; + 'popup:stopped': () => void; + 'popup:check-status': () => { active: boolean; startTime?: number }; + 'popup:ready': () => void; + + // --- Content 相关 --- + 'content:save-tracke-events': (event) => boolean; + 'content:start-recording': () => { ok: boolean; error?: string }; + 'content:stop-recording': () => { ok: boolean }; + 'content:check-status': () => boolean; + + // --- Offscreen 相关 --- + 'offscreen:start-recording': (streamId: string) => void; + 'offscreen:stop-recording': () => void; } export const { sendMessage, onMessage } = defineExtensionMessaging(); diff --git a/utils/useRecorder.tsx b/utils/useRecorder.tsx index 949d4bb..87b4e7f 100644 --- a/utils/useRecorder.tsx +++ b/utils/useRecorder.tsx @@ -1,18 +1,16 @@ import * as rrweb from 'rrweb'; import { listenerHandler } from '@rrweb/types'; import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record'; +import { sendMessage } from '@/utils/messages'; export const createRecorder = () => { let stopFn: listenerHandler | null = null; - const startRecord = async (msg: string) => { + const startRecord = async () => { try { const handler = rrweb.record({ emit(event) { - chrome.runtime.sendMessage({ - type: msg, - payload: event, - }); + sendMessage('content:save-tracke-events', event); }, plugins: [getRecordConsolePlugin()], });