新增移动端虚拟按键映射

This commit is contained in:
chaos-zhu 2024-10-20 19:59:34 +08:00
parent 6b5f882808
commit fc42e1b29a
6 changed files with 346 additions and 4 deletions

View File

@ -3,9 +3,11 @@
### Features ### Features
* 兼容移动端UI * 兼容移动端UI
* 新增移动端虚拟功能按键映射
* 调整终端功能菜单 * 调整终端功能菜单
* 修复终端选中文本无法复制的bug * 修复终端选中文本无法复制的bug
* 修复无法展示服务端ping客户端延迟ms的bug * 修复无法展示服务端ping客户端延迟ms的bug
* 修复暗黑模式下的一些样式问题
## [2.2.7](https://github.com/chaos-zhu/easynode/releases) (2024-10-17) ## [2.2.7](https://github.com/chaos-zhu/easynode/releases) (2024-10-17)

View File

@ -79,6 +79,9 @@ html.dark {
background-color: #6d6d6d; background-color: #6d6d6d;
} }
.el-menu {
border-right: none;
}
.el-menu-item:not(.is-active):hover { .el-menu-item:not(.is-active):hover {
color: var(--el-menu-active-color); color: var(--el-menu-active-color);
} }

View File

@ -0,0 +1,258 @@
<template>
<div class="mobile_float_menu_container">
<div
class="draggable_ball"
:style="styleObject"
@touchstart="startDrag"
@click.stop="handleClick"
>
<el-icon><Calendar /></el-icon>
</div>
<el-drawer
v-model="showMenu"
direction="ttb"
:with-header="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
:modal="false"
modal-class="keyboard_drawer"
>
<el-divider content-position="left">组合键</el-divider>
<ul class="keyboard">
<li
v-for="item in keyGroup"
:key="item.key"
class="key"
@click="handleClickKey(item)"
>
<div>{{ item.key }}</div>
</li>
</ul>
<el-divider content-position="left">功能键</el-divider>
<ul class="keyboard">
<li
v-for="item in keys"
:key="item.key"
:class="['key', { long_press: item.type === LONG_PRESS }]"
@click="handleClickKey(item)"
>
<div :class="{ active: (item.key === 'Ctrl' && longPressCtrl) || (item.key === 'Alt' && longPressAlt) }">
{{ item.key }}
</div>
</li>
</ul>
</el-drawer>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { Calendar } from '@element-plus/icons-vue'
import { virtualKeyType } from '@/utils/enum'
const props = defineProps({
show: {
type: Boolean,
default: false
},
longPressCtrl: {
type: Boolean,
default: false
},
longPressAlt: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:show', 'click-key',])
let showMenu = computed({
get: () => props.show,
set: (newVal) => emit('update:show', newVal) })
const { LONG_PRESS, SINGLE_PRESS } = virtualKeyType
const keys = ref([
{ key: 'Esc', ascii: 27, type: SINGLE_PRESS, ansi: '\x1B' },
{ key: 'Tab', ascii: 9, type: SINGLE_PRESS, ansi: '\x09' },
{ key: 'Ctrl', ascii: null, type: LONG_PRESS, ansi: '' },
{ key: 'Alt', ascii: null, type: LONG_PRESS, ansi: '' },
{ key: 'F1', ascii: 112, type: SINGLE_PRESS, ansi: '\x1BOP' },
{ key: 'F2', ascii: 113, type: SINGLE_PRESS, ansi: '\x1BOQ' },
{ key: 'F3', ascii: 114, type: SINGLE_PRESS, ansi: '\x1BOR' },
{ key: 'F4', ascii: 115, type: SINGLE_PRESS, ansi: '\x1BOS' },
{ key: 'F5', ascii: 116, type: SINGLE_PRESS, ansi: '\x1B[15~' },
{ key: 'F6', ascii: 117, type: SINGLE_PRESS, ansi: '\x1B[17~' },
{ key: 'F7', ascii: 118, type: SINGLE_PRESS, ansi: '\x1B[18~' },
{ key: 'F8', ascii: 119, type: SINGLE_PRESS, ansi: '\x1B[19~' },
{ key: 'F9', ascii: 120, type: SINGLE_PRESS, ansi: '\x1B[20~' },
{ key: 'F10', ascii: 121, type: SINGLE_PRESS, ansi: '\x1B[21~' },
{ key: 'F11', ascii: 122, type: SINGLE_PRESS, ansi: '\x1B[23~' },
{ key: 'F12', ascii: 123, type: SINGLE_PRESS, ansi: '\x1B[24~' },
{ key: 'Backspace', ascii: 8, type: SINGLE_PRESS, ansi: '\x7F' },
{ key: 'Delete', ascii: 46, type: SINGLE_PRESS, ansi: '\x1B[3~' },
{ key: '↑', ascii: 38, type: SINGLE_PRESS, ansi: '\x1B[A' },
{ key: '→', ascii: 39, type: SINGLE_PRESS, ansi: '\x1B[C' },
{ key: 'Home', ascii: 36, type: SINGLE_PRESS, ansi: '\x1B[H' },
{ key: 'End', ascii: 35, type: SINGLE_PRESS, ansi: '\x1B[F' },
{ key: '↓', ascii: 40, type: SINGLE_PRESS, ansi: '\x1B[B' },
{ key: '←', ascii: 37, type: SINGLE_PRESS, ansi: '\x1B[D' },
{ key: 'PageUp', ascii: 33, type: SINGLE_PRESS, ansi: '\x1B[5~' },
{ key: 'PageDown', ascii: 34, type: SINGLE_PRESS, ansi: '\x1B[6~' },
])
const keyGroup = ref([
{ key: 'Ctrl+C', ascii: null, type: SINGLE_PRESS, ansi: '\x03' },
{ key: 'Ctrl+A', ascii: null, type: SINGLE_PRESS, ansi: '\x01' },
{ key: 'Ctrl+E', ascii: null, type: SINGLE_PRESS, ansi: '\x05' },
{ key: 'Ctrl+L', ascii: null, type: SINGLE_PRESS, ansi: '\x0C' },
{ key: 'Ctrl+R', ascii: null, type: SINGLE_PRESS, ansi: '\x12' },
{ key: ':wq', ascii: null, type: SINGLE_PRESS, ansi: ':wq\r' },
{ key: ':q!', ascii: null, type: SINGLE_PRESS, ansi: ':q!\r' },
{ key: 'dd', ascii: null, type: SINGLE_PRESS, ansi: 'dd\r' },
])
const handleClickKey = (key) => {
emit('click-key', key)
}
const handleClick = () => {
showMenu.value = !showMenu.value
// if (!dragging || (Math.abs(initialX - x.value) < 10 && Math.abs(initialY - y.value) < 10)) {
// }
}
const radius = 20 //
const x = ref(window.innerWidth - radius * 2) //
const y = ref(window.innerHeight - radius * 2)
const styleObject = ref({
position: 'fixed',
top: `${ y.value }px`,
left: `${ x.value }px`,
cursor: 'grab',
userSelect: 'none',
width: `${ radius * 2 }px`, //
height: `${ radius * 2 }px`,
borderRadius: '50%',
backgroundColor: '#42b983',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '14px',
zIndex: '1000'
})
let startX = 0
let startY = 0
let dragging = false
let initialX = 0 // X
let initialY = 0 // Y
const startDrag = (event) => {
const touchEvent = event.type.includes('touch') ? event.touches[0] : event
dragging = true
initialX = touchEvent.clientX
initialY = touchEvent.clientY
startX = touchEvent.clientX - x.value
startY = touchEvent.clientY - y.value
if (event.type.includes('touch')) {
document.addEventListener('touchmove', onDragging)
document.addEventListener('touchend', stopDrag)
} else {
document.addEventListener('mousemove', onDragging)
document.addEventListener('mouseup', stopDrag)
}
// event.preventDefault()
}
const onDragging = (event) => {
if (dragging) {
const moveEvent = event.type.includes('touch') ? event.touches[0] : event
let newX = moveEvent.clientX - startX
let newY = moveEvent.clientY - startY
//
newX = Math.max(newX, -radius) //
newX = Math.min(newX, window.innerWidth - radius) //
newY = Math.max(newY, -radius) //
newY = Math.min(newY, window.innerHeight - radius) //
x.value = newX
y.value = newY
styleObject.value.top = `${ y.value }px`
styleObject.value.left = `${ x.value }px`
}
}
const stopDrag = (event) => {
dragging = false
if (event.type.includes('touch')) {
document.removeEventListener('touchmove', onDragging)
document.removeEventListener('touchend', stopDrag)
} else {
document.removeEventListener('mousemove', onDragging)
document.removeEventListener('mouseup', stopDrag)
}
}
//
onMounted(() => {
x.value = window.innerWidth - radius * 2
y.value = window.innerHeight - radius * 2
styleObject.value.top = `${ y.value }px`
styleObject.value.left = `${ x.value }px`
})
</script>
<style lang="scss">
.mobile_float_menu_container {
.draggable_ball {
transition: background-color 0.3s;
&:active,
&:touch-active {
background-color: #333;
cursor: grabbing;
}
}
.keyboard_drawer {
height: 25vh;
.el-drawer {
height: 100%!important;
.el-drawer__header {
margin-bottom: 10px;
}
.el-drawer__body {
padding: 0 20px;
}
}
.keyboard {
list-style: none;
display: flex;
flex-wrap: wrap;
padding: 0;
.key {
width: 80px;
font-size: 12px;
box-sizing: border-box;
padding: 10px;
text-align: center;
margin-right: 12px;
margin-bottom: 6px;
border: 1px solid #ccc;
min-height: 15px;
}
.long_press {
.active {
// color: red;
font-weight: bolder;
text-decoration: underline;
}
}
}
}
}
</style>

View File

@ -11,5 +11,7 @@ export const terminalStatusList = [
{ value: terminalStatus.CONNECT_FAIL, label: '连接失败', color: '#DC3545' }, { value: terminalStatus.CONNECT_FAIL, label: '连接失败', color: '#DC3545' },
{ value: terminalStatus.CONNECT_SUCCESS, label: '已连接', color: '#28A745' }, { value: terminalStatus.CONNECT_SUCCESS, label: '已连接', color: '#28A745' },
] ]
export const virtualKeyType = {
// other... LONG_PRESS: 'long-press',
SINGLE_PRESS: 'single-press'
}

View File

@ -33,10 +33,18 @@ const props = defineProps({
hostObj: { hostObj: {
required: true, required: true,
type: Object type: Object
},
longPressCtrl: {
type: Boolean,
default: false
},
longPressAlt: {
type: Boolean,
default: false
} }
}) })
const emit = defineEmits(['inputCommand', 'cdCommand', 'ping-data',]) const emit = defineEmits(['inputCommand', 'cdCommand', 'ping-data', 'reset-long-press',])
const socket = ref(null) const socket = ref(null)
// const commandHistoryList = ref([]) // const commandHistoryList = ref([])
@ -64,6 +72,8 @@ const menuCollapse = computed(() => $store.menuCollapse)
const quickCopy = computed(() => $store.terminalConfig.quickCopy) const quickCopy = computed(() => $store.terminalConfig.quickCopy)
const quickPaste = computed(() => $store.terminalConfig.quickPaste) const quickPaste = computed(() => $store.terminalConfig.quickPaste)
const autoExecuteScript = computed(() => $store.terminalConfig.autoExecuteScript) const autoExecuteScript = computed(() => $store.terminalConfig.autoExecuteScript)
const isLongPressCtrl = computed(() => props.longPressCtrl)
const isLongPressAlt = computed(() => props.longPressAlt)
watch(menuCollapse, () => { watch(menuCollapse, () => {
nextTick(() => { nextTick(() => {
@ -326,7 +336,21 @@ function extractLastCdPath(text) {
const onData = () => { const onData = () => {
// term.value.off('data', listenerInput) // term.value.off('data', listenerInput)
term.value.onData((key) => { term.value.onData((key) => {
// console.log('key: ', key)
// if (key === '\x03') console.log('Ctrl + C detected')
if (socketConnected.value === false) return if (socketConnected.value === false) return
if (isLongPressCtrl.value || isLongPressAlt.value) {
const keyCode = key.toUpperCase().charCodeAt(0)
const ansiCode = keyCode - 64
// console.log('ansiCode:', ansiCode)
if (ansiCode >= 1 && ansiCode <= 26) {
const controlChar = String.fromCharCode(ansiCode)
socket.value.emit('input', controlChar)
}
emit('reset-long-press')
return
}
let acsiiCode = key.codePointAt() let acsiiCode = key.codePointAt()
// console.log(acsiiCode) // console.log(acsiiCode)
if (acsiiCode === 22) return handlePaste() // Ctrl + V if (acsiiCode === 22) return handlePaste() // Ctrl + V

View File

@ -155,9 +155,12 @@
<TerminalTab <TerminalTab
ref="terminalRefs" ref="terminalRefs"
:host-obj="item" :host-obj="item"
:long-press-ctrl="longPressCtrl"
:long-press-alt="longPressAlt"
@input-command="terminalInput" @input-command="terminalInput"
@cd-command="cdCommand" @cd-command="cdCommand"
@ping-data="getPingData" @ping-data="getPingData"
@reset-long-press="resetLongPress"
/> />
<Sftp <Sftp
v-if="showSftp" v-if="showSftp"
@ -169,14 +172,25 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
<InputCommand v-model:show="showInputCommand" @input-command="handleInputCommand" /> <InputCommand v-model:show="showInputCommand" @input-command="handleInputCommand" />
<HostForm <HostForm
v-model:show="hostFormVisible" v-model:show="hostFormVisible"
:default-data="updateHostData" :default-data="updateHostData"
@update-list="handleUpdateList" @update-list="handleUpdateList"
@closed="updateHostData = null" @closed="updateHostData = null"
/> />
<TerminalSetting v-model:show="showSetting" /> <TerminalSetting v-model:show="showSetting" />
<FloatMenu
v-if="isMobileScreen"
v-model:show="showFloatMenu"
:long-press-ctrl="longPressCtrl"
:long-press-alt="longPressAlt"
@click-key="handleClickVirtualKeyboard"
/>
</div> </div>
</template> </template>
@ -185,7 +199,8 @@ import { ref, computed, getCurrentInstance, watch, onMounted, onBeforeUnmount, n
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 InputCommand from '@/components/input-command/index.vue' import InputCommand from '@/components/input-command/index.vue'
import { terminalStatusList } from '@/utils/enum' import FloatMenu from '@/components/float-menu/index.vue'
import { terminalStatusList, virtualKeyType } from '@/utils/enum'
import TerminalTab from './terminal-tab.vue' import TerminalTab from './terminal-tab.vue'
import InfoSide from './info-side.vue' import InfoSide from './info-side.vue'
import Sftp from './sftp.vue' import Sftp from './sftp.vue'
@ -217,6 +232,9 @@ const hostFormVisible = ref(false)
const updateHostData = ref(null) const updateHostData = ref(null)
const showSetting = ref(false) const showSetting = ref(false)
const showMobileInfoSideDialog = ref(false) const showMobileInfoSideDialog = ref(false)
const showFloatMenu = ref(false)
const longPressCtrl = ref(false)
const longPressAlt = ref(false)
const terminalTabs = computed(() => props.terminalTabs) const terminalTabs = computed(() => props.terminalTabs)
const terminalTabsLen = computed(() => props.terminalTabs.length) const terminalTabsLen = computed(() => props.terminalTabs.length)
@ -268,6 +286,41 @@ const handleCloseAllTab = () => {
emit('close-all-tab') emit('close-all-tab')
} }
const { LONG_PRESS, SINGLE_PRESS } = virtualKeyType
const handleClickVirtualKeyboard = async (virtualKey) => {
const { key, ansi ,type } = virtualKey
// console.log(key, ascii, ansi, type)
switch (type) {
case LONG_PRESS:
// console.log('')
if (key === 'Ctrl') {
longPressCtrl.value = true
longPressAlt.value = false
}
if (key === 'Alt') {
longPressAlt.value = true
longPressCtrl.value = false
}
// eslint-disable-next-line no-case-declarations
const curTerminalRef = terminalRefs.value[activeTabIndex.value]
await $nextTick()
curTerminalRef?.focusTab()
break
case SINGLE_PRESS:
longPressCtrl.value = false
longPressAlt.value = false
handleExecScript({ command: ansi })
break
default:
break
}
}
const resetLongPress = () => {
longPressCtrl.value = false
longPressAlt.value = false
}
const handleExecScript = (scriptObj) => { const handleExecScript = (scriptObj) => {
let { command } = scriptObj let { command } = scriptObj
if (!isSyncAllSession.value) return handleInputCommand(command) if (!isSyncAllSession.value) return handleInputCommand(command)