f7c3be1d30
- Created ImageForm.vue as standalone page for add/edit image functionality - Removed dialog-based image form from VmImages.vue - Implemented tagsViewStore for global tab state management - Added automatic tab closing on form cancel/back - Fixed data persistence issue when switching between image edits - Removed quick actions section from ImageForm - Updated router configuration for new image form route
848 lines
21 KiB
Vue
848 lines
21 KiB
Vue
<template>
|
||
<div class="server-form-container">
|
||
<!-- 顶部导航 -->
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<el-button @click="goBack" class="back-btn" circle>
|
||
<el-icon><ArrowLeft /></el-icon>
|
||
</el-button>
|
||
<div class="header-title-area">
|
||
<h1 class="page-title">{{ isEdit ? '编辑服务器' : '新建服务器' }}</h1>
|
||
<span class="page-subtitle">{{ isEdit ? '修改服务器配置信息' : '配置并部署新的服务器节点' }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button @click="goBack" size="large">取消</el-button>
|
||
<el-button type="primary" @click="submitForm" :loading="submitting" size="large" class="submit-btn">
|
||
{{ isEdit ? '保存修改' : '立即创建' }}
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主表单区域 -->
|
||
<div class="form-wrapper">
|
||
<el-form :model="form" label-position="top" :rules="rules" ref="formRef" class="main-form" size="large">
|
||
|
||
<!-- 左侧:主要配置 -->
|
||
<div class="form-main-col">
|
||
<el-card class="premium-card" shadow="never">
|
||
<div class="section-header">
|
||
<div class="section-icon"><el-icon><Monitor /></el-icon></div>
|
||
<div class="section-info">
|
||
<h3>基础信息</h3>
|
||
<p>配置服务器的基本标识与网络信息</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-grid-2">
|
||
<el-form-item label="服务器名称" prop="name">
|
||
<el-input v-model="form.name" placeholder="例如:生产环境-Web节点-01" />
|
||
</el-form-item>
|
||
<el-form-item label="IP地址" prop="server_ip">
|
||
<el-input v-model="form.server_ip" placeholder="例如:192.168.1.100" />
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<el-form-item label="所在地区" prop="location">
|
||
<el-cascader
|
||
v-model="locationArray"
|
||
:options="regionsBuff"
|
||
:props="optionProps"
|
||
placeholder="选择服务器所在的物理位置"
|
||
style="width: 100%"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-divider />
|
||
|
||
<div class="section-header">
|
||
<div class="section-icon"><el-icon><Cpu /></el-icon></div>
|
||
<div class="section-info">
|
||
<h3>硬件规格</h3>
|
||
<p>定义服务器的计算资源配额</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="resource-cards">
|
||
<div class="resource-item">
|
||
<div class="resource-label">CPU核心</div>
|
||
<el-input v-model="form.cpu" placeholder="0">
|
||
<template #suffix>核</template>
|
||
</el-input>
|
||
</div>
|
||
<div class="resource-item">
|
||
<div class="resource-label">内存容量</div>
|
||
<el-input v-model="form.memory" placeholder="0">
|
||
<template #suffix>MB</template>
|
||
</el-input>
|
||
</div>
|
||
<div class="resource-item">
|
||
<div class="resource-label">硬盘空间</div>
|
||
<el-input v-model="form.disk" placeholder="0">
|
||
<template #suffix>GB</template>
|
||
</el-input>
|
||
</div>
|
||
<div class="resource-item">
|
||
<div class="resource-label">网络带宽</div>
|
||
<el-input v-model="form.bandwidth" placeholder="0">
|
||
<template #suffix>Mbps</template>
|
||
</el-input>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card class="premium-card" shadow="never" style="margin-top: 24px;">
|
||
<div class="section-header">
|
||
<div class="section-icon"><el-icon><Connection /></el-icon></div>
|
||
<div class="section-info">
|
||
<h3>连接与认证</h3>
|
||
<p>配置服务器的访问方式与凭证</p>
|
||
</div>
|
||
</div>
|
||
|
||
<el-form-item label="服务器类型" prop="server_type" style="margin-bottom: 24px;">
|
||
<div class="type-selector">
|
||
<div
|
||
class="type-card"
|
||
:class="{ active: form.server_type === 'dockerContainer' }"
|
||
@click="form.server_type = 'dockerContainer'"
|
||
>
|
||
<div class="type-icon"><el-icon><Box /></el-icon></div>
|
||
<div class="type-info">
|
||
<div class="type-name">容器云服务器</div>
|
||
<div class="type-desc">基于Docker容器技术的轻量级实例</div>
|
||
</div>
|
||
<div class="type-check" v-if="form.server_type === 'dockerContainer'">
|
||
<el-icon><Check /></el-icon>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="type-card"
|
||
:class="{ active: form.server_type === 'hyperV' }"
|
||
@click="form.server_type = 'hyperV'"
|
||
>
|
||
<div class="type-icon"><el-icon><Platform /></el-icon></div>
|
||
<div class="type-info">
|
||
<div class="type-name">虚拟机云服务器</div>
|
||
<div class="type-desc">基于Hyper-V技术的完整虚拟化实例</div>
|
||
</div>
|
||
<div class="type-check" v-if="form.server_type === 'hyperV'">
|
||
<el-icon><Check /></el-icon>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
|
||
<!-- 容器云特有 -->
|
||
<template v-if="form.server_type === 'dockerContainer'">
|
||
<el-form-item label="Auth-ID" prop="auth_id">
|
||
<el-input v-model="form.auth_id" placeholder="输入服务器管理ID" />
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- 虚拟机特有 -->
|
||
<template v-if="form.server_type === 'hyperV'">
|
||
<el-form-item label="Guacamole网关" prop="guacamole_id">
|
||
<el-select
|
||
v-model="form.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"
|
||
:value="item.id"
|
||
>
|
||
<div class="guacamole-option">
|
||
<span class="url">{{ item.url }}</span>
|
||
<span class="user">{{ item.username }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<div class="form-grid-2">
|
||
<el-form-item label="登录用户名" prop="username">
|
||
<el-input v-model="form.username" placeholder="例如:Administrator" />
|
||
</el-form-item>
|
||
<el-form-item label="登录密码" prop="password">
|
||
<el-input
|
||
v-model="form.password"
|
||
placeholder="输入登录密码"
|
||
type="password"
|
||
show-password
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<el-form-item>
|
||
<div class="feature-switch">
|
||
<div class="switch-info">
|
||
<span class="switch-title">端口映射</span>
|
||
<span class="switch-desc">允许外部网络访问该服务器的特定端口</span>
|
||
</div>
|
||
<el-switch
|
||
v-model="form.allow_port_forward"
|
||
:active-value="1"
|
||
:inactive-value="0"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<el-form-item label="管理Token" prop="server_token">
|
||
<el-input
|
||
v-model="form.server_token"
|
||
placeholder="节点服务器管理员Token"
|
||
type="password"
|
||
show-password
|
||
/>
|
||
</el-form-item>
|
||
</el-card>
|
||
</div>
|
||
|
||
<!-- 右侧:高级设置 -->
|
||
<div class="form-side-col">
|
||
<el-card class="premium-card" shadow="never">
|
||
<div class="section-header small">
|
||
<div class="section-info">
|
||
<h3>高级设置</h3>
|
||
</div>
|
||
</div>
|
||
|
||
<el-form-item label="控制台连接">
|
||
<el-input v-model="form.console_url" placeholder="可选,https需反代">
|
||
<template #prefix><el-icon><Link /></el-icon></template>
|
||
</el-input>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="展示卡片HTML">
|
||
<el-input
|
||
v-model="form.html"
|
||
type="textarea"
|
||
:rows="6"
|
||
placeholder="自定义购买页面的展示样式代码"
|
||
resize="none"
|
||
/>
|
||
</el-form-item>
|
||
|
||
<el-divider />
|
||
|
||
<el-form-item>
|
||
<div class="feature-switch">
|
||
<div class="switch-info">
|
||
<span class="switch-title">购物车显示</span>
|
||
<span class="switch-desc">在前端购买页面展示此节点</span>
|
||
</div>
|
||
<el-switch
|
||
v-model="form.hide"
|
||
:active-value="0"
|
||
:inactive-value="1"
|
||
/>
|
||
</div>
|
||
</el-form-item>
|
||
</el-card>
|
||
</div>
|
||
|
||
</el-form>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, ElNotification } from 'element-plus'
|
||
import {
|
||
ArrowLeft, Monitor, Cpu, Link, Connection,
|
||
Box, Platform, Check
|
||
} from '@element-plus/icons-vue'
|
||
import { addServer, editServer, getServer } from '@/utils/acs/server'
|
||
import { getGuacamoleList } from '@/utils/acs/guacamole'
|
||
import regions from '@/utils/regions.json'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
const formRef = ref(null)
|
||
const submitting = ref(false)
|
||
const isEdit = computed(() => !!route.query.id)
|
||
|
||
const form = reactive({
|
||
server_id: '',
|
||
name: '',
|
||
server_ip: '',
|
||
location: '',
|
||
bandwidth: '',
|
||
disk: '',
|
||
memory: '',
|
||
cpu: '',
|
||
state: '',
|
||
auth_id: '',
|
||
server_token: '',
|
||
server_type: 'dockerContainer',
|
||
html: '',
|
||
hide: 0,
|
||
console_url: '',
|
||
guacamole_id: '',
|
||
username: '',
|
||
password: '',
|
||
allow_port_forward: 0
|
||
})
|
||
|
||
const rules = {
|
||
name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
|
||
server_ip: [
|
||
{ required: true, message: '请输入IP地址', trigger: 'blur' },
|
||
{ pattern: /^(\d{1,3}\.){3}\d{1,3}$/, message: '请输入有效的IP地址', trigger: 'blur' }
|
||
],
|
||
guacamole_id: [
|
||
{ required: false, message: '请输入Guacamole服务ID', trigger: 'blur' }
|
||
],
|
||
username: [
|
||
{ required: false, message: '请输入登录用户名', trigger: 'blur' }
|
||
],
|
||
password: [
|
||
{ required: false, message: '请输入登录密码', trigger: 'blur' }
|
||
]
|
||
}
|
||
|
||
// Guacamole 相关
|
||
const guacamoleList = ref([])
|
||
const guacamoleLoading = ref(false)
|
||
|
||
const fetchGuacamoleList = async () => {
|
||
if (guacamoleLoading.value) return
|
||
|
||
guacamoleLoading.value = true
|
||
try {
|
||
const res = await getGuacamoleList()
|
||
if (res && res.data && res.data.code === 200) {
|
||
guacamoleList.value = res.data.data || []
|
||
} else {
|
||
guacamoleList.value = []
|
||
}
|
||
} catch (error) {
|
||
console.error('获取Guacamole列表失败:', error)
|
||
guacamoleList.value = []
|
||
} finally {
|
||
guacamoleLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleGuacamoleChange = (selectedId) => {
|
||
if (!selectedId) {
|
||
form.username = ''
|
||
form.password = ''
|
||
return
|
||
}
|
||
}
|
||
|
||
// 地区数据处理
|
||
const regionsBuff = ref(regions)
|
||
const optionProps = {
|
||
label: 'label',
|
||
value: 'value',
|
||
children: 'children',
|
||
checkStrictly: false,
|
||
emitPath: true
|
||
}
|
||
|
||
const findValueByLabel = (label, options) => {
|
||
for (const option of options) {
|
||
if (option.label === label) return option.value
|
||
if (option.children) {
|
||
const result = findValueByLabel(label, option.children)
|
||
if (result) return result
|
||
}
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
const findLabelByValue = (value, options) => {
|
||
for (const option of options) {
|
||
if (option.value === value) return option.label
|
||
if (option.children) {
|
||
const result = findLabelByValue(value, option.children)
|
||
if (result) return result
|
||
}
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
const locationArray = computed({
|
||
get: () => {
|
||
if (form.location) {
|
||
try {
|
||
const labels = form.location.split(' ')
|
||
const values = labels.map(label => findValueByLabel(label, regionsBuff.value))
|
||
return values.filter(value => value !== undefined)
|
||
} catch (error) {
|
||
return []
|
||
}
|
||
}
|
||
return []
|
||
},
|
||
set: (newArray) => {
|
||
try {
|
||
if (Array.isArray(newArray) && newArray.length > 0) {
|
||
const labels = newArray.map(value => {
|
||
const label = findLabelByValue(value, regionsBuff.value)
|
||
return label || value
|
||
})
|
||
form.location = labels.join(' ')
|
||
} else {
|
||
form.location = ''
|
||
}
|
||
} catch (error) {
|
||
form.location = ''
|
||
}
|
||
}
|
||
})
|
||
|
||
// 初始化数据
|
||
const initData = async () => {
|
||
if (isEdit.value) {
|
||
const id = route.query.id
|
||
if (id) {
|
||
try {
|
||
const stateData = history.state.params
|
||
if (stateData) {
|
||
Object.keys(form).forEach(key => {
|
||
if (key in stateData) {
|
||
form[key] = stateData[key]
|
||
}
|
||
})
|
||
} else {
|
||
const res = await getServer(1, 100, '', route.query.type || 'dockerContainer')
|
||
if (res && res.data && res.data.data) {
|
||
const found = res.data.data.find(item => item.server_id == id)
|
||
if (found) {
|
||
Object.keys(form).forEach(key => {
|
||
if (key in found) {
|
||
form[key] = found[key]
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error(e)
|
||
}
|
||
}
|
||
} else {
|
||
form.server_type = route.query.type || 'dockerContainer'
|
||
}
|
||
|
||
if (form.server_type === 'hyperV') {
|
||
fetchGuacamoleList()
|
||
}
|
||
}
|
||
|
||
const goBack = () => {
|
||
router.back()
|
||
}
|
||
|
||
const submitForm = async () => {
|
||
if (!formRef.value) return
|
||
|
||
await formRef.value.validate(async (valid) => {
|
||
if (valid) {
|
||
submitting.value = true
|
||
try {
|
||
const formData = { ...form }
|
||
|
||
const numericFields = ['bandwidth', 'disk', 'memory', 'cpu', 'hide', 'allow_port_forward']
|
||
numericFields.forEach(field => {
|
||
if (formData[field] !== '' && formData[field] !== null && formData[field] !== undefined) {
|
||
formData[field] = Number(formData[field])
|
||
}
|
||
})
|
||
|
||
let res
|
||
if (!isEdit.value) {
|
||
res = await addServer(formData)
|
||
} else {
|
||
res = await editServer(formData)
|
||
}
|
||
|
||
if (res && res.data && res.data.code === 200) {
|
||
ElNotification({
|
||
title: !isEdit.value ? '添加成功' : '更新成功',
|
||
message: `服务器"${formData.name}"已${!isEdit.value ? '添加' : '更新'}成功`,
|
||
type: 'success',
|
||
duration: 3000
|
||
})
|
||
goBack()
|
||
} else {
|
||
ElMessage.error(res?.data?.msg || '操作失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('提交表单失败:', error)
|
||
ElMessage.error('提交失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
watch(() => form.server_type, (val) => {
|
||
if (val === 'hyperV') {
|
||
fetchGuacamoleList()
|
||
}
|
||
})
|
||
|
||
onMounted(() => {
|
||
initData()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.server-form-container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 24px;
|
||
}
|
||
|
||
/* 顶部导航 */
|
||
.page-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
background: #ffffff;
|
||
padding: 20px 32px;
|
||
border-radius: 12px;
|
||
border: 1px solid #e4e7ed;
|
||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.02);
|
||
}
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.back-btn {
|
||
border: none;
|
||
background: #f2f3f5;
|
||
color: #606266;
|
||
width: 36px;
|
||
height: 36px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.back-btn:hover {
|
||
background-color: #e6e8eb;
|
||
color: #303133;
|
||
}
|
||
|
||
.header-title-area {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
|
||
.page-title {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #1a1a1a;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.page-subtitle {
|
||
font-size: 13px;
|
||
color: #909399;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.submit-btn {
|
||
padding: 10px 24px;
|
||
font-weight: 600;
|
||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
|
||
}
|
||
|
||
/* 表单布局 */
|
||
.form-wrapper {
|
||
display: flex;
|
||
gap: 24px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.main-form {
|
||
display: flex;
|
||
width: 100%;
|
||
gap: 24px;
|
||
}
|
||
|
||
.form-main-col {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.form-side-col {
|
||
width: 320px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 卡片样式 */
|
||
.premium-card {
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 12px;
|
||
background: #ffffff;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.premium-card:hover {
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.premium-card :deep(.el-card__body) {
|
||
padding: 32px;
|
||
}
|
||
|
||
/* 章节标题 */
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 24px;
|
||
padding-bottom: 16px;
|
||
border-bottom: 1px solid #f0f2f5;
|
||
}
|
||
|
||
.section-header.small {
|
||
margin-bottom: 20px;
|
||
padding-bottom: 12px;
|
||
}
|
||
|
||
.section-icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
|
||
color: #409EFF;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.section-info h3 {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #303133;
|
||
}
|
||
|
||
.section-info p {
|
||
margin: 2px 0 0 0;
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
/* 表单网格 */
|
||
.form-grid-2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 24px;
|
||
}
|
||
|
||
/* 资源卡片 */
|
||
.resource-cards {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 16px;
|
||
}
|
||
|
||
.resource-item {
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
border: 1px solid #ebeef5;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.resource-item:hover {
|
||
border-color: #c6e2ff;
|
||
background: #f2f6fc;
|
||
}
|
||
|
||
.resource-label {
|
||
font-size: 12px;
|
||
color: #606266;
|
||
margin-bottom: 8px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
/* 类型选择器 */
|
||
.type-selector {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
.type-card {
|
||
position: relative;
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
transition: all 0.2s;
|
||
background: #fff;
|
||
}
|
||
|
||
.type-card:hover {
|
||
border-color: #409EFF;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.type-card.active {
|
||
border-color: #409EFF;
|
||
background: #ecf5ff;
|
||
}
|
||
|
||
.type-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
background: #f2f6fc;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
color: #606266;
|
||
}
|
||
|
||
.type-card.active .type-icon {
|
||
background: #fff;
|
||
color: #409EFF;
|
||
}
|
||
|
||
.type-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.type-name {
|
||
font-weight: 600;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.type-desc {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
|
||
.type-check {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
color: #409EFF;
|
||
font-size: 16px;
|
||
}
|
||
|
||
/* 开关样式 */
|
||
.feature-switch {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 12px 16px;
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
border: 1px solid #ebeef5;
|
||
}
|
||
|
||
.switch-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.switch-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.switch-desc {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.guacamole-option {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
width: 100%;
|
||
}
|
||
|
||
.guacamole-option .url {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.guacamole-option .user {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* 响应式 */
|
||
@media screen and (max-width: 992px) {
|
||
.main-form {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.form-side-col {
|
||
width: 100%;
|
||
}
|
||
|
||
.resource-cards {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
@media screen and (max-width: 768px) {
|
||
.server-form-container {
|
||
padding: 16px;
|
||
}
|
||
|
||
.page-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 16px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.header-actions {
|
||
width: 100%;
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
.header-actions .el-button {
|
||
flex: 1;
|
||
}
|
||
|
||
.form-grid-2 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.type-selector {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|