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