✨ 终端支持选中复制&右键粘贴
This commit is contained in:
parent
fd847c1925
commit
44dde760af
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
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"
|
: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 || ''
|
||||||
|
@ -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>
|
||||||
|
@ -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()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user