剪贴板API进阶

剪贴板API概述

剪贴板API(Clipboard API)提供了异步访问剪贴板的能力,可以读取和写入文本、图片等多种格式的数据。相比传统的document.execCommand()方法,新的剪贴板API更加现代化和安全。

东巴文(db-w.cn) 认为:剪贴板API让Web应用能够更好地与系统剪贴板交互,提升用户体验。

剪贴板API特点

核心特点

特点 说明
异步操作 使用Promise,不阻塞主线程
权限管理 需要用户授权,更安全
多种格式 支持文本、图片等多种格式
现代API 替代传统的execCommand方法
跨浏览器 主流浏览器支持良好

API方法

// 剪贴板API方法
const ClipboardMethods = {
    // 写入文本
    writeText: navigator.clipboard.writeText,
    
    // 读取文本
    readText: navigator.clipboard.readText,
    
    // 写入多种格式
    write: navigator.clipboard.write,
    
    // 读取多种格式
    read: navigator.clipboard.read
};

写入剪贴板

写入文本

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>写入剪贴板示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .copy-container {
            background: #f9f9f9;
            padding: 30px;
            border-radius: 10px;
            margin: 20px 0;
        }
        
        .text-area {
            width: 100%;
            min-height: 120px;
            padding: 15px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 16px;
            resize: vertical;
            margin-bottom: 15px;
        }
        
        .button-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        
        button {
            padding: 12px 24px;
            font-size: 16px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: background 0.3s;
        }
        
        button:hover {
            background: #0056b3;
        }
        
        button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        
        .copy-btn {
            background: #28a745;
        }
        
        .copy-btn:hover {
            background: #218838;
        }
        
        .notification {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 15px 25px;
            background: #28a745;
            color: white;
            border-radius: 5px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
            transform: translateX(200%);
            transition: transform 0.3s;
        }
        
        .notification.show {
            transform: translateX(0);
        }
        
        .notification.error {
            background: #dc3545;
        }
        
        .examples {
            margin-top: 30px;
        }
        
        .example-item {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 15px;
            background: white;
            border-radius: 5px;
            margin-bottom: 10px;
        }
        
        .example-item code {
            flex: 1;
            padding: 10px;
            background: #f5f5f5;
            border-radius: 3px;
            font-family: 'Courier New', monospace;
        }
        
        .example-item button {
            padding: 8px 16px;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <h1>写入剪贴板示例</h1>
    
    <div class="copy-container">
        <h2>复制文本</h2>
        <textarea class="text-area" id="textInput" placeholder="输入要复制的文本...">这是一段示例文本,可以复制到剪贴板。</textarea>
        <div class="button-group">
            <button class="copy-btn" onclick="copyText()">复制文本</button>
            <button onclick="copySpecialText()">复制特殊文本</button>
            <button onclick="copyMultiple()">批量复制</button>
        </div>
    </div>
    
    <div class="examples">
        <h2>快速复制示例</h2>
        
        <div class="example-item">
            <code>https://db-w.cn</code>
            <button onclick="copyToClipboard('https://db-w.cn')">复制</button>
        </div>
        
        <div class="example-item">
            <code>contact@db-w.cn</code>
            <button onclick="copyToClipboard('contact@db-w.cn')">复制</button>
        </div>
        
        <div class="example-item">
            <code>console.log('Hello, World!');</code>
            <button onclick="copyToClipboard('console.log(\'Hello, World!\');')">复制</button>
        </div>
    </div>
    
    <div class="notification" id="notification"></div>
    
    <script>
        // 复制文本
        async function copyText() {
            const text = document.getElementById('textInput').value;
            
            if (!text) {
                showNotification('请输入要复制的文本', 'error');
                return;
            }
            
            try {
                await navigator.clipboard.writeText(text);
                showNotification('文本已复制到剪贴板!');
            } catch (err) {
                showNotification('复制失败: ' + err.message, 'error');
            }
        }
        
        // 复制特殊文本
        async function copySpecialText() {
            const specialText = `
特殊文本示例:
• 时间:${new Date().toLocaleString()}
• 随机数:${Math.random().toFixed(6)}
• 用户代理:${navigator.userAgent}
            `.trim();
            
            try {
                await navigator.clipboard.writeText(specialText);
                showNotification('特殊文本已复制!');
            } catch (err) {
                showNotification('复制失败: ' + err.message, 'error');
            }
        }
        
        // 批量复制
        async function copyMultiple() {
            const items = [
                '第一项内容',
                '第二项内容',
                '第三项内容'
            ];
            
            const text = items.join('\n');
            
            try {
                await navigator.clipboard.writeText(text);
                showNotification('批量内容已复制!');
            } catch (err) {
                showNotification('复制失败: ' + err.message, 'error');
            }
        }
        
        // 复制到剪贴板
        async function copyToClipboard(text) {
            try {
                await navigator.clipboard.writeText(text);
                showNotification('已复制: ' + text);
            } catch (err) {
                showNotification('复制失败: ' + err.message, 'error');
            }
        }
        
        // 显示通知
        function showNotification(message, type = 'success') {
            const notification = document.getElementById('notification');
            notification.textContent = message;
            notification.className = 'notification ' + type + ' show';
            
            setTimeout(() => {
                notification.classList.remove('show');
            }, 3000);
        }
        
        // 检查API支持
        if (!navigator.clipboard) {
            showNotification('您的浏览器不支持剪贴板API', 'error');
        }
    </script>
</body>
</html>

读取剪贴板

读取文本

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>读取剪贴板示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .paste-container {
            background: #f9f9f9;
            padding: 30px;
            border-radius: 10px;
            margin: 20px 0;
        }
        
        .paste-area {
            width: 100%;
            min-height: 150px;
            padding: 15px;
            border: 2px dashed #ddd;
            border-radius: 5px;
            font-size: 16px;
            resize: vertical;
            margin-bottom: 15px;
            background: white;
            transition: border-color 0.3s;
        }
        
        .paste-area:focus {
            outline: none;
            border-color: #007bff;
        }
        
        button {
            padding: 12px 24px;
            font-size: 16px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            margin-right: 10px;
        }
        
        button:hover {
            background: #0056b3;
        }
        
        .paste-btn {
            background: #17a2b8;
        }
        
        .paste-btn:hover {
            background: #138496;
        }
        
        .info-box {
            background: #e7f3ff;
            border-left: 4px solid #007bff;
            padding: 15px;
            margin: 20px 0;
            border-radius: 5px;
        }
        
        .permission-status {
            display: inline-block;
            padding: 5px 10px;
            border-radius: 3px;
            font-size: 14px;
            margin-left: 10px;
        }
        
        .permission-status.granted {
            background: #d4edda;
            color: #155724;
        }
        
        .permission-status.denied {
            background: #f8d7da;
            color: #721c24;
        }
        
        .permission-status.prompt {
            background: #fff3cd;
            color: #856404;
        }
    </style>
</head>
<body>
    <h1>读取剪贴板示例</h1>
    
    <div class="paste-container">
        <h2>粘贴文本 <span class="permission-status" id="permissionStatus">检查权限中...</span></h2>
        
        <textarea class="paste-area" id="pasteArea" placeholder="点击下方按钮粘贴剪贴板内容,或直接在此处粘贴..."></textarea>
        
        <button class="paste-btn" onclick="pasteText()">从剪贴板粘贴</button>
        <button onclick="checkPermission()">检查权限</button>
        <button onclick="requestPermission()">请求权限</button>
    </div>
    
    <div class="info-box">
        <strong>说明:</strong>
        <ul>
            <li>读取剪贴板需要用户授权</li>
            <li>某些浏览器要求页面必须是活动标签页</li>
            <li>用户也可以使用Ctrl+V或右键菜单粘贴</li>
        </ul>
    </div>
    
    <script>
        // 检查权限
        async function checkPermission() {
            const statusElem = document.getElementById('permissionStatus');
            
            try {
                const result = await navigator.permissions.query({ name: 'clipboard-read' });
                
                statusElem.textContent = result.state;
                statusElem.className = 'permission-status ' + result.state;
                
                // 监听权限变化
                result.onchange = function() {
                    statusElem.textContent = this.state;
                    statusElem.className = 'permission-status ' + this.state;
                };
                
            } catch (err) {
                statusElem.textContent = '不支持';
                statusElem.className = 'permission-status';
            }
        }
        
        // 请求权限
        async function requestPermission() {
            try {
                const result = await navigator.permissions.query({ name: 'clipboard-read' });
                
                if (result.state === 'prompt') {
                    // 尝试读取剪贴板,这会触发权限请求
                    await navigator.clipboard.readText();
                }
                
                checkPermission();
            } catch (err) {
                console.error('权限请求失败:', err);
            }
        }
        
        // 粘贴文本
        async function pasteText() {
            try {
                const text = await navigator.clipboard.readText();
                document.getElementById('pasteArea').value = text;
                console.log('已从剪贴板粘贴文本');
            } catch (err) {
                console.error('粘贴失败:', err);
                alert('无法读取剪贴板。请确保已授权剪贴板访问权限。');
            }
        }
        
        // 监听粘贴事件
        document.getElementById('pasteArea').addEventListener('paste', function(e) {
            console.log('用户粘贴了内容');
            
            // 可以阻止默认行为,自定义处理
            // e.preventDefault();
            // const text = e.clipboardData.getData('text');
            // this.value = text;
        });
        
        // 初始检查
        checkPermission();
    </script>
</body>
</html>

写入多种格式

使用ClipboardItem

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>写入多种格式示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .format-container {
            background: #f9f9f9;
            padding: 30px;
            border-radius: 10px;
            margin: 20px 0;
        }
        
        .format-item {
            margin-bottom: 20px;
        }
        
        .format-item label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        
        textarea {
            width: 100%;
            min-height: 100px;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
            resize: vertical;
        }
        
        button {
            padding: 12px 24px;
            font-size: 16px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            margin: 5px;
        }
        
        button:hover {
            background: #0056b3;
        }
        
        .result {
            margin-top: 20px;
            padding: 15px;
            background: white;
            border-radius: 5px;
            border-left: 4px solid #28a745;
        }
        
        .result.error {
            border-left-color: #dc3545;
            background: #fff5f5;
        }
    </style>
</head>
<body>
    <h1>写入多种格式示例</h1>
    
    <div class="format-container">
        <h2>复制多种格式</h2>
        
        <div class="format-item">
            <label>纯文本格式:</label>
            <textarea id="plainText">这是纯文本内容</textarea>
        </div>
        
        <div class="format-item">
            <label>HTML格式:</label>
            <textarea id="htmlText"><strong>这是HTML内容</strong>,包含<strong>加粗</strong><em>斜体</em></textarea>
        </div>
        
        <button onclick="copyMultipleFormats()">复制多种格式</button>
        <button onclick="copyAsHTML()">仅复制HTML</button>
        <button onclick="copyAsText()">仅复制文本</button>
        
        <div class="result" id="result" style="display: none;"></div>
    </div>
    
    <script>
        // 复制多种格式
        async function copyMultipleFormats() {
            const plainText = document.getElementById('plainText').value;
            const htmlText = document.getElementById('htmlText').value;
            
            try {
                // 创建ClipboardItem
                const data = [
                    new ClipboardItem({
                        'text/plain': new Blob([plainText], { type: 'text/plain' }),
                        'text/html': new Blob([htmlText], { type: 'text/html' })
                    })
                ];
                
                await navigator.clipboard.write(data);
                
                showResult('已复制多种格式到剪贴板!\n粘贴到富文本编辑器时会保留格式。');
            } catch (err) {
                showResult('复制失败: ' + err.message, true);
            }
        }
        
        // 仅复制HTML
        async function copyAsHTML() {
            const htmlText = document.getElementById('htmlText').value;
            
            try {
                const data = [
                    new ClipboardItem({
                        'text/html': new Blob([htmlText], { type: 'text/html' }),
                        'text/plain': new Blob([htmlText.replace(/<[^>]*>/g, '')], { type: 'text/plain' })
                    })
                ];
                
                await navigator.clipboard.write(data);
                
                showResult('已复制HTML格式!');
            } catch (err) {
                showResult('复制失败: ' + err.message, true);
            }
        }
        
        // 仅复制文本
        async function copyAsText() {
            const plainText = document.getElementById('plainText').value;
            
            try {
                await navigator.clipboard.writeText(plainText);
                showResult('已复制纯文本格式!');
            } catch (err) {
                showResult('复制失败: ' + err.message, true);
            }
        }
        
        // 显示结果
        function showResult(message, isError = false) {
            const result = document.getElementById('result');
            result.textContent = message;
            result.className = 'result' + (isError ? ' error' : '');
            result.style.display = 'block';
        }
    </script>
</body>
</html>

复制图片

复制图片到剪贴板

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>复制图片示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .image-container {
            background: #f9f9f9;
            padding: 30px;
            border-radius: 10px;
            margin: 20px 0;
        }
        
        .canvas-area {
            width: 100%;
            height: 300px;
            background: white;
            border: 2px dashed #ddd;
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-bottom: 20px;
            overflow: hidden;
        }
        
        #drawCanvas {
            border: 1px solid #ddd;
            border-radius: 5px;
            cursor: crosshair;
        }
        
        .controls {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        
        button {
            padding: 12px 24px;
            font-size: 16px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        
        button:hover {
            background: #0056b3;
        }
        
        .color-picker {
            display: flex;
            align-items: center;
            gap: 10px;
        }
        
        .color-picker input {
            width: 50px;
            height: 40px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        
        .notification {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 15px 25px;
            background: #28a745;
            color: white;
            border-radius: 5px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
            transform: translateX(200%);
            transition: transform 0.3s;
        }
        
        .notification.show {
            transform: translateX(0);
        }
    </style>
</head>
<body>
    <h1>复制图片示例</h1>
    
    <div class="image-container">
        <h2>绘制并复制图片</h2>
        
        <div class="canvas-area">
            <canvas id="drawCanvas" width="400" height="250"></canvas>
        </div>
        
        <div class="controls">
            <div class="color-picker">
                <label>画笔颜色:</label>
                <input type="color" id="colorPicker" value="#007bff">
            </div>
            <button onclick="clearCanvas()">清空画布</button>
            <button onclick="copyCanvas()">复制图片</button>
            <button onclick="downloadCanvas()">下载图片</button>
        </div>
    </div>
    
    <div class="notification" id="notification"></div>
    
    <script>
        const canvas = document.getElementById('drawCanvas');
        const ctx = canvas.getContext('2d');
        const colorPicker = document.getElementById('colorPicker');
        
        let isDrawing = false;
        let lastX = 0;
        let lastY = 0;
        
        // 初始化画布
        function initCanvas() {
            ctx.fillStyle = 'white';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            ctx.strokeStyle = colorPicker.value;
            ctx.lineWidth = 3;
            ctx.lineCap = 'round';
            ctx.lineJoin = 'round';
        }
        
        // 开始绘制
        canvas.addEventListener('mousedown', function(e) {
            isDrawing = true;
            [lastX, lastY] = [e.offsetX, e.offsetY];
        });
        
        // 绘制
        canvas.addEventListener('mousemove', function(e) {
            if (!isDrawing) return;
            
            ctx.strokeStyle = colorPicker.value;
            ctx.beginPath();
            ctx.moveTo(lastX, lastY);
            ctx.lineTo(e.offsetX, e.offsetY);
            ctx.stroke();
            
            [lastX, lastY] = [e.offsetX, e.offsetY];
        });
        
        // 停止绘制
        canvas.addEventListener('mouseup', () => isDrawing = false);
        canvas.addEventListener('mouseout', () => isDrawing = false);
        
        // 清空画布
        function clearCanvas() {
            ctx.fillStyle = 'white';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
        }
        
        // 复制画布
        async function copyCanvas() {
            try {
                // 转换为Blob
                const blob = await new Promise(resolve => {
                    canvas.toBlob(resolve, 'image/png');
                });
                
                // 复制到剪贴板
                await navigator.clipboard.write([
                    new ClipboardItem({
                        'image/png': blob
                    })
                ]);
                
                showNotification('图片已复制到剪贴板!');
            } catch (err) {
                showNotification('复制失败: ' + err.message);
                console.error(err);
            }
        }
        
        // 下载画布
        function downloadCanvas() {
            const link = document.createElement('a');
            link.download = 'drawing.png';
            link.href = canvas.toDataURL();
            link.click();
        }
        
        // 显示通知
        function showNotification(message) {
            const notification = document.getElementById('notification');
            notification.textContent = message;
            notification.classList.add('show');
            
            setTimeout(() => {
                notification.classList.remove('show');
            }, 3000);
        }
        
        // 初始化
        initCanvas();
    </script>
</body>
</html>

ClipboardEvent事件

剪贴板事件

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>剪贴板事件示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .event-container {
            background: #f9f9f9;
            padding: 30px;
            border-radius: 10px;
            margin: 20px 0;
        }
        
        .input-area {
            width: 100%;
            min-height: 100px;
            padding: 15px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 16px;
            resize: vertical;
            margin-bottom: 15px;
        }
        
        .event-log {
            max-height: 300px;
            overflow-y: auto;
            background: white;
            border-radius: 5px;
            padding: 15px;
            margin-top: 20px;
        }
        
        .log-entry {
            padding: 10px;
            margin: 5px 0;
            background: #f5f5f5;
            border-radius: 3px;
            border-left: 4px solid #007bff;
            font-size: 14px;
        }
        
        .log-entry.copy {
            border-left-color: #28a745;
        }
        
        .log-entry.cut {
            border-left-color: #ffc107;
        }
        
        .log-entry.paste {
            border-left-color: #17a2b8;
        }
        
        .log-entry .time {
            color: #666;
            font-size: 12px;
        }
        
        .log-entry .type {
            font-weight: bold;
            color: #333;
        }
        
        .log-entry .data {
            color: #666;
            margin-top: 5px;
            word-break: break-all;
        }
    </style>
</head>
<body>
    <h1>剪贴板事件示例</h1>
    
    <div class="event-container">
        <h2>剪贴板事件监听</h2>
        <p>在下方文本框中尝试复制、剪切、粘贴操作:</p>
        
        <textarea class="input-area" id="inputArea" placeholder="在此输入文本,然后尝试复制、剪切、粘贴..."></textarea>
        
        <div class="event-log" id="eventLog">
            <p style="color: #999; text-align: center;">事件日志将显示在这里...</p>
        </div>
    </div>
    
    <script>
        const inputArea = document.getElementById('inputArea');
        const eventLog = document.getElementById('eventLog');
        
        // 添加日志
        function addLog(type, data) {
            // 移除初始提示
            const placeholder = eventLog.querySelector('p');
            if (placeholder) {
                placeholder.remove();
            }
            
            const entry = document.createElement('div');
            entry.className = 'log-entry ' + type.toLowerCase();
            entry.innerHTML = `
                <div>
                    <span class="time">[${new Date().toLocaleTimeString()}]</span>
                    <span class="type">${type} 事件</span>
                </div>
                <div class="data">${data}</div>
            `;
            
            eventLog.insertBefore(entry, eventLog.firstChild);
        }
        
        // 监听复制事件
        inputArea.addEventListener('copy', function(e) {
            const selection = window.getSelection().toString();
            addLog('Copy', `复制内容: "${selection}"`);
            
            // 可以修改复制的内容
            // e.preventDefault();
            // e.clipboardData.setData('text/plain', '修改后的内容');
        });
        
        // 监听剪切事件
        inputArea.addEventListener('cut', function(e) {
            const selection = window.getSelection().toString();
            addLog('Cut', `剪切内容: "${selection}"`);
        });
        
        // 监听粘贴事件
        inputArea.addEventListener('paste', function(e) {
            // 获取剪贴板数据
            const clipboardData = e.clipboardData || window.clipboardData;
            const text = clipboardData.getData('text/plain');
            const html = clipboardData.getData('text/html');
            
            let logData = `粘贴文本: "${text}"`;
            if (html) {
                logData += `\nHTML内容: ${html.substring(0, 100)}...`;
            }
            
            addLog('Paste', logData);
            
            // 可以阻止默认粘贴行为
            // e.preventDefault();
            // 自定义粘贴逻辑
        });
        
        // 监听beforecopy事件
        inputArea.addEventListener('beforecopy', function(e) {
            console.log('beforecopy事件触发');
        });
        
        // 监听beforecut事件
        inputArea.addEventListener('beforecut', function(e) {
            console.log('beforecut事件触发');
        });
        
        // 监听beforepaste事件
        inputArea.addEventListener('beforepaste', function(e) {
            console.log('beforepaste事件触发');
        });
    </script>
</body>
</html>

综合示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>剪贴板工具 - 东巴文</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1000px;
            margin: 0 auto;
        }
        
        .header {
            text-align: center;
            color: white;
            padding: 30px 0;
        }
        
        .header h1 {
            font-size: 36px;
            margin-bottom: 10px;
        }
        
        .card {
            background: white;
            border-radius: 15px;
            padding: 30px;
            margin: 20px 0;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
        }
        
        .card h2 {
            margin-bottom: 20px;
            color: #333;
        }
        
        .clipboard-history {
            max-height: 400px;
            overflow-y: auto;
        }
        
        .history-item {
            display: flex;
            align-items: center;
            padding: 15px;
            border-bottom: 1px solid #eee;
            transition: background 0.3s;
        }
        
        .history-item:hover {
            background: #f9f9f9;
        }
        
        .history-content {
            flex: 1;
            overflow: hidden;
        }
        
        .history-text {
            font-size: 14px;
            color: #333;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        
        .history-time {
            font-size: 12px;
            color: #999;
            margin-top: 5px;
        }
        
        .history-actions {
            display: flex;
            gap: 5px;
        }
        
        .action-btn {
            padding: 8px 15px;
            font-size: 12px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        
        .action-btn:hover {
            background: #0056b3;
        }
        
        .action-btn.delete {
            background: #dc3545;
        }
        
        .action-btn.delete:hover {
            background: #c82333;
        }
        
        .quick-actions {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 15px;
            margin: 20px 0;
        }
        
        .quick-btn {
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            transition: transform 0.3s;
            font-size: 16px;
        }
        
        .quick-btn:hover {
            transform: translateY(-5px);
        }
        
        .quick-btn .icon {
            font-size: 24px;
            display: block;
            margin-bottom: 10px;
        }
        
        .text-input {
            width: 100%;
            min-height: 150px;
            padding: 15px;
            border: 1px solid #ddd;
            border-radius: 10px;
            font-size: 16px;
            resize: vertical;
            margin-bottom: 15px;
        }
        
        .button-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        
        .btn {
            padding: 12px 24px;
            font-size: 16px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        
        .btn:hover {
            background: #0056b3;
        }
        
        .btn-success {
            background: #28a745;
        }
        
        .btn-success:hover {
            background: #218838;
        }
        
        .empty-state {
            text-align: center;
            padding: 40px;
            color: #999;
        }
        
        .notification {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 15px 25px;
            background: #28a745;
            color: white;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
            transform: translateX(200%);
            transition: transform 0.3s;
            z-index: 1000;
        }
        
        .notification.show {
            transform: translateX(0);
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📋 剪贴板工具</h1>
            <p>管理和操作剪贴板内容</p>
        </div>
        
        <div class="card">
            <h2>快速操作</h2>
            <div class="quick-actions">
                <button class="quick-btn" onclick="copyTimestamp()">
                    <span class="icon">🕐</span>
                    复制时间戳
                </button>
                <button class="quick-btn" onclick="copyDate()">
                    <span class="icon">📅</span>
                    复制日期
                </button>
                <button class="quick-btn" onclick="copyUUID()">
                    <span class="icon">🔑</span>
                    复制UUID
                </button>
                <button class="quick-btn" onclick="copyRandomString()">
                    <span class="icon">🎲</span>
                    复制随机字符串
                </button>
            </div>
        </div>
        
        <div class="card">
            <h2>自定义内容</h2>
            <textarea class="text-input" id="customText" placeholder="输入要复制的内容..."></textarea>
            <div class="button-group">
                <button class="btn btn-success" onclick="copyCustomText()">复制</button>
                <button class="btn" onclick="pasteCustomText()">粘贴</button>
                <button class="btn" onclick="clearCustomText()">清空</button>
            </div>
        </div>
        
        <div class="card">
            <h2>剪贴板历史</h2>
            <div class="clipboard-history" id="history">
                <div class="empty-state">暂无历史记录</div>
            </div>
        </div>
    </div>
    
    <div class="notification" id="notification"></div>
    
    <script>
        // 剪贴板管理器
        class ClipboardManager {
            constructor() {
                this.history = [];
                this.maxHistory = 20;
            }
            
            async copy(text) {
                try {
                    await navigator.clipboard.writeText(text);
                    this.addToHistory(text);
                    return true;
                } catch (err) {
                    console.error('复制失败:', err);
                    return false;
                }
            }
            
            async paste() {
                try {
                    const text = await navigator.clipboard.readText();
                    return text;
                } catch (err) {
                    console.error('粘贴失败:', err);
                    return null;
                }
            }
            
            addToHistory(text) {
                // 避免重复
                const exists = this.history.find(item => item.text === text);
                if (exists) {
                    exists.time = new Date();
                    this.renderHistory();
                    return;
                }
                
                this.history.unshift({
                    text: text,
                    time: new Date()
                });
                
                // 限制历史数量
                if (this.history.length > this.maxHistory) {
                    this.history.pop();
                }
                
                this.renderHistory();
            }
            
            deleteFromHistory(index) {
                this.history.splice(index, 1);
                this.renderHistory();
            }
            
            renderHistory() {
                const container = document.getElementById('history');
                
                if (this.history.length === 0) {
                    container.innerHTML = '<div class="empty-state">暂无历史记录</div>';
                    return;
                }
                
                container.innerHTML = this.history.map((item, index) => `
                    <div class="history-item">
                        <div class="history-content">
                            <div class="history-text">${this.escapeHtml(item.text)}</div>
                            <div class="history-time">${item.time.toLocaleString()}</div>
                        </div>
                        <div class="history-actions">
                            <button class="action-btn" onclick="copyFromHistory(${index})">复制</button>
                            <button class="action-btn delete" onclick="deleteFromHistory(${index})">删除</button>
                        </div>
                    </div>
                `).join('');
            }
            
            escapeHtml(text) {
                const div = document.createElement('div');
                div.textContent = text;
                return div.innerHTML;
            }
        }
        
        // 初始化管理器
        const clipboardManager = new ClipboardManager();
        
        // 复制时间戳
        async function copyTimestamp() {
            const timestamp = Date.now().toString();
            if (await clipboardManager.copy(timestamp)) {
                showNotification('时间戳已复制: ' + timestamp);
            }
        }
        
        // 复制日期
        async function copyDate() {
            const date = new Date().toLocaleString('zh-CN');
            if (await clipboardManager.copy(date)) {
                showNotification('日期已复制: ' + date);
            }
        }
        
        // 复制UUID
        async function copyUUID() {
            const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                const r = Math.random() * 16 | 0;
                const v = c === 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
            
            if (await clipboardManager.copy(uuid)) {
                showNotification('UUID已复制: ' + uuid);
            }
        }
        
        // 复制随机字符串
        async function copyRandomString() {
            const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
            const length = 16;
            let result = '';
            
            for (let i = 0; i < length; i++) {
                result += chars.charAt(Math.floor(Math.random() * chars.length));
            }
            
            if (await clipboardManager.copy(result)) {
                showNotification('随机字符串已复制: ' + result);
            }
        }
        
        // 复制自定义文本
        async function copyCustomText() {
            const text = document.getElementById('customText').value;
            
            if (!text) {
                showNotification('请输入要复制的内容');
                return;
            }
            
            if (await clipboardManager.copy(text)) {
                showNotification('内容已复制');
            }
        }
        
        // 粘贴自定义文本
        async function pasteCustomText() {
            const text = await clipboardManager.paste();
            
            if (text) {
                document.getElementById('customText').value = text;
                showNotification('内容已粘贴');
            } else {
                showNotification('粘贴失败,请检查权限');
            }
        }
        
        // 清空自定义文本
        function clearCustomText() {
            document.getElementById('customText').value = '';
        }
        
        // 从历史记录复制
        async function copyFromHistory(index) {
            const item = clipboardManager.history[index];
            if (item && await clipboardManager.copy(item.text)) {
                showNotification('内容已复制');
            }
        }
        
        // 从历史记录删除
        function deleteFromHistory(index) {
            clipboardManager.deleteFromHistory(index);
            showNotification('已删除');
        }
        
        // 显示通知
        function showNotification(message) {
            const notification = document.getElementById('notification');
            notification.textContent = message;
            notification.classList.add('show');
            
            setTimeout(() => {
                notification.classList.remove('show');
            }, 3000);
        }
    </script>
</body>
</html>

最佳实践

1. 权限处理

// 推荐:检查权限并处理
async function copyWithPermission(text) {
    try {
        // 检查权限
        const permission = await navigator.permissions.query({ name: 'clipboard-write' });
        
        if (permission.state === 'granted' || permission.state === 'prompt') {
            await navigator.clipboard.writeText(text);
            return true;
        }
        
        return false;
    } catch (err) {
        // 降级处理
        return fallbackCopy(text);
    }
}

// 降级方案
function fallbackCopy(text) {
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.style.position = 'fixed';
    textarea.style.opacity = '0';
    document.body.appendChild(textarea);
    textarea.select();
    
    try {
        document.execCommand('copy');
        return true;
    } catch (err) {
        return false;
    } finally {
        document.body.removeChild(textarea);
    }
}

2. 错误处理

// 推荐:完善的错误处理
async function safeCopy(text) {
    // 检查API支持
    if (!navigator.clipboard) {
        throw new Error('浏览器不支持剪贴板API');
    }
    
    // 检查参数
    if (typeof text !== 'string') {
        throw new Error('参数必须是字符串');
    }
    
    try {
        await navigator.clipboard.writeText(text);
        console.log('复制成功');
        return true;
    } catch (err) {
        console.error('复制失败:', err);
        
        // 根据错误类型处理
        if (err.name === 'NotAllowedError') {
            alert('请授权剪贴板访问权限');
        } else if (err.name === 'NotFoundError') {
            alert('剪贴板不可用');
        } else {
            alert('复制失败,请重试');
        }
        
        return false;
    }
}

3. 用户体验

// 推荐:提供反馈
async function copyWithFeedback(text) {
    const btn = event.target;
    const originalText = btn.textContent;
    
    // 显示加载状态
    btn.disabled = true;
    btn.textContent = '复制中...';
    
    try {
        await navigator.clipboard.writeText(text);
        
        // 显示成功状态
        btn.textContent = '已复制 ✓';
        btn.classList.add('success');
        
        setTimeout(() => {
            btn.textContent = originalText;
            btn.classList.remove('success');
        }, 2000);
        
    } catch (err) {
        btn.textContent = '复制失败';
        btn.classList.add('error');
        
        setTimeout(() => {
            btn.textContent = originalText;
            btn.classList.remove('error');
        }, 2000);
        
    } finally {
        btn.disabled = false;
    }
}

东巴文点评:剪贴板API使用要注重权限处理和用户反馈,提供良好的用户体验。

学习检验

知识点测试

问题1:以下哪个方法用于将文本写入剪贴板?

A. navigator.clipboard.write() B. navigator.clipboard.writeText() C. document.execCommand('copy') D. 以上都可以

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释navigator.clipboard.writeText()用于将纯文本写入剪贴板。navigator.clipboard.write()用于写入多种格式(如文本、图片)。document.execCommand('copy')是旧方法,已不推荐使用。

</details>

问题2:读取剪贴板内容需要什么权限?

A. clipboard-write B. clipboard-read C. clipboard D. 不需要权限

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释:读取剪贴板需要clipboard-read权限,写入剪贴板需要clipboard-write权限。可以通过navigator.permissions.query()检查权限状态。

</details>

实践任务

任务:创建一个代码片段管理器,支持保存、复制、删除代码片段。

<details> <summary>点击查看参考答案</summary>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>代码片段管理器 - 东巴文</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #1e1e1e;
            color: #d4d4d4;
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }
        
        .header {
            text-align: center;
            padding: 30px 0;
        }
        
        .header h1 {
            font-size: 36px;
            color: #4ec9b0;
            margin-bottom: 10px;
        }
        
        .header p {
            color: #808080;
        }
        
        .panel {
            background: #252526;
            border-radius: 10px;
            padding: 20px;
            margin: 20px 0;
            border: 1px solid #3c3c3c;
        }
        
        .panel h2 {
            color: #4ec9b0;
            margin-bottom: 15px;
            font-size: 20px;
        }
        
        .form-group {
            margin-bottom: 15px;
        }
        
        .form-group label {
            display: block;
            margin-bottom: 5px;
            color: #9cdcfe;
        }
        
        input[type="text"],
        select {
            width: 100%;
            padding: 10px;
            background: #1e1e1e;
            border: 1px solid #3c3c3c;
            border-radius: 5px;
            color: #d4d4d4;
            font-size: 14px;
        }
        
        textarea {
            width: 100%;
            min-height: 150px;
            padding: 15px;
            background: #1e1e1e;
            border: 1px solid #3c3c3c;
            border-radius: 5px;
            color: #d4d4d4;
            font-family: 'Courier New', monospace;
            font-size: 14px;
            resize: vertical;
        }
        
        textarea:focus {
            outline: none;
            border-color: #007acc;
        }
        
        .button-group {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
        }
        
        button {
            padding: 10px 20px;
            background: #0e639c;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.3s;
        }
        
        button:hover {
            background: #1177bb;
        }
        
        button.success {
            background: #4ec9b0;
        }
        
        button.success:hover {
            background: #6fd9c3;
        }
        
        button.danger {
            background: #f14c4c;
        }
        
        button.danger:hover {
            background: #f36c6c;
        }
        
        .snippets-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 15px;
            margin-top: 20px;
        }
        
        .snippet-card {
            background: #1e1e1e;
            border: 1px solid #3c3c3c;
            border-radius: 5px;
            padding: 15px;
            transition: border-color 0.3s;
        }
        
        .snippet-card:hover {
            border-color: #007acc;
        }
        
        .snippet-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 10px;
        }
        
        .snippet-title {
            font-weight: bold;
            color: #4ec9b0;
        }
        
        .snippet-language {
            padding: 3px 8px;
            background: #3c3c3c;
            border-radius: 3px;
            font-size: 12px;
            color: #ce9178;
        }
        
        .snippet-code {
            background: #252526;
            padding: 10px;
            border-radius: 3px;
            font-family: 'Courier New', monospace;
            font-size: 13px;
            max-height: 150px;
            overflow-y: auto;
            white-space: pre-wrap;
            word-break: break-all;
        }
        
        .snippet-footer {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid #3c3c3c;
        }
        
        .snippet-time {
            font-size: 12px;
            color: #808080;
        }
        
        .snippet-actions {
            display: flex;
            gap: 5px;
        }
        
        .snippet-actions button {
            padding: 5px 10px;
            font-size: 12px;
        }
        
        .empty-state {
            text-align: center;
            padding: 40px;
            color: #808080;
        }
        
        .notification {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 15px 25px;
            background: #4ec9b0;
            color: #1e1e1e;
            border-radius: 5px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
            transform: translateX(200%);
            transition: transform 0.3s;
            z-index: 1000;
        }
        
        .notification.show {
            transform: translateX(0);
        }
        
        .search-box {
            margin-bottom: 20px;
        }
        
        .search-box input {
            width: 100%;
            padding: 12px;
            background: #1e1e1e;
            border: 1px solid #3c3c3c;
            border-radius: 5px;
            color: #d4d4d4;
            font-size: 14px;
        }
        
        .search-box input:focus {
            outline: none;
            border-color: #007acc;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📝 代码片段管理器</h1>
            <p>保存、管理和快速复制代码片段</p>
        </div>
        
        <div class="panel">
            <h2>添加新片段</h2>
            
            <div class="form-group">
                <label>片段名称</label>
                <input type="text" id="snippetName" placeholder="例如:React组件模板">
            </div>
            
            <div class="form-group">
                <label>编程语言</label>
                <select id="snippetLanguage">
                    <option value="javascript">JavaScript</option>
                    <option value="html">HTML</option>
                    <option value="css">CSS</option>
                    <option value="python">Python</option>
                    <option value="java">Java</option>
                    <option value="other">其他</option>
                </select>
            </div>
            
            <div class="form-group">
                <label>代码内容</label>
                <textarea id="snippetCode" placeholder="粘贴代码内容..."></textarea>
            </div>
            
            <div class="button-group">
                <button class="success" onclick="saveSnippet()">保存片段</button>
                <button onclick="clearForm()">清空表单</button>
            </div>
        </div>
        
        <div class="panel">
            <h2>我的片段</h2>
            
            <div class="search-box">
                <input type="text" id="searchInput" placeholder="搜索片段..." oninput="filterSnippets()">
            </div>
            
            <div class="snippets-grid" id="snippetsGrid">
                <div class="empty-state">暂无保存的片段</div>
            </div>
        </div>
    </div>
    
    <div class="notification" id="notification"></div>
    
    <script>
        // 代码片段管理器
        class SnippetManager {
            constructor() {
                this.snippets = this.loadSnippets();
                this.renderSnippets();
            }
            
            loadSnippets() {
                const saved = localStorage.getItem('codeSnippets');
                return saved ? JSON.parse(saved) : [];
            }
            
            saveSnippets() {
                localStorage.setItem('codeSnippets', JSON.stringify(this.snippets));
            }
            
            addSnippet(name, language, code) {
                const snippet = {
                    id: Date.now(),
                    name: name,
                    language: language,
                    code: code,
                    time: new Date().toISOString()
                };
                
                this.snippets.unshift(snippet);
                this.saveSnippets();
                this.renderSnippets();
                
                return snippet;
            }
            
            deleteSnippet(id) {
                this.snippets = this.snippets.filter(s => s.id !== id);
                this.saveSnippets();
                this.renderSnippets();
            }
            
            async copySnippet(id) {
                const snippet = this.snippets.find(s => s.id === id);
                
                if (snippet) {
                    try {
                        await navigator.clipboard.writeText(snippet.code);
                        return true;
                    } catch (err) {
                        console.error('复制失败:', err);
                        return false;
                    }
                }
                
                return false;
            }
            
            filterSnippets(keyword) {
                if (!keyword) {
                    return this.snippets;
                }
                
                const lowerKeyword = keyword.toLowerCase();
                return this.snippets.filter(s => 
                    s.name.toLowerCase().includes(lowerKeyword) ||
                    s.code.toLowerCase().includes(lowerKeyword) ||
                    s.language.toLowerCase().includes(lowerKeyword)
                );
            }
            
            renderSnippets(filteredList = null) {
                const grid = document.getElementById('snippetsGrid');
                const snippets = filteredList || this.snippets;
                
                if (snippets.length === 0) {
                    grid.innerHTML = '<div class="empty-state">暂无保存的片段</div>';
                    return;
                }
                
                grid.innerHTML = snippets.map(snippet => `
                    <div class="snippet-card">
                        <div class="snippet-header">
                            <div class="snippet-title">${this.escapeHtml(snippet.name)}</div>
                            <div class="snippet-language">${snippet.language}</div>
                        </div>
                        <div class="snippet-code">${this.escapeHtml(snippet.code)}</div>
                        <div class="snippet-footer">
                            <div class="snippet-time">${new Date(snippet.time).toLocaleString()}</div>
                            <div class="snippet-actions">
                                <button onclick="copySnippet(${snippet.id})">复制</button>
                                <button class="danger" onclick="deleteSnippet(${snippet.id})">删除</button>
                            </div>
                        </div>
                    </div>
                `).join('');
            }
            
            escapeHtml(text) {
                const div = document.createElement('div');
                div.textContent = text;
                return div.innerHTML;
            }
        }
        
        // 初始化管理器
        const snippetManager = new SnippetManager();
        
        // 保存片段
        function saveSnippet() {
            const name = document.getElementById('snippetName').value.trim();
            const language = document.getElementById('snippetLanguage').value;
            const code = document.getElementById('snippetCode').value.trim();
            
            if (!name || !code) {
                showNotification('请填写片段名称和代码内容');
                return;
            }
            
            snippetManager.addSnippet(name, language, code);
            clearForm();
            showNotification('片段已保存');
        }
        
        // 清空表单
        function clearForm() {
            document.getElementById('snippetName').value = '';
            document.getElementById('snippetLanguage').value = 'javascript';
            document.getElementById('snippetCode').value = '';
        }
        
        // 复制片段
        async function copySnippet(id) {
            if (await snippetManager.copySnippet(id)) {
                showNotification('代码已复制到剪贴板');
            } else {
                showNotification('复制失败,请检查权限');
            }
        }
        
        // 删除片段
        function deleteSnippet(id) {
            if (confirm('确定要删除这个片段吗?')) {
                snippetManager.deleteSnippet(id);
                showNotification('片段已删除');
            }
        }
        
        // 搜索片段
        function filterSnippets() {
            const keyword = document.getElementById('searchInput').value;
            const filtered = snippetManager.filterSnippets(keyword);
            snippetManager.renderSnippets(filtered);
        }
        
        // 显示通知
        function showNotification(message) {
            const notification = document.getElementById('notification');
            notification.textContent = message;
            notification.classList.add('show');
            
            setTimeout(() => {
                notification.classList.remove('show');
            }, 3000);
        }
    </script>
</body>
</html>
</details>

东巴文(db-w.cn) - 让编程学习更有趣、更高效!