2026-01-20 09:05:51 +08:00

1250 lines
36 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

<template>
<div class="instance-create-container">
<!-- 面包屑导航 -->
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item>
<a href="/layout/admin/instance">容器实例</a>
</a-breadcrumb-item>
<a-breadcrumb-item>创建实例</a-breadcrumb-item>
</a-breadcrumb>
<!-- 警告提示 -->
<a-alert class="autodl-tip" message="严禁使用WebUI等算法生成违禁图片、严禁挖矿一经发现立即封号" type="warning" show-icon closable />
<!-- 计费方式 -->
<a-card class="card" title="计费方式">
<div class="billing-content">
<a-radio-group v-model:value="billingType" button-style="solid" @change="handleBillingTypeChange">
<a-radio-button value="PayOnTime">按量计费</a-radio-button>
<a-radio-button value="PayOnDay">包日</a-radio-button>
<a-radio-button value="PayOnWeek">包周</a-radio-button>
<a-radio-button value="PayOnMonth">包月</a-radio-button>
<a-radio-button value="PayOnYear">包年</a-radio-button>
</a-radio-group>
<!-- <a href="/docs/price/" target="_blank" class="billing-link">计费规则</a> -->
</div>
<div class="note">
创建完主机后仍然可以转换计费方式如选择按量计费价格发生变动以实例开机时的价格为准
</div>
</a-card>
<!-- 服务器选择 -->
<a-card class="card select-server" title="服务器选择">
<div class="list-filter">
<!-- 选择地区 -->
<div class="filter-item">
<span class="filter-label">选择地区:</span>
<div class="filter-content">
<a-radio-group v-model:value="selectedRegion" button-style="solid" @change="handleRegionChange">
<a-radio-button v-for="region in regions" :key="region.value" :value="region.value">
{{ region.label }}
<a-tag v-if="region.tag" :color="region.tag.color" class="region-tag">{{ region.tag.text
}}</a-tag>
</a-radio-button>
</a-radio-group>
</div>
</div>
<!-- 二级专区 -->
<div class="filter-item" v-if="showZones">
<span class="filter-label">二级专区:</span>
<div class="filter-content">
<a-radio-group v-model:value="selectedZone" button-style="solid" @change="handleZoneChange">
<a-radio-button v-for="zone in availableZones" :key="zone.value" :value="zone.value">
{{ zone.label }}
</a-radio-button>
</a-radio-group>
</div>
</div>
<!-- GPU型号 -->
<div class="filter-item">
<span class="filter-label">GPU型号:</span>
<div class="filter-content">
<a-checkbox-group v-model:value="selectedGpuModels" @change="handleGpuModelChange">
<a-checkbox value="all"
:disabled="selectedGpuModels.length > 0 && selectedGpuModels[0] !== 'all'">全部</a-checkbox>
<a-checkbox v-for="model in availableGpuModels" :key="model.value" :value="model.value"
:disabled="model.available === 0">
{{ model.label }}
<span class="note"> ({{ model.available }}/{{ model.total }})</span>
</a-checkbox>
</a-checkbox-group>
</div>
</div>
<!-- GPU数量 -->
<div class="filter-item">
<span class="filter-label">GPU数量:</span>
<div class="filter-content">
<a-radio-group v-model:value="gpuCount" button-style="solid" @change="handleGpuCountChange">
<a-radio-button v-for="count in gpuCountOptions" :key="count" :value="count"
:disabled="isGpuCountDisabled(count)">
{{ count }}
</a-radio-button>
</a-radio-group>
</div>
</div>
</div>
<!-- 主机表格 -->
<div class="machine-table">
<div class="table-header">
<span class="label">选择主机:</span>
<span class="total-count">共 {{ filteredMachineList.length }} 台可用主机</span>
</div>
<a-table :columns="columns" :data-source="filteredMachineList" :pagination="false"
:row-selection="{ type: 'radio', selectedRowKeys: [selectedMachineId] }" @row-click="handleRowClick"
:row-class-name="getRowClassName" :loading="tableLoading" size="middle">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-radio :checked="selectedMachineId === record.id"
@change="() => handleSelectMachine(record.id)" />
</template>
<template v-else-if="column.key === 'gpuModel'">
<div class="gpu-info">
<div class="gpu-name">{{ record.gpuModel }}</div>
<div class="gpu-memory">{{ record.gpuMemory }}</div>
</div>
</template>
<template v-else-if="column.key === 'gpuInfo'">
<div class="gpu-availability">
<span class="free-count">{{ record.freeGpu }}</span>
<span class="separator">/</span>
<span class="total-count">{{ record.totalGpu }}</span>
</div>
</template>
<template v-else-if="column.key === 'cpuInfo'">
<div class="cpu-memory-info">
<div>CPU{{ record.cpuCores }}核</div>
<div>内存:{{ record.memory }}GB</div>
</div>
</template>
<template v-else-if="column.key === 'disk'">
<div class="disk-info">
<div>数据盘:{{ record.dataDisk }}GB</div>
<div>可扩容:{{ record.expandableDisk }}GB</div>
</div>
</template>
<template v-else-if="column.key === 'driver'">
<div class="driver-info">
<div>驱动:{{ record.driver }}</div>
<div>CUDA{{ record.cuda }}</div>
</div>
</template>
<template v-else-if="column.key === 'price'">
<div class="price-info">
<div class="current-price">
¥{{ getPrice(record) }}/{{ billingUnit }}
</div>
<div v-if="record.originalPrice" class="original-price">
¥{{ getOriginalPrice(record) }}/{{ billingUnit }}
</div>
</div>
</template>
</template>
</a-table>
<!-- 空状态 -->
<div v-if="filteredMachineList.length === 0" class="empty-state">
<a-empty description="暂无可用主机,请调整筛选条件" />
</div>
</div>
</a-card>
<!-- 数据盘 -->
<a-card class="card data-disk" title="数据盘配置">
<div class="data-disk-content">
<div class="disk-base">
<span class="free-disk">免费50GB SSD</span>
<a-checkbox v-model:checked="needExpand">需要扩容</a-checkbox>
</div>
<div v-if="needExpand" class="disk-expand">
<span class="expand-label">扩容大小:</span>
<a-input-number v-model:value="expandSize" :min="1" :max="maxExpandSize" addon-after="GB"
placeholder="请输入扩容大小" />
<span class="expand-note">最大可扩容 {{ maxExpandSize }}GB</span>
</div>
</div>
</a-card>
<!-- 实例规格描述 -->
<a-card class="card instance-spec" title="实例规格">
<div class="spec-content">
<div class="spec-item">
<span class="spec-label">GPU型号</span>
<span class="spec-value">{{ selectedGpuModelLabel }} * {{ gpuCount }}</span>
</div>
<div class="spec-item">
<span class="spec-label">CPU</span>
<span class="spec-value">{{ cpuCores }}核心</span>
</div>
<div class="spec-item">
<span class="spec-label">内存</span>
<span class="spec-value">{{ memory }}GB</span>
</div>
<div class="spec-item">
<span class="spec-label">系统盘</span>
<span class="spec-value">30GB</span>
</div>
<div class="spec-item">
<span class="spec-label">数据盘</span>
<span class="spec-value">{{ totalDiskSize }}GB SSD</span>
</div>
</div>
</a-card>
<!-- 镜像选择 -->
<a-card class="card image-selection" title="镜像选择">
<a-form layout="vertical">
<a-form-item class="image-type">
<a-radio-group v-model:value="imageType" button-style="solid">
<a-radio-button value="platformImage">基础镜像</a-radio-button>
<a-radio-button value="CodeWithGPU">
社区镜像
<a-tag color="red" class="image-tag">hot</a-tag>
</a-radio-button>
<a-radio-button value="customImage">我的镜像</a-radio-button>
</a-radio-group>
<a href="/docs/base_config/" target="_blank" class="config-link">没有我要的环境</a>
</a-form-item>
<a-form-item v-if="imageType === 'platformImage'">
<div class="basic-image">
<div class="image-tip">
基础镜像包含常用基本软件深度学习框架Miniconda等如需其他软件可创建后安装
</div>
<a-cascader v-model:value="selectedImage" :options="imageOptions"
placeholder="请选择框架名称/框架版本/Python版本/CUDA版本" style="width: 400px" :show-search="{ filter }" />
</div>
<div class="note">创建完成后仍然可以更换其他镜像</div>
</a-form-item>
</a-form>
</a-card>
<!-- 优惠券 -->
<a-card class="card coupon-selection" title="优惠券">
<div class="coupon-content">
<a-select v-model:value="selectedCoupon" placeholder="请选择优惠券" style="width: 200px" allow-clear>
<a-select-option value="">不使用优惠券</a-select-option>
<a-select-option v-for="coupon in availableCoupons" :key="coupon.id" :value="coupon.id">
{{ coupon.name }} - {{ coupon.amount }}
</a-select-option>
</a-select>
<div class="coupon-balance">
可用优惠券{{ availableCoupons.length }}
</div>
</div>
</a-card>
<!-- 底部操作栏 -->
<div class="footer-actions">
<div class="price-summary">
<div class="price-main">
<span class="total-label">总计</span>
<div class="price-display">
<span class="total-price">{{ totalPrice }}</span>
<span class="price-unit">/{{ billingUnit }}</span>
</div>
<div v-if="originalTotalPrice" class="original-price-container">
<span class="original-total-price">{{ originalTotalPrice }}/{{ billingUnit }}</span>
</div>
</div>
</div>
<div class="action-buttons">
<a-button size="large" @click="handleCancel" class="cancel-btn">取消</a-button>
<a-button type="primary" size="large" @click="handleCreate" :loading="creating" :disabled="!canCreate"
class="create-btn">
<template #icon>
<PlusOutlined />
</template>
立即创建
</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, reactive, watch } from 'vue'
import { message } from 'ant-design-vue'
import type { TableColumnsType } from 'ant-design-vue'
// 类型定义
interface Machine {
id: string
machineName: string
gpuModel: string
gpuMemory: string
freeGpu: number
totalGpu: number
cpuCores: number
memory: number
cpuModel: string
dataDisk: number
expandableDisk: number
driver: string
cuda: string
basePrice: number
originalPrice?: number
region: string
zone: string
availableGpuCount: number
}
interface GpuModelOption {
value: string
label: string
available: number
total: number
regions: string[]
}
interface RegionOption {
value: string
label: string
tag?: { color: string; text: string }
}
interface ZoneOption {
value: string
label: string
regions: string[]
}
interface Coupon {
id: string
name: string
amount: number
type: string
}
// 响应式数据
const billingType = ref('PayOnTime')
const selectedRegion = ref('beijingDC2')
const selectedZone = ref('')
const selectedGpuModels = ref<string[]>(['RTX 5090'])
const gpuCount = ref(5)
const selectedMachineId = ref('')
const needExpand = ref(false)
const expandSize = ref(100)
const imageType = ref('platformImage')
const selectedImage = ref<string[]>([])
const selectedCoupon = ref('')
const tableLoading = ref(false)
const creating = ref(false)
// 所有机器数据
const allMachineList = ref<Machine[]>([
{
id: '1b0d49b68a',
machineName: '494机',
gpuModel: 'RTX 5090',
gpuMemory: '32GB',
freeGpu: 6,
totalGpu: 8,
cpuCores: 25,
memory: 90,
cpuModel: 'Xeon(R) Platinum 8470Q',
dataDisk: 50,
expandableDisk: 7500,
driver: '580.76.05',
cuda: '13.0',
basePrice: 437.00,
originalPrice: 460.00,
region: 'beijingDC2',
zone: 'beijingDC3',
availableGpuCount: 8
},
{
id: '63af4ab7db',
machineName: '518机',
gpuModel: 'RTX 5090',
gpuMemory: '32GB',
freeGpu: 6,
totalGpu: 8,
cpuCores: 25,
memory: 90,
cpuModel: 'Xeon(R) Platinum 8470Q',
dataDisk: 50,
expandableDisk: 7105,
driver: '580.76.05',
cuda: '13.0',
basePrice: 437.00,
originalPrice: 460.00,
region: 'beijingDC2',
zone: 'beijingDC4',
availableGpuCount: 8
},
{
id: 'ff5d489b4f',
machineName: '495机',
gpuModel: 'RTX 5090',
gpuMemory: '32GB',
freeGpu: 6,
totalGpu: 8,
cpuCores: 25,
memory: 90,
cpuModel: 'Xeon(R) Platinum 8470Q',
dataDisk: 50,
expandableDisk: 6700,
driver: '580.76.05',
cuda: '13.0',
basePrice: 437.00,
originalPrice: 460.00,
region: 'beijingDC2',
zone: 'beijingDC3',
availableGpuCount: 8
},
{
id: 'a1b2c3d4e5',
machineName: '600机',
gpuModel: 'RTX PRO 6000',
gpuMemory: '48GB',
freeGpu: 4,
totalGpu: 8,
cpuCores: 32,
memory: 128,
cpuModel: 'Xeon(R) Platinum 8470Q',
dataDisk: 100,
expandableDisk: 8000,
driver: '580.76.05',
cuda: '13.0',
basePrice: 589.00,
region: 'westDC3',
zone: 'beijingDC4',
availableGpuCount: 8
},
{
id: 'f6g7h8i9j0',
machineName: '601机',
gpuModel: 'vGPU-48GB',
gpuMemory: '48GB',
freeGpu: 8,
totalGpu: 12,
cpuCores: 48,
memory: 192,
cpuModel: 'Xeon(R) Platinum 8470Q',
dataDisk: 150,
expandableDisk: 10000,
driver: '580.76.05',
cuda: '13.0',
basePrice: 689.00,
region: 'chongqingDC1',
zone: 'neimengDC2',
availableGpuCount: 12
},
{
id: 'k1l2m3n4o5',
machineName: '602机',
gpuModel: 'RTX 4090',
gpuMemory: '24GB',
freeGpu: 3,
totalGpu: 8,
cpuCores: 20,
memory: 80,
cpuModel: 'Xeon(R) Platinum 8470Q',
dataDisk: 50,
expandableDisk: 5000,
driver: '580.76.05',
cuda: '13.0',
basePrice: 389.00,
originalPrice: 410.00,
region: 'beijingDC1',
zone: 'shanghaiDC1',
availableGpuCount: 8
}
])
// GPU型号配置
const gpuModelConfig = ref<GpuModelOption[]>([
{ value: 'RTX 5090', label: 'RTX 5090', available: 685, total: 3280, regions: ['beijingDC2', 'beijingDC1', 'chongqingDC1'] },
{ value: 'RTX PRO 6000', label: 'RTX PRO 6000', available: 0, total: 8, regions: ['westDC3'] },
{ value: 'vGPU-48GB', label: 'vGPU-48GB', available: 31, total: 250, regions: ['chongqingDC1', 'neimengDC3'] },
{ value: 'vGPU-48GB-425W', label: 'vGPU-48GB-425W', available: 58, total: 160, regions: ['neimengDC3'] },
{ value: 'RTX 5090 D', label: 'RTX 5090 D', available: 1, total: 11, regions: ['beijingDC2'] },
{ value: 'RTX 4090', label: 'RTX 4090', available: 3, total: 1000, regions: ['beijingDC1', 'foshanDC1'] },
{ value: 'CPU', label: 'CPU', available: 0, total: 44, regions: ['beijingDC2', 'beijingDC1'] }
])
// 配置数据
const regions = ref<RegionOption[]>([
{ value: 'beijingDC2', label: '北京B区' },
{ value: 'westDC3', label: '西北B区', tag: { color: 'red', text: 'PRO6000' } },
{ value: 'chongqingDC1', label: '重庆A区' },
{ value: 'neimengDC3', label: '内蒙B区' },
{ value: 'beijingDC1', label: '北京A区' },
{ value: 'foshanDC1', label: '佛山区' }
])
const zones = ref<ZoneOption[]>([
{ value: 'beijingDC4', label: 'L20专区', regions: ['beijingDC2'] },
{ value: 'beijingDC3', label: 'V100专区', regions: ['beijingDC2'] },
{ value: 'neimengDC2', label: 'A800专区', regions: ['neimengDC3'] },
{ value: 'shanghaiDC1', label: '摩尔线程专区', regions: ['beijingDC1'] },
{ value: 'guangdongDC1', label: '华为昇腾专区', regions: ['foshanDC1'] }
])
const gpuCountOptions = [1, 2, 3, 4, 5, 6, 7, 8, 10, 12]
// 优惠券数据
const availableCoupons = ref<Coupon[]>([
{ id: 'coupon1', name: '新用户优惠券', amount: 50, type: 'discount' },
{ id: 'coupon2', name: '周年庆优惠', amount: 100, type: 'discount' }
])
// 镜像选项
const imageOptions = ref([
{
value: 'pytorch',
label: 'PyTorch',
children: [
{
value: '1.12.0',
label: '1.12.0',
children: [
{
value: 'python3.8',
label: 'Python 3.8',
children: [
{ value: 'cuda11.6', label: 'CUDA 11.6' },
{ value: 'cuda11.7', label: 'CUDA 11.7' }
]
}
]
}
]
}
])
// 表格列定义
const columns: TableColumnsType = [
{
title: '',
key: 'action',
width: 50
},
{
title: '主机ID',
dataIndex: 'id',
key: 'id',
width: 100
},
{
title: '算力型号/显存',
key: 'gpuModel',
width: 150
},
{
title: '空闲GPU',
key: 'gpuInfo',
width: 100,
sorter: (a: Machine, b: Machine) => a.freeGpu - b.freeGpu
},
{
title: '每GPU分配',
key: 'cpuInfo',
width: 120
},
{
title: 'CPU型号',
dataIndex: 'cpuModel',
key: 'cpuModel',
width: 150
},
{
title: '硬盘',
key: 'disk',
width: 140,
sorter: (a: Machine, b: Machine) => a.dataDisk - b.dataDisk
},
{
title: '驱动/CUDA',
key: 'driver',
width: 140
},
{
title: '价格(单卡)',
key: 'price',
width: 140
}
]
// 计算属性
const cpuCores = computed(() => 25 * gpuCount.value)
const memory = computed(() => 90 * gpuCount.value)
// 计费单位
const billingUnit = computed(() => {
switch (billingType.value) {
case 'payg': return '小时'
case 'daily': return '天'
case 'weekly': return '周'
case 'monthly': return '月'
default: return '周'
}
})
// 可用的GPU型号根据地区筛选
const availableGpuModels = computed(() => {
return gpuModelConfig.value.filter(model =>
model.regions.includes(selectedRegion.value)
)
})
// 选中的GPU型号标签
const selectedGpuModelLabel = computed(() => {
if (selectedGpuModels.value.length === 0) return '未选择'
const model = gpuModelConfig.value.find(m => m.value === selectedGpuModels.value[0])
return model ? model.label : selectedGpuModels.value[0]
})
// 过滤后的机器列表
const filteredMachineList = computed(() => {
let filtered = allMachineList.value.filter(machine => {
// 按地区筛选
if (machine.region !== selectedRegion.value) return false
// 按专区筛选(如果有选择专区)
if (selectedZone.value && machine.zone !== selectedZone.value) return false
// 按GPU型号筛选
if (selectedGpuModels.value.length > 0 && !selectedGpuModels.value.includes('all')) {
if (!selectedGpuModels.value.includes(machine.gpuModel)) return false
}
// 按GPU数量筛选机器的可用GPU数量要大于等于选择的GPU数量
if (machine.availableGpuCount < gpuCount.value) return false
// 确保有足够的空闲GPU
if (machine.freeGpu < gpuCount.value) return false
return true
})
return filtered
})
const showZones = computed(() => {
return availableZones.value.length > 0
})
const availableZones = computed(() => {
return zones.value.filter(zone => zone.regions.includes(selectedRegion.value))
})
const totalDiskSize = computed(() => {
return needExpand.value ? 50 + expandSize.value : 50
})
const maxExpandSize = computed(() => {
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
return machine ? machine.expandableDisk : 0
})
const totalPrice = computed(() => {
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
if (!machine) return '0.00'
let price = machine.basePrice * gpuCount.value
if (billingType.value === 'payg') price = price / (7 * 24)
else if (billingType.value === 'daily') price = price / 7
else if (billingType.value === 'monthly') price = price * 4
return price.toFixed(2)
})
const originalTotalPrice = computed(() => {
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
if (!machine || !machine.originalPrice) return ''
let price = machine.originalPrice * gpuCount.value
if (billingType.value === 'payg') price = price / (7 * 24)
else if (billingType.value === 'daily') price = price / 7
else if (billingType.value === 'monthly') price = price * 4
return price.toFixed(2)
})
const canCreate = computed(() => {
return selectedMachineId.value && selectedImage.value.length > 0
})
// 价格计算方法
const getPrice = (record: Machine) => {
const basePrice = record.basePrice * gpuCount.value
switch (billingType.value) {
case 'payg': return (basePrice / (7 * 24)).toFixed(2)
case 'daily': return (basePrice / 7).toFixed(2)
case 'weekly': return basePrice.toFixed(2)
case 'monthly': return (basePrice * 4).toFixed(2)
default: return basePrice.toFixed(2)
}
}
const getOriginalPrice = (record: Machine) => {
if (!record.originalPrice) return ''
const originalPrice = record.originalPrice * gpuCount.value
switch (billingType.value) {
case 'payg': return (originalPrice / (7 * 24)).toFixed(2)
case 'daily': return (originalPrice / 7).toFixed(2)
case 'weekly': return originalPrice.toFixed(2)
case 'monthly': return (originalPrice * 4).toFixed(2)
default: return originalPrice.toFixed(2)
}
}
// 方法
const handleRowClick = (record: Machine) => {
selectedMachineId.value = record.id
}
const handleSelectMachine = (id: string) => {
selectedMachineId.value = id
}
const getRowClassName = (record: Machine) => {
return selectedMachineId.value === record.id ? 'selected-row' : ''
}
const handleBillingTypeChange = () => {
console.log('计费方式改变:', billingType.value)
}
const handleRegionChange = () => {
console.log('地区改变:', selectedRegion.value)
if (availableGpuModels.value.length > 0) {
selectedGpuModels.value = [availableGpuModels.value[0].value]
} else {
selectedGpuModels.value = []
}
selectedZone.value = ''
selectedMachineId.value = ''
}
const handleZoneChange = () => {
console.log('专区改变:', selectedZone.value)
selectedMachineId.value = ''
}
const handleGpuModelChange = () => {
console.log('GPU型号改变:', selectedGpuModels.value)
selectedMachineId.value = ''
}
const handleGpuCountChange = () => {
console.log('GPU数量改变:', gpuCount.value)
selectedMachineId.value = ''
}
const isGpuCountDisabled = (count: number) => {
const machine = filteredMachineList.value.find(m => m.id === selectedMachineId.value)
return machine ? count > machine.availableGpuCount : false
}
const filter = (inputValue: string, path: any[]) => {
return path.some(option =>
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
)
}
const handleCreate = async () => {
creating.value = true
try {
await new Promise(resolve => setTimeout(resolve, 2000))
message.success('实例创建成功!')
} catch (error) {
message.error('创建失败,请重试')
} finally {
creating.value = false
}
}
const handleCancel = () => {
window.history.back()
}
// 监听选中的机器列表,自动选择第一个可用的机器
watch(filteredMachineList, (newList) => {
if (newList.length > 0 && !selectedMachineId.value) {
selectedMachineId.value = newList[0].id
} else if (newList.length === 0) {
selectedMachineId.value = ''
}
}, { immediate: true })
// 监听器
watch(needExpand, (newVal) => {
if (!newVal) {
expandSize.value = 100
}
})
watch(selectedMachineId, (newId) => {
if (newId && needExpand.value) {
const machine = allMachineList.value.find(m => m.id === newId)
if (machine && expandSize.value > machine.expandableDisk) {
expandSize.value = machine.expandableDisk
}
}
})
// 初始化时设置默认的GPU型号
if (availableGpuModels.value.length > 0) {
selectedGpuModels.value = [availableGpuModels.value[0].value]
}
// 价格明细计算
const gpuPrice = computed(() => {
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
if (!machine) return '0.00'
let price = machine.basePrice * gpuCount.value
if (billingType.value === 'payg') price = price / (7 * 24)
else if (billingType.value === 'daily') price = price / 7
else if (billingType.value === 'monthly') price = price * 4
return price.toFixed(2)
})
const diskPrice = computed(() => {
if (!needExpand.value || expandSize.value <= 0) return '0.00'
// 假设每GB硬盘费用为0.1元/周
const pricePerGB = 0.1
let price = pricePerGB * expandSize.value
if (billingType.value === 'payg') price = price / (7 * 24)
else if (billingType.value === 'daily') price = price / 7
else if (billingType.value === 'monthly') price = price * 4
return price.toFixed(2)
})
const couponDiscount = computed(() => {
if (!selectedCoupon.value) return '0.00'
const coupon = availableCoupons.value.find(c => c.id === selectedCoupon.value)
return coupon ? coupon.amount.toFixed(2) : '0.00'
})
const showPriceBreakdown = computed(() => {
return diskPrice !== '0.00' || couponDiscount !== '0.00'
})
</script>
<style scoped>
.instance-create-container {
padding: 24px;
/* max-width: 1400px;
margin: 0 auto; */
background-color: #fff;
}
.breadcrumb {
margin-bottom: 16px;
}
.autodl-tip {
margin-bottom: 24px;
}
.card {
margin-bottom: 24px;
}
.billing-content {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 8px;
}
.billing-link {
font-size: 14px;
color: #1890ff;
text-decoration: none;
}
.note {
font-size: 12px;
color: #666;
margin-top: 8px;
}
.filter-item {
display: flex;
margin-bottom: 24px;
align-items: flex-start;
}
.filter-label {
width: 80px;
margin-right: 16px;
font-weight: 500;
flex-shrink: 0;
line-height: 32px;
}
.filter-content {
flex: 1;
}
.region-tag {
margin-left: 8px;
}
.machine-table {
margin-top: 24px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.table-header .label {
font-weight: 500;
}
.table-header .total-count {
color: #666;
font-size: 14px;
}
.gpu-info .gpu-name {
font-weight: 500;
}
.gpu-info .gpu-memory {
color: #666;
font-size: 12px;
}
.gpu-availability .free-count {
font-weight: bold;
color: #52c41a;
}
.gpu-availability .separator {
margin: 0 4px;
color: #666;
}
.gpu-availability .total-count {
color: #666;
}
.cpu-memory-info,
.disk-info,
.driver-info {
font-size: 12px;
line-height: 1.4;
}
.price-info .current-price {
color: #f7412d;
font-weight: 500;
}
.price-info .original-price {
text-decoration: line-through;
color: #999;
font-size: 12px;
}
.empty-state {
padding: 40px 0;
}
.data-disk-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.disk-base {
display: flex;
align-items: center;
gap: 16px;
}
.free-disk {
color: #52c41a;
font-weight: 500;
}
.disk-expand {
display: flex;
align-items: center;
gap: 12px;
}
.expand-label {
font-weight: 500;
}
.expand-note {
color: #666;
font-size: 12px;
}
.instance-spec .spec-content {
display: flex;
flex-wrap: wrap;
gap: 24px;
}
.spec-item {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 120px;
}
.spec-label {
color: #666;
font-size: 12px;
}
.spec-value {
font-weight: 500;
}
.image-type {
margin-bottom: 0;
}
.config-link {
margin-left: 16px;
font-size: 14px;
}
.image-tip {
color: #666;
font-size: 12px;
margin-bottom: 12px;
}
.image-tag {
margin-left: 8px;
}
.coupon-content {
display: flex;
align-items: center;
gap: 16px;
}
.coupon-balance {
color: #666;
font-size: 12px;
}
.footer-actions {
position: sticky;
bottom: 0;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
padding: 20px 32px;
border-top: 1px solid #e8e8e8;
box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.08);
display: flex;
justify-content: space-between;
align-items: flex-start;
margin: 0 -24px -24px;
backdrop-filter: blur(8px);
z-index: 100;
}
.price-summary {
display: flex;
align-items: flex-start;
gap: 32px;
}
.price-main {
display: flex;
flex-direction: column;
gap: 8px;
}
.total-label {
font-size: 14px;
color: #666;
font-weight: 500;
}
.price-display {
display: flex;
align-items: baseline;
gap: 4px;
}
.total-price {
font-size: 32px;
font-weight: 700;
color: #f7412d;
line-height: 1;
}
.price-unit {
font-size: 14px;
color: #999;
font-weight: 500;
}
.original-price-container {
margin-top: 2px;
}
.original-total-price {
text-decoration: line-through;
color: #999;
font-size: 14px;
font-weight: 500;
}
.price-breakdown {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.breakdown-item {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
font-size: 12px;
}
.breakdown-label {
color: #666;
white-space: nowrap;
}
.breakdown-value {
color: #333;
font-weight: 500;
white-space: nowrap;
}
.breakdown-value.discount {
color: #52c41a;
}
.action-buttons {
display: flex;
gap: 16px;
align-items: center;
}
.cancel-btn {
padding: 0 24px;
height: 48px;
font-weight: 500;
border: 1px solid #d9d9d9;
border-radius: 8px;
}
.cancel-btn:hover {
border-color: #1890ff;
color: #1890ff;
}
.create-btn {
padding: 0 32px;
height: 48px;
font-weight: 600;
border-radius: 8px;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border: none;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
transition: all 0.3s ease;
}
.create-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
}
.create-btn:disabled {
background: #f5f5f5;
color: #d9d9d9;
transform: none;
box-shadow: none;
cursor: not-allowed;
}
/* 响应式设计 */
@media (max-width: 768px) {
.footer-actions {
flex-direction: column;
gap: 20px;
padding: 16px 20px;
}
.price-summary {
width: 100%;
justify-content: space-between;
}
.price-breakdown {
display: none;
/* 移动端隐藏价格明细 */
}
.action-buttons {
width: 100%;
}
.cancel-btn,
.create-btn {
flex: 1;
}
.total-price {
font-size: 28px;
}
}
.price-unit {
color: #666;
}
.original-total-price {
text-decoration: line-through;
color: #999;
font-size: 14px;
}
.action-buttons {
display: flex;
gap: 12px;
}
.selected-row {
background-color: #f0f7ff !important;
}
:deep(.ant-radio-button-wrapper) {
margin-right: 8px;
}
:deep(.ant-table-thead > tr > th) {
background-color: #fafafa;
font-weight: 500;
}
:deep(.ant-table-row) {
cursor: pointer;
}
:deep(.ant-table-row:hover) {
background-color: #f5f5f5;
}
</style>