Leo_Ding 2d0d6eb59b 1
2026-01-16 18:25:56 +08:00

818 lines
19 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="container">
<div class="instance-create-container">
<!-- 服务器选择 -->
<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-input v-model:value="instanceName"></a-input>
</div>
</div>
<div class="filter-item">
<span class="filter-label">计费方式<span style="color: #ff4d4f;margin: 0 2px">*</span>:</span>
<div class="filter-content">
<a-radio-group v-model:value="billingType" button-style="solid">
<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>
</div>
</div>
<!-- GPU数量 -->
<div class="filter-item">
<span class="filter-label">GPU数量<span style="color: #ff4d4f;margin: 0 2px">*</span><span>:</span></span>
<div class="filter-content">
<a-radio-group v-model:value="gpuCount" button-style="solid">
<a-radio-button v-for="count in gpuCountOptions" :key="count" :value="count">
{{ count }}
</a-radio-button>
</a-radio-group>
</div>
</div>
<div class="filter-item">
<span class="filter-label">镜像<span style="color: #ff4d4f;margin: 0 2px;">*</span>:</span>
<a-form layout="vertical">
<a-form-item class="image-type">
<a-radio-group v-model:value="imageType" button-style="solid"
@change="getServiceImages()">
<a-radio-button value="SYSTEM">基础镜像</a-radio-button>
<a-radio-button value="USER">我的镜像</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item>
<div class="basic-image" style="margin-top: 15px;">
<a-select ref="select" v-model:value="selectedImage" style="width: 300px"
@change="handleChange">
<a-select-option v-for="item in imageOptions" :value="item.id">{{ item.label
}}</a-select-option>
</a-select>
</div>
</a-form-item>
</a-form>
</div>
</div>
<!-- 主机表格 -->
<div class="machine-table">
<a-descriptions layout="vertical" bordered :column="8">
<a-descriptions-item label="主机ID">{{ serviceInfo.id }}</a-descriptions-item>
<a-descriptions-item label="算力型号/显存">{{ serviceInfo.gpuType + '/' + serviceInfo.vram + 'GB'
}}</a-descriptions-item>
<a-descriptions-item label="空闲GPU">{{ serviceInfo.gpuAvailableNum }}</a-descriptions-item>
<a-descriptions-item label="每GPU分配">
<span>{{ 'CPU:' + serviceInfo.cpuNum + '核' }}</span><br>
<span>{{ '内存:' + serviceInfo.memory + "GB" }}</span>
</a-descriptions-item>
<a-descriptions-item label="CPU型号">{{ serviceInfo.cpuType }}</a-descriptions-item>
<a-descriptions-item label="硬盘">
<span>{{ '数据盘:' + serviceInfo.dataDisk + 'GB' }}</span><br>
<span>{{ '系统盘:' + serviceInfo.systemDisk + "GB" }}</span>
</a-descriptions-item>
<a-descriptions-item label="驱动/CUDA">{{ serviceInfo.gpuDriver + '/' + serviceInfo.cudaVersion
}}</a-descriptions-item>
<a-descriptions-item label="消耗算力点(单卡)">{{ serviceInfo.price }}</a-descriptions-item>
</a-descriptions>
</div>
</a-card>
<!-- 优惠券 -->
<a-card class="card coupon-selection">
<a-checkbox v-model:checked="checked">是否使用算力券<span style="color:#f34646;">{{ ' ( 可用点数:'+couponTotal+' 点 )' }}</span></a-checkbox>
</a-card>
<!-- 底部操作栏 -->
</div>
<div class="footer-actions">
<div class="price-summary">
<div>消耗算力点{{totalMoney }}</div>
<!-- 账户余额信息 -->
<div class="account-info" style="display: flex;align-items: center;">
<div class="balance-label" style="margin-right: 10px;">可用算力点{{ userInfo.computingPowerPoint+' 点' }}</div>
<!-- <div v-if="isShow" style="color: #ff4d4f;font-size: 12px;cursor: pointer;"
@click="router.push('/layout/admin/balance')">
算力点不足去充值</div> -->
</div>
</div>
<div class="action-buttons">
<!-- 费用预估提示 -->
<div class="price-tips">
<span class="tip-icon">💡</span>
<span class="tip-text">费用为预估按实际使用结算</span>
</div>
<!-- 操作按钮 -->
<div class="button-group">
<a-button size="large" @click="handleCancel" class="cancel-btn">
取消
</a-button>
<a-button type="primary" size="large" @click="handleCreate" :loading="creating"
:disabled="selectedImage==''" class="create-btn">
<template #icon>
<PlusOutlined />
</template>
立即创建
</a-button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, reactive, watch, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import type { TableColumnsType } from 'ant-design-vue'
import { useRoute,useRouter } from 'vue-router'
import { getImages } from "@/apis/home"
import { getMyCouponTotal, getTotalCost, getHostInfo,createHost } from "@/apis/home"
const router=useRouter()
interface GpuModelOption {
value: string
label: string
available: number
total: number
regions: string[]
}
const instanceName=ref('')
const checked = ref(false)
const totalMoney = ref(0)
// 响应式数据
const billingType = ref('PayOnTime')
const gpuCount = ref(5)
const selectedMachineId = ref('')
const imageType = ref('SYSTEM')
const selectedImage = ref<string>("")
const creating = ref(false)
const serviceInfo = ref<any>({})
const userInfo = ref<any>({})
const HostId=ref(0)
const computingPowerPoint=ref(0)
const couponTotal=ref(0)
onMounted(() => {
const userInfoStr = localStorage.getItem("userInfo")
if (userInfoStr) {
userInfo.value = JSON.parse(userInfoStr)
}
serviceInfo.value = useRoute().query
HostId.value=serviceInfo.value.id
console.log('1111',serviceInfo.value)
gpuCountOptions.value = Array.from({ length: Number(serviceInfo.value.gpuAvailableNum) }, (_, i) => i + 1)
gpuCount.value = gpuCountOptions.value[0]
getServiceImages()
getCoupon()
fetchTotalCost()
})
async function getCoupon() {
const res:any=await getMyCouponTotal()
couponTotal.value = res;
}
// 封装请求
async function fetchTotalCost() {
const res: any = await getTotalCost({
billingMethod: billingType.value,
gpuNum: gpuCount.value,
host_id: HostId.value
})
totalMoney.value = res.amount
computingPowerPoint.value=res.computingPowerPoint
}
// 监听依赖项变化
watch([billingType, gpuCount], () => {
fetchTotalCost()
getSingleHost()
}) // 组件加载时也执行一次
const getSingleHost = async () => {
const res: any = await getHostInfo({
billingMethod: billingType.value,
gpuNum: gpuCount.value,
host_id: HostId.value
})
serviceInfo.value = res
}
const getServiceImages = async () => {
try {
selectedImage.value = ''
const res = await getImages({ image_type: imageType.value })
console.log(res.data)
imageOptions.value = res.data.map((item: any) => ({ id: item.id, label: item.image_name }))
} catch (error) {
}
}
const gpuCountOptions = ref<number[]>([])
// 镜像选项
const imageOptions = ref<any>([])
const handleChange = (e: any) => {
console.log(e)
}
const isShow=computed(()=>{
return userInfo.value.balance<computingPowerPoint.value
})
const handleCreate = async () => {
creating.value = true
try {
const params={
host_id:HostId.value,
billingMethod:billingType.value,
gpuNum:gpuCount.value,
case_name:instanceName.value,
image_id:selectedImage.value,
is_use_voucher:checked.value
}
const res=await createHost(params)
message.success('实例创建成功!')
creating.value=false
} catch (error) {
message.error('创建失败,请重试')
} finally {
creating.value = false
}
}
const handleCancel = () => {
window.history.back()
}
</script>
<style scoped>
.container {
display: block;
width: 100%;
padding: 24px;
box-sizing: border-box;
background: url('@/assets/bgImg.png') no-repeat center / 100% 100%;
}
.instance-create-container {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
background-color: #fff;
/* 添加这个来确保容器有相对定位 */
position: relative;
margin-bottom: 150px;
}
.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: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
padding: 24px 32px;
border-top: 1px solid #e8e8e8;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
display: flex;
justify-content: flex-end;
align-items: flex-start;
backdrop-filter: blur(10px);
z-index: 100;
gap: 40px;
/* 关键:让它占满整个 container 宽度 */
/* width: 100%; */
box-sizing: border-box;
/* 如果 container 有 padding需要抵消 */
margin: 0 -24px;
/* 因为 .container 有 padding: 24px */
padding: 24px 32px;
}
/* 左侧:费用汇总 */
.price-summary {
display: flex;
flex-direction: column;
gap: 20px;
flex: 1;
}
/* 账户余额信息 */
.account-info {
min-width: 160px;
}
.balance-label {
font-size: 14px;
color: #666;
/* margin-bottom: 8px; */
font-weight: 500;
}
.balance-amount {
display: flex;
align-items: center;
gap: 12px;
}
.balance-amount .amount {
font-size: 24px;
font-weight: 700;
color: #1890ff;
line-height: 1;
}
.recharge-link {
font-size: 13px;
color: #1890ff;
text-decoration: none;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s;
white-space: nowrap;
}
.recharge-link:hover {
background-color: rgba(24, 144, 255, 0.1);
}
/* 费用明细 */
.price-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.price-item {
display: flex;
align-items: baseline;
gap: 8px;
}
.price-label {
font-size: 14px;
color: #666;
min-width: 80px;
text-align: right;
}
.price-value {
font-size: 20px;
font-weight: 600;
color: #f7412d;
min-width: 100px;
}
.price-unit {
font-size: 13px;
color: #999;
font-weight: 500;
}
/* 原价显示 */
.original-price-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: linear-gradient(135deg, #fff2f0 0%, #fff7f6 100%);
border-radius: 6px;
border: 1px solid #ffccc7;
margin-top: 4px;
}
.original-price-label {
font-size: 13px;
color: #666;
}
.original-price-value {
font-size: 14px;
color: #999;
text-decoration: line-through;
font-weight: 500;
}
.discount-badge {
font-size: 12px;
font-weight: 600;
color: #ff4d4f;
background: rgba(255, 77, 79, 0.1);
padding: 2px 8px;
border-radius: 10px;
margin-left: auto;
}
/* 右侧:操作按钮区域 */
.action-buttons {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 280px;
}
/* 费用提示 */
.price-tips {
display: flex;
align-items: center;
gap: 8px;
justify-content: flex-end;
}
.tip-icon {
font-size: 14px;
}
.tip-text {
font-size: 12px;
color: #999;
}
/* 按钮组 */
.button-group {
display: flex;
gap: 16px;
justify-content: flex-end;
}
.cancel-btn {
padding: 0 32px;
height: 48px;
font-weight: 500;
border: 1px solid #d9d9d9;
border-radius: 8px;
background: white;
transition: all 0.3s;
min-width: 120px;
}
.cancel-btn:hover {
border-color: #1890ff;
color: #1890ff;
background: rgba(24, 144, 255, 0.04);
}
.create-btn {
padding: 0 24px 0 16px;
/* 调整padding给图标空间 */
height: 48px;
font-weight: 600;
border-radius: 8px;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border: none;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.25);
transition: all 0.3s;
min-width: 180px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.create-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.create-btn:hover::before {
left: 100%;
}
.create-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.35);
}
.create-btn:active {
transform: translateY(0);
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.25);
}
.create-btn:disabled {
background: #f5f5f5;
color: #bfbfbf;
transform: none;
box-shadow: none;
cursor: not-allowed;
}
.create-btn:disabled::before {
display: none;
}
.create-price {
margin-left: 8px;
font-size: 12px;
font-weight: 500;
opacity: 0.9;
letter-spacing: -0.2px;
background: rgba(255, 255, 255, 0.2);
padding: 2px 6px;
border-radius: 4px;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.footer-actions {
flex-direction: column;
gap: 24px;
padding: 20px 24px;
}
.price-summary {
width: 100%;
gap: 24px;
}
.action-buttons {
width: 100%;
}
.button-group {
width: 100%;
}
.cancel-btn,
.create-btn {
flex: 1;
}
}
@media (max-width: 768px) {
.footer-actions {
padding: 16px 20px;
margin: 0 -20px -20px;
}
.price-summary {
flex-direction: column;
gap: 16px;
}
.account-info {
min-width: auto;
}
.balance-amount {
justify-content: space-between;
}
.price-details {
gap: 8px;
}
.price-item {
justify-content: space-between;
}
.price-label {
min-width: auto;
text-align: left;
}
.original-price-item {
flex-wrap: wrap;
gap: 6px;
}
.discount-badge {
margin-left: 0;
}
.button-group {
flex-direction: column;
}
.create-price {
display: block;
margin-left: 0;
margin-top: 4px;
font-size: 11px;
}
.price-tips {
justify-content: center;
text-align: center;
}
}
</style>