Compare commits
2 Commits
4d014be9c2
...
b8c5436584
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8c5436584 | ||
|
|
ee87fe40c8 |
2
.env.dev
2
.env.dev
@ -2,5 +2,5 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# api
|
# api
|
||||||
VITE_API_BASIC="http://10.10.1.29:8888"
|
VITE_API_BASIC="http://10.10.1.20:8888"
|
||||||
|
|
||||||
|
|||||||
@ -8,4 +8,6 @@ export const orderList = (params:any) => request.get('/v1/order/pay_list',{param
|
|||||||
export const useList = (params:any) => request.get('/v1/order/use_list',{params})
|
export const useList = (params:any) => request.get('/v1/order/use_list',{params})
|
||||||
|
|
||||||
// 容器实例列表
|
// 容器实例列表
|
||||||
export const hostCaseList = (params:any) => request.get('/v1/hostCase/list',{params})
|
export const hostCaseList = (params:any) => request.get('/v1/host_case/case_list',{params})
|
||||||
|
// 镜像列表
|
||||||
|
export const getImageList = (params:any) => request.get('/v1/image/image_list',{params})
|
||||||
@ -2,11 +2,14 @@
|
|||||||
import request from '@/utils/index'
|
import request from '@/utils/index'
|
||||||
|
|
||||||
// 轮播接口
|
// 轮播接口
|
||||||
export const getBannerList = () => request.get('/v1/home/banner_list')
|
export const getBannerList = () => request.get('/v1/home/home_banners')
|
||||||
// GPU说明列表
|
// GPU说明列表
|
||||||
export const getGpuList = () => request.get('/v1/home/gpu_list')
|
export const getGpuList = () => request.get('/v1/home/home_products')
|
||||||
// 获取产品优势
|
// 获取产品优势
|
||||||
export const getAdvantage = () => request.get('/v1/home/p_advantages_list')
|
export const getAdvantage = () => request.get('/v1/home/home_product_advs')
|
||||||
|
|
||||||
// 获取热门产品信息
|
// 获取热门产品信息
|
||||||
export const getHotProduct = () => request.get('/v1/product/host_info')
|
export const getHotProduct = () => request.get('/v1/product/host_info')
|
||||||
|
|
||||||
|
// 获取首页One列表
|
||||||
|
export const getApiOneList = () => request.get('/v1/home/home_top_labels')
|
||||||
12
src/apis/market.ts
Normal file
12
src/apis/market.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
import request from '@/utils/index'
|
||||||
|
|
||||||
|
// 算力列表
|
||||||
|
export const hostList = (params:any) => request.get('/v1/host/list',{params})
|
||||||
|
|
||||||
|
// 地区列表
|
||||||
|
export const getAreaListApi = () => request.get('/v1/host/center_coll')
|
||||||
|
|
||||||
|
// GPU型号列表
|
||||||
|
export const hostCaseList = (params:any) => request.get('/v1/hostCase/list',{params})
|
||||||
|
export const getGpuListApi = () => request.get('/v1/host/gpu_type_coll')
|
||||||
@ -375,25 +375,25 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
// ====== 添加全局前置守卫 ======
|
// ====== 添加全局前置守卫 ======
|
||||||
// router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
// console.log("Navigating to:", to.path);
|
console.log("Navigating to:", to.path);
|
||||||
// const list = ["/layout/home","/document/introdution","/layout/admin/home"];
|
const list = ["/layout/home","/document/introdution"];
|
||||||
// if (list.indexOf(to.path) != -1) {
|
if (list.indexOf(to.path) != -1) {
|
||||||
// next();
|
next();
|
||||||
// return;
|
return;
|
||||||
// } else {
|
} else {
|
||||||
// const token = localStorage.getItem("token"); // 或从 pinia/vuex 获取
|
const token = localStorage.getItem("token"); // 或从 pinia/vuex 获取
|
||||||
// const isLoginPage = to.path === "/login";
|
const isLoginPage = to.path === "/login";
|
||||||
// if (!token && !isLoginPage) {
|
if (!token && !isLoginPage) {
|
||||||
// // 没有 token 且不是去登录页 → 跳转登录
|
// 没有 token 且不是去登录页 → 跳转登录
|
||||||
// next({ path: "/login" });
|
next({ path: "/login" });
|
||||||
// } else if (token && isLoginPage) {
|
} else if (token && isLoginPage) {
|
||||||
// // 已登录却访问登录页 → 跳转首页(可选)
|
// 已登录却访问登录页 → 跳转首页(可选)
|
||||||
// next({ path: "/layout/home" });
|
next({ path: "/layout/home" });
|
||||||
// } else {
|
} else {
|
||||||
// // 正常访问
|
// 正常访问
|
||||||
// next();
|
next();
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// });
|
});
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@ -9,7 +9,7 @@ console.log('All env:', import.meta.env);
|
|||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: BASE_URL,
|
||||||
timeout: 10000, // 10 秒超时
|
timeout: 10000, // 10 秒超时
|
||||||
withCredentials: true, // 跨域请求时发送 cookies
|
withCredentials: false, // 跨域请求时发送 cookies
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
@ -36,6 +36,7 @@ request.interceptors.response.use(
|
|||||||
|
|
||||||
// 假设后端返回格式为 { code: 200, data: ..., message: '' }
|
// 假设后端返回格式为 { code: 200, data: ..., message: '' }
|
||||||
const { code, data, message } = response.data;
|
const { code, data, message } = response.data;
|
||||||
|
console.log('Response Data:', response.data);
|
||||||
if (code === 1) {
|
if (code === 1) {
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -105,15 +105,17 @@
|
|||||||
<img src="../../../assets/nav.png" />
|
<img src="../../../assets/nav.png" />
|
||||||
</span>
|
</span>
|
||||||
<div class="nav-account">
|
<div class="nav-account">
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
15100000000
|
{{ userInfo.userName }}
|
||||||
<i class="fee-value">
|
<i class="fee-value">
|
||||||
<CopyOutlined />
|
<CopyOutlined />
|
||||||
</i>
|
</i>
|
||||||
</p>
|
</p>
|
||||||
<p>立即认证</p>
|
<a-tag :color="getCertifyName(userInfo.certificationStatus).color">{{ getCertifyName(userInfo.certificationStatus).name }}</a-tag>
|
||||||
|
<!-- <p>{{getCertifyName(userInfo.certificationStatus).name}}</p> -->
|
||||||
</div>
|
</div>
|
||||||
<span class="btn-item">
|
<span class="btn-item" style="cursor: pointer;" @click="router.push('/layout/admin/accountSet')">
|
||||||
账户设置
|
账户设置
|
||||||
<ArrowRightOutlined />
|
<ArrowRightOutlined />
|
||||||
</span>
|
</span>
|
||||||
@ -128,8 +130,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="asset-content">
|
<div class="asset-content">
|
||||||
<div class="asset-name">我的余额</div>
|
<div class="asset-name">我的余额</div>
|
||||||
<div class="asset-value" v-if="balance >= 0">
|
<div class="asset-value" v-if="userInfo.balance >= 0">
|
||||||
<span class="amount">{{ formatAmount(balance) }}</span>
|
<span class="amount">{{ formatAmount(userInfo.balance) }}</span>
|
||||||
<a-button type="link" class="recharge-btn" @click="goToRecharge">去充值</a-button>
|
<a-button type="link" class="recharge-btn" @click="goToRecharge">去充值</a-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="asset-value" v-else>
|
<div class="asset-value" v-else>
|
||||||
@ -153,7 +155,7 @@
|
|||||||
<span class="rights-text">算力点</span>
|
<span class="rights-text">算力点</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rights-value">
|
<div class="rights-value">
|
||||||
<span class="rights-amount">{{ computingPoints }}</span>
|
<span class="rights-amount">{{ userInfo.computingPowerPoint}}</span>
|
||||||
<span class="rights-unit">点</span>
|
<span class="rights-unit">点</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -167,7 +169,7 @@
|
|||||||
<span class="rights-text">可用算力券</span>
|
<span class="rights-text">可用算力券</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="rights-value">
|
<div class="rights-value">
|
||||||
<span class="rights-amount">{{ availableCoupons }}</span>
|
<span class="rights-amount">{{ userInfo.voucherNum }}</span>
|
||||||
<span class="rights-unit">张</span>
|
<span class="rights-unit">张</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -208,16 +210,16 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
QuestionCircleOutlined // 新增图标
|
QuestionCircleOutlined // 新增图标
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import router from '@/router'
|
import { useRouter } from 'vue-router'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
const router = useRouter()
|
||||||
// 实例卡片的开关状态
|
// 实例卡片的开关状态
|
||||||
const switch1 = ref(true)
|
const switch1 = ref(true)
|
||||||
const switch2 = ref(false)
|
const switch2 = ref(false)
|
||||||
const userInfo = ref<any>({})
|
const userInfo = ref<any>({})
|
||||||
|
|
||||||
// 资产数据
|
// 资产数据
|
||||||
const balance = ref<number>(128.50) // 余额
|
const balance = ref<number>(0) // 余额
|
||||||
const computingPoints = ref<number>(5000) // 算力点
|
const computingPoints = ref<number>(5000) // 算力点
|
||||||
const availableCoupons = ref<number>(3) // 可用算力券数量
|
const availableCoupons = ref<number>(3) // 可用算力券数量
|
||||||
|
|
||||||
@ -231,7 +233,16 @@ onBeforeMount(() => {
|
|||||||
const formatAmount = (amount: number): string => {
|
const formatAmount = (amount: number): string => {
|
||||||
return amount.toFixed(2)
|
return amount.toFixed(2)
|
||||||
}
|
}
|
||||||
|
const certificationStatus = new Map<string, { name: string; color: string }>([
|
||||||
|
['PENDING_CERTIFICATION', { name: '待认证', color: '#faad14' }],
|
||||||
|
['CERTIFICATION_DFFILED', { name: '已提交', color: '#d9d9d9' }], // 注意拼写:DFFILED → FAILED?
|
||||||
|
['CERTIFICATION_PASSED', { name: '认证通过', color: '#52c41a' }],
|
||||||
|
['CERTIFICATION_FAILED', { name: '认证失败', color: '#ff4d4f' }],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getCertifyName = (status: string): { name: string; color: string } => {
|
||||||
|
return certificationStatus.get(status) ?? { name: '未认证', color: 'gray' };
|
||||||
|
};
|
||||||
// 格式化算力点显示(添加千分位)
|
// 格式化算力点显示(添加千分位)
|
||||||
const formatComputingPoints = (points: number): string => {
|
const formatComputingPoints = (points: number): string => {
|
||||||
return points.toLocaleString()
|
return points.toLocaleString()
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="my-images-page">
|
<div class="my-images-page">
|
||||||
<!-- 顶部警告提示 -->
|
<!-- 顶部警告提示 -->
|
||||||
@ -10,7 +9,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<div class="storage-info">
|
<div class="storage-info">
|
||||||
<div class="storage-desc">
|
<div class="storage-desc">
|
||||||
存储容量大小:<strong>{{ storageUsed }}</strong>
|
存储容量大小:<strong>{{ storageUsed }}</strong>
|
||||||
@ -30,9 +29,8 @@
|
|||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<a-table :data-source="getCurrentPageData" :columns="columns" :pagination="false"
|
<a-table :data-source="imageList" :columns="columns" :pagination="false" :scroll="{ x: 'max-content', y: 400 }"
|
||||||
:scroll="{ x: 'max-content', y: 400 }" :row-key="(record) => record.uid" :loading="loading" bordered
|
:row-key="(record) => record.uid" :loading="loading" bordered class="image-table">
|
||||||
class="image-table">
|
|
||||||
<template #action="{ record }">
|
<template #action="{ record }">
|
||||||
<a-button type="link" size="small" @click="handleView(record)">查看</a-button>
|
<a-button type="link" size="small" @click="handleView(record)">查看</a-button>
|
||||||
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
<a-button type="link" size="small" danger @click="handleDelete(record)">删除</a-button>
|
||||||
@ -42,6 +40,10 @@
|
|||||||
<a-tag v-if="record.status == 'enabled'" color="green">可用</a-tag>
|
<a-tag v-if="record.status == 'enabled'" color="green">可用</a-tag>
|
||||||
<a-tag v-if="record.status == 'disabled'" color="red">不可用</a-tag>
|
<a-tag v-if="record.status == 'disabled'" color="red">不可用</a-tag>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'imageIsPublic'">
|
||||||
|
<a-tag v-if="record.imageIsPublic" color="green">公开</a-tag>
|
||||||
|
<a-tag v-else color="red">不公开</a-tag>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="table-empty">暂无数据</div>
|
<div class="table-empty">暂无数据</div>
|
||||||
@ -62,13 +64,11 @@
|
|||||||
import editDialog from './editDialog.vue';
|
import editDialog from './editDialog.vue';
|
||||||
import { ref, reactive, onMounted, computed, } from 'vue';
|
import { ref, reactive, onMounted, computed, } from 'vue';
|
||||||
import {
|
import {
|
||||||
Table,
|
|
||||||
Pagination,
|
|
||||||
Progress,
|
|
||||||
message,
|
message,
|
||||||
Button
|
|
||||||
} from 'ant-design-vue';
|
} from 'ant-design-vue';
|
||||||
import apis from '@/apis';
|
import apis from '@/apis';
|
||||||
|
import { getImageList } from '@/apis/admin';
|
||||||
|
import { on } from 'events';
|
||||||
// 表格列定义
|
// 表格列定义
|
||||||
interface ImageItem {
|
interface ImageItem {
|
||||||
uid: string;
|
uid: string;
|
||||||
@ -87,10 +87,7 @@ const imageList = ref<ImageItem[]>([]);
|
|||||||
const editDialogRef = ref<any>(null);
|
const editDialogRef = ref<any>(null);
|
||||||
// 存储容量
|
// 存储容量
|
||||||
const storageUsed = ref('12.5GB');
|
const storageUsed = ref('12.5GB');
|
||||||
const storagePeak = ref('13.2GB');
|
|
||||||
const expectedFee = ref('0元');
|
|
||||||
const freeStorage = ref('30.00GB');
|
|
||||||
const paidStorage = ref('0GB');
|
|
||||||
|
|
||||||
// 计算进度百分比
|
// 计算进度百分比
|
||||||
const storagePercent = computed(() => {
|
const storagePercent = computed(() => {
|
||||||
@ -103,25 +100,21 @@ const storagePercent = computed(() => {
|
|||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: imageList.value.length,
|
total: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 加载状态
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
// 定义列
|
// 定义列
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 150 },
|
{ title: '名称', dataIndex: 'name', key: 'name', width: 150 },
|
||||||
{ title: 'cpu类型', dataIndex: 'cpu_type', key: 'cpu_type', width: 150 },
|
{ title: '镜像hash', dataIndex: 'imageHash', key: 'imageHash', width: 100 },
|
||||||
{ title: 'cuda版本', dataIndex: 'cuda_version', key: 'cuda_version', width: 100 },
|
{ title: '镜像是否公开', dataIndex: 'imageIsPublic', key: 'imageIsPublic', width: 130 },
|
||||||
{ title: 'gpu类型', dataIndex: 'gpu_type', key: 'gpu_type', width: 100 },
|
{ title: '镜像路径', dataIndex: 'imagePath', key: 'imagePath', width: 120 },
|
||||||
{ title: '镜像hash', dataIndex: 'image_hash', key: 'image_hash', width: 100 },
|
{ title: '镜像大小', dataIndex: 'imageSize', key: 'imageSize', width: 180 },
|
||||||
{ title: '镜像是否公开', dataIndex: 'image_is_public', key: 'image_is_public', width: 130 },
|
{ title: '镜像类型', dataIndex: 'imageType', key: 'imageType', width: 180 },
|
||||||
{ title: '镜像路径', dataIndex: 'image_path', key: 'image_path', width: 120 },
|
{ title: '镜像版本', dataIndex: 'imageVersion', key: 'imageVersion', width: 150 },
|
||||||
{ title: '镜像大小', dataIndex: 'image_size', key: 'image_size', width: 180 },
|
|
||||||
{ title: '镜像类型', dataIndex: 'image_type', key: 'image_type', width: 180 },
|
|
||||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 80 },
|
||||||
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
|
// { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 160 },
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
@ -160,12 +153,28 @@ const getCurrentPageData = computed(() => {
|
|||||||
const end = start + pagination.pageSize;
|
const end = start + pagination.pageSize;
|
||||||
return imageList.value.slice(start, end);
|
return imageList.value.slice(start, end);
|
||||||
});
|
});
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false);
|
||||||
|
const getDataList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
console.log('获取镜像列表');
|
||||||
|
const res: any = await getImageList({ page_num: pagination.current, page_size: pagination.pageSize });
|
||||||
|
console.log('镜像列表响应:', res);
|
||||||
|
imageList.value = res.data;
|
||||||
|
pagination.total = res.total;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取镜像列表错误:', error);
|
||||||
|
message.error('获取镜像列表失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
apis.imgs.fetchImageList({ pageNum: pagination.current, pageSize: pagination.pageSize }).then((response: any) => {
|
getDataList();
|
||||||
imageList.value = response.list;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
// getDataList()
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@ -23,8 +23,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { h, reactive, computed } from 'vue';
|
import { h, reactive, computed,watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter,useRoute } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
HomeOutlined,
|
HomeOutlined,
|
||||||
ConsoleSqlOutlined,
|
ConsoleSqlOutlined,
|
||||||
@ -39,7 +39,9 @@ import type { MenuMode, MenuTheme } from 'ant-design-vue';
|
|||||||
import type { MenuProps } from 'ant-design-vue';
|
import type { MenuProps } from 'ant-design-vue';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
watch(() => router.currentRoute.value.path, (newPath) => {
|
||||||
|
state.selectedKeys = [newPath];
|
||||||
|
});
|
||||||
// 定义菜单原始数据
|
// 定义菜单原始数据
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@ -21,39 +21,23 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-filter">
|
<div class="header-filter">
|
||||||
<a-select
|
<a-select v-model:value="filterStatus" placeholder="筛选状态" style="width: 160px;" size="large"
|
||||||
v-model:value="filterStatus"
|
@change="handleSearch">
|
||||||
placeholder="筛选状态"
|
|
||||||
style="width: 160px;"
|
|
||||||
size="large"
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<a-select-option value="all">全部状态</a-select-option>
|
<a-select-option value="all">全部状态</a-select-option>
|
||||||
<a-select-option value="running">运行中</a-select-option>
|
<a-select-option value="running">运行中</a-select-option>
|
||||||
<a-select-option value="stopped">已停止</a-select-option>
|
<a-select-option value="stopped">已停止</a-select-option>
|
||||||
<a-select-option value="error">异常</a-select-option>
|
<a-select-option value="error">异常</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-input-search
|
<a-input-search v-model:value="searchKeyword" placeholder="搜索实例名称/ID" style="width: 240px; margin-left: 12px;"
|
||||||
v-model:value="searchKeyword"
|
size="large" @search="handleSearch" />
|
||||||
placeholder="搜索实例名称/ID"
|
|
||||||
style="width: 240px; margin-left: 12px;"
|
|
||||||
size="large"
|
|
||||||
@search="handleSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<a-table
|
<a-table :dataSource="mockData" :columns="columns" bordered :pagination="paginationState" :loading="loading"
|
||||||
:dataSource="tableData"
|
@change="onTableChange">
|
||||||
:columns="columns"
|
|
||||||
bordered
|
|
||||||
:pagination="paginationState"
|
|
||||||
:loading="loading"
|
|
||||||
@change="onTableChange"
|
|
||||||
>
|
|
||||||
<!-- 状态列 -->
|
<!-- 状态列 -->
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'status'">
|
<template v-if="column.key === 'status'">
|
||||||
@ -84,21 +68,11 @@
|
|||||||
<!-- 操作列 -->
|
<!-- 操作列 -->
|
||||||
<template v-else-if="column.key === 'actions'">
|
<template v-else-if="column.key === 'actions'">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<a-button
|
<a-button v-if="record.status === 'running'" type="link" danger size="small"
|
||||||
v-if="record.status === 'running'"
|
@click="handlePowerOff(record)">
|
||||||
type="link"
|
|
||||||
danger
|
|
||||||
size="small"
|
|
||||||
@click="handlePowerOff(record)"
|
|
||||||
>
|
|
||||||
关机
|
关机
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
<a-button v-else-if="record.status === 'stopped'" type="link" size="small" @click="handlePowerOn(record)">
|
||||||
v-else-if="record.status === 'stopped'"
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
@click="handlePowerOn(record)"
|
|
||||||
>
|
|
||||||
开机
|
开机
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
@ -146,12 +120,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- 规格详情模态框 -->
|
<!-- 规格详情模态框 -->
|
||||||
<a-modal
|
<a-modal v-model:open="specModalVisible" title="规格详情" width="500px" :footer="null">
|
||||||
v-model:open="specModalVisible"
|
|
||||||
title="规格详情"
|
|
||||||
width="500px"
|
|
||||||
:footer="null"
|
|
||||||
>
|
|
||||||
<div class="spec-detail-content">
|
<div class="spec-detail-content">
|
||||||
<a-descriptions :column="1" bordered size="small">
|
<a-descriptions :column="1" bordered size="small">
|
||||||
<a-descriptions-item label="GPU型号">{{ specDetail.gpu_model }}</a-descriptions-item>
|
<a-descriptions-item label="GPU型号">{{ specDetail.gpu_model }}</a-descriptions-item>
|
||||||
@ -167,14 +136,10 @@
|
|||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<!-- 重置确认模态框 -->
|
<!-- 重置确认模态框 -->
|
||||||
<a-modal
|
<a-modal v-model:open="resetModalVisible" title="确认重置" @ok="confirmReset" @cancel="resetModalVisible = false">
|
||||||
v-model:open="resetModalVisible"
|
|
||||||
title="确认重置"
|
|
||||||
@ok="confirmReset"
|
|
||||||
@cancel="resetModalVisible = false"
|
|
||||||
>
|
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<exclamation-circle-outlined class="warning-icon" style="color: #faad14; font-size: 24px; margin-right: 12px;" />
|
<exclamation-circle-outlined class="warning-icon"
|
||||||
|
style="color: #faad14; font-size: 24px; margin-right: 12px;" />
|
||||||
<div>
|
<div>
|
||||||
<p style="font-weight: bold; margin-bottom: 8px;">确认要重置容器实例吗?</p>
|
<p style="font-weight: bold; margin-bottom: 8px;">确认要重置容器实例吗?</p>
|
||||||
<p style="color: #666; margin-bottom: 4px;">• 重置将清空容器内的所有数据</p>
|
<p style="color: #666; margin-bottom: 4px;">• 重置将清空容器内的所有数据</p>
|
||||||
@ -188,15 +153,11 @@
|
|||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<!-- 释放确认模态框 -->
|
<!-- 释放确认模态框 -->
|
||||||
<a-modal
|
<a-modal v-model:open="releaseModalVisible" title="确认释放" @ok="confirmRelease" @cancel="releaseModalVisible = false"
|
||||||
v-model:open="releaseModalVisible"
|
:ok-button-props="{ danger: true }">
|
||||||
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" style="color: #ff4d4f; font-size: 24px; margin-right: 12px;" />
|
<exclamation-circle-outlined class="warning-icon"
|
||||||
|
style="color: #ff4d4f; font-size: 24px; margin-right: 12px;" />
|
||||||
<div>
|
<div>
|
||||||
<p style="font-weight: bold; margin-bottom: 8px;">确认要释放实例吗?</p>
|
<p style="font-weight: bold; margin-bottom: 8px;">确认要释放实例吗?</p>
|
||||||
<p style="color: #ff4d4f; margin-bottom: 4px;">• 释放后实例将被永久删除</p>
|
<p style="color: #ff4d4f; margin-bottom: 4px;">• 释放后实例将被永久删除</p>
|
||||||
@ -212,19 +173,11 @@
|
|||||||
<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 {
|
import {ExclamationCircleOutlined,ReloadOutlined,EyeOutlined,DownOutlined,RedoOutlined,DeleteOutlined} from '@ant-design/icons-vue'
|
||||||
ExclamationCircleOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
EyeOutlined,
|
|
||||||
DownOutlined,
|
|
||||||
RedoOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
DashboardOutlined,
|
|
||||||
FileTextOutlined
|
|
||||||
} 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 { get } from 'http'
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 状态管理
|
// 状态管理
|
||||||
@ -256,98 +209,7 @@ const paginationState = ref({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 模拟数据
|
// 模拟数据
|
||||||
const mockData = ref([
|
const mockData = ref([])
|
||||||
{
|
|
||||||
id: 'ins-2024010001',
|
|
||||||
name: 'AI训练实例-01',
|
|
||||||
status: 'running',
|
|
||||||
gpu_model: 'NVIDIA A100',
|
|
||||||
gpu_count: 2,
|
|
||||||
gpu_memory_gb: 80,
|
|
||||||
cpu_cores: 8,
|
|
||||||
memory_mb: 16384,
|
|
||||||
system_disk: 200,
|
|
||||||
data_disk: 500,
|
|
||||||
health_status: 'healthy',
|
|
||||||
price_type: '按量付费',
|
|
||||||
release_at: '2024-12-31',
|
|
||||||
down_at: '-',
|
|
||||||
ssh_link: 'ssh root@10.0.0.1',
|
|
||||||
bandwidth: 1000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ins-2024010002',
|
|
||||||
name: '推理服务实例',
|
|
||||||
status: 'stopped',
|
|
||||||
gpu_model: 'NVIDIA V100',
|
|
||||||
gpu_count: 1,
|
|
||||||
gpu_memory_gb: 32,
|
|
||||||
cpu_cores: 4,
|
|
||||||
memory_mb: 8192,
|
|
||||||
system_disk: 100,
|
|
||||||
data_disk: 200,
|
|
||||||
health_status: 'healthy',
|
|
||||||
price_type: '包月',
|
|
||||||
release_at: '2024-11-30',
|
|
||||||
down_at: '2024-10-15',
|
|
||||||
ssh_link: 'ssh root@10.0.0.2',
|
|
||||||
bandwidth: 500
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ins-2024010003',
|
|
||||||
name: '开发测试实例',
|
|
||||||
status: 'running',
|
|
||||||
gpu_model: 'NVIDIA RTX 4090',
|
|
||||||
gpu_count: 1,
|
|
||||||
gpu_memory_gb: 24,
|
|
||||||
cpu_cores: 12,
|
|
||||||
memory_mb: 32768,
|
|
||||||
system_disk: 500,
|
|
||||||
data_disk: 1000,
|
|
||||||
health_status: 'warning',
|
|
||||||
price_type: '按量付费',
|
|
||||||
release_at: '2025-01-15',
|
|
||||||
down_at: '-',
|
|
||||||
ssh_link: 'ssh root@10.0.0.3',
|
|
||||||
bandwidth: 200
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ins-2024010004',
|
|
||||||
name: '数据科学实例',
|
|
||||||
status: 'error',
|
|
||||||
gpu_model: 'NVIDIA T4',
|
|
||||||
gpu_count: 1,
|
|
||||||
gpu_memory_gb: 16,
|
|
||||||
cpu_cores: 8,
|
|
||||||
memory_mb: 16384,
|
|
||||||
system_disk: 150,
|
|
||||||
data_disk: 300,
|
|
||||||
health_status: 'error',
|
|
||||||
price_type: '包年',
|
|
||||||
release_at: '2024-12-15',
|
|
||||||
down_at: '2024-10-10',
|
|
||||||
ssh_link: 'ssh root@10.0.0.4',
|
|
||||||
bandwidth: 100
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ins-2024010005',
|
|
||||||
name: '深度学习实例',
|
|
||||||
status: 'running',
|
|
||||||
gpu_model: 'NVIDIA A6000',
|
|
||||||
gpu_count: 4,
|
|
||||||
gpu_memory_gb: 48,
|
|
||||||
cpu_cores: 16,
|
|
||||||
memory_mb: 65536,
|
|
||||||
system_disk: 1000,
|
|
||||||
data_disk: 2000,
|
|
||||||
health_status: 'healthy',
|
|
||||||
price_type: '按量付费',
|
|
||||||
release_at: '2025-03-20',
|
|
||||||
down_at: '-',
|
|
||||||
ssh_link: 'ssh root@10.0.0.5',
|
|
||||||
bandwidth: 2000
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 表格列定义
|
// 表格列定义
|
||||||
const columns = ref<TableColumnType[]>([
|
const columns = ref<TableColumnType[]>([
|
||||||
@ -364,39 +226,34 @@ const columns = ref<TableColumnType[]>([
|
|||||||
{ title: '操作', key: 'actions', width: 150 }
|
{ title: '操作', key: 'actions', width: 150 }
|
||||||
])
|
])
|
||||||
|
|
||||||
// 计算表格数据(应用筛选)
|
const getDataList = async () => {
|
||||||
const tableData = computed(() => {
|
// 模拟数据加载
|
||||||
let filtered = [...mockData.value]
|
try {
|
||||||
|
loading.value = true
|
||||||
// 状态筛选
|
const params = {
|
||||||
if (filterStatus.value !== 'all') {
|
page_num: paginationState.value.current,
|
||||||
filtered = filtered.filter(item => item.status === filterStatus.value)
|
page_size: paginationState.value.pageSize
|
||||||
|
}
|
||||||
|
const res = await hostCaseList(params)
|
||||||
|
mockData.value = res.data
|
||||||
|
loading.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取实例列表失败:', error)
|
||||||
|
message.error('数据加载失败')
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关键词搜索
|
}
|
||||||
if (searchKeyword.value) {
|
getDataList()
|
||||||
const keyword = searchKeyword.value.toLowerCase()
|
|
||||||
filtered = filtered.filter(item =>
|
|
||||||
item.id.toLowerCase().includes(keyword) ||
|
|
||||||
item.name.toLowerCase().includes(keyword)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新分页总数
|
|
||||||
paginationState.value.total = filtered.length
|
|
||||||
|
|
||||||
// 分页处理
|
|
||||||
const start = (paginationState.value.current - 1) * paginationState.value.pageSize
|
|
||||||
const end = start + paginationState.value.pageSize
|
|
||||||
return filtered.slice(start, end)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 状态文本映射
|
// 状态文本映射
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, any> = {
|
||||||
running: '运行中',
|
"RUNNING": '运行中',
|
||||||
stopped: '已停止',
|
"STOPPED": '已停止',
|
||||||
error: '异常'
|
"CREATING": '创建中',
|
||||||
|
"RESTARTING": '重启中',
|
||||||
}
|
}
|
||||||
return map[status] || status
|
return map[status] || status
|
||||||
}
|
}
|
||||||
@ -503,11 +360,7 @@ const handleBatchRenew = () => {
|
|||||||
|
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
const refreshData = () => {
|
const refreshData = () => {
|
||||||
loading.value = true
|
getDataList()
|
||||||
setTimeout(() => {
|
|
||||||
loading.value = false
|
|
||||||
message.success('数据已刷新')
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
@ -641,7 +494,7 @@ onMounted(() => {
|
|||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
|
|
||||||
:deep(.ant-table) {
|
:deep(.ant-table) {
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead>tr>th {
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1f2d3d;
|
color: #1f2d3d;
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
</a-carousel>
|
</a-carousel>
|
||||||
</div>
|
</div>
|
||||||
<div class="one">
|
<div class="one">
|
||||||
<div v-for="item in oneList" style="display: flex;justify-content: center;align-items: center;cursor: pointer;" @click="router.push(item.path)">
|
<div v-for="item in oneList" style="display: flex;justify-content: center;align-items: center;cursor: pointer;"
|
||||||
|
@click="router.push(item.path)">
|
||||||
<div style="margin-right: 10px;"><img :src="item.img" alt="" srcset="" width="50" height="50"></div>
|
<div style="margin-right: 10px;"><img :src="item.img" alt="" srcset="" width="50" height="50"></div>
|
||||||
<div>
|
<div>
|
||||||
<h3>{{ item.title }}</h3>
|
<h3>{{ item.title }}</h3>
|
||||||
@ -68,31 +69,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="avatary_list">
|
<div class="avatary_list">
|
||||||
<div class="avatary_item">
|
<div class="avatary_item" v-for="item in advantageList">
|
||||||
<div>
|
<div>
|
||||||
<div class="title" style="font-size: 28px;">海量高性能 GPU 集群算力</div>
|
<div class="title" style="font-size: 28px;">{{ item.title }}</div>
|
||||||
<div class="subtitle" style="text-align: left;font-size: 18px;line-height:30px;">搭载 NVIDIA A100/H100/H800 等高性能
|
<div class="subtitle" style="text-align: left;font-size: 18px;line-height:30px;">{{ item.description }}</div>
|
||||||
GPU,拥有强悍算力与超高显存带宽,精准适配不同精度需求,大幅提升运算效率与资源利用率;同时优化稀疏矩阵运算,有效减少 AI 模型的冗余计算。</div>
|
|
||||||
</div>
|
</div>
|
||||||
<img src="@/assets/jimeng.png" alt="" srcset="" width="300" height="300">
|
<img :src="item.image_url" alt="" srcset="" width="300" height="300">
|
||||||
</div>
|
|
||||||
<div class="avatary_item" style="margin-bottom: 50px;">
|
|
||||||
<img src="@/assets/gpu.png" alt="" srcset="" width="300">
|
|
||||||
<div>
|
|
||||||
<div class="title" style="font-size: 28px;">算力灵活调度</div>
|
|
||||||
<div class="subtitle" style="text-align: left;font-size: 18px;line-height:30px;">
|
|
||||||
本平台支持按需计费与包周期计费两种正式模式,适配短期突发算力需求与长期稳定部署场景。按需计费按实结算,避免资源浪费;包周期计费性价比更优,保障持续运算。用户可自主择选,兼顾成本与效率,助力业务高效推进。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="avatary_item">
|
|
||||||
<div>
|
|
||||||
<div class="title" style="font-size: 28px;">低价高配 算力性价比超高</div>
|
|
||||||
<div class="subtitle" style="text-align: left;font-size: 18px;line-height:30px;">
|
|
||||||
算力性价比之王!花一半钱享翻倍算力,顶级硬件配置 + 灵活计费模式,无隐形消费。中小团队、初创企业无需负担高昂成本,轻松拥抱 AI 时代,高性价比之选。
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<img src="@/assets/jimeng2.png" alt="" srcset="" width="300" height="300px">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -136,7 +118,7 @@
|
|||||||
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons-vue';
|
import { LeftCircleOutlined, RightCircleOutlined } from '@ant-design/icons-vue';
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { getBannerList, getGpuList, getAdvantage, getHotProduct } from '@/apis/home';
|
import { getBannerList, getGpuList, getAdvantage, getHotProduct,getApiOneList } from '@/apis/home';
|
||||||
import one from '@/assets/1.png'
|
import one from '@/assets/1.png'
|
||||||
import two from '@/assets/2.png'
|
import two from '@/assets/2.png'
|
||||||
import three from '@/assets/3.png'
|
import three from '@/assets/3.png'
|
||||||
@ -147,30 +129,12 @@ import goumaijilu from '@/assets/goumaijilu.png'
|
|||||||
import defaultBanner3 from '@/assets/12.png';
|
import defaultBanner3 from '@/assets/12.png';
|
||||||
import defaultBanner4 from '@/assets/333.png';
|
import defaultBanner4 from '@/assets/333.png';
|
||||||
import defaultBanner from '@/assets/333.png';
|
import defaultBanner from '@/assets/333.png';
|
||||||
|
import { get } from 'http';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const listColor = ref(['#7ed321', '#21d3c0', '#35a4de'])
|
const listColor = ref(['#7ed321', '#21d3c0', '#35a4de'])
|
||||||
const source = ref([])
|
const source = ref<any[]>([])
|
||||||
const oneList = ref([
|
const oneList = ref<any[]>([])
|
||||||
{
|
const advantageList = ref<any[]>([])
|
||||||
img: youhuiquan,
|
|
||||||
title: '算力免费领',
|
|
||||||
description: '完成认证/问卷获得算力券',
|
|
||||||
path:'/active/newUser'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: yaoqinghaoyou,
|
|
||||||
title: '邀约优算友',
|
|
||||||
description: '邀请好友可获得算力券',
|
|
||||||
path:'/active/invite'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
img: goumaijilu,
|
|
||||||
title: '开业重磅福利',
|
|
||||||
description: '购算力,享豪礼',
|
|
||||||
path:'/active/newUser'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
const advantageList = ref([])
|
|
||||||
const bannerList = ref<any[]>([
|
const bannerList = ref<any[]>([
|
||||||
{ id: 0, image_url: defaultBanner3 },
|
{ id: 0, image_url: defaultBanner3 },
|
||||||
{ id: 1, image_url: defaultBanner4 }
|
{ id: 1, image_url: defaultBanner4 }
|
||||||
@ -213,16 +177,48 @@ const fetchBannerList = async () => {
|
|||||||
bannerList.value = [{ id: 0, image_url: defaultBanner }];
|
bannerList.value = [{ id: 0, image_url: defaultBanner }];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// 请求轮播
|
||||||
|
const getOneList = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await getApiOneList();
|
||||||
|
// 处理返回的数据
|
||||||
|
if (res && Array.isArray(res) && res.length > 0) {
|
||||||
|
oneList.value = res;
|
||||||
|
} else {
|
||||||
|
// API返回空数据或无效数据,使用默认图片
|
||||||
|
oneList.value = [{ id: 0, image_url: defaultBanner }];
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('轮播图请求失败:', error);
|
||||||
|
// 请求失败时使用默认图片
|
||||||
|
oneList.value = [
|
||||||
|
{
|
||||||
|
img: youhuiquan,
|
||||||
|
title: '算力免费领',
|
||||||
|
description: '完成认证/问卷获得算力券',
|
||||||
|
path: '/active/newUser'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: yaoqinghaoyou,
|
||||||
|
title: '邀约优算友',
|
||||||
|
description: '邀请好友可获得算力券',
|
||||||
|
path: '/active/invite'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
img: goumaijilu,
|
||||||
|
title: '开业重磅福利',
|
||||||
|
description: '购算力,享豪礼',
|
||||||
|
path: '/active/newUser'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
// 请求GPU列表
|
// 请求GPU列表
|
||||||
const getGPUList = async () => {
|
const getGPUList = async () => {
|
||||||
try {
|
try {
|
||||||
// const res: any = await getGpuList();
|
const res: any = await getGpuList();
|
||||||
source.value = [
|
source.value = res
|
||||||
{ gpu_type: 'NVIDIA RTX 3090', payOnTime: 15.0, memory: 64, vram: 24, gpu_num: 1 },
|
|
||||||
{ gpu_type: 'NVIDIA A100', payOnTime: 25.0, memory: 128, vram: 40, gpu_num: 1 },
|
|
||||||
{ gpu_type: 'NVIDIA RTX 4090', payOnTime: 30.0, memory: 96, vram: 24, gpu_num: 1 }
|
|
||||||
];
|
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('GPU列表请求失败:', error);
|
console.error('GPU列表请求失败:', error);
|
||||||
@ -232,26 +228,20 @@ const getGPUList = async () => {
|
|||||||
// 请求产品优势
|
// 请求产品优势
|
||||||
const getAdvantageList = async () => {
|
const getAdvantageList = async () => {
|
||||||
try {
|
try {
|
||||||
// const res: any = await getAdvantage();
|
const res: any = await getAdvantage();
|
||||||
const list = [
|
const list = res || [];
|
||||||
{ title: '高性能算力', description: '采用最新一代GPU,提供强大计算能力,满足各种复杂任务需求。' },
|
|
||||||
{ title: '灵活计费模式', description: '支持按需付费和包月套餐,用户可根据实际需求选择最合适的计费方式。' },
|
|
||||||
{ title: '全球节点覆盖', description: '在多个地区设有数据中心,确保用户无论身处何地都能享受低延迟的服务体验。' },
|
|
||||||
{ title: '专业技术支持', description: '提供7x24小时技术支持,确保用户在使用过程中遇到的问题能够及时得到解决。' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 给每个 item 添加 img 字段
|
// 给每个 item 添加 img 字段
|
||||||
list.forEach((item: any, index: number) => {
|
// list.forEach((item: any, index: number) => {
|
||||||
if (index === 0) {
|
// if (index === 0) {
|
||||||
item.img = one;
|
// item.img = one;
|
||||||
} else if (index === 1) {
|
// } else if (index === 1) {
|
||||||
item.img = two;
|
// item.img = two;
|
||||||
} else if (index === 2) {
|
// } else if (index === 2) {
|
||||||
item.img = three;
|
// item.img = three;
|
||||||
} else {
|
// } else {
|
||||||
item.img = firth;
|
// item.img = firth;
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
advantageList.value = list;
|
advantageList.value = list;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -262,10 +252,10 @@ const getAdvantageList = async () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// fetchBannerList();
|
fetchBannerList();
|
||||||
getGPUList();
|
getGPUList();
|
||||||
getAdvantageList();
|
getAdvantageList();
|
||||||
|
getOneList();
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.banner-slide {
|
.banner-slide {
|
||||||
@ -352,7 +342,8 @@ getAdvantageList();
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 100px;
|
gap: 100px;
|
||||||
&>div{
|
|
||||||
|
&>div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<div style="display: flex;align-items: center;justify-content: flex-end;">
|
<div style="display: flex;align-items: center;justify-content: flex-end;">
|
||||||
<a-avatar :size="24" :src="userInfo.avatar">
|
<a-avatar :size="24" :src="userInfo.avatar ?? avatar">
|
||||||
<!-- <template #icon>
|
<!-- <template #icon>
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
</template> -->
|
</template> -->
|
||||||
@ -25,19 +25,24 @@
|
|||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-card hoverable style="width: 300px">
|
<a-card hoverable style="width: 300px">
|
||||||
<template #cover>
|
<template #cover>
|
||||||
<div style="background:#f5f7fa;padding:10px;line-height: 30px">
|
<div style="background:#f5f7fa;padding:10px;line-height: 30px;display: flex;align-items: center;">
|
||||||
<div>{{ userInfo.userName }} <a-tag color="blue" style="margin-left: 10px;">{{
|
<div>
|
||||||
userInfo.accountType === 'USER' ? '个人认证' : '企业认证' }}</a-tag></div>
|
<img :src="avatar" alt="" srcset="" width="50">
|
||||||
<div>ID:{{ userInfo.id }}</div>
|
</div>
|
||||||
|
<div style="margin-left: 10px;">
|
||||||
|
<div>{{ userInfo.phone }} <a-tag color="blue"
|
||||||
|
style="margin-left: 10px;">{{
|
||||||
|
userInfo.accountType === 'USER' ? '个人账户' : '企业账户' }}</a-tag></div>
|
||||||
|
<div>用户ID:{{ userInfo.id }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 10px;line-height: 45px;">
|
<div style="padding: 10px;line-height: 45px;">
|
||||||
<div style="display: flex;justify-content: space-between;align-items: center;">
|
<div style="display: flex;justify-content: space-between;align-items: center;">
|
||||||
<div>可用余额:¥{{ userInfo.balace }}</div>
|
<div>可用余额:¥{{ userInfo.balance}}</div>
|
||||||
<!-- <a-button type="primary" danger ghost size="small">去充值</a-button> -->
|
<!-- <a-button type="primary" danger ghost size="small">去充值</a-button> -->
|
||||||
</div>
|
</div>
|
||||||
<div>实例数量:¥{{ userInfo.caseNum }}</div>
|
<div>未读消息:{{ userInfo.unreadMsgNum }}条</div>
|
||||||
<div>冻结余额:¥{{ userInfo.freezeBalace }}</div>
|
|
||||||
<div>未冻结余额:¥{{ userInfo.noFreezeBalace }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
@ -68,6 +73,7 @@ const route = useRoute()
|
|||||||
const isHome = ref(true)
|
const isHome = ref(true)
|
||||||
const isLogin = ref(!!localStorage.getItem('token'))
|
const isLogin = ref(!!localStorage.getItem('token'))
|
||||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||||
|
console.log('用户信息:', userInfo);
|
||||||
// 根据当前路由初始化菜单选中项(去掉 /layout 前缀)
|
// 根据当前路由初始化菜单选中项(去掉 /layout 前缀)
|
||||||
const getActiveKeyFromRoute = () => {
|
const getActiveKeyFromRoute = () => {
|
||||||
const path = route.path
|
const path = route.path
|
||||||
@ -170,7 +176,7 @@ const logout = () => {
|
|||||||
|
|
||||||
.gx_layout_content {
|
.gx_layout_content {
|
||||||
// margin-top: 60px;
|
// margin-top: 60px;
|
||||||
height: calc(100% - 60px);
|
// height: calc(100% - 60px);
|
||||||
min-height: calc(100% - 60px);
|
min-height: calc(100% - 60px);
|
||||||
background-color: rgba(240, 240, 240, 1);
|
background-color: rgba(240, 240, 240, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-form ref="formRef" :model="formState" name="normal_login" class="login-form" @finish="onFinish"
|
<a-form ref="formRef" :model="formState" name="normal_login" class="login-form" @finish="onFinish"
|
||||||
@finish-failed="onFinishFailed" >
|
@finish-failed="onFinishFailed">
|
||||||
<a-form-item name="phone" :rules="[{ required: true, message: '请输入用户名!' }]">
|
<a-form-item name="phone" :rules="[{ required: true, message: '请输入用户名!' }]">
|
||||||
<a-input v-model:value="formState.phone" placeholder="请输入账号">
|
<a-input v-model:value="formState.phone" placeholder="请输入账号">
|
||||||
<template #addonBefore><UserOutlined /></template>
|
<template #addonBefore>
|
||||||
|
<UserOutlined />
|
||||||
|
</template>
|
||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<!-- 手动控制密码字段的错误状态 -->
|
<!-- 手动控制密码字段的错误状态 -->
|
||||||
<a-form-item name="code" :help="codeHelp" :rules="[{ required: true, message: '请输入密码!' }]">
|
<a-form-item name="code" :help="codeHelp" :rules="[{ required: true, message: '请输入密码!' }]">
|
||||||
<a-input-password v-model:value="formState.code" placeholder="请输入密码">
|
<a-input-password v-model:value="formState.code" placeholder="请输入密码">
|
||||||
<template #addonBefore><UnlockOutlined /></template>
|
<template #addonBefore>
|
||||||
|
<UnlockOutlined />
|
||||||
|
</template>
|
||||||
</a-input-password>
|
</a-input-password>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
@ -24,11 +28,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, shallowRef,onMounted } from 'vue';
|
import { reactive, ref, shallowRef, onMounted,mounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { login, fetchUserInfo } from '@/apis/modules/login';
|
import { login, fetchUserInfo } from '@/apis/modules/login';
|
||||||
import { message, type FormInstance } from 'ant-design-vue';
|
import { message, type FormInstance } from 'ant-design-vue';
|
||||||
import { UserOutlined,UnlockOutlined } from '@ant-design/icons-vue';
|
import { UserOutlined, UnlockOutlined } from '@ant-design/icons-vue';
|
||||||
import confirm from 'ant-design-vue/es/modal/confirm';
|
import confirm from 'ant-design-vue/es/modal/confirm';
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
@ -67,7 +71,7 @@ const setCodeError = (msg: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFinish = async (values: FormState) => {
|
const onFinish = async (values: FormState) => {
|
||||||
if(!checked.value){
|
if (!formState.remember) {
|
||||||
message.warning('请同意用户协议及隐私政策');
|
message.warning('请同意用户协议及隐私政策');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -90,7 +94,7 @@ const onFinish = async (values: FormState) => {
|
|||||||
localStorage.setItem('token', token);
|
localStorage.setItem('token', token);
|
||||||
localStorage.setItem('savedUsername', formState.remember ? formState.phone : '');
|
localStorage.setItem('savedUsername', formState.remember ? formState.phone : '');
|
||||||
localStorage.setItem('rememberMe', formState.remember.toString());
|
localStorage.setItem('rememberMe', formState.remember.toString());
|
||||||
const userRes=await fetchUserInfo();
|
const userRes = await fetchUserInfo();
|
||||||
localStorage.setItem('userInfo', JSON.stringify(userRes));
|
localStorage.setItem('userInfo', JSON.stringify(userRes));
|
||||||
confirmLoading.value = false;
|
confirmLoading.value = false;
|
||||||
router.push('/layout/home');
|
router.push('/layout/home');
|
||||||
|
|||||||
@ -13,16 +13,18 @@
|
|||||||
<a-input-password v-model:value="formState.password" placeholder="请输入密码" />
|
<a-input-password v-model:value="formState.password" placeholder="请输入密码" />
|
||||||
<span style="font-size: 12px;color: #c9c9c9;">8~16个字符,至少包含字母和数字,不能包含空格</span>
|
<span style="font-size: 12px;color: #c9c9c9;">8~16个字符,至少包含字母和数字,不能包含空格</span>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item name="confirmPassword" :rules="[{ required: true, message: '请输入8~16位,且包含字母和数字!'},{validator: validatePassword }]">
|
<a-form-item name="confirm_password" :rules="[{ required: true, message: '请输入8~16位,且包含字母和数字!'},{validator: querenPassword }]">
|
||||||
<a-input-password v-model:value="formState.confirmPassword" placeholder="请确认密码" />
|
<a-input-password v-model:value="formState.confirm_password" placeholder="请确认密码" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item name="invite_id" >
|
||||||
|
<a-input v-model:value="formState.invite_id" placeholder="请输入邀请码" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-checkbox v-model:checked="checked">我已经阅读并同意<a href="">《用户协议》</a>及<a href="">《隐私政策》</a></a-checkbox>
|
<a-checkbox v-model:checked="checked">我已经阅读并同意<a href="">《用户协议》</a>及<a href="">《隐私政策》</a></a-checkbox>
|
||||||
<!-- <a-form-item name="inviteCode" :rules="[{ required: true, message: '请输入邀请码!' }]">
|
<!-- <a-form-item name="inviteCode" :rules="[{ required: true, message: '请输入邀请码!' }]">
|
||||||
<a-input v-model:value="formState.inviteCode" placeholder="请输入邀请码" />
|
<a-input v-model:value="formState.inviteCode" placeholder="请输入邀请码" />
|
||||||
</a-form-item> -->
|
</a-form-item> -->
|
||||||
<a-form-item style="margin-top:20px;margin-bottom: 10px !important;">
|
<a-form-item style="margin-top:20px;margin-bottom: 10px !important;">
|
||||||
<a-button type="primary" html-type="submit" block class="login-form-button">
|
<a-button type="primary" html-type="submit" block class="login-form-button" :disabled="!checked">
|
||||||
注 册
|
注 册
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@ -46,27 +48,36 @@ interface FormState {
|
|||||||
phone: string;
|
phone: string;
|
||||||
code: string;
|
code: string;
|
||||||
password: string;
|
password: string;
|
||||||
confirmPassword: string;
|
confirm_password: string;
|
||||||
inviteCode: string;
|
invite_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formState = reactive<FormState>({
|
const formState = reactive<FormState>({
|
||||||
phone: '',
|
phone: '',
|
||||||
code: '',
|
code: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirm_password: '',
|
||||||
inviteCode: '',
|
invite_id: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearCodeError = () => {
|
const clearCodeError = () => {
|
||||||
codeValidateStatus.value = '';
|
codeValidateStatus.value = '';
|
||||||
codeHelp.value = '';
|
codeHelp.value = '';
|
||||||
};
|
};
|
||||||
|
const querenPassword = (rule, value) => {
|
||||||
|
if (!value) {
|
||||||
|
return Promise.reject('请输入密码!');
|
||||||
|
}
|
||||||
|
if (value !== formState.password) {
|
||||||
|
return Promise.reject('两次输入的密码不一致!');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
const onFinish = async (values: FormState) => {
|
const onFinish = async (values: FormState) => {
|
||||||
clearCodeError();
|
clearCodeError();
|
||||||
const loginData = {
|
const loginData = {
|
||||||
...values,
|
...values,
|
||||||
|
checked: checked.value,
|
||||||
};
|
};
|
||||||
formRef.value?.validateFields().then(async () => {
|
formRef.value?.validateFields().then(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -6,17 +6,17 @@
|
|||||||
<SmsCodeInput v-model:value="formState.phone" />
|
<SmsCodeInput v-model:value="formState.phone" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<!-- 手动控制密码字段的错误状态 -->
|
<!-- 手动控制密码字段的错误状态 -->
|
||||||
<a-form-item name="code" :rules="[{ required: true, message: '请输入验证码!' }]">
|
<a-form-item name="success_code" :rules="[{ required: true, message: '请输入验证码!' }]">
|
||||||
<a-input v-model:value="formState.code" placeholder="请输入验证码" />
|
<a-input v-model:value="formState.success_code" placeholder="请输入验证码" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item name="password"
|
<a-form-item name="password"
|
||||||
:rules="[{ required: true, message: '请输入8~16位,且包含字母和数字!' }, { validator: validatePassword }]">
|
:rules="[{ required: true, message: '请输入8~16位,且包含字母和数字!' }, { validator: validatePassword }]">
|
||||||
<a-input-password v-model:value="formState.password" placeholder="请输入新密码" />
|
<a-input-password v-model:value="formState.password" placeholder="请输入新密码" />
|
||||||
<span style="font-size: 12px;color: #c9c9c9;">8~16个字符,至少包含字母和数字,不能包含空格</span>
|
<span style="font-size: 12px;color: #c9c9c9;">8~16个字符,至少包含字母和数字,不能包含空格</span>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item name="confirmPassword"
|
<a-form-item name="confirm_password"
|
||||||
:rules="[{ required: true, message: '请输入8~16位,且包含字母和数字!' }, { validator: validatePassword }]">
|
:rules="[{ required: true, message: '请输入8~16位,且包含字母和数字!' }, { validator: querenPassword }]">
|
||||||
<a-input-password v-model:value="formState.confirmPassword" placeholder="请确认新密码" />
|
<a-input-password v-model:value="formState.confirm_password" placeholder="请确认新密码" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button type="primary" html-type="submit" block class="login-form-button">
|
<a-button type="primary" html-type="submit" block class="login-form-button">
|
||||||
@ -41,23 +41,32 @@ const codeHelp = shallowRef('');
|
|||||||
|
|
||||||
interface FormState {
|
interface FormState {
|
||||||
phone: string;
|
phone: string;
|
||||||
code: string;
|
success_code: string;
|
||||||
password: string;
|
password: string;
|
||||||
confirmPassword: string;
|
confirm_password: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formState = reactive<FormState>({
|
const formState = reactive<FormState>({
|
||||||
phone: '',
|
phone: '',
|
||||||
code: '',
|
success_code: '',
|
||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirm_password: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const clearCodeError = () => {
|
const clearCodeError = () => {
|
||||||
codeValidateStatus.value = '';
|
codeValidateStatus.value = '';
|
||||||
codeHelp.value = '';
|
codeHelp.value = '';
|
||||||
};
|
};
|
||||||
|
const querenPassword = (rule, value) => {
|
||||||
|
if (!value) {
|
||||||
|
return Promise.reject('请输入密码!');
|
||||||
|
}
|
||||||
|
if (value !== formState.password) {
|
||||||
|
return Promise.reject('两次输入的密码不一致!');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
const onFinish = async (values: FormState) => {
|
const onFinish = async (values: FormState) => {
|
||||||
clearCodeError();
|
clearCodeError();
|
||||||
const loginData = {
|
const loginData = {
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
<Code v-if="currentTag == 'login' && state.tabsActiveName === 'code'" />
|
<Code v-if="currentTag == 'login' && state.tabsActiveName === 'code'" />
|
||||||
|
|
||||||
<div style="width: 100%;display: flex;justify-content: space-between;font-size: 14px;">
|
<div style="width: 100%;display: flex;justify-content: space-between;font-size: 14px;">
|
||||||
<div v-if="state.tabsActiveName === 'account'"> <a-checkbox v-model:checked="checked">记住密码</a-checkbox></div>
|
<div v-if="state.tabsActiveName === 'account'"> <a-checkbox v-model:checked="checked">记住账号</a-checkbox></div>
|
||||||
<div>
|
<div>
|
||||||
<span style="color:#1677ff;cursor: pointer;" @click="currentTag = 'register'">注册</span>
|
<span style="color:#1677ff;cursor: pointer;" @click="currentTag = 'register'">注册</span>
|
||||||
<span style="color: #666666;margin: 0 5px;">|</span>
|
<span style="color: #666666;margin: 0 5px;">|</span>
|
||||||
@ -63,7 +63,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, computed, ref } from "vue";
|
import { reactive, computed, ref,onMounted } from "vue";
|
||||||
import Account from "@/views/login/component/account.vue";
|
import Account from "@/views/login/component/account.vue";
|
||||||
import Code from "@/views/login/component/code.vue";
|
import Code from "@/views/login/component/code.vue";
|
||||||
import Register from "./component/register.vue";
|
import Register from "./component/register.vue";
|
||||||
@ -91,6 +91,12 @@ const state = reactive({
|
|||||||
// const getThemeConfig = computed(() => {
|
// const getThemeConfig = computed(() => {
|
||||||
// return theme.themeConfig;
|
// return theme.themeConfig;
|
||||||
// });
|
// });
|
||||||
|
onMounted(()=>{
|
||||||
|
const savedUsername = localStorage.getItem('savedUsername')
|
||||||
|
if (savedUsername) {
|
||||||
|
checked.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
// 切换密码、手机登录
|
// 切换密码、手机登录
|
||||||
const onTabsClick = () => {
|
const onTabsClick = () => {
|
||||||
state.isTabPaneShow = !state.isTabPaneShow;
|
state.isTabPaneShow = !state.isTabPaneShow;
|
||||||
@ -143,7 +149,7 @@ const handleMenuClick = (key) => {
|
|||||||
transform: translate(-50%, -50%) translate3d(0, 0, 0);
|
transform: translate(-50%, -50%) translate3d(0, 0, 0);
|
||||||
background-color: rgba(255, 255, 255, 0.99);
|
background-color: rgba(255, 255, 255, 0.99);
|
||||||
transition: height 0.2s linear;
|
transition: height 0.2s linear;
|
||||||
height: 520px;
|
height: 550px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|||||||
@ -4,34 +4,32 @@
|
|||||||
<h1>算力中心</h1>
|
<h1>算力中心</h1>
|
||||||
<h2>聚焦高效算力服务,为数字产业、智能应用提供强支撑</h2>
|
<h2>聚焦高效算力服务,为数字产业、智能应用提供强支撑</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="instance-create-body">
|
<div class="instance-create-body">
|
||||||
<!-- 筛选区域(保持不变) -->
|
<!-- 筛选区域(保持不变) -->
|
||||||
<a-card class="card select-server">
|
<a-card class="card select-server">
|
||||||
<div class="list-filter">
|
<div class="list-filter">
|
||||||
<div class="filter-item"> <span class="filter-label">计费方式:</span>
|
<div class="filter-item"> <span class="filter-label">计费方式:</span>
|
||||||
<div class="filter-content"> <a-radio-group v-model:value="billingType" button-style="solid"
|
<div class="filter-content"> <a-radio-group v-model:value="billingType" button-style="solid"
|
||||||
@change="handleBillingTypeChange"> <a-radio-button v-for="type in billingTypeOptions" :key="type.value"
|
@change="getHostList"> <a-radio-button v-for="type in billingTypeOptions" :key="type.value"
|
||||||
:value="type.value"> {{ type.label }} </a-radio-button> </a-radio-group> </div>
|
:value="type.value"> {{ type.label }} </a-radio-button> </a-radio-group> </div>
|
||||||
</div> <!-- 选择地区 -->
|
</div> <!-- 选择地区 -->
|
||||||
<div class="filter-item"> <span class="filter-label">选择地区:</span>
|
<div class="filter-item"> <span class="filter-label">选择地区:</span>
|
||||||
<div class="filter-content"> <a-radio-group v-model:value="selectedRegion" button-style="solid"
|
<div class="filter-content"> <a-radio-group v-model:value="selectedRegion" button-style="solid"
|
||||||
@change="handleRegionChange"> <a-radio-button v-for="region in regions" :key="region.value"
|
@change="getHostList">
|
||||||
:value="region.value"> {{ region.label }} <a-tag v-if="region.tag" :color="region.tag.color"
|
<a-radio-button v-for="region in areaList" :key="region.center_id" :value="region.center_id"> {{
|
||||||
class="region-tag">{{ region.tag.text }}</a-tag> </a-radio-button> </a-radio-group> </div>
|
region.center_name }}
|
||||||
</div> <!-- 专区选择 -->
|
</a-radio-button> </a-radio-group>
|
||||||
<div class="filter-item" v-if="showZones"> <span class="filter-label">选择专区:</span>
|
</div>
|
||||||
<div class="filter-content"> <a-radio-group v-model:value="selectedZone" button-style="solid"
|
</div>
|
||||||
@change="handleZoneChange"> <a-radio-button v-for="zone in availableZones" :key="zone.value"
|
|
||||||
:value="zone.value"> {{ zone.label }} </a-radio-button> </a-radio-group> </div>
|
|
||||||
</div> <!-- GPU型号 -->
|
|
||||||
<div class="filter-item"> <span class="filter-label">GPU型号:</span>
|
<div class="filter-item"> <span class="filter-label">GPU型号:</span>
|
||||||
<div class="filter-content"> <a-checkbox-group v-model:value="selectedGpuModels"
|
<div class="filter-content">
|
||||||
@change="handleGpuModelChange"> <a-checkbox value="all"
|
<a-radio-group v-model:value="selectedGpuModels" button-style="solid" @change="getHostList">
|
||||||
:disabled="selectedGpuModels.length > 0 && selectedGpuModels[0] !== 'all'">全部</a-checkbox> <a-checkbox
|
<a-radio-button v-for="gpuItem in gpuList" :key="gpuItem.gpuType" :value="gpuItem.gpuType">
|
||||||
v-for="model in availableGpuModels" :key="model.value" :value="model.value"
|
{{ gpuItem.gpuType }}
|
||||||
:disabled="model.available === 0"> {{ model.label }} <span class="note"> ({{ model.available }}/{{
|
<span v-if="gpuItem.gpuType !== '全部'">{{ "(" + gpuItem.gpuAvailableNum + "/" + gpuItem.gpuNum + ")"
|
||||||
model.total }})</span> </a-checkbox> </a-checkbox-group> </div>
|
}}</span>
|
||||||
|
</a-radio-button> </a-radio-group>
|
||||||
|
</div>
|
||||||
</div> <!-- GPU数量 -->
|
</div> <!-- GPU数量 -->
|
||||||
<div class="filter-item"> <span class="filter-label">GPU数量:</span>
|
<div class="filter-item"> <span class="filter-label">GPU数量:</span>
|
||||||
<div class="filter-content"> <a-select :size="'large'" default-value="1" style="width: 200px"
|
<div class="filter-content"> <a-select :size="'large'" default-value="1" style="width: 200px"
|
||||||
@ -42,18 +40,20 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
|
|
||||||
<!-- 机器列表 -->
|
<!-- 机器列表 -->
|
||||||
<a-card class="card machine-list" >
|
<a-card class="card machine-list">
|
||||||
<div class="image-card-container">
|
<template v-if="allMachineList.length > 0">
|
||||||
<div v-for="record in paginatedMachineList" :key="record.id" class="image-card"
|
<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)">
|
:class="{ selected: selectedMachineId === record.id }" @click="handleSelectMachine(record.id)">
|
||||||
<!-- 顶部 -->
|
<!-- 顶部 -->
|
||||||
<div class="image-card-header">
|
<div class="image-card-header">
|
||||||
<div class="gpu-title">
|
<div class="gpu-title">
|
||||||
{{ record.gpuModel }} {{ record.gpuMemory }}
|
{{ record.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="gpu-availability">
|
<div class="gpu-availability">
|
||||||
空闲 / 总量
|
空闲 / 总量
|
||||||
<strong style="font-size: 18px;">{{ record.freeGpu }}</strong> / {{ record.totalGpu }}
|
<strong style="font-size: 18px;">{{ record.gpuAvailableNum }}</strong> / {{ record.gpuNum }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -61,7 +61,7 @@
|
|||||||
<div class="image-card-info">
|
<div class="image-card-info">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-item-name">CPU</span>
|
<span class="info-item-name">CPU</span>
|
||||||
<span class="info-item-account">{{ record.cpuCores }} 核</span>
|
<span class="info-item-account">{{ record.cpuNum }} 核</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-item-name">内存</span>
|
<span class="info-item-name">内存</span>
|
||||||
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-item-name">显存</span>
|
<span class="info-item-name">显存</span>
|
||||||
<span class="info-item-account">{{ record.gpuMemory }}</span>
|
<span class="info-item-account">{{ record.vram }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-item-name">系统盘</span>
|
<span class="info-item-name">系统盘</span>
|
||||||
@ -80,7 +80,7 @@
|
|||||||
<!-- 底部价格 -->
|
<!-- 底部价格 -->
|
||||||
<div class="image-card-footer">
|
<div class="image-card-footer">
|
||||||
<div class="price">
|
<div class="price">
|
||||||
<span class="price-num">{{ getPrice(record) }}</span>
|
<span class="price-num">{{ record.price }}</span>
|
||||||
<span class="price-unit">元/卡/小时</span>
|
<span class="price-unit">元/卡/小时</span>
|
||||||
</div>
|
</div>
|
||||||
<a-button type="primary" class="buy-btn" :disabled="record.freeGpu === 0"
|
<a-button type="primary" class="buy-btn" :disabled="record.freeGpu === 0"
|
||||||
@ -89,13 +89,17 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<a-pagination v-model:current="currentPage" v-model:pageSize="pageSize" :total="filteredMachineList.length"
|
<a-pagination v-model:current="currentPage" v-model:pageSize="pageSize" :total="hostTotal"
|
||||||
show-size-changer show-quick-jumper />
|
show-size-changer show-quick-jumper />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-empty />
|
||||||
|
</template>
|
||||||
|
<!-- 分页 -->
|
||||||
|
|
||||||
</a-card>
|
</a-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -104,668 +108,95 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, reactive, watch } from 'vue'
|
import { ref, computed, reactive, watch } from 'vue'
|
||||||
|
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 router = useRouter();
|
const router = useRouter();
|
||||||
// 类型定义
|
|
||||||
interface Machine {
|
|
||||||
id: string
|
|
||||||
machineName: string
|
|
||||||
gpuModel: string
|
|
||||||
gpuMemory: string
|
|
||||||
freeGpu: number
|
|
||||||
totalGpu: number
|
|
||||||
cpuCores: number
|
|
||||||
memory: number
|
|
||||||
cpuModel: string
|
|
||||||
dataDisk: number
|
|
||||||
expandableDisk: number
|
|
||||||
driver: string
|
|
||||||
cuda: string
|
|
||||||
basePrice: number
|
|
||||||
originalPrice?: number
|
|
||||||
region: string
|
|
||||||
zone: string
|
|
||||||
availableGpuCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GpuModelOption {
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
available: number
|
|
||||||
total: number
|
|
||||||
regions: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RegionOption {
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
tag?: { color: string; text: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ZoneOption {
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
regions: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Coupon {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
amount: number
|
|
||||||
type: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const billingType = ref('weekly')
|
const hostTotal = ref(0)
|
||||||
|
const areaList = ref<any[]>([])
|
||||||
|
const gpuList = ref<any[]>([])
|
||||||
|
const billingType = ref('PayOnTime')
|
||||||
const selectedRegion = ref('beijingDC2')
|
const selectedRegion = ref('beijingDC2')
|
||||||
const selectedZone = ref('')
|
const selectedGpuModels = ref('全部')
|
||||||
const selectedGpuModels = ref<string[]>(['RTX 5090'])
|
|
||||||
const gpuCount = ref(1)
|
const gpuCount = ref(1)
|
||||||
const selectedMachineId = ref('')
|
const selectedMachineId = ref('')
|
||||||
const needExpand = ref(false)
|
|
||||||
const expandSize = ref(100)
|
|
||||||
const imageType = ref('platformImage')
|
|
||||||
const selectedImage = ref<string[]>([])
|
|
||||||
const selectedCoupon = ref('')
|
|
||||||
const tableLoading = ref(false)
|
const tableLoading = ref(false)
|
||||||
const creating = ref(false)
|
|
||||||
|
|
||||||
// 分页相关
|
// 分页相关
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
|
getHostList()
|
||||||
// 所有机器数据
|
getAreaList()
|
||||||
const allMachineList = ref<Machine[]>([
|
getGpuList()
|
||||||
{
|
async function getHostList() {
|
||||||
id: '5fb545beac',
|
tableLoading.value = true
|
||||||
machineName: '336机',
|
try {
|
||||||
gpuModel: 'vGPU-48GB',
|
const params = {
|
||||||
gpuMemory: '48 GB',
|
page_num: currentPage.value,
|
||||||
freeGpu: 1,
|
page_size: pageSize.value,
|
||||||
totalGpu: 10,
|
billingMethod: billingType.value,
|
||||||
cpuCores: 20,
|
centerId: selectedRegion.value,
|
||||||
memory: 90,
|
gpuType: selectedGpuModels.value == '全部' ? [] : selectedGpuModels.value,
|
||||||
cpuModel: 'Xeon(R) Platinum 8470Q',
|
gpuNum: gpuCount.value
|
||||||
dataDisk: 50,
|
|
||||||
expandableDisk: 110,
|
|
||||||
driver: '580.76.05',
|
|
||||||
cuda: '≤ 13.0',
|
|
||||||
basePrice: 2.98,
|
|
||||||
originalPrice: 3.14,
|
|
||||||
region: 'beijingDC2',
|
|
||||||
zone: 'beijingB',
|
|
||||||
availableGpuCount: 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '1b0d49b68a',
|
|
||||||
machineName: '494机',
|
|
||||||
gpuModel: 'RTX 5090',
|
|
||||||
gpuMemory: '32GB',
|
|
||||||
freeGpu: 6,
|
|
||||||
totalGpu: 8,
|
|
||||||
cpuCores: 25,
|
|
||||||
memory: 90,
|
|
||||||
cpuModel: 'Xeon(R) Platinum 8470Q',
|
|
||||||
dataDisk: 50,
|
|
||||||
expandableDisk: 7500,
|
|
||||||
driver: '580.76.05',
|
|
||||||
cuda: '13.0',
|
|
||||||
basePrice: 437.00,
|
|
||||||
originalPrice: 460.00,
|
|
||||||
region: 'beijingDC2',
|
|
||||||
zone: 'beijingDC3',
|
|
||||||
availableGpuCount: 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '63af4ab7db',
|
|
||||||
machineName: '518机',
|
|
||||||
gpuModel: 'RTX 5090',
|
|
||||||
gpuMemory: '32GB',
|
|
||||||
freeGpu: 6,
|
|
||||||
totalGpu: 8,
|
|
||||||
cpuCores: 25,
|
|
||||||
memory: 90,
|
|
||||||
cpuModel: 'Xeon(R) Platinum 8470Q',
|
|
||||||
dataDisk: 50,
|
|
||||||
expandableDisk: 7105,
|
|
||||||
driver: '580.76.05',
|
|
||||||
cuda: '13.0',
|
|
||||||
basePrice: 437.00,
|
|
||||||
originalPrice: 460.00,
|
|
||||||
region: 'beijingDC2',
|
|
||||||
zone: 'beijingDC4',
|
|
||||||
availableGpuCount: 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ff5d489b4f',
|
|
||||||
machineName: '495机',
|
|
||||||
gpuModel: 'RTX 5090',
|
|
||||||
gpuMemory: '32GB',
|
|
||||||
freeGpu: 6,
|
|
||||||
totalGpu: 8,
|
|
||||||
cpuCores: 25,
|
|
||||||
memory: 90,
|
|
||||||
cpuModel: 'Xeon(R) Platinum 8470Q',
|
|
||||||
dataDisk: 50,
|
|
||||||
expandableDisk: 6700,
|
|
||||||
driver: '580.76.05',
|
|
||||||
cuda: '13.0',
|
|
||||||
basePrice: 437.00,
|
|
||||||
originalPrice: 460.00,
|
|
||||||
region: 'beijingDC2',
|
|
||||||
zone: 'beijingDC3',
|
|
||||||
availableGpuCount: 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'a1b2c3d4e5',
|
|
||||||
machineName: '600机',
|
|
||||||
gpuModel: 'RTX PRO 6000',
|
|
||||||
gpuMemory: '48GB',
|
|
||||||
freeGpu: 4,
|
|
||||||
totalGpu: 8,
|
|
||||||
cpuCores: 32,
|
|
||||||
memory: 128,
|
|
||||||
cpuModel: 'Xeon(R) Platinum 8470Q',
|
|
||||||
dataDisk: 100,
|
|
||||||
expandableDisk: 8000,
|
|
||||||
driver: '580.76.05',
|
|
||||||
cuda: '13.0',
|
|
||||||
basePrice: 589.00,
|
|
||||||
region: 'westDC3',
|
|
||||||
zone: 'beijingDC4',
|
|
||||||
availableGpuCount: 8
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'f6g7h8i9j0',
|
|
||||||
machineName: '601机',
|
|
||||||
gpuModel: 'vGPU-48GB',
|
|
||||||
gpuMemory: '48GB',
|
|
||||||
freeGpu: 8,
|
|
||||||
totalGpu: 12,
|
|
||||||
cpuCores: 48,
|
|
||||||
memory: 192,
|
|
||||||
cpuModel: 'Xeon(R) Platinum 8470Q',
|
|
||||||
dataDisk: 150,
|
|
||||||
expandableDisk: 10000,
|
|
||||||
driver: '580.76.05',
|
|
||||||
cuda: '13.0',
|
|
||||||
basePrice: 689.00,
|
|
||||||
region: 'chongqingDC1',
|
|
||||||
zone: 'neimengDC2',
|
|
||||||
availableGpuCount: 12
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'k1l2m3n4o5',
|
|
||||||
machineName: '602机',
|
|
||||||
gpuModel: 'RTX 4090',
|
|
||||||
gpuMemory: '24GB',
|
|
||||||
freeGpu: 3,
|
|
||||||
totalGpu: 8,
|
|
||||||
cpuCores: 20,
|
|
||||||
memory: 80,
|
|
||||||
cpuModel: 'Xeon(R) Platinum 8470Q',
|
|
||||||
dataDisk: 50,
|
|
||||||
expandableDisk: 5000,
|
|
||||||
driver: '580.76.05',
|
|
||||||
cuda: '13.0',
|
|
||||||
basePrice: 389.00,
|
|
||||||
originalPrice: 410.00,
|
|
||||||
region: 'beijingDC1',
|
|
||||||
zone: 'shanghaiDC1',
|
|
||||||
availableGpuCount: 8
|
|
||||||
}
|
}
|
||||||
])
|
const res: any = await hostList(params)
|
||||||
|
console.log('机器列表:', res)
|
||||||
// GPU型号配置
|
allMachineList.value = res.data || []
|
||||||
const gpuModelConfig = ref<GpuModelOption[]>([
|
hostTotal.value = res.total || 0
|
||||||
{ value: 'RTX 5090', label: 'RTX 5090', available: 685, total: 3280, regions: ['beijingDC2', 'beijingDC1', 'chongqingDC1'] },
|
} catch (error) {
|
||||||
{ value: 'RTX PRO 6000', label: 'RTX PRO 6000', available: 0, total: 8, regions: ['westDC3'] },
|
message.error('获取机器列表失败')
|
||||||
{ value: 'vGPU-48GB', label: 'vGPU-48GB', available: 31, total: 250, regions: ['chongqingDC1', 'neimengDC3'] },
|
} finally {
|
||||||
{ value: 'vGPU-48GB-425W', label: 'vGPU-48GB-425W', available: 58, total: 160, regions: ['neimengDC3'] },
|
tableLoading.value = false
|
||||||
{ value: 'RTX 5090 D', label: 'RTX 5090 D', available: 1, total: 11, regions: ['beijingDC2'] },
|
}
|
||||||
{ value: 'RTX 4090', label: 'RTX 4090', available: 3, total: 1000, regions: ['beijingDC1', 'foshanDC1'] },
|
}
|
||||||
{ value: 'CPU', label: 'CPU', available: 0, total: 44, regions: ['beijingDC2', 'beijingDC1'] }
|
async function getAreaList() {
|
||||||
])
|
try {
|
||||||
|
const res: any = await getAreaListApi()
|
||||||
|
areaList.value = res || []
|
||||||
|
selectedRegion.value = areaList.value.length > 0 ? areaList.value[0].center_id : ''
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取地区列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function getGpuList() {
|
||||||
|
try {
|
||||||
|
const res: any = await getGpuListApi()
|
||||||
|
gpuList.value = res || []
|
||||||
|
gpuList.value.unshift({ gpuType: '全部' })
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取GPU列表失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 所有机器数据
|
||||||
|
const allMachineList = ref<any[]>([])
|
||||||
|
|
||||||
// 配置数据
|
// 配置数据
|
||||||
const billingTypeOptions = ref([
|
const billingTypeOptions = ref([
|
||||||
{ value: 'payg', label: '按量计费' },
|
{ value: 'PayOnTime', label: '按量计费' },
|
||||||
{ value: 'daily', label: '包日' },
|
{ value: 'PayOnDay', label: '包日' },
|
||||||
{ value: 'weekly', label: '包周' },
|
{ value: 'PayOnWeek', label: '包周' },
|
||||||
{ value: 'monthly', label: '包月' },
|
{ value: 'PayOnMonth', label: '包月' },
|
||||||
{ value: 'year', label: '包年' }
|
{ value: 'PayOnYear', label: '包年' }
|
||||||
])
|
|
||||||
|
|
||||||
const regions = ref<RegionOption[]>([
|
|
||||||
{ value: 'beijingDC2', label: '北京B区' },
|
|
||||||
{ value: 'westDC3', label: '西北B区' },
|
|
||||||
{ value: 'chongqingDC1', label: '重庆A区' },
|
|
||||||
{ value: 'neimengDC3', label: '内蒙B区' },
|
|
||||||
{ value: 'beijingDC1', label: '北京A区' },
|
|
||||||
{ value: 'foshanDC1', label: '佛山区' }
|
|
||||||
])
|
|
||||||
|
|
||||||
const zones = ref<ZoneOption[]>([
|
|
||||||
{ value: 'beijingB', label: '北京B区', regions: ['beijingDC2'] },
|
|
||||||
{ value: 'beijingDC4', label: 'L20专区', regions: ['beijingDC2'] },
|
|
||||||
{ value: 'beijingDC3', label: 'V100专区', regions: ['beijingDC2'] },
|
|
||||||
{ value: 'neimengDC2', label: 'A800专区', regions: ['neimengDC3'] },
|
|
||||||
{ value: 'shanghaiDC1', label: '摩尔线程专区', regions: ['beijingDC1'] },
|
|
||||||
{ value: 'guangdongDC1', label: '华为昇腾专区', regions: ['foshanDC1'] }
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const gpuCountOptions = [1, 2, 3, 4, 5, 6, 7, 8, 10, 12]
|
const gpuCountOptions = [1, 2, 3, 4, 5, 6, 7, 8, 10, 12]
|
||||||
|
|
||||||
// 优惠券数据
|
|
||||||
const availableCoupons = ref<Coupon[]>([
|
|
||||||
{ id: 'coupon1', name: '新用户优惠券', amount: 50, type: 'discount' },
|
|
||||||
{ id: 'coupon2', name: '周年庆优惠', amount: 100, type: 'discount' }
|
|
||||||
])
|
|
||||||
|
|
||||||
// 镜像选项
|
|
||||||
const imageOptions = ref([
|
|
||||||
{
|
|
||||||
value: 'pytorch',
|
|
||||||
label: 'PyTorch',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
value: '1.12.0',
|
|
||||||
label: '1.12.0',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
value: 'python3.8',
|
|
||||||
label: 'Python 3.8',
|
|
||||||
children: [
|
|
||||||
{ value: 'cuda11.6', label: 'CUDA 11.6' },
|
|
||||||
{ value: 'cuda11.7', label: 'CUDA 11.7' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
// 表格列定义
|
|
||||||
const columns: TableColumnsType = [
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
key: 'action',
|
|
||||||
width: 50
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '主机ID',
|
|
||||||
dataIndex: 'id',
|
|
||||||
key: 'id',
|
|
||||||
width: 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '算力型号/显存',
|
|
||||||
key: 'gpuModel',
|
|
||||||
width: 150
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '空闲GPU',
|
|
||||||
key: 'gpuInfo',
|
|
||||||
width: 100,
|
|
||||||
sorter: (a: Machine, b: Machine) => a.freeGpu - b.freeGpu
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '每GPU分配',
|
|
||||||
key: 'cpuInfo',
|
|
||||||
width: 120
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'CPU型号',
|
|
||||||
dataIndex: 'cpuModel',
|
|
||||||
key: 'cpuModel',
|
|
||||||
width: 180
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '硬盘',
|
|
||||||
key: 'disk',
|
|
||||||
width: 140,
|
|
||||||
sorter: (a: Machine, b: Machine) => a.dataDisk - b.dataDisk
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '驱动/CUDA',
|
|
||||||
key: 'driver',
|
|
||||||
width: 140
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '价格(单卡)',
|
|
||||||
key: 'price',
|
|
||||||
width: 160
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 计算属性
|
|
||||||
const cpuCores = computed(() => {
|
|
||||||
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
|
|
||||||
return machine ? machine.cpuCores * gpuCount.value : 20 * gpuCount.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const memory = computed(() => {
|
|
||||||
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
|
|
||||||
return machine ? machine.memory * gpuCount.value : 90 * gpuCount.value
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计费单位
|
|
||||||
const billingUnit = computed(() => {
|
|
||||||
switch (billingType.value) {
|
|
||||||
case 'payg': return '小时'
|
|
||||||
case 'daily': return '天'
|
|
||||||
case 'weekly': return '周'
|
|
||||||
case 'monthly': return '月'
|
|
||||||
default: return '周'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 可用的GPU型号(根据地区筛选)
|
|
||||||
const availableGpuModels = computed(() => {
|
|
||||||
return gpuModelConfig.value.filter(model =>
|
|
||||||
model.regions.includes(selectedRegion.value)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 选中的GPU型号标签
|
|
||||||
const selectedGpuModelLabel = computed(() => {
|
|
||||||
if (selectedGpuModels.value.length === 0) return '未选择'
|
|
||||||
const model = gpuModelConfig.value.find(m => m.value === selectedGpuModels.value[0])
|
|
||||||
return model ? model.label : selectedGpuModels.value[0]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 过滤后的机器列表
|
|
||||||
const filteredMachineList = computed(() => {
|
|
||||||
let filtered = allMachineList.value.filter(machine => {
|
|
||||||
// 按地区筛选
|
|
||||||
if (machine.region !== selectedRegion.value) return false
|
|
||||||
|
|
||||||
// 按专区筛选(如果有选择专区)
|
|
||||||
if (selectedZone.value && machine.zone !== selectedZone.value) return false
|
|
||||||
|
|
||||||
// 按GPU型号筛选
|
|
||||||
if (selectedGpuModels.value.length > 0 && !selectedGpuModels.value.includes('all')) {
|
|
||||||
if (!selectedGpuModels.value.includes(machine.gpuModel)) return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按GPU数量筛选:机器的可用GPU数量要大于等于选择的GPU数量
|
|
||||||
if (machine.availableGpuCount < gpuCount.value) return false
|
|
||||||
|
|
||||||
// 确保有足够的空闲GPU
|
|
||||||
if (machine.freeGpu < gpuCount.value) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
return filtered
|
|
||||||
})
|
|
||||||
|
|
||||||
// 分页后的机器列表
|
|
||||||
const paginatedMachineList = computed(() => {
|
|
||||||
const start = (currentPage.value - 1) * pageSize.value
|
|
||||||
const end = start + pageSize.value
|
|
||||||
return filteredMachineList.value.slice(start, end)
|
|
||||||
})
|
|
||||||
|
|
||||||
const showZones = computed(() => {
|
|
||||||
return availableZones.value.length > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const availableZones = computed(() => {
|
|
||||||
return zones.value.filter(zone => zone.regions.includes(selectedRegion.value))
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalDiskSize = computed(() => {
|
|
||||||
return needExpand.value ? 50 + expandSize.value : 50
|
|
||||||
})
|
|
||||||
|
|
||||||
const maxExpandSize = computed(() => {
|
|
||||||
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
|
|
||||||
return machine ? machine.expandableDisk : 0
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalPrice = computed(() => {
|
|
||||||
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
|
|
||||||
if (!machine) return '0.00'
|
|
||||||
|
|
||||||
let price = machine.basePrice * gpuCount.value
|
|
||||||
|
|
||||||
// 硬盘扩容费用
|
|
||||||
if (needExpand.value && expandSize.value > 0) {
|
|
||||||
const diskPricePerGB = 0.1 // 假设每GB 0.1元/周
|
|
||||||
price += diskPricePerGB * expandSize.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 优惠券折扣
|
|
||||||
if (selectedCoupon.value) {
|
|
||||||
const coupon = availableCoupons.value.find(c => c.id === selectedCoupon.value)
|
|
||||||
if (coupon) {
|
|
||||||
price = Math.max(0, price - coupon.amount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计费方式转换
|
|
||||||
if (billingType.value === 'payg') price = price / (7 * 24)
|
|
||||||
else if (billingType.value === 'daily') price = price / 7
|
|
||||||
else if (billingType.value === 'monthly') price = price * 4
|
|
||||||
|
|
||||||
return price.toFixed(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
const originalTotalPrice = computed(() => {
|
|
||||||
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
|
|
||||||
if (!machine || !machine.originalPrice) return ''
|
|
||||||
|
|
||||||
let price = machine.originalPrice * gpuCount.value
|
|
||||||
if (billingType.value === 'payg') price = price / (7 * 24)
|
|
||||||
else if (billingType.value === 'daily') price = price / 7
|
|
||||||
else if (billingType.value === 'monthly') price = price * 4
|
|
||||||
|
|
||||||
return price.toFixed(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
const canCreate = computed(() => {
|
|
||||||
return selectedMachineId.value && selectedImage.value.length > 0
|
|
||||||
})
|
|
||||||
|
|
||||||
// 价格计算方法
|
|
||||||
const getPrice = (record: Machine) => {
|
|
||||||
const basePrice = record.basePrice
|
|
||||||
switch (billingType.value) {
|
|
||||||
case 'payg': return (basePrice / (7 * 24)).toFixed(2)
|
|
||||||
case 'daily': return (basePrice / 7).toFixed(2)
|
|
||||||
case 'weekly': return basePrice.toFixed(2)
|
|
||||||
case 'monthly': return (basePrice * 4).toFixed(2)
|
|
||||||
default: return basePrice.toFixed(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getOriginalPrice = (record: Machine) => {
|
|
||||||
if (!record.originalPrice) return ''
|
|
||||||
const originalPrice = record.originalPrice
|
|
||||||
switch (billingType.value) {
|
|
||||||
case 'payg': return (originalPrice / (7 * 24)).toFixed(2)
|
|
||||||
case 'daily': return (originalPrice / 7).toFixed(2)
|
|
||||||
case 'weekly': return originalPrice.toFixed(2)
|
|
||||||
case 'monthly': return (originalPrice * 4).toFixed(2)
|
|
||||||
default: return originalPrice.toFixed(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 价格明细计算
|
|
||||||
const gpuPrice = computed(() => {
|
|
||||||
const machine = allMachineList.value.find(m => m.id === selectedMachineId.value)
|
|
||||||
if (!machine) return '0.00'
|
|
||||||
|
|
||||||
let price = machine.basePrice * gpuCount.value
|
|
||||||
if (billingType.value === 'payg') price = price / (7 * 24)
|
|
||||||
else if (billingType.value === 'daily') price = price / 7
|
|
||||||
else if (billingType.value === 'monthly') price = price * 4
|
|
||||||
|
|
||||||
return price.toFixed(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
const diskPrice = computed(() => {
|
|
||||||
if (!needExpand.value || expandSize.value <= 0) return '0.00'
|
|
||||||
// 假设每GB硬盘费用为0.1元/周
|
|
||||||
const pricePerGB = 0.1
|
|
||||||
let price = pricePerGB * expandSize.value
|
|
||||||
if (billingType.value === 'payg') price = price / (7 * 24)
|
|
||||||
else if (billingType.value === 'daily') price = price / 7
|
|
||||||
else if (billingType.value === 'monthly') price = price * 4
|
|
||||||
|
|
||||||
return price.toFixed(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
const couponDiscount = computed(() => {
|
|
||||||
if (!selectedCoupon.value) return '0.00'
|
|
||||||
const coupon = availableCoupons.value.find(c => c.id === selectedCoupon.value)
|
|
||||||
if (!coupon) return '0.00'
|
|
||||||
|
|
||||||
let discount = coupon.amount
|
|
||||||
if (billingType.value === 'payg') discount = discount / (7 * 24)
|
|
||||||
else if (billingType.value === 'daily') discount = discount / 7
|
|
||||||
else if (billingType.value === 'monthly') discount = discount * 4
|
|
||||||
|
|
||||||
return discount.toFixed(2)
|
|
||||||
})
|
|
||||||
|
|
||||||
const showPriceBreakdown = computed(() => {
|
|
||||||
return diskPrice.value !== '0.00' || couponDiscount.value !== '0.00'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 方法
|
|
||||||
const handleRowClick = (record: Machine) => {
|
|
||||||
selectedMachineId.value = record.id
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectMachine = (id: string) => {
|
const handleSelectMachine = (id: string) => {
|
||||||
selectedMachineId.value = id
|
selectedMachineId.value = id
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRowClassName = (record: Machine) => {
|
|
||||||
return selectedMachineId.value === record.id ? 'selected-row' : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 一卡可租
|
// 一卡可租
|
||||||
const handleRentMachine = (id: string) => {
|
const handleRentMachine = (id: string) => {
|
||||||
router.push("/layout/create")
|
router.push("/layout/create")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBillingTypeChange = () => {
|
|
||||||
console.log('计费方式改变:', billingType.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRegionChange = () => {
|
|
||||||
console.log('地区改变:', selectedRegion.value)
|
|
||||||
if (availableGpuModels.value.length > 0) {
|
|
||||||
selectedGpuModels.value = [availableGpuModels.value[0].value]
|
|
||||||
} else {
|
|
||||||
selectedGpuModels.value = []
|
|
||||||
}
|
|
||||||
selectedZone.value = ''
|
|
||||||
selectedMachineId.value = ''
|
|
||||||
currentPage.value = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleZoneChange = () => {
|
|
||||||
console.log('专区改变:', selectedZone.value)
|
|
||||||
selectedMachineId.value = ''
|
|
||||||
currentPage.value = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGpuModelChange = () => {
|
|
||||||
console.log('GPU型号改变:', selectedGpuModels.value)
|
|
||||||
selectedMachineId.value = ''
|
|
||||||
currentPage.value = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGpuCountChange = () => {
|
const handleGpuCountChange = () => {
|
||||||
console.log('GPU数量改变:', gpuCount.value)
|
console.log('GPU数量改变:', gpuCount.value)
|
||||||
selectedMachineId.value = ''
|
selectedMachineId.value = ''
|
||||||
currentPage.value = 1
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleImageTypeChange = () => {
|
|
||||||
console.log('镜像类型改变:', imageType.value)
|
|
||||||
selectedImage.value = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页处理方法
|
|
||||||
const handlePageChange = (page: number, pageSize?: number) => {
|
|
||||||
currentPage.value = page
|
|
||||||
if (pageSize) {
|
|
||||||
pageSize.value = pageSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePageSizeChange = (current: number, size: number) => {
|
|
||||||
currentPage.value = 1
|
|
||||||
pageSize.value = size
|
|
||||||
}
|
|
||||||
|
|
||||||
const isGpuCountDisabled = (count: number) => {
|
|
||||||
const machine = filteredMachineList.value.find(m => m.id === selectedMachineId.value)
|
|
||||||
return machine ? count > machine.availableGpuCount : false
|
|
||||||
}
|
|
||||||
|
|
||||||
const filter = (inputValue: string, path: any[]) => {
|
|
||||||
return path.some(option =>
|
|
||||||
option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
creating.value = true
|
|
||||||
try {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
||||||
message.success('实例创建成功!')
|
|
||||||
} catch (error) {
|
|
||||||
message.error('创建失败,请重试')
|
|
||||||
} finally {
|
|
||||||
creating.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
window.history.back()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听选中的机器列表,自动选择第一个可用的机器
|
|
||||||
watch(filteredMachineList, (newList) => {
|
|
||||||
if (newList.length > 0 && !selectedMachineId.value) {
|
|
||||||
selectedMachineId.value = newList[0].id
|
|
||||||
} else if (newList.length === 0) {
|
|
||||||
selectedMachineId.value = ''
|
|
||||||
}
|
|
||||||
// 重置到第一页
|
|
||||||
currentPage.value = 1
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// 监听器
|
|
||||||
watch(needExpand, (newVal) => {
|
|
||||||
if (!newVal) {
|
|
||||||
expandSize.value = 100
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(selectedMachineId, (newId) => {
|
|
||||||
if (newId && needExpand.value) {
|
|
||||||
const machine = allMachineList.value.find(m => m.id === newId)
|
|
||||||
if (machine && expandSize.value > machine.expandableDisk) {
|
|
||||||
expandSize.value = machine.expandableDisk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 初始化时设置默认的GPU型号
|
|
||||||
if (availableGpuModels.value.length > 0) {
|
|
||||||
selectedGpuModels.value = [availableGpuModels.value[0].value]
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@ -780,7 +211,7 @@ if (availableGpuModels.value.length > 0) {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* 关键:确保它能自然撑高 */
|
/* 关键:确保它能自然撑高 */
|
||||||
min-height: fit-content;
|
min-height: fit-content;
|
||||||
height: auto;
|
height: calc(100vh - 60px);
|
||||||
background: url('@/assets/bgImg.png') no-repeat center / 100% 100%;
|
background: url('@/assets/bgImg.png') no-repeat center / 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -788,7 +219,7 @@ if (availableGpuModels.value.length > 0) {
|
|||||||
width: 60%;
|
width: 60%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 0 20px 0;
|
padding: 30px 0 20px 0;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
@ -1536,11 +967,11 @@ if (availableGpuModels.value.length > 0) {
|
|||||||
.info-item {
|
.info-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.info-item-name{
|
.info-item-name {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item-account{
|
.info-item-account {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,5 +19,12 @@ export default defineConfig({
|
|||||||
port: 8080, // 设置开发服务器端口为 8080
|
port: 8080, // 设置开发服务器端口为 8080
|
||||||
host: true,
|
host: true,
|
||||||
open: true, // 可选:启动时自动打开浏览器
|
open: true, // 可选:启动时自动打开浏览器
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://10.10.1.34:8888', // 目标服务器的地址
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user