Commit 6eb1ff55 by Jony.L

大屏页面和后台管理调整1.0

parent 71ffe7d8
import request from '@/config/axios'
import type { Dayjs } from 'dayjs';
/** 首页大屏模拟数据配置信息 */
export interface HomeDashboardMock {
id: number; // 主键ID
configKey?: string; // 配置key
configType?: string; // 配置类型:switch开关/data数据
configValue: string; // 配置值
description: string; // 配置描述
}
// 首页大屏模拟数据配置 API
export const HomeDashboardMockApi = {
// 查询首页大屏模拟数据配置分页
getHomeDashboardMockPage: async (params: any) => {
return await request.get({ url: `/biz/home-dashboard-mock/page`, params })
},
// 查询首页大屏模拟数据配置详情
getHomeDashboardMock: async (id: number) => {
return await request.get({ url: `/biz/home-dashboard-mock/get?id=` + id })
},
// 新增首页大屏模拟数据配置
createHomeDashboardMock: async (data: HomeDashboardMock) => {
return await request.post({ url: `/biz/home-dashboard-mock/create`, data })
},
// 修改首页大屏模拟数据配置
updateHomeDashboardMock: async (data: HomeDashboardMock) => {
return await request.put({ url: `/biz/home-dashboard-mock/update`, data })
},
// 删除首页大屏模拟数据配置
deleteHomeDashboardMock: async (id: number) => {
return await request.delete({ url: `/biz/home-dashboard-mock/delete?id=` + id })
},
/** 批量删除首页大屏模拟数据配置 */
deleteHomeDashboardMockList: async (ids: number[]) => {
return await request.delete({ url: `/biz/home-dashboard-mock/delete-list?ids=${ids.join(',')}` })
},
// 导出首页大屏模拟数据配置 Excel
exportHomeDashboardMock: async (params) => {
return await request.download({ url: `/biz/home-dashboard-mock/export-excel`, params })
}
}
\ No newline at end of file
......@@ -4,7 +4,7 @@
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
import { onBeforeUnmount, onMounted, ref, nextTick, inject, watch } from 'vue'
import * as echarts from 'echarts/core'
import { TooltipComponent, VisualMapComponent, GeoComponent, GridComponent, LegendComponent } from 'echarts/components'
import {
......@@ -18,6 +18,9 @@ import geoJson from '@/assets/mapJson/china.json'
import labelGreen from '@/assets/images/label-green.png'
import labelRed from '@/assets/images/label-red.png'
// 从父组件获取统一数据
const dashboardData = inject('dashboardData', {})
echarts.use([
GridComponent,
LegendComponent,
......@@ -33,14 +36,40 @@ echarts.use([
])
const geoCoordMap = ref({
浙江省: [120.153576, 30.287459],
上海市: [121.472644, 31.231706],
江苏省: [87.617733, 43.792818],
辽宁省: [123.429096, 41.796767],
湖南省: [112.982279, 28.19409],
北京市: [116.403874, 39.914885],
天津市: [117.190182, 39.125523],
河北省: [114.502462, 38.045494],
山西省: [112.549248, 37.857014],
内蒙古自治区: [111.751990, 40.841470],
辽宁省: [123.429096, 41.796767],
吉林省: [125.324498, 43.886845],
黑龙江省: [126.642464, 45.802755],
上海市: [121.472644, 31.231706],
江苏省: [118.783957, 32.062785],
浙江省: [120.153576, 30.287459],
安徽省: [117.283447, 31.861193],
福建省: [119.306239, 26.075302],
江西省: [115.892151, 28.676461],
山东省: [117.000923, 36.675807],
河南省: [113.625367, 34.746570],
湖北省: [114.305539, 30.593098],
湖南省: [112.982279, 28.194090],
广东省: [113.264385, 23.129110],
广西壮族自治区: [108.320004, 22.824018],
海南省: [110.198289, 20.044008],
重庆市: [106.551559, 29.563010],
四川省: [104.065735, 30.659462],
贵州省: [106.707221, 26.598278],
云南省: [102.712251, 25.040609],
西藏自治区: [91.132212, 29.660359],
陕西省: [108.939838, 34.341275],
甘肃省: [103.823557, 36.058039],
福建省: [119.306239, 26.075302]
青海省: [101.777820, 36.616990],
宁夏回族自治区: [106.278080, 38.466370],
新疆维吾尔自治区: [87.617733, 43.792818],
台湾省: [121.520076, 25.047310],
香港特别行政区: [114.165460, 22.275345],
澳门特别行政区: [113.549148, 22.198755]
})
const mapData = ref([
......@@ -62,28 +91,7 @@ const mapData = ref([
// }
])
const mapData2 = ref([
{
name: '浙江省',
value: 12.4920
},
{
name: '山西省',
value: 1.7948
},
{
name: '江苏省',
value: 19.0543
},
{
name: '福建省',
value: 7.4283
},
{
name: '甘肃省',
value: 3.5283
}
])
const mapData2 = ref([])
const convertData = function (data) {
const res = []
......@@ -174,7 +182,7 @@ function initMapChart (el) {
label: {
show: true,
position: [-25, -80],
width: 360,
width: 420,
height: 72,
color: '#FFFFFF',
backgroundColor: { image: labelGreen },
......@@ -218,8 +226,8 @@ function initMapChart (el) {
symbolSize: 0,
label: {
show: true,
position: [-25, -80],
width: 360,
position: [-40, -80],
width: 500,
height: 72,
color: '#FFFFFF',
backgroundColor: { image: labelRed },
......@@ -228,12 +236,12 @@ function initMapChart (el) {
rich: {
txt: {
align: 'left',
padding: [40, 0, 0, 130],
padding: [40, 60, 0, 130],
fontSize: 22
},
txt2: {
fontSize: 36,
padding: [38, 0, 0, 10]
padding: [38, 20, 0, 10]
},
txt3: {
fontSize: 24,
......@@ -246,7 +254,7 @@ function initMapChart (el) {
data
} = params
if (data && data.value) {
return `{txt|${' 算力资源'}}{txt2|${data.value[2]}}{txt3|${'P'}}`
return `{txt|${' 算力资源'}}{txt2|${data.value[2]}}{txt3|${'PTOPS'}}`
}
return 'data'
}
......@@ -270,6 +278,8 @@ const resize = () => {
onMounted(async () => {
await nextTick()
// 从 dashboardData 初始化地图数据
mapData2.value = dashboardData.value.mapData || []
if (mapRef.value) {
chartInst = initMapChart(mapRef.value)
window.addEventListener('resize', resize)
......@@ -277,6 +287,23 @@ onMounted(async () => {
}
})
// 监听数据变化
watch(() => dashboardData.value.mapData, (newData) => {
mapData2.value = newData || []
if (chartInst) {
chartInst.setOption({
series: [
{
data: convertData(mapData.value)
},
{
data: convertData(mapData2.value)
}
]
})
}
}, { deep: true })
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
document.removeEventListener('fullscreenchange', resize)
......
......@@ -10,7 +10,7 @@
</template>
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { ref, watch, onMounted, onBeforeUnmount, inject } from 'vue'
import * as echarts from 'echarts/core'
import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { PieChart } from 'echarts/charts'
......@@ -23,22 +23,21 @@ let chart = null
const dim = ref('gpu')
const datasets = {
gpu: [
{ value: 45.11, name: '4090' },
{ value: 29.88, name: 'A100' },
{ value: 25, name: 'H100' }
],
source: [
{ value: 50, name: '自有' },
{ value: 30, name: '合作' },
{ value: 20, name: '社会' }
],
resource: [
{ value: 40, name: '裸金属' },
{ value: 35, name: 'VM' },
{ value: 25, name: '容器' }
]
// 从父组件获取统一数据
const dashboardData = inject('dashboardData', {})
const datasets = ref({
gpu: [],
source: [],
resource: []
})
// 获取算力资源分布数据
const fetchComputeDistribution = () => {
const data = dashboardData.value.computeDistribution || { gpu: [], source: [], resource: [] }
datasets.value.gpu = data.gpu || []
datasets.value.source = data.source || []
datasets.value.resource = data.resource || []
}
const titles = {
......@@ -50,7 +49,7 @@ const titles = {
const baseColors = ['#16FCFF', '#39E9D5', '#1DBAFF', '#77CCFF', '#E99102', '#3AEDCE']
const getOption = (which) => {
const data = datasets[which]
const data = datasets.value[which] || []
const total = data.reduce((s, i) => s + Number(i.value || 0), 0)
const dataMap = data.reduce((acc, i) => ((acc[i.name] = Number(i.value || 0)), acc), {})
return {
......@@ -139,10 +138,19 @@ watch(dim, () => {
})
onMounted(() => {
fetchComputeDistribution()
render()
window.addEventListener('resize', resize)
})
// 监听数据变化
watch(() => dashboardData.value.computeDistribution, () => {
fetchComputeDistribution()
if (chart) {
chart.setOption(getOption(dim.value), true)
}
}, { deep: true })
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
if (chart) {
......
......@@ -13,20 +13,21 @@
<div class="header-title">平台总体态势</div>
<div class="cumulative-delivery">
<img src="@/assets/images/cumulative-delivery-icon.png" alt="" />
<span class="label" style="margin-left: 20px">算力总规模(P)</span>
<span class="label" style="margin-left: 20px">算力总规模(PTOPS)</span>
<span class="value">
432.58
P</span>
{{ dashboardData.overallSituation.allCompute }}
PTOPS</span>
</div>
<div class="statistical">
<div class="statistical-item">
<i></i>
<div>
<div class="label">已租赁算力(P)</div>
<div class="label">已租赁算力(PTOPS)</div>
<div class="value">
139.94
<!-- <animation-count :end-val="53632" decimals :range-min="0.01" :range-max="0.02" />-->
{{ dashboardData.overallSituation.leaseCompute }} PTOPS
<!-- 139.94
<animation-count :end-val="53632" decimals :range-min="0.01" :range-max="0.02" />-->
</div>
</div>
</div>
......@@ -34,7 +35,8 @@
<i></i>
<div>
<div class="label">算力利用率</div>
<div class="value">42.37%</div>
<div class="value">{{ dashboardData.overallSituation.computeUtilizationRate }}%</div>
<!-- 42.37%-->
</div>
</div>
<div class="statistical-item">
......@@ -42,8 +44,9 @@
<div>
<div class="label">运行中任务数</div>
<div class="value">
29
<!-- <animation-count :end-val="0.83" decimals :range-min="40" :range-max="42" />-->
{{ dashboardData.overallSituation.runningTaskCount }}
<!-- 29
<animation-count :end-val="0.83" decimals :range-min="40" :range-max="42" />-->
</div>
</div>
</div>
......@@ -63,106 +66,39 @@
<div class="center">
<el-carousel indicator-position="none" arrow="never">
<el-carousel-item>
<button class="year-button" type="button">2025-08</button>
<el-carousel-item v-for="(item, index) in dashboardData.carouselItems" :key="index">
<button class="year-button" type="button">{{ item.yearMonth }}</button>
<div class="statistical">
<div class="statistical-item">
<img src="@/assets/images/statistical-icon1.png" />
<div>
<div class="label">上线应用数</div>
<div class="value">4</div>
<div class="value">{{ item.appOnlineCount }}</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon2.png" />
<div>
<div class="label">可用API</div>
<div class="value">14</div>
<div class="value">{{ item.apiOnline }}</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon3.png" />
<div>
<div class="label">模型服务在线率</div>
<div class="value">12.47%</div>
<div class="value">{{ item.modelOnlineRate }}%</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon4.png" />
<div>
<div class="label">Api调用次数</div>
<div class="value">1202</div>
<div class="value">{{ item.apiCallTotal }}</div>
</div>
</div>
</div>
</el-carousel-item>
<el-carousel-item>
<button class="year-button" type="button">2025-09</button>
<div class="statistical">
<div class="statistical-item">
<img src="@/assets/images/statistical-icon1.png" />
<div>
<div class="label">上线应用数</div>
<div class="value">8</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon2.png" />
<div>
<div class="label">可用API</div>
<div class="value">21个</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon3.png" />
<div>
<div class="label">模型服务在线率</div>
<div class="value">14.11%</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon4.png" />
<div>
<div class="label">Api调用次数</div>
<div class="value">1602 次</div>
</div>
</div>
</div>
</el-carousel-item>
<el-carousel-item>
<button class="year-button" type="button">2025-10</button>
<div class="statistical">
<div class="statistical-item">
<img src="@/assets/images/statistical-icon1.png" />
<div>
<div class="label">上线应用数</div>
<div class="value">12</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon2.png" />
<div>
<div class="label">可用API</div>
<div class="value">24个</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon3.png" />
<div>
<div class="label">模型服务在线率</div>
<div class="value">15.14%</div>
</div>
</div>
<div class="statistical-item">
<img src="@/assets/images/statistical-icon4.png" />
<div>
<div class="label">Api调用次数</div>
<div class="value">3048次</div>
</div>
</div>
</div>
</el-carousel-item>
</el-carousel>
<ChinaMap v-if="showMap === 'china'" :key="'china'" />
......@@ -217,6 +153,113 @@ import ChinaMap from './ChinaMap'
import { onBeforeUnmount, onMounted, ref, computed, provide, watch } from 'vue'
import { useRoute } from 'vue-router'
import WorldMap from './WorldMap'
import { HomeDashboardMockApi } from '@/api/biz/home'
import * as IndexCountApi from '@/api/Home/count'
// 平台总体态势数据
const overallSituation = ref({})
// 统一的大屏数据对象
const dashboardData = ref({
overallSituation: {},
apiCalls: [],
computeDistribution: {
gpu: [],
source: [],
resource: []
},
users: [],
serviceCapability: {
years: [],
appOnline: [],
apiOnline: []
},
carouselItems: [],
orders: [],
mapData: []
})
// 一次性获取所有数据(根据开关决定来源)
const fetchAllMockData = async () => {
try {
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 useMock = switchConfig?.configValue === 'true'
if (useMock) {
// 使用模拟数据
res.list.forEach(item => {
if (item.configKey && item.configValue) {
try {
const data = JSON.parse(item.configValue)
if (item.configKey === 'mock_overall_situation') {
dashboardData.value.overallSituation = data
} else if (item.configKey === 'mock_api_calls') {
dashboardData.value.apiCalls = data
} else if (item.configKey === 'mock_compute_distribution') {
dashboardData.value.computeDistribution = data
} else if (item.configKey === 'mock_users') {
dashboardData.value.users = data
} else if (item.configKey === 'mock_service_capability') {
dashboardData.value.serviceCapability = data
} else if (item.configKey === 'mock_app_and_model') {
dashboardData.value.carouselItems = data
} else if (item.configKey === 'mock_orders') {
dashboardData.value.orders = data
} else if (item.configKey === 'mock_map_data') {
dashboardData.value.mapData = data
}
} catch (e) {
console.error('解析模拟数据失败:', item.configKey, e)
}
}
})
} else {
// 使用真实数据 - 调用真实统计接口
await fetchRealData()
}
}
} catch (error) {
console.error('获取数据失败:', error)
}
}
// 获取真实数据
const fetchRealData = async () => {
try {
// 并行调用所有真实数据接口
const [topBarRes, apiCallsRes, usersRes] = await Promise.all([
IndexCountApi.getTopBarData(),
IndexCountApi.getApiCallsData('m'),
IndexCountApi.getUsersData('d')
])
// 总体态势数据
if (topBarRes) {
dashboardData.value.overallSituation = topBarRes
}
// API 调用趋势
if (Array.isArray(apiCallsRes)) {
dashboardData.value.apiCalls = apiCallsRes
}
// 用户数据
if (Array.isArray(usersRes)) {
dashboardData.value.users = usersRes
}
// TODO: 算力分布和服务能力数据的真实接口待实现
} catch (error) {
console.error('获取真实数据失败:', error)
}
}
// 提供数据给子组件
provide('dashboardData', dashboardData)
// 记住地图选择,避免切换/刷新或全屏导致回到默认值
const MAP_TYPE_KEY = 'home_show_map_type'
......@@ -359,6 +402,8 @@ onMounted(() => {
document.addEventListener('mozfullscreenchange', handleFsChange)
// @ts-ignore
document.addEventListener('MSFullscreenChange', handleFsChange)
// 获取所有模拟数据
fetchAllMockData()
})
onBeforeUnmount(() => {
......
......@@ -2,7 +2,7 @@
<div id="nvestment" class="echart-wrap"></div>
</template>
<script setup>
import { onMounted } from 'vue'
import { onMounted, inject, watch } from 'vue'
import * as echarts from 'echarts/core'
import {
TitleComponent,
......@@ -27,9 +27,23 @@ echarts.use([
UniversalTransition
])
// 从父组件获取统一数据
const dashboardData = inject('dashboardData', {})
let myChart = null
function init () {
const chartDom = document.getElementById('nvestment')
const myChart = echarts.init(chartDom)
if (!myChart) {
myChart = echarts.init(chartDom)
}
// 从 dashboardData 获取服务能力数据
const serviceCapability = dashboardData.value.serviceCapability || {}
const years = serviceCapability.years || []
const appOnline = serviceCapability.appOnline || []
const apiOnline = serviceCapability.apiOnline || []
const option = {
tooltip: {
trigger: 'axis',
......@@ -41,7 +55,7 @@ function init () {
}
},
legend: {
data: ['水能消耗', '电能消耗'],
data: ['上线应用', '上线API'],
show: false,
right: '2%',
top: 30,
......@@ -63,7 +77,7 @@ function init () {
{
type: 'category',
boundaryGap: true,
data: ['2018', '2019', '2020', '2021', '2022'],
data: years,
axisLabel: {
fontSize: 24,
color: '#ffffff'
......@@ -133,7 +147,7 @@ function init () {
}
])
},
data: [10.19, 189.61, 120.4, 75.14, '']
data: appOnline
},
{
name: '上线API',
......@@ -150,7 +164,7 @@ function init () {
emphasis: {
// focus: 'series'
},
data: [23.22, 12027.48, 75935.47, 195109.5381, '']
data: apiOnline
}
]
}
......@@ -160,6 +174,11 @@ function init () {
onMounted(() => {
init()
})
// 监听数据变化
watch(() => dashboardData.value.serviceCapability, () => {
init()
}, { deep: true })
</script>
<style scoped lang="scss">
......
......@@ -29,7 +29,7 @@
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, onMounted, onBeforeUnmount, inject, watch } from 'vue'
import * as echarts from 'echarts/core'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { LineChart } from 'echarts/charts'
......@@ -43,78 +43,99 @@ let chart = null
// 维度切换:d=日, m=月, y=年
const rangeType = ref('m')
// 从父组件获取统一数据
const dashboardData = inject('dashboardData', {})
// 嵌入 iframe 和全屏自适应
const inIframe = ref(false)
const isFullscreen = ref(false)
const outerPadding = ref('0px')
// 模拟接口:返回近 N 个月的请求量
function mockFetchApiTrend(months = 6) {
return new Promise((resolve) => {
setTimeout(() => {
const now = new Date()
const items = []
for (let i = months - 1; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
// 构造一个看起来合理的请求数:基础值 + 波动
const base = 20000 + (i * 1500)
const noise = Math.floor(Math.random() * 4000) - 2000 // ±2000 波动
const value = Math.max(0, base + noise)
items.push({ month: `${y}-${m}`, requests: value })
}
resolve({ code: 0, data: items })
}, 500)
})
}
// 原始生成模拟数据的方式(备份)
// function mockFetchApiTrend(months = 6) {
// return new Promise((resolve) => {
// setTimeout(() => {
// const now = new Date()
// const items = []
// for (let i = months - 1; i >= 0; i--) {
// const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
// const y = d.getFullYear()
// const m = String(d.getMonth() + 1).padStart(2, '0')
// const base = 20000 + (i * 1500)
// const noise = Math.floor(Math.random() * 4000) - 2000
// const value = Math.max(0, base + noise)
// items.push({ month: `${y}-${m}`, requests: value })
// }
// resolve({ code: 0, data: items })
// }, 500)
// })
// }
// 生成不同维度的数据
function genDaySeries(days = 7) {
const now = new Date()
const x = []
const y = []
for (let i = days - 1; i >= 0; i--) {
const d = new Date(now)
d.setDate(now.getDate() - i)
const mm = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
x.push(`${mm}-${dd}`)
// 日数据:较小基数 + 波动
const base = 800 + (days - i) * 30
const noise = Math.floor(Math.random() * 200) - 100
y.push(Math.max(0, base + noise))
}
return { x, y }
}
// 生成不同维度的数据(备份)
// function genDaySeries(days = 7) {
// const now = new Date()
// const x = []
// const y = []
// for (let i = days - 1; i >= 0; i--) {
// const d = new Date(now)
// d.setDate(now.getDate() - i)
// const mm = String(d.getMonth() + 1).padStart(2, '0')
// const dd = String(d.getDate()).padStart(2, '0')
// x.push(`${mm}-${dd}`)
// const base = 800 + (days - i) * 30
// const noise = Math.floor(Math.random() * 200) - 100
// y.push(Math.max(0, base + noise))
// }
// return { x, y }
// }
function genMonthSeries(months = 12) {
const now = new Date()
const x = []
const y = []
for (let i = months - 1; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
const m = d.getMonth() + 1
x.push(`${m}月`)
const base = 20000 + (months - i) * 1500
const noise = Math.floor(Math.random() * 4000) - 2000
y.push(Math.max(0, base + noise))
}
return { x, y }
}
// function genMonthSeries(months = 12) {
// const now = new Date()
// const x = []
// const y = []
// for (let i = months - 1; i >= 0; i--) {
// const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
// const m = d.getMonth() + 1
// x.push(`${m}月`)
// const base = 20000 + (months - i) * 1500
// const noise = Math.floor(Math.random() * 4000) - 2000
// y.push(Math.max(0, base + noise))
// }
// return { x, y }
// }
// function genYearSeries(years = 5) {
// const now = new Date()
// const x = []
// const y = []
// for (let i = years - 1; i >= 0; i--) {
// const d = new Date(now.getFullYear() - i, 0, 1)
// const yr = d.getFullYear()
// x.push(`${yr}`)
// const base = 200000 + (years - i) * 30000
// const noise = Math.floor(Math.random() * 40000) - 20000
// y.push(Math.max(0, base + noise))
// }
// return { x, y }
// }
function genYearSeries(years = 5) {
const now = new Date()
const x = []
const y = []
for (let i = years - 1; i >= 0; i--) {
const d = new Date(now.getFullYear() - i, 0, 1)
const yr = d.getFullYear()
x.push(`${yr}`)
const base = 200000 + (years - i) * 30000
const noise = Math.floor(Math.random() * 40000) - 20000
y.push(Math.max(0, base + noise))
// 从 dashboardData 获取图表数据
const getChartData = () => {
const data = dashboardData.value.apiCalls || []
if (!Array.isArray(data)) {
return { x: [], y: [] }
}
// 根据数据格式转换为图表数据
// apiCalls 格式: [{countDate: "2025-01-01", callsCount: 1202}, ...]
const x = data.map(item => {
const date = new Date(item.countDate)
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
return `${m}-${d}`
})
const y = data.map(item => item.callsCount)
return { x, y }
}
......@@ -173,16 +194,7 @@ function getOption(x, y) {
}
async function render() {
let x = []
let y = []
// 按维度生成数据
if (rangeType.value === 'd') {
;({ x, y } = genDaySeries(7))
} else if (rangeType.value === 'm') {
;({ x, y } = genMonthSeries(12))
} else {
;({ x, y } = genYearSeries(5))
}
const { x, y } = getChartData()
if (!chart) chart = echarts.init(chartRef.value)
chart.setOption(getOption(x, y), true)
......@@ -237,6 +249,11 @@ onMounted(() => {
document.addEventListener('MSFullscreenChange', onFsChange)
})
// 监听数据变化
watch(() => dashboardData.value.apiCalls, () => {
render()
}, { deep: true })
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
document.removeEventListener('fullscreenchange', onFsChange)
......
......@@ -22,17 +22,19 @@
</template>
<script setup lang="ts">
import { inject, computed, onMounted, onBeforeUnmount, ref } from 'vue'
import { inject, computed, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import * as echarts from 'echarts/core'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { LineChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
import { useI18n } from 'vue-i18n'
import type { EChartsOption } from 'echarts'
import * as IndexCountApi from '@/api/Home/count'
echarts.use([GridComponent, TooltipComponent, LegendComponent, LineChart, CanvasRenderer])
// 从父组件获取统一数据
const dashboardData = inject('dashboardData', {})
// 从父组件(Home.vue)注入全屏状态与方法
const fs = inject('fsState', null) as null | { isFullscreen: any; toggleFullscreen: () => void }
const fsIsFullscreen = computed(() => !!(fs && fs.isFullscreen && fs.isFullscreen.value))
......@@ -138,12 +140,14 @@ function getOption(xData: string[], seriesData: {
}
async function render() {
const res = await IndexCountApi.getOrdersData(dateType.value)
const x = (res || []).map((item: any) => t(item.countDate))
const computeCount = (res || []).map((item: any) => item.computeOrdersCount || 0)
const apiCount = (res || []).map((item: any) => item.apiOrdersCount || 0)
const computeAmount = (res || []).map((item: any) => Number(((item.computeOrdersAmount || 0) / 100).toFixed(2)))
const apiAmount = (res || []).map((item: any) => Number(((item.apiOrdersAmount || 0) / 100).toFixed(2)))
const orders = dashboardData.value.orders || {}
const res = orders[dateType.value] || []
const x = res.map((item: any) => t(item.countDate))
const computeCount = res.map((item: any) => item.computeOrdersCount || 0)
const apiCount = res.map((item: any) => item.apiOrdersCount || 0)
const computeAmount = res.map((item: any) => Number(((item.computeOrdersAmount || 0) / 100).toFixed(2)))
const apiAmount = res.map((item: any) => Number(((item.apiOrdersAmount || 0) / 100).toFixed(2)))
if (!chart) {
const el = document.getElementById('energyManage') as HTMLElement | null
......@@ -161,6 +165,11 @@ function changeType(t: 'd' | 'm' | 'y') {
const onResize = () => chart && chart.resize()
// 监听数据变化
watch(() => dashboardData.value.orders, () => {
render()
}, { deep: true })
onMounted(() => {
render()
window.addEventListener('resize', onResize)
......
......@@ -12,17 +12,19 @@
</template>
<script setup lang="ts">
import { inject, computed, onMounted, onBeforeUnmount, ref } from 'vue'
import { inject, computed, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import * as echarts from 'echarts/core'
import { GridComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { LineChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
import { useI18n } from 'vue-i18n'
import type { EChartsOption } from 'echarts'
import * as IndexCountApi from '@/api/Home/count'
echarts.use([GridComponent, TooltipComponent, LegendComponent, LineChart, CanvasRenderer])
// 从父组件获取统一数据
const dashboardData = inject('dashboardData', {})
// 从父组件(Home.vue)注入全屏状态与方法
const fs = inject('fsState', null) as null | { isFullscreen: any; toggleFullscreen: () => void }
const fsIsFullscreen = computed(() => !!(fs && fs.isFullscreen && fs.isFullscreen.value))
......@@ -88,8 +90,9 @@ function getOption(xData: string[], growth: number[], active: number[]): ECharts
}
async function render() {
const res = await IndexCountApi.getUsersData(dateType.value)
const arr = Array.isArray(res) ? res : []
// 直接从 dashboardData 获取数据
const arr = dashboardData.value.users || []
const x = arr.map((item: any) => t(item.countDate))
const growth = arr.map((item: any) => item.growthUsersCount ?? item.usersCount ?? 0)
const active = arr.map((item: any, idx: number) =>
......@@ -112,6 +115,11 @@ function changeType(t: 'd' | 'm' | 'y') {
const onResize = () => chart && chart.resize()
// 监听数据变化
watch(() => dashboardData.value.users, () => {
render()
}, { deep: true })
onMounted(() => {
render()
window.addEventListener('resize', onResize)
......
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="配置key" prop="configKey">
<el-input v-model="formData.configKey" placeholder="请输入配置key" />
</el-form-item>
<el-form-item label="配置类型" prop="configType">
<el-select v-model="formData.configType" placeholder="请选择配置类型" :disabled="formType === 'update'">
<el-option label="开关 (switch)" value="switch" />
<el-option label="数据 (data)" value="data" />
</el-select>
</el-form-item>
<el-form-item label="配置值" prop="configValue">
<el-input
v-model="formData.configValue"
type="textarea"
:rows="10"
placeholder="请输入配置值,JSON格式"
v-if="formData.configType === 'data'"
/>
<el-switch
v-model="switchValue"
active-text="开启"
inactive-text="关闭"
v-else
/>
</el-form-item>
<el-form-item v-if="formData.configType === 'data'">
<el-button @click="formatJson" type="primary" size="small">格式化 JSON</el-button>
<el-button @click="validateJson" type="success" size="small">校验 JSON</el-button>
</el-form-item>
<el-form-item label="配置描述" prop="description">
<Editor v-model="formData.description" height="150px" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { HomeDashboardMockApi, HomeDashboardMock } from '@/api/biz/home'
/** 首页大屏模拟数据配置 表单 */
defineOptions({ name: 'HomeDashboardMockForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
const formType = ref('') // 表单的类型:create - 新增;update - 修改
const formData = ref({
id: undefined,
configKey: undefined,
configType: undefined,
configValue: undefined,
description: undefined
})
const formRules = reactive({
configKey: [{ required: true, message: '配置key不能为空', trigger: 'blur' }],
configType: [{ required: true, message: '配置类型不能为空', trigger: 'change' }],
configValue: [{ required: true, message: '配置值不能为空', trigger: 'blur' }]
})
const formRef = ref() // 表单 Ref
// 开关类型的值处理
const switchValue = computed({
get: () => formData.value.configValue === 'true',
set: (val) => {
formData.value.configValue = val ? 'true' : 'false'
}
})
// 格式化JSON
const formatJson = () => {
try {
const parsed = JSON.parse(formData.value.configValue)
formData.value.configValue = JSON.stringify(parsed, null, 2)
message.success('JSON格式化成功')
} catch (e) {
message.error('JSON格式错误,无法格式化')
}
}
// 校验JSON
const validateJson = () => {
try {
JSON.parse(formData.value.configValue)
message.success('JSON格式正确')
} catch (e) {
message.error('JSON格式错误:' + e.message)
}
}
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await HomeDashboardMockApi.getHomeDashboardMock(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as HomeDashboardMock
if (formType.value === 'create') {
await HomeDashboardMockApi.createHomeDashboardMock(data)
message.success(t('common.createSuccess'))
} else {
await HomeDashboardMockApi.updateHomeDashboardMock(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
configKey: undefined,
configType: undefined,
configValue: undefined,
description: undefined
}
formRef.value?.resetFields()
}
</script>
<template>
<ContentWrap>
<div class="mock-config-page">
<!-- 模拟数据开关 -->
<el-card class="config-card" shadow="never">
<template #header>
<div class="card-header">
<Icon icon="ep:setting" :size="20" />
<span class="title">模拟数据开关</span>
</div>
</template>
<div class="switch-section">
<el-radio-group v-model="mockDataEnabled" @change="handleSwitchChange">
<el-radio :label="false">关闭(使用真实数据)</el-radio>
<el-radio :label="true">开启(使用模拟数据)</el-radio>
</el-radio-group>
<el-text type="info" style="margin-left: 20px;">
开启后,首页大屏将使用下方的模拟数据进行展示
</el-text>
</div>
</el-card>
<!-- 第一行:平台总体态势 + API请求趋势 -->
<el-row :gutter="20">
<el-col :span="12">
<!-- 平台总体态势 -->
<el-card class="config-card" shadow="never">
<template #header>
<div class="card-header">
<Icon icon="ep:data-analysis" :size="20" />
<span class="title">平台总体态势</span>
<el-button type="primary" size="small" @click="saveOverallSituation">保存</el-button>
</div>
</template>
<el-form :model="overallSituation" label-width="140px" class="config-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="算力总规模(P)">
<el-input-number v-model="overallSituation.allCompute" :precision="2" :min="0" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="已租赁算力(P)">
<el-input-number v-model="overallSituation.leaseCompute" :precision="2" :min="0" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="算力利用率(%)">
<el-input-number v-model="overallSituation.computeUtilizationRate" :precision="2" :min="0" :max="100" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="运行中任务数">
<el-input-number v-model="overallSituation.runningTaskCount" :min="0" />
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</el-col>
<el-col :span="12">
<!-- API请求趋势 -->
<el-card class="config-card" shadow="never">
<template #header>
<div class="card-header">
<Icon icon="ep:line-chart" :size="20" />
<span class="title">API请求趋势</span>
<el-button type="primary" size="small" @click="saveApiCalls">保存</el-button>
</div>
</template>
<div class="table-section">
<el-button type="primary" size="small" @click="addApiCallItem" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加数据项
</el-button>
<table class="data-table" v-if="apiCallsList.length > 0">
<thead>
<tr>
<th>日期</th>
<th>调用次数</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in apiCallsList" :key="index">
<td>
<el-date-picker
v-model="item.countDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
size="small"
/>
</td>
<td>
<el-input-number v-model="item.callsCount" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeApiCallItem(index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加" :image-size="80" />
</div>
</el-card>
</el-col>
</el-row>
<!-- 第二行:算力资源分布 + 用户管理 -->
<el-row :gutter="20">
<el-col :span="12">
<!-- 算力资源分布 -->
<el-card class="config-card" shadow="never">
<template #header>
<div class="card-header">
<Icon icon="ep:pie-chart" :size="20" />
<span class="title">算力资源分布</span>
<el-button type="primary" size="small" @click="saveComputeDistribution">保存</el-button>
</div>
</template>
<el-tabs v-model="activeTab" type="border-card">
<!-- GPU型号 -->
<el-tab-pane label="GPU型号" name="gpu">
<div class="table-section">
<el-button type="primary" size="small" @click="addComputeItem('gpu')" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加GPU型号
</el-button>
<table class="data-table" v-if="computeDistribution.gpu && computeDistribution.gpu.length > 0">
<thead>
<tr>
<th>型号名称</th>
<th>占比</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in computeDistribution.gpu" :key="index">
<td>
<el-input v-model="item.name" placeholder="如:4090" size="small" />
</td>
<td>
<el-input-number v-model="item.value" :precision="2" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeComputeItem('gpu', index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加" :image-size="80" />
</div>
</el-tab-pane>
<!-- 算力来源 -->
<el-tab-pane label="算力来源" name="source">
<div class="table-section">
<el-button type="primary" size="small" @click="addComputeItem('source')" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加来源
</el-button>
<table class="data-table" v-if="computeDistribution.source && computeDistribution.source.length > 0">
<thead>
<tr>
<th>来源名称</th>
<th>占比</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in computeDistribution.source" :key="index">
<td>
<el-input v-model="item.name" placeholder="如:自有" size="small" />
</td>
<td>
<el-input-number v-model="item.value" :precision="2" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeComputeItem('source', index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加" :image-size="80" />
</div>
</el-tab-pane>
<!-- 计算资源 -->
<el-tab-pane label="计算资源" name="resource">
<div class="table-section">
<el-button type="primary" size="small" @click="addComputeItem('resource')" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加资源类型
</el-button>
<table class="data-table" v-if="computeDistribution.resource && computeDistribution.resource.length > 0">
<thead>
<tr>
<th>资源名称</th>
<th>占比</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in computeDistribution.resource" :key="index">
<td>
<el-input v-model="item.name" placeholder="如:裸金属" size="small" />
</td>
<td>
<el-input-number v-model="item.value" :precision="2" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeComputeItem('resource', index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加" :image-size="80" />
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
<el-col :span="12">
<!-- 用户管理 -->
<el-card class="config-card" shadow="never">
<template #header>
<div class="card-header">
<Icon icon="ep:user" :size="20" />
<span class="title">用户管理</span>
<el-button type="primary" size="small" @click="saveUsers">保存</el-button>
</div>
</template>
<div class="table-section">
<el-button type="primary" size="small" @click="addUserItem" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加数据项
</el-button>
<table class="data-table" v-if="usersList.length > 0">
<thead>
<tr>
<th>日期</th>
<th>用户总数</th>
<th>增长用户数</th>
<th>活跃用户数</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in usersList" :key="index">
<td>
<el-date-picker
v-model="item.countDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
size="small"
/>
</td>
<td>
<el-input-number v-model="item.usersCount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.growthUsersCount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.activeUsersCount" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeUserItem(index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加" :image-size="80" />
</div>
</el-card>
</el-col>
</el-row>
<!-- 第三行:服务能力 + 订单管理 -->
<el-row :gutter="20">
<el-col :span="12">
<!-- 服务能力 -->
<el-card class="config-card" shadow="never">
<template #header>
<div class="card-header">
<Icon icon="ep:trend-charts" :size="20" />
<span class="title">服务能力</span>
<el-button type="primary" size="small" @click="saveServiceCapability">保存</el-button>
</div>
</template>
<el-form :model="serviceCapability" label-width="120px" class="config-form">
<el-form-item label="年份">
<el-input v-model="serviceCapability.yearsStr" placeholder="如:2018,2019,2020" />
</el-form-item>
<el-form-item label="上线应用">
<el-input v-model="serviceCapability.appOnlineStr" placeholder="如:10,20,30,40,50(逗号分隔)" />
</el-form-item>
<el-form-item label="上线API">
<el-input v-model="serviceCapability.apiOnlineStr" placeholder="如:100,200,300,400,500(逗号分隔)" />
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="12">
<!-- 订单管理 -->
<el-card class="config-card" shadow="never">
<template #header>
<div class="card-header">
<Icon icon="ep:shopping-cart" :size="20" />
<span class="title">订单管理</span>
<el-button type="primary" size="small" @click="saveOrders">保存</el-button>
</div>
</template>
<el-tabs v-model="ordersTab" type="border-card">
<!-- 日维度 -->
<el-tab-pane label="日" name="d">
<div class="table-section">
<el-button type="primary" size="small" @click="addOrderItem('d')" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加数据项
</el-button>
<table class="data-table" v-if="ordersData.d && ordersData.d.length > 0">
<thead>
<tr>
<th>日期</th>
<th>算力订单数</th>
<th>API订单数</th>
<th>算力订单金额(分)</th>
<th>API订单金额(分)</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in ordersData.d" :key="index">
<td>
<el-date-picker
v-model="item.countDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
size="small"
/>
</td>
<td>
<el-input-number v-model="item.computeOrdersCount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.apiOrdersCount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.computeOrdersAmount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.apiOrdersAmount" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeOrderItem('d', index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加" :image-size="80" />
</div>
</el-tab-pane>
<!-- 月维度 -->
<el-tab-pane label="月" name="m">
<div class="table-section">
<el-button type="primary" size="small" @click="addOrderItem('m')" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加数据项
</el-button>
<table class="data-table" v-if="ordersData.m && ordersData.m.length > 0">
<thead>
<tr>
<th>日期</th>
<th>算力订单数</th>
<th>API订单数</th>
<th>算力订单金额(分)</th>
<th>API订单金额(分)</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in ordersData.m" :key="index">
<td>
<el-date-picker
v-model="item.countDate"
type="month"
placeholder="选择月份"
format="YYYY-MM"
value-format="YYYY-MM"
size="small"
/>
</td>
<td>
<el-input-number v-model="item.computeOrdersCount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.apiOrdersCount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.computeOrdersAmount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.apiOrdersAmount" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeOrderItem('m', index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加" :image-size="80" />
</div>
</el-tab-pane>
<!-- 年维度 -->
<el-tab-pane label="年" name="y">
<div class="table-section">
<el-button type="primary" size="small" @click="addOrderItem('y')" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加数据项
</el-button>
<table class="data-table" v-if="ordersData.y && ordersData.y.length > 0">
<thead>
<tr>
<th>年份</th>
<th>算力订单数</th>
<th>API订单数</th>
<th>算力订单金额(分)</th>
<th>API订单金额(分)</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in ordersData.y" :key="index">
<td>
<el-input v-model="item.countDate" placeholder="如:2025" size="small" />
</td>
<td>
<el-input-number v-model="item.computeOrdersCount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.apiOrdersCount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.computeOrdersAmount" :min="0" size="small" />
</td>
<td>
<el-input-number v-model="item.apiOrdersAmount" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeOrderItem('y', index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加" :image-size="80" />
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</el-col>
</el-row>
<!-- 地图数据 -->
<el-row :gutter="20">
<el-col :span="24">
<el-card class="config-card" shadow="never">
<template #header>
<div class="card-header">
<Icon icon="ep:location" :size="20" />
<span class="title">地图数据</span>
<el-button type="primary" size="small" @click="saveMapData">保存</el-button>
</div>
</template>
<div class="table-section">
<el-button type="primary" size="small" @click="addMapDataItem" style="margin-bottom: 10px;">
<Icon icon="ep:plus" class="mr-5px" />添加省份
</el-button>
<table class="data-table" v-if="mapDataList.length > 0">
<thead>
<tr>
<th style="width: 200px">省份</th>
<th>算力资源(PTOPS)</th>
<th style="width: 80px">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in mapDataList" :key="index">
<td>
<el-select v-model="item.name" placeholder="选择省份" size="small" style="width: 100%">
<el-option v-for="province in provinces" :key="province" :label="province" :value="province" />
</el-select>
</td>
<td>
<el-input-number v-model="item.value" :precision="4" :min="0" size="small" />
</td>
<td>
<el-button type="danger" size="small" @click="removeMapDataItem(index)">
删除
</el-button>
</td>
</tr>
</tbody>
</table>
<el-empty v-else description="暂无数据,请添加省份" :image-size="80" />
</div>
</el-card>
</el-col>
</el-row>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { HomeDashboardMockApi } from '@/api/biz/home'
import { ElMessage } from 'element-plus'
defineOptions({ name: 'HomeDashboardConfig' })
const message = useMessage()
// 模拟数据开关
const mockDataEnabled = ref(false)
// 平台总体态势
const overallSituation = ref({
allCompute: 432.58,
leaseCompute: 139.94,
computeUtilizationRate: 42.37,
runningTaskCount: 29
})
// API请求趋势
const apiCallsList = ref([])
// 用户管理
const usersList = ref([])
// 服务能力
const serviceCapability = ref({
yearsStr: '',
appOnlineStr: '',
apiOnlineStr: ''
})
// 算力资源分布
const activeTab = ref('gpu')
const computeDistribution = ref({
gpu: [],
source: [],
resource: []
})
// 订单管理
const ordersTab = ref('d')
const ordersData = ref({
d: [],
m: [],
y: []
})
// 地图数据
const mapDataList = ref([])
const provinces = [
'北京市', '天津市', '河北省', '山西省', '内蒙古自治区',
'辽宁省', '吉林省', '黑龙江省', '上海市', '江苏省',
'浙江省', '安徽省', '福建省', '江西省', '山东省',
'河南省', '湖北省', '湖南省', '广东省', '广西壮族自治区',
'海南省', '重庆市', '四川省', '贵州省', '云南省',
'西藏自治区', '陕西省', '甘肃省', '青海省', '宁夏回族自治区',
'新疆维吾尔自治区', '台湾省', '香港特别行政区', '澳门特别行政区'
]
// 加载所有模拟数据
const loadAllMockData = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
res.list.forEach(item => {
if (item.configKey === 'use_mock_data') {
mockDataEnabled.value = item.configValue === 'true'
} else if (item.configKey === 'mock_overall_situation') {
try {
overallSituation.value = JSON.parse(item.configValue)
} catch (e) {
console.error('解析平台总体态势失败', e)
}
} else if (item.configKey === 'mock_api_calls') {
try {
apiCallsList.value = JSON.parse(item.configValue)
} catch (e) {
console.error('解析API请求趋势失败', e)
}
} else if (item.configKey === 'mock_compute_distribution') {
try {
computeDistribution.value = JSON.parse(item.configValue)
} catch (e) {
console.error('解析算力资源分布失败', e)
}
} else if (item.configKey === 'mock_users') {
try {
usersList.value = JSON.parse(item.configValue)
} catch (e) {
console.error('解析用户管理失败', e)
}
} else if (item.configKey === 'mock_service_capability') {
try {
const data = JSON.parse(item.configValue)
serviceCapability.value = {
yearsStr: data.years ? data.years.join(',') : '',
appOnlineStr: data.appOnline ? data.appOnline.join(',') : '',
apiOnlineStr: data.apiOnline ? data.apiOnline.join(',') : ''
}
} catch (e) {
console.error('解析服务能力失败', e)
}
} else if (item.configKey === 'mock_orders') {
try {
ordersData.value = JSON.parse(item.configValue)
} catch (e) {
console.error('解析订单管理失败', e)
}
} else if (item.configKey === 'mock_map_data') {
try {
mapDataList.value = JSON.parse(item.configValue)
} catch (e) {
console.error('解析地图数据失败', e)
}
}
})
}
} catch (error) {
console.error('加载模拟数据失败', error)
}
}
// 保存开关
const handleSwitchChange = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
const switchConfig = res.list.find(item => item.configKey === 'use_mock_data')
if (switchConfig) {
const updateData = { ...switchConfig, configValue: mockDataEnabled.value ? 'true' : 'false' }
await HomeDashboardMockApi.updateHomeDashboardMock(updateData)
message.success(mockDataEnabled.value ? '模拟数据已开启' : '模拟数据已关闭')
}
}
} catch (error) {
console.error('保存开关失败', error)
message.error('保存失败')
}
}
// 保存平台总体态势
const saveOverallSituation = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
const config = res.list.find(item => item.configKey === 'mock_overall_situation')
if (config) {
const updateData = { ...config, configValue: JSON.stringify(overallSituation.value) }
await HomeDashboardMockApi.updateHomeDashboardMock(updateData)
message.success('保存成功')
}
}
} catch (error) {
console.error('保存失败', error)
message.error('保存失败')
}
}
// 保存API请求趋势
const saveApiCalls = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
const config = res.list.find(item => item.configKey === 'mock_api_calls')
if (config) {
const updateData = { ...config, configValue: JSON.stringify(apiCallsList.value) }
await HomeDashboardMockApi.updateHomeDashboardMock(updateData)
message.success('保存成功')
}
}
} catch (error) {
console.error('保存失败', error)
message.error('保存失败')
}
}
// 添加API请求数据项
const addApiCallItem = () => {
apiCallsList.value.push({
countDate: '',
callsCount: 0
})
}
// 删除API请求数据项
const removeApiCallItem = (index: number) => {
apiCallsList.value.splice(index, 1)
}
// 保存算力资源分布
const saveComputeDistribution = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
const config = res.list.find(item => item.configKey === 'mock_compute_distribution')
if (config) {
const updateData = { ...config, configValue: JSON.stringify(computeDistribution.value) }
await HomeDashboardMockApi.updateHomeDashboardMock(updateData)
message.success('保存成功')
}
}
} catch (error) {
console.error('保存失败', error)
message.error('保存失败')
}
}
// 添加算力资源项
const addComputeItem = (type: string) => {
if (!computeDistribution.value[type]) {
computeDistribution.value[type] = []
}
computeDistribution.value[type].push({
name: '',
value: 0
})
}
// 删除算力资源项
const removeComputeItem = (type: string, index: number) => {
computeDistribution.value[type].splice(index, 1)
}
// 保存用户管理
const saveUsers = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
const config = res.list.find(item => item.configKey === 'mock_users')
if (config) {
const updateData = { ...config, configValue: JSON.stringify(usersList.value) }
await HomeDashboardMockApi.updateHomeDashboardMock(updateData)
message.success('保存成功')
}
}
} catch (error) {
console.error('保存失败', error)
message.error('保存失败')
}
}
// 添加用户数据项
const addUserItem = () => {
usersList.value.push({
countDate: '',
usersCount: 0,
growthUsersCount: 0,
activeUsersCount: 0
})
}
// 删除用户数据项
const removeUserItem = (index: number) => {
usersList.value.splice(index, 1)
}
// 保存服务能力
const saveServiceCapability = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
const config = res.list.find(item => item.configKey === 'mock_service_capability')
const data = {
years: serviceCapability.value.yearsStr.split(',').map(s => s.trim()),
appOnline: serviceCapability.value.appOnlineStr.split(',').map(s => parseFloat(s.trim()) || 0),
apiOnline: serviceCapability.value.apiOnlineStr.split(',').map(s => parseFloat(s.trim()) || 0)
}
if (config) {
const updateData = { ...config, configValue: JSON.stringify(data) }
await HomeDashboardMockApi.updateHomeDashboardMock(updateData)
} else {
const createData = {
configKey: 'mock_service_capability',
configType: 'data',
configValue: JSON.stringify(data),
description: '服务能力模拟数据'
}
await HomeDashboardMockApi.createHomeDashboardMock(createData)
}
message.success('保存成功')
}
} catch (error) {
console.error('保存失败', error)
message.error('保存失败')
}
}
// 订单管理 - 保存
const saveOrders = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
const config = res.list.find(item => item.configKey === 'mock_orders')
const data = ordersData.value
if (config) {
const updateData = { ...config, configValue: JSON.stringify(data) }
await HomeDashboardMockApi.updateHomeDashboardMock(updateData)
} else {
const createData = {
configKey: 'mock_orders',
configType: 'data',
configValue: JSON.stringify(data),
description: '订单管理模拟数据'
}
await HomeDashboardMockApi.createHomeDashboardMock(createData)
}
message.success('保存成功')
}
} catch (error) {
console.error('保存失败', error)
message.error('保存失败')
}
}
// 订单管理 - 添加数据项
const addOrderItem = (type: 'd' | 'm' | 'y') => {
if (!ordersData.value[type]) {
ordersData.value[type] = []
}
ordersData.value[type].push({
countDate: '',
computeOrdersCount: 0,
apiOrdersCount: 0,
computeOrdersAmount: 0,
apiOrdersAmount: 0
})
}
// 订单管理 - 删除数据项
const removeOrderItem = (type: 'd' | 'm' | 'y', index: number) => {
ordersData.value[type].splice(index, 1)
}
// 地图数据 - 保存
const saveMapData = async () => {
try {
const res = await HomeDashboardMockApi.getHomeDashboardMockPage({ pageNo: 1, pageSize: 100 })
if (res && res.list) {
const config = res.list.find(item => item.configKey === 'mock_map_data')
const data = mapDataList.value
if (config) {
const updateData = { ...config, configValue: JSON.stringify(data) }
await HomeDashboardMockApi.updateHomeDashboardMock(updateData)
} else {
const createData = {
configKey: 'mock_map_data',
configType: 'data',
configValue: JSON.stringify(data),
description: '地图数据模拟数据'
}
await HomeDashboardMockApi.createHomeDashboardMock(createData)
}
message.success('保存成功')
}
} catch (error) {
console.error('保存失败', error)
message.error('保存失败')
}
}
// 地图数据 - 添加数据项
const addMapDataItem = () => {
mapDataList.value.push({
name: '',
value: 0
})
}
// 地图数据 - 删除数据项
const removeMapDataItem = (index: number) => {
mapDataList.value.splice(index, 1)
}
onMounted(() => {
loadAllMockData()
})
</script>
<style scoped lang="scss">
.mock-config-page {
.config-card {
margin-bottom: 20px;
.card-header {
display: flex;
align-items: center;
gap: 8px;
.title {
font-size: 16px;
font-weight: 600;
flex: 1;
}
}
}
.switch-section {
display: flex;
align-items: center;
}
.config-form {
max-width: 800px;
}
.table-section {
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
th, td {
padding: 12px 8px;
text-align: center;
border: 1px solid #ebeef5;
}
th {
background-color: #f5f7fa;
font-weight: 600;
color: #606266;
}
td {
:deep(.el-input),
:deep(.el-input-number),
:deep(.el-date-picker) {
width: 100%;
}
}
}
}
}
</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