算力市场

This commit is contained in:
hrx 2026-06-16 11:50:02 +08:00
parent 35b0f3d5de
commit bb21b374d0
13 changed files with 2381 additions and 9 deletions

View File

@ -54,6 +54,24 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe600;</span>
<div class="name">购物车空</div>
<div class="code-name">&amp;#xe600;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe609;</span>
<div class="name">icon_关机-开机</div>
<div class="code-name">&amp;#xe609;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe62e;</span>
<div class="name">icon_arrow_left</div>
<div class="code-name">&amp;#xe62e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe60d;</span>
<div class="name">发送</div>
@ -138,9 +156,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1776735138822') format('woff2'),
url('iconfont.woff?t=1776735138822') format('woff'),
url('iconfont.ttf?t=1776735138822') format('truetype');
src: url('iconfont.woff2?t=1781579680075') format('woff2'),
url('iconfont.woff?t=1781579680075') format('woff'),
url('iconfont.ttf?t=1781579680075') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -166,6 +184,33 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-gouwuchekong"></span>
<div class="name">
购物车空
</div>
<div class="code-name">.icon-gouwuchekong
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-icon_guanji-kaiji"></span>
<div class="name">
icon_关机-开机
</div>
<div class="code-name">.icon-icon_guanji-kaiji
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-icon_arrow_left"></span>
<div class="name">
icon_arrow_left
</div>
<div class="code-name">.icon-icon_arrow_left
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-fasong"></span>
<div class="name">
@ -292,6 +337,30 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-gouwuchekong"></use>
</svg>
<div class="name">购物车空</div>
<div class="code-name">#icon-gouwuchekong</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-icon_guanji-kaiji"></use>
</svg>
<div class="name">icon_关机-开机</div>
<div class="code-name">#icon-icon_guanji-kaiji</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-icon_arrow_left"></use>
</svg>
<div class="name">icon_arrow_left</div>
<div class="code-name">#icon-icon_arrow_left</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-fasong"></use>

View File

