算力平台实例管理

This commit is contained in:
Leo_Ding 2026-01-20 09:05:51 +08:00
parent 2d0d6eb59b
commit f6ece66b1a
6 changed files with 431 additions and 390 deletions

View File

@ -52,3 +52,8 @@ export const exchangePoint=(params:any)=>request.put('/v1/balance/exchange_point
//创建实例 //创建实例
export const createHost=(params:any)=>request.post('/v1/host/host_case',params) export const createHost=(params:any)=>request.post('/v1/host/host_case',params)
//释放实例
export const releaseCase=(caseId:string)=>request.post(`/v1/host_case/release_case/${caseId}`)
//重置实例
export const reStartCase=(caseId:string)=>request.post(`/v1/host_case/restart_case/${caseId}`)

15
src/utils/dict.ts Normal file
View File

@ -0,0 +1,15 @@
export const payTypeDic = new Map([
["PayOnTime", "按量计费"],
["PayOnDay", "包日"],
["PayOnWeek", "包周"],
["PayOnMonth", "包月"],
["PayOnYear", "包年"],
]);
export const instanceStatus = new Map([
["RUNNING", { text: "运行中", color: "#67C23A" }],
["STOPPED", { text: "已停止", color: "#909399" }],
["CREATING", { text: "创建中", color: "#E6A23C" }],
["RESTARTING", { text: "重启中", color: "#E6A23C" }],
["RELEASED", { text: "已释放", color: "#F56C6C" }],
]);

View File

@ -6,8 +6,8 @@
<div class="header-left"> <div class="header-left">
<span class="page-title">容器实例</span> <span class="page-title">容器实例</span>
<span class="warning-tip"> <span class="warning-tip">
<exclamation-circle-outlined class="warning-icon" /> <!-- <exclamation-circle-outlined class="warning-icon" /> -->
实例连续关机15天会释放实例实例释放会导致数据清空且不可恢复释放前实例数据仍在 <!-- 实例连续关机15天会释放实例实例释放会导致数据清空且不可恢复释放前实例数据仍在 -->
</span> </span>
</div> </div>
</div> </div>
@ -23,10 +23,8 @@
<div class="header-filter"> <div class="header-filter">
<a-select v-model:value="filterStatus" placeholder="筛选状态" style="width: 160px;" size="large" <a-select v-model:value="filterStatus" placeholder="筛选状态" style="width: 160px;" size="large"
@change="handleSearch"> @change="handleSearch">
<a-select-option value="all">全部状态</a-select-option> <a-select-option value="">全部状态</a-select-option>
<a-select-option value="running">运行中</a-select-option> <a-select-option v-for="[key, config] in instanceStatus" :value="key">{{ config.text }}</a-select-option>
<a-select-option value="stopped">已停止</a-select-option>
<a-select-option value="error">异常</a-select-option>
</a-select> </a-select>
<a-input-search v-model:value="searchKeyword" placeholder="搜索实例名称/ID" style="width: 240px; margin-left: 12px;" <a-input-search v-model:value="searchKeyword" placeholder="搜索实例名称/ID" style="width: 240px; margin-left: 12px;"
size="large" @search="handleSearch" /> size="large" @search="handleSearch" />
@ -41,9 +39,8 @@
<!-- 状态列 --> <!-- 状态列 -->
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<span :class="`status-${record.status}`"> <span :style="{ color: instanceStatus.get(record.status)?.color }">{{ instanceStatus.get(record.status)?.text
{{ getStatusText(record.status) }} }}</span>
</span>
</template> </template>
<!-- 规格详情列 --> <!-- 规格详情列 -->
@ -64,7 +61,11 @@
{{ getHealthStatusText(record.health_status) }} {{ getHealthStatusText(record.health_status) }}
</span> </span>
</template> </template>
<template v-else-if="column.key === 'price_type'">
<span>
{{ payTypeDic.get(record.price_type) }}
</span>
</template>
<!-- 操作列 --> <!-- 操作列 -->
<template v-else-if="column.key === 'actions'"> <template v-else-if="column.key === 'actions'">
<div class="action-buttons"> <div class="action-buttons">
@ -147,14 +148,10 @@
<p style="color: #666;"> 实例配置和网络设置保持不变</p> <p style="color: #666;"> 实例配置和网络设置保持不变</p>
</div> </div>
</div> </div>
<template #okText>
<span style="color: #ff4d4f;">确认重置</span>
</template>
</a-modal> </a-modal>
<!-- 释放确认模态框 --> <!-- 释放确认模态框 -->
<a-modal v-model:open="releaseModalVisible" title="确认释放" @ok="confirmRelease" @cancel="releaseModalVisible = false" <a-modal v-model:open="releaseModalVisible" title="确认释放" @ok="confirmRelease" @cancel="releaseModalVisible = false" >
:ok-button-props="{ danger: true }">
<div class="modal-content"> <div class="modal-content">
<exclamation-circle-outlined class="warning-icon" <exclamation-circle-outlined class="warning-icon"
style="color: #ff4d4f; font-size: 24px; margin-right: 12px;" /> style="color: #ff4d4f; font-size: 24px; margin-right: 12px;" />
@ -173,15 +170,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import {ExclamationCircleOutlined,ReloadOutlined,EyeOutlined,DownOutlined,RedoOutlined,DeleteOutlined} from '@ant-design/icons-vue' import { ExclamationCircleOutlined, ReloadOutlined, EyeOutlined, DownOutlined, RedoOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import type { TableColumnType } from 'ant-design-vue' import type { TableColumnType } from 'ant-design-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { hostCaseList } from '@/apis/admin' import { hostCaseList } from '@/apis/admin'
import { releaseCase, reStartCase } from '@/apis/home'
import { get } from 'http' import { get } from 'http'
import { payTypeDic, instanceStatus } from '@/utils/dict'
const router = useRouter() const router = useRouter()
// //
const filterStatus = ref('all') const filterStatus = ref('')
const searchKeyword = ref('') const searchKeyword = ref('')
const loading = ref(false) const loading = ref(false)
const specModalVisible = ref(false) const specModalVisible = ref(false)
@ -217,7 +216,6 @@ const columns = ref<TableColumnType[]>([
{ title: '名称', dataIndex: 'name', key: 'name', width: 180 }, { title: '名称', dataIndex: 'name', key: 'name', width: 180 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 }, { title: '状态', dataIndex: 'status', key: 'status', width: 100 },
{ title: '规格详情', dataIndex: 'spec', key: 'spec', width: 200 }, { title: '规格详情', dataIndex: 'spec', key: 'spec', width: 200 },
{ title: '本地磁盘', dataIndex: 'system_disk', key: 'system_disk', width: 120 },
{ title: '健康状态', dataIndex: 'health_status', key: 'health_status', width: 120 }, { title: '健康状态', dataIndex: 'health_status', key: 'health_status', width: 120 },
{ title: '付费方式', dataIndex: 'price_type', key: 'price_type', width: 120 }, { title: '付费方式', dataIndex: 'price_type', key: 'price_type', width: 120 },
{ title: '释放时间', dataIndex: 'release_at', key: 'release_at', width: 120 }, { title: '释放时间', dataIndex: 'release_at', key: 'release_at', width: 120 },
@ -232,7 +230,9 @@ const getDataList = async () => {
loading.value = true loading.value = true
const params = { const params = {
page_num: paginationState.value.current, page_num: paginationState.value.current,
page_size: paginationState.value.pageSize page_size: paginationState.value.pageSize,
status: filterStatus.value,
name: searchKeyword.value
} }
const res = await hostCaseList(params) const res = await hostCaseList(params)
mockData.value = res.data mockData.value = res.data
@ -247,23 +247,11 @@ const getDataList = async () => {
getDataList() getDataList()
//
const getStatusText = (status: string) => {
const map: Record<string, any> = {
"RUNNING": '运行中',
"STOPPED": '已停止',
"CREATING": '创建中',
"RESTARTING": '重启中',
}
return map[status] || status
}
// //
const getHealthStatusText = (status: string) => { const getHealthStatusText = (status: string) => {
const map: Record<string, string> = { const map: Record<string, string> = {
healthy: '健康', Normal: '正常',
warning: '警告', Abnormal: '异常'
error: '异常'
} }
return map[status] || status return map[status] || status
} }
@ -325,32 +313,32 @@ const handleMenuClick = (key: string, record: any) => {
} }
// //
const confirmReset = () => { const confirmReset = async () => {
if (!currentInstance.value) return if (!currentInstance.value) return
try {
message.loading({ content: '正在重置实例...', key: 'reset', duration: 0 }) const res = await reStartCase(currentInstance.value.id)
setTimeout(() => {
//
currentInstance.value.health_status = 'healthy'
message.success({ content: '实例重置成功', key: 'reset' }) message.success({ content: '实例重置成功', key: 'reset' })
resetModalVisible.value = false resetModalVisible.value = false
}, 2000) getDataList()
} catch (error: any) {
message.error(error)
resetModalVisible.value = false
}
} }
// //
const confirmRelease = () => { const confirmRelease = async () => {
if (!currentInstance.value) return if (!currentInstance.value) return
try {
message.loading({ content: '正在释放实例...', key: 'release', duration: 0 }) const res = await releaseCase(currentInstance.value.id)
setTimeout(() => {
//
const index = mockData.value.findIndex(item => item.id === currentInstance.value.id)
if (index !== -1) {
mockData.value.splice(index, 1)
}
message.success({ content: '实例释放成功', key: 'release' }) message.success({ content: '实例释放成功', key: 'release' })
releaseModalVisible.value = false releaseModalVisible.value = false
}, 2000) getDataList()
} catch (error: any) {
message.error(error)
releaseModalVisible.value = false
}
} }
// //
@ -366,6 +354,7 @@ const refreshData = () => {
// //
const handleSearch = () => { const handleSearch = () => {
paginationState.value.current = 1 paginationState.value.current = 1
getDataList()
} }
// //
@ -379,7 +368,7 @@ const onTableChange = (pag: any) => {
// //
const handleRent = () => { const handleRent = () => {
router.push('/layout/admin/instanceCreate') router.push('/layout/market')
} }
onMounted(() => { onMounted(() => {
@ -572,7 +561,7 @@ onMounted(() => {
} }
// //
.health-healthy { .health-Normal {
color: #52c41a; color: #52c41a;
background: #f6ffed; background: #f6ffed;
padding: 4px 8px; padding: 4px 8px;
@ -581,16 +570,16 @@ onMounted(() => {
display: inline-block; display: inline-block;
} }
.health-warning { // .health-Abnormal {
color: #faad14; // color: #faad14;
background: #fffbe6; // background: #fffbe6;
padding: 4px 8px; // padding: 4px 8px;
border-radius: 4px; // border-radius: 4px;
font-size: 12px; // font-size: 12px;
display: inline-block; // display: inline-block;
} // }
.health-error { .health-Abnormal {
color: #ff4d4f; color: #ff4d4f;
background: #fff2f0; background: #fff2f0;
padding: 4px 8px; padding: 4px 8px;

View File

@ -15,12 +15,13 @@
<a-card class="card" title="计费方式"> <a-card class="card" title="计费方式">
<div class="billing-content"> <div class="billing-content">
<a-radio-group v-model:value="billingType" button-style="solid" @change="handleBillingTypeChange"> <a-radio-group v-model:value="billingType" button-style="solid" @change="handleBillingTypeChange">
<a-radio-button value="payg">按量计费</a-radio-button> <a-radio-button value="PayOnTime">按量计费</a-radio-button>
<a-radio-button value="daily">包日</a-radio-button> <a-radio-button value="PayOnDay">包日</a-radio-button>
<a-radio-button value="weekly">包周</a-radio-button> <a-radio-button value="PayOnWeek">包周</a-radio-button>
<a-radio-button value="monthly">包月</a-radio-button> <a-radio-button value="PayOnMonth">包月</a-radio-button>
<a-radio-button value="PayOnYear">包年</a-radio-button>
</a-radio-group> </a-radio-group>
<a href="/docs/price/" target="_blank" class="billing-link">计费规则</a> <!-- <a href="/docs/price/" target="_blank" class="billing-link">计费规则</a> -->
</div> </div>
<div class="note"> <div class="note">
创建完主机后仍然可以转换计费方式如选择按量计费价格发生变动以实例开机时的价格为准 创建完主机后仍然可以转换计费方式如选择按量计费价格发生变动以实例开机时的价格为准
@ -38,7 +39,7 @@
<a-radio-button v-for="region in regions" :key="region.value" :value="region.value"> <a-radio-button v-for="region in regions" :key="region.value" :value="region.value">
{{ region.label }} {{ region.label }}
<a-tag v-if="region.tag" :color="region.tag.color" class="region-tag">{{ region.tag.text <a-tag v-if="region.tag" :color="region.tag.color" class="region-tag">{{ region.tag.text
}}</a-tag> }}</a-tag>
</a-radio-button> </a-radio-button>
</a-radio-group> </a-radio-group>
</div> </div>
@ -320,7 +321,7 @@ interface Coupon {
} }
// //
const billingType = ref('weekly') const billingType = ref('PayOnTime')
const selectedRegion = ref('beijingDC2') const selectedRegion = ref('beijingDC2')
const selectedZone = ref('') const selectedZone = ref('')
const selectedGpuModels = ref<string[]>(['RTX 5090']) const selectedGpuModels = ref<string[]>(['RTX 5090'])
@ -1041,173 +1042,174 @@ const showPriceBreakdown = computed(() => {
} }
.footer-actions { .footer-actions {
position: sticky; position: sticky;
bottom: 0; bottom: 0;
background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); background: linear-gradient(135deg, #ffffff 0%, #fafafa 100%);
padding: 20px 32px; padding: 20px 32px;
border-top: 1px solid #e8e8e8; border-top: 1px solid #e8e8e8;
box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.08); box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.08);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
margin: 0 -24px -24px; margin: 0 -24px -24px;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
z-index: 100; z-index: 100;
} }
.price-summary { .price-summary {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 32px; gap: 32px;
} }
.price-main { .price-main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
} }
.total-label { .total-label {
font-size: 14px; font-size: 14px;
color: #666; color: #666;
font-weight: 500; font-weight: 500;
} }
.price-display { .price-display {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
gap: 4px; gap: 4px;
} }
.total-price { .total-price {
font-size: 32px; font-size: 32px;
font-weight: 700; font-weight: 700;
color: #f7412d; color: #f7412d;
line-height: 1; line-height: 1;
} }
.price-unit { .price-unit {
font-size: 14px; font-size: 14px;
color: #999; color: #999;
font-weight: 500; font-weight: 500;
} }
.original-price-container { .original-price-container {
margin-top: 2px; margin-top: 2px;
} }
.original-total-price { .original-total-price {
text-decoration: line-through; text-decoration: line-through;
color: #999; color: #999;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
} }
.price-breakdown { .price-breakdown {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
padding: 12px 16px; padding: 12px 16px;
background: #f8f9fa; background: #f8f9fa;
border-radius: 8px; border-radius: 8px;
border: 1px solid #e9ecef; border: 1px solid #e9ecef;
} }
.breakdown-item { .breakdown-item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
font-size: 12px; font-size: 12px;
} }
.breakdown-label { .breakdown-label {
color: #666; color: #666;
white-space: nowrap; white-space: nowrap;
} }
.breakdown-value { .breakdown-value {
color: #333; color: #333;
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
} }
.breakdown-value.discount { .breakdown-value.discount {
color: #52c41a; color: #52c41a;
} }
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 16px; gap: 16px;
align-items: center; align-items: center;
} }
.cancel-btn { .cancel-btn {
padding: 0 24px; padding: 0 24px;
height: 48px; height: 48px;
font-weight: 500; font-weight: 500;
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
border-radius: 8px; border-radius: 8px;
} }
.cancel-btn:hover { .cancel-btn:hover {
border-color: #1890ff; border-color: #1890ff;
color: #1890ff; color: #1890ff;
} }
.create-btn { .create-btn {
padding: 0 32px; padding: 0 32px;
height: 48px; height: 48px;
font-weight: 600; font-weight: 600;
border-radius: 8px; border-radius: 8px;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border: none; border: none;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3); box-shadow: 0 2px 8px rgba(24, 144, 255, 0.3);
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.create-btn:hover { .create-btn:hover {
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4); box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
} }
.create-btn:disabled { .create-btn:disabled {
background: #f5f5f5; background: #f5f5f5;
color: #d9d9d9; color: #d9d9d9;
transform: none; transform: none;
box-shadow: none; box-shadow: none;
cursor: not-allowed; cursor: not-allowed;
} }
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.footer-actions { .footer-actions {
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
padding: 16px 20px; padding: 16px 20px;
} }
.price-summary { .price-summary {
width: 100%; width: 100%;
justify-content: space-between; justify-content: space-between;
} }
.price-breakdown { .price-breakdown {
display: none; /* 移动端隐藏价格明细 */ display: none;
} /* 移动端隐藏价格明细 */
}
.action-buttons { .action-buttons {
width: 100%; width: 100%;
} }
.cancel-btn, .cancel-btn,
.create-btn { .create-btn {
flex: 1; flex: 1;
} }
.total-price { .total-price {
font-size: 28px; font-size: 28px;
} }
} }
.price-unit { .price-unit {

View File

@ -1,101 +1,114 @@
<template> <template>
<div class="container"> <div class="container">
<div class="instance-create-container"> <a-spin :spinning="spinning">
<!-- 服务器选择 --> <div class="instance-create-container">
<a-card class="card select-server" title="服务器选择"> <!-- 服务器选择 -->
<div class="list-filter"> <a-card class="card select-server" title="服务器选择">
<div class="filter-item"> <div class="list-filter">
<span class="filter-label">实例名称:</span> <div class="filter-item">
<div class="filter-content"> <span class="filter-label">实例名称:</span>
<a-input v-model:value="instanceName"></a-input> <div class="filter-content">
<a-input v-model:value="instanceName"></a-input>
</div>
</div> </div>
</div> <div class="filter-item">
<div class="filter-item"> <span class="filter-label">计费方式<span style="color: #ff4d4f;margin: 0 2px">*</span>:</span>
<span class="filter-label">计费方式<span style="color: #ff4d4f;margin: 0 2px">*</span>:</span> <div class="filter-content">
<div class="filter-content"> <a-radio-group v-model:value="billingType" button-style="solid">
<a-radio-group v-model:value="billingType" button-style="solid"> <a-radio-button value="PayOnTime">按量计费</a-radio-button>
<a-radio-button value="PayOnTime">按量计费</a-radio-button> <a-radio-button value="PayOnDay">包日</a-radio-button>
<a-radio-button value="PayOnDay">包日</a-radio-button> <a-radio-button value="PayOnWeek">包周</a-radio-button>
<a-radio-button value="PayOnWeek">包周</a-radio-button> <a-radio-button value="PayOnMonth">包月</a-radio-button>
<a-radio-button value="PayOnMonth">包月</a-radio-button> <a-radio-button value="PayOnYear">包年</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-radio-group>
</div>
</div>
</a-form-item> <!-- 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>
<a-form-item> <div class="filter-item">
<div class="basic-image" style="margin-top: 15px;"> <span class="filter-label">镜像<span style="color: #ff4d4f;margin: 0 2px;">*</span>:</span>
<a-select ref="select" v-model:value="selectedImage" style="width: 300px" <a-form layout="vertical">
@change="handleChange"> <a-form-item class="image-type">
<a-select-option v-for="item in imageOptions" :value="item.id">{{ item.label <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-option>
</a-select> </a-select>
</div> </div>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div>
</div> </div>
</div>
<!-- 主机表格 --> <!-- 主机表格 -->
<div class="machine-table"> <div class="machine-table">
<a-descriptions layout="vertical" bordered :column="8"> <a-descriptions layout="vertical" bordered :column="8">
<a-descriptions-item label="主机ID">{{ serviceInfo.id }}</a-descriptions-item> <a-descriptions-item label="主机ID">{{ serviceInfo.id }}</a-descriptions-item>
<a-descriptions-item label="算力型号/显存">{{ serviceInfo.gpuType + '/' + serviceInfo.vram + 'GB' <a-descriptions-item label="算力型号/显存">{{ serviceInfo.gpuType + '/' + serviceInfo.vram + 'GB'
}}</a-descriptions-item> }}</a-descriptions-item>
<a-descriptions-item label="空闲GPU">{{ serviceInfo.gpuAvailableNum }}</a-descriptions-item> <a-descriptions-item label="空闲GPU">{{ serviceInfo.gpuAvailableNum }}</a-descriptions-item>
<a-descriptions-item label="每GPU分配"> <a-descriptions-item label="每GPU分配">
<span>{{ 'CPU:' + serviceInfo.cpuNum + '核' }}</span><br> <span>{{ 'CPU:' + serviceInfo.cpuNum + '核' }}</span><br>
<span>{{ '内存:' + serviceInfo.memory + "GB" }}</span> <span>{{ '内存:' + serviceInfo.memory + "GB" }}</span>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="CPU型号">{{ serviceInfo.cpuType }}</a-descriptions-item> <a-descriptions-item label="CPU型号">{{ serviceInfo.cpuType }}</a-descriptions-item>
<a-descriptions-item label="硬盘"> <a-descriptions-item label="硬盘">
<span>{{ '数据盘:' + serviceInfo.dataDisk + 'GB' }}</span><br> <span>{{ '数据盘:' + serviceInfo.dataDisk + 'GB' }}</span><br>
<span>{{ '系统盘:' + serviceInfo.systemDisk + "GB" }}</span> <span>{{ '系统盘:' + serviceInfo.systemDisk + "GB" }}</span>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="驱动/CUDA">{{ serviceInfo.gpuDriver + '/' + serviceInfo.cudaVersion <a-descriptions-item label="驱动/CUDA">{{ serviceInfo.gpuDriver + '/' +
}}</a-descriptions-item> serviceInfo.cudaVersion
<a-descriptions-item label="消耗算力点(单卡)">{{ serviceInfo.price }}</a-descriptions-item> }}</a-descriptions-item>
</a-descriptions> <a-descriptions-item label="消耗算力点(单卡)">{{ serviceInfo.price }}</a-descriptions-item>
</div> </a-descriptions>
</a-card> </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 class="card coupon-selection">
</a-card> <div style="display: flex;align-items: center;">
<!-- 底部操作栏 --> <div>算力券</div>
</div> <a-select ref="select" v-model:value="voucherId" style="width:300px" @change="handleChange">
<a-select-option :value="''">不使用算力券</a-select-option>
<a-select-option v-for="value in voucherList" :value="value.id">{{ value.name + ' - '+''+value.amount }}</a-select-option>
</a-select>
</div>
</a-card>
<!-- 底部操作栏 -->
</div>
</a-spin>
<div class="footer-actions"> <div class="footer-actions">
<div class="price-summary"> <div class="price-summary">
<div>消耗算力点{{totalMoney }}</div> <div>消耗算力点{{ totalMoney }}</div>
<!-- 账户余额信息 --> <!-- 账户余额信息 -->
<div class="account-info" style="display: flex;align-items: center;"> <div class="account-info" style="display: flex;align-items: center;">
<div class="balance-label" style="margin-right: 10px;">可用算力点{{ userInfo.computingPowerPoint+' 点' }}</div> <div class="balance-label" style="margin-right: 10px;">可用算力点{{ userInfo.computingPowerPoint + ' 点'
}}
</div>
<!-- <div v-if="isShow" style="color: #ff4d4f;font-size: 12px;cursor: pointer;" <!-- <div v-if="isShow" style="color: #ff4d4f;font-size: 12px;cursor: pointer;"
@click="router.push('/layout/admin/balance')"> @click="router.push('/layout/admin/balance')">
算力点不足去充值</div> --> 算力点不足去充值</div> -->
@ -115,7 +128,7 @@
取消 取消
</a-button> </a-button>
<a-button type="primary" size="large" @click="handleCreate" :loading="creating" <a-button type="primary" size="large" @click="handleCreate" :loading="creating"
:disabled="selectedImage==''" class="create-btn"> :disabled="selectedImage == ''" class="create-btn">
<template #icon> <template #icon>
<PlusOutlined /> <PlusOutlined />
</template> </template>
@ -132,10 +145,10 @@
import { ref, computed, reactive, watch, onMounted } from 'vue' import { ref, computed, reactive, watch, onMounted } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { TableColumnsType } from 'ant-design-vue' import type { TableColumnsType } from 'ant-design-vue'
import { useRoute,useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { getImages } from "@/apis/home" import { getImages, getRecommendList } from "@/apis/home"
import { getMyCouponTotal, getTotalCost, getHostInfo,createHost } from "@/apis/home" import { getMyCouponTotal, getTotalCost, getHostInfo, createHost } from "@/apis/home"
const router=useRouter() const router = useRouter()
interface GpuModelOption { interface GpuModelOption {
value: string value: string
label: string label: string
@ -143,10 +156,11 @@ interface GpuModelOption {
total: number total: number
regions: string[] regions: string[]
} }
const voucherList = ref<any[]>([])
const instanceName=ref('') const instanceName = ref('')
const checked = ref(false) const checked = ref(false)
const totalMoney = ref(0) const totalMoney = ref(0)
const voucherId = ref('')
// //
const billingType = ref('PayOnTime') const billingType = ref('PayOnTime')
const gpuCount = ref(5) const gpuCount = ref(5)
@ -156,25 +170,33 @@ const selectedImage = ref<string>("")
const creating = ref(false) const creating = ref(false)
const serviceInfo = ref<any>({}) const serviceInfo = ref<any>({})
const userInfo = ref<any>({}) const userInfo = ref<any>({})
const HostId=ref(0) const HostId = ref(0)
const computingPowerPoint=ref(0) const computingPowerPoint = ref(0)
const couponTotal=ref(0) const couponTotal = ref(0)
onMounted(() => { const spinning = ref(false)
onMounted(async () => {
spinning.value = true
const userInfoStr = localStorage.getItem("userInfo") const userInfoStr = localStorage.getItem("userInfo")
if (userInfoStr) { if (userInfoStr) {
userInfo.value = JSON.parse(userInfoStr) userInfo.value = JSON.parse(userInfoStr)
} }
serviceInfo.value = useRoute().query serviceInfo.value = useRoute().query
HostId.value=serviceInfo.value.id HostId.value = serviceInfo.value.id
console.log('1111',serviceInfo.value) instanceName.value = serviceInfo.value.name
gpuCountOptions.value = Array.from({ length: Number(serviceInfo.value.gpuAvailableNum) }, (_, i) => i + 1) gpuCountOptions.value = Array.from({ length: Number(serviceInfo.value.gpuAvailableNum) }, (_, i) => i + 1)
gpuCount.value = gpuCountOptions.value[0] gpuCount.value = gpuCountOptions.value[0]
getServiceImages() await getServiceImages()
getCoupon() await getCoupon()
fetchTotalCost() await fetchTotalCost()
await getVoucherList()
spinning.value = false
}) })
async function getVoucherList() {
const res = await getRecommendList()
voucherList.value = res.data
}
async function getCoupon() { async function getCoupon() {
const res:any=await getMyCouponTotal() const res: any = await getMyCouponTotal()
couponTotal.value = res; couponTotal.value = res;
} }
// //
@ -185,7 +207,7 @@ async function fetchTotalCost() {
host_id: HostId.value host_id: HostId.value
}) })
totalMoney.value = res.amount totalMoney.value = res.amount
computingPowerPoint.value=res.computingPowerPoint computingPowerPoint.value = res.computingPowerPoint
} }
// //
@ -222,23 +244,23 @@ const handleChange = (e: any) => {
console.log(e) console.log(e)
} }
const isShow=computed(()=>{ const isShow = computed(() => {
return userInfo.value.balance<computingPowerPoint.value return userInfo.value.balance < computingPowerPoint.value
}) })
const handleCreate = async () => { const handleCreate = async () => {
creating.value = true creating.value = true
try { try {
const params={ const params = {
host_id:HostId.value, host_id: HostId.value,
billingMethod:billingType.value, billingMethod: billingType.value,
gpuNum:gpuCount.value, gpuNum: gpuCount.value,
case_name:instanceName.value, case_name: instanceName.value,
image_id:selectedImage.value, image_id: selectedImage.value,
is_use_voucher:checked.value voucher_id: voucherId.value
} }
const res=await createHost(params) const res = await createHost(params)
message.success('实例创建成功!') message.success('实例创建成功!')
creating.value=false creating.value = false
} catch (error) { } catch (error) {
message.error('创建失败,请重试') message.error('创建失败,请重试')
} finally { } finally {

View File

@ -1,118 +1,121 @@
<template> <template>
<div class="instance-create-container"> <div class="instance-create-container">
<div class="header"> <a-spin :spinning="spinning">
<h1>算力中心</h1> <div class="header">
<h2>聚焦高效算力服务为数字产业智能应用提供强支撑</h2> <h1>算力中心</h1>
</div> <h2>聚焦高效算力服务为数字产业智能应用提供强支撑</h2>
<div class="instance-create-body"> </div>
<!-- 筛选区域保持不变 --> <div class="instance-create-body">
<a-card class="card select-server"> <!-- 筛选区域保持不变 -->
<div class="list-filter"> <a-card class="card select-server">
<div class="filter-item"> <span class="filter-label">计费方式:</span> <div class="list-filter">
<div class="filter-content"> <a-radio-group v-model:value="billingType" button-style="solid" <div class="filter-item"> <span class="filter-label">计费方式:</span>
@change="getHostList"> <a-radio-button v-for="type in billingTypeOptions" :key="type.value" <div class="filter-content"> <a-radio-group v-model:value="billingType" button-style="solid"
:value="type.value"> {{ type.label }} </a-radio-button> </a-radio-group> </div> @change="getHostList"> <a-radio-button v-for="type in billingTypeOptions" :key="type.value"
</div> <!-- 选择地区 --> :value="type.value"> {{ type.label }} </a-radio-button> </a-radio-group> </div>
<div class="filter-item"> <span class="filter-label">选择地区:</span> </div> <!-- 选择地区 -->
<div class="filter-content"> <a-radio-group v-model:value="selectedRegion" button-style="solid" <div class="filter-item"> <span class="filter-label">选择地区:</span>
@change="getHostList"> <div class="filter-content"> <a-radio-group v-model:value="selectedRegion" button-style="solid"
<a-radio-button v-for="region in areaList" :key="region.center_id" :value="region.center_id"> {{ @change="getHostList">
region.center_name }} <a-radio-button v-for="region in areaList" :key="region.center_id" :value="region.center_id"> {{
</a-radio-button> </a-radio-group> region.center_name }}
</a-radio-button> </a-radio-group>
</div>
</div> </div>
</div> <div class="filter-item"> <span class="filter-label">GPU型号:</span>
<div class="filter-item"> <span class="filter-label">GPU型号:</span> <div class="filter-content">
<div class="filter-content"> <a-radio-group v-model:value="selectedGpuModels" button-style="solid" @change="getHostList">
<a-radio-group v-model:value="selectedGpuModels" button-style="solid" @change="getHostList"> <a-radio-button v-for="gpuItem in gpuList" :key="gpuItem.gpuType" :value="gpuItem.gpuType">
<a-radio-button v-for="gpuItem in gpuList" :key="gpuItem.gpuType" :value="gpuItem.gpuType"> {{ gpuItem.gpuType }}
{{ gpuItem.gpuType }} <span v-if="gpuItem.gpuType !== '全部'">{{ "(" + gpuItem.gpuAvailableNum + "/" + gpuItem.gpuNum + ")"
<span v-if="gpuItem.gpuType !== '全部'">{{ "(" + gpuItem.gpuAvailableNum + "/" + gpuItem.gpuNum + ")"
}}</span> }}</span>
</a-radio-button> </a-radio-group> </a-radio-button> </a-radio-group>
</div>
</div> <!-- GPU数量 -->
<div class="filter-item"> <span class="filter-label">GPU数量:</span>
<div class="filter-content"> <a-select :size="'large'" default-value="1" style="width: 200px"
@change="handleGpuCountChange"> <a-select-option v-for="count in gpuCountOptions" :key="count"
:value="count"> {{ count }} </a-select-option> </a-select> </div>
</div> </div>
</div> <!-- GPU数量 -->
<div class="filter-item"> <span class="filter-label">GPU数量:</span>
<div class="filter-content"> <a-select :size="'large'" default-value="1" style="width: 200px"
@change="handleGpuCountChange"> <a-select-option v-for="count in gpuCountOptions" :key="count"
:value="count"> {{ count }} </a-select-option> </a-select> </div>
</div> </div>
</div> </a-card>
</a-card>
<!-- 机器列表 --> <!-- 机器列表 -->
<a-card class="card machine-list"> <a-card class="card machine-list">
<template v-if="allMachineList.length > 0"> <template v-if="allMachineList.length > 0">
<div class="image-card-container" style="min-height:300px;"> <a-spin :spinning="tableLoading">
<div class="image-card-container" style="min-height:300px;">
<div v-for="record in allMachineList" :key="record.id" class="image-card"
:class="{ selected: selectedMachineId === record.id }" @click="handleSelectMachine(record.id)">
<!-- 顶部 -->
<div class="image-card-header">
<div class="gpu-title">
{{ record.name }}
</div>
<div class="gpu-availability">
空闲 / 总量
<strong style="font-size: 18px;">{{ record.gpuAvailableNum }}</strong> / {{ record.gpuNum }}
</div>
</div>
<div v-for="record in allMachineList" :key="record.id" class="image-card" <!-- 中部信息 -->
:class="{ selected: selectedMachineId === record.id }" @click="handleSelectMachine(record.id)"> <div class="image-card-info">
<!-- 顶部 --> <div class="info-item">
<div class="image-card-header"> <span class="info-item-name">CPU</span>
<div class="gpu-title"> <span class="info-item-account">{{ record.cpuNum }} </span>
{{ record.name }} </div>
</div> <div class="info-item">
<div class="gpu-availability"> <span class="info-item-name">内存</span>
空闲 / 总量 <span class="info-item-account">{{ record.memory }} GB</span>
<strong style="font-size: 18px;">{{ record.gpuAvailableNum }}</strong> / {{ record.gpuNum }} </div>
<div class="info-item">
<span class="info-item-name">显存</span>
<span class="info-item-account">{{ record.vram }}</span>
</div>
<div class="info-item">
<span class="info-item-name">系统盘</span>
<span class="info-item-account">30 GB</span>
</div>
</div>
<!-- 底部价格 -->
<div class="image-card-footer">
<div class="price">
<span class="price-num">{{ record.price }}</span>
<span class="price-unit">//小时</span>
</div>
<a-button type="primary" class="buy-btn" :disabled="record.freeGpu === 0"
@click.stop="handleRentMachine(record)">
立即创建
</a-button>
</div>
</div> </div>
</div> </div>
</a-spin>
<!-- 中部信息 --> </template>
<div class="image-card-info"> <template v-else>
<div class="info-item"> <a-empty />
<span class="info-item-name">CPU</span> </template>
<span class="info-item-account">{{ record.cpuNum }} </span> <!-- 分页 -->
</div> <div class="pagination-container">
<div class="info-item"> <a-pagination v-model:current="currentPage" v-model:pageSize="pageSize" :total="hostTotal" show-size-changer
<span class="info-item-name">内存</span> show-quick-jumper />
<span class="info-item-account">{{ record.memory }} GB</span>
</div>
<div class="info-item">
<span class="info-item-name">显存</span>
<span class="info-item-account">{{ record.vram }}</span>
</div>
<div class="info-item">
<span class="info-item-name">系统盘</span>
<span class="info-item-account">30 GB</span>
</div>
</div>
<!-- 底部价格 -->
<div class="image-card-footer">
<div class="price">
<span class="price-num">{{ record.price }}</span>
<span class="price-unit">//小时</span>
</div>
<a-button type="primary" class="buy-btn" :disabled="record.freeGpu === 0"
@click.stop="handleRentMachine(record)">
立即购买
</a-button>
</div>
</div>
</div> </div>
</template> </a-card>
<template v-else> </div>
<a-empty /> </a-spin>
</template>
<!-- 分页 -->
<div class="pagination-container">
<a-pagination v-model:current="currentPage" v-model:pageSize="pageSize" :total="hostTotal"
show-size-changer show-quick-jumper />
</div>
</a-card>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, reactive, watch } from 'vue' import { ref, computed, reactive, watch, onMounted } from 'vue'
import { hostList, getAreaListApi, getGpuListApi } from '@/apis/market' import { hostList, getAreaListApi, getGpuListApi } from '@/apis/market'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { TableColumnsType } from 'ant-design-vue' import type { TableColumnsType } from 'ant-design-vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const spinning = ref(false)
const router = useRouter(); const router = useRouter();
// //
const hostTotal = ref(0) const hostTotal = ref(0)
@ -127,9 +130,14 @@ const tableLoading = ref(false)
// //
const currentPage = ref(1) const currentPage = ref(1)
const pageSize = ref(10) const pageSize = ref(10)
getHostList() onMounted(async () => {
getAreaList() spinning.value = true
getGpuList() await getHostList()
await getAreaList()
await getGpuList()
spinning.value = false
})
async function getHostList() { async function getHostList() {
tableLoading.value = true tableLoading.value = true
try { try {
@ -187,13 +195,13 @@ const handleSelectMachine = (id: string) => {
} }
// //
const handleRentMachine = (record: any) => { const handleRentMachine = (record: any) => {
router.push({path:"/layout/create",query:record}) router.push({ path: "/layout/create", query: record })
} }
const handleGpuCountChange = (e:any) => { const handleGpuCountChange = (e: any) => {
// console.log(e) // console.log(e)
gpuCount.value=e gpuCount.value = e
getHostList() getHostList()
selectedMachineId.value = '' selectedMachineId.value = ''
currentPage.value = 1 currentPage.value = 1