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:
iven
2026-04-06 13:09:43 +08:00
parent fd6fb5cca0
commit e99ea53eba
30 changed files with 3493 additions and 856 deletions

View File

@@ -1,55 +1,82 @@
<template>
<div class="settings-page">
<div class="page-container">
<el-row :gutter="20">
<el-col :span="12">
<el-card shadow="hover">
<template #header><span class="card-title">系统信息</span></template>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="系统版本">v{{ version }}</el-descriptions-item>
<el-descriptions-item label="数据库">{{ dbInfo }}</el-descriptions-item>
<el-descriptions-item label="在线终端">{{ health.connected_clients }}</el-descriptions-item>
</el-descriptions>
</el-card>
<div class="csm-card">
<div class="csm-card-header">系统信息</div>
<div class="csm-card-body">
<div class="info-grid">
<div class="info-item">
<span class="info-label">系统版本</span>
<span class="info-value">v{{ version }}</span>
</div>
<div class="info-item">
<span class="info-label">数据库</span>
<span class="info-value">{{ dbInfo }}</span>
</div>
<div class="info-item">
<span class="info-label">在线终端</span>
<span class="info-value">{{ health.connected_clients }} </span>
</div>
</div>
</div>
</div>
<el-card shadow="hover" style="margin-top: 20px">
<template #header><span class="card-title">修改密码</span></template>
<el-form :model="pwdForm" label-width="100px" size="small">
<el-form-item label="当前密码">
<el-input v-model="pwdForm.oldPassword" type="password" show-password />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pwdForm.newPassword" type="password" show-password />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="pwdForm.confirmPassword" type="password" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="changePassword">修改密码</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="csm-card" style="margin-top: 20px">
<div class="csm-card-header">修改密码</div>
<div class="csm-card-body">
<el-form :model="pwdForm" label-width="100px" size="default">
<el-form-item label="当前密码">
<el-input v-model="pwdForm.oldPassword" type="password" show-password placeholder="输入当前密码" />
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pwdForm.newPassword" type="password" show-password placeholder="输入新密码" />
</el-form-item>
<el-form-item label="确认密码">
<el-input v-model="pwdForm.confirmPassword" type="password" show-password placeholder="再次输入新密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="pwdLoading" @click="changePassword">修改密码</el-button>
</el-form-item>
</el-form>
</div>
</div>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><span class="card-title">数据维护</span></template>
<el-form label-width="100px" size="small">
<el-form-item label="历史数据">
<el-button @click="showRetentionInfo">查看保留策略</el-button>
</el-form-item>
<el-form-item label="数据库">
<el-button type="warning" @click="manualCleanup">手动清理</el-button>
</el-form-item>
</el-form>
</el-card>
<div class="csm-card">
<div class="csm-card-header">数据维护</div>
<div class="csm-card-body">
<div class="maintenance-item">
<div>
<div style="font-weight:500">历史数据保留</div>
<div style="font-size:12px;color:var(--csm-text-tertiary);margin-top:4px">配置数据保留策略自动清理过期数据</div>
</div>
<el-button @click="showRetentionInfo">查看策略</el-button>
</div>
<el-divider style="margin:12px 0" />
<div class="maintenance-item">
<div>
<div style="font-weight:500">手动清理</div>
<div style="font-size:12px;color:var(--csm-text-tertiary);margin-top:4px">立即清理过期的历史数据</div>
</div>
<el-button type="warning" plain @click="manualCleanup">执行清理</el-button>
</div>
</div>
</div>
<el-card shadow="hover" style="margin-top: 20px">
<template #header><span class="card-title">当前用户</span></template>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="用户名">{{ user.username }}</el-descriptions-item>
<el-descriptions-item label="角色">{{ user.role }}</el-descriptions-item>
</el-descriptions>
</el-card>
<div class="csm-card" style="margin-top: 20px">
<div class="csm-card-header">当前用户</div>
<div class="csm-card-body">
<div class="user-card">
<div class="user-avatar-large">{{ user.username.charAt(0).toUpperCase() }}</div>
<div class="user-detail">
<div style="font-weight:600;font-size:16px">{{ user.username }}</div>
<el-tag size="small" type="warning" effect="light" style="margin-top:4px">{{ user.role }}</el-tag>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
@@ -66,9 +93,9 @@ const health = reactive({ connected_clients: 0, db_size_bytes: 0 })
const user = reactive({ username: 'admin', role: 'admin' })
const pwdForm = reactive({ oldPassword: '', newPassword: '', confirmPassword: '' })
const pwdLoading = ref(false)
onMounted(() => {
// Decode username from JWT token
try {
const token = localStorage.getItem('token')
if (token) {
@@ -83,21 +110,39 @@ onMounted(() => {
if (data.version) version.value = data.version
health.connected_clients = data.connected_clients || 0
const bytes = data.db_size_bytes || 0
dbInfo.value = `SQLite (WAL mode) - ${(bytes / 1024 / 1024).toFixed(2)} MB`
dbInfo.value = `SQLite (WAL) - ${(bytes / 1024 / 1024).toFixed(2)} MB`
})
.catch(() => { /* ignore */ })
})
function changePassword() {
async function changePassword() {
if (!pwdForm.oldPassword) {
ElMessage.error('请输入当前密码')
return
}
if (pwdForm.newPassword.length < 6) {
ElMessage.error('新密码至少6位')
return
}
if (pwdForm.newPassword !== pwdForm.confirmPassword) {
ElMessage.error('两次输入的密码不一致')
return
}
if (pwdForm.newPassword.length < 6) {
ElMessage.error('密码至少6位')
return
pwdLoading.value = true
try {
await api.put('/api/auth/change-password', {
old_password: pwdForm.oldPassword,
new_password: pwdForm.newPassword,
})
ElMessage.success('密码修改成功')
pwdForm.oldPassword = ''
pwdForm.newPassword = ''
pwdForm.confirmPassword = ''
} catch (e: any) {
ElMessage.error(e.message || '密码修改失败')
} finally {
pwdLoading.value = false
}
ElMessage.success('密码修改功能待实现')
}
function showRetentionInfo() {
@@ -110,6 +155,69 @@ function manualCleanup() {
</script>
<style scoped>
.settings-page { padding: 20px; }
.card-title { font-weight: 600; font-size: 15px; }
.csm-card-header {
font-weight: 600;
font-size: 15px;
color: var(--csm-text-primary);
padding: 16px 20px;
border-bottom: 1px solid var(--csm-border-color);
}
.csm-card-body {
padding: 20px;
}
.info-grid {
display: flex;
flex-direction: column;
gap: 16px;
}
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
}
.info-label {
font-size: 13px;
color: var(--csm-text-secondary);
}
.info-value {
font-size: 13px;
font-weight: 500;
color: var(--csm-text-primary);
}
.maintenance-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.user-card {
display: flex;
align-items: center;
gap: 16px;
}
.user-avatar-large {
width: 48px;
height: 48px;
border-radius: 12px;
background: var(--csm-primary);
color: #fff;
font-size: 20px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.user-detail {
display: flex;
flex-direction: column;
}
</style>