526 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="devices-page">
<ElCard shadow="never">
<template #header>
<div class="card-header">
<span>AMT 设备管理</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>
检测中...
</ElTag>
<ElInput
v-model="searchKeyword"
placeholder="搜索 IP 地址"
style="width: 200px; margin-right: 10px"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</ElInput>
<ElButton type="primary" :icon="Refresh" @click="handleRefresh">刷新</ElButton>
</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>
配置AMT账号
</ElButton>
<ElDropdown trigger="click" @command="handleBatchPowerCommand" :disabled="selectedDevices.length === 0">
<ElButton type="warning" :disabled="selectedDevices.length === 0">
<el-icon><Lightning /></el-icon>
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="power-on" :icon="VideoPlay">开机</ElDropdownItem>
<ElDropdownItem command="power-off" :icon="VideoPause">关机</ElDropdownItem>
<ElDropdownItem command="restart" :icon="RefreshRight">重启</ElDropdownItem>
<ElDropdownItem divided command="force-off" :icon="CircleClose">强制关机</ElDropdownItem>
<ElDropdownItem command="force-restart" :icon="Refresh">强制重启</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<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%"
@selection-change="handleSelectionChange"
>
<ElTableColumn type="selection" width="50" />
<ElTableColumn prop="ipAddress" label="IP 地址" width="140" />
<ElTableColumn prop="systemUuid" label="系统 UUID" width="320">
<template #default="{ row }">
<div style="display: flex; align-items: center; gap: 8px;">
<span v-if="row.systemUuid" style="font-family: monospace; font-size: 12px;">{{ row.systemUuid }}</span>
<ElTag v-else type="info" size="small">未获取</ElTag>
<ElButton
type="primary"
size="small"
link
@click="handleFetchUuid(row)"
:loading="row.fetchingUuid"
>
{{ row.systemUuid ? '刷新' : '获取' }}
</ElButton>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="AMT 版本" width="100">
<template #default="{ row }">
{{ row.majorVersion }}.{{ row.minorVersion }}
</template>
</ElTableColumn>
<ElTableColumn label="配置状态" width="100">
<template #default="{ row }">
<ElTag :type="getStateTagType(row.provisioningState)" size="small">
{{ getProvisioningStateText(row.provisioningState) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="AMT状态" width="90">
<template #default="{ row }">
<ElTag :type="row.amtOnline ? 'success' : 'info'" size="small">
{{ row.amtOnline ? '在线' : '离线' }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="AMT账号" width="120">
<template #default="{ row }">
<span v-if="row.amtUsername">{{ row.amtUsername }}</span>
<ElTag v-else type="warning" size="small">未配置</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="AMT密码" width="120">
<template #default="{ row }">
<span v-if="row.amtPasswordDecrypted">{{ row.amtPasswordDecrypted }}</span>
<ElTag v-else type="warning" size="small">未配置</ElTag>
</template>
</ElTableColumn>
<ElTableColumn prop="hostname" label="主机名" min-width="120" />
<ElTableColumn label="发现时间" width="160">
<template #default="{ row }">
{{ formatDateTime(row.discoveredAt) }}
</template>
</ElTableColumn>
<ElTableColumn label="操作" width="120" fixed="right">
<template #default="{ row }">
<ElButton type="primary" size="small" @click="handleViewHardware(row)">
硬件配置
</ElButton>
</template>
</ElTableColumn>
</ElTable>
</ElCard>
<!-- 硬件信息弹窗 -->
<HardwareInfoModal v-model="showHardwareModal" :device-id="selectedDeviceId" />
<!-- 远程桌面弹窗 -->
<RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" />
<!-- 配置 AMT 账号弹窗 -->
<ElDialog v-model="showCredentialsDialog" title="配置 AMT 登录账号" width="500px">
<div v-if="credentialsTargetDevices.length > 1" class="target-devices-info">
<ElAlert type="info" :closable="false" show-icon>
将为以下 {{ credentialsTargetDevices.length }} 台设备配置相同的 AMT 账号:
<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="AMT 登录用户名(通常为 admin" />
</ElFormItem>
<ElFormItem label="密码">
<ElInput v-model="credentialsForm.password" type="password" placeholder="AMT 登录密码" show-password />
</ElFormItem>
</ElForm>
<template #footer>
<ElButton @click="showCredentialsDialog = false">取消</ElButton>
<ElButton type="primary" @click="saveCredentials" :loading="savingCredentials">保存</ElButton>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, ArrowDown, VideoPlay, VideoPause, RefreshRight, CircleClose, Delete, Lightning, Key } from '@element-plus/icons-vue'
import { deviceApi, powerApi, hardwareApi } from '@/api/amt'
import HardwareInfoModal from './modules/hardware-info-modal.vue'
import RemoteDesktopModal from './modules/remote-desktop-modal.vue'
defineOptions({ name: 'AmtDevices' })
const devices = ref<any[]>([])
const selectedDevices = ref<any[]>([])
const loading = ref(false)
const isCheckingStatus = ref(false)
const searchKeyword = ref('')
const showHardwareModal = ref(false)
const showRemoteDesktopModal = ref(false)
const showCredentialsDialog = ref(false)
const selectedDeviceId = ref(0)
const selectedDevice = ref<any>(null)
const credentialsTargetDevices = ref<any[]>([])
const credentialsForm = ref({ username: '', password: '' })
const savingCredentials = ref(false)
let statusCheckInterval: number | null = null
onMounted(() => {
fetchDevices()
startStatusCheck()
})
onUnmounted(() => {
stopStatusCheck()
})
const fetchDevices = async () => {
loading.value = true
try {
devices.value = await deviceApi.getAllDevices()
} catch (error) {
console.error('获取设备列表失败:', error)
} finally {
loading.value = false
}
}
const handleSelectionChange = (selection: any[]) => {
selectedDevices.value = selection
}
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 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)
}
const stopStatusCheck = () => {
if (statusCheckInterval) {
clearInterval(statusCheckInterval)
statusCheckInterval = null
}
}
const handleBatchDelete = async () => {
if (selectedDevices.value.length === 0) return
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedDevices.value.length} 台设备吗?`,
'删除确认',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
)
const ids = selectedDevices.value.map(d => d.id)
let successCount = 0
for (const id of ids) {
try {
await deviceApi.deleteDevice(id)
successCount++
} catch (e) {
console.error('删除设备失败:', id, e)
}
}
ElMessage.success(`成功删除 ${successCount} 台设备`)
fetchDevices()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error('删除失败')
}
}
}
const handleViewHardware = (device: any) => {
selectedDeviceId.value = device.id
showHardwareModal.value = true
}
const handleRemoteDesktop = (device: any) => {
if (!device.osOnline) {
ElMessage.warning('设备操作系统未运行,无法连接远程桌面')
return
}
if (!device.windowsUsername) {
ElMessage.warning('请先配置该设备的 Windows 登录账号')
return
}
selectedDevice.value = device
showRemoteDesktopModal.value = true
}
const handleSetCredentials = async (device: any) => {
credentialsTargetDevices.value = [device]
credentialsForm.value = { username: device.amtUsername || 'admin', password: '' }
showCredentialsDialog.value = true
}
const handleBatchSetCredentials = () => {
if (selectedDevices.value.length === 0) return
credentialsTargetDevices.value = [...selectedDevices.value]
credentialsForm.value = { username: 'admin', password: '' }
showCredentialsDialog.value = true
}
const saveCredentials = async () => {
if (!credentialsForm.value.username) {
ElMessage.warning('请输入用户名')
return
}
if (!credentialsForm.value.password) {
ElMessage.warning('请输入密码')
return
}
savingCredentials.value = true
try {
let successCount = 0
for (const device of credentialsTargetDevices.value) {
try {
await deviceApi.setAmtCredentials(device.id, credentialsForm.value)
// 更新本地数据
const d = devices.value.find(item => item.id === device.id)
if (d) {
d.amtUsername = credentialsForm.value.username
}
successCount++
} catch (e) {
console.error('配置AMT账号失败:', device.ipAddress, e)
}
}
showCredentialsDialog.value = false
if (successCount === credentialsTargetDevices.value.length) {
ElMessage.success('AMT账号配置成功')
} else {
ElMessage.warning(`成功配置 ${successCount} 台,失败 ${credentialsTargetDevices.value.length - successCount}`)
}
} catch (error) {
ElMessage.error('保存失败')
} finally {
savingCredentials.value = false
}
}
const handleFetchUuid = async (device: any) => {
device.fetchingUuid = true
try {
const hardwareInfo = await hardwareApi.getHardwareInfo(device.id, true)
if (hardwareInfo.systemInfo?.uuid) {
device.systemUuid = hardwareInfo.systemInfo.uuid
ElMessage.success('UUID 获取成功')
} else {
ElMessage.warning('未能从设备获取 UUID')
}
} catch (error: any) {
ElMessage.error('获取 UUID 失败: ' + (error.message || '未知错误'))
} finally {
device.fetchingUuid = false
}
}
// 电源操作
const handleBatchPowerCommand = async (command: string) => {
if (selectedDevices.value.length === 0) 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: '确定要关机吗?这将优雅地关闭操作系统。' },
'restart': { api: powerApi.restart, name: '重启', confirmMsg: '确定要重启吗?这将优雅地重启操作系统。' },
'force-off': { api: powerApi.forceOff, name: '强制关机', confirmMsg: '确定要强制关机吗?这可能导致数据丢失!' },
'force-restart': { api: powerApi.forceRestart, name: '强制重启', confirmMsg: '确定要强制重启吗?这可能导致数据丢失!' }
}
const action = actionMap[command]
if (!action) return
const deviceIps = selectedDevices.value.map(d => d.ipAddress).join(', ')
try {
await ElMessageBox.confirm(
`选中设备: ${deviceIps}\n\n${action.confirmMsg}`,
`${action.name}确认`,
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: command.includes('force') ? 'warning' : 'info'
}
)
ElMessage.info(`正在执行${action.name}...`)
let successCount = 0
let failCount = 0
for (const device of selectedDevices.value) {
try {
const response = await action.api(device.id)
if (response.success) {
successCount++
} else {
failCount++
}
} catch (e) {
failCount++
}
}
if (successCount > 0) {
ElMessage.success(`${action.name}命令已发送: 成功 ${successCount}${failCount > 0 ? `, 失败 ${failCount}` : ''}`)
} else {
ElMessage.error(`${action.name}失败`)
}
setTimeout(() => checkAllDevicesStatus(), 3000)
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(`${action.name}失败`)
}
}
}
const getProvisioningStateText = (state: string) => {
const stateMap: Record<string, string> = {
'PRE': '预配置',
'IN': '配置中',
'POST': '已配置',
'UNKNOWN': '未知'
}
return stateMap[state] || state
}
const getStateTagType = (state: string) => {
const typeMap: Record<string, string> = {
'PRE': 'warning',
'IN': 'primary',
'POST': 'success',
'UNKNOWN': 'info'
}
return typeMap[state] || 'info'
}
const formatDateTime = (dateTime: string) => {
if (!dateTime) return '-'
return new Date(dateTime).toLocaleString('zh-CN')
}
</script>
<style scoped>
.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;
}
</style>