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

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 stream
.on('close', async () => { .on('close', async () => {
// ssh连接关闭后,再执行一次输出,防止最后一次节流函数发生在延迟时间内导致终端的输出数据丢失 // shell关闭后,再执行一次输出,防止最后一次节流函数发生在延迟时间内导致终端的输出数据丢失
await throttledDataHandler.last() // 等待最后一次节流函数执行完成,再执行一次数据输出 await throttledDataHandler.last() // 等待最后一次节流函数执行完成,再执行一次数据输出
// console.log('onekey终端执行完成, 关闭连接: ', curRes.host) // console.log('onekey终端执行完成, 关闭连接: ', curRes.host)
if (curRes.status === execStatusEnum.executing) { 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) => { module.exports = (httpServer) => {
const serverIo = new Server(httpServer, { const serverIo = new Server(httpServer, {
path: '/terminal', path: '/terminal',
@ -74,6 +97,11 @@ module.exports = (httpServer) => {
consola.success('连接终端成功:', host) consola.success('连接终端成功:', host)
socket.emit('connect_success', `已连接到终端:${ host }`) socket.emit('connect_success', `已连接到终端:${ host }`)
createTerminal(socket, sshClient) 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) => { .on('error', (err) => {
console.log(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" :show-message="false"
> >
<el-form-item label="主题" prop="theme"> <el-form-item label="主题" prop="theme">
<el-select <el-select v-model="theme" placeholder="" style="width: 100%;">
v-model="theme"
placeholder=""
style="width: 100%;"
>
<el-option <el-option
v-for="(value, key) in themeList" v-for="(value, key) in themeList"
:key="key" :key="key"
@ -56,6 +52,9 @@
/> />
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="字体" prop="fontSize">
<el-input-number v-model="fontSize" :min="12" :max="30" />
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog_footer"> <span class="dialog_footer">
@ -80,10 +79,14 @@ const props = defineProps({
}, },
background: { background: {
required: true, 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([ const backgroundImages = ref([
'/terminal/03.png', '/terminal/03.png',
@ -109,6 +112,10 @@ const backgroundUrl = computed({
get: () => props.background, get: () => props.background,
set: (newVal) => emit('update:background', newVal) set: (newVal) => emit('update:background', newVal)
}) })
const fontSize = computed({
get: () => props.fontSize,
set: (newVal) => emit('update:fontSize', newVal)
})
const changeBackground = (url) => { const changeBackground = (url) => {
backgroundUrl.value = url || '' backgroundUrl.value = url || ''

View File

@ -1,9 +1,19 @@
<template> <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> </template>
<script setup> <script setup>
import { ref, onMounted, computed, onBeforeUnmount, getCurrentInstance, watch, nextTick } from 'vue' import { ref, onMounted, computed, onBeforeUnmount, getCurrentInstance, watch, nextTick } from 'vue'
// import CommandHistory from './command_history.vue'
import { Terminal } from '@xterm/xterm' import { Terminal } from '@xterm/xterm'
import '@xterm/xterm/css/xterm.css' import '@xterm/xterm/css/xterm.css'
import { FitAddon } from '@xterm/addon-fit' import { FitAddon } from '@xterm/addon-fit'
@ -20,19 +30,25 @@ const props = defineProps({
required: true, required: true,
type: String type: String
}, },
fontSize: {
required: false,
default: 18,
type: Number
},
theme: { theme: {
required: true, required: true,
type: Object type: Object
}, },
background: { background: {
required: true, required: true,
type: String type: [String, null,]
} }
}) })
const emit = defineEmits(['inputCommand', 'cdCommand',]) const emit = defineEmits(['inputCommand', 'cdCommand',])
const socket = ref(null) const socket = ref(null)
// const commandHistoryList = ref([])
const term = ref(null) const term = ref(null)
const command = ref('') const command = ref('')
const timer = ref(null) const timer = ref(null)
@ -44,6 +60,7 @@ const terminalRef = ref(null)
const token = computed(() => $store.token) const token = computed(() => $store.token)
const theme = computed(() => props.theme) const theme = computed(() => props.theme)
const fontSize = computed(() => props.fontSize)
const background = computed(() => props.background) const background = computed(() => props.background)
watch(theme, () => { watch(theme, () => {
@ -53,6 +70,13 @@ watch(theme, () => {
}) })
}) })
watch(fontSize, () => {
nextTick(() => {
terminal.value.options.fontSize = fontSize.value
fitAddon.value.fit()
})
})
watch(background, (newVal) => { watch(background, (newVal) => {
nextTick(() => { nextTick(() => {
if (newVal) { if (newVal) {
@ -92,6 +116,10 @@ const connectIO = () => {
onWebLinks() onWebLinks()
if (command.value) socket.value.emit('input', command.value + '\n') 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) => { socket.value.on('create_fail', (message) => {
console.error(message) console.error(message)
@ -156,9 +184,10 @@ const createLocalTerminal = () => {
convertEol: true, convertEol: true,
cursorBlink: true, cursorBlink: true,
disableStdin: false, disableStdin: false,
fontSize: 18,
minimumContrastRatio: 7, minimumContrastRatio: 7,
allowTransparency: true, allowTransparency: true,
fontFamily: 'Cascadia Code, Menlo, monospace',
fontSize: fontSize.value,
theme: theme.value theme: theme.value
// { // {
// foreground: '#ECECEC', // 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 = () => { const handleClear = () => {
term.value.clear() term.value.clear()
} }
@ -349,18 +391,31 @@ defineExpose({
<style lang="scss" scoped> <style lang="scss" scoped>
.terminal_tab_container { .terminal_tab_container {
min-height: 200px; min-height: 200px;
position: relative;
.terminal_container {
background-size: 100% 100%;
background-repeat: no-repeat;
background-size: 100% 100%; :deep(.xterm) {
background-repeat: no-repeat; height: 100%;
}
:deep(.xterm) { :deep(.xterm-viewport),
height: 100%; :deep(.xterm-screen) {
padding: 0 0 0 10px;
border-radius: var(--el-border-radius-base);
}
} }
.terminal_command_history {
:deep(.xterm-viewport), width: 200px;
:deep(.xterm-screen) { height: 100%;
padding: 0 0 0 10px; overflow: auto;
border-radius: var(--el-border-radius-base); position: absolute;
top: 0;
right: 0;
z-index: 1;
background-color: #fff;
border-radius: 6px
} }
} }
</style> </style>

View File

@ -125,6 +125,7 @@
:host="item.host" :host="item.host"
:theme="themeObj" :theme="themeObj"
:background="terminalBackground" :background="terminalBackground"
:font-size="terminalFontSize"
@input-command="terminalInput" @input-command="terminalInput"
@cd-command="cdCommand" @cd-command="cdCommand"
/> />
@ -149,6 +150,7 @@
v-model:show="showSetting" v-model:show="showSetting"
v-model:themeName="themeName" v-model:themeName="themeName"
v-model:background="terminalBackground" v-model:background="terminalBackground"
v-model:font-size="terminalFontSize"
@closed="showSetting = false" @closed="showSetting = false"
/> />
</div> </div>
@ -191,7 +193,9 @@ const updateHostData = ref(null)
const showSetting = ref(false) const showSetting = ref(false)
const themeName = ref(localStorage.getItem('themeName') || 'Afterglow') const themeName = ref(localStorage.getItem('themeName') || 'Afterglow')
let localTerminalBackground = localStorage.getItem('terminalBackground') 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 terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length) const terminalTabsLen = computed(() => props.terminalTabs.length)
@ -208,6 +212,10 @@ watch(terminalBackground, (newVal) => {
console.log('update terminalBackground:', newVal) console.log('update terminalBackground:', newVal)
localStorage.setItem('terminalBackground', newVal) localStorage.setItem('terminalBackground', newVal)
}) })
watch(terminalFontSize, (newVal) => {
console.log('update terminalFontSize:', newVal)
localStorage.setItem('terminalFontSize', newVal)
})
onMounted(() => { onMounted(() => {
handleResizeTerminalSftp() handleResizeTerminalSftp()