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
- docs/integration/superrpa-integration.zh_CN.md: complete integration guide - rrweb-simple-ext/: minimal Chrome extension for page recording - replay.html: standalone drag-and-drop replay viewer - CLAUDE.md: project instructions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
207 lines
7.0 KiB
HTML
207 lines
7.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>rrweb Replay</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; font-family: sans-serif; background: #1a1a2e; color: #eee; }
|
|
#dropzone {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
z-index: 999999; background: #1a1a2e;
|
|
}
|
|
#dropzone h2 { font-size: 22px; margin-bottom: 8px; }
|
|
#dropzone p { color: #aaa; margin-bottom: 20px; }
|
|
#dropzone .hint { font-size: 13px; color: #666; margin-top: 12px; }
|
|
#fileInput { display: none; }
|
|
.open-btn { padding: 12px 28px; border: 2px dashed #4CAF50; border-radius: 8px; background: transparent; color: #4CAF50; font-size: 16px; cursor: pointer; }
|
|
.open-btn:hover { background: #4CAF50; color: white; }
|
|
#controls {
|
|
position: fixed; top: 0; left: 0; right: 0; z-index: 99999;
|
|
background: #16213e; padding: 10px 16px; display: none; align-items: center; gap: 12px;
|
|
border-bottom: 1px solid #0f3460;
|
|
}
|
|
#controls button { padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer; color: white; font-size: 13px; }
|
|
#controls button:hover { opacity: 0.85; }
|
|
.btn-play { background: #4CAF50; }
|
|
.btn-pause { background: #ff9800; }
|
|
.btn-speed { background: #2196F3; }
|
|
.btn-open { background: #9C27B0; }
|
|
#timeline { flex: 1; height: 6px; -webkit-appearance: none; appearance: none; background: #0f3460; border-radius: 3px; outline: none; }
|
|
#timeline::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #e94560; cursor: pointer; }
|
|
#info { font-size: 12px; color: #aaa; min-width: 120px; text-align: right; }
|
|
#replay-container { margin-top: 50px; }
|
|
</style>
|
|
<!-- Embed replay CSS inline so no external files needed -->
|
|
<script src="packages/rrweb/dist/rrweb.umd.min.cjs"></script>
|
|
<link rel="stylesheet" href="packages/rrweb/dist/style.min.css">
|
|
</head>
|
|
<body>
|
|
<!-- Drop zone: show when no events loaded -->
|
|
<div id="dropzone">
|
|
<h2>rrweb Replay</h2>
|
|
<p>Drag & drop a recording JSON file here</p>
|
|
<button class="open-btn" onclick="document.getElementById('fileInput').click()">Choose File</button>
|
|
<input type="file" id="fileInput" accept=".json">
|
|
<div class="hint">Or paste JSON via Ctrl+V</div>
|
|
</div>
|
|
|
|
<!-- Player controls -->
|
|
<div id="controls">
|
|
<button class="btn-open" id="openBtn">Open</button>
|
|
<button class="btn-play" id="playBtn">Play</button>
|
|
<button class="btn-pause" id="pauseBtn">Pause</button>
|
|
<input type="range" id="timeline" min="0" max="100" value="0">
|
|
<span id="info">0 / 0s</span>
|
|
<button class="btn-speed" id="speedBtn">1x</button>
|
|
</div>
|
|
|
|
<div id="replay-container"></div>
|
|
|
|
<script>
|
|
const dropzone = document.getElementById('dropzone');
|
|
const controls = document.getElementById('controls');
|
|
const container = document.getElementById('replay-container');
|
|
const playBtn = document.getElementById('playBtn');
|
|
const pauseBtn = document.getElementById('pauseBtn');
|
|
const timeline = document.getElementById('timeline');
|
|
const infoEl = document.getElementById('info');
|
|
const speedBtn = document.getElementById('speedBtn');
|
|
const openBtn = document.getElementById('openBtn');
|
|
const fileInput = document.getElementById('fileInput');
|
|
|
|
let replayer = null;
|
|
let playing = false;
|
|
let totalMs = 0;
|
|
let startMs = 0;
|
|
const speeds = [1, 2, 4, 8];
|
|
let speedIdx = 0;
|
|
|
|
function formatTime(ms) {
|
|
const s = Math.floor(ms / 1000);
|
|
const m = Math.floor(s / 60);
|
|
return m > 0 ? m + ':' + String(s % 60).padStart(2, '0') : s + 's';
|
|
}
|
|
|
|
function updateInfo(currentMs) {
|
|
infoEl.textContent = formatTime(currentMs) + ' / ' + formatTime(totalMs);
|
|
timeline.value = Math.round((currentMs / totalMs) * 100);
|
|
}
|
|
|
|
function startReplay(events) {
|
|
if (replayer) replayer.destroy();
|
|
container.innerHTML = '';
|
|
dropzone.style.display = 'none';
|
|
controls.style.display = 'flex';
|
|
playing = false;
|
|
playBtn.textContent = 'Play';
|
|
|
|
replayer = new rrweb.Replayer(events, {
|
|
root: container,
|
|
speed: speeds[speedIdx],
|
|
skipInactive: true,
|
|
showWarning: true,
|
|
});
|
|
|
|
const meta = replayer.getMetaData();
|
|
totalMs = meta.totalTime;
|
|
startMs = meta.startTime;
|
|
updateInfo(0);
|
|
|
|
replayer.on('finish', () => {
|
|
playing = false;
|
|
playBtn.textContent = 'Play';
|
|
updateInfo(totalMs);
|
|
});
|
|
}
|
|
|
|
function handleFile(file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
try {
|
|
const events = JSON.parse(e.target.result);
|
|
if (!Array.isArray(events) || events.length === 0) {
|
|
alert('Invalid or empty events file');
|
|
return;
|
|
}
|
|
startReplay(events);
|
|
} catch(err) {
|
|
alert('Failed to parse JSON: ' + err.message);
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
// File input
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files[0]) handleFile(e.target.files[0]);
|
|
});
|
|
|
|
// Open button
|
|
openBtn.addEventListener('click', () => fileInput.click());
|
|
|
|
// Drag & drop
|
|
document.addEventListener('dragover', (e) => { e.preventDefault(); });
|
|
document.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
const file = e.dataTransfer.files[0];
|
|
if (file) handleFile(file);
|
|
});
|
|
|
|
// Paste JSON
|
|
document.addEventListener('paste', (e) => {
|
|
const text = e.clipboardData.getData('text');
|
|
try {
|
|
const events = JSON.parse(text);
|
|
if (Array.isArray(events) && events.length > 0) startReplay(events);
|
|
} catch {}
|
|
});
|
|
|
|
// Playback controls
|
|
playBtn.addEventListener('click', () => {
|
|
if (!playing && replayer) {
|
|
replayer.play();
|
|
playing = true;
|
|
playBtn.textContent = 'Playing...';
|
|
}
|
|
});
|
|
|
|
pauseBtn.addEventListener('click', () => {
|
|
if (replayer) {
|
|
replayer.pause();
|
|
playing = false;
|
|
playBtn.textContent = 'Play';
|
|
}
|
|
});
|
|
|
|
speedBtn.addEventListener('click', () => {
|
|
speedIdx = (speedIdx + 1) % speeds.length;
|
|
if (replayer) replayer.setConfig({ speed: speeds[speedIdx] });
|
|
speedBtn.textContent = speeds[speedIdx] + 'x';
|
|
});
|
|
|
|
timeline.addEventListener('input', () => {
|
|
if (!replayer) return;
|
|
const pct = parseInt(timeline.value) / 100;
|
|
const targetMs = totalMs * pct;
|
|
replayer.pause();
|
|
playing = false;
|
|
playBtn.textContent = 'Play';
|
|
replayer.play(targetMs);
|
|
setTimeout(() => { replayer.pause(); updateInfo(targetMs); }, 50);
|
|
});
|
|
|
|
// Timeline animation
|
|
function tick() {
|
|
if (playing && replayer) {
|
|
const ct = replayer.getCurrentTime();
|
|
updateInfo(ct);
|
|
}
|
|
requestAnimationFrame(tick);
|
|
}
|
|
tick();
|
|
</script>
|
|
</body>
|
|
</html>
|