fix: 修复网络扫描进度显示问题,优化AMT设备列表UI
This commit is contained in:
parent
bd64e889fd
commit
ca7231ecb9
@ -6,7 +6,7 @@ export const scanApi = {
|
||||
startScan(networkSegment: string, subnetMask: string) {
|
||||
return request.post({
|
||||
url: '/api/scan/start',
|
||||
params: { networkSegment, subnetMask }
|
||||
data: { networkSegment, subnetMask }
|
||||
})
|
||||
},
|
||||
|
||||
@ -96,6 +96,15 @@ export const deviceApi = {
|
||||
return request.get({
|
||||
url: `/api/devices/${id}/credentials`
|
||||
})
|
||||
},
|
||||
|
||||
// 设置设备 AMT 凭据
|
||||
setAmtCredentials(id: number, data: { username: string; password: string }) {
|
||||
return request.put({
|
||||
url: `/api/devices/${id}/amt-credentials`,
|
||||
data: data,
|
||||
showSuccessMessage: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,174 +0,0 @@
|
||||
<template>
|
||||
<div class="credentials-page">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>AMT 凭据管理</span>
|
||||
<ElButton type="primary" @click="showAddDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加凭据
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElTable :data="credentials" v-loading="loading" style="width: 100%">
|
||||
<ElTableColumn prop="name" label="名称" width="180" />
|
||||
<ElTableColumn prop="username" label="用户名" width="150" />
|
||||
<ElTableColumn label="密码" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.hasPassword" type="success" size="small">已设置</ElTag>
|
||||
<ElTag v-else type="danger" size="small">未设置</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="默认" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.isDefault" type="primary" size="small">默认</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="description" label="描述" />
|
||||
<ElTableColumn label="操作" width="180">
|
||||
<template #default="{ row }">
|
||||
<ElButton size="small" @click="showEditDialog(row)">编辑</ElButton>
|
||||
<ElButton size="small" type="danger" @click="handleDelete(row)">删除</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<ElDialog v-model="dialogVisible" :title="isEdit ? '编辑凭据' : '添加凭据'" width="500px">
|
||||
<ElForm :model="form" label-width="100px">
|
||||
<ElFormItem label="名称" required>
|
||||
<ElInput v-model="form.name" placeholder="例如:默认凭据" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="用户名" required>
|
||||
<ElInput v-model="form.username" placeholder="例如:admin" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="密码" required>
|
||||
<ElInput v-model="form.password" type="password" placeholder="请输入AMT密码" show-password />
|
||||
<div v-if="isEdit" style="color: #909399; font-size: 12px; margin-top: 5px;">
|
||||
留空则不修改密码
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="设为默认">
|
||||
<ElSwitch v-model="form.isDefault" />
|
||||
<div style="color: #909399; font-size: 12px; margin-top: 5px;">
|
||||
默认凭据将用于扫描时的认证
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述">
|
||||
<ElInput v-model="form.description" type="textarea" :rows="3" placeholder="可选,描述此凭据的用途" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit">确定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { credentialApi } from '@/api/amt'
|
||||
|
||||
defineOptions({ name: 'AmtCredentials' })
|
||||
|
||||
const credentials = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({
|
||||
id: null as number | null,
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
isDefault: false,
|
||||
description: ''
|
||||
})
|
||||
|
||||
const loadCredentials = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
credentials.value = await credentialApi.getAllCredentials()
|
||||
} catch (error) {
|
||||
ElMessage.error('加载凭据列表失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showAddDialog = () => {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, name: '', username: '', password: '', isDefault: false, description: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const showEditDialog = (row: any) => {
|
||||
isEdit.value = true
|
||||
Object.assign(form, { id: row.id, name: row.name, username: row.username, password: '', isDefault: row.isDefault, description: row.description })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name || !form.username) {
|
||||
ElMessage.warning('请填写名称和用户名')
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEdit.value && !form.password) {
|
||||
ElMessage.warning('请填写密码')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEdit.value && form.id) {
|
||||
await credentialApi.updateCredential(form.id, form)
|
||||
} else {
|
||||
await credentialApi.createCredential(form)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadCredentials()
|
||||
} catch (error) {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '添加失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除凭据 "${row.name}" 吗?`, '确认删除', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await credentialApi.deleteCredential(row.id)
|
||||
loadCredentials()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCredentials()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.credentials-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -26,9 +26,51 @@
|
||||
</div>
|
||||
</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>
|
||||
配置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="360">
|
||||
<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>
|
||||
@ -64,16 +106,15 @@
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="系统状态" width="90">
|
||||
<ElTableColumn label="AMT账号" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.osOnline ? 'success' : 'danger'" size="small">
|
||||
{{ row.osOnline ? '运行中' : '已关机' }}
|
||||
</ElTag>
|
||||
<span v-if="row.amtUsername">{{ row.amtUsername }}</span>
|
||||
<ElTag v-else type="warning" size="small">未配置</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="Windows账号" width="120">
|
||||
<ElTableColumn label="AMT密码" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag v-if="row.windowsUsername" type="success" size="small">{{ row.windowsUsername }}</ElTag>
|
||||
<span v-if="row.amtPasswordDecrypted">{{ row.amtPasswordDecrypted }}</span>
|
||||
<ElTag v-else type="warning" size="small">未配置</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
@ -83,34 +124,11 @@
|
||||
{{ formatDateTime(row.discoveredAt) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="420" fixed="right">
|
||||
<ElTableColumn label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton type="success" size="small" @click="handleRemoteDesktop(row)" :disabled="!row.osOnline || !row.windowsUsername">
|
||||
远程桌面
|
||||
</ElButton>
|
||||
<ElButton type="info" size="small" @click="handleSetCredentials(row)">
|
||||
配置账号
|
||||
</ElButton>
|
||||
<ElDropdown trigger="click" @command="(cmd: string) => handlePowerCommand(cmd, row)" style="margin-left: 8px">
|
||||
<ElButton type="warning" size="small">
|
||||
电源管理 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
</ElButton>
|
||||
<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="primary" size="small" @click="handleViewHardware(row)" style="margin-left: 8px">
|
||||
<ElButton type="primary" size="small" @click="handleViewHardware(row)">
|
||||
硬件配置
|
||||
</ElButton>
|
||||
<ElButton type="danger" size="small" @click="handleDelete(row)">
|
||||
删除
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
@ -122,17 +140,23 @@
|
||||
<!-- 远程桌面弹窗 -->
|
||||
<RemoteDesktopModal v-model="showRemoteDesktopModal" :device="selectedDevice" />
|
||||
|
||||
<!-- 配置 Windows 账号弹窗 -->
|
||||
<ElDialog v-model="showCredentialsDialog" title="配置 Windows 登录账号" width="450px">
|
||||
<ElForm :model="credentialsForm" label-width="100px">
|
||||
<ElFormItem label="设备 IP">
|
||||
<ElInput :model-value="selectedDevice?.ipAddress" disabled />
|
||||
<!-- 配置 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="Windows 登录用户名" />
|
||||
<ElInput v-model="credentialsForm.username" placeholder="AMT 登录用户名(通常为 admin)" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="密码">
|
||||
<ElInput v-model="credentialsForm.password" type="password" placeholder="Windows 登录密码" show-password />
|
||||
<ElInput v-model="credentialsForm.password" type="password" placeholder="AMT 登录密码" show-password />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
@ -143,10 +167,11 @@
|
||||
</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 } from '@element-plus/icons-vue'
|
||||
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'
|
||||
@ -154,6 +179,7 @@ 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('')
|
||||
@ -162,6 +188,7 @@ 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)
|
||||
|
||||
@ -187,6 +214,10 @@ const fetchDevices = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedDevices.value = selection
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (searchKeyword.value) {
|
||||
loading.value = true
|
||||
@ -244,15 +275,28 @@ const stopStatusCheck = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (device: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除设备 ${device.ipAddress} 吗?`, '确认删除', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedDevices.value.length === 0) return
|
||||
|
||||
await deviceApi.deleteDevice(device.id)
|
||||
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') {
|
||||
@ -280,8 +324,15 @@ const handleRemoteDesktop = (device: any) => {
|
||||
}
|
||||
|
||||
const handleSetCredentials = async (device: any) => {
|
||||
selectedDevice.value = device
|
||||
credentialsForm.value = { username: device.windowsUsername || '', password: '' }
|
||||
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
|
||||
}
|
||||
|
||||
@ -290,17 +341,34 @@ const saveCredentials = async () => {
|
||||
ElMessage.warning('请输入用户名')
|
||||
return
|
||||
}
|
||||
if (!credentialsForm.value.password) {
|
||||
ElMessage.warning('请输入密码')
|
||||
return
|
||||
}
|
||||
|
||||
savingCredentials.value = true
|
||||
try {
|
||||
await deviceApi.setDeviceCredentials(selectedDevice.value.id, credentialsForm.value)
|
||||
let successCount = 0
|
||||
for (const device of credentialsTargetDevices.value) {
|
||||
try {
|
||||
await deviceApi.setAmtCredentials(device.id, credentialsForm.value)
|
||||
// 更新本地数据
|
||||
const device = devices.value.find(d => d.id === selectedDevice.value.id)
|
||||
if (device) {
|
||||
device.windowsUsername = credentialsForm.value.username
|
||||
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
|
||||
ElMessage.success('账号配置成功')
|
||||
if (successCount === credentialsTargetDevices.value.length) {
|
||||
ElMessage.success('AMT账号配置成功')
|
||||
} else {
|
||||
ElMessage.warning(`成功配置 ${successCount} 台,失败 ${credentialsTargetDevices.value.length - successCount} 台`)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('保存失败')
|
||||
} finally {
|
||||
@ -311,7 +379,6 @@ const saveCredentials = async () => {
|
||||
const handleFetchUuid = async (device: any) => {
|
||||
device.fetchingUuid = true
|
||||
try {
|
||||
// 强制刷新硬件信息以获取 UUID
|
||||
const hardwareInfo = await hardwareApi.getHardwareInfo(device.id, true)
|
||||
if (hardwareInfo.systemInfo?.uuid) {
|
||||
device.systemUuid = hardwareInfo.systemInfo.uuid
|
||||
@ -326,7 +393,11 @@ const handleFetchUuid = async (device: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handlePowerCommand = async (command: string, device: any) => {
|
||||
|
||||
// 电源操作
|
||||
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: '确定要关机吗?这将优雅地关闭操作系统。' },
|
||||
@ -338,23 +409,44 @@ const handlePowerCommand = async (command: string, device: any) => {
|
||||
const action = actionMap[command]
|
||||
if (!action) return
|
||||
|
||||
const deviceIps = selectedDevices.value.map(d => d.ipAddress).join(', ')
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(`设备: ${device.ipAddress}\n${action.confirmMsg}`, `确认${action.name}`, {
|
||||
await ElMessageBox.confirm(
|
||||
`选中设备: ${deviceIps}\n\n${action.confirmMsg}`,
|
||||
`${action.name}确认`,
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: command.includes('force') ? 'warning' : 'info'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
ElMessage.info(`正在执行${action.name}...`)
|
||||
|
||||
const response = await action.api(device.id)
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (const device of selectedDevices.value) {
|
||||
try {
|
||||
const response = await action.api(device.id)
|
||||
if (response.success) {
|
||||
ElMessage.success(response.message || `${action.name}命令已发送`)
|
||||
setTimeout(() => checkAllDevicesStatus(), 3000)
|
||||
successCount++
|
||||
} else {
|
||||
ElMessage.error(response.error || `${action.name}失败`)
|
||||
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}失败`)
|
||||
@ -405,4 +497,29 @@ const formatDateTime = (dateTime: string) => {
|
||||
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>
|
||||
|
||||
@ -204,6 +204,7 @@ const handleStartScan = async () => {
|
||||
|
||||
try {
|
||||
const result = await scanApi.startScan(scanForm.networkSegment, scanForm.subnetMask)
|
||||
console.log('Start scan result:', result)
|
||||
scanProgress.taskId = result.taskId
|
||||
ElMessage.success('扫描任务已启动')
|
||||
startPolling()
|
||||
@ -220,6 +221,7 @@ const startPolling = () => {
|
||||
pollTimer = window.setInterval(async () => {
|
||||
try {
|
||||
const status = await scanApi.getScanStatus(scanProgress.taskId)
|
||||
console.log('Scan status response:', status)
|
||||
scanProgress.scannedCount = status.scannedCount || 0
|
||||
scanProgress.totalCount = status.totalCount || 0
|
||||
scanProgress.foundDevices = status.foundDevices || 0
|
||||
@ -227,12 +229,14 @@ const startPolling = () => {
|
||||
? Math.round((status.scannedCount / status.totalCount) * 100)
|
||||
: 0
|
||||
|
||||
if (status.status === 'completed' || status.scannedCount >= status.totalCount) {
|
||||
// 只在状态为 completed 时才停止轮询,确保 foundDevices 已经更新
|
||||
if (status.status === 'completed') {
|
||||
stopPolling()
|
||||
scanning.value = false
|
||||
scanProgress.status = 'completed'
|
||||
scanProgress.progressPercentage = 100
|
||||
ElMessage.success(`扫描完成,发现 ${scanProgress.foundDevices} 个 AMT 设备`)
|
||||
// 使用后端返回的 foundDevices,而不是本地的
|
||||
ElMessage.success(`扫描完成,发现 ${status.foundDevices || 0} 个 AMT 设备`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取扫描状态失败:', error)
|
||||
|
||||
@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<div class="windows-credentials-page">
|
||||
<ElCard shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>Windows 凭据管理</span>
|
||||
<ElButton type="primary" @click="showAddDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加凭据
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElTable :data="credentials" v-loading="loading" style="width: 100%">
|
||||
<ElTableColumn prop="name" label="名称" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.name }}
|
||||
<ElTag v-if="row.isDefault" type="success" size="small" style="margin-left: 8px">默认</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="username" label="用户名" width="150" />
|
||||
<ElTableColumn prop="domain" label="域名" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.domain || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="note" label="备注">
|
||||
<template #default="{ row }">
|
||||
{{ row.note || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="240">
|
||||
<template #default="{ row }">
|
||||
<ElButton size="small" @click="showEditDialog(row)">编辑</ElButton>
|
||||
<ElButton v-if="!row.isDefault" size="small" type="success" @click="handleSetDefault(row)">设为默认</ElButton>
|
||||
<ElButton size="small" type="danger" @click="handleDelete(row)">删除</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<ElDialog v-model="dialogVisible" :title="isEdit ? '编辑凭据' : '添加凭据'" width="500px">
|
||||
<ElForm :model="form" label-width="100px">
|
||||
<ElFormItem label="名称" required>
|
||||
<ElInput v-model="form.name" placeholder="例如:服务器管理员" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="用户名" required>
|
||||
<ElInput v-model="form.username" placeholder="Windows 用户名" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="密码" :required="!isEdit">
|
||||
<ElInput v-model="form.password" type="password" :placeholder="isEdit ? '留空则不修改' : '密码'" show-password />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="域名">
|
||||
<ElInput v-model="form.domain" placeholder="可选,如:WORKGROUP" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="设为默认">
|
||||
<ElSwitch v-model="form.isDefault" />
|
||||
<div style="color: #909399; font-size: 12px; margin-top: 5px;">
|
||||
默认凭据将用于远程桌面连接
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput v-model="form.note" type="textarea" :rows="2" placeholder="可选备注" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit" :loading="saving">确定</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { windowsCredentialsApi } from '@/api/amt'
|
||||
|
||||
defineOptions({ name: 'WindowsCredentials' })
|
||||
|
||||
const credentials = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const form = reactive({
|
||||
id: null as number | null,
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
domain: '',
|
||||
isDefault: false,
|
||||
note: ''
|
||||
})
|
||||
|
||||
const loadCredentials = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
credentials.value = await windowsCredentialsApi.getAll()
|
||||
} catch (error) {
|
||||
ElMessage.error('加载凭据列表失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showAddDialog = () => {
|
||||
isEdit.value = false
|
||||
Object.assign(form, { id: null, name: '', username: '', password: '', domain: '', isDefault: false, note: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const showEditDialog = (row: any) => {
|
||||
isEdit.value = true
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
username: row.username,
|
||||
password: '',
|
||||
domain: row.domain || '',
|
||||
isDefault: row.isDefault,
|
||||
note: row.note || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.name || !form.username) {
|
||||
ElMessage.warning('请填写名称和用户名')
|
||||
return
|
||||
}
|
||||
|
||||
if (!isEdit.value && !form.password) {
|
||||
ElMessage.warning('请填写密码')
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
if (isEdit.value && form.id) {
|
||||
await windowsCredentialsApi.update(form.id, form)
|
||||
} else {
|
||||
await windowsCredentialsApi.create(form)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadCredentials()
|
||||
} catch (error) {
|
||||
ElMessage.error(isEdit.value ? '更新失败' : '添加失败')
|
||||
console.error(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSetDefault = async (row: any) => {
|
||||
try {
|
||||
await windowsCredentialsApi.setDefault(row.id)
|
||||
loadCredentials()
|
||||
} catch (error) {
|
||||
ElMessage.error('设置失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除凭据 "${row.name}" 吗?`, '确认删除', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await windowsCredentialsApi.delete(row.id)
|
||||
loadCredentials()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCredentials()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.windows-credentials-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -29,10 +29,32 @@ public class DevicesController : ControllerBase
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<ApiResponse<List<AmtDevice>>>> GetAllDevices()
|
||||
public async Task<ActionResult<ApiResponse<List<object>>>> GetAllDevices()
|
||||
{
|
||||
var devices = await _context.AmtDevices.ToListAsync();
|
||||
return Ok(ApiResponse<List<AmtDevice>>.Success(devices));
|
||||
|
||||
// 解密 AMT 密码返回给前端
|
||||
var result = devices.Select(d => new {
|
||||
d.Id,
|
||||
d.IpAddress,
|
||||
d.Hostname,
|
||||
d.SystemUuid,
|
||||
d.MajorVersion,
|
||||
d.MinorVersion,
|
||||
d.ProvisioningState,
|
||||
d.Description,
|
||||
d.AmtOnline,
|
||||
d.OsOnline,
|
||||
d.WindowsUsername,
|
||||
d.WindowsPassword,
|
||||
d.AmtUsername,
|
||||
AmtPasswordDecrypted = string.IsNullOrEmpty(d.AmtPassword) ? null :
|
||||
System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(d.AmtPassword)),
|
||||
d.DiscoveredAt,
|
||||
d.LastSeenAt
|
||||
}).ToList();
|
||||
|
||||
return Ok(ApiResponse<List<object>>.Success(result.Cast<object>().ToList()));
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -184,6 +206,35 @@ public class DevicesController : ControllerBase
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置设备的 AMT 登录凭据
|
||||
/// </summary>
|
||||
[HttpPut("{id}/amt-credentials")]
|
||||
public async Task<ActionResult<ApiResponse<object>>> SetAmtCredentials(long id, [FromBody] SetAmtCredentialsRequest request)
|
||||
{
|
||||
var device = await _context.AmtDevices.FindAsync(id);
|
||||
|
||||
if (device == null)
|
||||
{
|
||||
return Ok(ApiResponse<object>.Fail(404, "设备不存在"));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
|
||||
{
|
||||
return Ok(ApiResponse<object>.Fail(400, "用户名和密码不能为空"));
|
||||
}
|
||||
|
||||
device.AmtUsername = request.Username;
|
||||
// 简单加密存储密码(生产环境应使用更安全的加密方式)
|
||||
device.AmtPassword = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(request.Password));
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Updated AMT credentials for device {Id} ({Ip})", id, device.IpAddress);
|
||||
|
||||
return Ok(ApiResponse<object>.Success(null, "AMT凭据设置成功"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检测所有设备的在线状态
|
||||
/// </summary>
|
||||
@ -407,3 +458,12 @@ public class AddDeviceRequest
|
||||
public string? WindowsUsername { get; set; }
|
||||
public string? WindowsPassword { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置设备 AMT 凭据请求
|
||||
/// </summary>
|
||||
public class SetAmtCredentialsRequest
|
||||
{
|
||||
public string Username { get; set; } = string.Empty;
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@ -44,10 +44,8 @@ public class ScanController : ControllerBase
|
||||
FoundDevices = 0
|
||||
};
|
||||
|
||||
// Start scan in background
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var progress = new Progress<ScanProgress>(async p =>
|
||||
// 创建进度回调 - 直接使用 Action 而不是 Progress<T>
|
||||
Action<ScanProgress> progressCallback = p =>
|
||||
{
|
||||
// 更新状态存储
|
||||
if (_scanStatuses.TryGetValue(taskId, out var status))
|
||||
@ -56,28 +54,36 @@ public class ScanController : ControllerBase
|
||||
status.TotalCount = p.TotalCount;
|
||||
status.FoundDevices = p.FoundDevices;
|
||||
status.CurrentIp = p.CurrentIp;
|
||||
|
||||
_logger.LogInformation("Progress update: scanned={Scanned}, total={Total}, found={Found}, ip={Ip}",
|
||||
p.ScannedCount, p.TotalCount, p.FoundDevices, p.CurrentIp);
|
||||
}
|
||||
|
||||
await _hubContext.Clients.All.SendAsync("ReceiveScanProgress", p);
|
||||
});
|
||||
// 异步发送 SignalR 通知(不等待)
|
||||
_ = _hubContext.Clients.All.SendAsync("ReceiveScanProgress", p);
|
||||
};
|
||||
|
||||
// Start scan in background
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _scannerService.ScanNetworkAsync(
|
||||
var foundDevicesList = await _scannerService.ScanNetworkAsync(
|
||||
taskId,
|
||||
request.NetworkSegment,
|
||||
request.SubnetMask,
|
||||
progress
|
||||
progressCallback
|
||||
);
|
||||
|
||||
// 更新状态为完成
|
||||
// 更新状态为完成,并确保 foundDevices 是正确的
|
||||
if (_scanStatuses.TryGetValue(taskId, out var status))
|
||||
{
|
||||
status.Status = "completed";
|
||||
status.FoundDevices = foundDevicesList.Count;
|
||||
_logger.LogInformation("Scan task {TaskId} completed with {Count} devices found", taskId, foundDevicesList.Count);
|
||||
}
|
||||
|
||||
// Send completion notification
|
||||
_logger.LogInformation("Scan task {TaskId} completed", taskId);
|
||||
await _hubContext.Clients.All.SendAsync("ScanCompleted", new { taskId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -102,6 +108,8 @@ public class ScanController : ControllerBase
|
||||
{
|
||||
if (_scanStatuses.TryGetValue(taskId, out var status))
|
||||
{
|
||||
_logger.LogDebug("GetScanStatus: taskId={TaskId}, status={Status}, scanned={Scanned}, total={Total}, found={Found}",
|
||||
taskId, status.Status, status.ScannedCount, status.TotalCount, status.FoundDevices);
|
||||
return Ok(ApiResponse<ScanStatusInfo>.Success(status));
|
||||
}
|
||||
|
||||
|
||||
@ -192,5 +192,6 @@ public class AppDbContext : DbContext
|
||||
|
||||
modelBuilder.Entity<AmtDevice>()
|
||||
.HasIndex(d => d.SystemUuid);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,7 +97,10 @@ public static class DbSeeder
|
||||
|
||||
private static async Task SeedMenusAsync(AppDbContext context)
|
||||
{
|
||||
if (await context.Menus.AnyAsync()) return;
|
||||
if (await context.Menus.AnyAsync())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var menus = new List<Menu>
|
||||
{
|
||||
@ -106,13 +109,12 @@ public static class DbSeeder
|
||||
new() { Id = 2, ParentId = 1, Name = "Console", Path = "console", Component = "/dashboard/console", Title = "menus.dashboard.console", KeepAlive = false, Sort = 1, Roles = "R_SUPER,R_ADMIN,R_USER", IsSystem = true },
|
||||
|
||||
// AMT 设备管理菜单(系统内置)
|
||||
new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "AMT设备管理", Icon = "ri:computer-line", Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
new() { Id = 5, Name = "AmtManage", Path = "/amt", Component = "/index/index", Title = "AMT设备管理", Icon = "ri:computer-line", Sort = 3, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
new() { Id = 6, ParentId = 5, Name = "AmtScan", Path = "scan", Component = "/amt/scan", Title = "设备扫描", KeepAlive = true, Sort = 1, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
new() { Id = 7, ParentId = 5, Name = "AmtDevices", Path = "devices", Component = "/amt/devices", Title = "设备管理", KeepAlive = true, Sort = 2, Roles = "R_SUPER,R_ADMIN", IsSystem = true },
|
||||
new() { Id = 8, ParentId = 5, Name = "AmtCredentials", Path = "credentials", Component = "/amt/credentials", Title = "AMT凭据", KeepAlive = true, Sort = 3, 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 = 3, 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 },
|
||||
|
||||
// 系统管理菜单(系统内置)
|
||||
|
||||
801
backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.Designer.cs
generated
Normal file
801
backend-csharp/AmtScanner.Api/Migrations/20260121095352_AddDeviceReservation.Designer.cs
generated
Normal file
@ -0,0 +1,801 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AmtScanner.Api.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AmtScanner.Api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260121095352_AddDeviceReservation")]
|
||||
partial class AddDeviceReservation
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.AmtCredential", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("AmtCredentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.AmtDevice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("AmtOnline")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("DiscoveredAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Hostname")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<DateTime>("LastSeenAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("MajorVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("MinorVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("OsOnline")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<int>("ProvisioningState")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SystemUuid")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("WindowsPassword")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("WindowsUsername")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IpAddress")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SystemUuid");
|
||||
|
||||
b.ToTable("AmtDevices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.DeviceReservation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long>("DeviceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("EndTime")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("StartTime")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "Status", "EndTime");
|
||||
|
||||
b.ToTable("DeviceReservations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long>("DeviceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("LastUpdated")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("ProcessorCores")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("ProcessorCurrentClockSpeed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("ProcessorMaxClockSpeed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ProcessorModel")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int?>("ProcessorThreads")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SystemManufacturer")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("SystemModel")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("SystemSerialNumber")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("SystemUuid")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<long?>("TotalMemoryBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("LastUpdated");
|
||||
|
||||
b.ToTable("HardwareInfos");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.MemoryModule", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("CapacityBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("HardwareInfoId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Manufacturer")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("MemoryType")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("PartNumber")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("SerialNumber")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("SlotLocation")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int?>("SpeedMHz")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("HardwareInfoId");
|
||||
|
||||
b.ToTable("MemoryModules");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("AuthList")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("varchar(1000)");
|
||||
|
||||
b.Property<string>("Component")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Icon")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<bool>("IsHide")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsHideTab")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsIframe")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<bool>("KeepAlive")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Link")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<int?>("ParentId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("Roles")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<int>("Sort")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Menus");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.OsDevice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("AmtDeviceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Architecture")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("DiscoveredAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Hostname")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<bool>("IsOnline")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<DateTime?>("LastBootTime")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime?>("LastOnlineAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<DateTime>("LastUpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("LoggedInUser")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("MacAddress")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("OsType")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("OsVersion")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("SystemUuid")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AmtDeviceId");
|
||||
|
||||
b.HasIndex("IpAddress")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("SystemUuid");
|
||||
|
||||
b.ToTable("OsDevices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long>("DeviceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<bool>("IsUsed")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<int>("MaxUseCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("varchar(64)");
|
||||
|
||||
b.Property<int>("UseCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("UsedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("RemoteAccessTokens");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.Role", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("RoleCode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
|
||||
b.Property<string>("RoleName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleCode")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Roles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b =>
|
||||
{
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("MenuId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("RoleId", "MenuId");
|
||||
|
||||
b.HasIndex("MenuId");
|
||||
|
||||
b.ToTable("RoleMenus");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("CapacityBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("DeviceId")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<long>("HardwareInfoId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("InterfaceType")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("Model")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("HardwareInfoId");
|
||||
|
||||
b.ToTable("StorageDevices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.User", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Avatar")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("Gender")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1)
|
||||
.HasColumnType("varchar(1)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("NickName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpiryTime")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1)
|
||||
.HasColumnType("varchar(1)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("UpdatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserName")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b =>
|
||||
{
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("UserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.WindowsCredential", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Domain")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("varchar(500)");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("varchar(200)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name");
|
||||
|
||||
b.ToTable("WindowsCredentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.DeviceReservation", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AmtScanner.Api.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.MemoryModule", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo")
|
||||
.WithMany("MemoryModules")
|
||||
.HasForeignKey("HardwareInfoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("HardwareInfo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.Menu", "Parent")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("ParentId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.OsDevice", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.AmtDevice", "AmtDevice")
|
||||
.WithMany()
|
||||
.HasForeignKey("AmtDeviceId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("AmtDevice");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.RemoteAccessToken", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.RoleMenu", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.Menu", "Menu")
|
||||
.WithMany("RoleMenus")
|
||||
.HasForeignKey("MenuId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AmtScanner.Api.Models.Role", "Role")
|
||||
.WithMany("RoleMenus")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Menu");
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.StorageDevice", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.HardwareInfo", "HardwareInfo")
|
||||
.WithMany("StorageDevices")
|
||||
.HasForeignKey("HardwareInfoId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("HardwareInfo");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.UserRole", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.Role", "Role")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AmtScanner.Api.Models.User", "User")
|
||||
.WithMany("UserRoles")
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Role");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||
{
|
||||
b.Navigation("MemoryModules");
|
||||
|
||||
b.Navigation("StorageDevices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.Menu", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
|
||||
b.Navigation("RoleMenus");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.Role", b =>
|
||||
{
|
||||
b.Navigation("RoleMenus");
|
||||
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.User", b =>
|
||||
{
|
||||
b.Navigation("UserRoles");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AmtScanner.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDeviceReservation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DeviceReservations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
DeviceId = table.Column<long>(type: "bigint", nullable: false),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
StartTime = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
EndTime = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
AccessToken = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
Note = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DeviceReservations", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_DeviceReservations_AmtDevices_DeviceId",
|
||||
column: x => x.DeviceId,
|
||||
principalTable: "AmtDevices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_DeviceReservations_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeviceReservations_DeviceId",
|
||||
table: "DeviceReservations",
|
||||
column: "DeviceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeviceReservations_DeviceId_Status_EndTime",
|
||||
table: "DeviceReservations",
|
||||
columns: new[] { "DeviceId", "Status", "EndTime" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DeviceReservations_UserId",
|
||||
table: "DeviceReservations",
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DeviceReservations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -115,6 +115,47 @@ namespace AmtScanner.Api.Migrations
|
||||
b.ToTable("AmtDevices");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.DeviceReservation", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<long>("DeviceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("EndTime")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTime>("StartTime")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DeviceId");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("DeviceId", "Status", "EndTime");
|
||||
|
||||
b.ToTable("DeviceReservations");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@ -605,6 +646,25 @@ namespace AmtScanner.Api.Migrations
|
||||
b.ToTable("WindowsCredentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.DeviceReservation", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeviceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AmtScanner.Api.Models.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Device");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AmtScanner.Api.Models.HardwareInfo", b =>
|
||||
{
|
||||
b.HasOne("AmtScanner.Api.Models.AmtDevice", "Device")
|
||||
|
||||
@ -45,6 +45,16 @@ public class AmtDevice
|
||||
/// </summary>
|
||||
public string? WindowsPassword { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AMT 登录用户名
|
||||
/// </summary>
|
||||
public string? AmtUsername { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AMT 登录密码(加密存储)
|
||||
/// </summary>
|
||||
public string? AmtPassword { get; set; }
|
||||
|
||||
public DateTime DiscoveredAt { get; set; }
|
||||
|
||||
public DateTime LastSeenAt { get; set; }
|
||||
|
||||
@ -32,7 +32,7 @@ public class AmtScannerService : IAmtScannerService
|
||||
string taskId,
|
||||
string networkSegment,
|
||||
string subnetMask,
|
||||
IProgress<ScanProgress> progress,
|
||||
Action<ScanProgress> progressCallback,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_logger.LogInformation("Starting network scan for task: {TaskId}", taskId);
|
||||
@ -69,10 +69,13 @@ public class AmtScannerService : IAmtScannerService
|
||||
foundDevices.Add(device);
|
||||
var found = Interlocked.Increment(ref foundCount);
|
||||
|
||||
_logger.LogInformation("Found AMT device at {Ip}, total found: {Found}", ip, found);
|
||||
|
||||
// Save to database
|
||||
await SaveDeviceAsync(device);
|
||||
|
||||
progress.Report(new ScanProgress
|
||||
// 直接调用回调,不使用 Progress<T>
|
||||
progressCallback(new ScanProgress
|
||||
{
|
||||
TaskId = taskId,
|
||||
ScannedCount = scanned,
|
||||
@ -85,7 +88,8 @@ public class AmtScannerService : IAmtScannerService
|
||||
}
|
||||
else
|
||||
{
|
||||
progress.Report(new ScanProgress
|
||||
// 直接调用回调,不使用 Progress<T>
|
||||
progressCallback(new ScanProgress
|
||||
{
|
||||
TaskId = taskId,
|
||||
ScannedCount = scanned,
|
||||
|
||||
@ -104,7 +104,12 @@ public class GuacamoleService : IGuacamoleService
|
||||
["enable-menu-animations"] = "true",
|
||||
["disable-bitmap-caching"] = "false",
|
||||
["disable-offscreen-caching"] = "false",
|
||||
["disable-glyph-caching"] = "false"
|
||||
["disable-glyph-caching"] = "false",
|
||||
// 启用驱动器重定向(文件传输)
|
||||
["enable-drive"] = "true",
|
||||
["drive-name"] = "共享文件夹",
|
||||
["drive-path"] = "/drive",
|
||||
["create-drive-path"] = "true"
|
||||
},
|
||||
["attributes"] = new Dictionary<string, string>()
|
||||
};
|
||||
@ -204,7 +209,12 @@ public class GuacamoleService : IGuacamoleService
|
||||
["enable-font-smoothing"] = "true",
|
||||
["enable-full-window-drag"] = "true",
|
||||
["enable-desktop-composition"] = "true",
|
||||
["enable-menu-animations"] = "true"
|
||||
["enable-menu-animations"] = "true",
|
||||
// 启用驱动器重定向(文件传输)
|
||||
["enable-drive"] = "true",
|
||||
["drive-name"] = "共享文件夹",
|
||||
["drive-path"] = "/drive",
|
||||
["create-drive-path"] = "true"
|
||||
},
|
||||
["attributes"] = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ namespace AmtScanner.Api.Services;
|
||||
|
||||
public interface IAmtScannerService
|
||||
{
|
||||
Task<List<AmtDevice>> ScanNetworkAsync(string taskId, string networkSegment, string subnetMask, IProgress<ScanProgress> progress, CancellationToken cancellationToken = default);
|
||||
Task<List<AmtDevice>> ScanNetworkAsync(string taskId, string networkSegment, string subnetMask, Action<ScanProgress> progressCallback, CancellationToken cancellationToken = default);
|
||||
void CancelScan(string taskId);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
-- 为 AmtDevices 表添加 AMT 登录凭据字段
|
||||
-- 执行前请确保已备份数据库
|
||||
|
||||
-- 添加 AmtUsername 字段
|
||||
ALTER TABLE `AmtDevices` ADD COLUMN `AmtUsername` VARCHAR(100) NULL AFTER `WindowsPassword`;
|
||||
|
||||
-- 添加 AmtPassword 字段(加密存储)
|
||||
ALTER TABLE `AmtDevices` ADD COLUMN `AmtPassword` VARCHAR(500) NULL AFTER `AmtUsername`;
|
||||
|
||||
-- 验证字段是否添加成功
|
||||
-- SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'AmtDevices' AND COLUMN_NAME IN ('AmtUsername', 'AmtPassword');
|
||||
5
backend-csharp/AmtScanner.Api/delete_computer_use.sql
Normal file
5
backend-csharp/AmtScanner.Api/delete_computer_use.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- 删除"电脑使用"菜单
|
||||
DELETE FROM Menus WHERE Path = '/computer-use';
|
||||
|
||||
-- 删除预约表(如果存在)
|
||||
DROP TABLE IF EXISTS DeviceReservations;
|
||||
@ -210,3 +210,21 @@ ports:
|
||||
2. 修改数据库默认密码
|
||||
3. 生产环境使用 HTTPS(配置 Nginx 反向代理)
|
||||
4. 限制访问 IP 范围
|
||||
|
||||
## 文件传输功能
|
||||
|
||||
系统已启用 Guacamole 的驱动器重定向功能,可以在远程桌面中传输文件。
|
||||
|
||||
### 使用方法
|
||||
|
||||
1. 连接远程桌面后,打开 **文件资源管理器**
|
||||
2. 在左侧导航栏找到 **网络位置** 或 **此电脑**
|
||||
3. 会看到一个名为 **共享文件夹** 的网络驱动器
|
||||
4. 将文件拖放到该驱动器即可上传,从该驱动器复制文件即可下载
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 共享文件夹存储在 Docker 卷 `guacd-drive` 中
|
||||
- 所有连接共享同一个文件夹,请注意文件管理
|
||||
- 大文件传输可能较慢,取决于网络带宽
|
||||
- 如需为每个用户/连接分配独立文件夹,需要进一步配置
|
||||
|
||||
@ -6,6 +6,8 @@ services:
|
||||
image: guacamole/guacd:latest
|
||||
container_name: guacd
|
||||
restart: always
|
||||
volumes:
|
||||
- guacd-drive:/drive
|
||||
networks:
|
||||
- guacamole-net
|
||||
|
||||
@ -50,3 +52,4 @@ networks:
|
||||
|
||||
volumes:
|
||||
guacamole-db-data:
|
||||
guacd-drive:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user