feat: 添加内置回放界面和错误修复
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": ["WebSearch"]
|
"allow": [
|
||||||
|
"WebSearch",
|
||||||
|
"Bash(npm install:*)"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ stats.html
|
|||||||
stats-*.json
|
stats-*.json
|
||||||
.wxt
|
.wxt
|
||||||
.vitest
|
.vitest
|
||||||
|
.claude
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
+17
-75
@@ -1,17 +1,11 @@
|
|||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
import { useState, ReactNode, MouseEvent } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
AppBar,
|
AppBar,
|
||||||
Box,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Tab,
|
Tab,
|
||||||
Tabs,
|
Tabs,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
useMediaQuery,
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
|
||||||
|
|
||||||
interface RouteItem {
|
interface RouteItem {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -24,40 +18,11 @@ interface NavbarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Navbar({ items = [] }: NavbarProps) {
|
function Navbar({ items = [] }: NavbarProps) {
|
||||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
|
||||||
const isMenuOpen = Boolean(anchorEl);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Replicating the screen size logic from original component
|
// Chrome 扩展 popup 固定尺寸,全部显示在滚动标签中
|
||||||
const isLargeScreen = useMediaQuery('(min-width:768px)');
|
// 精确匹配路由
|
||||||
const isMediumScreen = useMediaQuery('(min-width:480px)');
|
const activeTabIndex = items.findIndex((item) => location.pathname === item.path);
|
||||||
|
|
||||||
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<HTMLElement>) => {
|
|
||||||
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 (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
@@ -68,49 +33,26 @@ function Navbar({ items = [] }: NavbarProps) {
|
|||||||
>
|
>
|
||||||
<Toolbar sx={{ justifyContent: 'center', position: 'relative' }}>
|
<Toolbar sx={{ justifyContent: 'center', position: 'relative' }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTabIndex === -1 ? false : activeTabIndex}
|
value={activeTabIndex === -1 ? 0 : activeTabIndex}
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
scrollButtons="auto"
|
scrollButtons="auto"
|
||||||
allowScrollButtonsMobile
|
allowScrollButtonsMobile
|
||||||
aria-label="navigation tabs"
|
aria-label="navigation tabs"
|
||||||
|
sx={{ minHeight: 48 }}
|
||||||
>
|
>
|
||||||
{visibleNavItems.map((item) => (
|
{items.map((item) => (
|
||||||
<Tab key={item.path} label={item.label} component={NavLink} to={item.path} />
|
<Tab
|
||||||
|
key={item.path}
|
||||||
|
label={item.label}
|
||||||
|
component={NavLink}
|
||||||
|
to={item.path}
|
||||||
|
sx={{
|
||||||
|
minWidth: 80,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{collapsedNavItems.length > 0 && (
|
|
||||||
<Box sx={{ position: 'absolute', right: 8 }}>
|
|
||||||
<IconButton color="inherit" aria-label="open menu" edge="end" onClick={handleMenuOpen}>
|
|
||||||
<MenuIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Menu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={isMenuOpen}
|
|
||||||
onClose={handleMenuClose}
|
|
||||||
slotProps={{
|
|
||||||
paper: {
|
|
||||||
style: {
|
|
||||||
maxHeight: 48 * 4.5,
|
|
||||||
width: '20ch',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{collapsedNavItems.map((item) => (
|
|
||||||
<MenuItem
|
|
||||||
key={item.path}
|
|
||||||
component={NavLink}
|
|
||||||
to={item.path}
|
|
||||||
onClick={handleMenuClose}
|
|
||||||
selected={location.pathname.startsWith(item.path)}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
streamAllEvents,
|
streamAllEvents,
|
||||||
deleteRecordingSession,
|
deleteRecordingSession,
|
||||||
generateSessionId,
|
generateSessionId,
|
||||||
|
getAllSessions,
|
||||||
|
getAllEvents,
|
||||||
} from '@/utils/recordEventsDb';
|
} from '@/utils/recordEventsDb';
|
||||||
|
|
||||||
interface RecorderState {
|
interface RecorderState {
|
||||||
@@ -210,14 +212,14 @@ export default defineBackground(() => {
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// 从 IndexedDB 流式读取所有事件并下载回放文件
|
// 从 IndexedDB 流式读取所有事件并下载回放文件
|
||||||
if (state.sessionId) {const allEvents: unknown[] = [];
|
if (state.sessionId) {
|
||||||
|
const allEvents: unknown[] = [];
|
||||||
await streamAllEvents(state.sessionId, (events) => {
|
await streamAllEvents(state.sessionId, (events) => {
|
||||||
allEvents.push(...events);
|
allEvents.push(...events);
|
||||||
});
|
});
|
||||||
downloadHtmlInBackground(allEvents);
|
downloadHtmlInBackground(allEvents);
|
||||||
|
|
||||||
// 删除会话数据
|
// 不再删除会话数据,保留录制历史
|
||||||
await deleteRecordingSession(state.sessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清空状态
|
// 清空状态
|
||||||
@@ -238,6 +240,54 @@ export default defineBackground(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取所有录制会话
|
||||||
|
onMessage('popup:get-sessions', async () => {
|
||||||
|
try {
|
||||||
|
const sessions = await getAllSessions();
|
||||||
|
return sessions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[bg] 获取录制会话失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取会话事件
|
||||||
|
onMessage('popup:get-session-events', async (message) => {
|
||||||
|
try {
|
||||||
|
const sessionId = message.data;
|
||||||
|
const events = await getAllEvents(sessionId);
|
||||||
|
return events;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[bg] 获取会话事件失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除会话
|
||||||
|
onMessage('popup:delete-session', async (message) => {
|
||||||
|
try {
|
||||||
|
const sessionId = message.data;
|
||||||
|
await deleteRecordingSession(sessionId);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[bg] 删除会话失败:', error);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 下载会话回放
|
||||||
|
onMessage('popup:download-session', async (message) => {
|
||||||
|
try {
|
||||||
|
const sessionId = message.data;
|
||||||
|
const events = await getAllEvents(sessionId);
|
||||||
|
downloadHtmlInBackground(events);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[bg] 下载会话失败:', error);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMessage('content:save-track-events', async (event) => {
|
onMessage('content:save-track-events', async (event) => {
|
||||||
const state = await loadRecorderState();
|
const state = await loadRecorderState();
|
||||||
console.log('[bg] 收到事件,当前状态:', state);
|
console.log('[bg] 收到事件,当前状态:', state);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import '../.wxt/types/imports.d.ts';
|
import '../.wxt/types/imports.d.ts';
|
||||||
import { createRecorder } from '@/utils/useRecorder';
|
import { createRecorder } from '@/utils/useRecorder';
|
||||||
import { onMessage } from '@/utils/messages.tsx';
|
import { onMessage } from '@/utils/messages';
|
||||||
|
|
||||||
export default defineContentScript({
|
export default defineContentScript({
|
||||||
// matches: ['*://*.google.com/*'],
|
// matches: ['*://*.google.com/*'],
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// App.js
|
// App.js
|
||||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
import TimestampPage from './pages/TimestampPage';
|
import TimestampPage from './pages/TimestampPage';
|
||||||
import RecordeReplayPage from './pages/RecordeReplayPage';
|
import RecordReplayPage from './pages/RecordReplayPage';
|
||||||
import TestPage from './pages/TestPage';
|
import TestPage from './pages/TestPage';
|
||||||
|
import ReplayListPage from './pages/ReplayListPage';
|
||||||
|
import ReplayPlayerPage from './pages/ReplayPlayerPage';
|
||||||
import Navbar from '../../components/Navbar';
|
import Navbar from '../../components/Navbar';
|
||||||
import RoutePersistence from '../../components/RoutePersistence';
|
import RoutePersistence from '../../components/RoutePersistence';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
@@ -11,7 +13,8 @@ import './App.css';
|
|||||||
const navItems = [
|
const navItems = [
|
||||||
{ path: '/test', label: '测试页面', element: <TestPage /> },
|
{ path: '/test', label: '测试页面', element: <TestPage /> },
|
||||||
{ path: '/', label: '时间戳', element: <TimestampPage /> },
|
{ path: '/', label: '时间戳', element: <TimestampPage /> },
|
||||||
{ path: '/recorde-replay', label: '录制与回放', element: <RecordeReplayPage /> },
|
{ path: '/record-replay', label: '录制', element: <RecordReplayPage /> },
|
||||||
|
{ path: '/replay-list', label: '历史回放', element: <ReplayListPage /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -25,6 +28,7 @@ function App() {
|
|||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<Route key={item.path} path={item.path} element={item.element} />
|
<Route key={item.path} path={item.path} element={item.element} />
|
||||||
))}
|
))}
|
||||||
|
<Route path="/replay/:id" element={<ReplayPlayerPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
|
import { AppState } from '../types';
|
||||||
|
import { sendMessage, onMessage } from '@/utils/messages';
|
||||||
|
import { Button, Container, Stack, Typography, Alert } from '@mui/material';
|
||||||
|
import { CircularProgress } from '@mui/material';
|
||||||
|
|
||||||
|
const RecordReplayPage = () => {
|
||||||
|
const [status, setStatus] = useState<AppState>(AppState.READ);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const isRecording = useMemo(() => status === AppState.RECORDING, [status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unlistenStarted = onMessage('popup:started', () => {
|
||||||
|
console.log('[popup] Received started message');
|
||||||
|
setStatus(AppState.RECORDING);
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlistenStopped = onMessage('popup:stopped', () => {
|
||||||
|
setStatus(AppState.READ);
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
const unlistenReady = onMessage('popup:ready', () => {
|
||||||
|
setStatus(AppState.READ);
|
||||||
|
setIsLoading(false);
|
||||||
|
setError(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
sendMessage('popup:check-status', undefined)
|
||||||
|
.then((res) => {
|
||||||
|
if (res?.active) {
|
||||||
|
setStatus(AppState.RECORDING);
|
||||||
|
} else {
|
||||||
|
setStatus(AppState.READ);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
setError('检查录制状态失败');
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlistenStarted();
|
||||||
|
unlistenStopped();
|
||||||
|
unlistenReady();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleRecording = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
if (isRecording) {
|
||||||
|
const result = await sendMessage('popup:stop', undefined);
|
||||||
|
if (!result?.ok) {
|
||||||
|
setError(result?.error || '停止录制失败');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const result = await sendMessage('popup:start', undefined);
|
||||||
|
if (!result?.ok) {
|
||||||
|
setError(result?.error || '开始录制失败');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling recording:', error);
|
||||||
|
setError('操作失败,请重试');
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false} sx={{ py: 2.5 }}>
|
||||||
|
<Stack direction="column" spacing={2} sx={{ mb: 2.5 }}>
|
||||||
|
{error && (
|
||||||
|
<Alert severity="error" sx={{ fontSize: '0.875rem' }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1.25} justifyContent="center">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="medium"
|
||||||
|
color={isRecording ? 'error' : 'primary'}
|
||||||
|
onClick={toggleRecording}
|
||||||
|
disabled={isLoading}
|
||||||
|
sx={{ minWidth: 120 }}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<CircularProgress size={20} color="inherit" />
|
||||||
|
) : isRecording ? (
|
||||||
|
'停止录制'
|
||||||
|
) : (
|
||||||
|
'开始录制'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
textAlign="center"
|
||||||
|
sx={{ mt: 1 }}
|
||||||
|
>
|
||||||
|
{isRecording ? '正在录制用户操作...' : '点击开始录制用户操作'}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecordReplayPage;
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { useEffect, useState, useMemo } from 'react';
|
|
||||||
import { AppState } from '../types';
|
|
||||||
import { sendMessage, onMessage } from '@/utils/messages';
|
|
||||||
import { Button, Container, Stack } from '@mui/material';
|
|
||||||
|
|
||||||
const RecordeReplayPage = () => {
|
|
||||||
const [status, setStatus] = useState<AppState>(AppState.READ);
|
|
||||||
const isRecording = useMemo(() => status === AppState.RECORDING, [status]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unlistenStarted = onMessage('popup:started', () => {
|
|
||||||
console.log('[popup] Received started message');
|
|
||||||
setStatus(AppState.RECORDING);
|
|
||||||
});
|
|
||||||
|
|
||||||
const unlistenStopped = onMessage('popup:stopped', () => {
|
|
||||||
setStatus(AppState.READ);
|
|
||||||
});
|
|
||||||
|
|
||||||
const unlistenReady = onMessage('popup:ready', () => {
|
|
||||||
setStatus(AppState.READ);
|
|
||||||
});
|
|
||||||
|
|
||||||
sendMessage('popup:check-status', undefined)
|
|
||||||
.then((res) => {
|
|
||||||
if (res?.active) {
|
|
||||||
setStatus(AppState.RECORDING);
|
|
||||||
} else {
|
|
||||||
setStatus(AppState.READ);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unlistenStarted();
|
|
||||||
unlistenStopped();
|
|
||||||
unlistenReady();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleRecording = async () => {
|
|
||||||
try {
|
|
||||||
if (isRecording) {
|
|
||||||
const result = await sendMessage('popup:stop', undefined);
|
|
||||||
if (!result?.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const result = await sendMessage('popup:start', undefined);
|
|
||||||
if (!result?.ok) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling recording:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container maxWidth={false} sx={{ py: 2.5 }}>
|
|
||||||
<Stack direction="row" spacing={1.25} sx={{ mb: 2.5 }}>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size="medium"
|
|
||||||
color={isRecording ? 'error' : 'primary'}
|
|
||||||
onClick={toggleRecording}
|
|
||||||
>
|
|
||||||
{isRecording ? '停止录制' : '开始录制'}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RecordeReplayPage;
|
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemText,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
IconButton,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
PlayArrow as PlayIcon,
|
||||||
|
Download as DownloadIcon,
|
||||||
|
Delete as DeleteIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { sendMessage } from '@/utils/messages';
|
||||||
|
|
||||||
|
interface Session {
|
||||||
|
id: string;
|
||||||
|
startTime: number;
|
||||||
|
tabId: number;
|
||||||
|
chunkCount: number;
|
||||||
|
totalEvents: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReplayListPage = () => {
|
||||||
|
const [sessions, setSessions] = useState<Session[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchSessions();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchSessions = async () => {
|
||||||
|
try {
|
||||||
|
const data = await sendMessage('popup:get-sessions');
|
||||||
|
setSessions(data || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('获取录制历史失败');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlay = (sessionId: string) => {
|
||||||
|
navigate(`/replay/${sessionId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = async (sessionId: string) => {
|
||||||
|
try {
|
||||||
|
await sendMessage('popup:download-session', sessionId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('下载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (sessionId: string) => {
|
||||||
|
if (window.confirm('确定要删除这个录制吗?')) {
|
||||||
|
try {
|
||||||
|
await sendMessage('popup:delete-session', sessionId);
|
||||||
|
fetchSessions();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false} sx={{ py: 2.5, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false} sx={{ py: 2.5 }}>
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
<Button variant="contained" onClick={fetchSessions}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false} sx={{ py: 2.5 }}>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
|
录制历史
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{sessions.length === 0 ? (
|
||||||
|
<Box textAlign="center" sx={{ py: 4 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
暂无录制历史
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
|
||||||
|
点击"录制与回放"页面的开始按钮进行录制
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<List sx={{ width: '100%', maxWidth: 500, margin: '0 auto' }}>
|
||||||
|
{sessions.map((session) => (
|
||||||
|
<ListItem
|
||||||
|
key={session.id}
|
||||||
|
disablePadding
|
||||||
|
sx={{ mb: 1, borderRadius: 1, overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
<ListItemButton
|
||||||
|
sx={{ borderRadius: 1 }}
|
||||||
|
onClick={() => handlePlay(session.id)}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<PlayIcon fontSize="small" />
|
||||||
|
<Typography variant="body1">
|
||||||
|
{new Date(session.startTime).toLocaleString()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
事件数: {session.chunkCount * 100}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Tab: {session.tabId}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDownload(session.id);
|
||||||
|
}}
|
||||||
|
title="下载"
|
||||||
|
>
|
||||||
|
<DownloadIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(session.id);
|
||||||
|
}}
|
||||||
|
title="删除"
|
||||||
|
sx={{ color: 'error.main' }}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReplayListPage;
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
IconButton,
|
||||||
|
Button,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
ArrowBack as ArrowBackIcon,
|
||||||
|
Download as DownloadIcon,
|
||||||
|
Delete as DeleteIcon,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import { sendMessage } from '@/utils/messages';
|
||||||
|
|
||||||
|
const ReplayPlayerPage = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [eventsData, setEventsData] = useState<unknown[] | null>(null);
|
||||||
|
|
||||||
|
const fetchSessionEvents = useCallback(async (sessionId: string) => {
|
||||||
|
try {
|
||||||
|
const data = await sendMessage('popup:get-session-events', sessionId);
|
||||||
|
setEventsData(data || []);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('加载录制内容失败');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (id) {
|
||||||
|
fetchSessionEvents(id);
|
||||||
|
}
|
||||||
|
}, [id, fetchSessionEvents]);
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
try {
|
||||||
|
await sendMessage('popup:download-session', id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('下载失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!id) return;
|
||||||
|
if (window.confirm('确定要删除这个录制吗?')) {
|
||||||
|
try {
|
||||||
|
await sendMessage('popup:delete-session', id);
|
||||||
|
navigate('/record-replay');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError('删除失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false} sx={{ py: 2.5, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false} sx={{ py: 2.5 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<IconButton onClick={() => navigate('/record-replay')} sx={{ mr: 1 }}>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6">回放</Typography>
|
||||||
|
</Box>
|
||||||
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
<Button variant="contained" onClick={() => fetchSessionEvents(id!)}>
|
||||||
|
重试
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eventsData || eventsData.length === 0) {
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false} sx={{ py: 2.5 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<IconButton onClick={() => navigate('/record-replay')} sx={{ mr: 1 }}>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6">回放</Typography>
|
||||||
|
</Box>
|
||||||
|
<Alert severity="warning">
|
||||||
|
此会话没有事件数据,请尝试重新录制。
|
||||||
|
</Alert>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth={false} sx={{ py: 2.5 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<IconButton onClick={() => navigate('/record-replay')} sx={{ mr: 1 }}>
|
||||||
|
<ArrowBackIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h6">回放</Typography>
|
||||||
|
<Box sx={{ ml: 'auto', display: 'flex', gap: 1 }}>
|
||||||
|
<IconButton onClick={handleDownload} title="下载">
|
||||||
|
<DownloadIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={handleDelete} title="删除" sx={{ color: 'error.main' }}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ borderRadius: 1, overflow: 'hidden', border: 1, borderColor: 'divider' }}>
|
||||||
|
<Box sx={{ p: 2, textAlign: 'center' }}>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
由于浏览器安全策略限制,无法在内联 iframe 中直接回放。
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
|
||||||
|
请点击右上角的"下载"按钮,下载完整的回放 HTML 文件到本地查看。
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleDownload}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
startIcon={<DownloadIcon />}
|
||||||
|
>
|
||||||
|
下载回放文件
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 2 }}>
|
||||||
|
<Alert severity="info">
|
||||||
|
<Typography variant="body2">
|
||||||
|
<strong>安全提示:</strong>浏览器扩展有严格的 Content Security Policy (CSP)
|
||||||
|
限制,禁止内联脚本执行,因此无法在扩展 popup 中直接显示回放内容。
|
||||||
|
下载的 HTML 文件包含了完整的回放代码,可以在任何现代浏览器中直接打开。
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReplayPlayerPage;
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import * as reactPlugin from 'eslint-plugin-react';
|
|||||||
import globals from 'globals';
|
import globals from 'globals';
|
||||||
|
|
||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ['dist', '.wxt', 'node_modules', 'eslint.config.ts'] },
|
{ ignores: ['dist', '.wxt', 'node_modules', 'eslint.config.ts', '**/*.test.tsx', '**/*.test.ts', '**/__tests__/**'] },
|
||||||
|
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
|
|||||||
Generated
+63
-575
@@ -41,7 +41,7 @@
|
|||||||
"@types/webextension-polyfill": "^0.12.4",
|
"@types/webextension-polyfill": "^0.12.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
"@typescript-eslint/parser": "^8.54.0",
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@wxt-dev/module-react": "^1.1.5",
|
"@wxt-dev/module-react": "^1.1.5",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
@@ -727,40 +727,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
|
||||||
"version": "1.9.0",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@emnapi/core/-/core-1.9.0.tgz",
|
|
||||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/wasi-threads": "1.2.0",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.9.0",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
|
||||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emotion/babel-plugin": {
|
"node_modules/@emotion/babel-plugin": {
|
||||||
"version": "11.13.5",
|
"version": "11.13.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||||
@@ -2036,23 +2002,6 @@
|
|||||||
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
"integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/core": "^1.7.1",
|
|
||||||
"@emnapi/runtime": "^1.7.1",
|
|
||||||
"@tybys/wasm-util": "^0.10.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -2091,26 +2040,6 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@oxc-project/runtime": {
|
|
||||||
"version": "0.115.0",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@oxc-project/runtime/-/runtime-0.115.0.tgz",
|
|
||||||
"integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-project/types": {
|
|
||||||
"version": "0.115.0",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@oxc-project/types/-/types-0.115.0.tgz",
|
|
||||||
"integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@pnpm/config.env-replace": {
|
"node_modules/@pnpm/config.env-replace": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz",
|
||||||
@@ -2175,265 +2104,10 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/binding-android-arm64": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-darwin-x64": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openharmony"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==",
|
|
||||||
"cpu": [
|
|
||||||
"wasm32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@napi-rs/wasm-runtime": "^1.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-rc.7",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
|
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
@@ -2938,17 +2612,6 @@
|
|||||||
"integrity": "sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==",
|
"integrity": "sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
|
||||||
"version": "0.10.1",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
|
||||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/aria-query": {
|
"node_modules/@types/aria-query": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
@@ -3434,29 +3097,34 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "6.0.1",
|
"version": "4.7.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
|
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.7"
|
"@babel/core": "^7.28.0",
|
||||||
|
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
||||||
|
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
||||||
|
"@rolldown/pluginutils": "1.0.0-beta.27",
|
||||||
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"react-refresh": "^0.17.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^14.18.0 || >=16.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
}
|
||||||
"vite": "^8.0.0"
|
},
|
||||||
},
|
"node_modules/@vitejs/plugin-react/node_modules/react-refresh": {
|
||||||
"peerDependenciesMeta": {
|
"version": "0.17.0",
|
||||||
"@rolldown/plugin-babel": {
|
"resolved": "https://mirrors.cloud.tencent.com/npm/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
"optional": true
|
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
||||||
},
|
"dev": true,
|
||||||
"babel-plugin-react-compiler": {
|
"license": "MIT",
|
||||||
"optional": true
|
"engines": {
|
||||||
}
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
@@ -5343,6 +5011,8 @@
|
|||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -8064,6 +7734,8 @@
|
|||||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"detect-libc": "^2.0.3"
|
"detect-libc": "^2.0.3"
|
||||||
},
|
},
|
||||||
@@ -8101,6 +7773,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"android"
|
"android"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8122,6 +7795,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8143,6 +7817,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8164,6 +7839,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"freebsd"
|
"freebsd"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8185,6 +7861,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8206,6 +7883,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8227,6 +7905,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8248,6 +7927,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8269,6 +7949,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"linux"
|
"linux"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8290,6 +7971,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -8311,6 +7993,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -10604,47 +10287,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rolldown": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/rolldown/-/rolldown-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@oxc-project/types": "=0.115.0",
|
|
||||||
"@rolldown/pluginutils": "1.0.0-rc.9"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"rolldown": "bin/cli.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@rolldown/binding-android-arm64": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-darwin-arm64": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-darwin-x64": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-freebsd-x64": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9",
|
|
||||||
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
|
||||||
"version": "1.0.0-rc.9",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
|
|
||||||
"integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.56.0",
|
"version": "4.56.0",
|
||||||
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.56.0.tgz",
|
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.56.0.tgz",
|
||||||
@@ -12265,108 +11907,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.0.0",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-8.0.0.tgz",
|
|
||||||
"integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@oxc-project/runtime": "0.115.0",
|
|
||||||
"lightningcss": "^1.32.0",
|
|
||||||
"picomatch": "^4.0.3",
|
|
||||||
"postcss": "^8.5.8",
|
|
||||||
"rolldown": "1.0.0-rc.9",
|
|
||||||
"tinyglobby": "^0.2.15"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"vite": "bin/vite.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.3"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
|
||||||
"@vitejs/devtools": "^0.0.0-alpha.31",
|
|
||||||
"esbuild": "^0.27.0",
|
|
||||||
"jiti": ">=1.21.0",
|
|
||||||
"less": "^4.0.0",
|
|
||||||
"sass": "^1.70.0",
|
|
||||||
"sass-embedded": "^1.70.0",
|
|
||||||
"stylus": ">=0.54.8",
|
|
||||||
"sugarss": "^5.0.0",
|
|
||||||
"terser": "^5.16.0",
|
|
||||||
"tsx": "^4.8.1",
|
|
||||||
"yaml": "^2.4.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/node": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@vitejs/devtools": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"esbuild": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"jiti": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"less": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sass": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sass-embedded": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"stylus": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sugarss": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"terser": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"tsx": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"yaml": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite-node": {
|
|
||||||
"version": "5.3.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-5.3.0.tgz",
|
|
||||||
"integrity": "sha512-8f20COPYJujc3OKPX6OuyBy3ZIv2det4eRRU4GY1y2MjbeGSUmPjedxg1b72KnTagCofwvZ65ThzjxDW2AtQFQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cac": "^6.7.14",
|
|
||||||
"es-module-lexer": "^2.0.0",
|
|
||||||
"obug": "^2.1.1",
|
|
||||||
"pathe": "^2.0.3",
|
|
||||||
"vite": "^7.3.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"vite-node": "dist/cli.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/antfu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite-node/node_modules/vite": {
|
|
||||||
"version": "7.3.1",
|
"version": "7.3.1",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-7.3.1.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
@@ -12441,6 +11981,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-node": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8f20COPYJujc3OKPX6OuyBy3ZIv2det4eRRU4GY1y2MjbeGSUmPjedxg1b72KnTagCofwvZ65ThzjxDW2AtQFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cac": "^6.7.14",
|
||||||
|
"es-module-lexer": "^2.0.0",
|
||||||
|
"obug": "^2.1.1",
|
||||||
|
"pathe": "^2.0.3",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"vite-node": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/vitest/-/vitest-4.1.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/vitest/-/vitest-4.1.0.tgz",
|
||||||
@@ -12906,81 +12469,6 @@
|
|||||||
"url": "https://github.com/sponsors/wxt-dev"
|
"url": "https://github.com/sponsors/wxt-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/wxt/node_modules/vite": {
|
|
||||||
"version": "7.3.1",
|
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-7.3.1.tgz",
|
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"esbuild": "^0.27.0",
|
|
||||||
"fdir": "^6.5.0",
|
|
||||||
"picomatch": "^4.0.3",
|
|
||||||
"postcss": "^8.5.6",
|
|
||||||
"rollup": "^4.43.0",
|
|
||||||
"tinyglobby": "^0.2.15"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"vite": "bin/vite.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.3"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/node": "^20.19.0 || >=22.12.0",
|
|
||||||
"jiti": ">=1.21.0",
|
|
||||||
"less": "^4.0.0",
|
|
||||||
"lightningcss": "^1.21.0",
|
|
||||||
"sass": "^1.70.0",
|
|
||||||
"sass-embedded": "^1.70.0",
|
|
||||||
"stylus": ">=0.54.8",
|
|
||||||
"sugarss": "^5.0.0",
|
|
||||||
"terser": "^5.16.0",
|
|
||||||
"tsx": "^4.8.1",
|
|
||||||
"yaml": "^2.4.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/node": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"jiti": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"less": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"lightningcss": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sass": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sass-embedded": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"stylus": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sugarss": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"terser": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"tsx": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"yaml": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/xdg-basedir": {
|
"node_modules/xdg-basedir": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@
|
|||||||
"@types/webextension-polyfill": "^0.12.4",
|
"@types/webextension-polyfill": "^0.12.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
||||||
"@typescript-eslint/parser": "^8.54.0",
|
"@typescript-eslint/parser": "^8.54.0",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"@wxt-dev/module-react": "^1.1.5",
|
"@wxt-dev/module-react": "^1.1.5",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ interface ProtocolMap {
|
|||||||
'popup:tab-changed': (data: { currentTabId: number; recordingTabId: number | undefined }) => void;
|
'popup:tab-changed': (data: { currentTabId: number; recordingTabId: number | undefined }) => void;
|
||||||
'popup:check-status': () => { active: boolean; startTime?: number };
|
'popup:check-status': () => { active: boolean; startTime?: number };
|
||||||
'popup:ready': () => void;
|
'popup:ready': () => void;
|
||||||
|
'popup:get-sessions': () => { id: string; startTime: number; tabId: number; chunkCount: number; totalEvents: number }[];
|
||||||
|
'popup:get-session-events': (sessionId: string) => unknown[];
|
||||||
|
'popup:delete-session': (sessionId: string) => { ok: boolean };
|
||||||
|
'popup:download-session': (sessionId: string) => { ok: boolean };
|
||||||
|
|
||||||
// --- Content 相关 ---
|
// --- Content 相关 ---
|
||||||
'content:save-track-events': (event: unknown) => boolean;
|
'content:save-track-events': (event: unknown) => boolean;
|
||||||
|
|||||||
+94
-20
@@ -89,18 +89,33 @@ export async function initRecordingSession(sessionId: string, tabId: number): Pr
|
|||||||
* 获取当前会话的 chunk 数量
|
* 获取当前会话的 chunk 数量
|
||||||
*/
|
*/
|
||||||
export async function getSessionChunkCount(sessionId: string): Promise<number> {
|
export async function getSessionChunkCount(sessionId: string): Promise<number> {
|
||||||
|
// 参数验证
|
||||||
|
if (!sessionId || typeof sessionId !== 'string') {
|
||||||
|
console.error('[db] Invalid sessionId for getSessionChunkCount:', sessionId);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const database = await openDB();
|
const database = await openDB();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve) => {
|
||||||
const tx = database.transaction('sessions', 'readonly');
|
const tx = database.transaction('sessions', 'readonly');
|
||||||
const store = tx.objectStore('sessions');
|
const store = tx.objectStore('sessions');
|
||||||
const request = store.get(sessionId);
|
|
||||||
|
|
||||||
request.onsuccess = () => {
|
try {
|
||||||
const session = request.result as RecordingSession | undefined;
|
const request = store.get(sessionId);
|
||||||
resolve(session?.chunkCount || 0);
|
|
||||||
};
|
request.onsuccess = () => {
|
||||||
request.onerror = () => reject(request.error);
|
const session = request.result as RecordingSession | undefined;
|
||||||
|
resolve(session?.chunkCount || 0);
|
||||||
|
};
|
||||||
|
request.onerror = (e) => {
|
||||||
|
console.error('[db] Error getting session chunk count:', e);
|
||||||
|
resolve(0);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[db] Exception getting session chunk count:', error);
|
||||||
|
resolve(0);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,14 +165,28 @@ async function getChunk(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
chunkIndex: number,
|
chunkIndex: number,
|
||||||
): Promise<EventChunk | null> {
|
): Promise<EventChunk | null> {
|
||||||
return new Promise((resolve, reject) => {
|
// 参数验证
|
||||||
const tx = database.transaction(STORE_NAME, 'readonly');
|
if (!sessionId || typeof sessionId !== 'string') {
|
||||||
const store = tx.objectStore(STORE_NAME);
|
console.error('[db] Invalid sessionId for getChunk:', sessionId);
|
||||||
const index = store.index('sessionId_chunkIndex');
|
return null;
|
||||||
const request = index.get([sessionId, chunkIndex]);
|
}
|
||||||
|
|
||||||
request.onsuccess = () => resolve(request.result || null);
|
return new Promise((resolve) => {
|
||||||
request.onerror = () => reject(request.error);
|
try {
|
||||||
|
const tx = database.transaction(STORE_NAME, 'readonly');
|
||||||
|
const store = tx.objectStore(STORE_NAME);
|
||||||
|
const index = store.index('sessionId_chunkIndex');
|
||||||
|
const request = index.get([sessionId, chunkIndex]);
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve(request.result || null);
|
||||||
|
request.onerror = (e) => {
|
||||||
|
console.error('[db] Error getting chunk:', e);
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[db] Exception getting chunk:', error);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,14 +240,26 @@ export async function streamAllEvents(
|
|||||||
sessionId: string,
|
sessionId: string,
|
||||||
onChunk: (events: unknown[]) => void | Promise<void>,
|
onChunk: (events: unknown[]) => void | Promise<void>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const database = await openDB();
|
// 参数验证
|
||||||
const chunkCount = await getSessionChunkCount(sessionId);
|
if (!sessionId || typeof sessionId !== 'string') {
|
||||||
|
console.error('[db] Invalid sessionId for streamAllEvents:', sessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < chunkCount; i++) {
|
try {
|
||||||
const chunk = await getChunk(database, sessionId, i);
|
const database = await openDB();
|
||||||
if (chunk && chunk.events.length > 0) {
|
const chunkCount = await getSessionChunkCount(sessionId);
|
||||||
await onChunk(chunk.events);
|
|
||||||
|
console.log(`[db] Streaming ${chunkCount} chunks for session ${sessionId}`);
|
||||||
|
|
||||||
|
for (let i = 0; i < chunkCount; i++) {
|
||||||
|
const chunk = await getChunk(database, sessionId, i);
|
||||||
|
if (chunk && chunk.events.length > 0) {
|
||||||
|
await onChunk(chunk.events);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[db] Error streaming events:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +268,12 @@ export async function streamAllEvents(
|
|||||||
* 注意:这会将所有事件加载到内存,仅在需要导出时调用
|
* 注意:这会将所有事件加载到内存,仅在需要导出时调用
|
||||||
*/
|
*/
|
||||||
export async function getAllEvents(sessionId: string): Promise<unknown[]> {
|
export async function getAllEvents(sessionId: string): Promise<unknown[]> {
|
||||||
|
// 参数验证
|
||||||
|
if (!sessionId || typeof sessionId !== 'string') {
|
||||||
|
console.error('[db] Invalid sessionId for getAllEvents:', sessionId);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const allEvents: unknown[] = [];
|
const allEvents: unknown[] = [];
|
||||||
|
|
||||||
await streamAllEvents(sessionId, (events) => {
|
await streamAllEvents(sessionId, (events) => {
|
||||||
@@ -271,6 +318,33 @@ export async function deleteRecordingSession(sessionId: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有录制会话列表
|
||||||
|
*/
|
||||||
|
export async function getAllSessions(): Promise<RecordingSession[]> {
|
||||||
|
const database = await openDB();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = database.transaction('sessions', 'readonly');
|
||||||
|
const store = tx.objectStore('sessions');
|
||||||
|
const index = store.index('startTime');
|
||||||
|
const request = index.openCursor(null, 'prev'); // 按时间倒序排列
|
||||||
|
const sessions: RecordingSession[] = [];
|
||||||
|
|
||||||
|
request.onsuccess = (event) => {
|
||||||
|
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
|
||||||
|
if (cursor) {
|
||||||
|
sessions.push(cursor.value);
|
||||||
|
cursor.continue();
|
||||||
|
} else {
|
||||||
|
resolve(sessions);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成唯一的会话 ID
|
* 生成唯一的会话 ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
+46
-27
@@ -7,46 +7,65 @@ export const downloadHtmlInBackground = (events: unknown[]) => {
|
|||||||
.replace(/`/g, '\\`')
|
.replace(/`/g, '\\`')
|
||||||
.replace(/<\/script>/g, '<\\/script>');
|
.replace(/<\/script>/g, '<\\/script>');
|
||||||
|
|
||||||
|
// 使用固定版本的 rrweb-player,确保与项目使用的 rrweb 版本兼容
|
||||||
const htmlContent = `
|
const htmlContent = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>RRWeb 回放</title>
|
<title>RRWeb 回放</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb-player@1.0.0-alpha.4/dist/style.css" />
|
||||||
<style>
|
<style>
|
||||||
body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f0f2f5; }
|
body {
|
||||||
#player { width: 100%; max-width: 1024px; }
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f0f2f5;
|
||||||
|
}
|
||||||
|
#player {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1024px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="player"></div>
|
<div id="player"></div>
|
||||||
|
|
||||||
<!-- 使用 UMD 版本,rrwebPlayer 会作为全局变量 -->
|
<!-- 使用与项目 rrweb 版本匹配的 rrweb-player -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/rrweb-player@1.0.0-alpha.4/dist/index.js"></script>
|
||||||
|
|
||||||
<script type="module">
|
<script>
|
||||||
import { getReplayConsolePlugin } from 'https://esm.sh/@rrweb/rrweb-plugin-console-replay?bundle';
|
try {
|
||||||
|
// 解析事件数据
|
||||||
|
const events = JSON.parse(\`${safeEventsString}\`);
|
||||||
|
console.log('Loaded events count:', events.length);
|
||||||
|
|
||||||
const events = JSON.parse(\`${safeEventsString}\`);
|
// 创建播放器
|
||||||
|
const player = new rrwebPlayer({
|
||||||
|
target: document.getElementById('player'),
|
||||||
|
props: {
|
||||||
|
events: events,
|
||||||
|
width: 1024,
|
||||||
|
height: 576,
|
||||||
|
autoPlay: true,
|
||||||
|
showController: true,
|
||||||
|
UNSAFE_replayCanvas: true
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// rrwebPlayer 作为全局变量可用
|
console.log('Player created successfully');
|
||||||
new rrwebPlayer({
|
} catch (error) {
|
||||||
target: document.getElementById('player'),
|
console.error('Replay error:', error);
|
||||||
props: {
|
document.body.innerHTML = \`
|
||||||
events: events,
|
<div style="padding: 20px; text-align: center; color: #d32f2f;">
|
||||||
width: 1024,
|
<h2>回放失败</h2>
|
||||||
height: 576,
|
<p>\${error.message}</p>
|
||||||
autoPlay: true,
|
<p>请检查控制台(F12)获取详细错误信息。</p>
|
||||||
showController: true,
|
</div>
|
||||||
UNSAFE_replayCanvas: true,
|
\`;
|
||||||
plugins: [
|
}
|
||||||
getReplayConsolePlugin({
|
|
||||||
level: ['info', 'log', 'warn', 'error'],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
@@ -61,7 +80,7 @@ export const downloadHtmlInBackground = (events: unknown[]) => {
|
|||||||
filename: `replay-${Date.now()}.html`,
|
filename: `replay-${Date.now()}.html`,
|
||||||
saveAs: true,
|
saveAs: true,
|
||||||
})
|
})
|
||||||
.then((r) => console.log(r));
|
.then((r) => console.log('Download started:', r));
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(blob);
|
reader.readAsDataURL(blob);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user