mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-05-06 21:40:35 +08:00
feat: 优化剪贴板读取
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
// 处理执行
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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
108
web/src/utils/clipboard.js
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// 获取挂起的会话列表
|
||||
|
||||
Reference in New Issue
Block a user