mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 08:20:49 +08:00
feat(web): 支持点击键盘状态灯切换锁定状态
为控制台普通和紧凑布局的 Caps、Num、Scroll 状态灯增加点击操作及中英文悬停提示,通过现有 HID 通道发送对应按键的按下和释放事件。 保留远端状态灯反馈,在 HID 未连接、按键已按住或发送过程中禁用操作,并在发送失败时显示提示。
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import type { CanonicalKey } from '@/types/generated'
|
import { CanonicalKey } from '@/types/generated'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Kbd } from '@/components/ui/kbd'
|
import { Kbd } from '@/components/ui/kbd'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
@@ -12,6 +12,7 @@ const props = defineProps<{
|
|||||||
numLock?: boolean
|
numLock?: boolean
|
||||||
scrollLock?: boolean
|
scrollLock?: boolean
|
||||||
keyboardLedEnabled?: boolean
|
keyboardLedEnabled?: boolean
|
||||||
|
keyboardLockDisabled?: boolean
|
||||||
mousePosition?: { x: number; y: number }
|
mousePosition?: { x: number; y: number }
|
||||||
debugMode?: boolean
|
debugMode?: boolean
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
@@ -19,8 +20,18 @@ const props = defineProps<{
|
|||||||
minimal?: boolean
|
minimal?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
toggleLock: [key: CanonicalKey.CapsLock | CanonicalKey.NumLock | CanonicalKey.ScrollLock]
|
||||||
|
}>()
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const lockButtons = computed(() => [
|
||||||
|
{ key: CanonicalKey.CapsLock, label: t('infobar.caps'), active: props.capsLock },
|
||||||
|
{ key: CanonicalKey.NumLock, label: t('infobar.num'), active: props.numLock },
|
||||||
|
{ key: CanonicalKey.ScrollLock, label: t('infobar.scroll'), active: props.scrollLock },
|
||||||
|
] as const)
|
||||||
|
|
||||||
const keyNameMap: Record<string, string> = {
|
const keyNameMap: Record<string, string> = {
|
||||||
MetaLeft: 'Win', MetaRight: 'Win',
|
MetaLeft: 'Win', MetaRight: 'Win',
|
||||||
ControlLeft: 'Ctrl', ControlRight: 'Ctrl',
|
ControlLeft: 'Ctrl', ControlRight: 'Ctrl',
|
||||||
@@ -49,14 +60,24 @@ const keysDisplay = computed(() => {
|
|||||||
<div :class="compact ? '' : 'sm:hidden'">
|
<div :class="compact ? '' : 'sm:hidden'">
|
||||||
<div class="flex items-center justify-between gap-3 text-xs px-2 h-7">
|
<div class="flex items-center justify-between gap-3 text-xs px-2 h-7">
|
||||||
<div v-if="keyboardLedEnabled && !minimal" class="flex items-center gap-1">
|
<div v-if="keyboardLedEnabled && !minimal" class="flex items-center gap-1">
|
||||||
<Badge variant="outline" class="h-5 gap-1 px-1 text-xs text-foreground">
|
<Badge
|
||||||
<span class="size-1.5 rounded-full" :class="capsLock ? 'bg-status-active' : 'bg-muted-foreground/30'" />Caps
|
v-for="lock in lockButtons"
|
||||||
</Badge>
|
:key="lock.key"
|
||||||
<Badge variant="outline" class="h-5 gap-1 px-1 text-xs text-foreground">
|
as="button"
|
||||||
<span class="size-1.5 rounded-full" :class="numLock ? 'bg-status-active' : 'bg-muted-foreground/30'" />Num
|
type="button"
|
||||||
</Badge>
|
variant="outline"
|
||||||
<Badge variant="outline" class="h-5 gap-1 px-1 text-xs text-foreground">
|
class="h-5 gap-1 px-1 text-xs text-foreground cursor-pointer hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
<span class="size-1.5 rounded-full" :class="scrollLock ? 'bg-status-active' : 'bg-muted-foreground/30'" />Scroll
|
:disabled="keyboardLockDisabled || pressedKeys?.includes(lock.key)"
|
||||||
|
:aria-pressed="!!lock.active"
|
||||||
|
:aria-label="t('infobar.toggleLock', { key: lock.key })"
|
||||||
|
:title="t('infobar.toggleLock', { key: lock.key })"
|
||||||
|
@mousedown.prevent
|
||||||
|
@keydown.stop
|
||||||
|
@keyup.stop
|
||||||
|
@click="emit('toggleLock', lock.key)"
|
||||||
|
>
|
||||||
|
<span class="size-1.5 rounded-full" :class="lock.active ? 'bg-status-active' : 'bg-muted-foreground/30'" />
|
||||||
|
{{ lock.label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!minimal" class="text-xs text-muted-foreground">
|
<div v-else-if="!minimal" class="text-xs text-muted-foreground">
|
||||||
@@ -89,17 +110,24 @@ const keysDisplay = computed(() => {
|
|||||||
<div class="flex items-center shrink-0">
|
<div class="flex items-center shrink-0">
|
||||||
<Separator orientation="vertical" class="h-5" />
|
<Separator orientation="vertical" class="h-5" />
|
||||||
<template v-if="keyboardLedEnabled">
|
<template v-if="keyboardLedEnabled">
|
||||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
<Badge
|
||||||
<span class="size-1.5 rounded-full" :class="capsLock ? 'bg-status-active' : 'bg-muted-foreground/30'" />
|
v-for="lock in lockButtons"
|
||||||
{{ t('infobar.caps') }}
|
:key="lock.key"
|
||||||
</Badge>
|
as="button"
|
||||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
type="button"
|
||||||
<span class="size-1.5 rounded-full" :class="numLock ? 'bg-status-active' : 'bg-muted-foreground/30'" />
|
variant="outline"
|
||||||
{{ t('infobar.num') }}
|
class="mx-1 gap-1.5 text-foreground cursor-pointer hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
</Badge>
|
:disabled="keyboardLockDisabled || pressedKeys?.includes(lock.key)"
|
||||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
:aria-pressed="!!lock.active"
|
||||||
<span class="size-1.5 rounded-full" :class="scrollLock ? 'bg-status-active' : 'bg-muted-foreground/30'" />
|
:aria-label="t('infobar.toggleLock', { key: lock.key })"
|
||||||
{{ t('infobar.scroll') }}
|
:title="t('infobar.toggleLock', { key: lock.key })"
|
||||||
|
@mousedown.prevent
|
||||||
|
@keydown.stop
|
||||||
|
@keyup.stop
|
||||||
|
@click="emit('toggleLock', lock.key)"
|
||||||
|
>
|
||||||
|
<span class="size-1.5 rounded-full" :class="lock.active ? 'bg-status-active' : 'bg-muted-foreground/30'" />
|
||||||
|
{{ lock.label }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="px-3 py-1 text-muted-foreground/60">
|
<div v-else class="px-3 py-1 text-muted-foreground/60">
|
||||||
|
|||||||
@@ -293,6 +293,8 @@ export default {
|
|||||||
num: 'Num',
|
num: 'Num',
|
||||||
scroll: 'Scroll',
|
scroll: 'Scroll',
|
||||||
keyboardLedUnavailable: 'Keyboard LED status is disabled or unsupported',
|
keyboardLedUnavailable: 'Keyboard LED status is disabled or unsupported',
|
||||||
|
toggleLock: 'Toggle {key}',
|
||||||
|
toggleLockFailed: 'Failed to toggle keyboard lock',
|
||||||
},
|
},
|
||||||
paste: {
|
paste: {
|
||||||
title: 'Paste Text',
|
title: 'Paste Text',
|
||||||
|
|||||||
@@ -293,6 +293,8 @@ export default {
|
|||||||
num: 'Num',
|
num: 'Num',
|
||||||
scroll: 'Scroll',
|
scroll: 'Scroll',
|
||||||
keyboardLedUnavailable: '键盘状态灯功能未开启或不支持',
|
keyboardLedUnavailable: '键盘状态灯功能未开启或不支持',
|
||||||
|
toggleLock: '切换 {key}',
|
||||||
|
toggleLockFailed: '切换键盘锁定状态失败',
|
||||||
},
|
},
|
||||||
paste: {
|
paste: {
|
||||||
title: '粘贴文本',
|
title: '粘贴文本',
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ const keyboardLed = computed(() => ({
|
|||||||
scrollLock: systemStore.hid?.ledState.scrollLock ?? false,
|
scrollLock: systemStore.hid?.ledState.scrollLock ?? false,
|
||||||
}))
|
}))
|
||||||
const keyboardLedEnabled = computed(() => systemStore.hid?.keyboardLedsEnabled ?? false)
|
const keyboardLedEnabled = computed(() => systemStore.hid?.keyboardLedsEnabled ?? false)
|
||||||
|
const keyboardLockPending = ref(false)
|
||||||
const activeModifierMask = ref(0)
|
const activeModifierMask = ref(0)
|
||||||
const mousePosition = ref({ x: 0, y: 0 })
|
const mousePosition = ref({ x: 0, y: 0 })
|
||||||
const lastMousePosition = ref({ x: 0, y: 0 })
|
const lastMousePosition = ref({ x: 0, y: 0 })
|
||||||
@@ -2210,7 +2211,7 @@ async function handleWol(mac: string) {
|
|||||||
function handleHidError(_error: any, _operation: string) {
|
function handleHidError(_error: any, _operation: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendKeyboardEvent(type: 'down' | 'up', key: CanonicalKey, modifier?: number) {
|
async function transmitKeyboardEvent(type: 'down' | 'up', key: CanonicalKey, modifier?: number) {
|
||||||
if (videoMode.value !== 'mjpeg' && webrtc.dataChannelReady.value) {
|
if (videoMode.value !== 'mjpeg' && webrtc.dataChannelReady.value) {
|
||||||
const event: HidKeyboardEvent = {
|
const event: HidKeyboardEvent = {
|
||||||
type: type === 'down' ? 'keydown' : 'keyup',
|
type: type === 'down' ? 'keydown' : 'keyup',
|
||||||
@@ -2220,7 +2221,30 @@ function sendKeyboardEvent(type: 'down' | 'up', key: CanonicalKey, modifier?: nu
|
|||||||
const sent = webrtc.sendKeyboard(event)
|
const sent = webrtc.sendKeyboard(event)
|
||||||
if (sent) return
|
if (sent) return
|
||||||
}
|
}
|
||||||
hidApi.keyboard(type, key, modifier).catch(err => handleHidError(err, `keyboard ${type}`))
|
await hidApi.keyboard(type, key, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendKeyboardEvent(type: 'down' | 'up', key: CanonicalKey, modifier?: number) {
|
||||||
|
transmitKeyboardEvent(type, key, modifier).catch(err => handleHidError(err, `keyboard ${type}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleKeyboardLock(key: CanonicalKey.CapsLock | CanonicalKey.NumLock | CanonicalKey.ScrollLock) {
|
||||||
|
if (!keyboardLedEnabled.value || hidStatus.value !== 'connected'
|
||||||
|
|| keyboardLockPending.value || pressedKeys.value.includes(key)) return
|
||||||
|
|
||||||
|
keyboardLockPending.value = true
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
await transmitKeyboardEvent('down', key, activeModifierMask.value)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
} finally {
|
||||||
|
await transmitKeyboardEvent('up', key, activeModifierMask.value)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error(t('infobar.toggleLockFailed'))
|
||||||
|
} finally {
|
||||||
|
keyboardLockPending.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMouseEvent(data: { type: 'move' | 'move_abs' | 'down' | 'up' | 'scroll'; x?: number; y?: number; button?: 'left' | 'right' | 'middle'; scroll?: number }) {
|
function sendMouseEvent(data: { type: 'move' | 'move_abs' | 'down' | 'up' | 'scroll'; x?: number; y?: number; button?: 'left' | 'right' | 'middle'; scroll?: number }) {
|
||||||
@@ -3650,8 +3674,10 @@ onUnmounted(() => {
|
|||||||
:num-lock="keyboardLed.numLock"
|
:num-lock="keyboardLed.numLock"
|
||||||
:scroll-lock="keyboardLed.scrollLock"
|
:scroll-lock="keyboardLed.scrollLock"
|
||||||
:keyboard-led-enabled="keyboardLedEnabled"
|
:keyboard-led-enabled="keyboardLedEnabled"
|
||||||
|
:keyboard-lock-disabled="hidStatus !== 'connected' || keyboardLockPending"
|
||||||
:mouse-position="mousePosition"
|
:mouse-position="mousePosition"
|
||||||
:debug-mode="false"
|
:debug-mode="false"
|
||||||
|
@toggle-lock="toggleKeyboardLock"
|
||||||
/>
|
/>
|
||||||
<StatsSheet
|
<StatsSheet
|
||||||
v-if="showConnectionStats"
|
v-if="showConnectionStats"
|
||||||
|
|||||||
Reference in New Issue
Block a user