Commit 3499d263 by renyizhao

分档计费与大屏

parent 493a6554
import request from '@/config/axios'
export interface AiTokenStatsVO {
statsCount: number
statsQuota: number
statsTokens: number
}
export const AiTokenStatsApi = {
getUserStats: async () => {
return request.get<AiTokenStatsVO>({ url: '/admin-api/apihub/ai-token-stats/user-stats' })
}
}
......@@ -4,8 +4,39 @@
<button :class="{ active: dim === 'gpu' }" @click="setDim('gpu')">GPU型号</button>
<button :class="{ active: dim === 'source' }" @click="setDim('source')">算力来源</button>
<button :class="{ active: dim === 'resource' }" @click="setDim('resource')">计算资源</button>
<button :class="{ active: dim === 'AIToken' }" @click="setDim('AIToken')">token总览</button>
</div>
<div ref="chartRef" class="echart-wrap"></div>
<div v-show="dim === 'AIToken'" class="ai-token-data">
<div v-if="aiTokenStats" class="ai-token-stats">
<div class="stat-row stat-row-center">
<div class="stat-item">
<img src="@/assets/images/statistical-icon1.png" />
<div class="stat-content">
<div class="stat-label">统计次数</div>
<div class="stat-num">{{ aiTokenStats.statsCount }}</div>
</div>
</div>
<div class="stat-item">
<img src="@/assets/images/statistical-icon4.png" />
<div class="stat-content">
<div class="stat-label">统计额度</div>
<div class="stat-num">¥{{ aiTokenStats.statsQuota }}</div>
</div>
</div>
</div>
<div class="stat-row stat-row-left">
<div class="stat-item">
<img src="@/assets/images/statistical-icon2.png" />
<div class="stat-content">
<div class="stat-label">统计Tokens</div>
<div class="stat-num">{{ aiTokenStats.statsTokens }}</div>
</div>
</div>
</div>
</div>
<div v-else class="loading">加载中...</div>
</div>
<div v-show="dim !== 'AIToken'" ref="chartRef" class="echart-wrap"></div>
</div>
</template>
......@@ -16,6 +47,7 @@ import * as echarts from 'echarts/core'
import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { PieChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
import { AiTokenStatsApi } from '@/api/apihub/aiTokenStats'
echarts.use([TitleComponent, TooltipComponent, LegendComponent, PieChart, CanvasRenderer])
......@@ -33,12 +65,27 @@ const datasets = ref({
resource: []
})
// AI Token 统计数据
const aiTokenStats = ref(null)
// 获取 AI Token 统计数据
const fetchAiTokenStats = async () => {
try {
const res = await AiTokenStatsApi.getUserStats()
if (res) {
aiTokenStats.value = res
}
} catch (e) {
console.error('获取AI Token统计失败', e)
}
}
// 获取算力资源分布数据
const fetchComputeDistribution = () => {
const data = dashboardData.value.computeDistribution || { gpu: [], source: [], resource: [] }
datasets.value.gpu = data.gpu || []
// 转换算力来源key为中文标签
datasets.value.source = (data.source || []).map(item => ({
datasets.value.source = (data.source || []).map((item) => ({
name: getDictLabel(DICT_TYPE.COMPUTE_RESOURCE_SOURCE, item.name) || item.name,
value: item.value
}))
......@@ -136,10 +183,13 @@ const resize = () => chart && chart.resize()
const setDim = (which) => {
if (dim.value === which) return
dim.value = which
if (which === 'AIToken') {
fetchAiTokenStats()
}
}
watch(dim, () => {
if (chart) chart.setOption(getOption(dim.value), true)
if (chart && dim.value !== 'AIToken') chart.setOption(getOption(dim.value), true)
})
onMounted(() => {
......@@ -149,12 +199,16 @@ onMounted(() => {
})
// 监听数据变化
watch(() => dashboardData.value.computeDistribution, () => {
fetchComputeDistribution()
if (chart) {
chart.setOption(getOption(dim.value), true)
}
}, { deep: true })
watch(
() => dashboardData.value.computeDistribution,
() => {
fetchComputeDistribution()
if (chart) {
chart.setOption(getOption(dim.value), true)
}
},
{ deep: true }
)
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
......@@ -179,14 +233,14 @@ onBeforeUnmount(() => {
.dim-switch button {
font-size: 26px;
color: #ffffff;
border: 1px solid #39E9D5;
border: 1px solid #39e9d5;
background: transparent;
padding: 8px 18px;
margin-left: 10px;
cursor: pointer;
}
.dim-switch button.active {
background: linear-gradient(to right, #13656A, #26A9A3, #13656A);
background: linear-gradient(to right, #13656a, #26a9a3, #13656a);
}
.echart-wrap {
......@@ -195,4 +249,69 @@ onBeforeUnmount(() => {
padding-top: 100px;
box-sizing: border-box;
}
.ai-token-data {
height: 640px;
padding-top: 100px;
box-sizing: border-box;
display: flex;
align-items: flex-start;
justify-content: center;
}
.ai-token-stats {
display: flex;
flex-direction: column;
gap: 60px;
margin-top: 100px;
padding: 0 70px;
.stat-row {
display: flex;
gap: 100px;
}
.stat-row-center {
justify-content: center;
}
.stat-row-left {
justify-content: flex-start;
}
.stat-item {
display: flex;
align-items: center;
gap: 30px;
img {
width: 160px;
}
.stat-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.stat-label {
font-size: 36px;
font-weight: bold;
color: #ffffff;
}
.stat-num {
font-size: 64px;
font-family: DIN;
font-weight: bold;
color: #16fcff;
text-shadow: 0 0 30px rgba(22, 252, 255, 0.8);
}
}
}
.loading {
font-size: 24px;
color: #39e9d5;
}
</style>
......@@ -16,7 +16,8 @@
<span class="label" style="margin-left: 20px">算力总规模(TOPS)</span>
<span class="value">
{{ dashboardData.overallSituation.allCompute }}
TOPS</span>
TOPS</span
>
</div>
<div class="statistical">
......@@ -26,7 +27,7 @@
<div class="label">已租赁算力(TOPS)</div>
<div class="value">
{{ dashboardData.overallSituation.leaseCompute }} TOPS
<!-- 139.94
<!-- 139.94
<animation-count :end-val="53632" decimals :range-min="0.01" :range-max="0.02" />-->
</div>
</div>
......@@ -35,23 +36,23 @@
<i></i>
<div>
<div class="label">闲置算力</div>
<div class="value">
{{ dashboardData.overallSituation.idleCompute }} TOPS
</div>
<div class="value"> {{ dashboardData.overallSituation.idleCompute }} TOPS </div>
</div>
</div>
<div class="statistical-item">
<i></i>
<div>
<div class="label">算力利用率</div>
<div class="value">{{ dashboardData.overallSituation.computeUtilizationRate }}%</div>
<!-- 42.37%-->
<div class="value"
>{{ dashboardData.overallSituation.computeUtilizationRate }}%</div
>
<!-- 42.37%-->
</div>
</div>
</div>
</div>
<div class="wrap environmental-contribution">
<div class="header-title">算力资源结构</div>
<div class="header-title">算力资源</div>
<EnvironmentalContribution />
</div>
<div class="wrap energy-manage">
......@@ -60,8 +61,6 @@
</div>
</div>
<div class="center">
<el-carousel indicator-position="none" arrow="never">
<el-carousel-item v-for="(item, index) in dashboardData.carouselItems" :key="index">
......@@ -102,14 +101,14 @@
<ChinaMap v-if="showMap === 'china'" :key="'china'" />
<WorldMap v-else :key="'world'" />
<!-- <div class="switch-button">-->
<!-- <button :class="{ active: showMap === 'china' }" @click="showMap = 'china'">-->
<!-- <div>中国地图</div>-->
<!-- </button>-->
<!-- <button :class="{ active: showMap === 'world' }" @click="showMap = 'world'">-->
<!-- <div>世界地图</div>-->
<!-- </button>-->
<!-- </div>-->
<!-- <div class="switch-button">-->
<!-- <button :class="{ active: showMap === 'china' }" @click="showMap = 'china'">-->
<!-- <div>中国地图</div>-->
<!-- </button>-->
<!-- <button :class="{ active: showMap === 'world' }" @click="showMap = 'world'">-->
<!-- <div>世界地图</div>-->
<!-- </button>-->
<!-- </div>-->
</div>
<div class="right">
......@@ -133,7 +132,6 @@
</div>
</div>
</div>
</div>
</template>
......@@ -183,12 +181,12 @@ const fetchAllMockData = async () => {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res && res.list) {
// 先获取开关状态
const switchConfig = res.list.find(item => item.configKey === 'use_mock_data')
const switchConfig = res.list.find((item) => item.configKey === 'use_mock_data')
const useMock = switchConfig?.configValue === 'true'
if (useMock) {
// 使用模拟数据
res.list.forEach(item => {
res.list.forEach((item) => {
if (item.configKey && item.configValue) {
try {
const data = JSON.parse(item.configValue)
......@@ -327,7 +325,9 @@ const calcScale = () => {
// 在 iframe / 去壳模式下,使用 cover 填充,避免出现留白
// 同时在进入全屏时也采用 cover,以充分利用屏幕空间
const useCover = fixedViewport.value || isFullscreen.value
const s = useCover ? Math.max(w / BASE_WIDTH, h / BASE_HEIGHT) : Math.min(w / BASE_WIDTH, h / BASE_HEIGHT)
const s = useCover
? Math.max(w / BASE_WIDTH, h / BASE_HEIGHT)
: Math.min(w / BASE_WIDTH, h / BASE_HEIGHT)
scale.value = s
// 用负偏移实现居中裁剪,彻底铺满容器
offsetX.value = (w - BASE_WIDTH * s) / 2
......@@ -462,8 +462,9 @@ provide('fsState', { isFullscreen, toggleFullscreen })
/* 在带有 Layout 外壳的普通页面中,按内容区域计算高度,避免覆盖顶部工具栏/标签/边距 */
.page-adapter.content-fit {
height: calc(
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-content-padding) -
var(--app-content-padding) - var(--app-footer-height, 0px)
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-content-padding) - var(
--app-content-padding
) - var(--app-footer-height, 0px)
);
}
......@@ -479,7 +480,7 @@ provide('fsState', { isFullscreen, toggleFullscreen })
position: relative;
width: 3840px; /* 基于设计稿宽度,保证内层布局比例 */
height: 2160px; /* 基于设计稿高度,保证内层布局比例 */
background: url("@/assets/images/page-bg.png") no-repeat center center / cover;
background: url('@/assets/images/page-bg.png') no-repeat center center / cover;
}
.main {
......@@ -488,7 +489,8 @@ provide('fsState', { isFullscreen, toggleFullscreen })
display: flex;
//pointer-events: none;
.left, .right {
.left,
.right {
width: 970px;
height: 1928px;
//pointer-events: all;
......@@ -506,7 +508,7 @@ provide('fsState', { isFullscreen, toggleFullscreen })
.wrap {
position: relative;
min-height: 500px;
color: #FFFFFF;
color: #ffffff;
}
.statistical {
......@@ -521,15 +523,15 @@ provide('fsState', { isFullscreen, toggleFullscreen })
.label {
font-size: 32px;
font-weight: bold;
color: #FFFFFF;
color: #ffffff;
}
.value {
font-size: 48px;
font-family: DIN;
font-weight: bold;
color: #FFFFFF;
background: linear-gradient(0deg, #16FCFF 0%, #FFFFFF 100%);
color: #ffffff;
background: linear-gradient(0deg, #16fcff 0%, #ffffff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
......@@ -542,15 +544,15 @@ provide('fsState', { isFullscreen, toggleFullscreen })
.label {
font-size: 32px;
font-weight: bold;
color: #FFFFFF;
color: #ffffff;
}
.value {
font-size: 48px;
font-family: DIN;
font-weight: bold;
color: #FFFFFF;
background: linear-gradient(0deg, #16FCFF 0%, #FFFFFF 100%);
color: #ffffff;
background: linear-gradient(0deg, #16fcff 0%, #ffffff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
......@@ -559,7 +561,7 @@ provide('fsState', { isFullscreen, toggleFullscreen })
display: flex;
align-items: center;
padding: 35px 40px;
background: url("@/assets/images/cumulative-delivery-bg.png") no-repeat center center / cover;
background: url('@/assets/images/cumulative-delivery-bg.png') no-repeat center center / cover;
img {
width: 120px;
......@@ -575,13 +577,14 @@ provide('fsState', { isFullscreen, toggleFullscreen })
i {
width: 60px;
height: 60px;
background: url("@/assets/images/round-icon.png") no-repeat center center / 100%;
background: url('@/assets/images/round-icon.png') no-repeat center center / 100%;
}
}
}
}
.environmental-contribution, .energy-manage {
.environmental-contribution,
.energy-manage {
margin-bottom: 50px;
.header-title {
......@@ -592,7 +595,7 @@ provide('fsState', { isFullscreen, toggleFullscreen })
.center {
position: relative;
padding-top: 60px;
background: url("@/assets/images/center-bg.png") no-repeat center 1200px / 100%;
background: url('@/assets/images/center-bg.png') no-repeat center 1200px / 100%;
min-height: 1300px; /* 确保地图容器有足够显示空间 */
.year-button {
......@@ -605,10 +608,10 @@ provide('fsState', { isFullscreen, toggleFullscreen })
font-style: italic;
padding-left: 80px;
text-align: left;
color: #FFFFFF;
color: #ffffff;
border: none;
outline: none;
background: transparent url("@/assets/images/year-button-bg.png") no-repeat center center / 100%;
background: transparent url('@/assets/images/year-button-bg.png') no-repeat center center / 100%;
}
.statistical {
......@@ -620,7 +623,7 @@ provide('fsState', { isFullscreen, toggleFullscreen })
height: 180px;
padding-top: 25px;
padding-left: 15px;
background: url("@/assets/images/statistical-bg.png") no-repeat center center / 100%;
background: url('@/assets/images/statistical-bg.png') no-repeat center center / 100%;
.label {
font-size: 30px;
......@@ -640,10 +643,10 @@ provide('fsState', { isFullscreen, toggleFullscreen })
button {
font-size: 30px;
font-weight: 500;
color: #FFFFFF;
color: #ffffff;
transform: skewX(-30deg);
padding: 25px 100px;
border: 1px solid #39E9D5;
border: 1px solid #39e9d5;
cursor: pointer;
background-color: rgba(29, 186, 255, 0.1);
......@@ -652,23 +655,22 @@ provide('fsState', { isFullscreen, toggleFullscreen })
}
&.active {
background: linear-gradient(to right, #13656A, #26A9A3, #13656A);
background: linear-gradient(to right, #13656a, #26a9a3, #13656a);
}
div {
transform: skewX(30deg)
transform: skewX(30deg);
}
}
}
}
.innovation {
margin-top: 30px;
.video-box {
height: 490px;
background: url("@/assets/images/video-bg.png") no-repeat center center / 100%;
background: url('@/assets/images/video-bg.png') no-repeat center center / 100%;
padding: 22px 15px;
video {
......@@ -693,12 +695,10 @@ provide('fsState', { isFullscreen, toggleFullscreen })
font-size: 36px;
font-family: DOUYU;
font-weight: normal;
color: #FFFFFF;
color: #ffffff;
text-shadow: 0 0 5px rgba(72, 144, 182, 0.5);
padding-left: 120px;
/* Vite 下不需要 ~,直接使用 @ 别名 */
background: url("@/assets/images/title-icon.png") no-repeat left center / 380px;
background: url('@/assets/images/title-icon.png') no-repeat left center / 380px;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment