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>