mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-27 16:00:49 +08:00
feat(web): 新增控制台布局切换并完善状态与磁盘交互
新增经典、悬浮和侧栏布局,保存用户偏好,并调整工具栏、状态摘要及窄屏交互。 统一控制台弹层外观、焦点行为和中英文文案。 根据虚拟磁盘文件访问能力显示浏览入口、容量信息和不可用状态。 验证:npm run build 通过(vue-tsc 类型检查和 Vite 生产构建)。
This commit is contained in:
@@ -14,6 +14,7 @@ import { useVideoScaling } from '@/composables/useVideoScaling'
|
||||
import { useComputerUseSocket, type ComputerUseServerMessage } from '@/composables/useComputerUseSocket'
|
||||
import { useFeatureVisibility } from '@/composables/useFeatureVisibility'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useConsoleLayout } from '@/composables/useConsoleLayout'
|
||||
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
|
||||
import { getMicrophone } from '@/composables/useMicrophone'
|
||||
import { streamApi, hidApi, atxApi, atxConfigApi, authApi, computerUseApi, uacApi } from '@/api'
|
||||
@@ -30,8 +31,11 @@ import { isAudioDeviceLostStateReason, isAudioStreamDeviceLostPayload } from '@/
|
||||
import type { StreamDeviceLostEventData, StreamStateChangedEventData } from '@/types/websocket'
|
||||
import type { VideoMode } from '@/components/VideoConfigPopover.vue'
|
||||
|
||||
import StatusCard, { type StatusDetail } from '@/components/StatusCard.vue'
|
||||
import StatusCard, { type StatusDetail, type ConnectionStatus } from '@/components/StatusCard.vue'
|
||||
import ActionBar from '@/components/ActionBar.vue'
|
||||
import ConsoleHeaderActions from '@/components/ConsoleHeaderActions.vue'
|
||||
import { provideConsoleAppearance } from '@/composables/useConsoleAppearance'
|
||||
import ConsoleStatusSummary, { type ConsoleStatusItem } from '@/components/ConsoleStatusSummary.vue'
|
||||
import InfoBar from '@/components/InfoBar.vue'
|
||||
import VirtualKeyboard from '@/components/VirtualKeyboard.vue'
|
||||
import StatsSheet from '@/components/StatsSheet.vue'
|
||||
@@ -236,14 +240,19 @@ const ttydStatus = ref<{ available: boolean; running: boolean } | null>(null)
|
||||
const showTerminalDialog = ref(false)
|
||||
const featureVisibility = useFeatureVisibility()
|
||||
const { isDark, toggleTheme } = useTheme()
|
||||
const { consoleLayout } = useConsoleLayout()
|
||||
provideConsoleAppearance(consoleLayout)
|
||||
const terminalAvailable = computed(() => ttydStatus.value?.available !== false)
|
||||
const showPower = computed(() => featureVisibility.value.power)
|
||||
const showTerminal = computed(() => terminalAvailable.value && featureVisibility.value.webTerminal)
|
||||
const showComputerUse = computed(() => featureVisibility.value.computerUse)
|
||||
const showPasteText = computed(() => featureVisibility.value.pasteText)
|
||||
|
||||
const videoStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => {
|
||||
const videoStatus = computed<ConnectionStatus>(() => {
|
||||
if (wsNetworkError.value) return 'connecting'
|
||||
if (streamSignalState.value === 'no_signal') return 'no_signal'
|
||||
if (streamSignalState.value === 'device_busy') return 'busy'
|
||||
if (streamSignalState.value === 'device_lost') return 'error'
|
||||
|
||||
if (videoError.value) return 'error'
|
||||
if (videoLoading.value) return 'connecting'
|
||||
@@ -256,6 +265,24 @@ const videoStatus = computed<'connected' | 'connecting' | 'disconnected' | 'erro
|
||||
return 'disconnected'
|
||||
})
|
||||
|
||||
// Optional idle devices are neutral; video and HID must stay visible when unavailable.
|
||||
const consoleStatusItems = computed<ConsoleStatusItem[]>(() => {
|
||||
const items: ConsoleStatusItem[] = [
|
||||
{ id: 'video', title: t('statusCard.video'), status: videoStatus.value,
|
||||
quickInfo: videoQuickInfo.value, details: videoDetails.value, required: true,
|
||||
errorMessage: showSignalOverlay.value ? signalOverlayInfo.value.title : videoErrorMessage.value },
|
||||
{ id: 'audio', title: t('statusCard.audio'), status: audioStatus.value,
|
||||
quickInfo: audioQuickInfo.value, details: audioDetails.value, errorMessage: audioErrorMessage.value },
|
||||
{ id: 'hid', title: t('statusCard.hid'), status: hidStatus.value,
|
||||
quickInfo: hidQuickInfo.value, details: hidDetails.value, errorMessage: hidErrorMessage.value, required: true },
|
||||
]
|
||||
if (showMsdStatusCard.value) items.push({
|
||||
id: 'msd', title: t('statusCard.msd'), status: msdStatus.value,
|
||||
quickInfo: msdQuickInfo.value, details: msdDetails.value, errorMessage: msdErrorMessage.value,
|
||||
})
|
||||
return items
|
||||
})
|
||||
|
||||
function openStatsSheet() {
|
||||
if (showConnectionStats.value) {
|
||||
statsSheetOpen.value = true
|
||||
@@ -288,6 +315,7 @@ const isMjpegPaused = computed(() => {
|
||||
})
|
||||
|
||||
const videoQuickInfo = computed(() => {
|
||||
if (videoStatus.value === 'no_signal' || videoStatus.value === 'busy') return t(`status.${videoStatus.value}`)
|
||||
const stream = systemStore.stream
|
||||
if (!stream?.resolution) return ''
|
||||
const resShort = getResolutionShortName(stream.resolution[0], stream.resolution[1])
|
||||
@@ -306,8 +334,9 @@ const videoDetails = computed<StatusDetail[]>(() => {
|
||||
const formatDisplay = inputFmt === outputFmt ? inputFmt : `${inputFmt} → ${outputFmt}`
|
||||
|
||||
const targetFpsValue = formatFpsValue(stream.targetFps ?? 0)
|
||||
const actualFpsValue = paused ? t('statusCard.paused') : formatFpsValue(receivedFps)
|
||||
const actualStatus: StatusDetail['status'] = paused
|
||||
const signalUnavailable = streamSignalState.value !== 'ok'
|
||||
const actualFpsValue = signalUnavailable ? videoQuickInfo.value : paused ? t('statusCard.paused') : formatFpsValue(receivedFps)
|
||||
const actualStatus: StatusDetail['status'] = signalUnavailable ? 'warning' : paused
|
||||
? undefined
|
||||
: receivedFps > 5 ? 'ok'
|
||||
: receivedFps > 0 ? 'warning'
|
||||
@@ -3157,17 +3186,22 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen h-dvh flex flex-col bg-background">
|
||||
<header class="shrink-0 border-b bg-background">
|
||||
<div class="relative h-screen h-dvh flex flex-col bg-background">
|
||||
<header class="shrink-0 border-b bg-background" :class="consoleLayout === 'floating' && 'console-header--floating'">
|
||||
<div class="px-2 sm:px-4">
|
||||
<div class="h-10 sm:h-14 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2 sm:gap-6">
|
||||
<div class="flex items-center gap-1.5 sm:gap-2">
|
||||
<div class="console-header__row h-10 sm:h-14 flex items-center justify-between">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 sm:gap-6">
|
||||
<div class="flex shrink-0 items-center gap-1.5 whitespace-nowrap sm:gap-2">
|
||||
<BrandMark size="md" class="hidden sm:block" />
|
||||
<BrandMark size="sm" class="sm:hidden" />
|
||||
<span class="font-bold text-sm sm:text-lg">One-KVM</span>
|
||||
</div>
|
||||
<div class="flex md:hidden items-center gap-1">
|
||||
<ConsoleStatusSummary
|
||||
v-if="consoleLayout !== 'current'"
|
||||
:items="consoleStatusItems"
|
||||
:show-video-info="consoleLayout === 'sidebar'"
|
||||
/>
|
||||
<div v-else class="flex md:hidden items-center gap-1">
|
||||
<StatusCard
|
||||
:title="t('statusCard.video')"
|
||||
type="video"
|
||||
@@ -3189,8 +3223,19 @@ onUnmounted(() => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 sm:gap-2">
|
||||
<div class="hidden md:flex items-center gap-2">
|
||||
<div v-if="consoleLayout === 'floating'" id="console-header-toolbar" class="console-header__toolbar" />
|
||||
<div class="console-header__account flex shrink-0 items-center gap-1 sm:gap-2">
|
||||
<ConsoleHeaderActions
|
||||
v-if="consoleLayout === 'floating'"
|
||||
:show-stats="showConnectionStats"
|
||||
:show-terminal="showTerminal"
|
||||
:terminal-running="!!ttydStatus?.running"
|
||||
:show-computer-use="showComputerUse"
|
||||
@open-stats="openStatsSheet"
|
||||
@open-terminal="openTerminal"
|
||||
@open-computer-use="openComputerUse"
|
||||
/>
|
||||
<div v-if="consoleLayout === 'current'" class="hidden md:flex items-center gap-2">
|
||||
<StatusCard
|
||||
:title="t('statusCard.video')"
|
||||
type="video"
|
||||
@@ -3270,7 +3315,9 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<Teleport defer :to="consoleLayout === 'floating' ? '#console-header-toolbar' : 'body'" :disabled="consoleLayout !== 'floating'">
|
||||
<ActionBar
|
||||
:layout="consoleLayout"
|
||||
:mouse-mode="mouseMode"
|
||||
:video-mode="videoMode"
|
||||
:ttyd-running="ttydStatus?.running"
|
||||
@@ -3294,7 +3341,11 @@ onUnmounted(() => {
|
||||
@open-terminal="openTerminal"
|
||||
@open-computer-use="openComputerUse"
|
||||
/>
|
||||
<div class="flex-1 overflow-hidden relative">
|
||||
</Teleport>
|
||||
<div
|
||||
class="flex-1 overflow-hidden relative transition-[padding] duration-300"
|
||||
:class="consoleLayout === 'sidebar' && 'pl-14 sm:pl-16'"
|
||||
>
|
||||
<div class="absolute inset-0 dot-grid-bg" />
|
||||
<div class="relative flex h-full w-full min-w-0 items-stretch gap-3 p-1 sm:p-4">
|
||||
<div
|
||||
@@ -3534,6 +3585,13 @@ onUnmounted(() => {
|
||||
</Teleport>
|
||||
<div id="keyboard-anchor"></div>
|
||||
<InfoBar
|
||||
v-if="consoleLayout !== 'floating' || pressedKeys.length > 0 || isPointerLocked"
|
||||
:compact="consoleLayout !== 'current'"
|
||||
:captured="isPointerLocked"
|
||||
:minimal="consoleLayout === 'floating'"
|
||||
:class="consoleLayout === 'floating'
|
||||
? 'pointer-events-none absolute bottom-2 left-1/2 z-30 max-w-[calc(100%-1rem)] -translate-x-1/2 rounded-lg border shadow-sm !w-auto'
|
||||
: 'shrink-0'"
|
||||
:pressed-keys="pressedKeys"
|
||||
:caps-lock="keyboardLed.capsLock"
|
||||
:num-lock="keyboardLed.numLock"
|
||||
@@ -3601,6 +3659,27 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Expanded controls may overlap header actions, but stay outside the video stage. */
|
||||
.console-header--floating .console-header__row {
|
||||
position: relative;
|
||||
height: 60px;
|
||||
}
|
||||
.console-header__toolbar {
|
||||
position: absolute;
|
||||
z-index: 40;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
max-width: 64rem;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.console-header__toolbar :deep(.console-action-bar) {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
Reference in New Issue
Block a user