feat: 重构桌面管理模块 - 拆分系统添加和系统管理菜单,支持批量配置Windows账号和删除设备
This commit is contained in:
parent
ca7231ecb9
commit
8acd7b0ab6
@ -341,7 +341,7 @@ export const osDeviceApi = {
|
|||||||
startScan(networkSegment: string, subnetMask: string) {
|
startScan(networkSegment: string, subnetMask: string) {
|
||||||
return request.post({
|
return request.post({
|
||||||
url: '/api/os-devices/scan/start',
|
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) {
|
cancelScan(taskId: string) {
|
||||||
return request.post({
|
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) {
|
delete(id: number) {
|
||||||
return request.del({
|
return request.del({
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<ElCard shadow="never">
|
<ElCard shadow="never">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span>远程桌面</span>
|
<span>系统管理</span>
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<ElTag v-if="isCheckingStatus" type="info" size="small" style="margin-right: 10px">
|
<ElTag v-if="isCheckingStatus" type="info" size="small" style="margin-right: 10px">
|
||||||
<el-icon class="is-loading"><Refresh /></el-icon>
|
<el-icon class="is-loading"><Refresh /></el-icon>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
</ElTag>
|
</ElTag>
|
||||||
<ElInput
|
<ElInput
|
||||||
v-model="searchKeyword"
|
v-model="searchKeyword"
|
||||||
placeholder="搜索 IP 地址"
|
placeholder="搜索 IP / 主机名"
|
||||||
style="width: 200px; margin-right: 10px"
|
style="width: 200px; margin-right: 10px"
|
||||||
clearable
|
clearable
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
@ -26,39 +26,79 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<ElTable :data="devices" v-loading="loading" stripe style="width: 100%">
|
<!-- 操作工具栏 -->
|
||||||
|
<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="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="ipAddress" label="IP 地址" width="140" />
|
||||||
<ElTableColumn prop="hostname" label="主机名" width="150" />
|
<ElTableColumn prop="hostname" label="主机名" width="150">
|
||||||
<ElTableColumn label="AMT状态" width="90">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElTag :type="row.amtOnline ? 'success' : 'info'" size="small">
|
{{ row.hostname || '-' }}
|
||||||
{{ row.amtOnline ? '在线' : '离线' }}
|
</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>
|
</ElTag>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="系统状态" width="90">
|
<ElTableColumn label="AMT 绑定" width="130">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElTag :type="row.osOnline ? 'success' : 'danger'" size="small">
|
<ElTag v-if="row.amtDeviceId" type="success" size="small">{{ row.amtDeviceIp }}</ElTag>
|
||||||
{{ row.osOnline ? '运行中' : '已关机' }}
|
<ElTag v-else type="info" size="small">未绑定</ElTag>
|
||||||
</ElTag>
|
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="Windows账号" width="120">
|
<ElTableColumn label="账号" width="120">
|
||||||
<template #default="{ row }">
|
<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>
|
<ElTag v-else type="warning" size="small">未配置</ElTag>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn prop="description" label="备注" min-width="150" />
|
<ElTableColumn label="密码" width="120">
|
||||||
<ElTableColumn label="操作" width="380" fixed="right">
|
|
||||||
<template #default="{ row }">
|
<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>
|
||||||
<ElButton type="info" size="small" @click="handleSetCredentials(row)">
|
<ElButton type="info" size="small" @click="handleFetchInfo(row)">
|
||||||
配置账号
|
获取信息
|
||||||
</ElButton>
|
</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">
|
<ElButton type="warning" size="small">
|
||||||
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||||
</ElButton>
|
</ElButton>
|
||||||
@ -75,16 +115,43 @@
|
|||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
</ElTable>
|
</ElTable>
|
||||||
|
|
||||||
|
<ElEmpty v-if="!loading && devices.length === 0" description="暂无设备,请先在系统添加中扫描并添加设备" />
|
||||||
</ElCard>
|
</ElCard>
|
||||||
|
|
||||||
<!-- 远程桌面弹窗 -->
|
<!-- 远程桌面弹窗 -->
|
||||||
<RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" />
|
<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 账号弹窗 -->
|
<!-- 配置 Windows 账号弹窗 -->
|
||||||
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="450px">
|
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="500px">
|
||||||
<ElForm :model="credentialsForm" label-width="100px">
|
<div v-if="credentialsTargetDevices.length > 1" class="target-devices-info">
|
||||||
<ElFormItem label="设备 IP">
|
<ElAlert type="info" :closable="false" show-icon>
|
||||||
<ElInput :model-value="selectedDevice?.ipAddress" disabled />
|
将为以下 {{ 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>
|
||||||
<ElFormItem label="用户名">
|
<ElFormItem label="用户名">
|
||||||
<ElInput v-model="credentialsForm.username" placeholder="Windows 登录用户名" />
|
<ElInput v-model="credentialsForm.username" placeholder="Windows 登录用户名" />
|
||||||
@ -102,26 +169,53 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose } from '@element-plus/icons-vue'
|
import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose, Delete, Key } from '@element-plus/icons-vue'
|
||||||
import { deviceApi, powerApi } from '@/api/amt'
|
import { osDeviceApi, powerApi } from '@/api/amt'
|
||||||
import RemoteDesktopModal from '@/views/amt/modules/remote-desktop-modal.vue'
|
import RemoteDesktopModal from '@/views/amt/modules/remote-desktop-modal.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'DesktopManageDevices' })
|
defineOptions({ name: 'DesktopManageDevices' })
|
||||||
|
|
||||||
const devices = ref<any[]>([])
|
const devices = ref<any[]>([])
|
||||||
|
const selectedDevices = ref<any[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const isCheckingStatus = ref(false)
|
const isCheckingStatus = ref(false)
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const showRemoteDesktopModal = ref(false)
|
const showRemoteDesktopModal = ref(false)
|
||||||
const showCredentialsDialog = ref(false)
|
|
||||||
const selectedDevice = ref<any>(null)
|
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 credentialsForm = ref({ username: '', password: '' })
|
||||||
const savingCredentials = ref(false)
|
const savingCredentials = ref(false)
|
||||||
|
|
||||||
let statusCheckInterval: number | null = null
|
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(() => {
|
onMounted(() => {
|
||||||
fetchDevices()
|
fetchDevices()
|
||||||
startStatusCheck()
|
startStatusCheck()
|
||||||
@ -134,7 +228,7 @@ onUnmounted(() => {
|
|||||||
const fetchDevices = async () => {
|
const fetchDevices = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
devices.value = await deviceApi.getAllDevices()
|
devices.value = await osDeviceApi.getAll()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取设备列表失败:', error)
|
console.error('获取设备列表失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@ -142,78 +236,106 @@ const fetchDevices = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = async () => {
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
if (searchKeyword.value) {
|
selectedDevices.value = selection
|
||||||
loading.value = true
|
}
|
||||||
try {
|
|
||||||
devices.value = await deviceApi.searchDevices(searchKeyword.value)
|
const handleSearch = () => {
|
||||||
} catch (error) {
|
// 前端过滤,不需要请求后端
|
||||||
console.error('搜索设备失败:', error)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fetchDevices()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
await fetchDevices()
|
await fetchDevices()
|
||||||
await checkAllDevicesStatus()
|
|
||||||
ElMessage.success('刷新成功')
|
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 = () => {
|
const startStatusCheck = () => {
|
||||||
checkAllDevicesStatus()
|
statusCheckInterval = window.setInterval(() => {
|
||||||
statusCheckInterval = window.setInterval(() => checkAllDevicesStatus(), 30000)
|
// 可以添加状态检查逻辑
|
||||||
|
}, 30000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopStatusCheck = () => {
|
const stopStatusCheck = () => {
|
||||||
if (statusCheckInterval) { clearInterval(statusCheckInterval); statusCheckInterval = null }
|
if (statusCheckInterval) {
|
||||||
|
clearInterval(statusCheckInterval)
|
||||||
|
statusCheckInterval = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoteDesktop = (device: any) => {
|
const handleRemoteDesktop = (device: any) => {
|
||||||
if (!device.osOnline) { ElMessage.warning('设备操作系统未运行,无法连接远程桌面'); return }
|
if (!device.isOnline) {
|
||||||
if (!device.windowsUsername) { ElMessage.warning('请先配置该设备的 Windows 登录账号'); return }
|
ElMessage.warning('设备离线,无法连接远程桌面')
|
||||||
selectedDevice.value = device
|
return
|
||||||
|
}
|
||||||
|
selectedDevice.value = {
|
||||||
|
id: device.amtDeviceId || device.id,
|
||||||
|
ipAddress: device.ipAddress,
|
||||||
|
hostname: device.hostname,
|
||||||
|
windowsUsername: device.loggedInUser
|
||||||
|
}
|
||||||
showRemoteDesktopModal.value = true
|
showRemoteDesktopModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSetCredentials = async (device: any) => {
|
const handleFetchInfo = (row: any) => {
|
||||||
selectedDevice.value = device
|
currentFetchDevice = row
|
||||||
credentialsForm.value = { username: device.windowsUsername || '', password: '' }
|
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
|
showCredentialsDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveCredentials = async () => {
|
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
|
savingCredentials.value = true
|
||||||
try {
|
try {
|
||||||
await deviceApi.setDeviceCredentials(selectedDevice.value.id, credentialsForm.value)
|
let successCount = 0
|
||||||
const device = devices.value.find(d => d.id === selectedDevice.value.id)
|
for (const device of credentialsTargetDevices.value) {
|
||||||
if (device) device.windowsUsername = credentialsForm.value.username
|
try {
|
||||||
|
await osDeviceApi.setCredentials(device.id, credentialsForm.value)
|
||||||
|
successCount++
|
||||||
|
} catch (e) {
|
||||||
|
console.error('配置Windows账号失败:', device.ipAddress, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showCredentialsDialog.value = false
|
showCredentialsDialog.value = false
|
||||||
ElMessage.success('账号配置成功')
|
if (successCount === credentialsTargetDevices.value.length) {
|
||||||
|
ElMessage.success('Windows账号配置成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning(`成功配置 ${successCount} 台,失败 ${credentialsTargetDevices.value.length - successCount} 台`)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('保存失败')
|
ElMessage.error('保存失败')
|
||||||
} finally {
|
} 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) => {
|
const handlePowerCommand = async (command: string, device: any) => {
|
||||||
|
if (!device.amtDeviceId) {
|
||||||
|
ElMessage.warning('该设备未绑定 AMT,无法进行电源管理')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = {
|
const actionMap: Record<string, { api: Function; name: string; confirmMsg: string }> = {
|
||||||
'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' },
|
'power-on': { api: powerApi.powerOn, name: '开机', confirmMsg: '确定要开机吗?' },
|
||||||
'power-off': { api: powerApi.powerOff, name: '关机', confirmMsg: '确定要关机吗?' },
|
'power-off': { api: powerApi.powerOff, name: '关机', confirmMsg: '确定要关机吗?' },
|
||||||
@ -231,15 +388,15 @@ const handlePowerCommand = async (command: string, device: any) => {
|
|||||||
}
|
}
|
||||||
const action = actionMap[command]
|
const action = actionMap[command]
|
||||||
if (!action) return
|
if (!action) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`设备: ${device.ipAddress}\n${action.confirmMsg}`, `确认${action.name}`, {
|
await ElMessageBox.confirm(`设备: ${device.ipAddress}\n${action.confirmMsg}`, `确认${action.name}`, {
|
||||||
confirmButtonText: '确定', cancelButtonText: '取消', type: command.includes('force') ? 'warning' : 'info'
|
confirmButtonText: '确定', cancelButtonText: '取消', type: command.includes('force') ? 'warning' : 'info'
|
||||||
})
|
})
|
||||||
ElMessage.info(`正在执行${action.name}...`)
|
ElMessage.info(`正在执行${action.name}...`)
|
||||||
const response = await action.api(device.id)
|
const response = await action.api(device.amtDeviceId)
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
ElMessage.success(response.message || `${action.name}命令已发送`)
|
ElMessage.success(response.message || `${action.name}命令已发送`)
|
||||||
setTimeout(() => checkAllDevicesStatus(), 3000)
|
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error(response.error || `${action.name}失败`)
|
ElMessage.error(response.error || `${action.name}失败`)
|
||||||
}
|
}
|
||||||
@ -253,4 +410,10 @@ const handlePowerCommand = async (command: string, device: any) => {
|
|||||||
.devices-page { padding: 0; }
|
.devices-page { padding: 0; }
|
||||||
.card-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: 500; }
|
.card-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: 500; }
|
||||||
.header-actions { display: flex; align-items: center; }
|
.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>
|
</style>
|
||||||
|
|||||||
@ -1,174 +1,121 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="os-devices-page">
|
<div class="os-devices-page">
|
||||||
<!-- 工具栏 -->
|
<ElCard shadow="never">
|
||||||
<div class="toolbar">
|
<template #header>
|
||||||
<div class="left">
|
<div class="card-header">
|
||||||
<ElButton type="primary" @click="showScanDialog = true">
|
<span>系统添加</span>
|
||||||
<ElIcon><Search /></ElIcon>
|
<div class="header-actions">
|
||||||
扫描操作系统
|
<ElButton type="primary" @click="showScanDialog = true">
|
||||||
</ElButton>
|
<ElIcon><Search /></ElIcon>
|
||||||
<ElButton @click="handleAutoBind" :loading="autoBinding">
|
扫描网络
|
||||||
<ElIcon><Link /></ElIcon>
|
</ElButton>
|
||||||
自动绑定 AMT
|
</div>
|
||||||
</ElButton>
|
</div>
|
||||||
<ElButton @click="loadDevices" :loading="loading">
|
</template>
|
||||||
<ElIcon><Refresh /></ElIcon>
|
|
||||||
刷新
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<ElInput v-model="searchKeyword" placeholder="搜索 IP/主机名" clearable style="width: 200px" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 设备列表 -->
|
<!-- 扫描结果列表 -->
|
||||||
<ElTable :data="filteredDevices" v-loading="loading" stripe>
|
<div v-if="scanResults.length > 0" class="scan-results">
|
||||||
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
|
<div class="results-header">
|
||||||
<ElTableColumn prop="hostname" label="主机名" width="150" />
|
<span>发现 {{ scanResults.length }} 台设备,请选择要添加的设备:</span>
|
||||||
<ElTableColumn label="操作系统" width="200">
|
<div>
|
||||||
<template #default="{ row }">
|
<ElButton size="small" @click="selectAll">全选</ElButton>
|
||||||
<ElTag :type="getOsTagType(row.osType)" size="small">{{ row.osType }}</ElTag>
|
<ElButton size="small" @click="deselectAll">取消全选</ElButton>
|
||||||
<span v-if="row.osVersion" style="margin-left: 5px; font-size: 12px; color: #999">
|
</div>
|
||||||
{{ row.osVersion?.substring(0, 30) }}
|
</div>
|
||||||
</span>
|
|
||||||
</template>
|
<ElTable :data="scanResults" @selection-change="handleSelectionChange" ref="tableRef">
|
||||||
</ElTableColumn>
|
<ElTableColumn type="selection" width="55" />
|
||||||
<ElTableColumn prop="systemUuid" label="UUID" width="280">
|
<ElTableColumn prop="ipAddress" label="IP 地址" width="150" />
|
||||||
<template #default="{ row }">
|
<ElTableColumn label="操作系统" width="120">
|
||||||
<span v-if="row.systemUuid" style="font-family: monospace; font-size: 11px">{{ row.systemUuid }}</span>
|
<template #default="{ row }">
|
||||||
<ElTag v-else type="warning" size="small">未获取</ElTag>
|
<ElTag :type="getOsTagType(row.osType)" size="small">{{ row.osType }}</ElTag>
|
||||||
</template>
|
</template>
|
||||||
</ElTableColumn>
|
</ElTableColumn>
|
||||||
<ElTableColumn label="AMT 绑定" width="150">
|
<ElTableColumn prop="hostname" label="主机名" width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElTag v-if="row.amtDeviceId" type="success" size="small">
|
{{ row.hostname || '-' }}
|
||||||
{{ row.amtDeviceIp }}
|
</template>
|
||||||
</ElTag>
|
</ElTableColumn>
|
||||||
<ElTag v-else type="info" size="small">未绑定</ElTag>
|
<ElTableColumn label="状态" width="100">
|
||||||
</template>
|
<template #default="{ row }">
|
||||||
</ElTableColumn>
|
<ElTag :type="row.isOnline ? 'success' : 'danger'" size="small">
|
||||||
<ElTableColumn label="状态" width="80">
|
{{ row.isOnline ? '在线' : '离线' }}
|
||||||
<template #default="{ row }">
|
</ElTag>
|
||||||
<ElTag :type="row.isOnline ? 'success' : 'danger'" size="small">
|
</template>
|
||||||
{{ row.isOnline ? '在线' : '离线' }}
|
</ElTableColumn>
|
||||||
</ElTag>
|
<ElTableColumn prop="discoveredAt" label="发现时间">
|
||||||
</template>
|
<template #default="{ row }">
|
||||||
</ElTableColumn>
|
{{ formatTime(row.discoveredAt) }}
|
||||||
<ElTableColumn label="操作" width="280" fixed="right">
|
</template>
|
||||||
<template #default="{ row }">
|
</ElTableColumn>
|
||||||
<ElButton size="small" @click="handleFetchInfo(row)">获取信息</ElButton>
|
</ElTable>
|
||||||
<ElButton size="small" @click="handleBindAmt(row)" v-if="!row.amtDeviceId">绑定 AMT</ElButton>
|
|
||||||
<ElButton size="small" type="warning" @click="handleUnbindAmt(row)" v-else>解绑</ElButton>
|
<div class="results-footer">
|
||||||
<ElButton size="small" type="danger" @click="handleDelete(row)">删除</ElButton>
|
<span>已选择 {{ selectedDevices.length }} 台设备</span>
|
||||||
</template>
|
<div>
|
||||||
</ElTableColumn>
|
<ElButton @click="clearResults">取消</ElButton>
|
||||||
</ElTable>
|
<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">
|
<ElForm :model="scanForm" label-width="100px">
|
||||||
<ElFormItem label="网段">
|
<ElFormItem label="网段">
|
||||||
<ElInput v-model="scanForm.networkSegment" placeholder="例如: 192.168.1.0" />
|
<ElInput v-model="scanForm.networkSegment" placeholder="例如: 192.168.1.0" />
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
<ElFormItem label="子网掩码">
|
<ElFormItem label="子网掩码">
|
||||||
<ElSelect v-model="scanForm.subnetMask" style="width: 100%">
|
<ElSelect v-model="scanForm.subnetMask" style="width: 100%">
|
||||||
<ElOption label="/24 (255.255.255.0)" value="/24" />
|
<ElOption label="/24 (255.255.255.0) - 254 台主机" value="/24" />
|
||||||
<ElOption label="/16 (255.255.0.0)" value="/16" />
|
<ElOption label="/23 (255.255.254.0) - 510 台主机" value="/23" />
|
||||||
<ElOption label="/8 (255.0.0.0)" value="/8" />
|
<ElOption label="/22 (255.255.252.0) - 1022 台主机" value="/22" />
|
||||||
</ElSelect>
|
</ElSelect>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
</ElForm>
|
</ElForm>
|
||||||
|
|
||||||
<div v-if="scanning" class="scan-progress">
|
<div v-if="scanning" class="scan-progress">
|
||||||
<ElProgress :percentage="scanProgress.progressPercentage" :format="() => `${scanProgress.scannedCount}/${scanProgress.totalCount}`" />
|
<ElProgress :percentage="scanProgress.progressPercentage" :format="formatProgress" />
|
||||||
<p>当前扫描: {{ scanProgress.currentIp }}</p>
|
<p>当前扫描: {{ scanProgress.currentIp || '-' }}</p>
|
||||||
<p>已发现: {{ scanProgress.foundDevices }} 台设备</p>
|
<p>已发现: {{ scanProgress.foundDevices }} 台设备</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<ElButton @click="showScanDialog = false" :disabled="scanning">取消</ElButton>
|
<ElButton @click="cancelScan" v-if="scanning">取消扫描</ElButton>
|
||||||
<ElButton type="primary" @click="startScan" :loading="scanning">
|
<ElButton @click="showScanDialog = false" v-else>关闭</ElButton>
|
||||||
|
<ElButton type="primary" @click="startScan" :loading="scanning" :disabled="scanning">
|
||||||
{{ scanning ? '扫描中...' : '开始扫描' }}
|
{{ scanning ? '扫描中...' : '开始扫描' }}
|
||||||
</ElButton>
|
</ElButton>
|
||||||
</template>
|
</template>
|
||||||
</ElDialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Search, Refresh, Link } from '@element-plus/icons-vue'
|
import { Search } from '@element-plus/icons-vue'
|
||||||
import { osDeviceApi, deviceApi } from '@/api/amt'
|
import { osDeviceApi } from '@/api/amt'
|
||||||
|
|
||||||
const loading = ref(false)
|
defineOptions({ name: 'OsDevices' })
|
||||||
const devices = ref<any[]>([])
|
|
||||||
const searchKeyword = ref('')
|
const tableRef = ref()
|
||||||
const autoBinding = ref(false)
|
const scanResults = ref<any[]>([])
|
||||||
|
const selectedDevices = ref<any[]>([])
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
// 扫描相关
|
// 扫描相关
|
||||||
const showScanDialog = ref(false)
|
const showScanDialog = ref(false)
|
||||||
const scanning = ref(false)
|
const scanning = ref(false)
|
||||||
const scanForm = ref({ networkSegment: '192.168.1.0', subnetMask: '/24' })
|
const scanForm = ref({ networkSegment: '192.168.1.0', subnetMask: '/24' })
|
||||||
const scanProgress = ref({ scannedCount: 0, totalCount: 0, foundDevices: 0, progressPercentage: 0, currentIp: '' })
|
const scanProgress = ref({ scannedCount: 0, totalCount: 0, foundDevices: 0, progressPercentage: 0, currentIp: '' })
|
||||||
let scanTaskId = ''
|
let currentTaskId = ''
|
||||||
|
|
||||||
// 获取信息相关
|
|
||||||
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)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const getOsTagType = (osType: string) => {
|
const getOsTagType = (osType: string) => {
|
||||||
switch (osType) {
|
switch (osType) {
|
||||||
@ -178,57 +125,66 @@ const getOsTagType = (osType: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadDevices = async () => {
|
const formatTime = (time: string) => {
|
||||||
loading.value = true
|
if (!time) return '-'
|
||||||
try {
|
return new Date(time).toLocaleString()
|
||||||
devices.value = await osDeviceApi.getAll()
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('加载设备失败', error)
|
const formatProgress = () => {
|
||||||
} finally {
|
return scanProgress.value.scannedCount + '/' + scanProgress.value.totalCount
|
||||||
loading.value = false
|
}
|
||||||
}
|
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedDevices.value = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAll = () => {
|
||||||
|
tableRef.value?.toggleAllSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deselectAll = () => {
|
||||||
|
tableRef.value?.clearSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
const startScan = async () => {
|
const startScan = async () => {
|
||||||
scanning.value = true
|
scanning.value = true
|
||||||
scanProgress.value = { scannedCount: 0, totalCount: 0, foundDevices: 0, progressPercentage: 0, currentIp: '' }
|
scanProgress.value = { scannedCount: 0, totalCount: 0, foundDevices: 0, progressPercentage: 0, currentIp: '' }
|
||||||
|
scanResults.value = []
|
||||||
|
selectedDevices.value = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await osDeviceApi.startScan(scanForm.value.networkSegment, scanForm.value.subnetMask)
|
const result = await osDeviceApi.startScan(scanForm.value.networkSegment, scanForm.value.subnetMask)
|
||||||
scanTaskId = result.taskId
|
currentTaskId = result.taskId
|
||||||
|
|
||||||
let retryCount = 0
|
|
||||||
const maxRetries = 3
|
|
||||||
|
|
||||||
// 轮询扫描进度
|
// 轮询扫描进度
|
||||||
const pollProgress = async () => {
|
const pollProgress = async () => {
|
||||||
if (!scanning.value) return
|
if (!scanning.value) return
|
||||||
try {
|
try {
|
||||||
const progress = await osDeviceApi.getScanStatus(scanTaskId)
|
const progress = await osDeviceApi.getScanStatus(currentTaskId)
|
||||||
retryCount = 0 // 成功后重置重试计数
|
|
||||||
|
// -1 表示任务不存在,停止轮询
|
||||||
|
if (progress.progressPercentage === -1) {
|
||||||
|
scanning.value = false
|
||||||
|
ElMessage.warning('扫描任务已失效')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
scanProgress.value = progress
|
scanProgress.value = progress
|
||||||
|
|
||||||
if (progress.progressPercentage < 100) {
|
if (progress.progressPercentage < 100) {
|
||||||
setTimeout(pollProgress, 500)
|
setTimeout(pollProgress, 500)
|
||||||
} else {
|
} else {
|
||||||
|
// 扫描完成,获取结果
|
||||||
scanning.value = false
|
scanning.value = false
|
||||||
showScanDialog.value = false
|
showScanDialog.value = false
|
||||||
ElMessage.success(`扫描完成,发现 ${progress.foundDevices} 台设备`)
|
await loadScanResults()
|
||||||
loadDevices()
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
retryCount++
|
scanning.value = false
|
||||||
if (retryCount < maxRetries) {
|
ElMessage.error('获取扫描进度失败')
|
||||||
// 重试,增加延迟
|
|
||||||
setTimeout(pollProgress, 1000)
|
|
||||||
} else {
|
|
||||||
scanning.value = false
|
|
||||||
ElMessage.error('获取扫描进度失败')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 延迟500ms后开始轮询,给后端一点时间注册任务
|
|
||||||
setTimeout(pollProgress, 500)
|
setTimeout(pollProgress, 500)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
scanning.value = false
|
scanning.value = false
|
||||||
@ -236,95 +192,63 @@ const startScan = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAutoBind = async () => {
|
const cancelScan = async () => {
|
||||||
autoBinding.value = true
|
|
||||||
try {
|
try {
|
||||||
await osDeviceApi.autoBind()
|
await osDeviceApi.cancelScan(currentTaskId)
|
||||||
loadDevices()
|
scanning.value = false
|
||||||
} finally {
|
ElMessage.info('扫描已取消')
|
||||||
autoBinding.value = false
|
} catch (error) {
|
||||||
|
ElMessage.error('取消扫描失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFetchInfo = (row: any) => {
|
const loadScanResults = async () => {
|
||||||
currentFetchDevice = row
|
try {
|
||||||
fetchForm.value = { username: '', password: '' }
|
const results = await osDeviceApi.getScanResults(currentTaskId)
|
||||||
showFetchDialog.value = true
|
scanResults.value = results
|
||||||
|
if (results.length === 0) {
|
||||||
|
ElMessage.info('未发现任何设备')
|
||||||
|
} else {
|
||||||
|
ElMessage.success(`发现 ${results.length} 台设备,请选择要添加的设备`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取扫描结果失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const doFetchInfo = async () => {
|
const saveSelectedDevices = async () => {
|
||||||
if (!fetchForm.value.username || !fetchForm.value.password) {
|
if (selectedDevices.value.length === 0) {
|
||||||
ElMessage.warning('请输入凭据')
|
ElMessage.warning('请选择要添加的设备')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fetching.value = true
|
|
||||||
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
await osDeviceApi.fetchInfo(currentFetchDevice.id, fetchForm.value)
|
const selectedIps = selectedDevices.value.map(d => d.ipAddress)
|
||||||
showFetchDialog.value = false
|
const result = await osDeviceApi.saveSelectedDevices(currentTaskId, selectedIps)
|
||||||
loadDevices()
|
ElMessage.success(result.message || `成功添加 ${result.savedCount} 台设备`)
|
||||||
} catch (error: any) {
|
clearResults()
|
||||||
ElMessage.error(error.message || '获取信息失败')
|
} catch (error) {
|
||||||
|
ElMessage.error('添加设备失败')
|
||||||
} finally {
|
} finally {
|
||||||
fetching.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBindAmt = async (row: any) => {
|
const clearResults = () => {
|
||||||
currentBindDevice = row
|
scanResults.value = []
|
||||||
selectedAmtDevice.value = null
|
selectedDevices.value = []
|
||||||
showBindDialog.value = true
|
currentTaskId = ''
|
||||||
loadingAmt.value = true
|
|
||||||
try {
|
|
||||||
amtDevices.value = await deviceApi.getAllDevices()
|
|
||||||
} finally {
|
|
||||||
loadingAmt.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.os-devices-page { padding: 20px; }
|
.os-devices-page { padding: 0; }
|
||||||
.toolbar { display: flex; justify-content: space-between; margin-bottom: 20px; }
|
.card-header { display: flex; justify-content: space-between; align-items: center; font-size: 16px; font-weight: 500; }
|
||||||
.toolbar .left { display: flex; gap: 10px; }
|
.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 { margin-top: 20px; text-align: center; }
|
||||||
.scan-progress p { margin: 10px 0; color: #666; }
|
.scan-progress p { margin: 10px 0; color: #666; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -52,6 +52,8 @@ public class OsDevicesController : ControllerBase
|
|||||||
DiscoveredAt = o.DiscoveredAt,
|
DiscoveredAt = o.DiscoveredAt,
|
||||||
LastUpdatedAt = o.LastUpdatedAt,
|
LastUpdatedAt = o.LastUpdatedAt,
|
||||||
Description = o.Description,
|
Description = o.Description,
|
||||||
|
WindowsUsername = o.WindowsUsername,
|
||||||
|
WindowsPassword = o.WindowsPassword,
|
||||||
AmtDeviceId = o.AmtDeviceId,
|
AmtDeviceId = o.AmtDeviceId,
|
||||||
AmtDeviceIp = o.AmtDevice != null ? o.AmtDevice.IpAddress : null
|
AmtDeviceIp = o.AmtDevice != null ? o.AmtDevice.IpAddress : null
|
||||||
})
|
})
|
||||||
@ -104,16 +106,28 @@ public class OsDevicesController : ControllerBase
|
|||||||
{
|
{
|
||||||
var taskId = Guid.NewGuid().ToString("N");
|
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;
|
_scanProgress[taskId] = p;
|
||||||
});
|
};
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _scannerService.ScanNetworkAsync(taskId, request.NetworkSegment, request.SubnetMask, progress);
|
await _scannerService.ScanNetworkAsync(taskId, request.NetworkSegment, request.SubnetMask, progressCallback);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -138,7 +152,16 @@ public class OsDevicesController : ControllerBase
|
|||||||
{
|
{
|
||||||
return Ok(ApiResponse<OsScanProgress>.Success(progress));
|
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>
|
/// <summary>
|
||||||
@ -151,6 +174,52 @@ public class OsDevicesController : ControllerBase
|
|||||||
return Ok(ApiResponse<object>.Success(null, "扫描已取消"));
|
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>
|
/// <summary>
|
||||||
/// 获取设备详细信息(通过 WMI)
|
/// 获取设备详细信息(通过 WMI)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -266,6 +335,24 @@ public class OsDevicesController : ControllerBase
|
|||||||
return Ok(ApiResponse<object>.Success(null, "自动绑定完成"));
|
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>
|
||||||
/// 删除设备
|
/// 删除设备
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -300,6 +387,8 @@ public class OsDeviceDto
|
|||||||
public DateTime DiscoveredAt { get; set; }
|
public DateTime DiscoveredAt { get; set; }
|
||||||
public DateTime LastUpdatedAt { get; set; }
|
public DateTime LastUpdatedAt { get; set; }
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
public string? WindowsUsername { get; set; }
|
||||||
|
public string? WindowsPassword { get; set; }
|
||||||
public long? AmtDeviceId { get; set; }
|
public long? AmtDeviceId { get; set; }
|
||||||
public string? AmtDeviceIp { get; set; }
|
public string? AmtDeviceIp { get; set; }
|
||||||
}
|
}
|
||||||
@ -321,3 +410,30 @@ public class OsScanRequest
|
|||||||
public string NetworkSegment { get; set; } = string.Empty;
|
public string NetworkSegment { get; set; } = string.Empty;
|
||||||
public string SubnetMask { 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 = 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 },
|
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>
|
/// </summary>
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows 登录用户名
|
||||||
|
/// </summary>
|
||||||
|
public string? WindowsUsername { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Windows 登录密码(加密存储)
|
||||||
|
/// </summary>
|
||||||
|
public string? WindowsPassword { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 关联的 AMT 设备 ID
|
/// 关联的 AMT 设备 ID
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -125,8 +125,13 @@ public class AmtPowerService : IAmtPowerService
|
|||||||
var returnValue = outputObject.GetProperty("ReturnValue");
|
var returnValue = outputObject.GetProperty("ReturnValue");
|
||||||
|
|
||||||
var returnCode = Convert.ToUInt32(returnValue.ToString());
|
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.Success = true;
|
||||||
result.Message = GetActionSuccessMessage(action);
|
result.Message = GetActionSuccessMessage(action);
|
||||||
|
|||||||
@ -11,11 +11,14 @@ namespace AmtScanner.Api.Services;
|
|||||||
public interface IWindowsScannerService
|
public interface IWindowsScannerService
|
||||||
{
|
{
|
||||||
Task<List<OsDevice>> ScanNetworkAsync(string taskId, string networkSegment, string subnetMask,
|
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<OsDevice?> GetOsInfoAsync(string ipAddress, string username, string password);
|
||||||
Task<string?> GetSystemUuidAsync(string ipAddress, string username, string password);
|
Task<string?> GetSystemUuidAsync(string ipAddress, string username, string password);
|
||||||
Task BindAmtDevicesAsync();
|
Task BindAmtDevicesAsync();
|
||||||
void CancelScan(string taskId);
|
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
|
public class WindowsScannerService : IWindowsScannerService
|
||||||
@ -24,6 +27,8 @@ public class WindowsScannerService : IWindowsScannerService
|
|||||||
private readonly ILogger<WindowsScannerService> _logger;
|
private readonly ILogger<WindowsScannerService> _logger;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationTokens = new();
|
private readonly ConcurrentDictionary<string, CancellationTokenSource> _cancellationTokens = new();
|
||||||
|
// 存储扫描发现的设备(未保存到数据库)
|
||||||
|
private static readonly ConcurrentDictionary<string, List<OsDevice>> _scanResults = new();
|
||||||
|
|
||||||
public WindowsScannerService(
|
public WindowsScannerService(
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
@ -39,17 +44,21 @@ public class WindowsScannerService : IWindowsScannerService
|
|||||||
string taskId,
|
string taskId,
|
||||||
string networkSegment,
|
string networkSegment,
|
||||||
string subnetMask,
|
string subnetMask,
|
||||||
IProgress<OsScanProgress> progress,
|
Action<OsScanProgress> progressCallback,
|
||||||
CancellationToken cancellationToken = default)
|
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);
|
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
_cancellationTokens[taskId] = cts;
|
_cancellationTokens[taskId] = cts;
|
||||||
|
_scanResults[taskId] = new List<OsDevice>();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var ipList = CalculateIpRange(networkSegment, subnetMask);
|
var ipList = CalculateIpRange(networkSegment, subnetMask);
|
||||||
|
_logger.LogInformation("Calculated {Count} IPs to scan", ipList.Count);
|
||||||
|
|
||||||
var foundDevices = new ConcurrentBag<OsDevice>();
|
var foundDevices = new ConcurrentBag<OsDevice>();
|
||||||
int scannedCount = 0;
|
int scannedCount = 0;
|
||||||
int foundCount = 0;
|
int foundCount = 0;
|
||||||
@ -72,9 +81,18 @@ public class WindowsScannerService : IWindowsScannerService
|
|||||||
{
|
{
|
||||||
foundDevices.Add(device);
|
foundDevices.Add(device);
|
||||||
var found = Interlocked.Increment(ref foundCount);
|
var found = Interlocked.Increment(ref foundCount);
|
||||||
await SaveOsDeviceAsync(device);
|
_logger.LogInformation("Found device: {Ip}, OS: {OsType}", ip, device.OsType);
|
||||||
|
|
||||||
progress.Report(new OsScanProgress
|
// 存储到内存中等待用户选择
|
||||||
|
lock (_scanResults)
|
||||||
|
{
|
||||||
|
if (_scanResults.TryGetValue(taskId, out var list))
|
||||||
|
{
|
||||||
|
list.Add(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressCallback(new OsScanProgress
|
||||||
{
|
{
|
||||||
TaskId = taskId,
|
TaskId = taskId,
|
||||||
ScannedCount = scanned,
|
ScannedCount = scanned,
|
||||||
@ -87,7 +105,7 @@ public class WindowsScannerService : IWindowsScannerService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
progress.Report(new OsScanProgress
|
progressCallback(new OsScanProgress
|
||||||
{
|
{
|
||||||
TaskId = taskId,
|
TaskId = taskId,
|
||||||
ScannedCount = scanned,
|
ScannedCount = scanned,
|
||||||
@ -104,9 +122,7 @@ public class WindowsScannerService : IWindowsScannerService
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 扫描完成后尝试绑定 AMT 设备
|
_logger.LogInformation("OS scan completed. Found {Count} devices", foundDevices.Count);
|
||||||
await BindAmtDevicesAsync();
|
|
||||||
|
|
||||||
return foundDevices.ToList();
|
return foundDevices.ToList();
|
||||||
}
|
}
|
||||||
finally
|
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)
|
public void CancelScan(string taskId)
|
||||||
{
|
{
|
||||||
if (_cancellationTokens.TryGetValue(taskId, out var cts))
|
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