feat(web): 改版设置页与控制台界面

统一登录页、控制台和设置页的视觉样式,固定控制台设置入口并补充控件展开状态。
设置页改为按卡片保存,分别处理采集、编码、连接、HID 功能和设备标识,保留相邻卡片草稿。
增加共享帮助提示和移动端导航,固定前端构建依赖。

验证:独立暂存快照前端构建通过;控制台浏览器测试 5 组通过,覆盖登录、三种布局、窄屏、主题和语言切换。
This commit is contained in:
mofeng-git
2026-10-28 07:18:59 +08:00
parent fc99c6f274
commit fed753261b
27 changed files with 6837 additions and 1961 deletions

View File

@@ -1,7 +1,6 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useSystemStore } from '@/stores/system'
import type { VideoRotation, VideoScaleMode } from '@/composables/useVideoScaling'
import { Button } from '@/components/ui/button'
@@ -33,7 +32,6 @@ import {
import {
ClipboardPaste,
HardDrive,
Settings,
Power,
BarChart3,
Terminal,
@@ -53,7 +51,6 @@ import VideoDisplayControls from '@/components/VideoDisplayControls.vue'
import type { ConsoleLayout } from '@/composables/useConsoleLayout'
const { t, locale } = useI18n()
const router = useRouter()
const systemStore = useSystemStore()
const overflowMenuOpen = ref(false)
@@ -72,6 +69,10 @@ const props = defineProps<{
showMic?: boolean
scaleMode?: VideoScaleMode
sourceSizeAvailable?: boolean
keyboardVisible?: boolean
fullscreen?: boolean
statsOpen?: boolean
terminalOpen?: boolean
}>()
const isSidebarLayout = computed(() => props.layout === 'sidebar')
const isFloatingLayout = computed(() => props.layout === 'floating')
@@ -159,7 +160,7 @@ let layoutResizeObserver: ResizeObserver | null = null
type CollapsibleItem =
| 'msd' | 'atx' | 'paste'
| 'stats' | 'terminal' | 'settings'
| 'stats' | 'terminal'
interface ItemSpec {
id: CollapsibleItem
@@ -172,7 +173,6 @@ const ITEM_SPECS: ItemSpec[] = [
{ id: 'paste', side: 'left' },
{ id: 'stats', side: 'right' },
{ id: 'terminal', side: 'right' },
{ id: 'settings', side: 'right' },
]
const measuredWidths = ref<Map<CollapsibleItem, { icon: number; label: number }>>(new Map())
@@ -300,7 +300,7 @@ const visibleSet = computed(() => {
if (isSidebarLayout.value) {
// Reserve More and a gap between the two groups before assigning vertical slots.
let available = barHeight.value - coreHeight.value - fixedHeight.value - actionHeight.value - 16
const priority: CollapsibleItem[] = ['paste', 'settings', 'msd', 'atx', 'stats', 'terminal']
const priority: CollapsibleItem[] = ['paste', 'msd', 'atx', 'stats', 'terminal']
for (const id of priority) {
if (!collapsibleItems.value.some(item => item.id === id)) continue
if (available < actionHeight.value) break
@@ -365,7 +365,7 @@ const hasRightOverflow = computed(() => {
:aria-expanded="false"
@click="setFloatingCollapsed(false)"
>
<ChevronDown class="size-4" />{{ t('actionbar.expandToolbar') }}
<ChevronDown class="size-4" /><span class="hidden sm:inline">{{ t('actionbar.expandToolbar') }}</span>
</Button>
<div
v-show="!isFloatingLayout || !floatingCollapsed"
@@ -424,6 +424,8 @@ const hasRightOverflow = computed(() => {
size="sm"
class="h-8 gap-1.5 text-xs"
:aria-label="t('actionbar.virtualMedia')"
aria-haspopup="dialog"
:aria-expanded="msdDialogOpen"
:title="t('actionbar.virtualMedia')"
@click="msdDialogOpen = true"
>
@@ -512,6 +514,8 @@ const hasRightOverflow = computed(() => {
:text-only-scale="isSidebarLayout"
:scale-mode="props.scaleMode"
:source-size-available="props.sourceSizeAvailable"
:keyboard-visible="props.keyboardVisible"
:fullscreen="props.fullscreen"
@toggle-fullscreen="emit('toggleFullscreen')"
@update:scale-mode="emit('update:scaleMode', $event)"
@toggle-virtual-keyboard="emit('toggleVirtualKeyboard')"
@@ -522,7 +526,7 @@ const hasRightOverflow = computed(() => {
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" :aria-label="t('actionbar.stats')" @click="emit('toggleStats')">
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" :aria-label="t('actionbar.stats')" :aria-pressed="!!props.statsOpen" @click="emit('toggleStats')">
<BarChart3 class="size-4" />
<span v-if="visibleSet.get('stats') === 'label'">{{ t('actionbar.stats') }}</span>
</Button>
@@ -545,6 +549,7 @@ const hasRightOverflow = computed(() => {
class="h-8 gap-1.5 text-xs"
:disabled="!props.ttydRunning"
:aria-label="t('actionbar.webTerminal')"
:aria-pressed="!!props.terminalOpen"
@click="emit('openTerminal')"
>
<Terminal class="size-4" />
@@ -558,25 +563,6 @@ const hasRightOverflow = computed(() => {
</TooltipProvider>
</div>
<!-- Settings - Adaptive -->
<div v-if="isVisible('settings')">
<TooltipProvider>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" :aria-label="t('actionbar.settings')" @click="router.push('/settings')">
<Settings class="size-4" />
<span v-if="visibleSet.get('settings') === 'label'">{{ t('actionbar.settings') }}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ t('actionbar.settingsTip') }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<!-- Overflow Menu - Only show if there are overflowed items -->
<DropdownMenu v-if="hasOverflow || minimalDisplayControls" v-model:open="overflowMenuOpen">
<DropdownMenuTrigger as-child>
@@ -632,12 +618,6 @@ const hasRightOverflow = computed(() => {
<Terminal class="size-4 mr-2" />
{{ t('actionbar.webTerminal') }}
</DropdownMenuItem>
<!-- Settings -->
<DropdownMenuItem v-if="!isFloatingLayout && !isVisible('settings')" @click="openFromOverflow(() => router.push('/settings'))">
<Settings class="size-4 mr-2" />
{{ t('actionbar.settings') }}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
@@ -700,7 +680,7 @@ const hasRightOverflow = computed(() => {
<!-- Hidden measurement container: renders each collapsible button in both
icon-only and with-label forms so we can read their real offsetWidth. -->
<div ref="measureRef" aria-hidden="true" class="fixed pointer-events-none" style="visibility: hidden; top: -9999px; left: -9999px; white-space: nowrap;">
<div ref="measureRef" aria-hidden="true" class="console-action-measure fixed pointer-events-none" style="visibility: hidden; top: -9999px; left: -9999px; white-space: nowrap;">
<div class="flex items-center gap-0.5 sm:gap-1.5 px-2 sm:px-4 py-1 sm:py-1.5">
<!-- MSD -->
<Button data-measure="msd-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
@@ -717,9 +697,6 @@ const hasRightOverflow = computed(() => {
<!-- Web Terminal -->
<Button data-measure="terminal-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="size-4" /></Button>
<Button data-measure="terminal-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="size-4" />{{ t('actionbar.webTerminal') }}</Button>
<!-- Settings -->
<Button data-measure="settings-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="size-4" /></Button>
<Button data-measure="settings-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="size-4" />{{ t('actionbar.settings') }}</Button>
</div>
</div>
</template>
@@ -765,19 +742,13 @@ const hasRightOverflow = computed(() => {
gap: 0;
overflow: hidden;
padding-inline: 0.5rem;
border-radius: 0.5rem;
border-radius: 10px;
}
.console-action-bar--sidebar :deep(button > span) {
display: none;
}
.console-action-bar--sidebar :deep([data-fixed-action][aria-hidden='true']) {
width: 1.5rem;
height: 1px;
margin: 0.35rem auto;
}
.console-action-bar--sidebar .console-action-bar__inner {
overflow-y: auto;
gap: 1rem;
@@ -801,10 +772,6 @@ const hasRightOverflow = computed(() => {
border-radius: 10px;
}
.console-action-bar--floating .console-action-bar__inner :deep(button[aria-expanded='true']) {
background: var(--accent);
}
@media (max-width: 639px) {
.console-action-bar--floating .console-action-bar__inner {
padding-inline: 4px;