refactor: 收敛单用户模型并优化可访问性与响应式体验

- 后端移除 is_admin 权限字段与相关逻辑,统一为单用户系统模型
- 修复会话过期清理的时间比较方式(改为 RFC3339 参数比较)
- /api/config 聚合配置增加敏感字段脱敏,避免暴露 TURN/RustDesk 密钥与密码
- 配置更新日志改为摘要,避免打印完整配置内容
- 前端修复可点击卡片语义与键盘可达,补齐图标按钮可访问名称
- 调整弹窗与抽屉的响应式尺寸,优化多端显示与交互
This commit is contained in:
mofeng-git
2026-02-10 22:30:52 +08:00
parent 394baca938
commit 261deb1303
13 changed files with 81 additions and 60 deletions

View File

@@ -1897,7 +1897,7 @@ onUnmounted(() => {
</script>
<template>
<div class="h-screen flex flex-col bg-background">
<div class="h-screen h-dvh flex flex-col bg-background">
<!-- Header -->
<header class="shrink-0 border-b border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">
<div class="px-4">
@@ -1960,13 +1960,13 @@ onUnmounted(() => {
<div class="h-6 w-px bg-slate-200 dark:bg-slate-700 hidden md:block mx-1" />
<!-- Theme Toggle -->
<Button variant="ghost" size="icon" class="h-8 w-8 hidden md:flex" @click="toggleTheme">
<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" />
</Button>
<!-- Language Toggle -->
<Button variant="ghost" size="icon" class="h-8 w-8 hidden md:flex" @click="toggleLanguage">
<Button variant="ghost" size="icon" class="h-8 w-8 hidden md:flex" :aria-label="t('common.toggleLanguage')" @click="toggleLanguage">
<Languages class="h-4 w-4" />
</Button>
@@ -2221,7 +2221,7 @@ onUnmounted(() => {
<!-- Terminal Dialog -->
<Dialog v-model:open="showTerminalDialog">
<DialogContent class="max-w-[95vw] w-[1200px] h-[600px] p-0 flex flex-col overflow-hidden">
<DialogContent class="w-[95vw] max-w-5xl h-[85dvh] max-h-[720px] p-0 flex flex-col overflow-hidden">
<DialogHeader class="px-4 py-3 border-b shrink-0">
<DialogTitle class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
@@ -2233,6 +2233,7 @@ onUnmounted(() => {
size="icon"
class="h-8 w-8 mr-8"
@click="openTerminalInNewTab"
:aria-label="t('extensions.ttyd.openInNewTab')"
:title="t('extensions.ttyd.openInNewTab')"
>
<ExternalLink class="h-4 w-4" />

View File

@@ -49,7 +49,7 @@ function handleKeydown(e: KeyboardEvent) {
</script>
<template>
<div class="min-h-screen flex items-center justify-center bg-background p-4">
<div class="min-h-screen min-h-dvh flex items-center justify-center bg-background p-4">
<div class="w-full max-w-sm space-y-6">
<!-- Logo and Title -->
<div class="text-center space-y-2">
@@ -91,6 +91,7 @@ function handleKeydown(e: KeyboardEvent) {
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
:aria-label="showPassword ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" class="w-4 h-4" />

View File

@@ -1260,6 +1260,7 @@ onMounted(async () => {
<SheetTrigger as-child>
<Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
<Menu class="h-4 w-4" />
<span class="sr-only">{{ t('common.menu') }}</span>
</Button>
</SheetTrigger>
<SheetContent side="left" class="w-72 p-0">
@@ -1269,6 +1270,7 @@ onMounted(async () => {
<div v-for="group in navGroups" :key="group.title" class="space-y-1">
<h3 class="px-3 text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">{{ group.title }}</h3>
<button
type="button"
v-for="item in group.items"
:key="item.id"
@click="selectSection(item.id)"
@@ -1299,6 +1301,7 @@ onMounted(async () => {
<div v-for="group in navGroups" :key="group.title" class="space-y-1">
<h3 class="px-3 text-xs font-medium text-muted-foreground uppercase tracking-wider mb-2">{{ group.title }}</h3>
<button
type="button"
v-for="item in group.items"
:key="item.id"
@click="activeSection = item.id"
@@ -1424,7 +1427,7 @@ onMounted(async () => {
<CardTitle>{{ t('settings.videoSettings') }}</CardTitle>
<CardDescription>{{ t('settings.videoSettingsDesc') }}</CardDescription>
</div>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="loadDevices">
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadDevices">
<RefreshCw class="h-4 w-4" />
</Button>
</CardHeader>
@@ -1533,6 +1536,7 @@ onMounted(async () => {
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground"
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords"
>
<Eye v-if="!showPasswords" class="h-4 w-4" />
@@ -1557,7 +1561,7 @@ onMounted(async () => {
<CardTitle>{{ t('settings.hidSettings') }}</CardTitle>
<CardDescription>{{ t('settings.hidSettingsDesc') }}</CardDescription>
</div>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="loadDevices">
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadDevices">
<RefreshCw class="h-4 w-4" />
</Button>
</CardHeader>
@@ -1779,7 +1783,7 @@ onMounted(async () => {
<div class="space-y-2">
<div v-for="(_, i) in bindAddressList" :key="`bind-${i}`" class="flex gap-2">
<Input v-model="bindAddressList[i]" placeholder="192.168.1.10" />
<Button variant="ghost" size="icon" @click="removeBindAddress(i)">
<Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeBindAddress(i)">
<Trash2 class="h-4 w-4" />
</Button>
</div>
@@ -1873,7 +1877,7 @@ onMounted(async () => {
<CardTitle>{{ t('settings.atxSettings') }}</CardTitle>
<CardDescription>{{ t('settings.atxSettingsDesc') }}</CardDescription>
</div>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="loadAtxDevices">
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadAtxDevices">
<RefreshCw class="h-4 w-4" />
</Button>
</CardHeader>
@@ -2118,7 +2122,7 @@ onMounted(async () => {
</div>
<!-- Logs -->
<div class="space-y-2">
<button @click="showLogs.ttyd = !showLogs.ttyd; if (showLogs.ttyd) refreshExtensionLogs('ttyd')" class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
<button type="button" @click="showLogs.ttyd = !showLogs.ttyd; if (showLogs.ttyd) refreshExtensionLogs('ttyd')" class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
<ChevronRight :class="['h-4 w-4 transition-transform', showLogs.ttyd ? 'rotate-90' : '']" />
{{ t('extensions.viewLogs') }}
</button>
@@ -2212,7 +2216,7 @@ onMounted(async () => {
</div>
<!-- Logs -->
<div class="space-y-2">
<button @click="showLogs.gostc = !showLogs.gostc; if (showLogs.gostc) refreshExtensionLogs('gostc')" class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
<button type="button" @click="showLogs.gostc = !showLogs.gostc; if (showLogs.gostc) refreshExtensionLogs('gostc')" class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
<ChevronRight :class="['h-4 w-4 transition-transform', showLogs.gostc ? 'rotate-90' : '']" />
{{ t('extensions.viewLogs') }}
</button>
@@ -2299,7 +2303,7 @@ onMounted(async () => {
<div class="sm:col-span-3 space-y-2">
<div v-for="(_, i) in extConfig.easytier.peer_urls" :key="i" class="flex gap-2">
<Input v-model="extConfig.easytier.peer_urls[i]" placeholder="tcp://1.2.3.4:11010" :disabled="isExtRunning(extensions?.easytier?.status)" />
<Button variant="ghost" size="icon" @click="removeEasytierPeer(i)" :disabled="isExtRunning(extensions?.easytier?.status)">
<Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeEasytierPeer(i)" :disabled="isExtRunning(extensions?.easytier?.status)">
<Trash2 class="h-4 w-4" />
</Button>
</div>
@@ -2319,7 +2323,7 @@ onMounted(async () => {
</div>
<!-- Logs -->
<div class="space-y-2">
<button @click="showLogs.easytier = !showLogs.easytier; if (showLogs.easytier) refreshExtensionLogs('easytier')" class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
<button type="button" @click="showLogs.easytier = !showLogs.easytier; if (showLogs.easytier) refreshExtensionLogs('easytier')" class="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
<ChevronRight :class="['h-4 w-4 transition-transform', showLogs.easytier ? 'rotate-90' : '']" />
{{ t('extensions.viewLogs') }}
</button>
@@ -2355,7 +2359,7 @@ onMounted(async () => {
<Badge :variant="rustdeskStatus?.service_status === 'running' ? 'default' : 'secondary'">
{{ getRustdeskServiceStatusText(rustdeskStatus?.service_status) }}
</Badge>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="loadRustdeskConfig" :disabled="rustdeskLoading">
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadRustdeskConfig" :disabled="rustdeskLoading">
<RefreshCw :class="['h-4 w-4', rustdeskLoading ? 'animate-spin' : '']" />
</Button>
</div>
@@ -2450,6 +2454,7 @@ onMounted(async () => {
variant="ghost"
size="icon"
class="h-8 w-8"
:aria-label="t('extensions.rustdesk.copyId')"
@click="copyToClipboard(rustdeskConfig?.device_id || '', 'id')"
:disabled="!rustdeskConfig?.device_id"
>
@@ -2472,6 +2477,7 @@ onMounted(async () => {
variant="ghost"
size="icon"
class="h-8 w-8"
:aria-label="t('extensions.rustdesk.copyPassword')"
@click="copyToClipboard(rustdeskPassword?.device_password || '', 'password')"
:disabled="!rustdeskPassword?.device_password"
>
@@ -2583,7 +2589,7 @@ onMounted(async () => {
<!-- Terminal Dialog -->
<Dialog v-model:open="showTerminalDialog">
<DialogContent class="max-w-[95vw] w-[1200px] h-[600px] p-0 flex flex-col overflow-hidden">
<DialogContent class="w-[95vw] max-w-5xl h-[85dvh] max-h-[720px] p-0 flex flex-col overflow-hidden">
<DialogHeader class="px-4 py-3 border-b shrink-0">
<DialogTitle class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
@@ -2595,6 +2601,7 @@ onMounted(async () => {
size="icon"
class="h-8 w-8 mr-8"
@click="openTerminalInNewTab"
:aria-label="t('extensions.ttyd.openInNewTab')"
:title="t('extensions.ttyd.openInNewTab')"
>
<ExternalLink class="h-4 w-4" />

View File

@@ -556,7 +556,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
</script>
<template>
<div class="min-h-screen flex items-start sm:items-center justify-center bg-background px-4 py-6 sm:py-10">
<div class="min-h-screen min-h-dvh flex items-start sm:items-center justify-center bg-background px-4 py-6 sm:py-10">
<Card class="w-full max-w-lg relative">
<!-- Language Switcher -->
<div class="absolute top-4 right-4">
@@ -677,6 +677,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
:aria-label="showPassword ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" class="w-4 h-4" />
@@ -727,7 +728,7 @@ 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">
<button type="button" class="text-muted-foreground hover:text-foreground transition-colors" :aria-label="t('common.info')">
<HelpCircle class="w-4 h-4" />
</button>
</HoverCardTrigger>
@@ -753,7 +754,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<Label for="videoFormat">{{ t('setup.videoFormat') }}</Label>
<HoverCard>
<HoverCardTrigger as-child>
<button type="button" class="text-muted-foreground hover:text-foreground transition-colors">
<button type="button" class="text-muted-foreground hover:text-foreground transition-colors" :aria-label="t('common.info')">
<HelpCircle class="w-4 h-4" />
</button>
</HoverCardTrigger>
@@ -818,7 +819,7 @@ 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">
<button type="button" class="text-muted-foreground hover:text-foreground transition-colors" :aria-label="t('common.info')">
<HelpCircle class="w-4 h-4" />
</button>
</HoverCardTrigger>
@@ -849,6 +850,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<button
type="button"
class="w-full flex items-center justify-between p-3 text-left hover:bg-muted/50 rounded-lg transition-colors"
:aria-label="t('setup.advancedEncoder')"
@click="showAdvancedEncoder = !showAdvancedEncoder"
>
<span class="text-sm font-medium">
@@ -965,6 +967,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
<button
type="button"
class="w-full flex items-center justify-between p-3 text-left hover:bg-muted/50 rounded-lg transition-colors"
:aria-label="t('setup.advancedOtg')"
@click="showAdvancedOtg = !showAdvancedOtg"
>
<span class="text-sm font-medium">