
关于
使用 Playwright 录制精美的 UI 演示视频。适用于用户需要创建 Web 应用的演示、操作指南、屏幕录制或教程视频时。生成带可见光标、自然节奏和专业感的 WebM 视频。
name: ui-demo description: 使用 Playwright 录制精美的 UI 演示视频。当用户要求创建演示、操作演示、屏幕录制或 Web 应用教程视频时使用。生成带有可见光标、自然节奏和专业感的 WebM 视频。 origin: ECC
UI 演示视频录制器
使用 Playwright 的视频录制功能录制精美的 Web 应用演示视频,包含注入的光标覆盖层、自然节奏和叙事流程。
使用场景
- 用户要求"演示视频"、"屏幕录制"、"操作演示"或"教程"
- 用户想要直观展示某个功能或工作流
- 用户需要用于文档、入职培训或利益相关者演示的视频
三阶段流程
每个演示都经过三个阶段:探索 -> 排练 -> 录制。永远不要直接跳到录制。
阶段 1:探索
在编写任何脚本之前,探索目标页面以了解实际内容。
原因
你无法为未见过的内容编写脚本。字段可能是 <input> 而非 <textarea>,下拉菜单可能是自定义组件而非 <select>,评论框可能支持 @提及 或 #标签。假设会导致录制静默失败。
方法
导航到流程中的每个页面并导出其交互元素:
// 在编写演示脚本之前,对流程中的每个页面运行此代码
const fields = await page.evaluate(() => {
const els = [];
document.querySelectorAll('input, select, textarea, button, [contenteditable]').forEach(el => {
if (el.offsetParent !== null) {
els.push({
tag: el.tagName,
type: el.type || '',
name: el.name || '',
placeholder: el.placeholder || '',
text: el.textContent?.trim().substring(0, 40) || '',
contentEditable: el.contentEditable === 'true',
role: el.getAttribute('role') || '',
});
}
});
return els;
});
console.log(JSON.stringify(fields, null, 2));
关注要点
- 表单字段:是
<select>、<input>、自定义下拉还是组合框? - 选择选项:导出选项值和文本。占位符通常有
value="0"或value="",看起来非空。使用Array.from(el.options).map(o => ({ value: o.value, text: o.text }))。跳过文本包含"Select"或值为"0"的选项。 - 富文本:评论框是否支持
@提及、#标签、markdown 或表情?检查占位符文本。 - 必填字段:哪些字段会阻止表单提交?检查
required、标签中的*,尝试空提交查看验证错误。 - 动态内容:字段是否在其他字段填写后才出现?
- 按钮标签:确切文本如
"Submit"、"Submit Request"或"Send"。 - 表格列标题:对于表格驱动的模态框,将每个
input[type="number"]映射到其列标题,而非假设所有数字输入含义相同。
输出
每个页面的字段映射,用于在脚本中编写正确的选择器。示例:
/purchase-requests/new:
- Budget Code: <select>(页面上第一个 select,4 个选项)
- Desired Delivery: <input type="date">
- Context: <textarea>(不是 input)
- BOM table: 内联可编辑单元格,span.cursor-pointer -> input 模式
- Submit: <button> text="Submit"
/purchase-requests/N (detail):
- Comment: <input placeholder="Type a message..."> 支持 @user 和 #PR 标签
- Send: <button> text="Send"(输入有内容前禁用)
阶段 2:排练
在不录制的情况下运行所有步骤。验证每个选择器都能解析。
原因
静默的选择器失败是演示录制中断的主要原因。排练在浪费录制之前捕获它们。
方法
使用 ensureVisible,一个记录日志并大声失败的包装器:
async function ensureVisible(page, locator, label) {
const el = typeof locator === 'string' ? page.locator(locator).first() : locator;
const visible = await el.isVisible().catch(() => false);
if (!visible) {
const msg = \`REHEARSAL FAIL: "${label}" not found - selector: ${typeof locator === 'string' ? locator : '(locator object)'}\`;
console.error(msg);
const found = await page.evaluate(() => {
return Array.from(document.querySelectorAll('button, input, select, textarea, a'))
.filter(el => el.offsetParent !== null)
.map(el => \`${el.tagName}[${el.type || ''}] "${el.textContent?.trim().substring(0, 30)}"\`)
.join('\n ');
});
console.error(' Visible elements:\n ' + found);
return false;
}
console.log(\`REHEARSAL OK: "${label}"\`);
return true;
}
排练脚本结构
const steps = [
{ label: '登录邮箱字段', selector: '#email' },
{ label: '登录提交', selector: 'button[type="submit"]' },
{ label: '新建请求按钮', selector: 'button:has-text("New Request")' },
{ label: '预算代码选择', selector: 'select' },
{ label: '交付日期', selector: 'input[type="date"]:visible' },
{ label: '描述字段', selector: 'textarea' },
];
for (const step of steps) {
const ok = await ensureVisible(page, step.selector, step.label);
if (!ok) process.exit(1);
}
阶段 3:录制
排练通过后,使用视频录制和光标注入进行最终录制。
录制设置
const { chromium } = require('playwright');
const browser = await chromium.launch();
const context = await browser.newContext({
recordVideo: {
dir: './videos/',
size: { width: 1280, height: 720 }
}
});
const page = await context.newPage();
光标注入
// 在页面加载后注入可见光标
await page.addStyleTag({
content: \`
* { cursor: none !important; }
.demo-cursor {
position: fixed;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(255, 0, 0, 0.6);
pointer-events: none;
z-index: 99999;
transition: all 0.1s ease;
}
\`
});
await page.evaluate(() => {
const cursor = document.createElement('div');
cursor.className = 'demo-cursor';
document.body.appendChild(cursor);
document.addEventListener('mousemove', (e) => {
cursor.style.left = e.clientX - 10 + 'px';
cursor.style.top = e.clientY - 10 + 'px';
});
});
自然节奏
// 在操作之间添加自然延迟
async function naturalPause(ms = 800) {
await page.waitForTimeout(ms + Math.random() * 400);
}
// 模拟自然打字
async function naturalType(selector, text) {
await page.click(selector);
await naturalPause(300);
for (const char of text) {
await page.keyboard.type(char, { delay: 50 + Math.random() * 80 });
}
await naturalPause(500);
}
最佳实践
- 始终先探索再编写脚本
- 排练验证所有选择器
- 使用自然节奏避免机器人感
- 保持视频简短聚焦(30-90 秒)
- 在关键操作后添加停顿让观众理解
兼容工具
Claude CodeCursor
标签
通用
