feat: 全面重构前端UI及完善后端功能
前端重构: - 重构Layout为左侧导航+顶栏的现代管理后台布局 - 重构设备管理页面(Devices.vue):左侧分组面板+右侧设备列表 - 重构设备详情(DeviceDetail.vue):集成硬件资产/软件资产/变更记录标签页 - 移除独立资产管理页面,功能合并至设备详情 - 重构Dashboard/登录/设置/告警/水印/上网管控等页面样式 - 新增全局CSS变量和统一样式系统 - 添加分组管理UI:新建/重命名/删除分组,移动设备到分组 后端完善: - 新增分组CRUD API(groups.rs):创建/重命名/删除分组,设备分组移动 - 客户端硬件采集:完善GPU/主板/序列号/磁盘信息采集(Windows PowerShell) - 客户端软件采集:通过Windows注册表读取已安装软件列表 - 新增SoftwareAssetReport消息类型(0x09)及处理链路 - 数据库新增upsert_software方法处理软件资产存储 - 服务端推送软件资产配置给新注册设备 - 修复密码修改功能,添加旧密码验证
This commit is contained in:
@@ -1,92 +1,177 @@
|
||||
<template>
|
||||
<div class="device-detail" v-loading="loading">
|
||||
<el-page-header @back="$router.back()" :title="'返回'">
|
||||
<template #content>
|
||||
<span>{{ device?.hostname || deviceUid }}</span>
|
||||
<el-tag v-if="device" :type="device.status === 'online' ? 'success' : 'info'" size="small" style="margin-left: 8px">
|
||||
{{ device.status === 'online' ? '在线' : '离线' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-page-header>
|
||||
<div class="page-container" v-loading="loading">
|
||||
<!-- Header -->
|
||||
<div class="detail-header">
|
||||
<el-page-header @back="$router.back()" title="返回列表">
|
||||
<template #content>
|
||||
<div class="header-device">
|
||||
<div class="device-avatar-lg" :class="device?.status">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:24px;height:24px">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="header-name">{{ device?.hostname || deviceUid }}</div>
|
||||
<div class="header-meta">
|
||||
<el-tag v-if="device" :type="device.status === 'online' ? 'success' : 'info'" size="small" effect="light">
|
||||
{{ device.status === 'online' ? '在线' : '离线' }}
|
||||
</el-tag>
|
||||
<span class="meta-text">{{ device?.ip_address }}</span>
|
||||
<el-tag v-if="device?.group_name" size="small" effect="plain" round>{{ device.group_name }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-page-header>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover">
|
||||
<template #header><span class="card-title">基本信息</span></template>
|
||||
<el-descriptions :column="1" size="small" border>
|
||||
<el-descriptions-item label="设备UID">{{ device?.device_uid }}</el-descriptions-item>
|
||||
<el-descriptions-item label="主机名">{{ device?.hostname }}</el-descriptions-item>
|
||||
<el-descriptions-item label="IP地址">{{ device?.ip_address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="MAC地址">{{ device?.mac_address || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作系统">{{ device?.os_version || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户端版本">{{ device?.client_version || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分组">{{ device?.group_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{ device?.registered_at || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后心跳">{{ device?.last_heartbeat || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<!-- Tabs -->
|
||||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||||
<!-- Tab 1: 概览 -->
|
||||
<el-tab-pane label="概览" name="overview">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="8">
|
||||
<div class="csm-card">
|
||||
<div class="card-section-title">基本信息</div>
|
||||
<div class="info-list">
|
||||
<div class="info-row" v-for="item in basicInfo" :key="item.label">
|
||||
<span class="info-label">{{ item.label }}</span>
|
||||
<span class="info-value" :title="item.value">{{ item.value || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" style="margin-bottom: 20px">
|
||||
<template #header><span class="card-title">实时状态</span></template>
|
||||
<el-row :gutter="20" v-if="status">
|
||||
<el-col :span="6">
|
||||
<div class="metric">
|
||||
<div class="metric-label">CPU</div>
|
||||
<el-progress type="dashboard" :percentage="Math.round(status.cpu_usage)" :width="100"
|
||||
:color="progressColor(status.cpu_usage)" />
|
||||
<el-col :span="16">
|
||||
<div class="csm-card" style="margin-bottom: 16px">
|
||||
<div class="card-section-title">实时状态</div>
|
||||
<div class="metrics-grid" v-if="status">
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">CPU</div>
|
||||
<el-progress type="dashboard" :percentage="Math.round(status.cpu_usage)" :width="90"
|
||||
:color="progressColor(status.cpu_usage)" :stroke-width="6" />
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">内存</div>
|
||||
<el-progress type="dashboard" :percentage="Math.round(status.memory_usage)" :width="90"
|
||||
:color="progressColor(status.memory_usage)" :stroke-width="6" />
|
||||
<div class="metric-sub">{{ formatMB(status.memory_total_mb) }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">磁盘</div>
|
||||
<el-progress type="dashboard" :percentage="Math.round(status.disk_usage)" :width="90"
|
||||
:color="progressColor(status.disk_usage)" :stroke-width="6" />
|
||||
<div class="metric-sub">{{ formatMB(status.disk_total_mb) }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">进程</div>
|
||||
<div class="metric-big-number">{{ status.running_procs }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="metric">
|
||||
<div class="metric-label">内存</div>
|
||||
<el-progress type="dashboard" :percentage="Math.round(status.memory_usage)" :width="100"
|
||||
:color="progressColor(status.memory_usage)" />
|
||||
<div class="metric-sub">{{ formatMB(status.memory_total_mb) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="metric">
|
||||
<div class="metric-label">磁盘</div>
|
||||
<el-progress type="dashboard" :percentage="Math.round(status.disk_usage)" :width="100"
|
||||
:color="progressColor(status.disk_usage)" />
|
||||
<div class="metric-sub">{{ formatMB(status.disk_total_mb) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="metric">
|
||||
<div class="metric-label">进程</div>
|
||||
<div class="metric-value">{{ status.running_procs }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-else description="暂无状态数据" :image-size="60" />
|
||||
</el-card>
|
||||
<el-empty v-else description="暂无状态数据" :image-size="64" />
|
||||
</div>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header><span class="card-title">Top 进程</span></template>
|
||||
<el-table :data="status?.top_processes || []" size="small" max-height="200">
|
||||
<el-table-column prop="name" label="进程名" />
|
||||
<el-table-column prop="pid" label="PID" width="80" />
|
||||
<el-table-column label="CPU" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="Math.min(Math.round(row.cpu_usage), 100)" :stroke-width="6" :color="progressColor(row.cpu_usage)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<div class="csm-card">
|
||||
<div class="card-section-title">Top 进程</div>
|
||||
<el-table :data="status?.top_processes || []" size="small" max-height="200" style="width:100%">
|
||||
<el-table-column prop="name" label="进程名" />
|
||||
<el-table-column prop="pid" label="PID" width="80">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.pid }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="CPU" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="Math.min(Math.round(row.cpu_usage), 100)" :stroke-width="6" :color="progressColor(row.cpu_usage)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="内存" width="100">
|
||||
<template #default="{ row }"><span class="mono-text">{{ formatMB(row.memory_mb) }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 2: 硬件资产 -->
|
||||
<el-tab-pane label="硬件资产" name="hardware">
|
||||
<div class="csm-card">
|
||||
<el-table :data="hardware" v-loading="hwLoading" style="width:100%">
|
||||
<el-table-column prop="cpu_model" label="CPU型号" min-width="200" />
|
||||
<el-table-column prop="cpu_cores" label="核心数" width="80" />
|
||||
<el-table-column label="内存" width="100">
|
||||
<template #default="{ row }">{{ formatMB(row.memory_mb) }}</template>
|
||||
<template #default="{ row }"><span class="mono-text">{{ formatMB(row.memory_total_mb) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="gpu_model" label="GPU" min-width="150">
|
||||
<template #default="{ row }"><span>{{ row.gpu_model || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="motherboard" label="主板" min-width="140">
|
||||
<template #default="{ row }"><span>{{ row.motherboard || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="disk_model" label="磁盘" min-width="140">
|
||||
<template #default="{ row }"><span>{{ row.disk_model || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="磁盘容量" width="100">
|
||||
<template #default="{ row }"><span class="mono-text">{{ formatMB(row.disk_total_mb) }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="serial_number" label="序列号" width="160">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.serial_number || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 3: 软件资产 -->
|
||||
<el-tab-pane label="软件资产" name="software">
|
||||
<div class="toolbar-inline">
|
||||
<el-input v-model="swSearch" placeholder="搜索软件名称 / 发行商" style="width: 260px" clearable :prefix-icon="Search" @input="fetchSoftware" />
|
||||
</div>
|
||||
<div class="csm-card">
|
||||
<el-table :data="software" v-loading="swLoading" style="width:100%">
|
||||
<el-table-column prop="name" label="软件名称" min-width="200">
|
||||
<template #default="{ row }"><span style="font-weight:500">{{ row.name }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="version" label="版本" width="120">
|
||||
<template #default="{ row }"><span class="mono-text">{{ row.version || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="publisher" label="发行商" min-width="150">
|
||||
<template #default="{ row }"><span>{{ row.publisher || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="install_date" label="安装日期" width="120">
|
||||
<template #default="{ row }"><span class="secondary-text">{{ row.install_date || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="install_path" label="安装路径" min-width="250" show-overflow-tooltip>
|
||||
<template #default="{ row }"><span class="mono-text secondary-text">{{ row.install_path || '-' }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab 4: 变更记录 -->
|
||||
<el-tab-pane label="变更记录" name="changes">
|
||||
<div class="csm-card">
|
||||
<el-table :data="changes" v-loading="chLoading" style="width:100%">
|
||||
<el-table-column prop="change_type" label="变更类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="changeTag(row.change_type)" size="small" effect="light">{{ changeLabel(row.change_type) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="change_detail" label="详情" min-width="400" show-overflow-tooltip />
|
||||
<el-table-column prop="detected_at" label="检测时间" width="170">
|
||||
<template #default="{ row }"><span class="secondary-text">{{ row.detected_at }}</span></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { api } from '@/lib/api'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -95,18 +180,94 @@ const deviceUid = route.params.uid as string
|
||||
const loading = ref(true)
|
||||
const device = ref<any>(null)
|
||||
const status = ref<any>(null)
|
||||
const activeTab = ref('overview')
|
||||
|
||||
// --- Overview data ---
|
||||
const basicInfo = computed(() => {
|
||||
if (!device.value) return []
|
||||
return [
|
||||
{ label: '设备UID', value: device.value.device_uid },
|
||||
{ label: '主机名', value: device.value.hostname },
|
||||
{ label: 'IP地址', value: device.value.ip_address },
|
||||
{ label: 'MAC地址', value: device.value.mac_address },
|
||||
{ label: '操作系统', value: device.value.os_version },
|
||||
{ label: '客户端版本', value: device.value.client_version },
|
||||
{ label: '分组', value: device.value.group_name },
|
||||
{ label: '注册时间', value: device.value.registered_at },
|
||||
{ label: '最后心跳', value: device.value.last_heartbeat },
|
||||
]
|
||||
})
|
||||
|
||||
// --- Hardware data ---
|
||||
const hardware = ref<any[]>([])
|
||||
const hwLoading = ref(false)
|
||||
|
||||
async function fetchHardware() {
|
||||
hwLoading.value = true
|
||||
try {
|
||||
const data = await api.get<any>(`/api/assets/hardware?device_uid=${deviceUid}`)
|
||||
hardware.value = data.hardware || []
|
||||
} catch { /* ignore */ } finally { hwLoading.value = false }
|
||||
}
|
||||
|
||||
// --- Software data ---
|
||||
const software = ref<any[]>([])
|
||||
const swLoading = ref(false)
|
||||
const swSearch = ref('')
|
||||
|
||||
async function fetchSoftware() {
|
||||
swLoading.value = true
|
||||
try {
|
||||
const params = new URLSearchParams({ device_uid: deviceUid })
|
||||
if (swSearch.value) params.set('search', swSearch.value)
|
||||
const data = await api.get<any>(`/api/assets/software?${params}`)
|
||||
software.value = data.software || []
|
||||
} catch { /* ignore */ } finally { swLoading.value = false }
|
||||
}
|
||||
|
||||
// --- Changes data ---
|
||||
const changes = ref<any[]>([])
|
||||
const chLoading = ref(false)
|
||||
|
||||
async function fetchChanges() {
|
||||
chLoading.value = true
|
||||
try {
|
||||
const data = await api.get<any>(`/api/assets/changes?device_uid=${deviceUid}`)
|
||||
changes.value = data.changes || []
|
||||
} catch { /* ignore */ } finally { chLoading.value = false }
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
function progressColor(value: number) {
|
||||
if (value > 90) return '#F56C6C'
|
||||
if (value > 70) return '#E6A23C'
|
||||
return '#67C23A'
|
||||
if (value > 90) return '#dc2626'
|
||||
if (value > 70) return '#d97706'
|
||||
return '#16a34a'
|
||||
}
|
||||
|
||||
function formatMB(mb: number) {
|
||||
if (!mb) return '-'
|
||||
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`
|
||||
return `${mb} MB`
|
||||
}
|
||||
|
||||
function changeTag(type: string) {
|
||||
const map: Record<string, string> = { hardware: 'warning', software_added: 'success', software_removed: 'danger' }
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
function changeLabel(type: string) {
|
||||
const map: Record<string, string> = { hardware: '硬件变更', software_added: '软件安装', software_removed: '软件卸载' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
// --- Tab switching ---
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'hardware' && hardware.value.length === 0) fetchHardware()
|
||||
else if (tab === 'software' && software.value.length === 0) fetchSoftware()
|
||||
else if (tab === 'changes' && changes.value.length === 0) fetchChanges()
|
||||
})
|
||||
|
||||
// --- Init ---
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [devData, statData] = await Promise.all([
|
||||
@@ -122,10 +283,164 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.device-detail { padding: 20px; }
|
||||
.card-title { font-weight: 600; font-size: 15px; }
|
||||
.metric { text-align: center; padding: 10px 0; }
|
||||
.metric-label { font-size: 13px; color: #909399; margin-bottom: 8px; }
|
||||
.metric-value { font-size: 32px; font-weight: 700; color: #303133; margin-top: 16px; }
|
||||
.metric-sub { font-size: 12px; color: #909399; margin-top: 4px; }
|
||||
.detail-header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--csm-border-color);
|
||||
}
|
||||
|
||||
.header-device {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: var(--csm-text-primary);
|
||||
}
|
||||
|
||||
.header-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.meta-text {
|
||||
font-size: 12px;
|
||||
color: var(--csm-text-tertiary);
|
||||
font-family: var(--csm-font-mono);
|
||||
}
|
||||
|
||||
.device-avatar-lg {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
background: #f1f5f9;
|
||||
color: #94a3b8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.device-avatar-lg.online {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.device-avatar-lg.offline {
|
||||
background: #f1f5f9;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.detail-tabs {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.detail-tabs :deep(.el-tabs__header) {
|
||||
margin: 0 0 16px 0;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--csm-border-color);
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.detail-tabs :deep(.el-tabs__item) {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.toolbar-inline {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card-section-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--csm-text-primary);
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--csm-border-color);
|
||||
}
|
||||
|
||||
.info-list {
|
||||
padding: 8px 20px 16px;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #f8fafc;
|
||||
}
|
||||
|
||||
.info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 13px;
|
||||
color: var(--csm-text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--csm-text-primary);
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 13px;
|
||||
color: var(--csm-text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-big-number {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: var(--csm-text-primary);
|
||||
margin-top: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.metric-sub {
|
||||
font-size: 12px;
|
||||
color: var(--csm-text-tertiary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mono-text {
|
||||
font-family: var(--csm-font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--csm-text-secondary);
|
||||
}
|
||||
|
||||
.secondary-text {
|
||||
font-size: 12px;
|
||||
color: var(--csm-text-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user