算力平台实例管理
This commit is contained in:
parent
2d0d6eb59b
commit
f6ece66b1a
@ -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 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
15
src/utils/dict.ts
Normal 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" }],
|
||||
]);
|
||||
@ -6,8 +6,8 @@
|
||||
<div class="header-left">
|
||||
<span class="page-title">容器实例</span>
|
||||
<span class="warning-tip">
|
||||
<exclamation-circle-outlined class="warning-icon" />
|
||||
实例连续关机15天会释放实例,实例释放会导致数据清空且不可恢复,释放前实例数据仍在。
|
||||
<!-- <exclamation-circle-outlined class="warning-icon" /> -->
|
||||
<!-- 实例连续关机15天会释放实例,实例释放会导致数据清空且不可恢复,释放前实例数据仍在。 -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -23,10 +23,8 @@
|
||||
<div class="header-filter">
|
||||
<a-select v-model:value="filterStatus" placeholder="筛选状态" style="width: 160px;" size="large"
|
||||
@change="handleSearch">
|
||||
<a-select-option value="all">全部状态</a-select-option>
|
||||
<a-select-option value="running">运行中</a-select-option>
|
||||
<a-select-option value="stopped">已停止</a-select-option>
|
||||
<a-select-option value="error">异常</a-select-option>
|
||||
<a-select-option value="">全部状态</a-select-option>
|
||||
<a-select-option v-for="[key, config] in instanceStatus" :value="key">{{ config.text }}</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search v-model:value="searchKeyword" placeholder="搜索实例名称/ID" style="width: 240px; margin-left: 12px;"
|
||||
size="large" @search="handleSearch" />
|
||||
@ -41,9 +39,8 @@
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<span :class="`status-${record.status}`">
|
||||
{{ getStatusText(record.status) }}
|
||||
</span>
|
||||
<span :style="{ color: instanceStatus.get(record.status)?.color }">{{ instanceStatus.get(record.status)?.text
|
||||
}}</span>
|
||||
</template>
|
||||
|
||||
<!-- 规格详情列 -->
|
||||
@ -64,7 +61,11 @@
|
||||
{{ getHealthStatusText(record.health_status) }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'price_type'">
|
||||
<span>
|
||||
{{ payTypeDic.get(record.price_type) }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 操作列 -->
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<div class="action-buttons">
|
||||
@ -147,14 +148,10 @@
|
||||
<p style="color: #666;">• 实例配置和网络设置保持不变</p>
|
||||
</div>
|
||||
</div>
|
||||
<template #okText>
|
||||
<span style="color: #ff4d4f;">确认重置</span>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<!-- 释放确认模态框 -->
|
||||
<a-modal v-model:open="releaseModalVisible" title="确认释放" @ok="confirmRelease" @cancel="releaseModalVisible = false"
|
||||
:ok-button-props="{ danger: true }">
|
||||
<a-modal v-model:open="releaseModalVisible" title="确认释放" @ok="confirmRelease" @cancel="releaseModalVisible = false" >
|
||||
<div class="modal-content">
|
||||
<exclamation-circle-outlined class="warning-icon"
|
||||
style="color: #ff4d4f; font-size: 24px; margin-right: 12px;" />
|
||||
@ -177,11 +174,13 @@ import {ExclamationCircleOutlined,ReloadOutlined,EyeOutlined,DownOutlined,RedoOu
|
||||
import type { TableColumnType } from 'ant-design-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { hostCaseList } from '@/apis/admin'
|
||||
import { releaseCase, reStartCase } from '@/apis/home'
|
||||
import { get } from 'http'
|
||||
import { payTypeDic, instanceStatus } from '@/utils/dict'
|
||||
const router = useRouter()
|
||||
|
||||
// 状态管理
|
||||
const filterStatus = ref('all')
|
||||
const filterStatus = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const loading = ref(false)
|
||||
const specModalVisible = ref(false)
|
||||
@ -217,7 +216,6 @@ const columns = ref<TableColumnType[]>([
|
||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 180 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ 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: 'price_type', key: 'price_type', width: 120 },
|
||||
{ title: '释放时间', dataIndex: 'release_at', key: 'release_at', width: 120 },
|
||||
@ -232,7 +230,9 @@ const getDataList = async () => {
|
||||
loading.value = true
|
||||
const params = {
|
||||
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)
|
||||
mockData.value = res.data
|
||||
@ -247,23 +247,11 @@ const getDataList = async () => {
|
||||
getDataList()
|
||||
|
||||
|
||||
// 状态文本映射
|
||||
const getStatusText = (status: string) => {
|
||||
const map: Record<string, any> = {
|
||||
"RUNNING": '运行中',
|
||||
"STOPPED": '已停止',
|
||||
"CREATING": '创建中',
|
||||
"RESTARTING": '重启中',
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
// 健康状态文本映射
|
||||
const getHealthStatusText = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
healthy: '健康',
|
||||
warning: '警告',
|
||||
error: '异常'
|
||||
Normal: '正常',
|
||||
Abnormal: '异常'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
@ -325,32 +313,32 @@ const handleMenuClick = (key: string, record: any) => {
|
||||
}
|
||||
|
||||
// 确认重置
|
||||
const confirmReset = () => {
|
||||
const confirmReset = async () => {
|
||||
if (!currentInstance.value) return
|
||||
|
||||
message.loading({ content: '正在重置实例...', key: 'reset', duration: 0 })
|
||||
setTimeout(() => {
|
||||
// 模拟重置操作
|
||||
currentInstance.value.health_status = 'healthy'
|
||||
try {
|
||||
const res = await reStartCase(currentInstance.value.id)
|
||||
message.success({ content: '实例重置成功', key: 'reset' })
|
||||
resetModalVisible.value = false
|
||||
}, 2000)
|
||||
getDataList()
|
||||
} catch (error: any) {
|
||||
message.error(error)
|
||||
resetModalVisible.value = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 确认释放
|
||||
const confirmRelease = () => {
|
||||
const confirmRelease = async () => {
|
||||
if (!currentInstance.value) return
|
||||
|
||||
message.loading({ content: '正在释放实例...', key: 'release', duration: 0 })
|
||||
setTimeout(() => {
|
||||
// 从数据中移除
|
||||
const index = mockData.value.findIndex(item => item.id === currentInstance.value.id)
|
||||
if (index !== -1) {
|
||||
mockData.value.splice(index, 1)
|
||||
}
|
||||
try {
|
||||
const res = await releaseCase(currentInstance.value.id)
|
||||
message.success({ content: '实例释放成功', key: 'release' })
|
||||
releaseModalVisible.value = false
|
||||
}, 2000)
|
||||
getDataList()
|
||||
} catch (error: any) {
|
||||
message.error(error)
|
||||
releaseModalVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 批量续费
|
||||
@ -366,6 +354,7 @@ const refreshData = () => {
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
paginationState.value.current = 1
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
@ -379,7 +368,7 @@ const onTableChange = (pag: any) => {
|
||||
|
||||
// 新建实例
|
||||
const handleRent = () => {
|
||||
router.push('/layout/admin/instanceCreate')
|
||||
router.push('/layout/market')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@ -572,7 +561,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
// 健康状态样式
|
||||
.health-healthy {
|
||||
.health-Normal {
|
||||
color: #52c41a;
|
||||
background: #f6ffed;
|
||||
padding: 4px 8px;
|
||||
@ -581,16 +570,16 @@ onMounted(() => {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.health-warning {
|
||||
color: #faad14;
|
||||
background: #fffbe6;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
}
|
||||
// .health-Abnormal {
|
||||
// color: #faad14;
|
||||
// background: #fffbe6;
|
||||
// padding: 4px 8px;
|
||||
// border-radius: 4px;
|
||||
// font-size: 12px;
|
||||
// display: inline-block;
|
||||
// }
|
||||
|
||||
.health-error {
|
||||
.health-Abnormal {
|
||||
color: #ff4d4f;
|
||||
background: #fff2f0;
|
||||
padding: 4px 8px;
|
||||
|
||||
@ -15,12 +15,13 @@
|
||||
<a-card class="card" title="计费方式">
|
||||
<div class="billing-content">
|
||||
<a-radio-group v-model:value="billingType" button-style="solid" @change="handleBillingTypeChange">
|
||||
<a-radio-button value="payg">按量计费</a-radio-button>
|
||||
<a-radio-button value="daily">包日</a-radio-button>
|
||||
<a-radio-button value="weekly">包周</a-radio-button>
|
||||
<a-radio-button value="monthly">包月</a-radio-button>
|
||||
<a-radio-button value="PayOnTime">按量计费</a-radio-button>
|
||||
<a-radio-button value="PayOnDay">包日</a-radio-button>
|
||||
<a-radio-button value="PayOnWeek">包周</a-radio-button>
|
||||
<a-radio-button value="PayOnMonth">包月</a-radio-button>
|
||||
<a-radio-button value="PayOnYear">包年</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a href="/docs/price/" target="_blank" class="billing-link">计费规则</a>
|
||||
<!-- <a href="/docs/price/" target="_blank" class="billing-link">计费规则</a> -->
|
||||
</div>
|
||||
<div class="note">
|
||||
创建完主机后仍然可以转换计费方式。如选择按量计费,价格发生变动以实例开机时的价格为准
|
||||
@ -320,7 +321,7 @@ interface Coupon {
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const billingType = ref('weekly')
|
||||
const billingType = ref('PayOnTime')
|
||||
const selectedRegion = ref('beijingDC2')
|
||||
const selectedZone = ref('')
|
||||
const selectedGpuModels = ref<string[]>(['RTX 5090'])
|
||||
@ -1193,7 +1194,8 @@ const showPriceBreakdown = computed(() => {
|
||||
}
|
||||
|
||||
.price-breakdown {
|
||||
display: none; /* 移动端隐藏价格明细 */
|
||||
display: none;
|
||||
/* 移动端隐藏价格明细 */
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-spin :spinning="spinning">
|
||||
<div class="instance-create-container">
|
||||
<!-- 服务器选择 -->
|
||||
<a-card class="card select-server" title="服务器选择">
|
||||
@ -25,7 +26,8 @@
|
||||
|
||||
<!-- GPU数量 -->
|
||||
<div class="filter-item">
|
||||
<span class="filter-label">GPU数量<span style="color: #ff4d4f;margin: 0 2px">*</span><span>:</span></span>
|
||||
<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">
|
||||
@ -77,7 +79,8 @@
|
||||
<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 label="驱动/CUDA">{{ serviceInfo.gpuDriver + '/' +
|
||||
serviceInfo.cudaVersion
|
||||
}}</a-descriptions-item>
|
||||
<a-descriptions-item label="消耗算力点(单卡)">{{ serviceInfo.price }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
@ -85,17 +88,27 @@
|
||||
</a-card>
|
||||
<!-- 优惠券 -->
|
||||
<a-card class="card coupon-selection">
|
||||
<a-checkbox v-model:checked="checked">是否使用算力券<span style="color:#f34646;">{{ ' ( 可用点数:'+couponTotal+' 点 )' }}</span></a-checkbox>
|
||||
<div style="display: flex;align-items: center;">
|
||||
<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="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 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> -->
|
||||
@ -133,7 +146,7 @@ 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 { getImages, getRecommendList } from "@/apis/home"
|
||||
import { getMyCouponTotal, getTotalCost, getHostInfo, createHost } from "@/apis/home"
|
||||
const router = useRouter()
|
||||
interface GpuModelOption {
|
||||
@ -143,10 +156,11 @@ interface GpuModelOption {
|
||||
total: number
|
||||
regions: string[]
|
||||
}
|
||||
|
||||
const voucherList = ref<any[]>([])
|
||||
const instanceName = ref('')
|
||||
const checked = ref(false)
|
||||
const totalMoney = ref(0)
|
||||
const voucherId = ref('')
|
||||
// 响应式数据
|
||||
const billingType = ref('PayOnTime')
|
||||
const gpuCount = ref(5)
|
||||
@ -159,20 +173,28 @@ const userInfo = ref<any>({})
|
||||
const HostId = ref(0)
|
||||
const computingPowerPoint = ref(0)
|
||||
const couponTotal = ref(0)
|
||||
onMounted(() => {
|
||||
const spinning = ref(false)
|
||||
onMounted(async () => {
|
||||
spinning.value = true
|
||||
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)
|
||||
instanceName.value = serviceInfo.value.name
|
||||
gpuCountOptions.value = Array.from({ length: Number(serviceInfo.value.gpuAvailableNum) }, (_, i) => i + 1)
|
||||
gpuCount.value = gpuCountOptions.value[0]
|
||||
getServiceImages()
|
||||
getCoupon()
|
||||
fetchTotalCost()
|
||||
await getServiceImages()
|
||||
await getCoupon()
|
||||
await fetchTotalCost()
|
||||
await getVoucherList()
|
||||
spinning.value = false
|
||||
})
|
||||
async function getVoucherList() {
|
||||
const res = await getRecommendList()
|
||||
voucherList.value = res.data
|
||||
}
|
||||
async function getCoupon() {
|
||||
const res: any = await getMyCouponTotal()
|
||||
couponTotal.value = res;
|
||||
@ -234,7 +256,7 @@ const handleCreate = async () => {
|
||||
gpuNum: gpuCount.value,
|
||||
case_name: instanceName.value,
|
||||
image_id: selectedImage.value,
|
||||
is_use_voucher:checked.value
|
||||
voucher_id: voucherId.value
|
||||
}
|
||||
const res = await createHost(params)
|
||||
message.success('实例创建成功!')
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="instance-create-container">
|
||||
<a-spin :spinning="spinning">
|
||||
<div class="header">
|
||||
<h1>算力中心</h1>
|
||||
<h2>聚焦高效算力服务,为数字产业、智能应用提供强支撑</h2>
|
||||
@ -42,8 +43,8 @@
|
||||
<!-- 机器列表 -->
|
||||
<a-card class="card machine-list">
|
||||
<template v-if="allMachineList.length > 0">
|
||||
<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)">
|
||||
<!-- 顶部 -->
|
||||
@ -85,34 +86,36 @@
|
||||
</div>
|
||||
<a-button type="primary" class="buy-btn" :disabled="record.freeGpu === 0"
|
||||
@click.stop="handleRentMachine(record)">
|
||||
立即购买
|
||||
立即创建
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</a-spin>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-empty />
|
||||
</template>
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<a-pagination v-model:current="currentPage" v-model:pageSize="pageSize" :total="hostTotal"
|
||||
show-size-changer show-quick-jumper />
|
||||
<a-pagination v-model:current="currentPage" v-model:pageSize="pageSize" :total="hostTotal" show-size-changer
|
||||
show-quick-jumper />
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<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 { message } from 'ant-design-vue'
|
||||
import type { TableColumnsType } from 'ant-design-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const spinning = ref(false)
|
||||
const router = useRouter();
|
||||
// 响应式数据
|
||||
const hostTotal = ref(0)
|
||||
@ -127,9 +130,14 @@ const tableLoading = ref(false)
|
||||
// 分页相关
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
getHostList()
|
||||
getAreaList()
|
||||
getGpuList()
|
||||
onMounted(async () => {
|
||||
spinning.value = true
|
||||
await getHostList()
|
||||
await getAreaList()
|
||||
await getGpuList()
|
||||
spinning.value = false
|
||||
})
|
||||
|
||||
async function getHostList() {
|
||||
tableLoading.value = true
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user