feat: 完善架构优化性能

- 调整音视频架构,提升 RKMPP 编码 MJPEG-->H264 性能,同时解决丢帧马赛克问题;
- 删除多用户逻辑,只保留单用户,支持设置 web 单会话;
- 修复删除体验不好的的回退逻辑,前端页面菜单位置微调;
- 增加 OTG USB 设备动态调整功能;
- 修复 mdns 问题,webrtc 视频切换更顺畅。
This commit is contained in:
mofeng
2026-01-25 16:04:29 +08:00
parent 01e01430da
commit 1786b7689d
66 changed files with 4225 additions and 2936 deletions

View File

@@ -10,7 +10,7 @@ import { useHidWebSocket } from '@/composables/useHidWebSocket'
import { useWebRTC } from '@/composables/useWebRTC'
import { useVideoSession } from '@/composables/useVideoSession'
import { getUnifiedAudio } from '@/composables/useUnifiedAudio'
import { streamApi, hidApi, atxApi, extensionsApi, atxConfigApi, userApi } from '@/api'
import { streamApi, hidApi, atxApi, extensionsApi, atxConfigApi, authApi } from '@/api'
import type { HidKeyboardEvent, HidMouseEvent } from '@/types/hid'
import { toast } from 'vue-sonner'
import { generateUUID } from '@/lib/utils'
@@ -641,7 +641,7 @@ function handleStreamConfigChanging(data: any) {
})
}
function handleStreamConfigApplied(data: any) {
async function handleStreamConfigApplied(data: any) {
// Reset consecutive error counter for new config
consecutiveErrors = 0
@@ -662,6 +662,10 @@ function handleStreamConfigApplied(data: any) {
if (videoMode.value !== 'mjpeg') {
// In WebRTC mode, reconnect WebRTC (session was closed due to config change)
const ready = await videoSession.waitForWebRTCReadyAny(3000)
if (!ready) {
console.warn('[WebRTC] Backend not ready after timeout (config change), attempting connection anyway')
}
switchToWebRTC(videoMode.value)
} else {
// In MJPEG mode, refresh the MJPEG stream
@@ -1259,16 +1263,7 @@ async function handleChangePassword() {
changingPassword.value = true
try {
// Get current user ID - we need to fetch user list first
const result = await userApi.list()
const currentUser = result.users.find(u => u.username === authStore.user)
if (!currentUser) {
toast.error(t('auth.userNotFound'))
return
}
await userApi.changePassword(currentUser.id, newPassword.value, currentPassword.value)
await authApi.changePassword(currentPassword.value, newPassword.value)
toast.success(t('auth.passwordChanged'))
// Reset form and close dialog

View File

@@ -1,11 +1,13 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSystemStore } from '@/stores/system'
import { useAuthStore } from '@/stores/auth'
import {
authApi,
authConfigApi,
configApi,
streamApi,
userApi,
videoConfigApi,
streamConfigApi,
hidConfigApi,
@@ -16,7 +18,7 @@ import {
webConfigApi,
systemApi,
type EncoderBackendInfo,
type User as UserType,
type AuthConfig,
type RustDeskConfigResponse,
type RustDeskStatusResponse,
type RustDeskPasswordResponse,
@@ -28,6 +30,8 @@ import type {
AtxDriverType,
ActiveLevel,
AtxDevices,
OtgHidProfile,
OtgHidFunctions,
} from '@/types/generated'
import { setLanguage } from '@/i18n'
import { useClipboard } from '@/composables/useClipboard'
@@ -57,16 +61,11 @@ import {
EyeOff,
Save,
Check,
Network,
HardDrive,
Power,
UserPlus,
User,
Pencil,
Trash2,
Menu,
Users,
Globe,
Lock,
User,
RefreshCw,
Terminal,
Play,
@@ -80,6 +79,7 @@ import {
const { t, locale } = useI18n()
const systemStore = useSystemStore()
const authStore = useAuthStore()
// Settings state
const activeSection = ref('appearance')
@@ -90,9 +90,11 @@ const saved = ref(false)
// Navigation structure
const navGroups = computed(() => [
{
title: t('settings.general'),
title: t('settings.system'),
items: [
{ id: 'appearance', label: t('settings.appearance'), icon: Sun },
{ id: 'account', label: t('settings.account'), icon: User },
{ id: 'access', label: t('settings.access'), icon: Lock },
]
},
{
@@ -100,7 +102,7 @@ const navGroups = computed(() => [
items: [
{ id: 'video', label: t('settings.video'), icon: Monitor, status: config.value.video_device ? t('settings.configured') : null },
{ id: 'hid', label: t('settings.hid'), icon: Keyboard, status: config.value.hid_backend.toUpperCase() },
{ id: 'msd', label: t('settings.msd'), icon: HardDrive },
...(config.value.msd_enabled ? [{ id: 'msd', label: t('settings.msd'), icon: HardDrive }] : []),
{ id: 'atx', label: t('settings.atx'), icon: Power },
]
},
@@ -108,16 +110,13 @@ const navGroups = computed(() => [
title: t('settings.extensions'),
items: [
{ id: 'ext-rustdesk', label: t('extensions.rustdesk.title'), icon: ScreenShare },
{ id: 'ext-remote-access', label: t('extensions.remoteAccess.title'), icon: ExternalLink },
{ id: 'ext-ttyd', label: t('extensions.ttyd.title'), icon: Terminal },
{ id: 'ext-gostc', label: t('extensions.gostc.title'), icon: Globe },
{ id: 'ext-easytier', label: t('extensions.easytier.title'), icon: Network },
]
},
{
title: t('settings.system'),
title: t('settings.other'),
items: [
{ id: 'web-server', label: t('settings.webServer'), icon: Globe },
{ id: 'users', label: t('settings.users'), icon: Users },
{ id: 'about', label: t('settings.about'), icon: Info },
]
}
@@ -131,22 +130,28 @@ function selectSection(id: string) {
// Theme
const theme = ref<'light' | 'dark' | 'system'>('system')
// Password change
const showPasswordDialog = ref(false)
// Account settings
const usernameInput = ref('')
const usernamePassword = ref('')
const usernameSaving = ref(false)
const usernameSaved = ref(false)
const usernameError = ref('')
const currentPassword = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const showPasswords = ref(false)
const passwordSaving = ref(false)
const passwordSaved = ref(false)
const passwordError = ref('')
const showPasswords = ref(false)
// User management
const users = ref<UserType[]>([])
const usersLoading = ref(false)
const showAddUserDialog = ref(false)
const showEditUserDialog = ref(false)
const editingUser = ref<UserType | null>(null)
const newUser = ref({ username: '', password: '', role: 'user' as 'admin' | 'user' })
const editUserData = ref({ username: '', role: 'user' as 'admin' | 'user' })
// Auth config state
const authConfig = ref<AuthConfig>({
session_timeout_secs: 3600 * 24,
single_user_allow_multiple_sessions: false,
totp_enabled: false,
totp_secret: undefined,
})
const authConfigLoading = ref(false)
// Extensions management
const extensions = ref<ExtensionsStatus | null>(null)
@@ -232,6 +237,13 @@ const config = ref({
hid_backend: 'ch9329',
hid_serial_device: '',
hid_serial_baudrate: 9600,
hid_otg_profile: 'full' as OtgHidProfile,
hid_otg_functions: {
keyboard: true,
mouse_relative: true,
mouse_absolute: true,
consumer: true,
} as OtgHidFunctions,
msd_enabled: false,
msd_dir: '',
network_port: 8080,
@@ -246,6 +258,13 @@ const config = ref({
// 跟踪服务器是否已配置 TURN 密码
const hasTurnPassword = ref(false)
const isHidFunctionSelectionValid = computed(() => {
if (config.value.hid_backend !== 'otg') return true
if (config.value.hid_otg_profile !== 'custom') return true
const f = config.value.hid_otg_functions
return !!(f.keyboard || f.mouse_relative || f.mouse_absolute || f.consumer)
})
// OTG Descriptor settings
const otgVendorIdHex = ref('1d6b')
const otgProductIdHex = ref('0104')
@@ -259,6 +278,12 @@ const validateHex = (event: Event, _field: string) => {
input.value = input.value.replace(/[^0-9a-fA-F]/g, '').toLowerCase()
}
watch(() => config.value.msd_enabled, (enabled) => {
if (!enabled && activeSection.value === 'msd') {
activeSection.value = 'hid'
}
})
// ATX config state
const atxConfig = ref({
enabled: false,
@@ -300,9 +325,6 @@ const selectedBackendFormats = computed(() => {
const isCh9329Backend = computed(() => config.value.hid_backend === 'ch9329')
// Video selection computed properties
import { watch } from 'vue'
const selectedDevice = computed(() => {
return devices.value.video.find(d => d.path === config.value.video_device)
})
@@ -384,6 +406,12 @@ watch(() => [config.value.video_width, config.value.video_height], () => {
}
})
watch(() => authStore.user, (value) => {
if (value) {
usernameInput.value = value
}
})
// Format bytes to human readable string
function formatBytes(bytes: number): string {
@@ -414,39 +442,71 @@ function handleLanguageChange(lang: string) {
}
}
// Password change
// Account updates
async function changeUsername() {
usernameError.value = ''
usernameSaved.value = false
if (usernameInput.value.length < 2) {
usernameError.value = t('auth.enterUsername')
return
}
if (!usernamePassword.value) {
usernameError.value = t('auth.enterPassword')
return
}
usernameSaving.value = true
try {
await authApi.changeUsername(usernameInput.value, usernamePassword.value)
usernameSaved.value = true
usernamePassword.value = ''
await authStore.checkAuth()
usernameInput.value = authStore.user || usernameInput.value
setTimeout(() => {
usernameSaved.value = false
}, 2000)
} catch (e) {
usernameError.value = t('auth.invalidPassword')
} finally {
usernameSaving.value = false
}
}
async function changePassword() {
passwordError.value = ''
passwordSaved.value = false
if (!currentPassword.value) {
passwordError.value = t('auth.enterPassword')
return
}
if (newPassword.value.length < 4) {
passwordError.value = t('setup.passwordHint')
return
}
if (newPassword.value !== confirmPassword.value) {
passwordError.value = t('setup.passwordMismatch')
return
}
passwordSaving.value = true
try {
await configApi.update({
current_password: currentPassword.value,
new_password: newPassword.value,
})
showPasswordDialog.value = false
await authApi.changePassword(currentPassword.value, newPassword.value)
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
passwordSaved.value = true
setTimeout(() => {
passwordSaved.value = false
}, 2000)
} catch (e) {
passwordError.value = t('auth.invalidPassword')
} finally {
passwordSaving.value = false
}
}
// MSD 开关变更处理
function onMsdEnabledChange(val: boolean) {
config.value.msd_enabled = val
}
// Save config - 使用域分离 API
async function saveConfig() {
loading.value = true
@@ -481,6 +541,20 @@ async function saveConfig() {
// HID 配置
if (activeSection.value === 'hid') {
if (!isHidFunctionSelectionValid.value) {
return
}
let desiredMsdEnabled = config.value.msd_enabled
if (config.value.hid_backend === 'otg') {
if (config.value.hid_otg_profile === 'full') {
desiredMsdEnabled = true
} else if (
config.value.hid_otg_profile === 'legacy_keyboard'
|| config.value.hid_otg_profile === 'legacy_mouse_relative'
) {
desiredMsdEnabled = false
}
}
const hidUpdate: any = {
backend: config.value.hid_backend as any,
ch9329_port: config.value.hid_serial_device || undefined,
@@ -495,15 +569,25 @@ async function saveConfig() {
product: otgProduct.value || 'One-KVM USB Device',
serial_number: otgSerialNumber.value || undefined,
}
hidUpdate.otg_profile = config.value.hid_otg_profile
hidUpdate.otg_functions = { ...config.value.hid_otg_functions }
}
savePromises.push(hidConfigApi.update(hidUpdate))
if (config.value.msd_enabled !== desiredMsdEnabled) {
config.value.msd_enabled = desiredMsdEnabled
}
savePromises.push(
msdConfigApi.update({
enabled: desiredMsdEnabled,
})
)
}
// MSD 配置
if (activeSection.value === 'msd') {
savePromises.push(
msdConfigApi.update({
enabled: config.value.msd_enabled,
msd_dir: config.value.msd_dir || undefined,
})
)
}
@@ -538,6 +622,13 @@ async function loadConfig() {
hid_backend: hid.backend || 'none',
hid_serial_device: hid.ch9329_port || '',
hid_serial_baudrate: hid.ch9329_baudrate || 9600,
hid_otg_profile: (hid.otg_profile || 'full') as OtgHidProfile,
hid_otg_functions: {
keyboard: hid.otg_functions?.keyboard ?? true,
mouse_relative: hid.otg_functions?.mouse_relative ?? true,
mouse_absolute: hid.otg_functions?.mouse_absolute ?? true,
consumer: hid.otg_functions?.consumer ?? true,
} as OtgHidFunctions,
msd_enabled: msd.enabled || false,
msd_dir: msd.msd_dir || '',
network_port: 8080, // 从旧 API 加载
@@ -591,56 +682,29 @@ async function loadBackends() {
}
}
// User management functions
async function loadUsers() {
usersLoading.value = true
// Auth config functions
async function loadAuthConfig() {
authConfigLoading.value = true
try {
const result = await userApi.list()
users.value = result.users || []
authConfig.value = await authConfigApi.get()
} catch (e) {
console.error('Failed to load users:', e)
console.error('Failed to load auth config:', e)
} finally {
usersLoading.value = false
authConfigLoading.value = false
}
}
async function createUser() {
if (!newUser.value.username || !newUser.value.password) return
async function saveAuthConfig() {
authConfigLoading.value = true
try {
await userApi.create(newUser.value.username, newUser.value.password, newUser.value.role)
showAddUserDialog.value = false
newUser.value = { username: '', password: '', role: 'user' }
await loadUsers()
await authConfigApi.update({
single_user_allow_multiple_sessions: authConfig.value.single_user_allow_multiple_sessions,
})
await loadAuthConfig()
} catch (e) {
console.error('Failed to create user:', e)
}
}
function openEditUserDialog(user: UserType) {
editingUser.value = user
editUserData.value = { username: user.username, role: user.role }
showEditUserDialog.value = true
}
async function updateUser() {
if (!editingUser.value) return
try {
await userApi.update(editingUser.value.id, editUserData.value)
showEditUserDialog.value = false
editingUser.value = null
await loadUsers()
} catch (e) {
console.error('Failed to update user:', e)
}
}
async function confirmDeleteUser(user: UserType) {
if (!confirm(`Delete user "${user.username}"?`)) return
try {
await userApi.delete(user.id)
await loadUsers()
} catch (e) {
console.error('Failed to delete user:', e)
console.error('Failed to save auth config:', e)
} finally {
authConfigLoading.value = false
}
}
@@ -1052,7 +1116,7 @@ onMounted(async () => {
loadConfig(),
loadDevices(),
loadBackends(),
loadUsers(),
loadAuthConfig(),
loadExtensions(),
loadAtxConfig(),
loadAtxDevices(),
@@ -1060,6 +1124,7 @@ onMounted(async () => {
loadRustdeskPassword(),
loadWebServerConfig(),
])
usernameInput.value = authStore.user || ''
})
</script>
@@ -1171,6 +1236,63 @@ onMounted(async () => {
</Card>
</div>
<!-- Account Section -->
<div v-show="activeSection === 'account'" class="space-y-6">
<Card>
<CardHeader>
<CardTitle>{{ t('settings.username') }}</CardTitle>
<CardDescription>{{ t('settings.usernameDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2">
<Label for="account-username">{{ t('settings.username') }}</Label>
<Input id="account-username" v-model="usernameInput" />
</div>
<div class="space-y-2">
<Label for="account-username-password">{{ t('settings.currentPassword') }}</Label>
<Input id="account-username-password" v-model="usernamePassword" type="password" />
</div>
<p v-if="usernameError" class="text-xs text-destructive">{{ usernameError }}</p>
<p v-else-if="usernameSaved" class="text-xs text-emerald-600">{{ t('common.success') }}</p>
<div class="flex justify-end">
<Button @click="changeUsername" :disabled="usernameSaving">
<Save class="h-4 w-4 mr-2" />
{{ t('common.save') }}
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>{{ t('settings.changePassword') }}</CardTitle>
<CardDescription>{{ t('settings.passwordDesc') }}</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div class="space-y-2">
<Label for="account-current-password">{{ t('settings.currentPassword') }}</Label>
<Input id="account-current-password" v-model="currentPassword" type="password" />
</div>
<div class="space-y-2">
<Label for="account-new-password">{{ t('settings.newPassword') }}</Label>
<Input id="account-new-password" v-model="newPassword" type="password" />
</div>
<div class="space-y-2">
<Label for="account-confirm-password">{{ t('auth.confirmPassword') }}</Label>
<Input id="account-confirm-password" v-model="confirmPassword" type="password" />
</div>
<p v-if="passwordError" class="text-xs text-destructive">{{ passwordError }}</p>
<p v-else-if="passwordSaved" class="text-xs text-emerald-600">{{ t('common.success') }}</p>
<div class="flex justify-end">
<Button @click="changePassword" :disabled="passwordSaving">
<Save class="h-4 w-4 mr-2" />
{{ t('common.save') }}
</Button>
</div>
</CardContent>
</Card>
</div>
<!-- Video Section -->
<div v-show="activeSection === 'video'" class="space-y-6">
<!-- Video Device Settings -->
@@ -1345,6 +1467,66 @@ onMounted(async () => {
<!-- OTG Descriptor Settings -->
<template v-if="config.hid_backend === 'otg'">
<Separator class="my-4" />
<div class="space-y-4">
<div>
<h4 class="text-sm font-medium">{{ t('settings.otgHidProfile') }}</h4>
<p class="text-sm text-muted-foreground">{{ t('settings.otgHidProfileDesc') }}</p>
</div>
<div class="space-y-2">
<Label for="otg-profile">{{ t('settings.profile') }}</Label>
<select id="otg-profile" v-model="config.hid_otg_profile" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
<option value="full">{{ t('settings.otgProfileFull') }}</option>
<option value="legacy_keyboard">{{ t('settings.otgProfileLegacyKeyboard') }}</option>
<option value="legacy_mouse_relative">{{ t('settings.otgProfileLegacyMouseRelative') }}</option>
<option value="custom">{{ t('settings.otgProfileCustom') }}</option>
</select>
</div>
<div v-if="config.hid_otg_profile === 'custom'" class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionKeyboard') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionKeyboardDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.keyboard" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMouseRelative') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMouseRelativeDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.mouse_relative" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMouseAbsolute') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMouseAbsoluteDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.mouse_absolute" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionConsumer') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionConsumerDesc') }}</p>
</div>
<Switch v-model="config.hid_otg_functions.consumer" />
</div>
<Separator />
<div class="flex items-center justify-between">
<div>
<Label>{{ t('settings.otgFunctionMsd') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.otgFunctionMsdDesc') }}</p>
</div>
<Switch v-model="config.msd_enabled" />
</div>
</div>
<p class="text-xs text-amber-600 dark:text-amber-400">
{{ t('settings.otgProfileWarning') }}
</p>
</div>
<Separator class="my-4" />
<div class="space-y-4">
<div>
@@ -1409,8 +1591,8 @@ onMounted(async () => {
</Card>
</div>
<!-- Web Server Section -->
<div v-show="activeSection === 'web-server'" class="space-y-6">
<!-- Access Section -->
<div v-show="activeSection === 'access'" class="space-y-6">
<Card>
<CardHeader>
<CardTitle>{{ t('settings.webServer') }}</CardTitle>
@@ -1452,51 +1634,36 @@ onMounted(async () => {
</div>
</CardContent>
</Card>
</div>
<!-- Users Section -->
<div v-show="activeSection === 'users'" class="space-y-6">
<Card>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-4">
<div class="space-y-1.5">
<CardTitle>{{ t('settings.userManagement') }}</CardTitle>
<CardDescription>{{ t('settings.userManagementDesc') }}</CardDescription>
</div>
<Button size="sm" @click="showAddUserDialog = true">
<UserPlus class="h-4 w-4 mr-2" />{{ t('settings.addUser') }}
</Button>
<CardHeader>
<CardTitle>{{ t('settings.authSettings') }}</CardTitle>
<CardDescription>{{ t('settings.authSettingsDesc') }}</CardDescription>
</CardHeader>
<CardContent>
<div v-if="usersLoading" class="text-center py-8">
<p class="text-sm text-muted-foreground">{{ t('settings.loadingUsers') }}</p>
</div>
<div v-else-if="users.length === 0" class="text-center py-8">
<User class="h-8 w-8 mx-auto mb-2 text-muted-foreground" />
<p class="text-sm text-muted-foreground">{{ t('settings.noUsers') }}</p>
</div>
<div v-else class="divide-y">
<div v-for="user in users" :key="user.id" class="flex items-center justify-between py-3">
<div class="flex items-center gap-3">
<div class="h-8 w-8 rounded-full bg-muted flex items-center justify-center">
<User class="h-4 w-4" />
</div>
<div>
<p class="text-sm font-medium">{{ user.username }}</p>
<Badge variant="outline" class="text-xs">{{ user.role === 'admin' ? t('settings.roleAdmin') : t('settings.roleUser') }}</Badge>
</div>
</div>
<div class="flex gap-1">
<Button size="icon" variant="ghost" class="h-8 w-8" @click="openEditUserDialog(user)"><Pencil class="h-4 w-4" /></Button>
<Button size="icon" variant="ghost" class="h-8 w-8 text-destructive" :disabled="user.role === 'admin' && users.filter(u => u.role === 'admin').length === 1" @click="confirmDeleteUser(user)"><Trash2 class="h-4 w-4" /></Button>
</div>
<CardContent class="space-y-4">
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<Label>{{ t('settings.allowMultipleSessions') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.allowMultipleSessionsDesc') }}</p>
</div>
<Switch
v-model="authConfig.single_user_allow_multiple_sessions"
:disabled="authConfigLoading"
/>
</div>
<Separator />
<p class="text-xs text-muted-foreground">{{ t('settings.singleUserSessionNote') }}</p>
<div class="flex justify-end pt-2">
<Button @click="saveAuthConfig" :disabled="authConfigLoading">
<Save class="h-4 w-4 mr-2" />
{{ t('common.save') }}
</Button>
</div>
</CardContent>
</Card>
</div>
<!-- MSD Section -->
<div v-show="activeSection === 'msd'" class="space-y-6">
<div v-show="activeSection === 'msd' && config.msd_enabled" class="space-y-6">
<Card>
<CardHeader>
<CardTitle>{{ t('settings.msdSettings') }}</CardTitle>
@@ -1507,19 +1674,6 @@ onMounted(async () => {
<p class="font-medium">{{ t('settings.msdCh9329Warning') }}</p>
<p class="text-xs text-amber-900/80">{{ t('settings.msdCh9329WarningDesc') }}</p>
</div>
<div class="flex items-center justify-between">
<div class="space-y-0.5">
<Label for="msd-enabled">{{ t('settings.msdEnable') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.msdEnableDesc') }}</p>
</div>
<Switch
id="msd-enabled"
:disabled="isCh9329Backend"
:model-value="config.msd_enabled"
@update:model-value="onMsdEnabledChange"
/>
</div>
<Separator />
<div class="space-y-4">
<div class="space-y-2">
<Label for="msd-dir">{{ t('settings.msdDir') }}</Label>
@@ -1821,8 +1975,8 @@ onMounted(async () => {
</div>
</div>
<!-- gostc Section -->
<div v-show="activeSection === 'ext-gostc'" class="space-y-6">
<!-- Remote Access Section -->
<div v-show="activeSection === 'ext-remote-access'" class="space-y-6">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
@@ -1913,10 +2067,7 @@ onMounted(async () => {
<Check v-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ saved ? t('common.success') : t('common.save') }}
</Button>
</div>
</div>
<!-- easytier Section -->
<div v-show="activeSection === 'ext-easytier'" class="space-y-6">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
@@ -2249,9 +2400,14 @@ onMounted(async () => {
<!-- Save Button (sticky) -->
<div v-if="['video', 'hid', 'msd'].includes(activeSection)" class="sticky bottom-0 pt-4 pb-2 bg-background border-t -mx-6 px-6 lg:-mx-8 lg:px-8">
<div class="flex justify-end">
<Button :disabled="loading" @click="saveConfig">
<div class="flex items-center gap-3">
<p v-if="activeSection === 'hid' && !isHidFunctionSelectionValid" class="text-xs text-amber-600 dark:text-amber-400">
{{ t('settings.otgFunctionMinWarning') }}
</p>
<Button :disabled="loading || (activeSection === 'hid' && !isHidFunctionSelectionValid)" @click="saveConfig">
<Check v-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ saved ? t('common.success') : t('common.save') }}
</Button>
</Button>
</div>
</div>
</div>
@@ -2259,95 +2415,6 @@ onMounted(async () => {
</main>
</div>
<!-- Password Change Dialog -->
<Dialog v-model:open="showPasswordDialog">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ t('settings.changePassword') }}</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div class="space-y-2">
<Label for="current-password">{{ t('settings.currentPassword') }}</Label>
<div class="relative">
<Input id="current-password" v-model="currentPassword" :type="showPasswords ? 'text' : 'password'" />
<button type="button" class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground" @click="showPasswords = !showPasswords">
<Eye v-if="!showPasswords" class="h-4 w-4" /><EyeOff v-else class="h-4 w-4" />
</button>
</div>
</div>
<div class="space-y-2">
<Label for="new-password">{{ t('settings.newPassword') }}</Label>
<Input id="new-password" v-model="newPassword" :type="showPasswords ? 'text' : 'password'" />
</div>
<div class="space-y-2">
<Label for="confirm-password">{{ t('setup.confirmPassword') }}</Label>
<Input id="confirm-password" v-model="confirmPassword" :type="showPasswords ? 'text' : 'password'" />
</div>
<p v-if="passwordError" class="text-sm text-destructive">{{ passwordError }}</p>
</div>
<DialogFooter>
<Button variant="outline" size="sm" @click="showPasswordDialog = false">{{ t('common.cancel') }}</Button>
<Button size="sm" @click="changePassword">{{ t('common.save') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Add User Dialog -->
<Dialog v-model:open="showAddUserDialog">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ t('settings.addUser') }}</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div class="space-y-2">
<Label for="new-username">{{ t('settings.username') }}</Label>
<Input id="new-username" v-model="newUser.username" />
</div>
<div class="space-y-2">
<Label for="new-user-password">{{ t('settings.password') }}</Label>
<Input id="new-user-password" v-model="newUser.password" type="password" />
</div>
<div class="space-y-2">
<Label for="new-user-role">{{ t('settings.role') }}</Label>
<select id="new-user-role" v-model="newUser.role" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
<option value="user">{{ t('settings.roleUser') }}</option>
<option value="admin">{{ t('settings.roleAdmin') }}</option>
</select>
</div>
</div>
<DialogFooter>
<Button variant="outline" size="sm" @click="showAddUserDialog = false">{{ t('common.cancel') }}</Button>
<Button size="sm" @click="createUser">{{ t('settings.create') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Edit User Dialog -->
<Dialog v-model:open="showEditUserDialog">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{ t('settings.editUser') }}</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div class="space-y-2">
<Label for="edit-username">{{ t('settings.username') }}</Label>
<Input id="edit-username" v-model="editUserData.username" />
</div>
<div class="space-y-2">
<Label for="edit-user-role">{{ t('settings.role') }}</Label>
<select id="edit-user-role" v-model="editUserData.role" class="w-full h-9 px-3 rounded-md border border-input bg-background text-sm">
<option value="user">{{ t('settings.roleUser') }}</option>
<option value="admin">{{ t('settings.roleAdmin') }}</option>
</select>
</div>
</div>
<DialogFooter>
<Button variant="outline" size="sm" @click="showEditUserDialog = false">{{ t('common.cancel') }}</Button>
<Button size="sm" @click="updateUser">{{ t('common.save') }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Terminal Dialog -->
<Dialog v-model:open="showTerminalDialog">
<DialogContent class="max-w-[95vw] w-[1200px] h-[600px] p-0 flex flex-col overflow-hidden">

View File

@@ -99,9 +99,7 @@ const otgUdc = ref('')
// Extension settings
const ttydEnabled = ref(false)
const rustdeskEnabled = ref(false)
const ttydAvailable = ref(false)
const rustdeskAvailable = ref(true) // RustDesk is built-in, always available
// Encoder backend settings
const encoderBackend = ref('auto')
@@ -139,7 +137,6 @@ interface DeviceInfo {
udc: Array<{ name: string }>
extensions: {
ttyd_available: boolean
rustdesk_available: boolean
}
}
@@ -150,7 +147,6 @@ const devices = ref<DeviceInfo>({
udc: [],
extensions: {
ttyd_available: false,
rustdesk_available: true,
},
})
@@ -351,7 +347,6 @@ onMounted(async () => {
// Set extension availability from devices API
if (result.extensions) {
ttydAvailable.value = result.extensions.ttyd_available
rustdeskAvailable.value = result.extensions.rustdesk_available
}
} catch {
// Use defaults
@@ -506,7 +501,6 @@ async function handleSetup() {
// Extension settings
setupData.ttyd_enabled = ttydEnabled.value
setupData.rustdesk_enabled = rustdeskEnabled.value
const success = await authStore.setup(setupData)
@@ -956,19 +950,6 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<Switch v-model="ttydEnabled" :disabled="!ttydAvailable" />
</div>
<!-- RustDesk -->
<div class="flex items-center justify-between p-4 rounded-lg border">
<div class="space-y-1">
<div class="flex items-center gap-2">
<Label class="text-base font-medium">{{ t('setup.rustdeskTitle') }}</Label>
</div>
<p class="text-sm text-muted-foreground">
{{ t('setup.rustdeskDescription') }}
</p>
</div>
<Switch v-model="rustdeskEnabled" />
</div>
<p class="text-xs text-muted-foreground text-center pt-2">
{{ t('setup.extensionsHint') }}
</p>