Add SuperRPA integration guide, simple extension and standalone replay page
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
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>
This commit is contained in:
206
replay.html
Normal file
206
replay.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user