1250 lines
36 KiB
Vue
1250 lines
36 KiB
Vue
<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> |