✨ 终端支持选中复制&右键粘贴
This commit is contained in:
parent
fd847c1925
commit
44dde760af
@ -49,7 +49,7 @@ function execShell(socket, sshClient, curRes, resolve) {
|
||||
}
|
||||
stream
|
||||
.on('close', async () => {
|
||||
// ssh连接关闭后,再执行一次输出,防止最后一次节流函数发生在延迟时间内导致终端的输出数据丢失
|
||||
// shell关闭后,再执行一次输出,防止最后一次节流函数发生在延迟时间内导致终端的输出数据丢失
|
||||
await throttledDataHandler.last() // 等待最后一次节流函数执行完成,再执行一次数据输出
|
||||
// console.log('onekey终端执行完成, 关闭连接: ', curRes.host)
|
||||
if (curRes.status === execStatusEnum.executing) {
|
||||
|
@ -29,6 +29,29 @@ function createTerminal(socket, sshClient) {
|
||||
})
|
||||
}
|
||||
|
||||
function execShell(sshClient, command = '', callback) {
|
||||
if (!command) return
|
||||
let result = ''
|
||||
sshClient.exec(`source ~/.bashrc && ${ command }`, (err, stream) => {
|
||||
if (err) return callback(err.toString())
|
||||
stream
|
||||
.on('data', (data) => {
|
||||
result += data.toString()
|
||||
})
|
||||
.stderr
|
||||
.on('data', (data) => {
|
||||
result += data.toString()
|
||||
})
|
||||
.on('close', () => {
|
||||
consola.info('一次性指令执行完成:', command)
|
||||
callback(result)
|
||||
})
|
||||
.on('error', (error) => {
|
||||
console.log('Error:', error.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = (httpServer) => {
|
||||
const serverIo = new Server(httpServer, {
|
||||
path: '/terminal',
|
||||
@ -74,6 +97,11 @@ module.exports = (httpServer) => {
|
||||
consola.success('连接终端成功:', host)
|
||||
socket.emit('connect_success', `已连接到终端:${ host }`)
|
||||
createTerminal(socket, sshClient)
|
||||
// execShell(sshClient, 'history', (data) => {
|
||||
// data = data.split('\n').filter(item => item)
|
||||
// console.log(data)
|
||||
// socket.emit('terminal_command_history', data)
|
||||
// })
|
||||
})
|
||||
.on('error', (err) => {
|
||||
console.log(err)
|
||||
|
30
web/src/views/terminal/components/command_history.vue
Normal file
30
web/src/views/terminal/components/command_history.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="command_history_container">
|
||||
<ul>
|
||||
<li
|
||||
v-for="item in list"
|
||||
:key="item"
|
||||
>
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
list: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.command_history_container {
|
||||
|
||||
}
|
||||
</style>
|
@ -14,11 +14,7 @@
|
||||
:show-message="false"
|
||||
>
|
||||
<el-form-item label="主题" prop="theme">
|
||||
<el-select
|
||||
v-model="theme"
|
||||
placeholder=""
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-select v-model="theme" placeholder="" style="width: 100%;">
|
||||
<el-option
|
||||
v-for="(value, key) in themeList"
|
||||
:key="key"
|
||||
@ -56,6 +52,9 @@
|
||||
/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="字体" prop="fontSize">
|
||||
<el-input-number v-model="fontSize" :min="12" :max="30" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog_footer">
|
||||
@ -80,10 +79,14 @@ const props = defineProps({
|
||||
},
|
||||
background: {
|
||||
required: true,
|
||||
type: String
|
||||
type: [String, null,]
|
||||
},
|
||||
fontSize: {
|
||||
required: true,
|
||||
type: Number
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:show', 'update:themeName', 'update:background',])
|
||||
const emit = defineEmits(['update:show', 'update:themeName', 'update:background', 'update:fontSize',])
|
||||
|
||||
const backgroundImages = ref([
|
||||
'/terminal/03.png',
|
||||
@ -109,6 +112,10 @@ const backgroundUrl = computed({
|
||||
get: () => props.background,
|
||||
set: (newVal) => emit('update:background', newVal)
|
||||
})
|
||||
const fontSize = computed({
|
||||
get: () => props.fontSize,
|
||||
set: (newVal) => emit('update:fontSize', newVal)
|
||||
})
|
||||
|
||||
const changeBackground = (url) => {
|
||||
backgroundUrl.value = url || ''
|
||||
|
@ -1,9 +1,19 @@
|
||||
<template>
|
||||
<div ref="terminalRef" class="terminal_tab_container" />
|
||||
<div class="terminal_tab_container">
|
||||
<div
|
||||
ref="terminalRef"
|
||||
class="terminal_container"
|
||||
@contextmenu.prevent="handleRightClick"
|
||||
/>
|
||||
<!-- <div class="terminal_command_history">
|
||||
<CommandHistory :list="commandHistoryList" />
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, onBeforeUnmount, getCurrentInstance, watch, nextTick } from 'vue'
|
||||
// import CommandHistory from './command_history.vue'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
@ -20,19 +30,25 @@ const props = defineProps({
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
fontSize: {
|
||||
required: false,
|
||||
default: 18,
|
||||
type: Number
|
||||
},
|
||||
theme: {
|
||||
required: true,
|
||||
type: Object
|
||||
},
|
||||
background: {
|
||||
required: true,
|
||||
type: String
|
||||
type: [String, null,]
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['inputCommand', 'cdCommand',])
|
||||
|
||||
const socket = ref(null)
|
||||
// const commandHistoryList = ref([])
|
||||
const term = ref(null)
|
||||
const command = ref('')
|
||||
const timer = ref(null)
|
||||
@ -44,6 +60,7 @@ const terminalRef = ref(null)
|
||||
|
||||
const token = computed(() => $store.token)
|
||||
const theme = computed(() => props.theme)
|
||||
const fontSize = computed(() => props.fontSize)
|
||||
const background = computed(() => props.background)
|
||||
|
||||
watch(theme, () => {
|
||||
@ -53,6 +70,13 @@ watch(theme, () => {
|
||||
})
|
||||
})
|
||||
|
||||
watch(fontSize, () => {
|
||||
nextTick(() => {
|
||||
terminal.value.options.fontSize = fontSize.value
|
||||
fitAddon.value.fit()
|
||||
})
|
||||
})
|
||||
|
||||
watch(background, (newVal) => {
|
||||
nextTick(() => {
|
||||
if (newVal) {
|
||||
@ -92,6 +116,10 @@ const connectIO = () => {
|
||||
onWebLinks()
|
||||
if (command.value) socket.value.emit('input', command.value + '\n')
|
||||
})
|
||||
// socket.value.on('terminal_command_history', (data) => {
|
||||
// console.log(data)
|
||||
// commandHistoryList.value = data
|
||||
// })
|
||||
})
|
||||
socket.value.on('create_fail', (message) => {
|
||||
console.error(message)
|
||||
@ -156,9 +184,10 @@ const createLocalTerminal = () => {
|
||||
convertEol: true,
|
||||
cursorBlink: true,
|
||||
disableStdin: false,
|
||||
fontSize: 18,
|
||||
minimumContrastRatio: 7,
|
||||
allowTransparency: true,
|
||||
fontFamily: 'Cascadia Code, Menlo, monospace',
|
||||
fontSize: fontSize.value,
|
||||
theme: theme.value
|
||||
// {
|
||||
// foreground: '#ECECEC',
|
||||
@ -304,6 +333,19 @@ const onData = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleRightClick = async () => {
|
||||
try {
|
||||
const clipboardText = await navigator.clipboard.readText()
|
||||
if (!clipboardText) return
|
||||
// 移除多余空格与换行符
|
||||
const formattedText = clipboardText.trim().replace(/\s+/g, ' ').replace(/\n/g, '')
|
||||
if (formattedText.includes('rm -rf /')) return $message.warning(`高危指令,禁止粘贴: ${ formattedText }` )
|
||||
socket.value.emit('input', clipboardText)
|
||||
} catch (error) {
|
||||
$message.warning('右键默认粘贴行为,需要https支持')
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
term.value.clear()
|
||||
}
|
||||
@ -349,18 +391,31 @@ defineExpose({
|
||||
<style lang="scss" scoped>
|
||||
.terminal_tab_container {
|
||||
min-height: 200px;
|
||||
position: relative;
|
||||
.terminal_container {
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
:deep(.xterm) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.xterm) {
|
||||
height: 100%;
|
||||
:deep(.xterm-viewport),
|
||||
:deep(.xterm-screen) {
|
||||
padding: 0 0 0 10px;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.xterm-viewport),
|
||||
:deep(.xterm-screen) {
|
||||
padding: 0 0 0 10px;
|
||||
border-radius: var(--el-border-radius-base);
|
||||
.terminal_command_history {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
background-color: #fff;
|
||||
border-radius: 6px
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -125,6 +125,7 @@
|
||||
:host="item.host"
|
||||
:theme="themeObj"
|
||||
:background="terminalBackground"
|
||||
:font-size="terminalFontSize"
|
||||
@input-command="terminalInput"
|
||||
@cd-command="cdCommand"
|
||||
/>
|
||||
@ -149,6 +150,7 @@
|
||||
v-model:show="showSetting"
|
||||
v-model:themeName="themeName"
|
||||
v-model:background="terminalBackground"
|
||||
v-model:font-size="terminalFontSize"
|
||||
@closed="showSetting = false"
|
||||
/>
|
||||
</div>
|
||||
@ -191,7 +193,9 @@ const updateHostData = ref(null)
|
||||
const showSetting = ref(false)
|
||||
const themeName = ref(localStorage.getItem('themeName') || 'Afterglow')
|
||||
let localTerminalBackground = localStorage.getItem('terminalBackground')
|
||||
const terminalBackground = ref(localTerminalBackground === undefined ? '/01.png' : localTerminalBackground)
|
||||
const terminalBackground = ref(localTerminalBackground || '/terminal/01.png')
|
||||
let localTerminalFontSize = localStorage.getItem('terminalFontSize')
|
||||
const terminalFontSize = ref(Number(localTerminalFontSize) || 18)
|
||||
|
||||
const terminalTabs = computed(() => props.terminalTabs)
|
||||
const terminalTabsLen = computed(() => props.terminalTabs.length)
|
||||
@ -208,6 +212,10 @@ watch(terminalBackground, (newVal) => {
|
||||
console.log('update terminalBackground:', newVal)
|
||||
localStorage.setItem('terminalBackground', newVal)
|
||||
})
|
||||
watch(terminalFontSize, (newVal) => {
|
||||
console.log('update terminalFontSize:', newVal)
|
||||
localStorage.setItem('terminalFontSize', newVal)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
handleResizeTerminalSftp()
|
||||
|
Loading…
x
Reference in New Issue
Block a user