334 lines
9.6 KiB
Vue
334 lines
9.6 KiB
Vue
<template>
|
|
<div class="scan-page">
|
|
<!-- 添加方式选择 -->
|
|
<ElCard shadow="never">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>设备添加</span>
|
|
</div>
|
|
</template>
|
|
|
|
<ElTabs v-model="activeTab">
|
|
<!-- 网络扫描 -->
|
|
<ElTabPane label="网络扫描" name="scan">
|
|
<ElForm :model="scanForm" :rules="scanRules" ref="scanFormRef" label-width="120px">
|
|
<ElRow :gutter="20">
|
|
<ElCol :span="8">
|
|
<ElFormItem label="网段地址" prop="networkSegment">
|
|
<ElInput v-model="scanForm.networkSegment" placeholder="例如: 192.168.1.0" />
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="8">
|
|
<ElFormItem label="子网掩码" prop="subnetMask">
|
|
<ElInput v-model="scanForm.subnetMask" placeholder="例如: 255.255.255.0 或 /24" />
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="8">
|
|
<ElFormItem>
|
|
<ElButton type="primary" @click="handleStartScan" :loading="scanning">
|
|
<el-icon><Search /></el-icon>
|
|
开始扫描
|
|
</ElButton>
|
|
<ElButton @click="handleResetScan">重置</ElButton>
|
|
</ElFormItem>
|
|
</ElCol>
|
|
</ElRow>
|
|
</ElForm>
|
|
|
|
<ElAlert
|
|
title="提示"
|
|
type="info"
|
|
description="扫描将检测指定网段内所有支持 Intel AMT 的设备,请确保网络连接正常。"
|
|
:closable="false"
|
|
show-icon
|
|
style="margin-top: 10px"
|
|
/>
|
|
</ElTabPane>
|
|
|
|
<!-- 手动添加 -->
|
|
<ElTabPane label="手动添加" name="manual">
|
|
<ElForm :model="manualForm" :rules="manualRules" ref="manualFormRef" label-width="120px" style="max-width: 600px">
|
|
<ElFormItem label="IP 地址" prop="ipAddress">
|
|
<ElInput v-model="manualForm.ipAddress" placeholder="设备 IP 地址" />
|
|
</ElFormItem>
|
|
<ElFormItem label="主机名">
|
|
<ElInput v-model="manualForm.hostname" placeholder="可选,设备主机名" />
|
|
</ElFormItem>
|
|
<ElFormItem label="备注">
|
|
<ElInput v-model="manualForm.description" type="textarea" :rows="2" placeholder="可选,设备备注信息" />
|
|
</ElFormItem>
|
|
<ElFormItem>
|
|
<ElButton type="primary" @click="handleAddDevice" :loading="adding">添加设备</ElButton>
|
|
<ElButton @click="handleResetManual">重置</ElButton>
|
|
</ElFormItem>
|
|
</ElForm>
|
|
</ElTabPane>
|
|
</ElTabs>
|
|
</ElCard>
|
|
|
|
<!-- 扫描进度 -->
|
|
<ElCard v-if="scanning || scanProgress.status === 'completed'" class="progress-card" shadow="never">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>扫描进度</span>
|
|
<ElButton v-if="scanning" type="danger" size="small" @click="handleCancelScan">
|
|
取消扫描
|
|
</ElButton>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="progress-info">
|
|
<ElProgress
|
|
:percentage="scanProgress.progressPercentage"
|
|
:status="progressStatus"
|
|
:stroke-width="20"
|
|
/>
|
|
<div class="progress-stats">
|
|
<ElStatistic title="已扫描" :value="scanProgress.scannedCount" />
|
|
<ElStatistic title="总数量" :value="scanProgress.totalCount" />
|
|
<ElStatistic title="发现设备" :value="scanProgress.foundDevices" />
|
|
</div>
|
|
</div>
|
|
|
|
<ElResult
|
|
v-if="scanProgress.status === 'completed'"
|
|
icon="success"
|
|
:title="`扫描完成,发现 ${scanProgress.foundDevices} 个 AMT 设备`"
|
|
>
|
|
<template #extra>
|
|
<ElButton type="primary" @click="goToDeviceList">查看设备列表</ElButton>
|
|
</template>
|
|
</ElResult>
|
|
</ElCard>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onUnmounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { ElMessage } from 'element-plus'
|
|
import { Search } from '@element-plus/icons-vue'
|
|
import { scanApi, deviceApi } from '@/api/amt'
|
|
|
|
defineOptions({ name: 'AmtScan' })
|
|
|
|
const router = useRouter()
|
|
const activeTab = ref('scan')
|
|
const scanFormRef = ref()
|
|
const manualFormRef = ref()
|
|
const scanning = ref(false)
|
|
const adding = ref(false)
|
|
|
|
// 扫描表单
|
|
const scanForm = reactive({
|
|
networkSegment: '192.168.1.0',
|
|
subnetMask: '255.255.255.0'
|
|
})
|
|
|
|
// 手动添加表单
|
|
const manualForm = reactive({
|
|
ipAddress: '',
|
|
hostname: '',
|
|
description: ''
|
|
})
|
|
|
|
const scanProgress = reactive({
|
|
taskId: '',
|
|
scannedCount: 0,
|
|
totalCount: 0,
|
|
foundDevices: 0,
|
|
progressPercentage: 0,
|
|
status: 'idle' as 'idle' | 'running' | 'completed' | 'cancelled'
|
|
})
|
|
|
|
const progressStatus = computed(() => {
|
|
if (scanProgress.status === 'completed') return 'success'
|
|
if (scanProgress.status === 'cancelled') return 'exception'
|
|
return undefined
|
|
})
|
|
|
|
const validateIp = (_rule: any, value: string, callback: Function) => {
|
|
const ipRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
|
if (!value) {
|
|
callback(new Error('请输入 IP 地址'))
|
|
} else if (!ipRegex.test(value)) {
|
|
callback(new Error('请输入有效的 IP 地址'))
|
|
} else {
|
|
callback()
|
|
}
|
|
}
|
|
|
|
const validateSubnetMask = (_rule: any, value: string, callback: Function) => {
|
|
if (!value) {
|
|
callback(new Error('请输入子网掩码'))
|
|
} else if (value.startsWith('/')) {
|
|
const cidr = parseInt(value.substring(1))
|
|
if (isNaN(cidr) || cidr < 0 || cidr > 32) {
|
|
callback(new Error('CIDR 格式无效,应为 /0 到 /32'))
|
|
} else {
|
|
callback()
|
|
}
|
|
} else {
|
|
const maskRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
|
|
if (!maskRegex.test(value)) {
|
|
callback(new Error('请输入有效的子网掩码'))
|
|
} else {
|
|
callback()
|
|
}
|
|
}
|
|
}
|
|
|
|
const scanRules = {
|
|
networkSegment: [{ validator: validateIp, trigger: 'blur' }],
|
|
subnetMask: [{ validator: validateSubnetMask, trigger: 'blur' }]
|
|
}
|
|
|
|
const manualRules = {
|
|
ipAddress: [{ validator: validateIp, trigger: 'blur' }]
|
|
}
|
|
|
|
let pollTimer: number | null = null
|
|
|
|
// 扫描相关方法
|
|
const handleStartScan = async () => {
|
|
if (!scanFormRef.value) return
|
|
|
|
await scanFormRef.value.validate(async (valid: boolean) => {
|
|
if (valid) {
|
|
scanning.value = true
|
|
scanProgress.status = 'running'
|
|
scanProgress.scannedCount = 0
|
|
scanProgress.totalCount = 0
|
|
scanProgress.foundDevices = 0
|
|
scanProgress.progressPercentage = 0
|
|
|
|
try {
|
|
const result = await scanApi.startScan(scanForm.networkSegment, scanForm.subnetMask)
|
|
scanProgress.taskId = result.taskId
|
|
ElMessage.success('扫描任务已启动')
|
|
startPolling()
|
|
} catch (error) {
|
|
scanning.value = false
|
|
scanProgress.status = 'idle'
|
|
ElMessage.error('启动扫描失败')
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const startPolling = () => {
|
|
pollTimer = window.setInterval(async () => {
|
|
try {
|
|
const status = await scanApi.getScanStatus(scanProgress.taskId)
|
|
scanProgress.scannedCount = status.scannedCount || 0
|
|
scanProgress.totalCount = status.totalCount || 0
|
|
scanProgress.foundDevices = status.foundDevices || 0
|
|
scanProgress.progressPercentage = status.totalCount > 0
|
|
? Math.round((status.scannedCount / status.totalCount) * 100)
|
|
: 0
|
|
|
|
if (status.status === 'completed' || status.scannedCount >= status.totalCount) {
|
|
stopPolling()
|
|
scanning.value = false
|
|
scanProgress.status = 'completed'
|
|
scanProgress.progressPercentage = 100
|
|
ElMessage.success(`扫描完成,发现 ${scanProgress.foundDevices} 个 AMT 设备`)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取扫描状态失败:', error)
|
|
}
|
|
}, 1000)
|
|
}
|
|
|
|
const stopPolling = () => {
|
|
if (pollTimer) {
|
|
clearInterval(pollTimer)
|
|
pollTimer = null
|
|
}
|
|
}
|
|
|
|
const handleCancelScan = async () => {
|
|
try {
|
|
await scanApi.cancelScan(scanProgress.taskId)
|
|
stopPolling()
|
|
scanning.value = false
|
|
scanProgress.status = 'cancelled'
|
|
ElMessage.warning('扫描已取消')
|
|
} catch (error) {
|
|
ElMessage.error('取消扫描失败')
|
|
}
|
|
}
|
|
|
|
const handleResetScan = () => {
|
|
scanFormRef.value?.resetFields()
|
|
}
|
|
|
|
// 手动添加相关方法
|
|
const handleAddDevice = async () => {
|
|
if (!manualFormRef.value) return
|
|
|
|
await manualFormRef.value.validate(async (valid: boolean) => {
|
|
if (valid) {
|
|
adding.value = true
|
|
try {
|
|
await deviceApi.addDevice({
|
|
ipAddress: manualForm.ipAddress,
|
|
hostname: manualForm.hostname || undefined,
|
|
description: manualForm.description || undefined
|
|
})
|
|
ElMessage.success('设备添加成功')
|
|
handleResetManual()
|
|
goToDeviceList()
|
|
} catch (error: any) {
|
|
ElMessage.error(error.message || '添加设备失败')
|
|
} finally {
|
|
adding.value = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleResetManual = () => {
|
|
manualFormRef.value?.resetFields()
|
|
manualForm.ipAddress = ''
|
|
manualForm.hostname = ''
|
|
manualForm.description = ''
|
|
}
|
|
|
|
const goToDeviceList = () => {
|
|
router.push('/amt/devices')
|
|
}
|
|
|
|
onUnmounted(() => {
|
|
stopPolling()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.scan-page {
|
|
padding: 0;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.progress-card {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.progress-info {
|
|
padding: 20px 0;
|
|
}
|
|
|
|
.progress-stats {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
margin-top: 30px;
|
|
}
|
|
</style>
|