feat(nav): 更新导航菜单结构并添加客户端管理功能
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 39s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m25s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m31s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m24s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m52s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m38s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m13s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m44s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 2m2s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m39s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m19s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 39s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m25s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m31s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m24s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m52s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m38s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m13s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m44s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 2m2s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m39s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m19s
- 将插件页面替换为客户端管理页面 - 添加客户端管理视图组件,支持查看客户端列表和状态 - 集成服务器更新检查功能,在页脚显示版本和更新状态 - 添加桌面、服务器、勾选和箭头图标用于界面展示 - 实现客户端统计卡片显示在线和离线状态 - 优化路由配置,移除插件相关路由并添加客户端路由 - 更新DTO结构,分离OS和Arch字段替代平台字段
This commit is contained in:
@@ -39,7 +39,8 @@ type VersionInfo struct {
|
|||||||
GitCommit string `json:"git_commit,omitempty"`
|
GitCommit string `json:"git_commit,omitempty"`
|
||||||
BuildTime string `json:"build_time,omitempty"`
|
BuildTime string `json:"build_time,omitempty"`
|
||||||
GoVersion string `json:"go_version,omitempty"`
|
GoVersion string `json:"go_version,omitempty"`
|
||||||
Platform string `json:"platform,omitempty"`
|
OS string `json:"os,omitempty"`
|
||||||
|
Arch string `json:"arch,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusResponse 服务器状态响应
|
// StatusResponse 服务器状态响应
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ func getVersionInfo() dto.VersionInfo {
|
|||||||
GitCommit: info.GitCommit,
|
GitCommit: info.GitCommit,
|
||||||
BuildTime: info.BuildTime,
|
BuildTime: info.BuildTime,
|
||||||
GoVersion: info.GoVersion,
|
GoVersion: info.GoVersion,
|
||||||
Platform: info.OS + "/" + info.Arch,
|
OS: info.OS,
|
||||||
|
Arch: info.Arch,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import {
|
|||||||
type GlobalThemeOverrides
|
type GlobalThemeOverrides
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
HomeOutline, ExtensionPuzzleOutline, SettingsOutline,
|
HomeOutline, DesktopOutline, SettingsOutline,
|
||||||
PersonCircleOutline, LogOutOutline, LogoGithub
|
PersonCircleOutline, LogOutOutline, LogoGithub, ServerOutline, CheckmarkCircleOutline, ArrowUpCircleOutline
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import { getServerStatus, getVersionInfo, removeToken, getToken } from './api'
|
import { getServerStatus, getVersionInfo, checkServerUpdate, removeToken, getToken, type UpdateInfo } from './api'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -17,20 +17,20 @@ const serverInfo = ref({ bind_addr: '', bind_port: 0 })
|
|||||||
const clientCount = ref(0)
|
const clientCount = ref(0)
|
||||||
const version = ref('')
|
const version = ref('')
|
||||||
const showUserMenu = ref(false)
|
const showUserMenu = ref(false)
|
||||||
|
const updateInfo = ref<UpdateInfo | null>(null)
|
||||||
|
|
||||||
const isLoginPage = computed(() => route.path === '/login')
|
const isLoginPage = computed(() => route.path === '/login')
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ key: 'home', label: '首页', icon: HomeOutline, path: '/' },
|
{ key: 'home', label: '首页', icon: HomeOutline, path: '/' },
|
||||||
{ key: 'plugins', label: '插件', icon: ExtensionPuzzleOutline, path: '/plugins' },
|
{ key: 'clients', label: '客户端', icon: DesktopOutline, path: '/clients' },
|
||||||
{ key: 'settings', label: '设置', icon: SettingsOutline, path: '/settings' }
|
{ key: 'settings', label: '设置', icon: SettingsOutline, path: '/settings' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const activeNav = computed(() => {
|
const activeNav = computed(() => {
|
||||||
const path = route.path
|
const path = route.path
|
||||||
if (path === '/' || path === '/home') return 'home'
|
if (path === '/' || path === '/home') return 'home'
|
||||||
if (path.startsWith('/client')) return 'home'
|
if (path === '/clients' || path.startsWith('/client/')) return 'clients'
|
||||||
if (path === '/plugins') return 'plugins'
|
|
||||||
if (path === '/settings') return 'settings'
|
if (path === '/settings') return 'settings'
|
||||||
return 'home'
|
return 'home'
|
||||||
})
|
})
|
||||||
@@ -56,16 +56,28 @@ const fetchVersion = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkUpdate = async () => {
|
||||||
|
if (isLoginPage.value || !getToken()) return
|
||||||
|
try {
|
||||||
|
const { data } = await checkServerUpdate()
|
||||||
|
updateInfo.value = data
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to check update', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => route.path, (newPath, oldPath) => {
|
watch(() => route.path, (newPath, oldPath) => {
|
||||||
if (oldPath === '/login' && newPath !== '/login') {
|
if (oldPath === '/login' && newPath !== '/login') {
|
||||||
fetchServerStatus()
|
fetchServerStatus()
|
||||||
fetchVersion()
|
fetchVersion()
|
||||||
|
checkUpdate()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchServerStatus()
|
fetchServerStatus()
|
||||||
fetchVersion()
|
fetchVersion()
|
||||||
|
checkUpdate()
|
||||||
})
|
})
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
@@ -139,7 +151,20 @@ const themeOverrides: GlobalThemeOverrides = {
|
|||||||
<footer class="app-footer">
|
<footer class="app-footer">
|
||||||
<div class="footer-left">
|
<div class="footer-left">
|
||||||
<span class="brand">GoTunnel</span>
|
<span class="brand">GoTunnel</span>
|
||||||
<span v-if="version" class="version">v{{ version }}</span>
|
<div v-if="version" class="version-info">
|
||||||
|
<ServerOutline class="version-icon" />
|
||||||
|
<span class="version">v{{ version }}</span>
|
||||||
|
<span v-if="updateInfo" class="update-status" :class="{ latest: !updateInfo.available, 'has-update': updateInfo.available }">
|
||||||
|
<template v-if="updateInfo.available">
|
||||||
|
<ArrowUpCircleOutline class="status-icon" />
|
||||||
|
<span>新版本 ({{ updateInfo.latest }})</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<CheckmarkCircleOutline class="status-icon" />
|
||||||
|
<span>最新版本</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="https://github.com/user/gotunnel" target="_blank" class="footer-link">
|
<a href="https://github.com/user/gotunnel" target="_blank" class="footer-link">
|
||||||
<LogoGithub class="footer-icon" />
|
<LogoGithub class="footer-icon" />
|
||||||
@@ -310,6 +335,42 @@ const themeOverrides: GlobalThemeOverrides = {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-status.latest {
|
||||||
|
color: #34d399;
|
||||||
|
background: rgba(52, 211, 153, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-status.has-update {
|
||||||
|
color: #fbbf24;
|
||||||
|
background: rgba(251, 191, 36, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-link {
|
.footer-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ const router = createRouter({
|
|||||||
name: 'home',
|
name: 'home',
|
||||||
component: () => import('../views/HomeView.vue'),
|
component: () => import('../views/HomeView.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/clients',
|
||||||
|
name: 'clients',
|
||||||
|
component: () => import('../views/ClientsView.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/client/:id',
|
path: '/client/:id',
|
||||||
name: 'client',
|
name: 'client',
|
||||||
component: () => import('../views/ClientView.vue'),
|
component: () => import('../views/ClientView.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/plugins',
|
|
||||||
name: 'plugins',
|
|
||||||
component: () => import('../views/PluginsView.vue'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
|
|||||||
317
web/src/views/ClientsView.vue
Normal file
317
web/src/views/ClientsView.vue
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { getClients } from '../api'
|
||||||
|
import type { ClientStatus } from '../types'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const clients = ref<ClientStatus[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
const loadClients = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await getClients()
|
||||||
|
clients.value = data || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load clients', e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onlineClients = computed(() => clients.value.filter(c => c.online).length)
|
||||||
|
|
||||||
|
const viewClient = (id: string) => {
|
||||||
|
router.push(`/client/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadClients)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="clients-page">
|
||||||
|
<!-- Particles -->
|
||||||
|
<div class="particles">
|
||||||
|
<div class="particle particle-1"></div>
|
||||||
|
<div class="particle particle-2"></div>
|
||||||
|
<div class="particle particle-3"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="clients-content">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">客户端管理</h1>
|
||||||
|
<p class="page-subtitle">管理所有连接的客户端</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Row -->
|
||||||
|
<div class="stats-row">
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-value">{{ clients.length }}</span>
|
||||||
|
<span class="stat-label">总客户端</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-value online">{{ onlineClients }}</span>
|
||||||
|
<span class="stat-label">在线</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-value offline">{{ clients.length - onlineClients }}</span>
|
||||||
|
<span class="stat-label">离线</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Client List -->
|
||||||
|
<div class="glass-card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>客户端列表</h3>
|
||||||
|
<button class="glass-btn small" @click="loadClients">刷新</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div v-if="loading" class="loading-state">加载中...</div>
|
||||||
|
<div v-else-if="clients.length === 0" class="empty-state">
|
||||||
|
<p>暂无客户端连接</p>
|
||||||
|
<p class="empty-hint">等待客户端连接...</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="clients-grid">
|
||||||
|
<div
|
||||||
|
v-for="client in clients"
|
||||||
|
:key="client.id"
|
||||||
|
class="client-card"
|
||||||
|
@click="viewClient(client.id)"
|
||||||
|
>
|
||||||
|
<div class="client-header">
|
||||||
|
<div class="client-status" :class="{ online: client.online }"></div>
|
||||||
|
<h4 class="client-name">{{ client.nickname || client.id }}</h4>
|
||||||
|
</div>
|
||||||
|
<p v-if="client.nickname" class="client-id">{{ client.id }}</p>
|
||||||
|
<div class="client-info">
|
||||||
|
<span v-if="client.remote_addr && client.online">{{ client.remote_addr }}</span>
|
||||||
|
<span>{{ client.rule_count || 0 }} 条规则</span>
|
||||||
|
</div>
|
||||||
|
<div class="client-tag" :class="client.online ? 'online' : 'offline'">
|
||||||
|
{{ client.online ? '在线' : '离线' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.clients-page {
|
||||||
|
min-height: calc(100vh - 108px);
|
||||||
|
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 30%, #4c1d95 60%, #581c87 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particles {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, rgba(255,255,255,0.15), rgba(255,255,255,0.05));
|
||||||
|
animation: float-particle 20s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.particle-1 { width: 250px; height: 250px; top: -80px; right: -50px; }
|
||||||
|
.particle-2 { width: 180px; height: 180px; bottom: 10%; left: 5%; animation-delay: -7s; }
|
||||||
|
.particle-3 { width: 120px; height: 120px; top: 50%; right: 15%; animation-delay: -12s; }
|
||||||
|
|
||||||
|
@keyframes float-particle {
|
||||||
|
0%, 100% { transform: translate(0, 0) scale(1); opacity: 0.3; }
|
||||||
|
50% { transform: translate(-20px, -60px) scale(0.95); opacity: 0.4; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.clients-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header { margin-bottom: 24px; }
|
||||||
|
.page-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
.page-subtitle {
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats */
|
||||||
|
.stats-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.stat-value.online { color: #34d399; }
|
||||||
|
.stat-value.offline { color: rgba(255,255,255,0.5); }
|
||||||
|
.stat-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Card */
|
||||||
|
.glass-card {
|
||||||
|
background: rgba(255,255,255,0.08);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.08);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.card-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.card-body { padding: 20px; }
|
||||||
|
|
||||||
|
.loading-state, .empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px;
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
.empty-hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255,255,255,0.3);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clients Grid */
|
||||||
|
.clients-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.clients-grid { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
.stats-row { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.clients-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-card {
|
||||||
|
background: rgba(255,255,255,0.05);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid rgba(255,255,255,0.08);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.client-card:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-status {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
.client-status.online {
|
||||||
|
background: #34d399;
|
||||||
|
box-shadow: 0 0 8px rgba(52,211,153,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: white;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255,255,255,0.4);
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255,255,255,0.6);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.client-tag.online {
|
||||||
|
background: rgba(52,211,153,0.2);
|
||||||
|
color: #34d399;
|
||||||
|
}
|
||||||
|
.client-tag.offline {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
color: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button */
|
||||||
|
.glass-btn {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: 1px solid rgba(255,255,255,0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.glass-btn:hover { background: rgba(255,255,255,0.2); }
|
||||||
|
.glass-btn.small { padding: 6px 12px; font-size: 12px; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user