feat: 重构桌面管理模块 - 拆分系统添加和系统管理菜单,支持批量配置Windows账号和删除设备
This commit is contained in:
parent
ca7231ecb9
commit
8acd7b0ab6
@ -341,7 +341,7 @@ export const osDeviceApi = {
|
||||
startScan(networkSegment: string, subnetMask: string) {
|
||||
return request.post({
|
||||
url: '/api/os-devices/scan/start',
|
||||
params: { networkSegment, subnetMask }
|
||||
data: { networkSegment, subnetMask }
|
||||
})
|
||||
},
|
||||
|
||||
@ -352,6 +352,21 @@ export const osDeviceApi = {
|
||||
})
|
||||
},
|
||||
|
||||
// 获取扫描结果(未保存的设备列表)
|
||||
getScanResults(taskId: string) {
|
||||
return request.get<any[]>({
|
||||
url: `/api/os-devices/scan/results/${taskId}`
|
||||
})
|
||||
},
|
||||
|
||||
// 保存选中的设备
|
||||
saveSelectedDevices(taskId: string, selectedIps: string[]) {
|
||||
return request.post({
|
||||
url: '/api/os-devices/scan/save',
|
||||
data: { taskId, selectedIps }
|
||||
})
|
||||
},
|
||||
|
||||
// 取消扫描
|
||||
cancelScan(taskId: string) {
|
||||
return request.post({
|
||||
@ -392,6 +407,14 @@ export const osDeviceApi = {
|
||||
})
|
||||
},
|
||||
|
||||
// 设置 Windows 登录凭据
|
||||
setCredentials(id: number, credentials: { username: string; password: string }) {
|
||||
return request.put({
|
||||
url: `/api/os-devices/${id}/credentials`,
|
||||
data: credentials
|
||||
})
|
||||
},
|
||||
|
||||
// 删除设备
|
||||
delete(id: number) {
|
||||
return request.del({
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>远程桌面</span>
|
||||
<span>系统管理</span>
|
||||
<div class="header-actions">
|
||||
<ElTag v-if="isCheckingStatus" type="info" size="small" style="margin-right: 10px">
|
||||
<el-icon class="is-loading"><Refresh /></el-icon>
|
||||
@ -11,7 +11,7 @@
|
||||
</ElTag>
|
||||
<ElInput
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索 IP 地址"
|
||||
placeholder="搜索 IP / 主机名"
|
||||
style="width: 200px; margin-right: 10px"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
@ -25,40 +25,80 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作工具栏 -->
|
||||
<div class="batch-toolbar">
|
||||
<div class="selection-info">
|
||||
<ElTag v-if="selectedDevices.length > 0" type="primary">
|
||||
已选择 {{ selectedDevices.length }} 台设备
|
||||
</ElTag>
|
||||
<span v-else class="hint-text">请勾选设备进行操作</span>
|
||||
</div>
|
||||
<div class="batch-actions">
|
||||
<ElButton type="info" :disabled="selectedDevices.length === 0" @click="handleBatchSetCredentials">
|
||||
<el-icon><Key /></el-icon>
|
||||
配置Windows账号
|
||||
</ElButton>
|
||||
<ElButton type="danger" :disabled="selectedDevices.length === 0" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElTable :data="devices" v-loading="loading" stripe style="width: 100%">
|
||||
<ElTable
|
||||
:data="filteredDevices"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn type="selection" width="50" />
|
||||
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
|
||||
<ElTableColumn prop="hostname" label="主机名" width="150" />
|
||||
<ElTableColumn label="AMT状态" width="90">
|
||||
<ElTableColumn prop="hostname" label="主机名" width="150">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.amtOnline ? 'success' : 'info'" size="small">
|
||||
{{ row.amtOnline ? '在线' : '离线' }}
|
||||
{{ row.hostname || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作系统" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getOsTagType(row.osType)" size="small">{{ row.osType }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.isOnline ? 'success' : 'danger'" size="small">
|
||||
{{ row.isOnline ? '在线' : '离线' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="系统状态" width="90">
|
||||
<ElTableColumn label="AMT 绑定" width="130">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.osOnline ? 'success' : 'danger'" size="small">
|
||||
{{ row.osOnline ? '运行中' : '已关机' }}
|
||||
</ElTag>
|
||||
<ElTag v-if="row.amtDeviceId" type="success" size="small">{{ row.amtDeviceIp }}</ElTag>
|
||||
<ElTag v-else type="info" size="small">未绑定</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="Windows账号" width="120">
|
||||
<ElTableColumn label="账号" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.windowsUsername" type="success" size="small">{{ row.windowsUsername }}</ElTag>
|
||||
<span v-if="row.windowsUsername">{{ row.windowsUsername }}</span>
|
||||
<ElTag v-else type="warning" size="small">未配置</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="description" label="备注" min-width="150" />
|
||||
<ElTableColumn label="操作" width="380" fixed="right">
|
||||
<ElTableColumn label="密码" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="success" size="small" @click="handleRemoteDesktop(row)" :disabled="!row.osOnline || !row.windowsUsername">
|
||||
<span v-if="row.windowsPassword">{{ row.windowsPassword }}</span>
|
||||
<ElTag v-else type="warning" size="small">未配置</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="360" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="success" size="small" @click="handleRemoteDesktop(row)" :disabled="!row.isOnline">
|
||||
远程桌面
|
||||
</ElButton>
|
||||
<ElButton type="info" size="small" @click="handleSetCredentials(row)">
|
||||
配置账号
|
||||
<ElButton type="info" size="small" @click="handleFetchInfo(row)">
|
||||
获取信息
|
||||
</ElButton>
|
||||
<ElDropdown trigger="click" @command="(cmd: string) => handlePowerCommand(cmd, row)" style="margin-left: 8px">
|
||||
<ElDropdown v-if="row.amtDeviceId" trigger="click" @command="(cmd: string) => handlePowerCommand(cmd, row)" style="margin-left: 8px">
|
||||
<ElButton type="warning" size="small">
|
||||
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</ElButton>
|
||||
@ -75,16 +115,43 @@
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<ElEmpty v-if="!loading && devices.length === 0" description="暂无设备,请先在系统添加中扫描并添加设备" />
|
||||
</ElCard>
|
||||
|
||||
<!-- 远程桌面弹窗 -->
|
||||
<RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" />
|
||||
|
||||
<!-- 获取信息对话框 -->
|
||||
<ElDialog v-model="showFetchDialog" title="获取系统信息" width="400px">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 15px">
|
||||
需要提供 Windows 管理员凭据通过 WMI 获取系统信息
|
||||
</ElAlert>
|
||||
<ElForm :model="fetchForm" label-width="80px">
|
||||
<ElFormItem label="用户名">
|
||||
<ElInput v-model="fetchForm.username" placeholder="administrator" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="密码">
|
||||
<ElInput v-model="fetchForm.password" type="password" show-password />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="showFetchDialog = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="doFetchInfo" :loading="fetching">获取</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 配置 Windows 账号弹窗 -->
|
||||
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="450px">
|
||||
<ElForm :model="credentialsForm" label-width="100px">
|
||||
<ElFormItem label="设备 IP">
|
||||
<ElInput :model-value="selectedDevice?.ipAddress" disabled />
|
||||
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="500px">
|
||||
<div v-if="credentialsTargetDevices.length > 1" class="target-devices-info">
|
||||
<ElAlert type="info" :closable="false" show-icon>
|
||||
将为以下 {{ credentialsTargetDevices.length }} 台设备配置相同的 Windows 账号:
|
||||
<div class="device-list">{{ credentialsTargetDevices.map(d => d.ipAddress).join(', ') }}</div>
|
||||
</ElAlert>
|
||||
</div>
|
||||
<ElForm :model="credentialsForm" label-width="100px" style="margin-top: 16px">
|
||||
<ElFormItem v-if="credentialsTargetDevices.length === 1" label="设备 IP">
|
||||
<ElInput :model-value="credentialsTargetDevices[0]?.ipAddress" disabled />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="用户名">
|
||||
<ElInput v-model="credentialsForm.username" placeholder="Windows 登录用户名" />
|
||||
@ -102,26 +169,53 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose } from '@element-plus/icons-vue'
|
||||
import { deviceApi, powerApi } from '@/api/amt'
|
||||
import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose, Delete, Key } from '@element-plus/icons-vue'
|
||||
import { osDeviceApi, powerApi } from '@/api/amt'
|
||||
import RemoteDesktopModal from '@/views/amt/modules/remote-desktop-modal.vue'
|
||||
|
||||
defineOptions({ name: 'DesktopManageDevices' })
|
||||
|
||||
const devices = ref<any[]>([])
|
||||
const selectedDevices = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const isCheckingStatus = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const showRemoteDesktopModal = ref(false)
|
||||
const showCredentialsDialog = ref(false)
|
||||
const selectedDevice = ref<any>(null)
|
||||
|
||||
// 获取信息相关
|
||||
const showFetchDialog = ref(false)
|
||||
const fetching = ref(false)
|
||||
const fetchForm = ref({ username: '', password: '' })
|
||||
let currentFetchDevice: any = null
|
||||
|
||||
// 配置账号相关
|
||||
const showCredentialsDialog = ref(false)
|
||||
const credentialsTargetDevices = ref<any[]>([])
|
||||
const credentialsForm = ref({ username: '', password: '' })
|
||||
const savingCredentials = ref(false)
|
||||
|
||||
let statusCheckInterval: number | null = null
|
||||
|
||||
const filteredDevices = computed(() => {
|
||||
if (!searchKeyword.value) return devices.value
|
||||
const kw = searchKeyword.value.toLowerCase()
|
||||
return devices.value.filter(d =>
|
||||
d.ipAddress?.toLowerCase().includes(kw) ||
|
||||
d.hostname?.toLowerCase().includes(kw)
|
||||
)
|
||||
})
|
||||
|
||||
const getOsTagType = (osType: string) => {
|
||||
switch (osType) {
|
||||
case 'Windows': return 'primary'
|
||||
case 'Linux': return 'success'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDevices()
|
||||
startStatusCheck()
|
||||
@ -134,7 +228,7 @@ onUnmounted(() => {
|
||||
const fetchDevices = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
devices.value = await deviceApi.getAllDevices()
|
||||
devices.value = await osDeviceApi.getAll()
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
} finally {
|
||||
@ -142,78 +236,106 @@ const fetchDevices = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (searchKeyword.value) {
|
||||
loading.value = true
|
||||
try {
|
||||
devices.value = await deviceApi.searchDevices(searchKeyword.value)
|
||||
} catch (error) {
|
||||
console.error('搜索设备失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
fetchDevices()
|
||||
}
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedDevices.value = selection
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
// 前端过滤,不需要请求后端
|
||||
}
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await fetchDevices()
|
||||
await checkAllDevicesStatus()
|
||||
ElMessage.success('刷新成功')
|
||||
}
|
||||
|
||||
const checkAllDevicesStatus = async () => {
|
||||
if (isCheckingStatus.value || devices.value.length === 0) return
|
||||
isCheckingStatus.value = true
|
||||
try {
|
||||
const statusList = await deviceApi.checkAllDevicesStatus()
|
||||
const statusMap = new Map(statusList.map((s: any) => [s.id, { amtOnline: s.amtOnline, osOnline: s.osOnline }]))
|
||||
devices.value.forEach(device => {
|
||||
if (statusMap.has(device.id)) {
|
||||
const status = statusMap.get(device.id)
|
||||
device.amtOnline = status.amtOnline
|
||||
device.osOnline = status.osOnline
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('检测设备状态失败:', error)
|
||||
} finally {
|
||||
isCheckingStatus.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startStatusCheck = () => {
|
||||
checkAllDevicesStatus()
|
||||
statusCheckInterval = window.setInterval(() => checkAllDevicesStatus(), 30000)
|
||||
statusCheckInterval = window.setInterval(() => {
|
||||
// 可以添加状态检查逻辑
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
const stopStatusCheck = () => {
|
||||
if (statusCheckInterval) { clearInterval(statusCheckInterval); statusCheckInterval = null }
|
||||
if (statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval)
|
||||
statusCheckInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoteDesktop = (device: any) => {
|
||||
if (!device.osOnline) { ElMessage.warning('设备操作系统未运行,无法连接远程桌面'); return }
|
||||
if (!device.windowsUsername) { ElMessage.warning('请先配置该设备的 Windows 登录账号'); return }
|
||||
selectedDevice.value = device
|
||||
if (!device.isOnline) {
|
||||
ElMessage.warning('设备离线,无法连接远程桌面')
|
||||
return
|
||||
}
|
||||
selectedDevice.value = {
|
||||
id: device.amtDeviceId || device.id,
|
||||
ipAddress: device.ipAddress,
|
||||
hostname: device.hostname,
|
||||
windowsUsername: device.loggedInUser
|
||||
}
|
||||
showRemoteDesktopModal.value = true
|
||||
}
|
||||
|
||||
const handleSetCredentials = async (device: any) => {
|
||||
selectedDevice.value = device
|
||||
credentialsForm.value = { username: device.windowsUsername || '', password: '' }
|
||||
const handleFetchInfo = (row: any) => {
|
||||
currentFetchDevice = row
|
||||
fetchForm.value = { username: '', password: '' }
|
||||
showFetchDialog.value = true
|
||||
}
|
||||
|
||||
const doFetchInfo = async () => {
|
||||
if (!fetchForm.value.username || !fetchForm.value.password) {
|
||||
ElMessage.warning('请输入凭据')
|
||||
return
|
||||
}
|
||||
fetching.value = true
|
||||
try {
|
||||
await osDeviceApi.fetchInfo(currentFetchDevice.id, fetchForm.value)
|
||||
showFetchDialog.value = false
|
||||
fetchDevices()
|
||||
ElMessage.success('系统信息已更新')
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取信息失败')
|
||||
} finally {
|
||||
fetching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量配置 Windows 账号
|
||||
const handleBatchSetCredentials = () => {
|
||||
if (selectedDevices.value.length === 0) return
|
||||
credentialsTargetDevices.value = [...selectedDevices.value]
|
||||
credentialsForm.value = { username: 'administrator', password: '' }
|
||||
showCredentialsDialog.value = true
|
||||
}
|
||||
|
||||
const saveCredentials = async () => {
|
||||
if (!credentialsForm.value.username) { ElMessage.warning('请输入用户名'); return }
|
||||
if (!credentialsForm.value.username) {
|
||||
ElMessage.warning('请输入用户名')
|
||||
return
|
||||
}
|
||||
if (!credentialsForm.value.password) {
|
||||
ElMessage.warning('请输入密码')
|
||||
return
|
||||
}
|
||||
|
||||
savingCredentials.value = true
|
||||
try {
|
||||
await deviceApi.setDeviceCredentials(selectedDevice.value.id, credentialsForm.value)
|
||||
const device = devices.value.find(d => d.id === selectedDevice.value.id)
|
||||
if (device) device.windowsUsername = credentialsForm.value.username
|
||||
let successCount = 0
|
||||
for (const device of credentialsTargetDevices.value) {
|
||||
try {
|
||||
await osDeviceApi.setCredentials(device.id, credentialsForm.value)
|
||||
successCount++
|
||||
} catch (e) {
|
||||
console.error('配置Windows账号失败:', device.ipAddress, e)
|
||||
}
|
||||
}
|
||||
|
||||
showCredentialsDialog.value = false
|
||||
ElMessage.success('账号配置成功')
|
||||
if (successCount === credentialsTargetDevices.value.length) {
|
||||
ElMessage.success('Windows账号配置成功')
|
||||
} else {
|
||||
ElMessage.warning(`成功配置 ${successCount} 台,失败 ${credentialsTargetDevices.value.length - successCount} 台`)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
@ -221,7 +343,42 @@ const saveCredentials = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedDevices.value.length === 0) return
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除选中的 ${selectedDevices.value.length} 台设备吗?`,
|
||||
'删除确认',
|
||||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
|
||||
let successCount = 0
|
||||
for (const device of selectedDevices.value) {
|
||||
try {
|
||||
await osDeviceApi.delete(device.id)
|
||||
successCount++
|
||||
} catch (e) {
|
||||
console.error('删除设备失败:', device.id, e)
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success(`成功删除 ${successCount} 台设备`)
|
||||
fetchDevices()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePowerCommand = async (command: string, device: any) => {
|
||||
if (!device.amtDeviceId) {
|
||||
ElMessage.warning('该设备未绑定 AMT,无法进行电源管理')
|
||||
return
|
||||
}
|
||||
|
||||
const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = {
|
||||
'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' },
|
||||
'power-off': { api: powerApi.powerOff, name: '关机', confirmMsg: '确定要关机吗?' },
|
||||
@ -231,15 +388,15 @@ const handlePowerCommand = async (command: string, device: any) => {
|
||||
}
|
||||
const action = actionMap[command]
|
||||
if (!action) return
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(`设备: ${device.ipAddress}\n${action.confirmMsg}`, `确认${action.name}`, {
|
||||
confirmButtonText: '确定', cancelButtonText: '取消', type: command.includes('force') ? 'warning' : 'info'
|
||||
})
|
||||
ElMessage.info(`正在执行${action.name}...`)
|
||||
const response = await action.api(device.id)
|
||||
const response = await action.api(device.amtDeviceId)
|
||||
if (response.success) {
|
||||
ElMessage.success(response.message || `${action.name}命令已发送`)
|
||||
setTimeout(() => checkAllDevicesStatus(), 3000)
|
||||
} else {
|
||||
ElMessage.error(response.error || `${action.name}失败`)
|
||||
}
|
||||
@ -253,4 +410,10 @@ const handlePowerCommand = async (command: string, device: any) => {
|
||||
.devices-page { padding: 0; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: 500; }
|
||||
.header-actions { display: flex; align-items: center; }
|
||||
.batch-toolbar { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; margin-bottom: 16px; background: #f5f7fa; border-radius: 4px; }
|
||||
.selection-info { display: flex; align-items: center; }
|
||||
.hint-text { color: #909399; font-size: 14px; }
|
||||
.batch-actions { display: flex; gap: 10px; }
|
||||
.target-devices-info { margin-bottom: 16px; }
|
||||
.device-list { margin-top: 8px; font-family: monospace; font-size: 12px; word-break: break-all; }
|
||||
</style>
|
||||
|
||||
@ -1,174 +1,121 @@
|
||||
<template>
|
||||
<div class="os-devices-page">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<div class="left">
|
||||
<ElButton type="primary" @click="showScanDialog = true">
|
||||
<ElIcon><Search /></ElIcon>
|
||||
扫描操作系统
|
||||
</ElButton>
|
||||
<ElButton @click="handleAutoBind" :loading="autoBinding">
|
||||
<ElIcon><Link /></ElIcon>
|
||||
自动绑定 AMT
|
||||
</ElButton>
|
||||
<ElButton @click="loadDevices" :loading="loading">
|
||||
<ElIcon><Refresh /></ElIcon>
|
||||
刷新
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="right">
|
||||
<ElInput v-model="searchKeyword" placeholder="搜索 IP/主机名" clearable style="width: 200px" />
|
||||
</div>
|
||||
</div>
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>系统添加</span>
|
||||
<div class="header-actions">
|
||||
<ElButton type="primary" @click="showScanDialog = true">
|
||||
<ElIcon><Search /></ElIcon>
|
||||
扫描网络
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 设备列表 -->
|
||||
<ElTable :data="filteredDevices" v-loading="loading" stripe>
|
||||
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
|
||||
<ElTableColumn prop="hostname" label="主机名" width="150" />
|
||||
<ElTableColumn label="操作系统" width="200">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getOsTagType(row.osType)" size="small">{{ row.osType }}</ElTag>
|
||||
<span v-if="row.osVersion" style="margin-left: 5px; font-size: 12px; color: #999">
|
||||
{{ row.osVersion?.substring(0, 30) }}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="systemUuid" label="UUID" width="280">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.systemUuid" style="font-family: monospace; font-size: 11px">{{ row.systemUuid }}</span>
|
||||
<ElTag v-else type="warning" size="small">未获取</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="AMT 绑定" width="150">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.amtDeviceId" type="success" size="small">
|
||||
{{ row.amtDeviceIp }}
|
||||
</ElTag>
|
||||
<ElTag v-else type="info" size="small">未绑定</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.isOnline ? 'success' : 'danger'" size="small">
|
||||
{{ row.isOnline ? '在线' : '离线' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton size="small" @click="handleFetchInfo(row)">获取信息</ElButton>
|
||||
<ElButton size="small" @click="handleBindAmt(row)" v-if="!row.amtDeviceId">绑定 AMT</ElButton>
|
||||
<ElButton size="small" type="warning" @click="handleUnbindAmt(row)" v-else>解绑</ElButton>
|
||||
<ElButton size="small" type="danger" @click="handleDelete(row)">删除</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<!-- 扫描结果列表 -->
|
||||
<div v-if="scanResults.length > 0" class="scan-results">
|
||||
<div class="results-header">
|
||||
<span>发现 {{ scanResults.length }} 台设备,请选择要添加的设备:</span>
|
||||
<div>
|
||||
<ElButton size="small" @click="selectAll">全选</ElButton>
|
||||
<ElButton size="small" @click="deselectAll">取消全选</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElTable :data="scanResults" @selection-change="handleSelectionChange" ref="tableRef">
|
||||
<ElTableColumn type="selection" width="55" />
|
||||
<ElTableColumn prop="ipAddress" label="IP 地址" width="150" />
|
||||
<ElTableColumn label="操作系统" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getOsTagType(row.osType)" size="small">{{ row.osType }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="hostname" label="主机名" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ row.hostname || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.isOnline ? 'success' : 'danger'" size="small">
|
||||
{{ row.isOnline ? '在线' : '离线' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="discoveredAt" label="发现时间">
|
||||
<template #default="{ row }">
|
||||
{{ formatTime(row.discoveredAt) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="results-footer">
|
||||
<span>已选择 {{ selectedDevices.length }} 台设备</span>
|
||||
<div>
|
||||
<ElButton @click="clearResults">取消</ElButton>
|
||||
<ElButton type="primary" @click="saveSelectedDevices" :loading="saving" :disabled="selectedDevices.length === 0">
|
||||
添加选中设备
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<ElEmpty v-else description="点击扫描网络发现局域网内的设备" />
|
||||
</ElCard>
|
||||
|
||||
<!-- 扫描对话框 -->
|
||||
<ElDialog v-model="showScanDialog" title="扫描操作系统" width="500px">
|
||||
<ElDialog v-model="showScanDialog" title="扫描网络" width="500px" :close-on-click-modal="false">
|
||||
<ElForm :model="scanForm" label-width="100px">
|
||||
<ElFormItem label="网段">
|
||||
<ElInput v-model="scanForm.networkSegment" placeholder="例如: 192.168.1.0" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="子网掩码">
|
||||
<ElSelect v-model="scanForm.subnetMask" style="width: 100%">
|
||||
<ElOption label="/24 (255.255.255.0)" value="/24" />
|
||||
<ElOption label="/16 (255.255.0.0)" value="/16" />
|
||||
<ElOption label="/8 (255.0.0.0)" value="/8" />
|
||||
<ElOption label="/24 (255.255.255.0) - 254 台主机" value="/24" />
|
||||
<ElOption label="/23 (255.255.254.0) - 510 台主机" value="/23" />
|
||||
<ElOption label="/22 (255.255.252.0) - 1022 台主机" value="/22" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div v-if="scanning" class="scan-progress">
|
||||
<ElProgress :percentage="scanProgress.progressPercentage" :format="() => `${scanProgress.scannedCount}/${scanProgress.totalCount}`" />
|
||||
<p>当前扫描: {{ scanProgress.currentIp }}</p>
|
||||
<ElProgress :percentage="scanProgress.progressPercentage" :format="formatProgress" />
|
||||
<p>当前扫描: {{ scanProgress.currentIp || '-' }}</p>
|
||||
<p>已发现: {{ scanProgress.foundDevices }} 台设备</p>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ElButton @click="showScanDialog = false" :disabled="scanning">取消</ElButton>
|
||||
<ElButton type="primary" @click="startScan" :loading="scanning">
|
||||
<ElButton @click="cancelScan" v-if="scanning">取消扫描</ElButton>
|
||||
<ElButton @click="showScanDialog = false" v-else>关闭</ElButton>
|
||||
<ElButton type="primary" @click="startScan" :loading="scanning" :disabled="scanning">
|
||||
{{ scanning ? '扫描中...' : '开始扫描' }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 获取信息对话框 -->
|
||||
<ElDialog v-model="showFetchDialog" title="获取系统信息" width="400px">
|
||||
<ElAlert type="info" :closable="false" style="margin-bottom: 15px">
|
||||
需要提供 Windows 管理员凭据通过 WMI 获取系统信息
|
||||
</ElAlert>
|
||||
<ElForm :model="fetchForm" label-width="80px">
|
||||
<ElFormItem label="用户名">
|
||||
<ElInput v-model="fetchForm.username" placeholder="administrator" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="密码">
|
||||
<ElInput v-model="fetchForm.password" type="password" show-password />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="showFetchDialog = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="doFetchInfo" :loading="fetching">获取</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 绑定 AMT 对话框 -->
|
||||
<ElDialog v-model="showBindDialog" title="绑定 AMT 设备" width="500px">
|
||||
<ElTable :data="amtDevices" v-loading="loadingAmt" @row-click="selectAmtDevice" highlight-current-row>
|
||||
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
|
||||
<ElTableColumn prop="hostname" label="主机名" />
|
||||
<ElTableColumn prop="systemUuid" label="UUID" width="280">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.systemUuid" style="font-family: monospace; font-size: 11px">{{ row.systemUuid }}</span>
|
||||
<ElTag v-else type="warning" size="small">未获取</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
<template #footer>
|
||||
<ElButton @click="showBindDialog = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="doBind" :disabled="!selectedAmtDevice">绑定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Link } from '@element-plus/icons-vue'
|
||||
import { osDeviceApi, deviceApi } from '@/api/amt'
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { osDeviceApi } from '@/api/amt'
|
||||
|
||||
const loading = ref(false)
|
||||
const devices = ref<any[]>([])
|
||||
const searchKeyword = ref('')
|
||||
const autoBinding = ref(false)
|
||||
defineOptions({ name: 'OsDevices' })
|
||||
|
||||
const tableRef = ref()
|
||||
const scanResults = ref<any[]>([])
|
||||
const selectedDevices = ref<any[]>([])
|
||||
const saving = ref(false)
|
||||
|
||||
// 扫描相关
|
||||
const showScanDialog = ref(false)
|
||||
const scanning = ref(false)
|
||||
const scanForm = ref({ networkSegment: '192.168.1.0', subnetMask: '/24' })
|
||||
const scanProgress = ref({ scannedCount: 0, totalCount: 0, foundDevices: 0, progressPercentage: 0, currentIp: '' })
|
||||
let scanTaskId = ''
|
||||
|
||||
// 获取信息相关
|
||||
const showFetchDialog = ref(false)
|
||||
const fetching = ref(false)
|
||||
const fetchForm = ref({ username: '', password: '' })
|
||||
let currentFetchDevice: any = null
|
||||
|
||||
// 绑定相关
|
||||
const showBindDialog = ref(false)
|
||||
const loadingAmt = ref(false)
|
||||
const amtDevices = ref<any[]>([])
|
||||
const selectedAmtDevice = ref<any>(null)
|
||||
let currentBindDevice: any = null
|
||||
|
||||
const filteredDevices = computed(() => {
|
||||
if (!searchKeyword.value) return devices.value
|
||||
const kw = searchKeyword.value.toLowerCase()
|
||||
return devices.value.filter(d =>
|
||||
d.ipAddress?.toLowerCase().includes(kw) ||
|
||||
d.hostname?.toLowerCase().includes(kw)
|
||||
)
|
||||
})
|
||||
let currentTaskId = ''
|
||||
|
||||
const getOsTagType = (osType: string) => {
|
||||
switch (osType) {
|
||||
@ -178,57 +125,66 @@ const getOsTagType = (osType: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadDevices = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
devices.value = await osDeviceApi.getAll()
|
||||
} catch (error) {
|
||||
console.error('加载设备失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
const formatTime = (time: string) => {
|
||||
if (!time) return '-'
|
||||
return new Date(time).toLocaleString()
|
||||
}
|
||||
|
||||
const formatProgress = () => {
|
||||
return scanProgress.value.scannedCount + '/' + scanProgress.value.totalCount
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedDevices.value = selection
|
||||
}
|
||||
|
||||
const selectAll = () => {
|
||||
tableRef.value?.toggleAllSelection()
|
||||
}
|
||||
|
||||
const deselectAll = () => {
|
||||
tableRef.value?.clearSelection()
|
||||
}
|
||||
|
||||
const startScan = async () => {
|
||||
scanning.value = true
|
||||
scanProgress.value = { scannedCount: 0, totalCount: 0, foundDevices: 0, progressPercentage: 0, currentIp: '' }
|
||||
scanResults.value = []
|
||||
selectedDevices.value = []
|
||||
|
||||
try {
|
||||
const result = await osDeviceApi.startScan(scanForm.value.networkSegment, scanForm.value.subnetMask)
|
||||
scanTaskId = result.taskId
|
||||
|
||||
let retryCount = 0
|
||||
const maxRetries = 3
|
||||
currentTaskId = result.taskId
|
||||
|
||||
// 轮询扫描进度
|
||||
const pollProgress = async () => {
|
||||
if (!scanning.value) return
|
||||
try {
|
||||
const progress = await osDeviceApi.getScanStatus(scanTaskId)
|
||||
retryCount = 0 // 成功后重置重试计数
|
||||
const progress = await osDeviceApi.getScanStatus(currentTaskId)
|
||||
|
||||
// -1 表示任务不存在,停止轮询
|
||||
if (progress.progressPercentage === -1) {
|
||||
scanning.value = false
|
||||
ElMessage.warning('扫描任务已失效')
|
||||
return
|
||||
}
|
||||
|
||||
scanProgress.value = progress
|
||||
|
||||
if (progress.progressPercentage < 100) {
|
||||
setTimeout(pollProgress, 500)
|
||||
} else {
|
||||
// 扫描完成,获取结果
|
||||
scanning.value = false
|
||||
showScanDialog.value = false
|
||||
ElMessage.success(`扫描完成,发现 ${progress.foundDevices} 台设备`)
|
||||
loadDevices()
|
||||
await loadScanResults()
|
||||
}
|
||||
} catch (error) {
|
||||
retryCount++
|
||||
if (retryCount < maxRetries) {
|
||||
// 重试,增加延迟
|
||||
setTimeout(pollProgress, 1000)
|
||||
} else {
|
||||
scanning.value = false
|
||||
ElMessage.error('获取扫描进度失败')
|
||||
}
|
||||
scanning.value = false
|
||||
ElMessage.error('获取扫描进度失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 延迟500ms后开始轮询,给后端一点时间注册任务
|
||||
setTimeout(pollProgress, 500)
|
||||
} catch (error) {
|
||||
scanning.value = false
|
||||
@ -236,95 +192,63 @@ const startScan = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAutoBind = async () => {
|
||||
autoBinding.value = true
|
||||
const cancelScan = async () => {
|
||||
try {
|
||||
await osDeviceApi.autoBind()
|
||||
loadDevices()
|
||||
} finally {
|
||||
autoBinding.value = false
|
||||
await osDeviceApi.cancelScan(currentTaskId)
|
||||
scanning.value = false
|
||||
ElMessage.info('扫描已取消')
|
||||
} catch (error) {
|
||||
ElMessage.error('取消扫描失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFetchInfo = (row: any) => {
|
||||
currentFetchDevice = row
|
||||
fetchForm.value = { username: '', password: '' }
|
||||
showFetchDialog.value = true
|
||||
const loadScanResults = async () => {
|
||||
try {
|
||||
const results = await osDeviceApi.getScanResults(currentTaskId)
|
||||
scanResults.value = results
|
||||
if (results.length === 0) {
|
||||
ElMessage.info('未发现任何设备')
|
||||
} else {
|
||||
ElMessage.success(`发现 ${results.length} 台设备,请选择要添加的设备`)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取扫描结果失败')
|
||||
}
|
||||
}
|
||||
|
||||
const doFetchInfo = async () => {
|
||||
if (!fetchForm.value.username || !fetchForm.value.password) {
|
||||
ElMessage.warning('请输入凭据')
|
||||
const saveSelectedDevices = async () => {
|
||||
if (selectedDevices.value.length === 0) {
|
||||
ElMessage.warning('请选择要添加的设备')
|
||||
return
|
||||
}
|
||||
fetching.value = true
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
await osDeviceApi.fetchInfo(currentFetchDevice.id, fetchForm.value)
|
||||
showFetchDialog.value = false
|
||||
loadDevices()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取信息失败')
|
||||
const selectedIps = selectedDevices.value.map(d => d.ipAddress)
|
||||
const result = await osDeviceApi.saveSelectedDevices(currentTaskId, selectedIps)
|
||||
ElMessage.success(result.message || `成功添加 ${result.savedCount} 台设备`)
|
||||
clearResults()
|
||||
} catch (error) {
|
||||
ElMessage.error('添加设备失败')
|
||||
} finally {
|
||||
fetching.value = false
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleBindAmt = async (row: any) => {
|
||||
currentBindDevice = row
|
||||
selectedAmtDevice.value = null
|
||||
showBindDialog.value = true
|
||||
loadingAmt.value = true
|
||||
try {
|
||||
amtDevices.value = await deviceApi.getAllDevices()
|
||||
} finally {
|
||||
loadingAmt.value = false
|
||||
}
|
||||
const clearResults = () => {
|
||||
scanResults.value = []
|
||||
selectedDevices.value = []
|
||||
currentTaskId = ''
|
||||
}
|
||||
|
||||
const selectAmtDevice = (row: any) => {
|
||||
selectedAmtDevice.value = row
|
||||
}
|
||||
|
||||
const doBind = async () => {
|
||||
if (!selectedAmtDevice.value) return
|
||||
try {
|
||||
await osDeviceApi.bindAmt(currentBindDevice.id, selectedAmtDevice.value.id)
|
||||
showBindDialog.value = false
|
||||
loadDevices()
|
||||
} catch (error) {
|
||||
ElMessage.error('绑定失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUnbindAmt = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要解除 AMT 绑定吗?', '确认')
|
||||
await osDeviceApi.unbindAmt(row.id)
|
||||
loadDevices()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error('解绑失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除此设备吗?', '确认删除', { type: 'warning' })
|
||||
await osDeviceApi.delete(row.id)
|
||||
loadDevices()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDevices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.os-devices-page { padding: 20px; }
|
||||
.toolbar { display: flex; justify-content: space-between; margin-bottom: 20px; }
|
||||
.toolbar .left { display: flex; gap: 10px; }
|
||||
.os-devices-page { padding: 0; }
|
||||
.card-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: 500; }
|
||||
.header-actions { display: flex; gap: 10px; }
|
||||
.scan-results { }
|
||||
.results-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
|
||||
.results-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; }
|
||||
.scan-progress { margin-top: 20px; text-align: center; }
|
||||
.scan-progress p { margin: 10px 0; color: #666; }
|
||||
</style>
|
||||
|
||||
@ -52,6 +52,8 @@ public class OsDevicesController : ControllerBase
|
||||
DiscoveredAt = o.DiscoveredAt,
|
||||
LastUpdatedAt = o.LastUpdatedAt,
|
||||
Description = o.Description,
|
||||
WindowsUsername = o.WindowsUsername,
|
||||
WindowsPassword = o.WindowsPassword,
|
||||
AmtDeviceId = o.AmtDeviceId,
|
||||
AmtDeviceIp = o.AmtDevice != null ? o.AmtDevice.IpAddress : null
|
||||
})
|
||||
@ -104,16 +106,28 @@ public class OsDevicesController : ControllerBase
|
||||
{
|
||||
var taskId = Guid.NewGuid().ToString("N");
|
||||
|
||||
var progress = new Progress<OsScanProgress>(p =>
|
||||
// 初始化进度为 0%
|
||||
_scanProgress[taskId] = new OsScanProgress
|
||||
{
|
||||
TaskId = taskId,
|
||||
ScannedCount = 0,
|
||||
TotalCount = 1, // 避免除以0
|
||||
FoundDevices = 0,
|
||||
ProgressPercentage = 0,
|
||||
CurrentIp = "初始化中..."
|
||||
};
|
||||
|
||||
// 使用 Action 回调直接更新进度
|
||||
Action<OsScanProgress> progressCallback = p =>
|
||||
{
|
||||
_scanProgress[taskId] = p;
|
||||
});
|
||||
};
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _scannerService.ScanNetworkAsync(taskId, request.NetworkSegment, request.SubnetMask, progress);
|
||||
await _scannerService.ScanNetworkAsync(taskId, request.NetworkSegment, request.SubnetMask, progressCallback);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -138,7 +152,16 @@ public class OsDevicesController : ControllerBase
|
||||
{
|
||||
return Ok(ApiResponse<OsScanProgress>.Success(progress));
|
||||
}
|
||||
return Ok(ApiResponse<OsScanProgress>.Fail(404, "扫描任务不存在"));
|
||||
// 任务不存在时返回 -1 表示任务不存在,前端应该停止轮询
|
||||
return Ok(ApiResponse<OsScanProgress>.Success(new OsScanProgress
|
||||
{
|
||||
TaskId = taskId,
|
||||
ScannedCount = 0,
|
||||
TotalCount = 0,
|
||||
FoundDevices = 0,
|
||||
ProgressPercentage = -1, // -1 表示任务不存在
|
||||
CurrentIp = null
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -151,6 +174,52 @@ public class OsDevicesController : ControllerBase
|
||||
return Ok(ApiResponse<object>.Success(null, "扫描已取消"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取扫描发现的设备列表(未保存到数据库)
|
||||
/// </summary>
|
||||
[HttpGet("scan/results/{taskId}")]
|
||||
public ActionResult<ApiResponse<List<ScanResultDto>>> GetScanResults(string taskId)
|
||||
{
|
||||
_logger.LogInformation("Getting scan results for task: {TaskId}", taskId);
|
||||
var devices = _scannerService.GetScanResults(taskId);
|
||||
_logger.LogInformation("Found {Count} devices in scan results for task: {TaskId}", devices.Count, taskId);
|
||||
|
||||
var results = devices.Select(d => new ScanResultDto
|
||||
{
|
||||
IpAddress = d.IpAddress,
|
||||
OsType = d.OsType.ToString(),
|
||||
Hostname = d.Hostname,
|
||||
IsOnline = d.IsOnline,
|
||||
DiscoveredAt = d.DiscoveredAt
|
||||
}).ToList();
|
||||
|
||||
return Ok(ApiResponse<List<ScanResultDto>>.Success(results));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存选中的设备到数据库
|
||||
/// </summary>
|
||||
[HttpPost("scan/save")]
|
||||
public async Task<ActionResult<ApiResponse<SaveDevicesResponse>>> SaveSelectedDevices(
|
||||
[FromBody] SaveDevicesRequest request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(request.TaskId) || request.SelectedIps == null || request.SelectedIps.Count == 0)
|
||||
{
|
||||
return Ok(ApiResponse<SaveDevicesResponse>.Fail(400, "请选择要添加的设备"));
|
||||
}
|
||||
|
||||
var savedCount = await _scannerService.SaveSelectedDevicesAsync(request.TaskId, request.SelectedIps);
|
||||
|
||||
// 清除扫描结果
|
||||
_scannerService.ClearScanResults(request.TaskId);
|
||||
|
||||
return Ok(ApiResponse<SaveDevicesResponse>.Success(new SaveDevicesResponse
|
||||
{
|
||||
SavedCount = savedCount,
|
||||
Message = $"成功添加 {savedCount} 台设备"
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取设备详细信息(通过 WMI)
|
||||
/// </summary>
|
||||
@ -266,6 +335,24 @@ public class OsDevicesController : ControllerBase
|
||||
return Ok(ApiResponse<object>.Success(null, "自动绑定完成"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Windows 登录凭据
|
||||
/// </summary>
|
||||
[HttpPut("{id}/credentials")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SetCredentials(long id, [FromBody] WindowsCredentialsRequest request)
|
||||
{
|
||||
var device = await _context.OsDevices.FindAsync(id);
|
||||
if (device == null)
|
||||
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
|
||||
|
||||
device.WindowsUsername = request.Username;
|
||||
device.WindowsPassword = request.Password; // TODO: 加密存储
|
||||
device.LastUpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return Ok(ApiResponse<object>.Success(null, "凭据已保存"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除设备
|
||||
/// </summary>
|
||||
@ -300,6 +387,8 @@ public class OsDeviceDto
|
||||
public DateTime DiscoveredAt { get; set; }
|
||||
public DateTime LastUpdatedAt { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? WindowsUsername { get; set; }
|
||||
public string? WindowsPassword { get; set; }
|
||||
public long? AmtDeviceId { get; set; }
|
||||
public string? AmtDeviceIp { get; set; }
|
||||
}
|
||||
@ -321,3 +410,30 @@ public class OsScanRequest
|
||||
public string NetworkSegment { get; set; } = string.Empty;
|
||||
public string SubnetMask { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ScanResultDto
|
||||
{
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
public string OsType { get; set; } = string.Empty;
|
||||
public string? Hostname { get; set; }
|
||||
public bool IsOnline { get; set; }
|
||||
public DateTime DiscoveredAt { get; set; }
|
||||
}
|
||||
|
||||
public class SaveDevicesRequest
|
||||
{
|
||||
public string TaskId { get; set; } = string.Empty;
|
||||
public List<string> SelectedIps { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SaveDevicesResponse
|
||||
{
|
||||
public int SavedCount { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class WindowsCredentialsRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -115,7 +115,8 @@ public static class DbSeeder
|
||||
|
||||
// 桌面管理菜单(系统内置)
|
||||
new() { Id = 20, Name = "DesktopManage", Path = "/desktop-manage", Component = "/index/index", Title = "桌面管理", Icon = "ri:remote-control-line", Sort = 4, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
new() { Id = 21, ParentId = 20, Name = "DesktopDevices", Path = "devices", Component = "/desktop-manage/devices", Title = "远程桌面", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
new() { Id = 21, ParentId = 20, Name = "OsDevices", Path = "os-devices", Component = "/desktop-manage/os-devices", Title = "系统添加", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
new() { Id = 22, ParentId = 20, Name = "DesktopDevices", Path = "devices", Component = "/desktop-manage/devices", Title = "系统管理", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
|
||||
// 系统管理菜单(系统内置)
|
||||
new() { Id = 10, Name = "System", Path = "/system", Component = "/index/index", Title = "menus.system.title", Icon = "ri:user-3-line", Sort = 99, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
|
||||
@ -82,6 +82,16 @@ public class OsDevice
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Windows 登录用户名
|
||||
/// </summary>
|
||||
public string? WindowsUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Windows 登录密码(加密存储)
|
||||
/// </summary>
|
||||
public string? WindowsPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 关联的 AMT 设备 ID
|
||||
/// </summary>
|
||||
|
||||
@ -125,8 +125,13 @@ public class AmtPowerService : IAmtPowerService
|
||||
var returnValue = outputObject.GetProperty("ReturnValue");
|
||||
|
||||
var returnCode = Convert.ToUInt32(returnValue.ToString());
|
||||
_logger.LogInformation("电源操作返回码: {ReturnCode} for {Ip}", returnCode, ipAddress);
|
||||
|
||||
if (returnCode == 0)
|
||||
// AMT 返回码说明:
|
||||
// 0 = 成功完成
|
||||
// 4096 = 作业已启动(异步操作,也是成功)
|
||||
// 2 = 操作不允许
|
||||
if (returnCode == 0 || returnCode == 4096)
|
||||
{
|
||||
result.Success = true;
|
||||
result.Message = GetActionSuccessMessage(action);
|
||||
|
||||
@ -11,11 +11,14 @@ namespace AmtScanner.Api.Services;
|
||||
public interface IWindowsScannerService
|
||||
{
|
||||
Task<List<OsDevice>> ScanNetworkAsync(string taskId, string networkSegment, string subnetMask,
|
||||
IProgress<OsScanProgress> progress, CancellationToken cancellationToken = default);
|
||||
Action<OsScanProgress> progressCallback, CancellationToken cancellationToken = default);
|
||||
Task<OsDevice?> GetOsInfoAsync(string ipAddress, string username, string password);
|
||||
Task<string?> GetSystemUuidAsync(string ipAddress, string username, string password);
|
||||
Task BindAmtDevicesAsync();
|
||||
void CancelScan(string taskId);
|
||||
List<OsDevice> GetScanResults(string taskId);
|
||||
void ClearScanResults(string taskId);
|
||||
Task<int> SaveSelectedDevicesAsync(string taskId, List<string> selectedIps);
|
||||
}
|
||||
|
||||
public class WindowsScannerService : IWindowsScannerService
|
||||
@ -24,6 +27,8 @@ public class WindowsScannerService : IWindowsScannerService
|
||||
private readonly ILogger<WindowsScannerService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationTokens = new();
|
||||
// 存储扫描发现的设备(未保存到数据库)
|
||||
private static readonly ConcurrentDictionary<string, List<OsDevice>> _scanResults = new();
|
||||
|
||||
public WindowsScannerService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
@ -39,17 +44,21 @@ public class WindowsScannerService : IWindowsScannerService
|
||||
string taskId,
|
||||
string networkSegment,
|
||||
string subnetMask,
|
||||
IProgress<OsScanProgress> progress,
|
||||
Action<OsScanProgress> progressCallback,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Starting OS scan for task: {TaskId}", taskId);
|
||||
_logger.LogInformation("Starting OS scan for task: {TaskId}, network: {Network}{Mask}",
|
||||
taskId, networkSegment, subnetMask);
|
||||
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
_cancellationTokens[taskId] = cts;
|
||||
_scanResults[taskId] = new List<OsDevice>();
|
||||
|
||||
try
|
||||
{
|
||||
var ipList = CalculateIpRange(networkSegment, subnetMask);
|
||||
_logger.LogInformation("Calculated {Count} IPs to scan", ipList.Count);
|
||||
|
||||
var foundDevices = new ConcurrentBag<OsDevice>();
|
||||
int scannedCount = 0;
|
||||
int foundCount = 0;
|
||||
@ -72,9 +81,18 @@ public class WindowsScannerService : IWindowsScannerService
|
||||
{
|
||||
foundDevices.Add(device);
|
||||
var found = Interlocked.Increment(ref foundCount);
|
||||
await SaveOsDeviceAsync(device);
|
||||
_logger.LogInformation("Found device: {Ip}, OS: {OsType}", ip, device.OsType);
|
||||
|
||||
// 存储到内存中等待用户选择
|
||||
lock (_scanResults)
|
||||
{
|
||||
if (_scanResults.TryGetValue(taskId, out var list))
|
||||
{
|
||||
list.Add(device);
|
||||
}
|
||||
}
|
||||
|
||||
progress.Report(new OsScanProgress
|
||||
progressCallback(new OsScanProgress
|
||||
{
|
||||
TaskId = taskId,
|
||||
ScannedCount = scanned,
|
||||
@ -87,7 +105,7 @@ public class WindowsScannerService : IWindowsScannerService
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.Report(new OsScanProgress
|
||||
progressCallback(new OsScanProgress
|
||||
{
|
||||
TaskId = taskId,
|
||||
ScannedCount = scanned,
|
||||
@ -104,9 +122,7 @@ public class WindowsScannerService : IWindowsScannerService
|
||||
}
|
||||
});
|
||||
|
||||
// 扫描完成后尝试绑定 AMT 设备
|
||||
await BindAmtDevicesAsync();
|
||||
|
||||
_logger.LogInformation("OS scan completed. Found {Count} devices", foundDevices.Count);
|
||||
return foundDevices.ToList();
|
||||
}
|
||||
finally
|
||||
@ -116,6 +132,56 @@ public class WindowsScannerService : IWindowsScannerService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取扫描结果(未保存的设备列表)
|
||||
/// </summary>
|
||||
public List<OsDevice> GetScanResults(string taskId)
|
||||
{
|
||||
_logger.LogInformation("GetScanResults called for task: {TaskId}, available tasks: {Tasks}",
|
||||
taskId, string.Join(", ", _scanResults.Keys));
|
||||
|
||||
if (_scanResults.TryGetValue(taskId, out var devices))
|
||||
{
|
||||
_logger.LogInformation("Found {Count} devices for task: {TaskId}", devices.Count, taskId);
|
||||
return devices.ToList();
|
||||
}
|
||||
_logger.LogWarning("No scan results found for task: {TaskId}", taskId);
|
||||
return new List<OsDevice>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除扫描结果
|
||||
/// </summary>
|
||||
public void ClearScanResults(string taskId)
|
||||
{
|
||||
_scanResults.TryRemove(taskId, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存选中的设备到数据库
|
||||
/// </summary>
|
||||
public async Task<int> SaveSelectedDevicesAsync(string taskId, List<string> selectedIps)
|
||||
{
|
||||
if (!_scanResults.TryGetValue(taskId, out var devices))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var selectedDevices = devices.Where(d => selectedIps.Contains(d.IpAddress)).ToList();
|
||||
int savedCount = 0;
|
||||
|
||||
foreach (var device in selectedDevices)
|
||||
{
|
||||
await SaveOsDeviceAsync(device);
|
||||
savedCount++;
|
||||
}
|
||||
|
||||
// 保存后尝试绑定 AMT 设备
|
||||
await BindAmtDevicesAsync();
|
||||
|
||||
return savedCount;
|
||||
}
|
||||
|
||||
public void CancelScan(string taskId)
|
||||
{
|
||||
if (_cancellationTokens.TryGetValue(taskId, out var cts))
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
-- 为 OsDevices 表添加 Windows 凭据字段
|
||||
ALTER TABLE `OsDevices`
|
||||
ADD COLUMN `WindowsUsername` VARCHAR(100) NULL AFTER `Description`,
|
||||
ADD COLUMN `WindowsPassword` VARCHAR(500) NULL AFTER `WindowsUsername`;
|
||||
25
backend-csharp/AmtScanner.Api/update_desktop_menu.sql
Normal file
25
backend-csharp/AmtScanner.Api/update_desktop_menu.sql
Normal file
@ -0,0 +1,25 @@
|
||||
-- 更新桌面管理菜单结构
|
||||
-- 将原来的"远程桌面"改为"系统添加"和"系统管理"两个子菜单
|
||||
|
||||
-- 1. 更新原有的菜单项(ID=21)为"系统添加"
|
||||
UPDATE Menus SET
|
||||
Name = 'OsDevices',
|
||||
Path = 'os-devices',
|
||||
Component = '/desktop-manage/os-devices',
|
||||
Title = '系统添加',
|
||||
Sort = 1
|
||||
WHERE Id = 21;
|
||||
|
||||
-- 2. 添加新的"系统管理"菜单项(ID=22)
|
||||
INSERT INTO Menus (Id, ParentId, Name, Path, Component, Title, Icon, Sort, Roles, IsHide, KeepAlive, IsSystem, IsIframe, IsHideTab, CreatedAt)
|
||||
VALUES (22, 20, 'DesktopDevices', 'devices', '/desktop-manage/devices', '系统管理', NULL, 2, 'R_SUPER,R_ADMIN', 0, 1, 1, 0, 0, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
Name = 'DesktopDevices',
|
||||
Path = 'devices',
|
||||
Component = '/desktop-manage/devices',
|
||||
Title = '系统管理',
|
||||
Sort = 2;
|
||||
|
||||
-- 3. 为超级管理员和管理员角色添加新菜单权限
|
||||
INSERT IGNORE INTO RoleMenus (RoleId, MenuId)
|
||||
SELECT r.Id, 22 FROM Roles r WHERE r.RoleCode IN ('R_SUPER', 'R_ADMIN');
|
||||
Loading…
x
Reference in New Issue
Block a user