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,63 +1,91 @@
<template>
<div class="usb-page">
<el-tabs v-model="activeTab">
<div class="page-container">
<el-tabs v-model="activeTab" class="page-tabs">
<el-tab-pane label="策略管理" name="policies">
<div class="toolbar">
<el-button type="primary" @click="showPolicyDialog()">新建策略</el-button>
<div class="page-toolbar">
<el-button type="primary" @click="showPolicyDialog()">
<el-icon><Plus /></el-icon>新建策略
</el-button>
</div>
<div class="csm-card">
<el-table :data="policies" v-loading="loading" style="width:100%">
<el-table-column prop="name" label="策略名称" min-width="180">
<template #default="{ row }">
<span style="font-weight:500">{{ row.name }}</span>
</template>
</el-table-column>
<el-table-column prop="policy_type" label="策略类型" width="130">
<template #default="{ row }">
<el-tag :type="policyTypeTag(row.policy_type)" size="small" effect="light">
{{ policyTypeLabel(row.policy_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="target_group" label="目标分组" width="120">
<template #default="{ row }">
<el-tag size="small" effect="plain">{{ row.target_group || '全部' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="enabled" label="启用" width="80">
<template #default="{ row }">
<el-switch :model-value="row.enabled" :active-value="1" :inactive-value="0" @change="togglePolicy(row)" size="small" />
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="170">
<template #default="{ row }">
<span class="secondary-text">{{ row.created_at }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="showPolicyDialog(row)">编辑</el-button>
<el-button link type="danger" size="small" @click="deletePolicy(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-table :data="policies" v-loading="loading" stripe size="small">
<el-table-column prop="name" label="策略名称" width="180" />
<el-table-column prop="policy_type" label="策略类型" width="120">
<template #default="{ row }">
<el-tag :type="policyTypeTag(row.policy_type)" size="small">{{ policyTypeLabel(row.policy_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="target_group" label="目标分组" width="120" />
<el-table-column prop="enabled" label="启用" width="80">
<template #default="{ row }">
<el-switch :model-value="row.enabled" @change="togglePolicy(row)" size="small" />
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="170" />
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="showPolicyDialog(row)">编辑</el-button>
<el-button link type="danger" size="small" @click="deletePolicy(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="事件日志" name="events">
<div class="toolbar">
<div class="page-toolbar">
<el-select v-model="eventFilter" placeholder="事件类型" clearable style="width: 150px" @change="fetchEvents">
<el-option label="插入" value="Inserted" />
<el-option label="拔出" value="Removed" />
<el-option label="拦截" value="Blocked" />
</el-select>
</div>
<el-table :data="events" v-loading="evLoading" stripe size="small">
<el-table-column prop="device_name" label="USB设备" width="150" />
<el-table-column label="事件类型" width="100">
<template #default="{ row }">
<el-tag :type="row.event_type === 'Inserted' ? 'success' : row.event_type === 'Blocked' ? 'danger' : 'info'" size="small">
{{ eventTypeLabel(row.event_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="vendor_id" label="VID" width="100" />
<el-table-column prop="product_id" label="PID" width="100" />
<el-table-column prop="serial_number" label="序列号" width="160" />
<el-table-column prop="device_uid" label="终端UID" min-width="160" show-overflow-tooltip />
<el-table-column prop="event_time" label="时间" width="170" />
</el-table>
<div class="csm-card">
<el-table :data="events" v-loading="evLoading" style="width:100%">
<el-table-column prop="device_name" label="USB设备" width="150" />
<el-table-column label="事件类型" width="100">
<template #default="{ row }">
<el-tag :type="row.event_type === 'Inserted' ? 'success' : row.event_type === 'Blocked' ? 'danger' : 'info'" size="small" effect="light">
{{ eventTypeLabel(row.event_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="vendor_id" label="VID" width="100">
<template #default="{ row }"><span class="mono-text">{{ row.vendor_id }}</span></template>
</el-table-column>
<el-table-column prop="product_id" label="PID" width="100">
<template #default="{ row }"><span class="mono-text">{{ row.product_id }}</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-column prop="device_uid" label="终端UID" min-width="160" show-overflow-tooltip />
<el-table-column prop="event_time" label="时间" width="170">
<template #default="{ row }"><span class="secondary-text">{{ row.event_time }}</span></template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
<el-dialog v-model="policyDialogVisible" :title="editingPolicy ? '编辑策略' : '新建策略'" width="500px">
<el-dialog v-model="policyDialogVisible" :title="editingPolicy ? '编辑策略' : '新建策略'" width="520px" destroy-on-close>
<el-form :model="policyForm" label-width="100px">
<el-form-item label="策略名称">
<el-input v-model="policyForm.name" />
<el-input v-model="policyForm.name" placeholder="输入策略名称" />
</el-form-item>
<el-form-item label="策略类型">
<el-select v-model="policyForm.policy_type" style="width: 100%">
@@ -84,11 +112,11 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { api } from '@/lib/api'
const activeTab = ref('policies')
// Policies
const policies = ref<any[]>([])
const loading = ref(false)
const policyDialogVisible = ref(false)
@@ -160,7 +188,6 @@ function policyTypeLabel(type: string) {
return map[type] || type
}
// Events
const events = ref<any[]>([])
const evLoading = ref(false)
const eventFilter = ref('')
@@ -187,6 +214,21 @@ onMounted(() => {
</script>
<style scoped>
.usb-page { padding: 20px; }
.toolbar { display: flex; gap: 12px; margin-bottom: 16px; }
.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);
}
.page-tabs :deep(.el-tabs__header) {
margin: 0 0 20px 0;
padding: 0 24px;
background: #fff;
border-bottom: 1px solid var(--csm-border-color);
}
</style>