Files
csm/web/src/views/UsbPolicy.vue
iven e99ea53eba feat: 全面重构前端UI及完善后端功能
前端重构:
- 重构Layout为左侧导航+顶栏的现代管理后台布局
- 重构设备管理页面(Devices.vue):左侧分组面板+右侧设备列表
- 重构设备详情(DeviceDetail.vue):集成硬件资产/软件资产/变更记录标签页
- 移除独立资产管理页面,功能合并至设备详情
- 重构Dashboard/登录/设置/告警/水印/上网管控等页面样式
- 新增全局CSS变量和统一样式系统
- 添加分组管理UI:新建/重命名/删除分组,移动设备到分组

后端完善:
- 新增分组CRUD API(groups.rs):创建/重命名/删除分组,设备分组移动
- 客户端硬件采集:完善GPU/主板/序列号/磁盘信息采集(Windows PowerShell)
- 客户端软件采集:通过Windows注册表读取已安装软件列表
- 新增SoftwareAssetReport消息类型(0x09)及处理链路
- 数据库新增upsert_software方法处理软件资产存储
- 服务端推送软件资产配置给新注册设备
- 修复密码修改功能,添加旧密码验证
2026-04-06 13:09:43 +08:00

235 lines
9.0 KiB
Vue

<template>
<div class="page-container">
<el-tabs v-model="activeTab" class="page-tabs">
<el-tab-pane label="策略管理" name="policies">
<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-tab-pane>
<el-tab-pane label="事件日志" name="events">
<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>
<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="520px" destroy-on-close>
<el-form :model="policyForm" label-width="100px">
<el-form-item label="策略名称">
<el-input v-model="policyForm.name" placeholder="输入策略名称" />
</el-form-item>
<el-form-item label="策略类型">
<el-select v-model="policyForm.policy_type" style="width: 100%">
<el-option label="全部拦截" value="all_block" />
<el-option label="白名单" value="whitelist" />
<el-option label="黑名单" value="blacklist" />
</el-select>
</el-form-item>
<el-form-item label="目标分组">
<el-input v-model="policyForm.target_group" placeholder="留空表示全部终端" />
</el-form-item>
<el-form-item label="设备规则">
<el-input v-model="policyForm.rules" type="textarea" :rows="3" placeholder='[{"vendor_id":"1234","product_id":"5678"}]' />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="policyDialogVisible = false">取消</el-button>
<el-button type="primary" @click="savePolicy">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<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')
const policies = ref<any[]>([])
const loading = ref(false)
const policyDialogVisible = ref(false)
const editingPolicy = ref<any>(null)
const policyForm = reactive({ name: '', policy_type: 'all_block', target_group: '', rules: '[]' })
async function fetchPolicies() {
loading.value = true
try {
const data = await api.get<any>('/api/usb/policies')
policies.value = data.policies || []
} catch { /* api.ts handles 401 */ } finally { loading.value = false }
}
function showPolicyDialog(row?: any) {
if (row) {
editingPolicy.value = row
policyForm.name = row.name
policyForm.policy_type = row.policy_type
policyForm.target_group = row.target_group || ''
policyForm.rules = row.rules || '[]'
} else {
editingPolicy.value = null
policyForm.name = ''
policyForm.policy_type = 'all_block'
policyForm.target_group = ''
policyForm.rules = '[]'
}
policyDialogVisible.value = true
}
async function savePolicy() {
try {
if (editingPolicy.value) {
await api.put(`/api/usb/policies/${editingPolicy.value.id}`, policyForm)
ElMessage.success('策略已更新')
} else {
await api.post('/api/usb/policies', policyForm)
ElMessage.success('策略已创建')
}
policyDialogVisible.value = false
fetchPolicies()
} catch (e: any) { ElMessage.error(e.message || '操作失败') }
}
async function togglePolicy(row: any) {
try {
await api.put(`/api/usb/policies/${row.id}`, { enabled: !row.enabled ? 1 : 0 })
fetchPolicies()
} catch { /* ignore */ }
}
async function deletePolicy(id: number) {
await ElMessageBox.confirm('确定删除该策略?', '确认', { type: 'warning' })
try {
await api.delete(`/api/usb/policies/${id}`)
ElMessage.success('策略已删除')
fetchPolicies()
} catch (e: any) { ElMessage.error(e.message || '删除失败') }
}
function policyTypeTag(type: string) {
const map: Record<string, string> = { all_block: 'danger', whitelist: 'success', blacklist: 'warning' }
return map[type] || 'info'
}
function policyTypeLabel(type: string) {
const map: Record<string, string> = { all_block: '全部拦截', whitelist: '白名单', blacklist: '黑名单' }
return map[type] || type
}
const events = ref<any[]>([])
const evLoading = ref(false)
const eventFilter = ref('')
async function fetchEvents() {
evLoading.value = true
try {
const params = new URLSearchParams()
if (eventFilter.value) params.set('event_type', eventFilter.value)
const data = await api.get<any>(`/api/usb/events?${params}`)
events.value = data.events || []
} catch { /* api.ts handles 401 */ } finally { evLoading.value = false }
}
function eventTypeLabel(type: string) {
const map: Record<string, string> = { Inserted: '插入', Removed: '拔出', Blocked: '拦截' }
return map[type] || type
}
onMounted(() => {
fetchPolicies()
fetchEvents()
})
</script>
<style scoped>
.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>