@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 5043107 */
src: url('iconfont.woff2?t=1776735138822') format('woff2'),
url('iconfont.woff?t=1776735138822') format('woff'),
url('iconfont.ttf?t=1776735138822') format('truetype');
src: url('iconfont.woff2?t=1781579680075') format('woff2'),
url('iconfont.woff?t=1781579680075') format('woff'),
url('iconfont.ttf?t=1781579680075') format('truetype');
}
.iconfont {
@ -13,6 +13,18 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-gouwuchekong:before {
content: "\e600";
}
.icon-icon_guanji-kaiji:before {
content: "\e609";
}
.icon-icon_arrow_left:before {
content: "\e62e";
}
.icon-fasong:before {
content: "\e60d";
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,27 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "1306",
"name": "购物车空",
"font_class": "gouwuchekong",
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "11518201",
"name": "icon_关机-开机",
"font_class": "icon_guanji-kaiji",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "18418399",
"name": "icon_arrow_left",
"font_class": "icon_arrow_left",
"unicode": "e62e",
"unicode_decimal": 58926
},
{
"icon_id": "12719937",
"name": "发送",

View File

@ -337,6 +337,18 @@ export const constantRoutes = [
name: "homePageIndex",
hidden: true,
meta: { title: "首页", onCache: true },
}, {
path: "computeMarket",
component: () => import("@/views/homePage/computeMarket/index.vue"),
name: "computeMarket",
hidden: true,
meta: { title: "算力市场", onCache: true },
}, {
path: "computeMarket/createInstance",
component: () => import("@/views/homePage/computeMarket/createInstance.vue"),
name: "computeMarketCreateInstance",
hidden: true,
meta: { title: "创建实例", onCache: true },
}, {
path: "detail",
component: () => import("@/views/homePage/detail/index.vue"),
@ -1127,6 +1139,33 @@ export const asyncRoutes = [
},
// 容器实例 - 客户角色可见
{
path: "/containerInstance",
component: Layout,
redirect: "/containerInstance/index",
meta: {
title: "容器实例",
icon: "el-icon-box",
noCache: true,
fullPath: "/containerInstance",
roles: ["客户"]
},
children: [
{
path: "index",
component: () => import("@/views/customer/containerInstance"),
name: "ContainerInstance",
meta: {
title: "容器实例",
fullPath: "/containerInstance/index",
roles: ["客户"]
}
}
]
},
// 退订管理 - 变为一级菜单(包含子菜单)
{
path: "/unsubscribeManagement",

View File

@ -28,6 +28,7 @@ const BASE_USER_ROUTE_PATHS = ['/orderManagement', '/resourceManagement'];
// 客户角色额外能看到的一级菜单。
const CUSTOMER_EXTRA_ROUTE_PATHS = [
'/containerInstance',
'/unsubscribeManagement',
'/informationPerfect',
'/rechargeManagement',

View File

@ -0,0 +1,500 @@
<template>
<div class="container-instance-page">
<div class="page-header">
<div>
<h1>容器实例</h1>
<p>管理训练推理和实验环境中的容器实例</p>
</div>
</div>
<div class="stat-grid">
<div v-for="item in statCards" :key="item.label" class="stat-card">
<div class="stat-card__top">
<span class="stat-icon" :class="item.type">
<i :class="item.icon"></i>
</span>
<span>{{ item.label }}</span>
</div>
<strong>{{ item.value }}</strong>
</div>
</div>
<div class="table-card">
<el-table :data="containerList" stripe class="container-table">
<el-table-column width="48">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.checked"></el-checkbox>
</template>
</el-table-column>
<el-table-column label="容器名称" min-width="220">
<template slot-scope="scope">
<div class="name-cell">
<strong>{{ scope.row.name }}</strong>
<span>Pod: {{ scope.row.pod }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="120">
<template slot-scope="scope">
<span class="status-badge" :class="scope.row.statusType">
<i></i>{{ scope.row.status }}
</span>
</template>
</el-table-column>
<el-table-column label="命名空间" width="130">
<template slot-scope="scope">
<span class="namespace-tag">{{ scope.row.namespace }}</span>
</template>
</el-table-column>
<el-table-column label="镜像" min-width="200">
<template slot-scope="scope">
<div class="image-cell">
<strong>{{ scope.row.image }}</strong>
<span>{{ scope.row.gpu }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="disk" label="本地磁盘" width="100"></el-table-column>
<el-table-column prop="spec" label="CPU/内存" width="130"></el-table-column>
<el-table-column label="付费方式" width="110">
<template slot-scope="scope">
<span class="pay-tag" :class="scope.row.payType">{{ scope.row.payText }}</span>
</template>
</el-table-column>
<el-table-column prop="releaseTime" label="释放时间/停机时间" min-width="150"></el-table-column>
<el-table-column label="登录指令/密码" min-width="210">
<template slot-scope="scope">
<div class="login-cell">
<span>SSH: <em>{{ scope.row.ssh }}</em></span>
<span>密码: <em>{{ scope.row.password }}</em></span>
</div>
</template>
</el-table-column>
<el-table-column label="快捷工具" width="170">
<template slot-scope="scope">
<div class="tool-cell">
<el-button
size="mini"
:disabled="scope.row.statusType === 'pending'"
@click="handleTool('Jupyter', scope.row)"
>
Jupyter
</el-button>
<el-button
size="mini"
type="success"
plain
:disabled="scope.row.statusType === 'pending'"
@click="handleTool('实例监测', scope.row)"
>
实例监测
</el-button>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="210" fixed="right">
<template slot-scope="scope">
<div class="action-cell">
<el-button
v-if="scope.row.statusType === 'stopped' || scope.row.statusType === 'error'"
type="text"
class="success-text"
@click="handleAction('启动', scope.row)"
>
启动
</el-button>
<el-button
v-else
type="text"
class="warning-text"
:disabled="scope.row.statusType === 'pending'"
@click="handleAction('停止', scope.row)"
>
停止
</el-button>
<el-button type="text" @click="handleAction('重启', scope.row)">重启</el-button>
<el-button type="text" @click="handleAction('日志', scope.row)">日志</el-button>
<el-button type="text" class="danger-text" @click="handleAction('删除', scope.row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<script>
export default {
name: 'ContainerInstance',
data() {
return {
statCards: [
{ label: '运行中', value: 5, icon: 'el-icon-check', type: 'success' },
{ label: 'Pending', value: 1, icon: 'el-icon-time', type: 'warning' },
{ label: '异常/重启', value: 0, icon: 'el-icon-close', type: 'danger' },
{ label: 'GPU使用', value: '18 / 32 GB', icon: 'el-icon-cpu', type: 'primary' }
],
containerList: [
{
name: 'train-bert-finetune-7b8f9c',
pod: 'train-bert-finetune-7b8f9c-xkq2m',
status: 'Running',
statusType: 'running',
namespace: 'ml-training',
image: 'pytorch/pytorch:2.1.0',
gpu: 'GPU: A100 40GB x 1',
disk: '50GB',
spec: '4 Core / 16 GB',
payText: '按量计费',
payType: 'hourly',
releaseTime: '2025-02-18 10:30',
ssh: 'ssh root@10.0.0.1',
password: 'Vm**k9#2'
},
{
name: 'llama2-inference-3d2e1a',
pod: 'llama2-inference-3d2e1a-pqr8n',
status: 'Running',
statusType: 'running',
namespace: 'ml-inference',
image: 'meta-llama/llama2:7b-chat',
gpu: 'GPU: RTX 4090 x 1',
disk: '100GB',
spec: '8 Core / 32 GB',
payText: '包月',
payType: 'monthly',
releaseTime: '2025-02-15 09:15',
ssh: 'ssh root@10.0.0.2',
password: 'Ab**c3$5'
},
{
name: 'sd-training-exp-9f4b2c',
pod: 'sd-training-exp-9f4b2c-zlm3p',
status: 'Pending',
statusType: 'pending',
namespace: 'ml-training',
image: 'stabilityai/stable-diffusion:fp16',
gpu: 'GPU: A100 80GB x 2',
disk: '200GB',
spec: '16 Core / 64 GB',
payText: '按量计费',
payType: 'hourly',
releaseTime: '-',
ssh: '等待分配',
password: '-'
},
{
name: 'glm-finetune-pod-5e6d7f',
pod: 'glm-finetune-pod-5e6d7f-abc9h',
status: 'Error',
statusType: 'error',
namespace: 'ml-tuning',
image: 'chatglm2-6b:fp16',
gpu: 'GPU: V100 32GB x 1',
disk: '80GB',
spec: '8 Core / 32 GB',
payText: '按量计费',
payType: 'hourly',
releaseTime: '异常停机',
ssh: 'ssh root@10.0.0.4',
password: 'Xy**z8#1'
},
{
name: 'qwen-infer-srv-2c3d4e',
pod: 'qwen-infer-srv-2c3d4e-hjk7m',
status: 'Stopped',
statusType: 'stopped',
namespace: 'ml-inference',
image: 'qwen-7b-chat:beta',
gpu: 'GPU: A10G x 1',
disk: '50GB',
spec: '4 Core / 16 GB',
payText: '包周',
payType: 'weekly',
releaseTime: '2025-01-24 14:20',
ssh: 'ssh root@10.0.0.5',
password: 'Pq**r6$2'
}
]
}
},
methods: {
handleCreate() {
this.$router.push('/homePage/computeMarket')
},
handleTool(name, row) {
this.$message.info(`${name}${row.name}`)
},
handleAction(action, row) {
this.$message.info(`${action}${row.name}`)
}
}
}
</script>
<style scoped lang="scss">
.container-instance-page {
min-height: calc(100vh - 84px);
padding: 24px;
background: #f5f7fb;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
h1 {
margin: 0;
color: #1f2937;
font-size: 24px;
font-weight: 700;
}
p {
margin: 8px 0 0;
color: #8a94a6;
font-size: 14px;
}
}
.create-btn {
border-radius: 10px;
box-shadow: 0 10px 18px rgba(64, 158, 255, 0.18);
}
.stat-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
margin-bottom: 24px;
}
.stat-card,
.table-card {
background: #fff;
border: 1px solid #edf1f7;
border-radius: 16px;
box-shadow: 0 10px 28px rgba(31, 72, 135, 0.06);
}
.stat-card {
padding: 22px;
strong {
display: block;
color: #1f2937;
font-size: 26px;
line-height: 1.2;
}
}
.stat-card__top {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
color: #667085;
font-size: 14px;
}
.stat-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 12px;
&.success {
color: #059669;
background: #dff8ee;
}
&.warning {
color: #d97706;
background: #fef3c7;
}
&.danger {
color: #dc2626;
background: #fee2e2;
}
&.primary {
color: #1e6fff;
background: #e8f1ff;
}
}
.table-card {
padding: 14px;
}
.container-table {
::v-deep th {
color: #667085;
font-size: 12px;
font-weight: 700;
background: #f8fafc;
}
::v-deep td {
color: #5f6b7a;
font-size: 13px;
}
}
.name-cell,
.image-cell,
.login-cell {
display: flex;
flex-direction: column;
gap: 4px;
strong {
color: #1f2937;
font-weight: 600;
}
span {
color: #98a2b3;
font-size: 12px;
}
}
.login-cell em {
color: #1e6fff;
font-family: Consolas, Monaco, monospace;
font-style: normal;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 600;
i {
width: 7px;
height: 7px;
border-radius: 50%;
}
&.running {
color: #059669;
background: #dff8ee;
i {
background: #10b981;
}
}
&.pending {
color: #d97706;
background: #fef3c7;
i {
background: #f59e0b;
}
}
&.error {
color: #dc2626;
background: #fee2e2;
i {
background: #ef4444;
}
}
&.stopped {
color: #667085;
background: #eef2f7;
i {
background: #98a2b3;
}
}
}
.namespace-tag,
.pay-tag {
display: inline-flex;
padding: 4px 8px;
border-radius: 8px;
font-size: 12px;
}
.namespace-tag {
color: #1e6fff;
background: #eef5ff;
}
.pay-tag.hourly {
color: #d97706;
background: #fef3c7;
}
.pay-tag.monthly,
.pay-tag.weekly {
color: #1e6fff;
background: #eef5ff;
}
.tool-cell,
.action-cell {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.success-text {
color: #10b981;
}
.warning-text {
color: #f59e0b;
}
.danger-text {
color: #ef4444;
}
@media (max-width: 1100px) {
.stat-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.container-instance-page {
padding: 16px;
}
.page-header {
align-items: flex-start;
flex-direction: column;
}
.stat-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -19,9 +19,11 @@
<a @click="goHome">首页</a>
</p>
<!-- 产品与服务鼠标移入显示子菜单 -->
<p @mouseleave="sildeOut" @mouseenter="sildeIn(product_service)">
<!-- <p @mouseleave="sildeOut" @mouseenter="sildeIn(product_service)">
<a>算力云</a>
</p>
</p> -->
<!-- 算力市场 -->
<p class="nav-hover" :class="{ active: $route.path.includes('/computeMarket') }" @click="$router.push('/homePage/computeMarket')">算力市场</p>
<p class="nav-hover" :class="{ active: isActiveTokenMarket }" @click="handleModelSquareClick">token市集</p>
<!-- 训推平台 -->
<p class="nav-hover" @click="goTrainPlatform">训推平台</p>
@ -410,6 +412,11 @@ export default Vue.extend({
})
},
//
goComputeMarket() {
this.$router.push('/homePage/computeMarket')
},
//
goTrainPlatform() {
window.open('http://101.200.145.167:8923/', '_blank')

View File

@ -0,0 +1,713 @@
<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('请先登录后再创建实例')
this.$router.push({
path: '/login',
query: {
fromPath: 'computeMarketCreateInstance',
redirect: this.$route.fullPath
}
})
return
}
if (!this.selectedEnv) {
this.$message.warning('请先选择镜像环境')
return
}
this.$message.success('实例创建请求已提交')
this.$router.push('/containerInstance/index')
}
}
}
</script>
<style scoped lang="scss">
.create-instance-page {
min-height: 100vh;
padding: 28px 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;
}
}
</style>

File diff suppressed because it is too large Load Diff