style: 统一管理员和访客卡片为Material Design风格

This commit is contained in:
shirainbown
2026-06-19 14:58:32 +08:00
parent f10fc599f8
commit 0d960964e5

View File

@@ -1,4 +1,18 @@
<template>
.material-card.admin-card {
cursor: pointer;
}
.material-card.admin-card:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
border-color: var(--border-strong);
}
.material-extra {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}<template>
<div class="page">
<!-- Toolbar -->
<div class="toolbar" v-if="isAdmin">
@@ -28,154 +42,172 @@
<!-- Cards Grid -->
<div class="cards-grid" v-if="machines.length">
<!-- 管理员卡片完整信息 + 可点击 -->
<div v-if="isAdmin" v-for="m in machines" :key="m.id" class="server-card"
:class="[{ 'offline-card': !m.is_online }, osClass(m.os_type)]"
<!-- 管理员卡片Material 风格 + 可点击 + 完整信息 -->
<div v-if="isAdmin" v-for="(m, idx) in machines" :key="m.id" class="material-card admin-card"
:class="[{ 'offline-card': !m.is_online }]"
:style="getMaterialStyle(idx, m.is_online)"
@click="goDetail(m.id)">
<div class="card-content">
<div class="card-header">
<div class="title-row">
<div class="os-badge" :class="osClass(m.os_type)" :title="m.os_type">
<el-icon :size="12">
<component :is="osIcon(m.os_type)" />
</el-icon>
<span>{{ osShort(m.os_type) }}</span>
</div>
<span class="hostname">{{ m.hostname }}</span>
<el-tag :type="m.is_online ? 'success' : 'danger'" size="small" effect="light" round class="status-tag">
<el-icon :size="10"><component :is="m.is_online ? CircleCheck : CircleClose" /></el-icon>
<div class="material-content">
<!-- 顶部状态 + IP -->
<div class="material-header">
<div class="status-indicator">
<span class="status-dot" :class="{ pulse: m.is_online }"></span>
<span class="status-text" :class="m.is_online ? 'online' : 'offline'">
{{ m.is_online ? '在线' : '离线' }}
</el-tag>
</span>
</div>
<span class="material-ip">{{ m.ip }}</span>
</div>
<!-- 主机名 -->
<h3 class="material-hostname">{{ m.hostname }}</h3>
<!-- OS 信息 + 额外标签 -->
<div class="material-os">
<span class="os-dot" :style="{ background: getMaterialColor(idx).accent }"></span>
<span>{{ m.os_type }} {{ m.os_version || '' }}</span>
<el-tag v-if="m.service_count" size="small" effect="plain" class="svc-tag" round>
<el-icon :size="10"><Service /></el-icon> {{ m.service_count }}
</el-tag>
</div>
<div class="meta-row">
<span class="meta-ip">
<el-icon :size="10"><Link /></el-icon> {{ m.ip }}
</span>
<span class="meta-item">
<el-icon :size="10"><Cpu /></el-icon> {{ m.os_type }}
</span>
<span v-if="m.os_version" class="meta-item">{{ m.os_version }}</span>
<span v-if="m.uptime" class="meta-uptime">
<el-icon :size="10"><Timer /></el-icon> {{ m.uptime }}
</span>
<span v-if="m.pve_host_id && m.pve_vmid" class="meta-pve">
<el-tag size="small" :type="m.pve_vm_status === 'running' ? 'success' : 'danger'" effect="light" round class="vm-tag">
<el-tag v-if="m.pve_host_id && m.pve_vmid" size="small" :type="m.pve_vm_status === 'running' ? 'success' : 'danger'" effect="light" round class="vm-tag">
<el-icon :size="10"><component :is="m.pve_vm_status === 'running' ? VideoPlay : VideoPause" /></el-icon>
{{ m.pve_vm_status === 'running' ? 'VM运行中' : m.pve_vm_status === 'stopped' ? 'VM已停止' : 'VM检测中' }}
</el-tag>
<span v-if="m.uptime" class="meta-uptime">
<el-icon :size="10"><Timer /></el-icon> {{ m.uptime }}
</span>
</div>
</div>
<div v-if="m.cpu_info || m.memory_info || m.disk_info" class="stats-row">
<div v-if="m.cpu_info" class="stat-pill">
<div class="pill-icon cpu">
<el-icon :size="12"><Cpu /></el-icon>
<!-- 资源进度条 -->
<div class="material-progress-list" v-if="m.cpu_info || m.memory_info || m.disk_info">
<div v-if="m.cpu_info" class="progress-item">
<div class="progress-label-row">
<span class="progress-label">CPU</span>
<span class="progress-value" :class="{ warning: extractPercent(m.cpu_info) >= 80 }">
{{ extractPercent(m.cpu_info) }}%
</span>
</div>
<div class="pill-body">
<div class="pill-label">CPU</div>
<div class="pill-bar"><div class="pill-fill" :style="{ width: extractPercent(m.cpu_info) + '%' }"></div></div>
<div class="progress-track">
<div class="progress-fill" :style="{
width: extractPercent(m.cpu_info) + '%',
background: getProgressColor(extractPercent(m.cpu_info), getMaterialColor(idx).accent)
}"></div>
</div>
<div class="pill-value">{{ extractPercent(m.cpu_info) }}%</div>
</div>
<div v-if="m.memory_info" class="stat-pill">
<div class="pill-icon mem">
<el-icon :size="12"><Collection /></el-icon>
<div v-if="m.memory_info" class="progress-item">
<div class="progress-label-row">
<span class="progress-label">内存</span>
<span class="progress-value" :class="{ warning: extractPercent(m.memory_info) >= 80 }">
{{ extractDetail(m.memory_info) }}
</span>
</div>
<div class="pill-body">
<div class="pill-label">RAM</div>
<div class="pill-bar"><div class="pill-fill" :style="{ width: extractPercent(m.memory_info) + '%', background: getMemBarColor(extractPercent(m.memory_info)) }"></div></div>
<div class="progress-track">
<div class="progress-fill" :style="{
width: extractPercent(m.memory_info) + '%',
background: getProgressColor(extractPercent(m.memory_info), getMaterialColor(idx).accent)
}"></div>
</div>
<div class="pill-value">{{ extractDetail(m.memory_info) }}</div>
</div>
<div v-if="getMainDisk(m.disk_info)" class="stat-pill">
<div class="pill-icon disk">
<el-icon :size="12"><Histogram /></el-icon>
<div v-if="getMainDisk(m.disk_info)" class="progress-item">
<div class="progress-label-row">
<span class="progress-label">磁盘</span>
<span class="progress-value" :class="{ warning: getMainDisk(m.disk_info).percent >= 80 }">
{{ getMainDisk(m.disk_info).detail }}
</span>
</div>
<div class="pill-body">
<div class="pill-label">DISK</div>
<div class="pill-bar"><div class="pill-fill" :style="{ width: getMainDisk(m.disk_info).percent + '%', background: getDiskBarColor(getMainDisk(m.disk_info).percent) }"></div></div>
<div class="progress-track">
<div class="progress-fill" :style="{
width: getMainDisk(m.disk_info).percent + '%',
background: getProgressColor(getMainDisk(m.disk_info).percent, getMaterialColor(idx).accent)
}"></div>
</div>
<div class="pill-value">{{ getMainDisk(m.disk_info).detail }}</div>
</div>
</div>
<!-- 端口 + 同步时间 -->
<div v-if="m.listen_ports || m.ssh_synced_at" class="material-extra">
<div v-if="m.listen_ports" class="ports-row">
<el-tag v-for="p in m.listen_ports.split(',').slice(0,6)" :key="p" size="small" effect="plain" round class="port-tag">
<el-tag v-for="p in m.listen_ports.split(',').slice(0,4)" :key="p" size="small" effect="plain" round class="port-tag">
<el-icon :size="9"><Connection /></el-icon> {{ p.trim() }}
</el-tag>
<span v-if="m.listen_ports.split(',').length > 6" class="more-ports">+{{ m.listen_ports.split(',').length - 6 }}</span>
<span v-if="m.listen_ports.split(',').length > 4" class="more-ports">+{{ m.listen_ports.split(',').length - 4 }}</span>
</div>
<div v-if="m.ssh_synced_at" class="sync-time">
<el-icon :size="10"><Clock /></el-icon>
同步于 {{ formatTime(m.ssh_synced_at) }}
</div>
</div>
</div>
<!-- 未登录用户卡片机器名 + 状态 + OS + IP不可点击 -->
<div v-if="!isAdmin" v-for="m in machines" :key="m.id" class="server-card public-card"
:class="[{ 'offline-card': !m.is_online }, osClass(m.os_type)]">
<div class="card-content">
<div class="card-header">
<div class="title-row">
<div class="os-badge" :class="osClass(m.os_type)" :title="m.os_type">
<el-icon :size="12">
<component :is="osIcon(m.os_type)" />
</el-icon>
<span>{{ osShort(m.os_type) }}</span>
</div>
<span class="hostname">{{ m.hostname }}</span>
<el-tag :type="m.is_online ? 'success' : 'danger'" size="small" effect="light" round class="status-tag">
<el-icon :size="10"><component :is="m.is_online ? CircleCheck : CircleClose" /></el-icon>
<!-- 未登录用户卡片Material Design 风格 -->
<div v-if="!isAdmin" v-for="(m, idx) in machines" :key="m.id" class="material-card"
:class="[{ 'offline-card': !m.is_online }]"
:style="getMaterialStyle(idx, m.is_online)">
<div class="material-content">
<!-- 顶部状态 + IP -->
<div class="material-header">
<div class="status-indicator">
<span class="status-dot" :class="{ pulse: m.is_online }"></span>
<span class="status-text" :class="m.is_online ? 'online' : 'offline'">
{{ m.is_online ? '在线' : '离线' }}
</el-tag>
</div>
<div class="meta-row">
<span class="meta-ip">
<el-icon :size="10"><Link /></el-icon> {{ m.ip }}
</span>
<span class="meta-item">
<el-icon :size="10"><Cpu /></el-icon> {{ m.os_type }} {{ m.os_version || '' }}
</span>
</div>
<span class="material-ip">{{ m.ip }}</span>
</div>
<!-- 未登录用户也展示资源条只读 -->
<div v-if="m.cpu_info || m.memory_info || m.disk_info" class="stats-row">
<div v-if="m.cpu_info" class="stat-pill">
<div class="pill-icon cpu">
<el-icon :size="12"><Cpu /></el-icon>
<!-- 主机名 -->
<h3 class="material-hostname">{{ m.hostname }}</h3>
<!-- OS 信息 -->
<div class="material-os">
<span class="os-dot" :style="{ background: getMaterialColor(idx).accent }"></span>
<span>{{ m.os_type }} {{ m.os_version || '' }}</span>
</div>
<div class="pill-body">
<div class="pill-label">CPU</div>
<div class="pill-bar"><div class="pill-fill" :style="{ width: extractPercent(m.cpu_info) + '%' }"></div></div>
<!-- 资源进度条 -->
<div class="material-progress-list" v-if="m.cpu_info || m.memory_info || m.disk_info">
<div v-if="m.cpu_info" class="progress-item">
<div class="progress-label-row">
<span class="progress-label">CPU</span>
<span class="progress-value" :class="{ warning: extractPercent(m.cpu_info) >= 80 }">
{{ extractPercent(m.cpu_info) }}%
</span>
</div>
<div class="pill-value">{{ extractPercent(m.cpu_info) }}%</div>
<div class="progress-track">
<div class="progress-fill" :style="{
width: extractPercent(m.cpu_info) + '%',
background: getProgressColor(extractPercent(m.cpu_info), getMaterialColor(idx).accent)
}"></div>
</div>
<div v-if="m.memory_info" class="stat-pill">
<div class="pill-icon mem">
<el-icon :size="12"><Collection /></el-icon>
</div>
<div class="pill-body">
<div class="pill-label">RAM</div>
<div class="pill-bar"><div class="pill-fill" :style="{ width: extractPercent(m.memory_info) + '%', background: getMemBarColor(extractPercent(m.memory_info)) }"></div></div>
<div v-if="m.memory_info" class="progress-item">
<div class="progress-label-row">
<span class="progress-label">内存</span>
<span class="progress-value" :class="{ warning: extractPercent(m.memory_info) >= 80 }">
{{ extractDetail(m.memory_info) }}
</span>
</div>
<div class="pill-value">{{ extractDetail(m.memory_info) }}</div>
<div class="progress-track">
<div class="progress-fill" :style="{
width: extractPercent(m.memory_info) + '%',
background: getProgressColor(extractPercent(m.memory_info), getMaterialColor(idx).accent)
}"></div>
</div>
<div v-if="getMainDisk(m.disk_info)" class="stat-pill">
<div class="pill-icon disk">
<el-icon :size="12"><Histogram /></el-icon>
</div>
<div class="pill-body">
<div class="pill-label">DISK</div>
<div class="pill-bar"><div class="pill-fill" :style="{ width: getMainDisk(m.disk_info).percent + '%', background: getDiskBarColor(getMainDisk(m.disk_info).percent) }"></div></div>
<div v-if="getMainDisk(m.disk_info)" class="progress-item">
<div class="progress-label-row">
<span class="progress-label">磁盘</span>
<span class="progress-value" :class="{ warning: getMainDisk(m.disk_info).percent >= 80 }">
{{ getMainDisk(m.disk_info).detail }}
</span>
</div>
<div class="progress-track">
<div class="progress-fill" :style="{
width: getMainDisk(m.disk_info).percent + '%',
background: getProgressColor(getMainDisk(m.disk_info).percent, getMaterialColor(idx).accent)
}"></div>
</div>
<div class="pill-value">{{ getMainDisk(m.disk_info).detail }}</div>
</div>
</div>
</div>
@@ -412,6 +444,33 @@ function extractDetail(str) {
return s
}
function getMaterialColor(index) {
const colors = [
{ accent: '#58a6ff' },
{ accent: '#3fb950' },
{ accent: '#a371f7' },
{ accent: '#d29922' },
{ accent: '#f0883e' },
{ accent: '#f778ba' },
{ accent: '#39c5cf' },
{ accent: '#7ee787' },
]
return colors[index % colors.length]
}
function getMaterialStyle(index, isOnline) {
if (!isOnline) {
return { opacity: '0.55' }
}
return {}
}
function getProgressColor(value, accent) {
if (value >= 80) return '#F44336'
if (value >= 60) return '#FFC107'
return accent
}
function getMemBarColor(v) {
if (v >= 90) return 'var(--danger)'
if (v >= 70) return 'var(--warning)'
@@ -501,7 +560,155 @@ function formatTime(t) {
.cards-grid { grid-template-columns: 1fr; }
}
/* ─── Neon Tech Card ─── */
/* ─── Material Design 访客卡片 ─── */
.material-card {
position: relative;
border-radius: 16px;
border: 1px solid var(--border);
padding: 16px;
min-height: 180px;
cursor: default;
transition: all 0.2s ease;
overflow: hidden;
background: var(--card-bg);
}
.material-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
border-color: var(--border-strong);
}
.material-card.offline-card {
opacity: 0.6;
}
.material-content {
display: flex;
flex-direction: column;
height: 100%;
}
.material-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
position: relative;
}
.status-dot.pulse {
background: var(--success);
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.25);
animation: pulse-ring 2s ease-in-out infinite;
}
.status-dot:not(.pulse) {
background: var(--text-muted);
}
@keyframes pulse-ring {
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
70% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); }
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
}
.status-text {
font-size: 12px;
font-weight: 600;
}
.status-text.online { color: var(--success); }
.status-text.offline { color: var(--text-muted); }
.material-ip {
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
}
.material-hostname {
font-size: 15px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 4px;
line-height: 1.3;
}
.material-os {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
margin-bottom: 12px;
}
.os-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.material-progress-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: auto;
}
.progress-item {
display: flex;
flex-direction: column;
gap: 3px;
}
.progress-label-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.progress-label {
font-size: 11px;
font-weight: 500;
color: var(--text-muted);
}
.progress-value {
font-size: 12px;
font-weight: 700;
font-variant-numeric: tabular-nums;
color: var(--text-primary);
}
.progress-value.warning {
color: var(--danger);
}
.progress-track {
width: 100%;
height: 4px;
border-radius: 2px;
background: var(--border);
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 2px;
transition: width 0.6s ease;
}
/* ─── 管理员卡片(保持原有) ─── */
.server-card {
position: relative;
background: var(--card-bg);