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 { EventBus } from '@/utils'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
|
import clipboard from '@/utils/clipboard'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
node: {
|
node: {
|
||||||
@@ -106,13 +107,7 @@ const highlightedCode = computed(() => {
|
|||||||
// 处理复制
|
// 处理复制
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
const code = props.node?.code || props.node?.value || ''
|
const code = props.node?.code || props.node?.value || ''
|
||||||
navigator.clipboard.writeText(code)
|
clipboard.copy(code)
|
||||||
.then(() => {
|
|
||||||
ElMessage.success('复制成功')
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
ElMessage.error('复制失败')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理执行
|
// 处理执行
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ import { useAIChat } from '@/composables/useAIChat'
|
|||||||
import AiApiConfig from './ai-api-config.vue'
|
import AiApiConfig from './ai-api-config.vue'
|
||||||
import { EventBus } from '@/utils'
|
import { EventBus } from '@/utils'
|
||||||
import CustomCodeBlock from './custom-code-block.vue'
|
import CustomCodeBlock from './custom-code-block.vue'
|
||||||
|
import clipboard from '@/utils/clipboard'
|
||||||
|
|
||||||
const { proxy: { $message, $store } } = getCurrentInstance()
|
const { proxy: { $message, $store } } = getCurrentInstance()
|
||||||
|
|
||||||
@@ -444,13 +445,8 @@ const submit = async function (questionStr) {
|
|||||||
await sendMessage(questionStr, activeModel.value)
|
await sendMessage(questionStr, activeModel.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyContent = async (content) => {
|
const copyContent = (content) => {
|
||||||
try {
|
clipboard.copy(content)
|
||||||
await navigator.clipboard.writeText(content)
|
|
||||||
$message.success('复制成功')
|
|
||||||
} catch (err) {
|
|
||||||
$message.error('复制失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSetting = () => {
|
const handleSetting = () => {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { isValidDate } from '@/utils'
|
|||||||
|
|
||||||
const useStore = defineStore('global', {
|
const useStore = defineStore('global', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
serviceURI: null,
|
|
||||||
hostList: [],
|
hostList: [],
|
||||||
groupList: [],
|
groupList: [],
|
||||||
sshList: [],
|
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
|
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 = {}) => {
|
export const generateSocketInstance = (path, config = { forceNew: false, reconnection: true, reconnectionAttempts: 3 }, query = {}) => {
|
||||||
return socketIo(serviceURI, {
|
return socketIo(serviceURI, {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
|
|||||||
@@ -137,6 +137,7 @@
|
|||||||
import { ref, computed, getCurrentInstance, nextTick } from 'vue'
|
import { ref, computed, getCurrentInstance, nextTick } from 'vue'
|
||||||
import { ArrowDown } from '@element-plus/icons-vue'
|
import { ArrowDown } from '@element-plus/icons-vue'
|
||||||
import useMobileWidth from '@/composables/useMobileWidth'
|
import useMobileWidth from '@/composables/useMobileWidth'
|
||||||
|
import clipboard from '@/utils/clipboard'
|
||||||
|
|
||||||
const { proxy: { $message, $messageBox, $api, $router, $store } } = getCurrentInstance()
|
const { proxy: { $message, $messageBox, $api, $router, $store } } = getCurrentInstance()
|
||||||
|
|
||||||
@@ -246,9 +247,8 @@ const handleRemoveHost = async ({ id }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopy = async (host) => {
|
const handleCopy = (host) => {
|
||||||
await navigator.clipboard.writeText(host)
|
clipboard.copy(host)
|
||||||
$message.success({ message: '复制成功', center: true })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatProxyType = ({ proxyType, jumpHosts, proxyServer }) => {
|
const formatProxyType = ({ proxyType, jumpHosts, proxyServer }) => {
|
||||||
|
|||||||
@@ -217,6 +217,7 @@
|
|||||||
import { ref, computed, getCurrentInstance, watch, onBeforeUnmount, nextTick, onMounted, toRaw, shallowRef } from 'vue'
|
import { ref, computed, getCurrentInstance, watch, onBeforeUnmount, nextTick, onMounted, toRaw, shallowRef } from 'vue'
|
||||||
import { Chart, registerables } from 'chart.js'
|
import { Chart, registerables } from 'chart.js'
|
||||||
import { generateSocketInstance } from '@/utils'
|
import { generateSocketInstance } from '@/utils'
|
||||||
|
import clipboard from '@/utils/clipboard'
|
||||||
|
|
||||||
// 注册Chart.js所有组件
|
// 注册Chart.js所有组件
|
||||||
Chart.register(...registerables)
|
Chart.register(...registerables)
|
||||||
@@ -604,9 +605,8 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
const handleCopy = async () => {
|
const handleCopy = () => {
|
||||||
await navigator.clipboard.writeText(host.value)
|
clipboard.copy(host.value)
|
||||||
$message.success({ message: '复制成功', center: true })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUsedColor = (num) => {
|
const handleUsedColor = (num) => {
|
||||||
|
|||||||
@@ -665,6 +665,7 @@ import unknowIcon from '@/assets/image/system/unknow.png'
|
|||||||
import { useContextMenu } from '@/composables/useContextMenu'
|
import { useContextMenu } from '@/composables/useContextMenu'
|
||||||
import TextEditor from '@/components/text-editor/index.vue'
|
import TextEditor from '@/components/text-editor/index.vue'
|
||||||
import ImagePreview from '@/components/image-preview/index.vue'
|
import ImagePreview from '@/components/image-preview/index.vue'
|
||||||
|
import clipboard from '@/utils/clipboard'
|
||||||
|
|
||||||
const emit = defineEmits(['exec-script', ])
|
const emit = defineEmits(['exec-script', ])
|
||||||
|
|
||||||
@@ -1592,11 +1593,7 @@ const cancelEditPath = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const copyCurrentPath = () => {
|
const copyCurrentPath = () => {
|
||||||
navigator.clipboard.writeText(currentPath.value).then(() => {
|
clipboard.copy(currentPath.value)
|
||||||
$message.success('路径已复制')
|
|
||||||
}).catch(() => {
|
|
||||||
$message.error('复制失败')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpload = (type) => {
|
const handleUpload = (type) => {
|
||||||
@@ -1962,8 +1959,7 @@ const onRowContextMenu = (row, _column, event) => {
|
|||||||
fullPath = `${ currentPathValue }/${ row.name }`
|
fullPath = `${ currentPathValue }/${ row.name }`
|
||||||
}
|
}
|
||||||
fullPath = fullPath.replace(/\/+/g, '/')
|
fullPath = fullPath.replace(/\/+/g, '/')
|
||||||
navigator.clipboard.writeText(fullPath)
|
clipboard.copy(fullPath)
|
||||||
$message.success('已复制路径')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { useContextMenu } from '@/composables/useContextMenu'
|
|||||||
import { EventBus, isDockerId, isDockerComposeYml, generateSocketInstance } from '@/utils'
|
import { EventBus, isDockerId, isDockerComposeYml, generateSocketInstance } from '@/utils'
|
||||||
import useMobileWidth from '@/composables/useMobileWidth'
|
import useMobileWidth from '@/composables/useMobileWidth'
|
||||||
import { TerminalHighlighter } from '@/utils/highlighter'
|
import { TerminalHighlighter } from '@/utils/highlighter'
|
||||||
|
import clipboard from '@/utils/clipboard'
|
||||||
|
|
||||||
const { CONNECTING, CONNECT_SUCCESS, CONNECT_FAIL, SUSPENDED, RESUMING } = terminalStatus
|
const { CONNECTING, CONNECT_SUCCESS, CONNECT_FAIL, SUSPENDED, RESUMING } = terminalStatus
|
||||||
|
|
||||||
@@ -552,11 +553,8 @@ const onData = () => {
|
|||||||
const handleCopySelection = async () => {
|
const handleCopySelection = async () => {
|
||||||
let str = term.value.getSelection().trim()
|
let str = term.value.getSelection().trim()
|
||||||
if (!str) return
|
if (!str) return
|
||||||
const text = new Blob([str,], { type: 'text/plain' })
|
|
||||||
const item = new ClipboardItem({
|
await clipboard.copy(str)
|
||||||
'text/plain': text
|
|
||||||
})
|
|
||||||
await navigator.clipboard.write([item,])
|
|
||||||
term.value.clearSelection()
|
term.value.clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -855,18 +853,24 @@ const handleClear = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePaste = async () => {
|
const handlePaste = async () => {
|
||||||
let key = await navigator.clipboard.readText()
|
try {
|
||||||
// 规范换行符:无论来自 Windows (\r\n) 还是 Unix (\n),都统一替换成 \r
|
let key = await clipboard.paste()
|
||||||
key = key.replace(/\r\n/g, '\r').replace(/\n/g, '\r')
|
// 规范换行符:无论来自 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)
|
while (key.endsWith('\n')) {
|
||||||
}
|
key = key.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
emit('inputCommand', key, uid)
|
emit('inputCommand', key, uid)
|
||||||
socket.value.emit('input', key)
|
socket.value.emit('input', key)
|
||||||
term.value.focus()
|
term.value.focus()
|
||||||
term.value.clearSelection()
|
term.value.clearSelection()
|
||||||
|
} catch (err) {
|
||||||
|
// HTTP环境下无法读取剪贴板,提示用户使用浏览器原生粘贴
|
||||||
|
console.warn('剪贴板读取失败,请使用 Ctrl+V 或右键粘贴:', err.message)
|
||||||
|
// 不显示错误提示,因为 Ctrl+V 会触发浏览器原生粘贴事件
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusTab = () => {
|
const focusTab = () => {
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ import TerminalWrapper from './components/terminal-wrapper.vue'
|
|||||||
import HostForm from '../server/components/host-form.vue'
|
import HostForm from '../server/components/host-form.vue'
|
||||||
import { randomStr } from '@utils/index.js'
|
import { randomStr } from '@utils/index.js'
|
||||||
import { terminalStatus } from '@/utils/enum'
|
import { terminalStatus } from '@/utils/enum'
|
||||||
|
import clipboard from '@/utils/clipboard'
|
||||||
const { CONNECTING, RESUMING } = terminalStatus
|
const { CONNECTING, RESUMING } = terminalStatus
|
||||||
|
|
||||||
const { proxy: { $store, $message, $api } } = getCurrentInstance()
|
const { proxy: { $store, $message, $api } } = getCurrentInstance()
|
||||||
@@ -216,9 +217,8 @@ onActivated(async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleCopy = async (host) => {
|
const handleCopy = (host) => {
|
||||||
await navigator.clipboard.writeText(host)
|
clipboard.copy(host)
|
||||||
$message.success({ message: '复制成功', center: true })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取挂起的会话列表
|
// 获取挂起的会话列表
|
||||||
|
|||||||
Reference in New Issue
Block a user