Merge pull request 'master' (#2) from master into deploy
Build and Deploy Vue3 / build (push) Successful in 1m23s
Build and Deploy Vue3 / deploy (push) Successful in 1m47s

Reviewed-on: lin/ApiServer-Web-admin_dashboard_pc#2
This commit was merged in pull request #2.
This commit is contained in:
2025-10-01 01:17:14 +08:00
2 changed files with 216 additions and 16 deletions
+118 -14
View File
@@ -143,16 +143,39 @@
<el-form-item label="镜像名称" prop="name"> <el-form-item label="镜像名称" prop="name">
<el-input v-model="imageForm.name" placeholder="请输入镜像名称" /> <el-input v-model="imageForm.name" placeholder="请输入镜像名称" />
</el-form-item> </el-form-item>
<el-form-item label="文件路径" prop="path" v-if="editOr == true"> <el-form-item label="文件路径" prop="path" >
<el-input v-model="imageForm.path" placeholder="请输入镜像文件路径" /> <el-input v-model="imageForm.path" placeholder="请输入镜像文件路径" />
</el-form-item> </el-form-item>
<el-form-item label="展示名称" prop="show_name"> <el-form-item label="展示名称" prop="show_name">
<el-input v-model="imageForm.show_name" placeholder="请输入展示名称" /> <el-input v-model="imageForm.show_name" placeholder="请输入展示名称" />
</el-form-item> </el-form-item>
<el-form-item label="分类" prop="class_id" v-if="editOr == true"> <el-form-item label="分类" prop="class_id" >
<el-select v-model="imageForm.class_id" placeholder="请选择分类" clearable style="width: 100%"> <div class="category-select-container">
<el-option v-for="item in categoryList" :key="item.class_id" :label="item.name" :value="item.class_id" /> <el-select
</el-select> v-model="imageForm.class_id"
placeholder="请选择分类"
clearable
style="width: 100%"
@change="handleCategoryChange"
>
<el-option v-for="item in categoryList" :key="item.class_id" :label="item.name" :value="item.class_id" />
</el-select>
<div class="category-input-container" v-if="showNewCategoryInput">
<el-input
v-model="imageForm.class_name"
placeholder="请输入新分类名称"
style="width: 100%; margin-top: 8px"
/>
<el-button
type="primary"
size="small"
@click="createNewCategory"
style="margin-top: 8px"
>
创建新分类
</el-button>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item label="图标" prop="image_ico"> <el-form-item label="图标" prop="image_ico">
<div class="image-icon-upload"> <div class="image-icon-upload">
@@ -249,7 +272,7 @@ import {
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { getServer, getServerPlan } from '@/utils/acs/server' import { getServer, getServerPlan } from '@/utils/acs/server'
import { import {
editMirror, delMirror, getUserMirrorList, addVirtualMirror, getImageTypeList editMirror, delMirror, getUserMirrorList, addVirtualMirror, getImageTypeList, createImageType
} from '@/utils/acs/mirror' } from '@/utils/acs/mirror'
import { uploadFile, getFileList } from '@/utils/acs/message' import { uploadFile, getFileList } from '@/utils/acs/message'
import { mainUrl } from '@/utils/request' import { mainUrl } from '@/utils/request'
@@ -298,13 +321,15 @@ const imageForm = reactive({
image_ico: '', image_ico: '',
server_id: '', server_id: '',
path: '', path: '',
class_id: '' class_id: '',
class_name: ''
}) })
const rules = { const rules = {
name: [{ required: true, message: '请输入镜像名称', trigger: 'blur' }], // 根据接口文档,所有字段都是可选的
path: [{ required: true, message: '请输入镜像文件路径', trigger: 'blur' }], // name: [{ required: true, message: '请输入镜像名称', trigger: 'blur' }],
show_name: [{ required: true, message: '请输入展示名称', trigger: 'blur' }], // path: [{ required: true, message: '请输入镜像文件路径', trigger: 'blur' }],
// show_name: [{ required: true, message: '请输入展示名称', trigger: 'blur' }],
// plan_id: [{ required: true, message: '请选择套餐', trigger: 'change' }] // plan_id: [{ required: true, message: '请选择套餐', trigger: 'change' }]
} }
@@ -320,6 +345,7 @@ const picList = ref([])
const total = ref(10) const total = ref(10)
const currentIndex = ref(null) const currentIndex = ref(null)
const categoryList = ref([]) const categoryList = ref([])
const showNewCategoryInput = ref(false)
// 获取操作系统图标 // 获取操作系统图标
const getOsIcon = (os) => { const getOsIcon = (os) => {
@@ -447,6 +473,47 @@ const fetchCategoryList = async (serverId) => {
} }
} }
// 处理分类选择变化
const handleCategoryChange = (value) => {
if (value) {
// 选择了现有分类,清空新分类名称
imageForm.class_name = ''
showNewCategoryInput.value = false
} else {
// 清空选择,显示新分类输入框
showNewCategoryInput.value = true
}
}
// 创建新分类
const createNewCategory = async () => {
if (!imageForm.class_name.trim()) {
ElMessage.warning('请输入分类名称')
return
}
try {
const response = await createImageType(nowserver_id.value, imageForm.class_name.trim(), '')
if (response.data.code === 200) {
ElMessage.success('分类创建成功')
// 重新获取分类列表
await fetchCategoryList(nowserver_id.value)
// 设置新创建的分类为选中状态
const newCategory = categoryList.value.find(item => item.name === imageForm.class_name.trim())
if (newCategory) {
imageForm.class_id = newCategory.class_id
imageForm.class_name = ''
showNewCategoryInput.value = false
}
} else {
ElMessage.error('创建分类失败:' + response.data.message)
}
} catch (error) {
console.error('创建分类出错:', error)
ElMessage.error('创建分类失败')
}
}
// 新增镜像 // 新增镜像
const toLoad = async (data) => { const toLoad = async (data) => {
dialogVisible.value = true dialogVisible.value = true
@@ -456,6 +523,7 @@ const toLoad = async (data) => {
}) })
imageForm.server_id = data imageForm.server_id = data
nowserver_id.value = data nowserver_id.value = data
showNewCategoryInput.value = true // 添加镜像时默认显示新分类输入框
try { try {
// 获取套餐列表 // 获取套餐列表
let res = await getServerPlan({ server_id: data }) let res = await getServerPlan({ server_id: data })
@@ -515,6 +583,7 @@ const handleEdit = async (row) => {
editOr.value = true editOr.value = true
dialogVisible.value = true dialogVisible.value = true
showNewCategoryInput.value = false // 编辑时默认不显示新分类输入框
for (const key in row) { for (const key in row) {
if (row.hasOwnProperty(key)) { if (row.hasOwnProperty(key)) {
imageForm[key] = row[key] imageForm[key] = row[key]
@@ -553,6 +622,7 @@ const handleCreateVM = (row) => {
// 关闭对话框 // 关闭对话框
const handleDialogClose = () => { const handleDialogClose = () => {
dialogVisible.value = false dialogVisible.value = false
showNewCategoryInput.value = false
if (imageFormRef.value) { if (imageFormRef.value) {
imageFormRef.value.resetFields() imageFormRef.value.resetFields()
} }
@@ -564,10 +634,27 @@ const submitForm = async () => {
await imageFormRef.value.validate((valid) => { await imageFormRef.value.validate((valid) => {
if (valid) { if (valid) {
// 准备提交数据,根据接口要求处理分类字段
const submitData = { ...imageForm }
// 如果选择了现有分类,清空 class_name
if (submitData.class_id) {
submitData.class_name = ''
}
// 如果没有选择现有分类但有新分类名称,清空 class_id
else if (submitData.class_name) {
submitData.class_id = ''
}
// 如果都没有,清空两个字段
else {
submitData.class_id = ''
submitData.class_name = ''
}
if (editOr.value) { if (editOr.value) {
imageForm.image_id = imageForm.id submitData.image_id = submitData.id
delete imageForm.id delete submitData.id
editMirror(imageForm).then(res => { editMirror(submitData).then(res => {
if (res.data.code == 200) { if (res.data.code == 200) {
dialogVisible.value = false dialogVisible.value = false
ElMessage.success('编辑成功') ElMessage.success('编辑成功')
@@ -579,7 +666,7 @@ const submitForm = async () => {
ElMessage.error('编辑失败') ElMessage.error('编辑失败')
}) })
} else { } else {
addVirtualMirror(imageForm).then(res => { addVirtualMirror(submitData).then(res => {
if (res.data.code == 200) { if (res.data.code == 200) {
dialogVisible.value = false dialogVisible.value = false
ElMessage.success('添加成功') ElMessage.success('添加成功')
@@ -882,6 +969,23 @@ onMounted(() => {
border-radius: 4px; border-radius: 4px;
} }
/* 分类选择容器样式 */
.category-select-container {
width: 100%;
}
.category-input-container {
margin-top: 8px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.category-input-container .el-input {
margin-bottom: 8px;
}
/* 响应式调整 */ /* 响应式调整 */
@media (max-width: 768px) { @media (max-width: 768px) {
.image-info-cell { .image-info-cell {
+98 -2
View File
@@ -203,8 +203,28 @@
<el-form-item v-if="serverForm.server_type === 'dockerContainer'" label="Auth-ID"> <el-form-item v-if="serverForm.server_type === 'dockerContainer'" label="Auth-ID">
<el-input v-model="serverForm.auth_id" placeholder="服务器管理id" /> <el-input v-model="serverForm.auth_id" placeholder="服务器管理id" />
</el-form-item> </el-form-item>
<el-form-item v-if="serverForm.server_type === 'hyperV'" label="Guacamole-ID"> <el-form-item v-if="serverForm.server_type === 'hyperV'" label="Guacamole配置">
<el-input v-model="serverForm.guacamole_id" placeholder="guacamole服务id" /> <el-select
v-model="serverForm.guacamole_id"
placeholder="请选择Guacamole配置"
filterable
clearable
:loading="guacamoleLoading"
@change="handleGuacamoleChange"
style="width: 100%"
>
<el-option
v-for="item in guacamoleList"
:key="item.id"
:label="`${item.url} (${item.username})`"
:value="item.id"
>
<div class="guacamole-option">
<div class="option-main">{{ item.url }}</div>
<div class="option-sub">用户: {{ item.username }} | 创建时间: {{ item.created_at }}</div>
</div>
</el-option>
</el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="serverForm.server_type === 'hyperV'" label="登录用户名"> <el-form-item v-if="serverForm.server_type === 'hyperV'" label="登录用户名">
<el-input v-model="serverForm.username" placeholder="服务器登录用户名" /> <el-input v-model="serverForm.username" placeholder="服务器登录用户名" />
@@ -283,6 +303,7 @@ import {
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus' import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import { getServer, addServer, editServer, deleteServer, getServerStatus } from '@/utils/acs/server' import { getServer, addServer, editServer, deleteServer, getServerStatus } from '@/utils/acs/server'
import { copyDomText } from "@/utils/hide" import { copyDomText } from "@/utils/hide"
import {getGuacamoleList} from '@/utils/acs/guacamole'
const router = useRouter() const router = useRouter()
@@ -334,6 +355,10 @@ const handleSizeChange = (val) => {
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogType = ref('add') // 'add', 'edit' const dialogType = ref('add') // 'add', 'edit'
// Guacamole 相关数据
const guacamoleList = ref([])
const guacamoleLoading = ref(false)
// 表单对象和规则 // 表单对象和规则
const serverFormRef = ref(null) const serverFormRef = ref(null)
const serverForm = reactive({ const serverForm = reactive({
@@ -442,6 +467,49 @@ const updateStats = () => {
serverStats.offline = serverData.value.length - serverStats.online serverStats.offline = serverData.value.length - serverStats.online
} }
// 获取 Guacamole 列表
const fetchGuacamoleList = async () => {
if (guacamoleLoading.value) return
guacamoleLoading.value = true
try {
const res = await getGuacamoleList()
console.log("列表数据",res)
if (res && res.data && res.data.code === 200) {
guacamoleList.value = res.data.data || []
} else {
ElMessage.error('获取Guacamole配置列表失败')
guacamoleList.value = []
}
} catch (error) {
console.error('获取Guacamole列表失败:', error)
ElMessage.error('获取Guacamole配置列表失败')
guacamoleList.value = []
} finally {
guacamoleLoading.value = false
}
}
// 处理 Guacamole 选择变化
const handleGuacamoleChange = (selectedId) => {
if (!selectedId) {
// 清空相关字段
serverForm.username = ''
serverForm.password = ''
return
}
// 找到选中的 Guacamole 配置
const selectedGuacamole = guacamoleList.value.find(item => item.id === selectedId)
if (selectedGuacamole) {
// 自动填充相关字段
serverForm.username = selectedGuacamole.username
serverForm.password = selectedGuacamole.password
ElMessage.success(`已自动填充 ${selectedGuacamole.url} 的配置信息`)
}
}
// 添加服务器 // 添加服务器
const handleAdd = () => { const handleAdd = () => {
dialogType.value = 'add' dialogType.value = 'add'
@@ -460,6 +528,11 @@ const handleAdd = () => {
} }
}) })
// 如果是虚拟机类型,获取 Guacamole 列表
if (serverType.value === 'hyperV') {
fetchGuacamoleList()
}
// 确保在下一个 tick 重置表单验证状态 // 确保在下一个 tick 重置表单验证状态
nextTick(() => { nextTick(() => {
if (serverFormRef.value) { if (serverFormRef.value) {
@@ -480,6 +553,11 @@ const handleEdit = (row) => {
} }
}) })
// 如果是虚拟机类型,获取 Guacamole 列表
if (row.server_type === 'hyperV') {
fetchGuacamoleList()
}
nextTick(() => { nextTick(() => {
if (serverFormRef.value) { if (serverFormRef.value) {
serverFormRef.value.clearValidate() serverFormRef.value.clearValidate()
@@ -1027,6 +1105,24 @@ onMounted(async () => {
line-height: 1.2; line-height: 1.2;
} }
/* Guacamole 选项样式 */
.guacamole-option {
padding: 4px 0;
}
.guacamole-option .option-main {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 2px;
}
.guacamole-option .option-sub {
font-size: 12px;
color: #909399;
line-height: 1.2;
}
.form-grid { .form-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);