feat: 优化剪贴板读取

This commit is contained in:
chaos-zhu
2026-02-08 15:35:22 +08:00
parent d6fd7964b5
commit 8a1b678432
10 changed files with 147 additions and 48 deletions

View File

@@ -32,6 +32,7 @@ import { computed } from 'vue'
import { EventBus } from '@/utils'
import { ElMessage } from 'element-plus'
import hljs from 'highlight.js'
import clipboard from '@/utils/clipboard'
const props = defineProps({
node: {
@@ -106,13 +107,7 @@ const highlightedCode = computed(() => {
// 处理复制
const handleCopy = () => {
const code = props.node?.code || props.node?.value || ''
navigator.clipboard.writeText(code)
.then(() => {
ElMessage.success('复制成功')
})
.catch(() => {
ElMessage.error('复制失败')
})
clipboard.copy(code)
}
// 处理执行

View File

@@ -271,6 +271,7 @@ import { useAIChat } from '@/composables/useAIChat'
import AiApiConfig from './ai-api-config.vue'
import { EventBus } from '@/utils'
import CustomCodeBlock from './custom-code-block.vue'
import clipboard from '@/utils/clipboard'
const { proxy: { $message, $store } } = getCurrentInstance()
@@ -444,13 +445,8 @@ const submit = async function (questionStr) {
await sendMessage(questionStr, activeModel.value)
}
const copyContent = async (content) => {
try {
await navigator.clipboard.writeText(content)
$message.success('复制成功')
} catch (err) {
$message.error('复制失败')
}
const copyContent = (content) => {
clipboard.copy(content)
}
const handleSetting = () => {

View File

@@ -5,7 +5,6 @@ import { isValidDate } from '@/utils'
const useStore = defineStore('global', {
state: () => ({
serviceURI: null,
hostList: [],
groupList: [],
sshList: [],

108
web/src/utils/clipboard.js Normal file
View File

@@ -0,0 +1,108 @@
/**
* 通用剪贴板工具
* - HTTPS环境: 使用 navigator.clipboard API
* - HTTP环境: 降级到 document.execCommand (兼容内网部署)
*/
import { ElMessage, ElMessageBox } from 'element-plus'
/**
* 检测是否支持现代剪贴板API
*/
function isModernAPISupported() {
return !!(navigator.clipboard && window.isSecureContext)
}
/**
* 使用传统方法复制文本 (HTTP环境兼容)
*/
function copyWithLegacyAPI(text) {
const textarea = document.createElement('textarea')
textarea.value = text
textarea.style.position = 'fixed'
textarea.style.left = '-9999px'
textarea.style.opacity = '0'
textarea.setAttribute('readonly', '')
document.body.appendChild(textarea)
try {
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
const successful = document.execCommand('copy')
return successful
} catch (err) {
console.error('复制失败:', err)
return false
} finally {
document.body.removeChild(textarea)
}
}
/**
* 复制文本到剪贴板
* @param {string} text - 要复制的文本
* @param {boolean} showMessage - 是否显示提示消息,默认true
* @returns {Promise<boolean>} 是否成功
*/
export async function copyText(text, showMessage = true) {
if (!text) {
if (showMessage) ElMessage.warning('复制内容为空')
return false
}
let success = false
// 优先尝试现代API (HTTPS环境)
if (isModernAPISupported()) {
try {
await navigator.clipboard.writeText(text)
success = true
} catch (err) {
console.warn('现代API复制失败,降级到传统方法:', err)
success = copyWithLegacyAPI(text)
}
} else {
// HTTP环境使用传统方法
success = copyWithLegacyAPI(text)
}
if (showMessage) {
success ? ElMessage.success('复制成功') : ElMessage.error('复制失败')
}
return success
}
/**
* 从剪贴板读取文本
* 注意: HTTP环境下无法读取剪贴板
* @param {boolean} showMessage - 是否显示提示消息,默认false
* @returns {Promise<string>} 剪贴板文本
*/
export async function pasteText(showMessage = false) {
if (!isModernAPISupported()) {
ElMessageBox.confirm('浏览器安全限制,不支持剪贴板读取需配置https', '提示', {
confirmButtonText: '好的',
showCancelButton: false,
type: 'warning',
dangerouslyUseHTMLString: true
}).catch(() => {})
const errorMsg = 'HTTP环境不支持读取剪贴板,请使用 Ctrl+V 或右键粘贴'
throw new Error(errorMsg)
}
try {
const text = await navigator.clipboard.readText()
return text
} catch (err) {
if (showMessage) ElMessage.error('读取剪贴板失败')
throw err
}
}
// 默认导出
export default {
copy: copyText,
paste: pasteText
}

View File

@@ -196,7 +196,8 @@ export const isValidDate = (dateString) => {
return !isNaN(date.getTime()) && date instanceof Date
}
const serviceURI = import.meta.env.DEV ? process.env.serviceURI : location.origin
const { protocol, hostname } = location
const serviceURI = import.meta.env.DEV ? `${ protocol }//${ hostname }:8082` : location.origin
export const generateSocketInstance = (path, config = { forceNew: false, reconnection: true, reconnectionAttempts: 3 }, query = {}) => {
return socketIo(serviceURI, {
withCredentials: true,

View File

@@ -137,6 +137,7 @@
import { ref, computed, getCurrentInstance, nextTick } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue'
import useMobileWidth from '@/composables/useMobileWidth'
import clipboard from '@/utils/clipboard'
const { proxy: { $message, $messageBox, $api, $router, $store } } = getCurrentInstance()
@@ -246,9 +247,8 @@ const handleRemoveHost = async ({ id }) => {
})
}
const handleCopy = async (host) => {
await navigator.clipboard.writeText(host)
$message.success({ message: '复制成功', center: true })
const handleCopy = (host) => {
clipboard.copy(host)
}
const formatProxyType = ({ proxyType, jumpHosts, proxyServer }) => {

View File

@@ -217,6 +217,7 @@
import { ref, computed, getCurrentInstance, watch, onBeforeUnmount, nextTick, onMounted, toRaw, shallowRef } from 'vue'
import { Chart, registerables } from 'chart.js'
import { generateSocketInstance } from '@/utils'
import clipboard from '@/utils/clipboard'
// 注册Chart.js所有组件
Chart.register(...registerables)
@@ -604,9 +605,8 @@ watch(
)
// 工具函数
const handleCopy = async () => {
await navigator.clipboard.writeText(host.value)
$message.success({ message: '复制成功', center: true })
const handleCopy = () => {
clipboard.copy(host.value)
}
const handleUsedColor = (num) => {

View File

@@ -665,6 +665,7 @@ import unknowIcon from '@/assets/image/system/unknow.png'
import { useContextMenu } from '@/composables/useContextMenu'
import TextEditor from '@/components/text-editor/index.vue'
import ImagePreview from '@/components/image-preview/index.vue'
import clipboard from '@/utils/clipboard'
const emit = defineEmits(['exec-script', ])
@@ -1592,11 +1593,7 @@ const cancelEditPath = () => {
}
const copyCurrentPath = () => {
navigator.clipboard.writeText(currentPath.value).then(() => {
$message.success('路径已复制')
}).catch(() => {
$message.error('复制失败')
})
clipboard.copy(currentPath.value)
}
const handleUpload = (type) => {
@@ -1962,8 +1959,7 @@ const onRowContextMenu = (row, _column, event) => {
fullPath = `${ currentPathValue }/${ row.name }`
}
fullPath = fullPath.replace(/\/+/g, '/')
navigator.clipboard.writeText(fullPath)
$message.success('已复制路径')
clipboard.copy(fullPath)
}
})

View File

@@ -39,6 +39,7 @@ import { useContextMenu } from '@/composables/useContextMenu'
import { EventBus, isDockerId, isDockerComposeYml, generateSocketInstance } from '@/utils'
import useMobileWidth from '@/composables/useMobileWidth'
import { TerminalHighlighter } from '@/utils/highlighter'
import clipboard from '@/utils/clipboard'
const { CONNECTING, CONNECT_SUCCESS, CONNECT_FAIL, SUSPENDED, RESUMING } = terminalStatus
@@ -552,11 +553,8 @@ const onData = () => {
const handleCopySelection = async () => {
let str = term.value.getSelection().trim()
if (!str) return
const text = new Blob([str,], { type: 'text/plain' })
const item = new ClipboardItem({
'text/plain': text
})
await navigator.clipboard.write([item,])
await clipboard.copy(str)
term.value.clearSelection()
}
@@ -855,18 +853,24 @@ const handleClear = () => {
}
const handlePaste = async () => {
let key = await navigator.clipboard.readText()
// 规范换行符:无论来自 Windows (\r\n) 还是 Unix (\n),都统一替换成 \r
key = key.replace(/\r\n/g, '\r').replace(/\n/g, '\r')
// 如果粘贴的内容以换行符结尾则去掉换行符(防止自动执行)
while (key.endsWith('\n')) {
key = key.slice(0, -1)
}
try {
let key = await clipboard.paste()
// 规范换行符:无论来自 Windows (\r\n) 还是 Unix (\n),都统一替换成 \r
key = key.replace(/\r\n/g, '\r').replace(/\n/g, '\r')
// 如果粘贴的内容以换行符结尾则去掉换行符(防止自动执行)
while (key.endsWith('\n')) {
key = key.slice(0, -1)
}
emit('inputCommand', key, uid)
socket.value.emit('input', key)
term.value.focus()
term.value.clearSelection()
emit('inputCommand', key, uid)
socket.value.emit('input', key)
term.value.focus()
term.value.clearSelection()
} catch (err) {
// HTTP环境下无法读取剪贴板,提示用户使用浏览器原生粘贴
console.warn('剪贴板读取失败,请使用 Ctrl+V 或右键粘贴:', err.message)
// 不显示错误提示,因为 Ctrl+V 会触发浏览器原生粘贴事件
}
}
const focusTab = () => {

View File

@@ -101,6 +101,7 @@ import TerminalWrapper from './components/terminal-wrapper.vue'
import HostForm from '../server/components/host-form.vue'
import { randomStr } from '@utils/index.js'
import { terminalStatus } from '@/utils/enum'
import clipboard from '@/utils/clipboard'
const { CONNECTING, RESUMING } = terminalStatus
const { proxy: { $store, $message, $api } } = getCurrentInstance()
@@ -216,9 +217,8 @@ onActivated(async () => {
})
})
const handleCopy = async (host) => {
await navigator.clipboard.writeText(host)
$message.success({ message: '复制成功', center: true })
const handleCopy = (host) => {
clipboard.copy(host)
}
// 获取挂起的会话列表