mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-02-01 02:21:53 +08:00
feat: 完善架构优化性能
- 调整音视频架构,提升 RKMPP 编码 MJPEG-->H264 性能,同时解决丢帧马赛克问题; - 删除多用户逻辑,只保留单用户,支持设置 web 单会话; - 修复删除体验不好的的回退逻辑,前端页面菜单位置微调; - 增加 OTG USB 设备动态调整功能; - 修复 mdns 问题,webrtc 视频切换更顺畅。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user