2026-07-29 11:07:19 +08:00

714 lines
16 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="create-instance-page">
<main class="create-main">
<div class="breadcrumb-row">
<div class="breadcrumb">
<span class="link" @click="$router.push('/homePage/computeMarket')"> <i class="iconfont icon-icon_arrow_left"></i> 返回算力市场</span>
</div>
</div>
<section class="content-card">
<div class="card-header">
<h2>计费方式:</h2>
<button type="button" class="link-btn">计费规则</button>
</div>
<div class="option-list">
<button
v-for="option in billingOptions"
:key="option.value"
type="button"
class="option-btn"
:class="{ active: billingType === option.value }"
@click="billingType = option.value"
>
{{ option.label }}
</button>
</div>
<p class="card-tip">创建完主机后仍然可以转换计费方式如选择按量计费价格发生变动以实例开机时的价格为准</p>
</section>
<section class="content-card">
<div class="card-header">
<h2>选择主机:</h2>
</div>
<div class="host-table-wrap">
<table class="host-table">
<thead>
<tr>
<th></th>
<th>主机ID</th>
<th>算力型号/显存</th>
<th>空闲GPU</th>
<th>每GPU分配</th>
<th>CPU型号</th>
<th>硬盘</th>
<th>驱动/CUDA</th>
<th>价格(单卡)</th>
</tr>
</thead>
<tbody>
<tr
v-for="host in hosts"
:key="host.id"
:class="{ selected: selectedHostId === host.id }"
@click="selectedHostId = host.id"
>
<td>
<span class="radio-dot"></span>
</td>
<td class="host-name">{{ host.name }}</td>
<td>
<strong class="gpu-name">{{ host.gpu }}</strong>
<span class="gpu-memory"> {{ host.memory }}</span>
</td>
<td><strong class="free-count">{{ host.freeGpu }}</strong></td>
<td>{{ host.allocation }}</td>
<td class="muted small">{{ host.cpu }}</td>
<td class="muted small">{{ host.disk }}</td>
<td class="muted small">{{ host.driver }}</td>
<td>
<strong class="price">¥{{ host.price }}/</strong>
<span class="origin-price">¥{{ host.originalPrice }}/</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="field-block">
<h3>GPU数量:</h3>
<div class="option-list option-list--compact">
<button
v-for="count in gpuCounts"
:key="count"
type="button"
class="option-btn gpu-count-btn"
:class="{ active: gpuCount === count }"
@click="gpuCount = count"
>
{{ count }}
</button>
</div>
</div>
<div class="disk-row">
<span>数据盘: 免费50GB</span>
<label>
<input v-model="needExpandDisk" type="checkbox">
<span>需要扩容</span>
</label>
<div v-if="needExpandDisk" class="disk-input">
<input v-model.number="expandDiskSize" type="number" min="50">
<span>GB</span>
</div>
</div>
</section>
<section class="spec-bar">
<strong>实例规格:</strong>
<span>{{ specSummary }}</span>
</section>
<section class="content-card">
<div class="card-header">
<h2>镜像:</h2>
<button type="button" class="link-btn">没有我要的环境?</button>
</div>
<div class="option-list">
<button
v-for="option in imageTypes"
:key="option.value"
type="button"
class="option-btn image-option"
:class="{ active: imageType === option.value }"
@click="imageType = option.value"
>
{{ option.label }}
<em v-if="option.hot">HOT</em>
</button>
</div>
<p class="card-tip">基础镜像包含常用基本软件深度学习框架Miniconda等如需其他软件可创建后安装</p>
<div class="select-wrap">
<select v-model="selectedEnv">
<option value="">请选择框架名称/框架版本/Python版本/CUDA版本</option>
<option value="pytorch">PyTorch 2.1.0 / Python 3.10 / CUDA 12.1</option>
<option value="tensorflow">TensorFlow 2.14.0 / Python 3.10 / CUDA 12.1</option>
<option value="paddle">PaddlePaddle 2.5.1 / Python 3.10 / CUDA 11.8</option>
</select>
</div>
</section>
<section class="content-card">
<div class="card-header">
<h2>优惠券:</h2>
</div>
<div class="select-wrap">
<select v-model="coupon">
<option value="">请选择</option>
<option value="10">新人专享满100减10元</option>
<option value="50">充值满500减50元</option>
<option value="100">VIP用户满1000减100元</option>
</select>
</div>
</section>
<aside class="submit-bar">
<div class="submit-bar__inner">
<div class="submit-costs">
<span>日常费用: ¥0.00/</span>
<span>
配置费用
<strong>¥{{ estimatedPrice }}/</strong>
</span>
<button type="button" class="detail-btn">费用明细</button>
</div>
<div class="submit-actions">
<!-- <span class="balance">账户余额 ¥0.00 <em>余额不足去充值</em></span> -->
<button type="button" class="cancel-btn" @click="$router.push('/homePage/computeMarket')">取消</button>
<button type="button" class="create-btn" @click="submitCreate">
<i class="iconfont icon-icon_guanji-kaiji"></i>
创建并开机
</button>
</div>
</div>
</aside>
</main>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'ComputeMarketCreateInstance',
data() {
return {
billingType: 'hourly',
selectedHostId: 'D42',
gpuCount: 1,
needExpandDisk: false,
expandDiskSize: 100,
imageType: 'basic',
selectedEnv: '',
coupon: '',
billingOptions: [
{ label: '按量计费', value: 'hourly' },
{ label: '包日', value: 'daily' },
{ label: '包周', value: 'weekly' },
{ label: '包月', value: 'monthly' },
{ label: '包年', value: 'yearly' }
],
gpuCounts: [1, 2, 3, 4, 5, 6, 7, 8],
imageTypes: [
{ label: '基础镜像', value: 'basic' },
{ label: '社区镜像', value: 'community', hot: true },
{ label: '我的镜像', value: 'custom' }
],
hosts: [
{
id: 'D42',
name: 'D42机',
gpu: 'RTX PRO 6000',
memory: '96GB',
freeGpu: '1 / 8',
allocation: 'CPU: 25核 内存: 120GB',
cpu: 'Xeon(R) Platinum 8470Q',
disk: '数据盘: 50GB 可扩容: 1622GB',
driver: '驱动: 595.58.03 CUDA: 13.2',
price: 5.98,
originalPrice: 7.97
}
]
}
},
computed: {
...mapState({
loginStateVuex: state => state.login.loginState
}),
loginState() {
const userId = sessionStorage.getItem('userId')
return this.loginStateVuex || (userId !== null && userId !== 'null' && userId !== '')
},
selectedHost() {
return this.hosts.find(host => host.id === this.selectedHostId) || this.hosts[0]
},
dataDiskText() {
return this.needExpandDisk ? `数据盘:${this.expandDiskSize}GB SSD` : '数据盘免费50GB SSD'
},
specSummary() {
const host = this.selectedHost
return `GPU型号${host.gpu} *${this.gpuCount}卡 | CPU25核心 | 内存120GB | 系统盘30GB | ${this.dataDiskText}`
},
estimatedPrice() {
return (Number(this.selectedHost.price) * this.gpuCount).toFixed(2)
}
},
created() {
const gpu = this.$route.query.gpu
if (gpu) {
this.hosts[0].gpu = String(gpu).split('/')[0].trim()
this.hosts[0].memory = (String(gpu).split('/')[1] || '96 GB').trim()
}
},
methods: {
submitCreate() {
if (!this.loginState) {
this.$message.warning('请先登录后再创建实例')
window.dispatchEvent(new Event('kboss-open-login-dialog'))
return
}
if (!this.selectedEnv) {
this.$message.warning('请先选择镜像环境')
return
}
this.$message.success('实例创建请求已提交')
this.$router.push('/homePage/computeMarket')
}
}
}
</script>
<style scoped lang="scss">
.create-instance-page {
min-height: 100vh;
padding: 82px 0 88px;
background:
radial-gradient(circle at top left, rgba(30, 111, 255, 0.12), transparent 30%),
linear-gradient(180deg, #f7fbff 0%, #f5f7fb 100%);
}
.create-main {
width: min(1160px, calc(100% - 48px));
margin: 0 auto;
}
.breadcrumb-row {
display: flex;
justify-content: space-between;
margin-bottom: 18px;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 10px;
color: #98a2b3;
font-size: 14px;
.link {
color: #1e6fff;
cursor: pointer;
font-weight: 600;
}
strong {
color: #344054;
}
}
.content-card,
.spec-bar,
.submit-bar {
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(219, 234, 254, 0.78);
border-radius: 18px;
box-shadow: 0 12px 34px rgba(31, 72, 135, 0.08);
}
aside{
margin: 0;
}
.content-card {
padding: 24px;
margin-bottom: 18px;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
h2 {
margin: 0;
color: #1f2937;
font-size: 16px;
}
}
.link-btn {
color: #1e6fff;
font-size: 14px;
background: transparent;
border: none;
cursor: pointer;
}
.option-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 14px;
}
.option-list--compact {
margin-bottom: 0;
}
.option-btn {
min-width: 88px;
height: 40px;
padding: 0 18px;
color: #5f6b7a;
font-size: 14px;
font-weight: 600;
background: #fff;
border: 2px solid #e3e8f0;
border-radius: 12px;
cursor: pointer;
position: relative;
transition: all 0.2s ease;
&.active,
&:hover {
color: #1e6fff;
border-color: #1e6fff;
background: #eef5ff;
}
}
.image-option em {
position: absolute;
top: -10px;
right: -10px;
padding: 2px 7px;
color: #fff;
font-size: 10px;
font-style: normal;
background: #ef4444;
border-radius: 999px;
}
.card-tip {
margin: 0;
color: #8a94a6;
font-size: 13px;
line-height: 1.7;
}
.host-table-wrap {
overflow-x: auto;
margin-bottom: 22px;
border: 1px solid #edf1f7;
border-radius: 16px;
}
.host-table {
width: 100%;
min-width: 1060px;
border-collapse: collapse;
font-size: 13px;
th {
padding: 14px 12px;
color: #667085;
text-align: left;
font-weight: 700;
background: #f8fafc;
}
td {
padding: 16px 12px;
color: #5f6b7a;
border-top: 1px solid #edf1f7;
}
tr {
cursor: pointer;
}
tr.selected {
background: #f2f7ff;
}
}
.radio-dot {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #1e6fff;
border-radius: 50%;
box-shadow: inset 0 0 0 4px #fff;
background: #1e6fff;
}
.host-name {
color: #1f2937;
font-weight: 700;
}
.gpu-name {
color: #1e6fff;
}
.gpu-memory,
.muted {
color: #8a94a6;
}
.small {
font-size: 12px;
}
.free-count {
color: #10b981;
}
.price {
display: block;
color: #ef4444;
font-weight: 800;
}
.origin-price {
display: block;
color: #98a2b3;
font-size: 12px;
text-decoration: line-through;
}
.field-block {
margin-bottom: 18px;
h3 {
margin: 0 0 12px;
color: #1f2937;
font-size: 16px;
}
}
.gpu-count-btn {
min-width: 44px;
padding: 0 14px;
}
.disk-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 18px;
color: #5f6b7a;
font-size: 14px;
label {
display: inline-flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
input[type='checkbox'] {
width: 15px;
height: 15px;
accent-color: #1e6fff;
}
}
.disk-input {
display: inline-flex;
align-items: center;
gap: 6px;
input {
width: 92px;
height: 34px;
padding: 0 10px;
border: 1px solid #dfe5ef;
border-radius: 10px;
outline: none;
}
}
.spec-bar {
display: flex;
gap: 12px;
align-items: center;
padding: 18px 20px;
margin-bottom: 18px;
background: #eef5ff;
strong {
flex-shrink: 0;
color: #1e6fff;
font-size: 16px;
}
span {
color: #344054;
font-size: 14px;
line-height: 1.7;
}
}
.select-wrap {
position: relative;
select {
width: 100%;
height: 46px;
padding: 0 14px;
color: #667085;
background: #fff;
border: 1px solid #dfe5ef;
border-radius: 12px;
outline: none;
}
}
.submit-bar {
position: fixed;
right: 0;
bottom: 0 !important;
left: 0;
z-index: 30;
width: 100%;
padding: 0;
background: rgba(255, 255, 255, 0.98);
border: none;
border-top: 1px solid rgba(223, 229, 239, 0.95);
border-radius: 0;
box-shadow: 0 -10px 30px rgba(31, 72, 135, 0.08);
backdrop-filter: blur(10px);
}
.submit-bar__inner {
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
width: min(1600px, calc(100% - 48px));
min-height: 86px;
margin: 0 auto;
padding: 18px 0;
}
.submit-costs,
.submit-actions {
display: flex;
align-items: center;
gap: 16px;
}
.submit-costs span,
.balance {
color: #667085;
font-size: 14px;
}
.submit-costs strong {
color: #ef4444;
font-size: 20px;
font-weight: 800;
}
.detail-btn {
color: #1e6fff;
font-size: 13px;
background: transparent;
border: none;
cursor: pointer;
}
.balance em {
margin-left: 6px;
color: #ef4444;
font-size: 12px;
font-style: normal;
}
.cancel-btn,
.create-btn {
height: 48px;
padding: 0 26px;
font-size: 14px;
font-weight: 700;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
}
.cancel-btn {
color: #667085;
background: #fff;
border: 1px solid #dfe5ef;
&:hover {
color: #1e6fff;
border-color: #1e6fff;
}
}
.create-btn {
display: inline-flex;
align-items: center;
gap: 8px;
color: #fff;
background: linear-gradient(135deg, #1e6fff, #43a3ff);
border: none;
box-shadow: 0 10px 20px rgba(30, 111, 255, 0.24);
&:hover {
transform: translateY(-1px);
box-shadow: 0 12px 24px rgba(30, 111, 255, 0.3);
}
i {
font-size: 15px;
}
}
@media (max-width: 768px) {
.create-main {
width: calc(100% - 24px);
}
.content-card {
padding: 18px;
}
.card-header,
.spec-bar {
align-items: flex-start;
flex-direction: column;
}
.submit-bar__inner {
width: calc(100% - 24px);
min-height: auto;
align-items: stretch;
flex-direction: column;
gap: 10px;
padding: 16px 0;
}
.submit-costs,
.submit-actions {
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}
.create-btn {
flex: 1;
justify-content: center;
}
}
@media (max-width: 640px) {
.create-instance-page {
padding-top: 16px;
}
}
</style>