Skip to content

Commit

Permalink
✨ 新增移动端虚拟按键映射
Browse files Browse the repository at this point in the history
  • Loading branch information
chaos-zhu committed Oct 20, 2024
1 parent 6b5f882 commit fc42e1b
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
### Features

* 兼容移动端UI
* 新增移动端虚拟功能按键映射
* 调整终端功能菜单
* 修复终端选中文本无法复制的bug
* 修复无法展示服务端ping客户端延迟ms的bug
* 修复暗黑模式下的一些样式问题

## [2.2.7](https://github.com/chaos-zhu/easynode/releases) (2024-10-17)

Expand Down
3 changes: 3 additions & 0 deletions web/src/assets/scss/element/dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ html.dark {
background-color: #6d6d6d;
}

.el-menu {
border-right: none;
}
.el-menu-item:not(.is-active):hover {
color: var(--el-menu-active-color);
}
Expand Down
258 changes: 258 additions & 0 deletions web/src/components/float-menu/index.vue
Original file line number Diff line number Diff line change
@@ -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>
6 changes: 4 additions & 2 deletions web/src/utils/enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ export const terminalStatusList = [
{ value: terminalStatus.CONNECT_FAIL, label: '连接失败', color: '#DC3545' },
{ value: terminalStatus.CONNECT_SUCCESS, label: '已连接', color: '#28A745' },
]

// other...
export const virtualKeyType = {
LONG_PRESS: 'long-press',
SINGLE_PRESS: 'single-press'
}
26 changes: 25 additions & 1 deletion web/src/views/terminal/components/terminal-tab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,18 @@ const props = defineProps({
hostObj: {
required: true,
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 commandHistoryList = ref([])
Expand Down Expand Up @@ -64,6 +72,8 @@ const menuCollapse = computed(() => $store.menuCollapse)
const quickCopy = computed(() => $store.terminalConfig.quickCopy)
const quickPaste = computed(() => $store.terminalConfig.quickPaste)
const autoExecuteScript = computed(() => $store.terminalConfig.autoExecuteScript)
const isLongPressCtrl = computed(() => props.longPressCtrl)
const isLongPressAlt = computed(() => props.longPressAlt)
watch(menuCollapse, () => {
nextTick(() => {
Expand Down Expand Up @@ -326,7 +336,21 @@ function extractLastCdPath(text) {
const onData = () => {
// term.value.off('data', listenerInput)
term.value.onData((key) => {
// console.log('key: ', key)
// if (key === '\x03') console.log('Ctrl + C detected')
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()
// console.log(acsiiCode)
if (acsiiCode === 22) return handlePaste() // Ctrl + V
Expand Down
Loading

0 comments on commit fc42e1b

Please sign in to comment.