终端支持选中复制&右键粘贴

This commit is contained in:
chaos-zhu 2024-08-15 04:16:33 +08:00
parent fd847c1925
commit 44dde760af
6 changed files with 149 additions and 21 deletions

View File

@ -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) {

View File

@ -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)

View 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>

View File

@ -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 || ''

View File

@ -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>

View File

@ -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()