update: 更新前端依赖;修改前端样式

This commit is contained in:
mofeng-git
2026-07-17 01:33:44 +08:00
parent aca847e76c
commit 57d1597264
146 changed files with 3790 additions and 2982 deletions

View File

@@ -12,6 +12,7 @@ import { useWebRTC } from '@/composables/useWebRTC'
import { useVideoSession } from '@/composables/useVideoSession'
import { useComputerUseSocket, type ComputerUseServerMessage } from '@/composables/useComputerUseSocket'
import { useFeatureVisibility } from '@/composables/useFeatureVisibility'
import { useTheme } from '@/composables/useTheme'
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
import { streamApi, hidApi, atxApi, atxConfigApi, authApi, computerUseApi } from '@/api'
import type { ComputerUseScreenshot, ComputerUseSession } from '@/api'
@@ -187,13 +188,12 @@ const changingPassword = ref(false)
const ttydStatus = ref<{ available: boolean; running: boolean } | null>(null)
const showTerminalDialog = ref(false)
const featureVisibility = useFeatureVisibility()
const { isDark, toggleTheme } = useTheme()
const terminalAvailable = computed(() => ttydStatus.value?.available !== false)
const showTerminal = computed(() => terminalAvailable.value && featureVisibility.value.webTerminal)
const showComputerUse = computed(() => featureVisibility.value.computerUse)
const showPasteText = computed(() => featureVisibility.value.pasteText)
const isDark = ref(document.documentElement.classList.contains('dark'))
const videoStatus = computed<'connected' | 'connecting' | 'disconnected' | 'error'>(() => {
if (wsNetworkError.value) return 'connecting'
@@ -1973,12 +1973,6 @@ async function toggleFullscreen() {
}
}
function toggleTheme() {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
}
async function logout() {
await authStore.logout()
router.push('/login')
@@ -2657,12 +2651,6 @@ onMounted(async () => {
syncMouseModeFromConfig()
})
const storedTheme = localStorage.getItem('theme')
if (storedTheme === 'dark' || (!storedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
isDark.value = true
document.documentElement.classList.add('dark')
}
try {
const modeResp = await streamApi.getMode()
const serverMode = normalizeServerMode(modeResp?.mode)
@@ -2720,7 +2708,7 @@ onUnmounted(() => {
<template>
<div class="h-screen h-dvh flex flex-col bg-background">
<header class="shrink-0 border-b border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<header class="shrink-0 border-b bg-background">
<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">
@@ -2790,7 +2778,7 @@ onUnmounted(() => {
hover-align="end"
/>
</div>
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700 hidden md:block mx-1" />
<div class="mx-1 hidden h-6 w-px bg-border md:block" />
<Button variant="ghost" size="icon" class="h-8 w-8 hidden md:flex" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
<Sun v-if="isDark" class="h-4 w-4" />
<Moon v-else class="h-4 w-4" />

View File

@@ -5,12 +5,13 @@ import { useI18n } from 'vue-i18n'
import { useAuthStore } from '@/stores/auth'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
import LanguageToggleButton from '@/components/LanguageToggleButton.vue'
import BrandMark from '@/components/BrandMark.vue'
import { Lock, Eye, EyeOff, User, CircleHelp } from 'lucide-vue-next'
import { AlertCircle, Lock, Eye, EyeOff, User, CircleHelp } from 'lucide-vue-next'
const { t } = useI18n()
const router = useRouter()
@@ -73,9 +74,10 @@ async function handleLogin() {
</CardHeader>
<CardContent>
<form class="space-y-4" @submit.prevent="handleLogin">
<div class="space-y-2">
<Label for="username">{{ t('auth.username') }}</Label>
<form @submit.prevent="handleLogin">
<FieldGroup>
<Field>
<FieldLabel for="username">{{ t('auth.username') }}</FieldLabel>
<div class="relative">
<User class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
@@ -87,10 +89,30 @@ async function handleLogin() {
class="pl-10"
/>
</div>
</div>
</Field>
<div class="space-y-2">
<Label for="password">{{ t('auth.password') }}</Label>
<Field>
<div class="flex items-center justify-between gap-2">
<FieldLabel for="password">{{ t('auth.password') }}</FieldLabel>
<Popover>
<PopoverTrigger as-child>
<Button
type="button"
variant="link"
size="sm"
class="h-auto gap-1 p-0 text-xs text-muted-foreground"
>
{{ t('auth.forgotPassword') }}
<CircleHelp class="h-3.5 w-3.5" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-80 p-3" align="end">
<p class="text-xs text-muted-foreground">
{{ t('auth.forgotPasswordHint') }}
</p>
</PopoverContent>
</Popover>
</div>
<div class="relative">
<Lock class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
@@ -101,43 +123,31 @@ async function handleLogin() {
:placeholder="t('auth.password')"
class="pl-10 pr-10"
/>
<button
<Button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
variant="ghost"
size="icon-sm"
class="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground"
:aria-label="showPassword ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" class="w-4 h-4" />
<EyeOff v-else class="w-4 h-4" />
</button>
</Button>
</div>
</div>
</Field>
<p v-if="error" class="text-center text-sm text-destructive">{{ error }}</p>
<Alert v-if="error" variant="destructive">
<AlertCircle />
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
<Button type="submit" class="w-full" :disabled="loading">
<span v-if="loading">{{ t('common.loading') }}</span>
<span v-else>{{ t('auth.login') }}</span>
</Button>
<div class="text-right">
<Popover>
<PopoverTrigger as-child>
<button
type="button"
class="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
>
{{ t('auth.forgotPassword') }}
<CircleHelp class="h-3.5 w-3.5" />
</button>
</PopoverTrigger>
<PopoverContent class="w-80 p-3" align="end">
<p class="text-xs text-muted-foreground">
{{ t('auth.forgotPasswordHint') }}
</p>
</PopoverContent>
</Popover>
</div>
</FieldGroup>
</form>
</CardContent>
</Card>

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,9 @@ import {
HoverCardTrigger,
} from '@/components/ui/hover-card'
import { Switch } from '@/components/ui/switch'
import { Stepper, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/components/ui/stepper'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
import {
Eye,
EyeOff,
@@ -37,6 +40,7 @@ import {
HelpCircle,
Puzzle,
RefreshCw,
AlertTriangle,
} from 'lucide-vue-next'
const { t } = useI18n()
@@ -157,7 +161,7 @@ const passwordStrengthText = computed(() => {
})
const passwordStrengthColor = computed(() => {
const colors = ['bg-muted', 'bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500']
const colors = ['bg-muted', 'bg-destructive', 'bg-warning', 'bg-warning', 'bg-success']
return colors[passwordStrength.value] || colors[0]
})
@@ -552,37 +556,36 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
{{ t('setup.progress', { current: step, total: totalSteps }) }}
</p>
<!-- Step Indicator with Labels -->
<div class="flex items-center justify-center gap-1.5 sm:gap-2 mb-5 sm:mb-6">
<template v-for="i in totalSteps" :key="i">
<div class="flex flex-col items-center gap-1">
<div
class="flex items-center justify-center w-9 h-9 sm:w-10 sm:h-10 rounded-full border-2 transition-all duration-300"
:class="
step > i
? 'bg-primary border-primary text-primary-foreground scale-100'
: step === i
? 'border-primary text-primary scale-110'
: 'border-muted text-muted-foreground scale-100'
"
>
<Check v-if="step > i" class="w-4 h-4 sm:w-5 sm:h-5" />
<component :is="stepIcons[i - 1]" v-else class="w-4 h-4 sm:w-5 sm:h-5" />
</div>
<span
class="text-[10px] sm:text-xs transition-colors duration-300 max-w-14 sm:max-w-16 text-center leading-tight"
:class="step >= i ? 'text-foreground font-medium' : 'text-muted-foreground'"
>
{{ stepLabels[i - 1] }}
</span>
</div>
<div
<Stepper :model-value="step" class="mb-5 flex w-full items-start gap-1 sm:mb-6 sm:gap-2">
<StepperItem
v-for="i in totalSteps"
:key="i"
v-slot="{ state }"
:step="i"
class="relative flex w-full flex-col items-center justify-center"
>
<StepperSeparator
v-if="i < totalSteps"
class="w-5 sm:w-8 h-0.5 transition-colors duration-300 mb-5 sm:mb-6"
:class="step > i ? 'bg-primary' : 'bg-muted'"
class="absolute left-[calc(50%+18px)] right-[calc(-50%+8px)] top-5 h-0.5 rounded-full group-data-[state=completed]:bg-primary"
/>
</template>
</div>
<StepperTrigger as-child>
<Button
type="button"
size="icon"
:variant="state === 'completed' || state === 'active' ? 'default' : 'outline'"
class="z-10 size-9 shrink-0 rounded-full disabled:opacity-100 sm:size-10"
:class="state === 'active' && 'ring-2 ring-ring ring-offset-2 ring-offset-background'"
disabled
>
<Check v-if="state === 'completed'" class="size-4 sm:size-5" />
<component :is="stepIcons[i - 1]" v-else class="size-4 sm:size-5" />
</Button>
</StepperTrigger>
<StepperTitle class="mt-1 max-w-14 whitespace-normal text-center text-[10px] leading-tight sm:max-w-16 sm:text-xs" :class="state === 'active' ? 'text-foreground' : 'text-muted-foreground'">
{{ stepLabels[i - 1] }}
</StepperTitle>
</StepperItem>
</Stepper>
<!-- Step Content with Animation -->
<Transition :name="slideDirection === 'forward' ? 'slide-forward' : 'slide-backward'" mode="out-in">
@@ -629,15 +632,17 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
@blur="validatePassword"
@input="passwordTouched && validatePassword()"
/>
<button
<Button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
variant="ghost"
size="icon-sm"
class="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground"
:aria-label="showPassword ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" class="w-4 h-4" />
<EyeOff v-else class="w-4 h-4" />
</button>
</Button>
</div>
<p v-if="passwordError" class="text-xs text-destructive">{{ passwordError }}</p>
@@ -684,92 +689,79 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<Label for="videoDevice">{{ t('setup.videoDevice') }}</Label>
<HoverCard>
<HoverCardTrigger as-child>
<button type="button" class="text-muted-foreground hover:text-foreground transition-colors" :aria-label="t('common.info')">
<Button type="button" variant="ghost" size="icon-xs" class="text-muted-foreground" :aria-label="t('common.info')">
<HelpCircle class="w-4 h-4" />
</button>
</Button>
</HoverCardTrigger>
<HoverCardContent class="w-64 text-sm">
{{ t('setup.videoDeviceHelp') }}
</HoverCardContent>
</HoverCard>
</div>
<Select v-model="videoDevice">
<SelectTrigger>
<SelectValue :placeholder="t('setup.selectVideoDevice')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="dev in devices.video" :key="dev.path" :value="dev.path">
<NativeSelect v-model="videoDevice" class="w-full">
<NativeSelectOption value="">{{ t('setup.selectVideoDevice') }}</NativeSelectOption>
<NativeSelectOption v-for="dev in devices.video" :key="dev.path" :value="dev.path">
{{ formatVideoDeviceLabel(dev) }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
<div v-if="videoDevice && !selectedDeviceHasSignal" class="flex items-center gap-3 p-3 rounded-lg border border-orange-500/30 bg-orange-500/5 text-sm text-orange-600 dark:text-orange-400">
<Alert v-if="videoDevice && !selectedDeviceHasSignal" variant="warning">
<AlertTriangle />
<AlertDescription class="flex items-center gap-3">
<p class="flex-1">{{ t('setup.noSignalDetected') }}</p>
<Button variant="outline" size="sm" :disabled="refreshingDevices" @click="refreshDeviceList">
<RefreshCw class="w-4 h-4 mr-1" :class="{ 'animate-spin': refreshingDevices }" />
{{ t('setup.refreshDevices') }}
</Button>
</div>
</AlertDescription>
</Alert>
<div v-if="videoDevice" class="space-y-2">
<div class="flex items-center gap-2">
<Label for="videoFormat">{{ t('setup.videoFormat') }}</Label>
<HoverCard>
<HoverCardTrigger as-child>
<button type="button" class="text-muted-foreground hover:text-foreground transition-colors" :aria-label="t('common.info')">
<Button type="button" variant="ghost" size="icon-xs" class="text-muted-foreground" :aria-label="t('common.info')">
<HelpCircle class="w-4 h-4" />
</button>
</Button>
</HoverCardTrigger>
<HoverCardContent class="w-64 text-sm">
{{ t('setup.videoFormatHelp') }}
</HoverCardContent>
</HoverCard>
</div>
<Select v-model="videoFormat">
<SelectTrigger>
<SelectValue :placeholder="t('setup.selectFormat')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="fmt in availableFormats" :key="fmt.format" :value="fmt.format">
<NativeSelect v-model="videoFormat" class="w-full">
<NativeSelectOption value="">{{ t('setup.selectFormat') }}</NativeSelectOption>
<NativeSelectOption v-for="fmt in availableFormats" :key="fmt.format" :value="fmt.format">
{{ fmt.format }} - {{ fmt.description }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
<div v-if="videoFormat" class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="videoResolution">{{ t('setup.resolution') }}</Label>
<Select v-model="videoResolution">
<SelectTrigger>
<SelectValue :placeholder="t('setup.selectResolution')" />
</SelectTrigger>
<SelectContent>
<SelectItem
<NativeSelect v-model="videoResolution" class="w-full">
<NativeSelectOption value="">{{ t('setup.selectResolution') }}</NativeSelectOption>
<NativeSelectOption
v-for="res in availableResolutions"
:key="`${res.width}x${res.height}`"
:value="`${res.width}x${res.height}`"
>
{{ res.width }}x{{ res.height }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
<div class="space-y-2">
<Label for="videoFps">{{ t('setup.fps') }}</Label>
<Select v-model="videoFps">
<SelectTrigger>
<SelectValue :placeholder="t('setup.selectFps')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="fps in availableFps" :key="fps" :value="fps">
<NativeSelect :model-value="videoFps" class="w-full" @update:model-value="value => videoFps = value === '' ? null : Number(value)">
<NativeSelectOption value="">{{ t('setup.selectFps') }}</NativeSelectOption>
<NativeSelectOption v-for="fps in availableFps" :key="fps" :value="fps">
{{ formatFpsLabel(fps) }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
</div>
</div>
@@ -783,27 +775,20 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<Label for="audioDevice">{{ t('setup.audioDevice') }}</Label>
<HoverCard>
<HoverCardTrigger as-child>
<button type="button" class="text-muted-foreground hover:text-foreground transition-colors" :aria-label="t('common.info')">
<Button type="button" variant="ghost" size="icon-xs" class="text-muted-foreground" :aria-label="t('common.info')">
<HelpCircle class="w-4 h-4" />
</button>
</Button>
</HoverCardTrigger>
<HoverCardContent class="w-64 text-sm">
{{ t('setup.audioDeviceHelp') }}
</HoverCardContent>
</HoverCard>
</div>
<Select v-model="audioDevice" :disabled="!audioEnabled">
<SelectTrigger>
<SelectValue :placeholder="t('setup.selectAudioDevice')" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none__">{{ t('setup.noAudio') }}</SelectItem>
<SelectItem v-for="dev in devices.audio" :key="dev.name" :value="dev.name">
{{ dev.description }}
<span v-if="dev.is_hdmi" class="text-xs text-muted-foreground ml-1">(HDMI)</span>
</SelectItem>
</SelectContent>
</Select>
<NativeSelect v-model="audioDevice" class="w-full" :disabled="!audioEnabled">
<NativeSelectOption value="">{{ t('setup.selectAudioDevice') }}</NativeSelectOption>
<NativeSelectOption value="__none__">{{ t('setup.noAudio') }}</NativeSelectOption>
<NativeSelectOption v-for="dev in devices.audio" :key="dev.name" :value="dev.name">{{ dev.description }}{{ dev.is_hdmi ? ' (HDMI)' : '' }}</NativeSelectOption>
</NativeSelect>
<p v-if="!devices.audio.length" class="text-xs text-muted-foreground">
{{ t('setup.noAudioDevices') }}
</p>
@@ -811,9 +796,10 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<!-- Advanced: Encoder Backend (Collapsible) -->
<div class="mt-4 border rounded-lg">
<button
<Button
type="button"
class="w-full flex items-center justify-between p-3 text-left hover:bg-muted/50 rounded-lg transition-colors"
variant="ghost"
class="h-auto w-full justify-between rounded-lg p-3 text-left"
:aria-label="t('setup.advancedEncoder')"
@click="showAdvancedEncoder = !showAdvancedEncoder"
>
@@ -824,7 +810,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
class="h-4 w-4 transition-transform duration-200"
:class="{ 'rotate-90': showAdvancedEncoder }"
/>
</button>
</Button>
<div v-if="showAdvancedEncoder" class="px-3 pb-3 space-y-3">
<p class="text-xs text-muted-foreground">
{{ t('setup.encoderHint') }}
@@ -873,16 +859,12 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<div class="space-y-2">
<Label for="ch9329Port">{{ t('setup.serialPort') }}</Label>
<Select v-model="ch9329Port">
<SelectTrigger>
<SelectValue :placeholder="t('setup.selectSerialPort')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="port in devices.serial" :key="port.path" :value="port.path">
<NativeSelect v-model="ch9329Port" class="w-full">
<NativeSelectOption value="">{{ t('setup.selectSerialPort') }}</NativeSelectOption>
<NativeSelectOption v-for="port in devices.serial" :key="port.path" :value="port.path">
{{ port.name }} ({{ port.path }})
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
<p v-if="!devices.serial.length" class="text-xs text-muted-foreground">
{{ t('setup.noSerialDevices') }}
</p>
@@ -890,7 +872,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<div class="space-y-2">
<Label for="ch9329Baudrate">{{ t('setup.baudRate') }}</Label>
<Select v-model="ch9329Baudrate">
<Select :model-value="ch9329Baudrate" @update:model-value="value => ch9329Baudrate = Number(value)">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
@@ -912,16 +894,12 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<div class="space-y-2">
<Label for="otgUdc">{{ t('setup.udc') }}</Label>
<Select v-model="otgUdc">
<SelectTrigger>
<SelectValue :placeholder="t('setup.selectUdc')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="udc in devices.udc" :key="udc.name" :value="udc.name">
<NativeSelect v-model="otgUdc" class="w-full">
<NativeSelectOption value="">{{ t('setup.selectUdc') }}</NativeSelectOption>
<NativeSelectOption v-for="udc in devices.udc" :key="udc.name" :value="udc.name">
{{ udc.name }}
</SelectItem>
</SelectContent>
</Select>
</NativeSelectOption>
</NativeSelect>
<p v-if="!devices.udc.length" class="text-xs text-muted-foreground">
{{ t('setup.noUdcDevices') }}
</p>
@@ -960,7 +938,10 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<!-- Error Message -->
<Transition name="fade">
<p v-if="error" class="text-sm text-destructive text-center">{{ error }}</p>
<Alert v-if="error" variant="destructive">
<AlertTriangle />
<AlertDescription>{{ error }}</AlertDescription>
</Alert>
</Transition>
<!-- Navigation Buttons -->