Files
rrweb/index-backup.html
xugp 71438691b3
Some checks failed
Tests / Tests (push) Has been cancelled
ESLint Check / ESLint Check and Report Upload (push) Has been cancelled
Prettier Check / Format Check (push) Has been cancelled
Prettier Check / Format Code (push) Has been cancelled
ESLint Check / Build Base for Bundle Size Comparison (push) Has been cancelled
feat: enhance web extension with export functionality and utility improvements
- Add export functionality to SessionList and Player pages
- Add new utility modules: dataOperations, format, path, settings
- Update manifest with export and download permissions
- Enhance storage utility with new data operations
- Add various test scripts and documentation files
2026-04-16 10:44:50 +08:00

534 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>rrweb 示例</title>
<style>
body {
font-family: 'Segoe UI', Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 {
color: #333;
border-bottom: 3px solid #007bff;
padding-bottom: 10px;
}
.container {
display: flex;
gap: 20px;
}
.panel {
flex: 1;
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
background: white;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.test-area {
min-height: 300px;
border: 2px dashed #007bff;
padding: 20px;
margin: 15px 0;
border-radius: 8px;
background: #f8f9fa;
}
button {
padding: 12px 24px;
margin: 8px;
cursor: pointer;
background: #007bff;
color: white;
border: none;
border-radius: 6px;
font-size: 15px;
font-weight: 600;
}
button:hover {
background: #0056b3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,123,255,0.4);
}
button:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.record-btn {
background: #28a745;
min-width: 120px;
}
.record-btn:hover {
background: #1e7e34;
}
.stop-btn {
background: #dc3545;
min-width: 120px;
}
.stop-btn:hover {
background: #b02a37;
}
.status-bar {
padding: 15px;
margin: 15px 0;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
text-align: center;
}
.status-bar.idle {
background: #e2e3e5;
color: #383d41;
}
.status-bar.recording {
background: #d4edda;
color: #155724;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.info-box {
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
color: white;
margin-top: 15px;
}
.info-box h3 {
margin-top: 0;
}
.info-box ul {
margin: 10px 0;
padding-left: 20px;
}
.replay-controls {
margin-top: 15px;
padding: 15px;
border: 1px solid #d8e2ef;
border-radius: 8px;
background: #fff;
}
.replay-controls.disabled {
opacity: 0.6;
}
.replay-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.timeline {
flex: 1 1 240px;
margin: 0;
accent-color: #007bff;
}
.time-label {
min-width: 48px;
text-align: center;
color: #495057;
font-variant-numeric: tabular-nums;
}
.speed-controls {
display: flex;
gap: 8px;
margin-top: 12px;
flex-wrap: wrap;
}
.speed-controls button,
#play-toggle-btn {
margin: 0;
}
.speed-controls button.active {
background: #17a2b8;
}
@media (max-width: 900px) {
.container {
flex-direction: column;
}
#replayer {
height: 320px !important;
}
}
</style>
</head>
<body>
<h1>🎥 rrweb 录制与回放</h1>
<div class="container">
<div class="panel">
<h2>📹 录制区域</h2>
<div class="test-area">
<h3 style="margin-top:0;">在此区域进行操作</h3>
<p>点击按钮、输入文字,所有操作都会被记录:</p>
<button onclick="changeColor()">🎨 随机变色</button>
<button onclick="addCounter()">🔢 添加计数器</button>
<button onclick="showAlert()">💬 测试弹窗</button>
<div id="counters" style="display:flex; gap:10px; flex-wrap:wrap; margin-top:15px;"></div>
</div>
<div id="status-bar" class="status-bar idle">
⚪ 等待录制
</div>
<div style="margin: 15px 0;">
<button id="start-btn" class="record-btn" onclick="startRecording()">▶ 开始录制</button>
<button id="stop-btn" class="stop-btn" onclick="stopRecording()" disabled>⏹ 停止录制</button>
<button onclick="clearAll()">🗑️ 清空</button>
</div>
<div class="info-box">
<h3>✨ 功能说明</h3>
<ul>
<li>点击"开始录制"开始记录</li>
<li>点击"停止录制"结束记录</li>
<li>右侧会自动回放录制的操作</li>
<li>支持播放速度控制</li>
</ul>
</div>
</div>
<div class="panel">
<h2>▶️ 回放区域</h2>
<div id="replayer" style="width:100%; height:400px; border:2px solid #007bff; border-radius:8px; background:#f8f9fa;"></div>
<div id="replay-controls" class="replay-controls disabled">
<div class="replay-row">
<button id="play-toggle-btn" type="button" disabled>▶ 播放</button>
<span id="current-time" class="time-label">00:00</span>
<input id="timeline" class="timeline" type="range" min="0" max="0" step="1" value="0" disabled>
<span id="total-time" class="time-label">00:00</span>
</div>
<div class="speed-controls">
<button type="button" data-speed="0.5" disabled>0.5x</button>
<button type="button" data-speed="1" class="active" disabled>1x</button>
<button type="button" data-speed="2" disabled>2x</button>
<button type="button" data-speed="4" disabled>4x</button>
</div>
</div>
<div class="info-box" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<h3>🎬 回放控制</h3>
<p>播放器提供完整控制:</p>
<ul>
<li>时间轴拖动:点击任意位置跳转</li>
<li>播放/暂停:控制播放状态</li>
<li>速度控制0.5x、1x、2x、4x</li>
</ul>
</div>
</div>
</div>
<!-- rrweb 浏览器版本 -->
<link rel="stylesheet" href="./packages/rrweb/dist/style.css">
<script src="./packages/rrweb/dist/rrweb.umd.cjs"></script>
<script>
let events = [];
let stopRecordingFn = null;
let replayer = null;
let replayMeta = null;
let replayFrame = null;
let isReplaying = false;
let currentSpeed = 1;
function getReplayElements() {
return {
controls: document.getElementById('replay-controls'),
playToggle: document.getElementById('play-toggle-btn'),
timeline: document.getElementById('timeline'),
currentTime: document.getElementById('current-time'),
totalTime: document.getElementById('total-time'),
speedButtons: Array.from(document.querySelectorAll('#replay-controls [data-speed]')),
};
}
function formatTime(ms) {
const safeMs = Math.max(0, Math.floor(ms));
const totalSeconds = Math.floor(safeMs / 1000);
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
const seconds = String(totalSeconds % 60).padStart(2, '0');
if (safeMs < 60000) {
const tenths = Math.floor((safeMs % 1000) / 100);
return `${minutes}:${seconds}.${tenths}`;
}
return `${minutes}:${seconds}`;
}
function updateStatus(text, type) {
const statusEl = document.getElementById('status-bar');
statusEl.className = `status-bar ${type}`;
statusEl.textContent = text;
}
function updatePlayToggle() {
const { playToggle } = getReplayElements();
playToggle.textContent = isReplaying ? '⏸ 暂停' : '▶ 播放';
}
function setActiveSpeed(speed) {
currentSpeed = speed;
const { speedButtons } = getReplayElements();
speedButtons.forEach((button) => {
button.classList.toggle('active', Number(button.dataset.speed) === speed);
});
}
function setReplayControlsEnabled(enabled) {
const { controls, playToggle, timeline, speedButtons } = getReplayElements();
controls.classList.toggle('disabled', !enabled);
playToggle.disabled = !enabled;
timeline.disabled = !enabled;
speedButtons.forEach((button) => {
button.disabled = !enabled;
});
}
function syncReplayUI() {
if (!replayer || !replayMeta) {
return;
}
const { timeline, currentTime, totalTime } = getReplayElements();
const timeOffset = Math.min(replayer.getCurrentTime(), replayMeta.totalTime);
timeline.max = String(replayMeta.totalTime);
timeline.value = String(Math.max(0, Math.floor(timeOffset)));
currentTime.textContent = formatTime(timeOffset);
totalTime.textContent = formatTime(replayMeta.totalTime);
if (isReplaying && timeOffset >= replayMeta.totalTime) {
isReplaying = false;
updatePlayToggle();
stopReplayLoop();
}
}
function stopReplayLoop() {
if (replayFrame !== null) {
cancelAnimationFrame(replayFrame);
replayFrame = null;
}
}
function startReplayLoop() {
stopReplayLoop();
const tick = () => {
syncReplayUI();
if (isReplaying && replayer) {
replayFrame = requestAnimationFrame(tick);
} else {
replayFrame = null;
}
};
replayFrame = requestAnimationFrame(tick);
}
function resetReplayControls() {
replayMeta = null;
isReplaying = false;
stopReplayLoop();
setReplayControlsEnabled(false);
setActiveSpeed(1);
updatePlayToggle();
const { timeline, currentTime, totalTime } = getReplayElements();
timeline.max = '0';
timeline.value = '0';
currentTime.textContent = '00:00';
totalTime.textContent = '00:00';
}
function destroyReplay() {
stopReplayLoop();
if (replayer && typeof replayer.destroy === 'function') {
replayer.destroy();
}
replayer = null;
document.getElementById('replayer').innerHTML = '';
resetReplayControls();
}
function renderReplay() {
if (!events.length) {
return;
}
destroyReplay();
const target = document.getElementById('replayer');
replayer = new rrweb.Replayer(events, {
root: target,
speed: currentSpeed,
});
replayMeta = replayer.getMetaData();
setReplayControlsEnabled(true);
syncReplayUI();
replayer.play();
isReplaying = true;
updatePlayToggle();
startReplayLoop();
}
function toggleReplay() {
if (!replayer || !replayMeta) {
return;
}
const { timeline } = getReplayElements();
const timeOffset = Number(timeline.value);
if (isReplaying) {
replayer.pause();
isReplaying = false;
stopReplayLoop();
} else {
replayer.play(timeOffset);
isReplaying = true;
startReplayLoop();
}
updatePlayToggle();
syncReplayUI();
}
function seekReplay(timeOffset) {
if (!replayer || !replayMeta) {
return;
}
const safeOffset = Math.max(0, Math.min(timeOffset, replayMeta.totalTime));
if (isReplaying) {
replayer.play(safeOffset);
startReplayLoop();
} else {
replayer.pause(safeOffset);
}
syncReplayUI();
}
function changeReplaySpeed(speed) {
setActiveSpeed(speed);
if (replayer) {
replayer.setConfig({ speed });
}
}
function setupButtons() {
document.getElementById('start-btn').onclick = startRecording;
document.getElementById('stop-btn').onclick = stopRecording;
document.getElementById('play-toggle-btn').onclick = toggleReplay;
document.getElementById('timeline').addEventListener('input', (event) => {
const value = Number(event.target.value);
getReplayElements().currentTime.textContent = formatTime(value);
});
document.getElementById('timeline').addEventListener('change', (event) => {
seekReplay(Number(event.target.value));
});
getReplayElements().speedButtons.forEach((button) => {
button.addEventListener('click', () => {
changeReplaySpeed(Number(button.dataset.speed));
});
});
}
function initWhenReady() {
if (
typeof rrweb !== 'undefined' &&
typeof rrweb.record === 'function' &&
typeof rrweb.Replayer === 'function'
) {
console.log('rrweb 已加载,准备就绪');
setupButtons();
resetReplayControls();
} else {
console.log('等待 rrweb 加载...');
setTimeout(initWhenReady, 100);
}
}
function startRecording() {
if (typeof stopRecordingFn === 'function') {
stopRecordingFn();
}
stopRecordingFn = null;
events = [];
destroyReplay();
try {
stopRecordingFn = rrweb.record({
emit(event) {
events.push(event);
},
recordCanvas: true,
recordCrossOriginIframes: true,
recordAfter: 'DOMContentLoaded',
ignoreSelector: '.status-bar, .info-box, #replayer, #replay-controls',
});
updateStatus('🔴 正在录制...', 'recording');
document.getElementById('start-btn').disabled = true;
document.getElementById('stop-btn').disabled = false;
console.log('录制已启动');
} catch (error) {
updateStatus('⚪ 等待录制', 'idle');
console.error('启动录制失败:', error);
alert('启动录制失败: ' + error.message);
}
}
function stopRecording() {
if (typeof stopRecordingFn !== 'function') {
return;
}
stopRecordingFn();
stopRecordingFn = null;
updateStatus(`✅ 已录制 ${events.length} 个事件`, 'idle');
document.getElementById('start-btn').disabled = false;
document.getElementById('stop-btn').disabled = true;
try {
renderReplay();
console.log('回放已初始化');
} catch (error) {
console.error('初始化回放失败:', error);
alert('初始化回放失败: ' + error.message);
return;
}
console.log('录制完成,事件数量:', events.length);
console.log('事件列表:', events);
alert(`录制完成!\n共记录了 ${events.length} 个事件。\n请在右侧查看回放与控制条。`);
}
function changeColor() {
const colors = ['#f8f9fa', '#e3f2fd', '#fff3cd', '#d1ecf1d', '#f8d7da', '#d6d8db', '#cce5ff', '#e2d9f7'];
document.querySelector('.test-area').style.background = colors[Math.floor(Math.random() * colors.length)];
}
function addCounter() {
const counters = document.getElementById('counters');
const count = counters.children.length;
const div = document.createElement('div');
div.style.cssText = 'padding:10px 20px; background:white; border:2px solid #007bff; border-radius:8px; font-size:24px; font-weight:bold; cursor:pointer;';
div.innerHTML = `<span>#${count + 1}</span>`;
div.onclick = function() {
const span = this.querySelector('span');
span.innerText = parseInt(span.innerText) + 1;
};
counters.appendChild(div);
}
function showAlert() {
alert('这是一个测试弹窗!\nrrweb 会记录并回放这个弹窗操作。');
}
function clearAll() {
if (typeof stopRecordingFn === 'function') {
stopRecordingFn();
stopRecordingFn = null;
}
events = [];
document.getElementById('start-btn').disabled = false;
document.getElementById('stop-btn').disabled = true;
destroyReplay();
updateStatus('⚪ 等待录制', 'idle');
}
setTimeout(initWhenReady, 100);
</script>
</body>
</html>