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

@@ -52,7 +52,7 @@ async function handleLogout() {
</script>
<template>
<div class="h-screen flex flex-col bg-background overflow-hidden">
<div class="h-screen h-dvh flex flex-col bg-background overflow-hidden">
<!-- Header -->
<header class="shrink-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div class="flex h-14 items-center px-4 max-w-full">
@@ -86,14 +86,14 @@ async function handleLogout() {
</span>
<!-- Theme Toggle -->
<Button variant="ghost" size="icon" @click="toggleTheme">
<Button variant="ghost" size="icon" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
<Sun class="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon class="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span class="sr-only">{{ t('common.toggleTheme') }}</span>
</Button>
<!-- Language Toggle -->
<Button variant="ghost" size="icon" @click="toggleLanguage">
<Button variant="ghost" size="icon" :aria-label="t('common.toggleLanguage')" @click="toggleLanguage">
<Languages class="h-4 w-4" />
<span class="sr-only">{{ t('common.toggleLanguage') }}</span>
</Button>
@@ -101,7 +101,7 @@ async function handleLogout() {
<!-- Mobile Menu -->
<DropdownMenu>
<DropdownMenuTrigger as-child class="md:hidden">
<Button variant="ghost" size="icon">
<Button variant="ghost" size="icon" :aria-label="t('common.menu')">
<Menu class="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
@@ -119,7 +119,7 @@ async function handleLogout() {
</DropdownMenu>
<!-- Logout Button (Desktop) -->
<Button variant="ghost" size="icon" class="hidden md:flex" @click="handleLogout">
<Button variant="ghost" size="icon" class="hidden md:flex" :aria-label="t('nav.logout')" @click="handleLogout">
<LogOut class="h-4 w-4" />
<span class="sr-only">{{ t('nav.logout') }}</span>
</Button>

View File

@@ -442,7 +442,7 @@ onUnmounted(() => {
<Sheet :open="props.open" @update:open="emit('update:open', $event)">
<SheetContent
side="right"
class="w-[400px] sm:w-[440px] p-0 border-l border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950"
class="w-[90vw] max-w-[440px] p-0 border-l border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950"
>
<!-- Header -->
<SheetHeader class="px-6 py-3 border-b border-slate-200 dark:border-slate-800">
@@ -454,7 +454,7 @@ onUnmounted(() => {
</div>
</SheetHeader>
<ScrollArea class="h-[calc(100vh-60px)]">
<ScrollArea class="h-[calc(100dvh-60px)]">
<div class="px-6 py-4 space-y-6">
<!-- Video Section Header -->
<div>

View File

@@ -129,9 +129,11 @@ const statusBadgeText = computed(() => {
<HoverCard v-if="!prefersPopover" :open-delay="200" :close-delay="100">
<HoverCardTrigger as-child>
<!-- New layout: vertical with title on top, status+quickInfo on bottom -->
<div
<button
type="button"
:aria-label="`${title}: ${quickInfo || subtitle || statusText}`"
:class="cn(
'flex flex-col gap-0.5 rounded-md border cursor-pointer transition-colors',
'flex flex-col gap-0.5 rounded-md border cursor-pointer transition-colors text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
compact ? 'px-2 py-1 text-xs min-w-[80px]' : 'px-3 py-1.5 text-sm min-w-[100px]',
'bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700',
'border-slate-200 dark:border-slate-700',
@@ -147,7 +149,7 @@ const statusBadgeText = computed(() => {
{{ quickInfo || subtitle || statusText }}
</span>
</div>
</div>
</button>
</HoverCardTrigger>
<HoverCardContent class="w-80" :align="hoverAlign">
@@ -228,9 +230,11 @@ const statusBadgeText = computed(() => {
<Popover v-else>
<PopoverTrigger as-child>
<!-- New layout: vertical with title on top, status+quickInfo on bottom -->
<div
<button
type="button"
:aria-label="`${title}: ${quickInfo || subtitle || statusText}`"
:class="cn(
'flex flex-col gap-0.5 rounded-md border cursor-pointer transition-colors',
'flex flex-col gap-0.5 rounded-md border cursor-pointer transition-colors text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
compact ? 'px-2 py-1 text-xs min-w-[80px]' : 'px-3 py-1.5 text-sm min-w-[100px]',
'bg-white dark:bg-slate-800 hover:bg-slate-50 dark:hover:bg-slate-700',
'border-slate-200 dark:border-slate-700',
@@ -246,7 +250,7 @@ const statusBadgeText = computed(() => {
{{ quickInfo || subtitle || statusText }}
</span>
</div>
</div>
</button>
</PopoverTrigger>
<PopoverContent class="w-80" :align="hoverAlign">

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">