涨知识-AI分析结果
navigator.clipboard.writeText(text)
在服务器环境执行失败,主要与浏览器的安全机制和 API 运行条件有关,具体原因如下:
-
仅支持浏览器环境
navigator.clipboard
是浏览器提供的 API,依赖浏览器的剪贴板权限和安全上下文。服务器端环境(如 Node.js)没有navigator
对象,自然无法调用该 API,会直接报错(如navigator is not defined
)。 -
需要 HTTPS 安全上下文
即使在浏览器中,该 API 也要求页面运行在 HTTPS 协议 或 localhost 本地环境 中。如果服务器使用 HTTP 协议(非本地),浏览器会出于安全考虑禁用剪贴板 API,导致调用失败。
-
需要用户交互触发
部分浏览器要求剪贴板操作必须由 用户主动交互(如点击、按键等事件)触发,否则会被拦截。如果在服务器渲染页面时自动执行(无用户交互),也会失败。
-
权限问题
用户可能在浏览器中禁用了剪贴板权限,此时调用会抛出
NotAllowedError
异常。
推荐的 clipboardy 是nodejs环境,不符合项目要求;
解决方案
clipboard-utils.js
ini
/**
* 通用剪贴板工具库
* 解决不同环境下的剪贴板兼容性问题
*
* 支持的环境:
* - HTTPS环境(现代浏览器)
* - HTTP环境(降级方案)
* - 服务器环境(多种降级方案)
*
* @version 1.0.0
*/
class ClipboardUtils {
constructor() {
this.isSecureContext = window.isSecureContext;
this.hasClipboardAPI = !!(navigator.clipboard && navigator.clipboard.writeText);
this.hasExecCommand = document.queryCommandSupported && document.queryCommandSupported('copy');
console.log('📋 剪贴板工具初始化:', {
isSecureContext: this.isSecureContext,
hasClipboardAPI: this.hasClipboardAPI,
hasExecCommand: this.hasExecCommand
});
}
/**
* 复制文本到剪贴板(主要方法)
* @param {string} text 要复制的文本
* @param {string} successMessage 成功提示消息
* @param {Function} onSuccess 成功回调
* @param {Function} onError 失败回调
* @returns {Promise<boolean>} 是否成功
*/
async copyText(text, successMessage = '内容已复制到剪贴板', onSuccess = null, onError = null) {
if (!text || typeof text !== 'string') {
const error = '复制内容不能为空';
this._handleError(error, onError);
return false;
}
console.log('📋 开始复制文本,长度:', text.length);
// 方法1: 现代剪贴板API
if (this.hasClipboardAPI && this.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
console.log('✅ 现代剪贴板API复制成功');
this._handleSuccess(successMessage, onSuccess);
return true;
} catch (error) {
console.warn('⚠️ 现代剪贴板API失败,尝试降级方案:', error.message);
}
}
// 方法2: execCommand降级方案
if (this.hasExecCommand) {
try {
const success = await this._copyWithExecCommand(text);
if (success) {
console.log('✅ execCommand复制成功');
this._handleSuccess(successMessage, onSuccess);
return true;
} else {
console.warn('⚠️ execCommand复制失败');
}
} catch (error) {
console.warn('⚠️ execCommand方法异常:', error.message);
}
}
// 方法3: 文本选择降级方案
try {
this._selectText(text);
console.log('📝 已选择文本,请用户手动复制');
this._handleError('自动复制失败,已为您选中内容,请使用 Ctrl+C 复制', onError);
return false;
} catch (error) {
console.error('❌ 所有复制方案都失败了:', error.message);
this._handleError('复制失败,请手动选择并复制内容', onError);
return false;
}
}
/**
* 使用execCommand复制文本
* @param {string} text 要复制的文本
* @returns {Promise<boolean>} 是否成功
*/
async _copyWithExecCommand(text) {
return new Promise((resolve) => {
const textArea = document.createElement('textarea');
textArea.value = text;
// 设置样式使其不可见但可操作
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
textArea.style.opacity = '0';
textArea.style.pointerEvents = 'none';
textArea.setAttribute('readonly', '');
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
textArea.setSelectionRange(0, text.length);
const successful = document.execCommand('copy');
resolve(successful);
} catch (error) {
console.error('execCommand执行失败:', error);
resolve(false);
} finally {
if (textArea.parentNode) {
document.body.removeChild(textArea);
}
}
});
}
/**
* 选择文本(最后的降级方案)
* @param {string} text 要选择的文本
*/
_selectText(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '50%';
textArea.style.left = '50%';
textArea.style.transform = 'translate(-50%, -50%)';
textArea.style.width = '300px';
textArea.style.height = '100px';
textArea.style.padding = '10px';
textArea.style.border = '2px solid #007bff';
textArea.style.borderRadius = '4px';
textArea.style.backgroundColor = '#f8f9fa';
textArea.style.zIndex = '9999';
textArea.setAttribute('readonly', '');
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
// 5秒后自动移除
setTimeout(() => {
if (textArea.parentNode) {
document.body.removeChild(textArea);
}
}, 5000);
}
/**
* 处理成功情况
* @param {string} message 成功消息
* @param {Function} callback 成功回调
*/
_handleSuccess(message, callback) {
if (typeof showToast === 'function') {
showToast(message, 'success');
} else {
console.log('✅', message);
}
if (typeof callback === 'function') {
callback(true, message);
}
}
/**
* 处理错误情况
* @param {string} message 错误消息
* @param {Function} callback 错误回调
*/
_handleError(message, callback) {
if (typeof showToast === 'function') {
showToast(message, 'warning');
} else {
console.warn('⚠️', message);
}
if (typeof callback === 'function') {
callback(false, message);
}
}
/**
* 复制JSON对象(格式化后复制)
* @param {Object} obj 要复制的对象
* @param {number} indent 缩进空格数
* @param {string} successMessage 成功消息
* @returns {Promise<boolean>} 是否成功
*/
async copyJSON(obj, indent = 2, successMessage = 'JSON已复制到剪贴板') {
try {
const jsonString = JSON.stringify(obj, null, indent);
return await this.copyText(jsonString, successMessage);
} catch (error) {
console.error('JSON序列化失败:', error);
this._handleError('JSON格式化失败', null);
return false;
}
}
/**
* 复制HTML元素的文本内容
* @param {string|HTMLElement} elementOrId 元素ID或元素对象
* @param {string} successMessage 成功消息
* @returns {Promise<boolean>} 是否成功
*/
async copyElementText(elementOrId, successMessage = '内容已复制到剪贴板') {
let element;
if (typeof elementOrId === 'string') {
element = document.getElementById(elementOrId);
} else if (elementOrId instanceof HTMLElement) {
element = elementOrId;
}
if (!element) {
this._handleError('没有找到要复制的元素', null);
return false;
}
const text = element.textContent || element.innerText || '';
if (!text.trim()) {
this._handleError('元素内容为空', null);
return false;
}
return await this.copyText(text, successMessage);
}
/**
* 检查剪贴板功能可用性
* @returns {Object} 功能可用性信息
*/
checkAvailability() {
return {
isSecureContext: this.isSecureContext,
hasClipboardAPI: this.hasClipboardAPI,
hasExecCommand: this.hasExecCommand,
recommended: this.hasClipboardAPI && this.isSecureContext ? 'modern' :
this.hasExecCommand ? 'legacy' : 'manual'
};
}
}
// 创建全局实例
const clipboardUtils = new ClipboardUtils();
// 兼容性函数(保持向后兼容)
function copyTextToClipboard(text, successMessage = '内容已复制到剪贴板') {
return clipboardUtils.copyText(text, successMessage);
}
function copyJsonResult() {
return clipboardUtils.copyElementText('jsonOutput', '结果已复制到剪贴板');
}
function copyFinalPrompt() {
return clipboardUtils.copyElementText('finalPromptPreview', '提示词已复制到剪贴板');
}
// 导出(如果支持模块化)
if (typeof module !== 'undefined' && module.exports) {
module.exports = ClipboardUtils;
}
if (typeof window !== 'undefined') {
window.ClipboardUtils = ClipboardUtils;
window.clipboardUtils = clipboardUtils;
}
test-clipboard.html
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>剪贴板工具测试页面</title>
<link href="assets/css/bootstrap.min.css" rel="stylesheet">
<link href="assets/css/bootstrap-icons.css" rel="stylesheet">
<style>
.test-section {
margin-bottom: 2rem;
padding: 1.5rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
background: #f9fafb;
}
.test-content {
background: white;
padding: 1rem;
border-radius: 0.375rem;
border: 1px solid #d1d5db;
margin: 1rem 0;
}
.status-info {
font-size: 0.875rem;
color: #6b7280;
}
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
}
</style>
</head>
<body>
<div class="container mt-4">
<div class="row">
<div class="col-12">
<h1 class="mb-4">
<i class="bi bi-clipboard-check me-2"></i>
剪贴板工具测试页面
</h1>
<!-- 环境信息 -->
<div class="test-section">
<h3>环境信息</h3>
<div class="status-info">
<p><strong>安全上下文:</strong> <span id="secureContext">检测中...</span></p>
<p><strong>剪贴板API:</strong> <span id="clipboardAPI">检测中...</span></p>
<p><strong>execCommand:</strong> <span id="execCommand">检测中...</span></p>
<p><strong>推荐方案:</strong> <span id="recommended">检测中...</span></p>
</div>
</div>
<!-- 文本复制测试 -->
<div class="test-section">
<h3>文本复制测试</h3>
<div class="test-content">
<textarea class="form-control mb-3" id="testText" rows="3" placeholder="输入要复制的文本...">这是一段测试文本,用于验证剪贴板复制功能是否正常工作。</textarea>
<button class="btn btn-primary" onclick="testTextCopy()">
<i class="bi bi-clipboard me-2"></i>复制文本
</button>
</div>
</div>
<!-- JSON复制测试 -->
<div class="test-section">
<h3>JSON复制测试</h3>
<div class="test-content">
<pre id="jsonContent">{
"name": "projectName",
"version": "1.0.0",
"description": "我是测试",
"features": [
"识别",
"表单填写"
],
"timestamp": "2024-01-01T12:00:00Z"
}</pre>
<button class="btn btn-success" onclick="testJSONCopy()">
<i class="bi bi-file-code me-2"></i>复制JSON
</button>
</div>
</div>
<!-- 元素内容复制测试 -->
<div class="test-section">
<h3>元素内容复制测试</h3>
<div class="test-content">
<div id="elementContent" class="p-3 bg-light border rounded">
<h5>这是一个HTML元素</h5>
<p>包含多行文本内容,用于测试从DOM元素复制文本的功能。</p>
<ul>
<li>列表项1</li>
<li>列表项2</li>
<li>列表项3</li>
</ul>
</div>
<button class="btn btn-info mt-2" onclick="testElementCopy()">
<i class="bi bi-box-arrow-up me-2"></i>复制元素内容
</button>
</div>
</div>
<!-- 大文本复制测试 -->
<div class="test-section">
<h3>大文本复制测试</h3>
<div class="test-content">
<p class="text-muted">测试复制大量文本数据的性能和稳定性</p>
<button class="btn btn-warning" onclick="testLargeTextCopy()">
<i class="bi bi-file-text me-2"></i>复制大文本 (10KB)
</button>
</div>
</div>
<!-- 错误处理测试 -->
<div class="test-section">
<h3>错误处理测试</h3>
<div class="test-content">
<button class="btn btn-danger me-2" onclick="testEmptyContent()">
<i class="bi bi-x-circle me-2"></i>复制空内容
</button>
<button class="btn btn-danger" onclick="testNonExistentElement()">
<i class="bi bi-question-circle me-2"></i>复制不存在的元素
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Toast容器 -->
<div class="toast-container"></div>
<!-- 引入必要的脚本 -->
<script src="assets/js/jquery-3.7.1.min.js"></script>
<script src="assets/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/admin-common.js"></script>
<script src="assets/js/clipboard-utils.js"></script>
<script>
// 页面初始化
document.addEventListener('DOMContentLoaded', function() {
// 检测环境信息
if (typeof clipboardUtils !== 'undefined') {
const availability = clipboardUtils.checkAvailability();
document.getElementById('secureContext').textContent = availability.isSecureContext ? '✅ 是' : '❌ 否';
document.getElementById('clipboardAPI').textContent = availability.hasClipboardAPI ? '✅ 支持' : '❌ 不支持';
document.getElementById('execCommand').textContent = availability.hasExecCommand ? '✅ 支持' : '❌ 不支持';
const recommendedText = {
'modern': '现代剪贴板API',
'legacy': 'execCommand降级方案',
'manual': '手动复制'
};
document.getElementById('recommended').textContent = recommendedText[availability.recommended] || '未知';
} else {
document.getElementById('secureContext').textContent = '❌ 剪贴板工具库未加载';
document.getElementById('clipboardAPI').textContent = '❌ 剪贴板工具库未加载';
document.getElementById('execCommand').textContent = '❌ 剪贴板工具库未加载';
document.getElementById('recommended').textContent = '❌ 剪贴板工具库未加载';
}
});
// 测试文本复制
function testTextCopy() {
const text = document.getElementById('testText').value;
if (typeof clipboardUtils !== 'undefined') {
clipboardUtils.copyText(text, '文本复制成功!');
} else {
showToast('剪贴板工具库未加载', 'error');
}
}
// 测试JSON复制
function testJSONCopy() {
const jsonObj = {
name: "ProjectName",
version: "1.0.0",
description: "我是张三啊!",
features: ["识别", "表单填写"],
timestamp: new Date().toISOString()
};
if (typeof clipboardUtils !== 'undefined') {
clipboardUtils.copyJSON(jsonObj, 2, 'JSON复制成功!');
} else {
showToast('剪贴板工具库未加载', 'error');
}
}
// 测试元素内容复制
function testElementCopy() {
if (typeof clipboardUtils !== 'undefined') {
clipboardUtils.copyElementText('elementContent', '元素内容复制成功!');
} else {
showToast('剪贴板工具库未加载', 'error');
}
}
// 测试大文本复制
function testLargeTextCopy() {
// 生成约10KB的文本
const baseText = "这是一段用于测试大文本复制功能的内容。";
const largeText = baseText.repeat(200); // 约10KB
if (typeof clipboardUtils !== 'undefined') {
clipboardUtils.copyText(largeText, `大文本复制成功!(${(largeText.length / 1024).toFixed(1)}KB)`);
} else {
showToast('剪贴板工具库未加载', 'error');
}
}
// 测试空内容复制
function testEmptyContent() {
if (typeof clipboardUtils !== 'undefined') {
clipboardUtils.copyText('', '这应该显示错误信息');
} else {
showToast('剪贴板工具库未加载', 'error');
}
}
// 测试不存在的元素
function testNonExistentElement() {
if (typeof clipboardUtils !== 'undefined') {
clipboardUtils.copyElementText('nonExistentElement', '这应该显示错误信息');
} else {
showToast('剪贴板工具库未加载', 'error');
}
}
</script>
</body>
</html>