GPU_Web/src/views/market/index.vue
2025-12-02 14:10:33 +08:00

1555 lines
35 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-alert class="autodl-tip" message="严禁使用WebUI等算法生成违禁图片、严禁挖矿一经发现立即封号" type="warning" show-icon closable />
<!-- 服务器选择 -->
<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="billingType" button-style="solid" @change="handleBillingTypeChange">
<a-radio-button v-for="type in billingTypeOptions" :key="type.value" :value="type.value">
{{ type.label }}
</a-radio-button>
</a-radio-group>
</div>
</div>
<!-- 选择地区 -->
<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>
</a-card>
<!-- 机器列表区域 -->
<a-card class="card machine-list" title="机器列表">
<!-- 新的卡片式布局 -->
<div class="machine-card-container">
<div
v-for="record in paginatedMachineList"
:key="record.id"
class="machine-card"
:class="{ 'selected-card': selectedMachineId === record.id }"
@click="handleSelectMachine(record.id)"
>
<!-- 卡片头部信息 -->
<div class="card-header">
<div class="location-info">
<span>{{ record.region === 'beijingDC2' ? '北京B区' : record.region }}</span>
<span class="separator">/</span>
<span>{{ record.machineName }}</span>
<span class="id-code">
{{ record.id }}
<span style="margin-left: 20px;">可租用至2027-01-01</span>
</span>
</div>
<div class="gpu-spec">
<div class="gpu-model">{{ record.gpuModel }}</div>
<div class="gpu-memory">/ {{ record.gpuMemory }}</div>
<div class="availability">
空闲/总量 <span style="font-size: 18px;color: #333;">{{ record.freeGpu }}</span> / {{ record.totalGpu }}
</div>
</div>
</div>
<!-- 主要规格信息 -->
<div class="card-body">
<div class="specs-grid">
<!-- 每GPU分配 -->
<div class="spec-column">
<div class="spec-title">每GPU分配</div>
<div class="spec-item">
<div class="spec-label">CPU:</div>
<div class="spec-value">{{ record.cpuCores }} , {{ record.cpuModel }}</div>
</div>
<div class="spec-item">
<div class="spec-label">内存:</div>
<div class="spec-value">{{ record.memory }} GB</div>
</div>
</div>
<!-- 硬盘 -->
<div class="spec-column">
<div class="spec-title">硬盘</div>
<div class="spec-item">
<div class="spec-label">系统盘:</div>
<div class="spec-value">30 GB</div>
</div>
<div class="spec-item">
<div class="spec-label">数据盘:</div>
<div class="spec-value">{{ record.dataDisk }} GB可扩容 {{ record.expandableDisk }} GB</div>
</div>
</div>
<!-- 其它信息 -->
<div class="spec-column">
<div class="spec-title">其它</div>
<div class="spec-item">
<div class="spec-label">GPU驱动:</div>
<div class="spec-value">{{ record.driver }}</div>
</div>
<div class="spec-item">
<div class="spec-label">CUDA版本:</div>
<div class="spec-value">{{ record.cuda }}</div>
</div>
</div>
<!-- 价格信息 -->
<div class="price-column">
<div class="price-info">
<div class="current-price">
¥<strong>{{ getPrice(record) }}</strong>/
</div>
<div class="original-price">
¥{{ getOriginalPrice(record) }}/
</div>
<div class="discount-badge">
<span class="discount-icon">💰</span>
<span class="discount-text">9.5</span>
</div>
<div class="member-note">
会员最低享9.5 ¥2.98/
</div>
<a-button type="primary" class="rent-button" @click="handleRentMachine(record.id)">
{{ record.freeGpu > 0 ? '1卡可租' : '暂不可租' }}
</a-button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-container">
<a-pagination
v-model:current="currentPage"
v-model:pageSize="pageSize"
:total="filteredMachineList.length"
:show-size-changer="true"
:page-size-options="['10', '20', '50', '100']"
show-quick-jumper
show-total
:show-total="total => `${total} 台机器`"
@change="handlePageChange"
@showSizeChange="handlePageSizeChange"
/>
</div>
<!-- 空状态 -->
<div v-if="filteredMachineList.length === 0" class="empty-state">
<a-empty description="暂无可用机器请尝试调整筛选条件" />
</div>
</a-card>
</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'
import { useRouter } from 'vue-router'
const router = useRouter();
// 类型定义
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('weekly')
const selectedRegion = ref('beijingDC2')
const selectedZone = ref('')
const selectedGpuModels = ref<string[]>(['RTX 5090'])
const gpuCount = ref(1)
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 currentPage = ref(1)
const pageSize = ref(10)
// 所有机器数据
const allMachineList = ref<Machine[]>([
{
id: '5fb545beac',
machineName: '336机',
gpuModel: 'vGPU-48GB',
gpuMemory: '48 GB',
freeGpu: 1,
totalGpu: 10,
cpuCores: 20,
memory: 90,
cpuModel: 'Xeon(R) Platinum 8470Q',
dataDisk: 50,
expandableDisk: 110,
driver: '580.76.05',
cuda: '≤ 13.0',
basePrice: 2.98,
originalPrice: 3.14,
region: 'beijingDC2',
zone: 'beijingB',
availableGpuCount: 10
},
{
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 billingTypeOptions = ref([
{ value: 'payg', label: '按量计费' },
{ value: 'daily', label: '包日' },
{ value: 'weekly', label: '包周' },
{ value: 'monthly', label: '包月' }
])
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: 'beijingB', label: '北京B区', regions: ['beijingDC2'] },
{ 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: 120
},
{
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: 180
},
{
title: '硬盘',
key: 'disk',
width: 140,
sorter: (a: Machine, b: Machine) => a.dataDisk - b.dataDisk
},
{
title: '驱动/CUDA',
key: 'driver',
width: 140
},
{
title: '价格(单卡)',
key: 'price',
width: 160
}
]
// 计算属性
const cpuCores = computed(() => {
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
return machine ? machine.cpuCores * gpuCount.value : 20 * gpuCount.value
})
const memory = computed(() => {
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
return machine ? machine.memory * gpuCount.value : 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 paginatedMachineList = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredMachineList.value.slice(start, end)
})
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 (needExpand.value && expandSize.value > 0) {
const diskPricePerGB = 0.1 // 假设每GB 0.1元/周
price += diskPricePerGB * expandSize.value
}
// 优惠券折扣
if (selectedCoupon.value) {
const coupon = availableCoupons.value.find(c => c.id === selectedCoupon.value)
if (coupon) {
price = Math.max(0, price - coupon.amount)
}
}
// 计费方式转换
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
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
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 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)
if (!coupon) return '0.00'
let discount = coupon.amount
if (billingType.value === 'payg') discount = discount / (7 * 24)
else if (billingType.value === 'daily') discount = discount / 7
else if (billingType.value === 'monthly') discount = discount * 4
return discount.toFixed(2)
})
const showPriceBreakdown = computed(() => {
return diskPrice.value !== '0.00' || couponDiscount.value !== '0.00'
})
// 方法
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 handleRentMachine = (id:string) => {
router.push("/layout/create")
}
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 = ''
currentPage.value = 1
}
const handleZoneChange = () => {
console.log('专区改变:', selectedZone.value)
selectedMachineId.value = ''
currentPage.value = 1
}
const handleGpuModelChange = () => {
console.log('GPU型号改变:', selectedGpuModels.value)
selectedMachineId.value = ''
currentPage.value = 1
}
const handleGpuCountChange = () => {
console.log('GPU数量改变:', gpuCount.value)
selectedMachineId.value = ''
currentPage.value = 1
}
const handleImageTypeChange = () => {
console.log('镜像类型改变:', imageType.value)
selectedImage.value = []
}
// 分页处理方法
const handlePageChange = (page: number, pageSize?: number) => {
currentPage.value = page
if (pageSize) {
pageSize.value = pageSize
}
}
const handlePageSizeChange = (current: number, size: number) => {
currentPage.value = 1
pageSize.value = size
}
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 = ''
}
// 重置到第一页
currentPage.value = 1
}, { 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]
}
</script>
<style scoped>
/* 强制让容器参与文档流并允许增长 */
.instance-create-container {
display: block;
width: 90%;
max-width: 1200px;
margin: 0 auto;
padding: 24px;
background: #fff;
box-sizing: border-box;
/* 关键:确保它能自然撑高 */
min-height: fit-content;
height: auto;
}
.breadcrumb {
margin-bottom: 16px;
}
.autodl-tip {
margin-bottom: 24px;
}
.card {
margin-bottom: 24px;
}
.select-server {
margin-bottom: 24px;
}
.machine-list {
margin-bottom: 24px;
}
.list-filter {
margin-bottom: 16px;
}
.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;
font-size: 16px;
}
.table-header .total-count {
color: #666;
font-size: 14px;
}
.machine-id-info .machine-id {
font-weight: 500;
}
.machine-id-info .machine-name {
color: #666;
font-size: 12px;
margin-top: 2px;
}
.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;
}
.disk-info .expandable-note {
color: #1890ff;
font-size: 11px;
margin-top: 2px;
}
.price-info {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.price-info .current-price {
color: #f7412d;
font-weight: 500;
font-size: 14px;
}
.price-info .original-price {
text-decoration: line-through;
color: #999;
font-size: 12px;
}
.price-info .billing-unit {
color: #999;
font-size: 12px;
margin-top: 2px;
}
.price-info .rentable-tag {
background-color: #f6ffed;
border: 1px solid #b7eb8f;
color: #52c41a;
padding: 1px 6px;
border-radius: 4px;
font-size: 11px;
margin-top: 4px;
}
.empty-state {
padding: 40px 0;
text-align: center;
}
.pagination-container {
margin-top: 24px;
display: flex;
justify-content: flex-end;
}
.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;
display: flex;
align-items: center;
gap: 16px;
}
.config-link {
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 {
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
padding: 20px 0;
border-top: 1px solid #e8e8e8;
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-top: 40px;
width: 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;
}
.selected-row {
background-color: #f0f7ff !important;
}
/* 响应式设计 */
@media (max-width: 768px) {
.instance-create-container {
width: 100%;
padding: 16px;
}
.footer-actions {
flex-direction: column;
gap: 20px;
padding: 16px 0;
}
.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;
}
.filter-item {
flex-direction: column;
gap: 12px;
}
.filter-label {
width: 100%;
margin-right: 0;
}
}
: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;
}
:deep(.ant-pagination) {
font-size: 14px;
}
:deep(.ant-pagination-total-text) {
margin-right: 16px;
}
/* 新增的卡片式布局样式 */
.machine-card-container {
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 24px;
}
.machine-card {
border: 1px solid #d9d9d9;
border-radius: 8px;
background: white;
transition: all 0.2s ease;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.machine-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transform: translateY(-1px);
}
.selected-card {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.card-header {
padding: 16px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8e8e8;
/* display: flex;
justify-content: space-between;
align-items: center; */
}
.location-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
}
.location-info .separator {
color: #ccc;
}
.id-code {
font-size: 12px;
color: #999;
}
.available-info {
font-size: 12px;
color: #999;
}
.card-body {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
.gpu-spec {
display: flex;
flex-direction: row;
gap: 8px;
margin-top: 15px;
}
.gpu-model {
font-size: 18px;
font-weight: 500;
color: #333;
}
.gpu-memory {
font-size: 18px;
color: #333;
font-weight: 500;
}
.availability {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
gap: 8px;
padding-left: 20px;
}
.specs-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 24px;
margin-top: 16px;
}
.spec-column {
display: flex;
flex-direction: column;
gap: 12px;
}
.spec-title {
font-size: 12px;
color: #666;
font-weight: 500;
}
.spec-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.spec-label {
font-size: 12px;
color: #999;
}
.spec-value {
font-size: 14px;
color: #333;
word-break: break-all;
}
.price-column {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 12px;
}
.price-info {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
width: 100%;
}
.current-price {
font-size: 18px;
font-weight: 500;
color: #f7412d;
display: flex;
align-items: center;
gap: 4px;
}
.original-price {
font-size: 12px;
color: #999;
text-decoration: line-through;
}
.discount-badge {
display: flex;
align-items: center;
gap: 4px;
background: #fff5e6;
border: 1px solid #ffe4b5;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.discount-icon {
font-size: 12px;
}
.discount-text {
color: #f7412d;
font-weight: 500;
}
.member-note {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.rent-button {
width: 100px;
height: 32px;
font-size: 12px;
border-radius: 4px;
margin-top: 8px;
}
/* 响应式设计 - 小屏幕适配 */
@media (max-width: 768px) {
.specs-grid {
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.price-column {
grid-column: span 2;
}
.card-header {
flex-direction: column;
gap: 8px;
padding: 12px 16px;
}
.location-info {
flex-direction: column;
gap: 4px;
}
.available-info {
font-size: 10px;
}
}
</style>