mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-03-15 07:26:44 +08:00
refactor: 收敛单用户模型并优化可访问性与响应式体验
- 后端移除 is_admin 权限字段与相关逻辑,统一为单用户系统模型 - 修复会话过期清理的时间比较方式(改为 RFC3339 参数比较) - /api/config 聚合配置增加敏感字段脱敏,避免暴露 TURN/RustDesk 密钥与密码 - 配置更新日志改为摘要,避免打印完整配置内容 - 前端修复可点击卡片语义与键盘可达,补齐图标按钮可访问名称 - 调整弹窗与抽屉的响应式尺寸,优化多端显示与交互
This commit is contained in:
@@ -110,7 +110,9 @@ impl SessionStore {
|
||||
|
||||
/// Delete all expired sessions
|
||||
pub async fn cleanup_expired(&self) -> Result<u64> {
|
||||
let result = sqlx::query("DELETE FROM sessions WHERE expires_at < datetime('now')")
|
||||
let now = Utc::now().to_rfc3339();
|
||||
let result = sqlx::query("DELETE FROM sessions WHERE expires_at < ?1")
|
||||
.bind(now)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::password::{hash_password, verify_password};
|
||||
use crate::error::{AppError, Result};
|
||||
|
||||
/// User row type from database
|
||||
type UserRow = (String, String, String, i32, String, String);
|
||||
type UserRow = (String, String, String, String, String);
|
||||
|
||||
/// User data
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -16,7 +16,6 @@ pub struct User {
|
||||
pub username: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub password_hash: String,
|
||||
pub is_admin: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -24,12 +23,11 @@ pub struct User {
|
||||
impl User {
|
||||
/// Convert from database row to User
|
||||
fn from_row(row: UserRow) -> Self {
|
||||
let (id, username, password_hash, is_admin, created_at, updated_at) = row;
|
||||
let (id, username, password_hash, created_at, updated_at) = row;
|
||||
Self {
|
||||
id,
|
||||
username,
|
||||
password_hash,
|
||||
is_admin: is_admin != 0,
|
||||
created_at: DateTime::parse_from_rfc3339(&created_at)
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(|_| Utc::now()),
|
||||
@@ -53,7 +51,7 @@ impl UserStore {
|
||||
}
|
||||
|
||||
/// Create a new user
|
||||
pub async fn create(&self, username: &str, password: &str, is_admin: bool) -> Result<User> {
|
||||
pub async fn create(&self, username: &str, password: &str) -> Result<User> {
|
||||
// Check if username already exists
|
||||
if self.get_by_username(username).await?.is_some() {
|
||||
return Err(AppError::BadRequest(format!(
|
||||
@@ -68,21 +66,19 @@ impl UserStore {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
username: username.to_string(),
|
||||
password_hash,
|
||||
is_admin,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO users (id, username, password_hash, is_admin, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
INSERT INTO users (id, username, password_hash, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
"#,
|
||||
)
|
||||
.bind(&user.id)
|
||||
.bind(&user.username)
|
||||
.bind(&user.password_hash)
|
||||
.bind(user.is_admin as i32)
|
||||
.bind(user.created_at.to_rfc3339())
|
||||
.bind(user.updated_at.to_rfc3339())
|
||||
.execute(&self.pool)
|
||||
@@ -94,7 +90,7 @@ impl UserStore {
|
||||
/// Get user by ID
|
||||
pub async fn get(&self, user_id: &str) -> Result<Option<User>> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users WHERE id = ?1",
|
||||
"SELECT id, username, password_hash, created_at, updated_at FROM users WHERE id = ?1",
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -106,7 +102,7 @@ impl UserStore {
|
||||
/// Get user by username
|
||||
pub async fn get_by_username(&self, username: &str) -> Result<Option<User>> {
|
||||
let row: Option<UserRow> = sqlx::query_as(
|
||||
"SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users WHERE username = ?1",
|
||||
"SELECT id, username, password_hash, created_at, updated_at FROM users WHERE username = ?1",
|
||||
)
|
||||
.bind(username)
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -178,7 +174,7 @@ impl UserStore {
|
||||
/// List all users
|
||||
pub async fn list(&self) -> Result<Vec<User>> {
|
||||
let rows: Vec<UserRow> = sqlx::query_as(
|
||||
"SELECT id, username, password_hash, is_admin, created_at, updated_at FROM users ORDER BY created_at",
|
||||
"SELECT id, username, password_hash, created_at, updated_at FROM users ORDER BY created_at",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
@@ -82,7 +82,6 @@ impl ConfigStore {
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
|
||||
@@ -50,10 +50,26 @@ use std::sync::Arc;
|
||||
use crate::config::AppConfig;
|
||||
use crate::state::AppState;
|
||||
|
||||
fn sanitize_config_for_api(config: &mut AppConfig) {
|
||||
// Auth secrets
|
||||
config.auth.totp_secret = None;
|
||||
|
||||
// Stream secrets
|
||||
config.stream.turn_password = None;
|
||||
|
||||
// RustDesk secrets
|
||||
config.rustdesk.device_password.clear();
|
||||
config.rustdesk.relay_key = None;
|
||||
config.rustdesk.public_key = None;
|
||||
config.rustdesk.private_key = None;
|
||||
config.rustdesk.signing_public_key = None;
|
||||
config.rustdesk.signing_private_key = None;
|
||||
}
|
||||
|
||||
/// 获取完整配置
|
||||
pub async fn get_all_config(State(state): State<Arc<AppState>>) -> Json<AppConfig> {
|
||||
let mut config = (*state.config.get()).clone();
|
||||
// 不暴露敏感信息
|
||||
config.auth.totp_secret = None;
|
||||
sanitize_config_for_api(&mut config);
|
||||
Json(config)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ pub async fn regenerate_device_password(
|
||||
Ok(Json(RustDeskConfigResponse::from(&new_config)))
|
||||
}
|
||||
|
||||
/// 获取设备密码(管理员专用)
|
||||
/// 获取设备密码(已认证用户)
|
||||
pub async fn get_device_password(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||
let config = state.config.get().rustdesk.clone();
|
||||
Json(serde_json::json!({
|
||||
|
||||
@@ -589,11 +589,8 @@ pub async fn setup_init(
|
||||
));
|
||||
}
|
||||
|
||||
// Create admin user
|
||||
state
|
||||
.users
|
||||
.create(&req.username, &req.password, true)
|
||||
.await?;
|
||||
// Create single system user
|
||||
state.users.create(&req.username, &req.password).await?;
|
||||
|
||||
// Update config
|
||||
state
|
||||
@@ -771,10 +768,7 @@ pub async fn setup_init(
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"System initialized successfully with admin user: {}",
|
||||
req.username
|
||||
);
|
||||
tracing::info!("System initialized successfully");
|
||||
|
||||
Ok(Json(LoginResponse {
|
||||
success: true,
|
||||
@@ -799,7 +793,7 @@ pub async fn update_config(
|
||||
// Keep old config for rollback
|
||||
let old_config = state.config.get();
|
||||
|
||||
tracing::info!("Received config update: {:?}", req.updates);
|
||||
tracing::info!("Received config update request");
|
||||
|
||||
// Validate and merge config first (outside the update closure)
|
||||
let config_json = serde_json::to_value(&old_config)
|
||||
@@ -808,8 +802,6 @@ pub async fn update_config(
|
||||
let merged = merge_json(config_json, req.updates.clone())
|
||||
.map_err(|_| AppError::Internal("Failed to merge config".to_string()))?;
|
||||
|
||||
tracing::debug!("Merged config: {:?}", merged);
|
||||
|
||||
let new_config: AppConfig = serde_json::from_value(merged)
|
||||
.map_err(|e| AppError::BadRequest(format!("Invalid config format: {}", e)))?;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user