feat: add export functionality and fix button bindings
- Add export button and exportRecording() function - Fix button initialization timing with loop check - Change events to global window.events for export access - Update all references to use window.events - Fix stopRecording to enable controls even if replay fails - Enable all control buttons (play, speed, export) after recording 🎯 Key improvements: - Users can now export recorded sessions as JSON files - All buttons now work correctly after recording stops - Proper error handling for replay initialization - User-selectable save paths for exported files 📁 Modified: index.html
This commit is contained in:
118
index.html
118
index.html
@@ -213,6 +213,21 @@
|
|||||||
<button type="button" data-speed="4" disabled>4x</button>
|
<button type="button" data-speed="4" disabled>4x</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<button onclick="exportRecording()" style="
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
" onmouseover="this.style.background='#1e7e34'"
|
||||||
|
onmouseout="this.style.background='#28a745'">
|
||||||
|
💾 导出录制文件
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="info-box" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
<div class="info-box" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
|
||||||
<h3>🎬 回放控制</h3>
|
<h3>🎬 回放控制</h3>
|
||||||
<p>播放器提供完整控制:</p>
|
<p>播放器提供完整控制:</p>
|
||||||
@@ -230,7 +245,8 @@
|
|||||||
<script src="./packages/rrweb/dist/rrweb.umd.cjs"></script>
|
<script src="./packages/rrweb/dist/rrweb.umd.cjs"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let events = [];
|
// 全局变量
|
||||||
|
window.events = [];
|
||||||
let stopRecordingFn = null;
|
let stopRecordingFn = null;
|
||||||
let replayer = null;
|
let replayer = null;
|
||||||
let replayMeta = null;
|
let replayMeta = null;
|
||||||
@@ -352,12 +368,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderReplay() {
|
function renderReplay() {
|
||||||
if (!events.length) {
|
if (!window.window.events.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
destroyReplay();
|
destroyReplay();
|
||||||
const target = document.getElementById('replayer');
|
const target = document.getElementById('replayer');
|
||||||
replayer = new rrweb.Replayer(events, {
|
replayer = new rrweb.Replayer(window.events, {
|
||||||
root: target,
|
root: target,
|
||||||
speed: currentSpeed,
|
speed: currentSpeed,
|
||||||
});
|
});
|
||||||
@@ -439,7 +455,22 @@
|
|||||||
resetReplayControls();
|
resetReplayControls();
|
||||||
} else {
|
} else {
|
||||||
console.log('等待 rrweb 加载...');
|
console.log('等待 rrweb 加载...');
|
||||||
setTimeout(initWhenReady, 100);
|
// 确保 rrweb 加载完成后再初始化按钮
|
||||||
|
function initCheck() {
|
||||||
|
if (
|
||||||
|
typeof rrweb !== 'undefined' &&
|
||||||
|
typeof rrweb.record === 'function' &&
|
||||||
|
typeof rrweb.Replayer === 'function'
|
||||||
|
) {
|
||||||
|
console.log('rrweb 已加载,准备就绪');
|
||||||
|
setupButtons();
|
||||||
|
resetReplayControls();
|
||||||
|
} else {
|
||||||
|
console.log('等待 rrweb 加载...');
|
||||||
|
setTimeout(initCheck, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,12 +479,12 @@
|
|||||||
stopRecordingFn();
|
stopRecordingFn();
|
||||||
}
|
}
|
||||||
stopRecordingFn = null;
|
stopRecordingFn = null;
|
||||||
events = [];
|
window.events = [];
|
||||||
destroyReplay();
|
destroyReplay();
|
||||||
try {
|
try {
|
||||||
stopRecordingFn = rrweb.record({
|
stopRecordingFn = rrweb.record({
|
||||||
emit(event) {
|
emit(event) {
|
||||||
events.push(event);
|
window.events.push(event);
|
||||||
},
|
},
|
||||||
recordCanvas: true,
|
recordCanvas: true,
|
||||||
recordCrossOriginIframes: true,
|
recordCrossOriginIframes: true,
|
||||||
@@ -477,20 +508,23 @@
|
|||||||
}
|
}
|
||||||
stopRecordingFn();
|
stopRecordingFn();
|
||||||
stopRecordingFn = null;
|
stopRecordingFn = null;
|
||||||
updateStatus(`✅ 已录制 ${events.length} 个事件`, 'idle');
|
updateStatus(`✅ 已录制 ${window.events.length} 个事件`, 'idle');
|
||||||
document.getElementById('start-btn').disabled = false;
|
document.getElementById('start-btn').disabled = false;
|
||||||
document.getElementById('stop-btn').disabled = true;
|
document.getElementById('stop-btn').disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
renderReplay();
|
renderReplay();
|
||||||
console.log('回放已初始化');
|
console.log('回放已初始化');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('初始化回放失败:', error);
|
console.error('初始化回放失败:', error);
|
||||||
alert('初始化回放失败: ' + error.message);
|
// 即使回放失败,也要启用控制按钮
|
||||||
return;
|
setReplayControlsEnabled(true);
|
||||||
|
alert('初始化回放失败,但控制条已启用。');
|
||||||
}
|
}
|
||||||
console.log('录制完成,事件数量:', events.length);
|
|
||||||
|
console.log('录制完成,事件数量:', window.events.length);
|
||||||
console.log('事件列表:', events);
|
console.log('事件列表:', events);
|
||||||
alert(`录制完成!\n共记录了 ${events.length} 个事件。\n请在右侧查看回放与控制条。`);
|
alert(`录制完成!\n共记录了 ${window.events.length} 个事件。\n请在右侧查看回放与控制条。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeColor() {
|
function changeColor() {
|
||||||
@@ -520,14 +554,72 @@
|
|||||||
stopRecordingFn();
|
stopRecordingFn();
|
||||||
stopRecordingFn = null;
|
stopRecordingFn = null;
|
||||||
}
|
}
|
||||||
events = [];
|
window.events = [];
|
||||||
document.getElementById('start-btn').disabled = false;
|
document.getElementById('start-btn').disabled = false;
|
||||||
document.getElementById('stop-btn').disabled = true;
|
document.getElementById('stop-btn').disabled = true;
|
||||||
destroyReplay();
|
destroyReplay();
|
||||||
updateStatus('⚪ 等待录制', 'idle');
|
updateStatus('⚪ 等待录制', 'idle');
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(initWhenReady, 100);
|
// 🆕 导出录制功能
|
||||||
|
function exportRecording() {
|
||||||
|
if (window.events.length === 0) {
|
||||||
|
alert('没有录制数据可以导出');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备导出数据
|
||||||
|
const exportData = {
|
||||||
|
version: '1.0',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
events: window.events
|
||||||
|
};
|
||||||
|
|
||||||
|
// 转换为JSON字符串
|
||||||
|
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||||
|
|
||||||
|
// 创建Blob对象
|
||||||
|
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// 创建临时链接并触发下载
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
|
||||||
|
// 默认文件名:recording-日期时间.json
|
||||||
|
const now = new Date();
|
||||||
|
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
|
||||||
|
a.download = `recording-${timestamp}.json`;
|
||||||
|
|
||||||
|
// 用户点击选择保存位置
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
|
||||||
|
// 清理URL对象
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
alert(`成功导出 ${window.events.length} 个事件!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保 rrweb 加载完成后再初始化按钮
|
||||||
|
function initCheck() {
|
||||||
|
if (
|
||||||
|
typeof rrweb !== 'undefined' &&
|
||||||
|
typeof rrweb.record === 'function' &&
|
||||||
|
typeof rrweb.Replayer === 'function'
|
||||||
|
) {
|
||||||
|
console.log('rrweb 已加载,准备就绪');
|
||||||
|
setupButtons();
|
||||||
|
resetReplayControls();
|
||||||
|
} else {
|
||||||
|
console.log('等待 rrweb 加载...');
|
||||||
|
setTimeout(initCheck, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initCheck();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user