fex: 样式修改
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="选择网络" width="800px" append-to-body @close="handleClose">
|
||||||
|
<div class="selector-container">
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-input v-model="keyword" placeholder="搜索网络" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-select v-model="typeFilter" placeholder="网络类型" clearable style="width: 130px" @change="handleSearch">
|
||||||
|
<el-option label="网桥(Bridge)" value="bridge" />
|
||||||
|
<el-option label="内网(NAT)" value="nat" />
|
||||||
|
</el-select>
|
||||||
|
<el-button :icon="Refresh" @click="loadList" circle />
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||||
|
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||||
|
<el-table-column prop="id" label="ID" width="60" />
|
||||||
|
<el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column label="类型" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.type === 'bridge' ? 'success' : 'warning'" size="small">
|
||||||
|
{{ row.type === 'bridge' ? '网桥' : 'NAT' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="address" label="地址(CIDR)" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="gateway" label="网关" width="130" />
|
||||||
|
<el-table-column prop="nameservers" label="DNS" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="bridge_name" label="网桥名称" width="100" />
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination-wrapper" v-if="total > 0">
|
||||||
|
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||||
|
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||||
|
@current-change="p => { page = p; loadList() }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { getNetworkList } from '@/api/admin/kvmService'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
serviceId: { type: Number, default: 0 },
|
||||||
|
hostId: { type: Number, default: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const keyword = ref('')
|
||||||
|
const typeFilter = ref('')
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
if (val) {
|
||||||
|
page.value = 1
|
||||||
|
keyword.value = ''
|
||||||
|
typeFilter.value = ''
|
||||||
|
selectedItem.value = null
|
||||||
|
loadList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(visible, (val) => emit('update:modelValue', val))
|
||||||
|
|
||||||
|
const handleSearch = () => { page.value = 1; loadList() }
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
if (!props.serviceId || !props.hostId) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = { service_id: props.serviceId, host_id: props.hostId, page: page.value, page_size: pageSize.value }
|
||||||
|
if (keyword.value) params.keyword = keyword.value
|
||||||
|
if (typeFilter.value) params.type = typeFilter.value
|
||||||
|
const res = await getNetworkList(params)
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const inner = res.data.data
|
||||||
|
list.value = inner.data || inner.networks || (Array.isArray(inner) ? inner : [])
|
||||||
|
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||||
|
} else { list.value = []; total.value = 0 }
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||||
|
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (selectedItem.value) {
|
||||||
|
emit('confirm', selectedItem.value)
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleClose = () => { selectedItem.value = null }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selector-container { min-height: 200px; }
|
||||||
|
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||||
|
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||||
|
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="选择安全组" width="700px" append-to-body @close="handleClose">
|
||||||
|
<div class="selector-container">
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-input v-model="keyword" placeholder="搜索安全组" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-button :icon="Refresh" @click="loadList" circle />
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||||
|
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||||
|
<el-table-column prop="id" label="ID" width="60" />
|
||||||
|
<el-table-column prop="name" label="名称" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="方向" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.direction === 'in' ? 'primary' : 'warning'" size="small">{{ row.direction === 'in' ? '入站' : '出站' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="锁定" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.lock ? 'danger' : 'info'" size="small">{{ row.lock ? '是' : '否' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="白名单" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="note" label="备注" min-width="120" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination-wrapper" v-if="total > 0">
|
||||||
|
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||||
|
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||||
|
@current-change="p => { page = p; loadList() }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { getSecurityGroupList } from '@/api/admin/kvmService'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
serviceId: { type: Number, default: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const keyword = ref('')
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
if (val) { page.value = 1; keyword.value = ''; selectedItem.value = null; loadList() }
|
||||||
|
})
|
||||||
|
watch(visible, (val) => emit('update:modelValue', val))
|
||||||
|
|
||||||
|
const handleSearch = () => { page.value = 1; loadList() }
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
if (!props.serviceId) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = { service_id: props.serviceId, page: page.value, page_size: pageSize.value }
|
||||||
|
if (keyword.value) params.keyword = keyword.value
|
||||||
|
const res = await getSecurityGroupList(params)
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const inner = res.data.data
|
||||||
|
list.value = inner.groups || inner.post_groups || inner.data || (Array.isArray(inner) ? inner : [])
|
||||||
|
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||||
|
} else { list.value = []; total.value = 0 }
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||||
|
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (selectedItem.value) { emit('confirm', selectedItem.value); visible.value = false }
|
||||||
|
}
|
||||||
|
const handleClose = () => { selectedItem.value = null }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selector-container { min-height: 200px; }
|
||||||
|
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||||
|
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||||
|
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="选择数据卷" width="750px" append-to-body @close="handleClose">
|
||||||
|
<div class="selector-container">
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-input v-model="keyword" placeholder="搜索数据卷" clearable style="width: 200px" @keyup.enter="handleSearch" @clear="handleSearch">
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-select v-model="statusFilter" placeholder="状态" clearable style="width: 120px" @change="handleSearch">
|
||||||
|
<el-option label="就绪" value="ready" />
|
||||||
|
<el-option label="等待中" value="pending" />
|
||||||
|
</el-select>
|
||||||
|
<el-button :icon="Refresh" @click="loadList" circle />
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading" :data="list" highlight-current-row @current-change="handleCurrentChange"
|
||||||
|
:height="340" :row-class-name="rowClassName" size="small" stripe>
|
||||||
|
<el-table-column prop="id" label="ID" width="60" />
|
||||||
|
<el-table-column prop="name" label="名称" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="大小" width="90">
|
||||||
|
<template #default="{ row }">{{ row.size ? row.size + ' GB' : '-' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="类型" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_system ? 'danger' : ''" size="small">{{ row.is_system ? '系统盘' : '数据盘' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="挂载" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.is_mount ? 'warning' : 'success'" size="small">{{ row.is_mount ? '已挂载' : '未挂载' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="path" label="路径" min-width="180" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
<div class="pagination-wrapper" v-if="total > 0">
|
||||||
|
<el-pagination v-model:current-page="page" v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50]" :total="total" layout="total, sizes, prev, pager, next" small
|
||||||
|
@size-change="s => { pageSize = s; page = 1; loadList() }"
|
||||||
|
@current-change="p => { page = p; loadList() }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :disabled="!selectedItem" @click="handleConfirm">确认选择</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||||
|
import { getVolumeList } from '@/api/admin/kvmService'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Boolean, default: false },
|
||||||
|
serviceId: { type: Number, default: 0 },
|
||||||
|
hostId: { type: Number, default: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'confirm'])
|
||||||
|
|
||||||
|
const visible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const keyword = ref('')
|
||||||
|
const statusFilter = ref('')
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (val) => {
|
||||||
|
visible.value = val
|
||||||
|
if (val) {
|
||||||
|
page.value = 1
|
||||||
|
keyword.value = ''
|
||||||
|
statusFilter.value = ''
|
||||||
|
selectedItem.value = null
|
||||||
|
loadList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(visible, (val) => emit('update:modelValue', val))
|
||||||
|
|
||||||
|
const handleSearch = () => { page.value = 1; loadList() }
|
||||||
|
|
||||||
|
const loadList = async () => {
|
||||||
|
if (!props.serviceId || !props.hostId) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = { service_id: props.serviceId, host_id: props.hostId, page: page.value, page_size: pageSize.value }
|
||||||
|
if (keyword.value) params.keyword = keyword.value
|
||||||
|
if (statusFilter.value) params.status = statusFilter.value
|
||||||
|
const res = await getVolumeList(params)
|
||||||
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
|
const inner = res.data.data
|
||||||
|
list.value = inner.data || inner.volumes || (Array.isArray(inner) ? inner : [])
|
||||||
|
total.value = inner.meta?.count ?? inner.total ?? list.value.length
|
||||||
|
} else { list.value = []; total.value = 0 }
|
||||||
|
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusType = (s) => ({ ready: 'success', pending: 'warning', error: 'danger' }[s] || 'info')
|
||||||
|
const statusLabel = (s) => ({ ready: '就绪', pending: '等待中', creating: '创建中', error: '错误' }[s] || s || '-')
|
||||||
|
|
||||||
|
const rowClassName = ({ row }) => row.id === selectedItem.value?.id ? 'selected-row' : ''
|
||||||
|
const handleCurrentChange = (row) => { selectedItem.value = row }
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (selectedItem.value) {
|
||||||
|
emit('confirm', selectedItem.value)
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleClose = () => { selectedItem.value = null }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selector-container { min-height: 200px; }
|
||||||
|
.filter-bar { display: flex; gap: 8px; margin-bottom: 12px; align-items: center; }
|
||||||
|
.pagination-wrapper { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||||
|
:deep(.selected-row) { background-color: #ecf5ff !important; }
|
||||||
|
</style>
|
||||||
@@ -27,9 +27,7 @@
|
|||||||
<el-option label="网桥(Bridge)" value="bridge" />
|
<el-option label="网桥(Bridge)" value="bridge" />
|
||||||
<el-option label="内网(NAT)" value="nat" />
|
<el-option label="内网(NAT)" value="nat" />
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-select v-if="!injectedHostId?.value" v-model="hostIdInput" placeholder="选择宿主机" clearable filterable style="width: 220px" @change="handleSearch">
|
|
||||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 网络列表 -->
|
<!-- 网络列表 -->
|
||||||
@@ -255,16 +253,24 @@ const handleSubmit = () => {
|
|||||||
if (!valid) return
|
if (!valid) return
|
||||||
submitLoading.value = true
|
submitLoading.value = true
|
||||||
try {
|
try {
|
||||||
const payload = { ...formData, service_id: serviceId.value }
|
const fd = new FormData()
|
||||||
// 空高级参数不提交
|
fd.append('service_id', serviceId.value)
|
||||||
const optionalFields = ['mac_address', 'bridge_name', 'ls_bridge_name', 'ls_name', 'nameservers', 'target_device']
|
fd.append('name', formData.name)
|
||||||
optionalFields.forEach(f => { if (!payload[f]) delete payload[f] })
|
fd.append('address', formData.address)
|
||||||
|
fd.append('gateway', formData.gateway)
|
||||||
|
fd.append('type', formData.type)
|
||||||
|
fd.append('host_id', formData.host_id)
|
||||||
|
if (formData.nameservers) fd.append('nameservers', formData.nameservers)
|
||||||
|
if (formData.mac_address) fd.append('mac_address', formData.mac_address)
|
||||||
|
if (formData.bridge_name) fd.append('bridge_name', formData.bridge_name)
|
||||||
|
if (formData.ls_bridge_name) fd.append('ls_bridge_name', formData.ls_bridge_name)
|
||||||
|
if (formData.ls_name) fd.append('ls_name', formData.ls_name)
|
||||||
let res
|
let res
|
||||||
if (dialogType.value === 'add') {
|
if (dialogType.value === 'add') {
|
||||||
delete payload.id
|
res = await createNetwork(fd)
|
||||||
res = await createNetwork(payload)
|
|
||||||
} else {
|
} else {
|
||||||
res = await updateNetwork(payload)
|
fd.append('network_id', formData.id)
|
||||||
|
res = await updateNetwork(fd)
|
||||||
}
|
}
|
||||||
if (res?.data?.code === 200) {
|
if (res?.data?.code === 200) {
|
||||||
ElMessage.success(dialogType.value === 'add' ? '创建成功' : '修改成功')
|
ElMessage.success(dialogType.value === 'add' ? '创建成功' : '修改成功')
|
||||||
@@ -274,7 +280,7 @@ const handleSubmit = () => {
|
|||||||
ElMessage.error(extractApiError(res?.data, '操作失败'))
|
ElMessage.error(extractApiError(res?.data, '操作失败'))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('操作失败: ' + (e?.response?.data?.message || e.message))
|
ElMessage.error(extractApiError(e?.response?.data, '操作失败'))
|
||||||
} finally { submitLoading.value = false }
|
} finally { submitLoading.value = false }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<el-button link type="primary" size="small" @click="handleGoDetail(row)">编辑</el-button>
|
<el-button link type="primary" size="small" @click="handleGoDetail(row)">编辑</el-button>
|
||||||
<el-button link type="success" size="small" @click="handleSync(row)">同步</el-button>
|
<el-button link type="success" size="small" @click="handleSync(row)">同步</el-button>
|
||||||
<el-button link type="warning" size="small" @click="handleApply(row)">应用</el-button>
|
<el-button link type="warning" size="small" @click="handleApply(row)">应用</el-button>
|
||||||
<el-dropdown trigger="click" @command="cmd => handleRowMore(row, cmd)" style="margin-left: 4px">
|
<el-dropdown trigger="click" @command="cmd => handleRowMore(row, cmd)" style="margin-left: 4px;margin-top: 4.5px">
|
||||||
<el-button link type="info" size="small">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
<el-button link type="info" size="small">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
|
|||||||
@@ -154,7 +154,10 @@
|
|||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3 class="section-title">网络信息</h3>
|
<h3 class="section-title">网络信息</h3>
|
||||||
<el-button size="small" type="primary" @click="handleAddNetwork">添加网络</el-button>
|
<div style="display: flex; gap: 8px">
|
||||||
|
<el-button size="small" type="primary" @click="showCreateNetworkDialog">创建网络</el-button>
|
||||||
|
<el-button size="small" @click="handleAddNetwork">添加已有网络</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-if="vmNetworks.length" :data="pagedNetworks" size="small" stripe>
|
<el-table v-if="vmNetworks.length" :data="pagedNetworks" size="small" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="60" />
|
<el-table-column prop="id" label="ID" width="60" />
|
||||||
@@ -187,7 +190,10 @@
|
|||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3 class="section-title">磁盘卷信息</h3>
|
<h3 class="section-title">磁盘卷信息</h3>
|
||||||
<el-button size="small" type="primary" @click="handleAddVolume">添加数据卷</el-button>
|
<div style="display: flex; gap: 8px">
|
||||||
|
<el-button size="small" type="primary" @click="showCreateVolumeDialog">创建数据卷</el-button>
|
||||||
|
<el-button size="small" @click="handleAddVolume">挂载已有数据卷</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-if="vmVolumes.length" :data="pagedVolumes" size="small" stripe>
|
<el-table v-if="vmVolumes.length" :data="pagedVolumes" size="small" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="60" />
|
<el-table-column prop="id" label="ID" width="60" />
|
||||||
@@ -232,20 +238,31 @@
|
|||||||
<div class="section-block">
|
<div class="section-block">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3 class="section-title">安全组管理</h3>
|
<h3 class="section-title">安全组管理</h3>
|
||||||
<el-button size="small" type="primary" @click="handleBindSgFromTab">绑定安全组</el-button>
|
<div style="display: flex; gap: 8px; align-items: center">
|
||||||
|
<el-select v-model="sgDirectionFilter" placeholder="方向" clearable style="width: 100px" size="small">
|
||||||
|
<el-option label="入站" value="in" />
|
||||||
|
<el-option label="出站" value="out" />
|
||||||
|
</el-select>
|
||||||
|
<el-button size="small" type="primary" @click="handleBindSgFromTab">绑定安全组</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table v-if="vmSecurityGroups.length" :data="pagedSecurityGroups" size="small" stripe>
|
<el-table v-if="filteredSecurityGroups.length" :data="pagedSecurityGroups" size="small" stripe>
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
<el-table-column prop="name" label="名称" min-width="120" />
|
<el-table-column prop="name" label="名称" min-width="120" />
|
||||||
<el-table-column prop="note" label="备注" min-width="160" show-overflow-tooltip />
|
<el-table-column label="方向" width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.direction === 'in' ? 'primary' : 'warning'" size="small">{{ row.direction === 'in' ? '入站' : '出站' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="note" label="备注" min-width="140" show-overflow-tooltip />
|
||||||
<el-table-column label="锁定" width="80">
|
<el-table-column label="锁定" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.lock ? 'danger' : 'info'" size="small">{{ row.lock ? '已锁定' : '未锁定' }}</el-tag>
|
<el-tag :type="row.lock ? 'danger' : 'info'" size="small">{{ row.lock ? '已锁定' : '未锁定' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="白名单" width="90">
|
<el-table-column label="白名单" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '已开启' : '未开启' }}</el-tag>
|
<el-tag :type="row.drop_all ? 'warning' : 'info'" size="small">{{ row.drop_all ? '开启' : '关闭' }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="共享" width="80">
|
<el-table-column label="共享" width="80">
|
||||||
@@ -260,9 +277,9 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<el-empty v-else description="暂无绑定的安全组" :image-size="60" />
|
<el-empty v-else description="暂无绑定的安全组" :image-size="60" />
|
||||||
<div class="pagination-wrapper" v-if="vmSecurityGroups.length > 0">
|
<div class="pagination-wrapper" v-if="filteredSecurityGroups.length > 0">
|
||||||
<el-pagination v-model:current-page="securityPage" v-model:page-size="securityPageSize"
|
<el-pagination v-model:current-page="securityPage" v-model:page-size="securityPageSize"
|
||||||
:page-sizes="[10, 20, 50]" :total="vmSecurityGroups.length" layout="total, sizes, prev, pager, next" small
|
:page-sizes="[10, 20, 50]" :total="filteredSecurityGroups.length" layout="total, sizes, prev, pager, next" small
|
||||||
@size-change="s => { securityPageSize = s; securityPage = 1 }"
|
@size-change="s => { securityPageSize = s; securityPage = 1 }"
|
||||||
@current-change="p => { securityPage = p }" />
|
@current-change="p => { securityPage = p }" />
|
||||||
</div>
|
</div>
|
||||||
@@ -547,13 +564,21 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 重构虚拟机弹窗 -->
|
<!-- 重构虚拟机弹窗 -->
|
||||||
<el-dialog v-model="refactorDialogVisible" title="重构虚拟机" width="600px" destroy-on-close>
|
<el-dialog v-model="refactorDialogVisible" title="重构虚拟机" width="650px" destroy-on-close>
|
||||||
<el-alert title="重构会修改虚拟机的底层配置参数,请谨慎操作!" type="warning" :closable="false" style="margin-bottom: 16px" />
|
<el-alert title="重构会修改虚拟机的底层配置参数,请谨慎操作!" type="warning" :closable="false" style="margin-bottom: 16px" />
|
||||||
<el-form ref="refactorFormRef" :model="refactorForm" label-width="120px">
|
<el-form ref="refactorFormRef" :model="refactorForm" label-width="130px">
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="内存(KB)">
|
<el-form-item label="内存">
|
||||||
<el-input-number v-model="refactorForm.memory" :min="0" :step="1024" controls-position="right" style="width: 100%" />
|
<div style="display: flex; align-items: center; gap: 6px; width: 100%">
|
||||||
|
<el-input-number v-model="refactorMemDisplay" :min="0" :step="refactorMemUnit === 1048576 ? 1 : (refactorMemUnit === 1024 ? 512 : 1024)" :precision="refactorMemUnit === 1048576 ? 2 : 0" controls-position="right" style="flex: 1" />
|
||||||
|
<el-select v-model="refactorMemUnit" style="width: 80px" @change="onRefactorMemUnitChange">
|
||||||
|
<el-option label="KB" :value="1" />
|
||||||
|
<el-option label="MB" :value="1024" />
|
||||||
|
<el-option label="GB" :value="1048576" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
<span style="color: #909399; font-size: 12px">({{ refactorForm.memory }} KB)</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -564,12 +589,12 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="下行带宽">
|
<el-form-item label="下行带宽(Mbps)">
|
||||||
<el-input-number v-model="refactorForm.rx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
<el-input-number v-model="refactorForm.rx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="上行带宽">
|
<el-form-item label="上行带宽(Mbps)">
|
||||||
<el-input-number v-model="refactorForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
<el-input-number v-model="refactorForm.tx_bandwidth" :min="0" controls-position="right" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -577,6 +602,18 @@
|
|||||||
<el-form-item label="Root密码">
|
<el-form-item label="Root密码">
|
||||||
<el-input v-model="refactorForm.root_password" placeholder="不修改留空" show-password />
|
<el-input v-model="refactorForm.root_password" placeholder="不修改留空" show-password />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="UUID">
|
||||||
|
<el-input v-model="refactorForm.uuid" placeholder="虚拟机UUID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Mate Data ID">
|
||||||
|
<el-input v-model="refactorForm.mate_data_id" placeholder="元数据ID" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="物理机名称">
|
||||||
|
<el-input v-model="refactorForm.physical_name" placeholder="不修改留空" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配置路径">
|
||||||
|
<el-input v-model="refactorForm.config_path" placeholder="不修改留空" />
|
||||||
|
</el-form-item>
|
||||||
<el-row :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="SSH端口">
|
<el-form-item label="SSH端口">
|
||||||
@@ -592,6 +629,21 @@
|
|||||||
<el-form-item label="VNC密码">
|
<el-form-item label="VNC密码">
|
||||||
<el-input v-model="refactorForm.vnc_password" placeholder="不填随机" show-password />
|
<el-input v-model="refactorForm.vnc_password" placeholder="不填随机" show-password />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="网络">
|
||||||
|
<div style="width: 100%">
|
||||||
|
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px" v-if="refactorSelectedNetworks.length">
|
||||||
|
<el-tag v-for="n in refactorSelectedNetworks" :key="n.id" closable size="small" @close="removeRefactorNetwork(n.id)">
|
||||||
|
{{ n.name }} (ID:{{ n.id }})
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-button size="small" @click="showRefactorNetworkSelector = true">选择网络</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="外网网络">
|
||||||
|
<el-select v-model="refactorForm.internet_network_id" placeholder="选择外网网络(可选)" clearable filterable style="width: 100%">
|
||||||
|
<el-option v-for="n in refactorSelectedNetworks" :key="n.id" :label="`${n.name} (ID: ${n.id})`" :value="n.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="安全组">
|
<el-form-item label="安全组">
|
||||||
<el-select v-model="refactorForm.port_group_id" placeholder="选择安全组(可选)" filterable clearable style="width: 100%">
|
<el-select v-model="refactorForm.port_group_id" placeholder="选择安全组(可选)" filterable clearable style="width: 100%">
|
||||||
<el-option v-for="g in sgOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
|
<el-option v-for="g in sgOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
|
||||||
@@ -604,6 +656,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 重构用网络选择器 -->
|
||||||
|
<NetworkSelectorPopup v-model="showRefactorNetworkSelector" :service-id="serviceId" :host-id="vmHostId" @confirm="handleRefactorNetworkConfirm" />
|
||||||
|
|
||||||
<!-- VNC 连接弹窗 -->
|
<!-- VNC 连接弹窗 -->
|
||||||
<el-dialog v-model="vncDialogVisible" title="获取 VNC 连接" width="560px" destroy-on-close>
|
<el-dialog v-model="vncDialogVisible" title="获取 VNC 连接" width="560px" destroy-on-close>
|
||||||
<el-form label-width="100px">
|
<el-form label-width="100px">
|
||||||
@@ -629,22 +684,8 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 绑定/解绑安全组弹窗 -->
|
<!-- 绑定/解绑安全组弹窗 -->
|
||||||
<el-dialog v-model="sgDialogVisible" :title="sgDialogType === 'bind' ? '绑定安全组' : '解绑安全组'" width="480px" destroy-on-close>
|
<!-- 绑定安全组(弹窗选择组件) -->
|
||||||
<el-form label-width="100px">
|
<SecurityGroupSelectorPopup v-model="sgBindSelectorVisible" :service-id="serviceId" @confirm="handleSgBindConfirm" />
|
||||||
<el-form-item label="虚拟机">{{ detail?.name || '-' }}</el-form-item>
|
|
||||||
<el-form-item label="安全组">
|
|
||||||
<el-select v-model="sgSelectedId" placeholder="选择安全组" filterable style="width: 100%">
|
|
||||||
<el-option v-for="g in sgOptions" :key="g.id" :label="`${g.name} (ID: ${g.id})`" :value="g.id" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="sgDialogVisible = false">取消</el-button>
|
|
||||||
<el-button :type="sgDialogType === 'bind' ? 'success' : 'warning'" :loading="actionLoading" @click="submitSgAction">
|
|
||||||
{{ sgDialogType === 'bind' ? '绑定' : '解绑' }}
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 编辑网络弹窗 -->
|
<!-- 编辑网络弹窗 -->
|
||||||
<el-dialog v-model="netEditVisible" title="编辑网络" width="560px" destroy-on-close>
|
<el-dialog v-model="netEditVisible" title="编辑网络" width="560px" destroy-on-close>
|
||||||
@@ -695,62 +736,78 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 添加网络弹窗(从已有列表选择) -->
|
<!-- 创建网络弹窗 -->
|
||||||
<el-dialog v-model="netAddVisible" title="添加网络到虚拟机" width="600px" destroy-on-close>
|
<el-dialog v-model="netCreateVisible" title="创建网络" width="600px" destroy-on-close>
|
||||||
<el-form label-width="100px">
|
<el-form ref="netCreateFormRef" :model="netCreateForm" :rules="netCreateRules" label-width="120px">
|
||||||
<el-form-item label="选择宿主机">
|
<el-form-item label="名称" prop="name">
|
||||||
<el-select v-model="netAddHostId" placeholder="选择宿主机以加载网络列表" filterable style="width: 100%" @change="loadAvailableNetworks">
|
<el-input v-model="netCreateForm.name" placeholder="网络名称" />
|
||||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
</el-form-item>
|
||||||
|
<el-form-item label="网络类型" prop="type">
|
||||||
|
<el-select v-model="netCreateForm.type" style="width: 100%">
|
||||||
|
<el-option label="网桥(Bridge/外网)" value="bridge" />
|
||||||
|
<el-option label="内网(NAT)" value="nat" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="选择网络">
|
<el-form-item label="IP 地址(CIDR)" prop="address">
|
||||||
<el-select v-model="netAddSelectedId" placeholder="请先选择宿主机" filterable style="width: 100%" :loading="netOptionsLoading" :disabled="!netAddHostId">
|
<el-input v-model="netCreateForm.address" placeholder="例如 192.168.1.0/24" />
|
||||||
<el-option v-for="n in availableNetworks" :key="n.id" :label="`${n.name} - ${n.address || ''}`" :value="n.id" />
|
</el-form-item>
|
||||||
</el-select>
|
<el-form-item label="网关地址" prop="gateway">
|
||||||
|
<el-input v-model="netCreateForm.gateway" placeholder="例如 192.168.1.1" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="DNS 服务器">
|
||||||
|
<el-input v-model="netCreateForm.nameservers" placeholder="默认 114.114.114.114,8.8.8.8" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-divider content-position="left">高级配置(可选)</el-divider>
|
||||||
|
<el-form-item label="MAC 地址">
|
||||||
|
<el-input v-model="netCreateForm.mac_address" placeholder="不填则随机" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="虚拟网桥名">
|
||||||
|
<el-input v-model="netCreateForm.bridge_name" placeholder="不填使用默认" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="逻辑网桥名">
|
||||||
|
<el-input v-model="netCreateForm.ls_bridge_name" placeholder="不填使用默认" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="逻辑端口名">
|
||||||
|
<el-input v-model="netCreateForm.ls_name" placeholder="不填使用默认" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div v-if="netAddSelectedId" style="margin-top: 8px">
|
|
||||||
<el-descriptions :column="2" border size="small">
|
|
||||||
<el-descriptions-item label="名称">{{ selectedNetworkInfo?.name || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="IP地址">{{ selectedNetworkInfo?.address || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="网关">{{ selectedNetworkInfo?.gateway || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="类型">{{ selectedNetworkInfo?.type || '-' }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="netAddVisible = false">取消</el-button>
|
<el-button @click="netCreateVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="actionLoading" @click="submitAddNetwork" :disabled="!netAddSelectedId">添加</el-button>
|
<el-button type="primary" :loading="actionLoading" @click="submitCreateNetwork">创建</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 挂载数据卷弹窗(从已有列表选择) -->
|
<!-- 添加已有网络弹窗 -->
|
||||||
<el-dialog v-model="volAddVisible" title="挂载数据卷到虚拟机" width="600px" destroy-on-close>
|
<NetworkSelectorPopup v-model="netAddVisible" :service-id="serviceId" :host-id="vmHostId" @confirm="handleNetworkSelectorConfirm" />
|
||||||
<el-form label-width="100px">
|
|
||||||
<el-form-item label="选择宿主机">
|
<!-- 创建数据卷弹窗 -->
|
||||||
<el-select v-model="volAddHostId" placeholder="选择宿主机以加载数据卷列表" filterable style="width: 100%" @change="loadAvailableVolumes">
|
<el-dialog v-model="volCreateVisible" title="创建数据卷" width="550px" destroy-on-close>
|
||||||
<el-option v-for="h in hostOptions" :key="h.id" :label="`${h.name} (${h.ip || h.id})`" :value="h.id" />
|
<el-form ref="volCreateFormRef" :model="volCreateForm" :rules="volCreateRules" label-width="120px">
|
||||||
</el-select>
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="volCreateForm.name" placeholder="数据卷名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="选择数据卷">
|
<el-form-item label="大小(GB)" prop="size">
|
||||||
<el-select v-model="volAddSelectedId" placeholder="请先选择宿主机" filterable style="width: 100%" :loading="volOptionsLoading" :disabled="!volAddHostId">
|
<el-input-number v-model="volCreateForm.size" :min="1" :step="10" style="width: 100%" />
|
||||||
<el-option v-for="v in availableVolumes" :key="v.id" :label="`${v.name} (${v.size || 0} GB, ID: ${v.id})`" :value="v.id" />
|
</el-form-item>
|
||||||
</el-select>
|
<el-form-item label="是否系统镜像">
|
||||||
|
<el-switch v-model="volCreateForm.is_system" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="挂载设备名">
|
||||||
|
<el-input v-model="volCreateForm.target_device" placeholder="不填自动生成" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="所属镜像ID">
|
||||||
|
<el-input-number v-model="volCreateForm.image_id" :min="0" style="width: 100%" placeholder="镜像ID" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div v-if="volAddSelectedId" style="margin-top: 8px">
|
|
||||||
<el-descriptions :column="2" border size="small">
|
|
||||||
<el-descriptions-item label="名称">{{ selectedVolumeInfo?.name || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="大小">{{ selectedVolumeInfo?.size ? selectedVolumeInfo.size + ' GB' : '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="状态">{{ selectedVolumeInfo?.status || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="路径">{{ selectedVolumeInfo?.path || '-' }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="volAddVisible = false">取消</el-button>
|
<el-button @click="volCreateVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="actionLoading" @click="submitAddVolume" :disabled="!volAddSelectedId">挂载</el-button>
|
<el-button type="primary" :loading="actionLoading" @click="submitCreateVolume">创建</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 挂载已有数据卷弹窗 -->
|
||||||
|
<VolumeSelectorPopup v-model="volAddVisible" :service-id="serviceId" :host-id="vmHostId" @confirm="handleVolumeSelectorConfirm" />
|
||||||
|
|
||||||
<!-- 迁移卷弹窗 -->
|
<!-- 迁移卷弹窗 -->
|
||||||
<el-dialog v-model="volTransferVisible" title="迁移数据卷" width="480px" destroy-on-close>
|
<el-dialog v-model="volTransferVisible" title="迁移数据卷" width="480px" destroy-on-close>
|
||||||
<el-form :model="volTransferForm" label-width="120px">
|
<el-form :model="volTransferForm" label-width="120px">
|
||||||
@@ -808,6 +865,9 @@ import {
|
|||||||
import { extractApiError } from '@/utils/kvmErrorUtil'
|
import { extractApiError } from '@/utils/kvmErrorUtil'
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
import ImageSelectorPopup from '@/components/admin/ImageSelectorPopup.vue'
|
||||||
|
import VolumeSelectorPopup from '@/components/admin/VolumeSelectorPopup.vue'
|
||||||
|
import NetworkSelectorPopup from '@/components/admin/NetworkSelectorPopup.vue'
|
||||||
|
import SecurityGroupSelectorPopup from '@/components/admin/SecurityGroupSelectorPopup.vue'
|
||||||
import { useTagsViewStore } from '@/store/tagsViewStore'
|
import { useTagsViewStore } from '@/store/tagsViewStore'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -823,6 +883,7 @@ const actionLoading = ref(false)
|
|||||||
const statusLoading = ref(false)
|
const statusLoading = ref(false)
|
||||||
const metricsLoading = ref(false)
|
const metricsLoading = ref(false)
|
||||||
const detail = ref(null)
|
const detail = ref(null)
|
||||||
|
const vmHostId = ref(0)
|
||||||
const vmNetworks = ref([])
|
const vmNetworks = ref([])
|
||||||
const vmVolumes = ref([])
|
const vmVolumes = ref([])
|
||||||
const vmImage = ref(null)
|
const vmImage = ref(null)
|
||||||
@@ -919,13 +980,14 @@ const loadDetail = async () => {
|
|||||||
vmVolumes.value = d.volumes || []
|
vmVolumes.value = d.volumes || []
|
||||||
vmImage.value = d.image || null
|
vmImage.value = d.image || null
|
||||||
vmPortGroup.value = d.in_port_group || null
|
vmPortGroup.value = d.in_port_group || null
|
||||||
|
vmHostId.value = detail.value?.host_id || vmVolumes.value[0]?.host_id || vmNetworks.value[0]?.host_id || 0
|
||||||
} else ElMessage.error(extractApiError(res?.data, '加载失败'))
|
} else ElMessage.error(extractApiError(res?.data, '加载失败'))
|
||||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加载失败')) } finally { loading.value = false }
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '加载失败')) } finally { loading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadVmVolumes = async () => {
|
const loadVmVolumes = async () => {
|
||||||
if (!detail.value) return
|
if (!detail.value) return
|
||||||
const hid = detail.value.host_id
|
const hid = vmHostId.value
|
||||||
if (!hid) return
|
if (!hid) return
|
||||||
try {
|
try {
|
||||||
const res = await getVolumeList({ service_id: serviceId.value, host_id: hid, vm_id: vmId.value, page: 1, count: 200 })
|
const res = await getVolumeList({ service_id: serviceId.value, host_id: hid, vm_id: vmId.value, page: 1, count: 200 })
|
||||||
@@ -938,7 +1000,7 @@ const loadVmVolumes = async () => {
|
|||||||
|
|
||||||
const loadVmNetworks = async () => {
|
const loadVmNetworks = async () => {
|
||||||
if (!detail.value) return
|
if (!detail.value) return
|
||||||
const hid = detail.value.host_id
|
const hid = vmHostId.value
|
||||||
if (!hid) return
|
if (!hid) return
|
||||||
try {
|
try {
|
||||||
const res = await getNetworkList({ service_id: serviceId.value, host_id: hid, page: 1, page_size: 200 })
|
const res = await getNetworkList({ service_id: serviceId.value, host_id: hid, page: 1, page_size: 200 })
|
||||||
@@ -1226,7 +1288,31 @@ const submitEditVm = async () => {
|
|||||||
// ---- 重构虚拟机 ----
|
// ---- 重构虚拟机 ----
|
||||||
const refactorDialogVisible = ref(false)
|
const refactorDialogVisible = ref(false)
|
||||||
const refactorFormRef = ref(null)
|
const refactorFormRef = ref(null)
|
||||||
const refactorForm = reactive({ memory: 0, vcpu: 0, rx_bandwidth: 0, tx_bandwidth: 0, root_password: '', ssh_port: 0, vnc_port: 0, vnc_password: '', port_group_id: 0 })
|
const refactorForm = reactive({
|
||||||
|
memory: 0, vcpu: 0, rx_bandwidth: 0, tx_bandwidth: 0,
|
||||||
|
root_password: '', uuid: '', mate_data_id: '', physical_name: '', config_path: '',
|
||||||
|
ssh_port: 0, vnc_port: 0, vnc_password: '',
|
||||||
|
internet_network_id: 0, port_group_id: 0
|
||||||
|
})
|
||||||
|
const refactorMemUnit = ref(1048576)
|
||||||
|
const refactorMemDisplay = ref(0)
|
||||||
|
const refactorSelectedNetworks = ref([])
|
||||||
|
const showRefactorNetworkSelector = ref(false)
|
||||||
|
|
||||||
|
const onRefactorMemUnitChange = () => {
|
||||||
|
refactorMemDisplay.value = refactorForm.memory ? Math.round(refactorForm.memory / refactorMemUnit.value * 100) / 100 : 0
|
||||||
|
}
|
||||||
|
watch(refactorMemDisplay, (v) => { refactorForm.memory = Math.round(v * refactorMemUnit.value) })
|
||||||
|
|
||||||
|
const handleRefactorNetworkConfirm = (network) => {
|
||||||
|
if (!refactorSelectedNetworks.value.find(n => n.id === network.id)) {
|
||||||
|
refactorSelectedNetworks.value.push({ id: network.id, name: network.name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const removeRefactorNetwork = (id) => {
|
||||||
|
refactorSelectedNetworks.value = refactorSelectedNetworks.value.filter(n => n.id !== id)
|
||||||
|
if (refactorForm.internet_network_id === id) refactorForm.internet_network_id = 0
|
||||||
|
}
|
||||||
|
|
||||||
const handleRefactorVm = async () => {
|
const handleRefactorVm = async () => {
|
||||||
if (!detail.value) return
|
if (!detail.value) return
|
||||||
@@ -1234,10 +1320,18 @@ const handleRefactorVm = async () => {
|
|||||||
Object.assign(refactorForm, {
|
Object.assign(refactorForm, {
|
||||||
memory: d.memory || 0, vcpu: d.vcpu || 0,
|
memory: d.memory || 0, vcpu: d.vcpu || 0,
|
||||||
rx_bandwidth: d.rx_bandwidth || 0, tx_bandwidth: d.tx_bandwidth || 0,
|
rx_bandwidth: d.rx_bandwidth || 0, tx_bandwidth: d.tx_bandwidth || 0,
|
||||||
root_password: '', ssh_port: d.ssh_port || 0,
|
root_password: d.root_password || '',
|
||||||
vnc_port: 0, vnc_password: '',
|
uuid: d.uuid || '', mate_data_id: d.mate_data_id || '',
|
||||||
port_group_id: vmPortGroup.value?.id || null
|
physical_name: d.physical_name || '', config_path: d.config_path || '',
|
||||||
|
ssh_port: d.ssh_port || 0, vnc_port: 0, vnc_password: '',
|
||||||
|
internet_network_id: 0,
|
||||||
|
port_group_id: vmPortGroup.value?.id || 0
|
||||||
})
|
})
|
||||||
|
refactorSelectedNetworks.value = vmNetworks.value.map(n => ({ id: n.id, name: n.name }))
|
||||||
|
const mem = d.memory || 0
|
||||||
|
if (mem >= 1048576 && mem % 1048576 === 0) { refactorMemUnit.value = 1048576; refactorMemDisplay.value = mem / 1048576 }
|
||||||
|
else if (mem >= 1024 && mem % 1024 === 0) { refactorMemUnit.value = 1024; refactorMemDisplay.value = mem / 1024 }
|
||||||
|
else { refactorMemUnit.value = 1; refactorMemDisplay.value = mem }
|
||||||
if (!sgOptions.value.length) await loadSgOptions()
|
if (!sgOptions.value.length) await loadSgOptions()
|
||||||
refactorDialogVisible.value = true
|
refactorDialogVisible.value = true
|
||||||
}
|
}
|
||||||
@@ -1253,9 +1347,15 @@ const submitRefactorVm = async () => {
|
|||||||
fd.append('rx_bandwidth', refactorForm.rx_bandwidth)
|
fd.append('rx_bandwidth', refactorForm.rx_bandwidth)
|
||||||
fd.append('tx_bandwidth', refactorForm.tx_bandwidth)
|
fd.append('tx_bandwidth', refactorForm.tx_bandwidth)
|
||||||
if (refactorForm.root_password) fd.append('root_password', refactorForm.root_password)
|
if (refactorForm.root_password) fd.append('root_password', refactorForm.root_password)
|
||||||
|
if (refactorForm.uuid) fd.append('uuid', refactorForm.uuid)
|
||||||
|
if (refactorForm.mate_data_id) fd.append('mate_data_id', refactorForm.mate_data_id)
|
||||||
|
if (refactorForm.physical_name) fd.append('physical_name', refactorForm.physical_name)
|
||||||
|
if (refactorForm.config_path) fd.append('config_path', refactorForm.config_path)
|
||||||
if (refactorForm.ssh_port) fd.append('ssh_port', refactorForm.ssh_port)
|
if (refactorForm.ssh_port) fd.append('ssh_port', refactorForm.ssh_port)
|
||||||
if (refactorForm.vnc_port) fd.append('vnc_port', refactorForm.vnc_port)
|
if (refactorForm.vnc_port) fd.append('vnc_port', refactorForm.vnc_port)
|
||||||
if (refactorForm.vnc_password) fd.append('vnc_password', refactorForm.vnc_password)
|
if (refactorForm.vnc_password) fd.append('vnc_password', refactorForm.vnc_password)
|
||||||
|
if (refactorSelectedNetworks.value.length) fd.append('network_ids', refactorSelectedNetworks.value.map(n => n.id).join(','))
|
||||||
|
if (refactorForm.internet_network_id) fd.append('internet_network_id', refactorForm.internet_network_id)
|
||||||
if (refactorForm.port_group_id) fd.append('port_group_id', refactorForm.port_group_id)
|
if (refactorForm.port_group_id) fd.append('port_group_id', refactorForm.port_group_id)
|
||||||
const res = await refactorVm(fd)
|
const res = await refactorVm(fd)
|
||||||
if (res?.data?.code === 200) { ElMessage.success('重构成功'); refactorDialogVisible.value = false; loadDetail() }
|
if (res?.data?.code === 200) { ElMessage.success('重构成功'); refactorDialogVisible.value = false; loadDetail() }
|
||||||
@@ -1317,7 +1417,6 @@ const handleGetVnc = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const submitGetVnc = async () => {
|
const submitGetVnc = async () => {
|
||||||
if (!vncNodeId.value) { ElMessage.warning('请选择VNC节点'); return }
|
|
||||||
vncLoading.value = true
|
vncLoading.value = true
|
||||||
vncResult.value = null
|
vncResult.value = null
|
||||||
try {
|
try {
|
||||||
@@ -1333,9 +1432,14 @@ const vmSecurityGroups = ref([])
|
|||||||
const sgListLoading = ref(false)
|
const sgListLoading = ref(false)
|
||||||
const securityPage = ref(1)
|
const securityPage = ref(1)
|
||||||
const securityPageSize = ref(10)
|
const securityPageSize = ref(10)
|
||||||
|
const sgDirectionFilter = ref('')
|
||||||
|
const filteredSecurityGroups = computed(() => {
|
||||||
|
if (!sgDirectionFilter.value) return vmSecurityGroups.value
|
||||||
|
return vmSecurityGroups.value.filter(g => g.direction === sgDirectionFilter.value)
|
||||||
|
})
|
||||||
const pagedSecurityGroups = computed(() => {
|
const pagedSecurityGroups = computed(() => {
|
||||||
const start = (securityPage.value - 1) * securityPageSize.value
|
const start = (securityPage.value - 1) * securityPageSize.value
|
||||||
return vmSecurityGroups.value.slice(start, start + securityPageSize.value)
|
return filteredSecurityGroups.value.slice(start, start + securityPageSize.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadVmSecurityGroups = async () => {
|
const loadVmSecurityGroups = async () => {
|
||||||
@@ -1350,9 +1454,7 @@ const loadVmSecurityGroups = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- 绑定/解绑安全组 ----
|
// ---- 绑定/解绑安全组 ----
|
||||||
const sgDialogVisible = ref(false)
|
const sgBindSelectorVisible = ref(false)
|
||||||
const sgDialogType = ref('bind')
|
|
||||||
const sgSelectedId = ref(null)
|
|
||||||
const sgOptions = ref([])
|
const sgOptions = ref([])
|
||||||
|
|
||||||
const loadSgOptions = async () => {
|
const loadSgOptions = async () => {
|
||||||
@@ -1365,21 +1467,22 @@ const loadSgOptions = async () => {
|
|||||||
} catch { /* */ }
|
} catch { /* */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBindSg = async () => {
|
const handleBindSg = () => { sgBindSelectorVisible.value = true }
|
||||||
sgDialogType.value = 'bind'; sgSelectedId.value = null
|
const handleUnbindSg = () => { sgBindSelectorVisible.value = true }
|
||||||
if (!sgOptions.value.length) await loadSgOptions()
|
const handleBindSgFromTab = () => { sgBindSelectorVisible.value = true }
|
||||||
sgDialogVisible.value = true
|
|
||||||
}
|
|
||||||
const handleUnbindSg = async () => {
|
|
||||||
sgDialogType.value = 'unbind'; sgSelectedId.value = null
|
|
||||||
if (!sgOptions.value.length) await loadSgOptions()
|
|
||||||
sgDialogVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBindSgFromTab = async () => {
|
const handleSgBindConfirm = async (sg) => {
|
||||||
sgDialogType.value = 'bind'; sgSelectedId.value = null
|
if (!sg?.id) return
|
||||||
if (!sgOptions.value.length) await loadSgOptions()
|
actionLoading.value = true
|
||||||
sgDialogVisible.value = true
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('service_id', serviceId.value)
|
||||||
|
fd.append('id', sg.id)
|
||||||
|
fd.append('vm_id', vmId.value)
|
||||||
|
const res = await bindSecurityGroup(fd)
|
||||||
|
if (res?.data?.code === 200) { ElMessage.success('绑定成功'); loadVmSecurityGroups() }
|
||||||
|
else ElMessage.error(extractApiError(res?.data, '绑定失败'))
|
||||||
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '绑定失败')) } finally { actionLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUnbindSgFromTab = async (row) => {
|
const handleUnbindSgFromTab = async (row) => {
|
||||||
@@ -1398,67 +1501,62 @@ const handleUnbindSgFromTab = async (row) => {
|
|||||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '解绑失败')) } finally { actionLoading.value = false }
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '解绑失败')) } finally { actionLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitSgAction = async () => {
|
// ---- 创建网络(表单弹窗) ----
|
||||||
if (!sgSelectedId.value) { ElMessage.warning('请选择安全组'); return }
|
const netCreateVisible = ref(false)
|
||||||
actionLoading.value = true
|
const netCreateFormRef = ref(null)
|
||||||
try {
|
const netCreateForm = reactive({ name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '' })
|
||||||
const api = sgDialogType.value === 'bind' ? bindSecurityGroup : unbindSecurityGroup
|
const netCreateRules = {
|
||||||
const fd = new FormData()
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
fd.append('service_id', serviceId.value)
|
address: [{ required: true, message: '请输入IP地址(CIDR)', trigger: 'blur' }],
|
||||||
fd.append('id', sgSelectedId.value)
|
gateway: [{ required: true, message: '请输入网关地址', trigger: 'blur' }],
|
||||||
fd.append('vm_id', vmId.value)
|
type: [{ required: true, message: '请选择网络类型', trigger: 'change' }]
|
||||||
const res = await api(fd)
|
|
||||||
if (res?.data?.code === 200) {
|
|
||||||
ElMessage.success(sgDialogType.value === 'bind' ? '绑定成功' : '解绑成功')
|
|
||||||
sgDialogVisible.value = false
|
|
||||||
loadVmSecurityGroups()
|
|
||||||
}
|
|
||||||
else ElMessage.error(extractApiError(res?.data, '操作失败'))
|
|
||||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '操作失败')) } finally { actionLoading.value = false }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 添加网络(从已有列表选择) ----
|
const showCreateNetworkDialog = () => {
|
||||||
|
Object.assign(netCreateForm, { name: '', address: '', gateway: '', nameservers: '', type: 'bridge', mac_address: '', bridge_name: '', ls_bridge_name: '', ls_name: '' })
|
||||||
|
netCreateVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitCreateNetwork = () => {
|
||||||
|
netCreateFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
actionLoading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('service_id', serviceId.value)
|
||||||
|
fd.append('name', netCreateForm.name)
|
||||||
|
fd.append('address', netCreateForm.address)
|
||||||
|
fd.append('gateway', netCreateForm.gateway)
|
||||||
|
fd.append('type', netCreateForm.type)
|
||||||
|
fd.append('host_id', vmHostId.value)
|
||||||
|
if (netCreateForm.nameservers) fd.append('nameservers', netCreateForm.nameservers)
|
||||||
|
if (netCreateForm.mac_address) fd.append('mac_address', netCreateForm.mac_address)
|
||||||
|
if (netCreateForm.bridge_name) fd.append('bridge_name', netCreateForm.bridge_name)
|
||||||
|
if (netCreateForm.ls_bridge_name) fd.append('ls_bridge_name', netCreateForm.ls_bridge_name)
|
||||||
|
if (netCreateForm.ls_name) fd.append('ls_name', netCreateForm.ls_name)
|
||||||
|
const res = await createNetwork(fd)
|
||||||
|
if (res?.data?.code === 200) { ElMessage.success('网络创建成功'); netCreateVisible.value = false; loadDetail() }
|
||||||
|
else ElMessage.error(extractApiError(res?.data, '创建失败'))
|
||||||
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 添加已有网络(弹窗选择组件) ----
|
||||||
const netAddVisible = ref(false)
|
const netAddVisible = ref(false)
|
||||||
const netAddHostId = ref(null)
|
|
||||||
const netAddSelectedId = ref(null)
|
|
||||||
const availableNetworks = ref([])
|
|
||||||
const netOptionsLoading = ref(false)
|
|
||||||
|
|
||||||
const selectedNetworkInfo = computed(() => availableNetworks.value.find(n => n.id === netAddSelectedId.value) || null)
|
const handleAddNetwork = () => { netAddVisible.value = true }
|
||||||
|
|
||||||
const loadAvailableNetworks = async (hostId) => {
|
const handleNetworkSelectorConfirm = async (network) => {
|
||||||
netAddSelectedId.value = null
|
if (!network?.id) return
|
||||||
availableNetworks.value = []
|
|
||||||
if (!hostId) return
|
|
||||||
netOptionsLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getNetworkList({ service_id: serviceId.value, host_id: hostId, used: false, page: 1, page_size: 200 })
|
|
||||||
if (res?.data?.code === 200 && res?.data?.data) {
|
|
||||||
const inner = res.data.data
|
|
||||||
availableNetworks.value = inner.networks || inner.data || (Array.isArray(inner) ? inner : [])
|
|
||||||
}
|
|
||||||
} catch { /* */ } finally { netOptionsLoading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddNetwork = () => {
|
|
||||||
netAddHostId.value = null
|
|
||||||
netAddSelectedId.value = null
|
|
||||||
availableNetworks.value = []
|
|
||||||
netAddVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitAddNetwork = async () => {
|
|
||||||
if (!netAddSelectedId.value) { ElMessage.warning('请选择网络'); return }
|
|
||||||
actionLoading.value = true
|
actionLoading.value = true
|
||||||
try {
|
try {
|
||||||
const net = selectedNetworkInfo.value
|
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append('service_id', serviceId.value)
|
fd.append('service_id', serviceId.value)
|
||||||
fd.append('vm_id', vmId.value)
|
fd.append('vm_id', vmId.value)
|
||||||
fd.append('network_id', netAddSelectedId.value)
|
fd.append('network_id', network.id)
|
||||||
if (net?.host_id) fd.append('host_id', net.host_id)
|
if (network.host_id) fd.append('host_id', network.host_id)
|
||||||
const res = await createNetwork(fd)
|
const res = await createNetwork(fd)
|
||||||
if (res?.data?.code === 200) { ElMessage.success('网络添加成功'); netAddVisible.value = false; loadDetail() }
|
if (res?.data?.code === 200) { ElMessage.success('网络添加成功'); loadDetail() }
|
||||||
else ElMessage.error(extractApiError(res?.data, '添加失败'))
|
else ElMessage.error(extractApiError(res?.data, '添加失败'))
|
||||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '添加失败')) } finally { actionLoading.value = false }
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '添加失败')) } finally { actionLoading.value = false }
|
||||||
}
|
}
|
||||||
@@ -1508,47 +1606,56 @@ const handleDeleteNetwork = (row) => {
|
|||||||
}).catch(() => {})
|
}).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 挂载数据卷(从已有列表选择) ----
|
// ---- 创建数据卷(表单弹窗) ----
|
||||||
|
const volCreateVisible = ref(false)
|
||||||
|
const volCreateFormRef = ref(null)
|
||||||
|
const volCreateForm = reactive({ name: '', size: 10, is_system: false, target_device: '', image_id: 0 })
|
||||||
|
const volCreateRules = {
|
||||||
|
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||||
|
size: [{ required: true, message: '请输入大小', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const showCreateVolumeDialog = () => {
|
||||||
|
Object.assign(volCreateForm, { name: '', size: 10, is_system: false, target_device: '', image_id: 0 })
|
||||||
|
volCreateVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitCreateVolume = () => {
|
||||||
|
volCreateFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
actionLoading.value = true
|
||||||
|
try {
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('service_id', serviceId.value)
|
||||||
|
fd.append('name', volCreateForm.name)
|
||||||
|
fd.append('size', volCreateForm.size)
|
||||||
|
fd.append('is_system', volCreateForm.is_system)
|
||||||
|
fd.append('vm_id', vmId.value)
|
||||||
|
fd.append('host_id', vmHostId.value)
|
||||||
|
if (volCreateForm.target_device) fd.append('target_device', volCreateForm.target_device)
|
||||||
|
if (volCreateForm.image_id) fd.append('image_id', volCreateForm.image_id)
|
||||||
|
const res = await createVolume(fd)
|
||||||
|
if (res?.data?.code === 200) { ElMessage.success('数据卷创建成功'); volCreateVisible.value = false; loadDetail() }
|
||||||
|
else ElMessage.error(extractApiError(res?.data, '创建失败'))
|
||||||
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '创建失败')) } finally { actionLoading.value = false }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 挂载已有数据卷(弹窗选择组件) ----
|
||||||
const volAddVisible = ref(false)
|
const volAddVisible = ref(false)
|
||||||
const volAddHostId = ref(null)
|
|
||||||
const volAddSelectedId = ref(null)
|
|
||||||
const availableVolumes = ref([])
|
|
||||||
const volOptionsLoading = ref(false)
|
|
||||||
|
|
||||||
const selectedVolumeInfo = computed(() => availableVolumes.value.find(v => v.id === volAddSelectedId.value) || null)
|
const handleAddVolume = () => { volAddVisible.value = true }
|
||||||
|
|
||||||
const loadAvailableVolumes = async (hostId) => {
|
const handleVolumeSelectorConfirm = async (volume) => {
|
||||||
volAddSelectedId.value = null
|
if (!volume?.id) return
|
||||||
availableVolumes.value = []
|
|
||||||
if (!hostId) return
|
|
||||||
volOptionsLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getVolumeList({ service_id: serviceId.value, host_id: hostId, page: 1, page_size: 200 })
|
|
||||||
if (res?.data?.code === 200 && res?.data?.data) {
|
|
||||||
const inner = res.data.data
|
|
||||||
const list = inner.volumes || inner.data || (Array.isArray(inner) ? inner : [])
|
|
||||||
availableVolumes.value = list.filter(v => !v.is_mount)
|
|
||||||
}
|
|
||||||
} catch { /* */ } finally { volOptionsLoading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddVolume = () => {
|
|
||||||
volAddHostId.value = null
|
|
||||||
volAddSelectedId.value = null
|
|
||||||
availableVolumes.value = []
|
|
||||||
volAddVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitAddVolume = async () => {
|
|
||||||
if (!volAddSelectedId.value) { ElMessage.warning('请选择数据卷'); return }
|
|
||||||
actionLoading.value = true
|
actionLoading.value = true
|
||||||
try {
|
try {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append('service_id', serviceId.value)
|
fd.append('service_id', serviceId.value)
|
||||||
fd.append('vm_id', vmId.value)
|
fd.append('vm_id', vmId.value)
|
||||||
fd.append('volume_id', volAddSelectedId.value)
|
fd.append('volume_id', volume.id)
|
||||||
const res = await mountVolume(fd)
|
const res = await mountVolume(fd)
|
||||||
if (res?.data?.code === 200) { ElMessage.success('数据卷挂载成功'); volAddVisible.value = false; loadDetail() }
|
if (res?.data?.code === 200) { ElMessage.success('数据卷挂载成功'); loadDetail() }
|
||||||
else ElMessage.error(extractApiError(res?.data, '挂载失败'))
|
else ElMessage.error(extractApiError(res?.data, '挂载失败'))
|
||||||
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '挂载失败')) } finally { actionLoading.value = false }
|
} catch (e) { ElMessage.error(extractApiError(e?.response?.data, '挂载失败')) } finally { actionLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<el-button link type="primary" size="small" @click="handleGoDetail(row)">详情</el-button>
|
<el-button link type="primary" size="small" @click="handleGoDetail(row)">详情</el-button>
|
||||||
<el-button link type="success" size="small" @click="handlePower(row, 'start')" :disabled="row.status === 'running'">启动</el-button>
|
<el-button link type="success" size="small" @click="handlePower(row, 'start')" :disabled="row.status === 'running'">启动</el-button>
|
||||||
<el-button link type="warning" size="small" @click="handlePower(row, 'stop')" :disabled="row.status === 'stopped' || row.status === 'stop'">关机</el-button>
|
<el-button link type="warning" size="small" @click="handlePower(row, 'stop')" :disabled="row.status === 'stopped' || row.status === 'stop'">关机</el-button>
|
||||||
<el-dropdown trigger="click" @command="cmd => handleMoreAction(row, cmd)" style="margin-left: 4px">
|
<el-dropdown trigger="click" @command="cmd => handleMoreAction(row, cmd)" style="margin-left: 10px;margin-top: 4.5px">
|
||||||
<el-button link type="info" size="small">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
<el-button link type="info" size="small">更多<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
@@ -209,17 +209,25 @@
|
|||||||
<el-descriptions-item label="名称">{{ currentDetail.name }}</el-descriptions-item>
|
<el-descriptions-item label="名称">{{ currentDetail.name }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="CPU">{{ currentDetail.vcpu }} 核</el-descriptions-item>
|
<el-descriptions-item label="CPU">{{ currentDetail.vcpu }} 核</el-descriptions-item>
|
||||||
<el-descriptions-item label="内存">{{ formatMemory(currentDetail.memory) }}</el-descriptions-item>
|
<el-descriptions-item label="内存">{{ formatMemory(currentDetail.memory) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="系统盘">{{ currentDetail.system_size }} GB</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag :type="vmStatusType(currentDetail.status)" size="small">{{ vmStatusLabel(currentDetail.status) }}</el-tag>
|
<el-tag :type="vmStatusType(currentDetail.status)" size="small">{{ vmStatusLabel(currentDetail.status) }}</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="下行带宽">{{ currentDetail.rx_bandwidth || 0 }} Mbps</el-descriptions-item>
|
<el-descriptions-item label="下行带宽">{{ currentDetail.rx_bandwidth || 0 }} Mbps</el-descriptions-item>
|
||||||
<el-descriptions-item label="上行带宽">{{ currentDetail.tx_bandwidth || 0 }} Mbps</el-descriptions-item>
|
<el-descriptions-item label="上行带宽">{{ currentDetail.tx_bandwidth || 0 }} Mbps</el-descriptions-item>
|
||||||
<el-descriptions-item label="宿主机">{{ getHostLabel(currentDetail.host_id) }}</el-descriptions-item>
|
<el-descriptions-item label="SSH端口">{{ currentDetail.ssh_port || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="镜像ID">{{ currentDetail.image_id || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="IP" :span="2">{{ currentDetail.ips || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="用户ID">{{ currentDetail.user_id || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="UUID" :span="2">
|
||||||
<el-descriptions-item label="宿主机组ID">{{ currentDetail.host_group_id || '-' }}</el-descriptions-item>
|
<span style="font-family: Consolas, monospace; font-size: 12px">{{ currentDetail.uuid || '-' }}</span>
|
||||||
<el-descriptions-item label="IP" :span="2">{{ currentDetail.ip || '-' }}</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Mate Data ID" :span="2">
|
||||||
|
<span style="font-family: Consolas, monospace; font-size: 12px">{{ currentDetail.mate_data_id || '-' }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="Root密码" v-if="currentDetail.root_password">
|
||||||
|
<span style="font-family: Consolas, monospace">{{ currentDetail.root_password }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="流量上限" v-if="currentDetail.traffic_max">{{ currentDetail.traffic_max }} GB</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="快照上限" v-if="currentDetail.snapshot_num">{{ currentDetail.snapshot_num }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备份上限" v-if="currentDetail.backup_num">{{ currentDetail.backup_num }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="创建时间">{{ formatTimestamp(currentDetail.created_at) }}</el-descriptions-item>
|
<el-descriptions-item label="创建时间">{{ formatTimestamp(currentDetail.created_at) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="更新时间">{{ formatTimestamp(currentDetail.updated_at) }}</el-descriptions-item>
|
<el-descriptions-item label="更新时间">{{ formatTimestamp(currentDetail.updated_at) }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
@@ -640,8 +648,13 @@ const handleViewDetail = async (row) => {
|
|||||||
const res = await getVmDetail({ service_id: serviceId.value, vm_id: row.id })
|
const res = await getVmDetail({ service_id: serviceId.value, vm_id: row.id })
|
||||||
if (res?.data?.code === 200 && res?.data?.data) {
|
if (res?.data?.code === 200 && res?.data?.data) {
|
||||||
const d = res.data.data
|
const d = res.data.data
|
||||||
// API may return data.vm nested, or data.data, or flat
|
const vm = d.data ?? d.vm ?? d
|
||||||
currentDetail.value = d.vm ?? d.data ?? d
|
vm.networks = d.networks || []
|
||||||
|
vm.volumes = d.volumes || []
|
||||||
|
vm.image = d.image || null
|
||||||
|
vm.in_port_group = d.in_port_group || null
|
||||||
|
if (!vm.ips && vm.networks?.length) vm.ips = vm.networks.map(n => n.address?.split('/')[0]).filter(Boolean).join(', ')
|
||||||
|
currentDetail.value = vm
|
||||||
}
|
}
|
||||||
} catch { /* fallback */ } finally { detailLoading.value = false }
|
} catch { /* fallback */ } finally { detailLoading.value = false }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user