Commit ed0005a3 by lijinqi

大屏

parent 8e31864c
@font-face {
font-family: "DIN";
src: url("din/DIN-Bold.eot"); /* IE9 */
src: url("din/DIN-Bold.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
url("din/DIN-Bold.woff") format("woff"), /* chromefirefox */
url("din/DIN-Bold.ttf") format("truetype");/* chrome、firefox、opera、Safari, Android, iOS 4.2+ */
font-style: normal;
font-weight: normal;
}
@font-face {
font-family: "DOUYU";
src: url("douyu/douyuFont-2.otf");
font-style: normal;
font-weight: normal;
}
.font_din {
font-family: DIN;
}
.font_douyu {
font-family: DOUYU;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"Afghanistan": "阿富汗",
"Albania": "阿尔巴尼亚",
"Algeria": "阿尔及利亚",
"Angola": "安哥拉",
"Argentina": "阿根廷",
"Armenia": "亚美尼亚",
"Australia": "澳大利亚",
"Austria": "奥地利",
"Azerbaijan": "阿塞拜疆",
"Bahamas": "巴哈马",
"Bahrain": "巴林",
"Bangladesh": "孟加拉国",
"Belarus": "白俄罗斯",
"Belgium": "比利时",
"Belize": "伯利兹",
"Benin": "贝宁",
"Bhutan": "不丹",
"Bolivia": "玻利维亚",
"Bosnia and Herz.": "波斯尼亚和黑塞哥维那",
"Botswana": "博茨瓦纳",
"Brazil": "巴西",
"British Virgin Islands": "英属维京群岛",
"Brunei": "文莱",
"Bulgaria": "保加利亚",
"Burkina Faso": "布基纳法索",
"Burundi": "布隆迪",
"Cambodia": "柬埔寨",
"Cameroon": "喀麦隆",
"Canada": "加拿大",
"Cape Verde": "佛得角",
"Cayman Islands": "开曼群岛",
"Central African Rep.": "中非共和国",
"Chad": "乍得",
"Chile": "智利",
"China": "中国",
"Colombia": "哥伦比亚",
"Comoros": "科摩罗",
"Congo": "刚果",
"Costa Rica": "哥斯达黎加",
"Croatia": "克罗地亚",
"Cuba": "古巴",
"Cyprus": "塞浦路斯",
"Czech Rep.": "捷克共和国",
"Côte d'Ivoire": "科特迪瓦",
"Dem. Rep. Congo": "刚果民主共和国",
"Dem. Rep. Korea": "朝鲜",
"Denmark": "丹麦",
"Djibouti": "吉布提",
"Dominican Rep.": "多米尼加共和国",
"Ecuador": "厄瓜多尔",
"Egypt": "埃及",
"El Salvador": "萨尔瓦多",
"Equatorial Guinea": "赤道几内亚",
"Eritrea": "厄立特里亚",
"Estonia": "爱沙尼亚",
"Ethiopia": "埃塞俄比亚",
"Falkland Is.": "福克兰群岛",
"Fiji": "斐济",
"Finland": "芬兰",
"Fr. S. Antarctic Lands": "所罗门群岛",
"France": "法国",
"Gabon": "加蓬",
"Gambia": "冈比亚",
"Georgia": "格鲁吉亚",
"Germany": "德国",
"Ghana": "加纳",
"Greece": "希腊",
"Greenland": "格陵兰",
"Guatemala": "危地马拉",
"Guinea": "几内亚",
"Guinea-Bissau": "几内亚比绍",
"Guyana": "圭亚那",
"Haiti": "海地",
"Honduras": "洪都拉斯",
"Hungary": "匈牙利",
"Iceland": "冰岛",
"India": "印度",
"Indonesia": "印度尼西亚",
"Iran": "伊朗",
"Iraq": "伊拉克",
"Ireland": "爱尔兰",
"Isle of Man": "马恩岛",
"Israel": "以色列",
"Italy": "意大利",
"Jamaica": "牙买加",
"Japan": "日本",
"Jordan": "约旦",
"Kazakhstan": "哈萨克斯坦",
"Kenya": "肯尼亚",
"Korea": "韩国",
"Kuwait": "科威特",
"Kyrgyzstan": "吉尔吉斯斯坦",
"Lao PDR": "老挝",
"Latvia": "拉脱维亚",
"Lebanon": "黎巴嫩",
"Lesotho": "莱索托",
"Liberia": "利比里亚",
"Libya": "利比亚",
"Lithuania": "立陶宛",
"Luxembourg": "卢森堡",
"Macedonia": "马其顿",
"Madagascar": "马达加斯加",
"Malawi": "马拉维",
"Malaysia": "马来西亚",
"Maldives": "马尔代夫",
"Mali": "马里",
"Malta": "马耳他",
"Mauritania": "毛利塔尼亚",
"Mauritius": "毛里求斯",
"Mexico": "墨西哥",
"Moldova": "摩尔多瓦",
"Monaco": "摩纳哥",
"Mongolia": "蒙古",
"Montenegro": "黑山共和国",
"Morocco": "摩洛哥",
"Mozambique": "莫桑比克",
"Myanmar": "缅甸",
"Namibia": "纳米比亚",
"Nepal": "尼泊尔",
"Netherlands": "荷兰",
"New Caledonia": "新喀里多尼亚",
"New Zealand": "新西兰",
"Nicaragua": "尼加拉瓜",
"Niger": "尼日尔",
"Nigeria": "尼日利亚",
"Norway": "挪威",
"Oman": "阿曼",
"Pakistan": "巴基斯坦",
"Panama": "巴拿马",
"Papua New Guinea": "巴布亚新几内亚",
"Paraguay": "巴拉圭",
"Peru": "秘鲁",
"Philippines": "菲律宾",
"Poland": "波兰",
"Portugal": "葡萄牙",
"Puerto Rico": "波多黎各",
"Qatar": "卡塔尔",
"Reunion": "留尼旺",
"Romania": "罗马尼亚",
"Russia": "俄罗斯",
"Rwanda": "卢旺达",
"S. Geo. and S. Sandw. Is.": "南乔治亚和南桑威奇群岛",
"S. Sudan": "南苏丹",
"San Marino": "圣马力诺",
"Saudi Arabia": "沙特阿拉伯",
"Senegal": "塞内加尔",
"Serbia": "塞尔维亚",
"Sierra Leone": "塞拉利昂",
"Singapore": "新加坡",
"Slovakia": "斯洛伐克",
"Slovenia": "斯洛文尼亚",
"Solomon Is.": "所罗门群岛",
"Somalia": "索马里",
"South Africa": "南非",
"Spain": "西班牙",
"Sri Lanka": "斯里兰卡",
"Sudan": "苏丹",
"Suriname": "苏里南",
"Swaziland": "斯威士兰",
"Sweden": "瑞典",
"Switzerland": "瑞士",
"Syria": "叙利亚",
"Taiwan": "中国台湾",
"Tajikistan": "塔吉克斯坦",
"Tanzania": "坦桑尼亚",
"Thailand": "泰国",
"Togo": "多哥",
"Tonga": "汤加",
"Trinidad and Tobago": "特立尼达和多巴哥",
"Tunisia": "突尼斯",
"Turkey": "土耳其",
"Turkmenistan": "土库曼斯坦",
"U.S. Virgin Islands": "美属维尔京群岛",
"Uganda": "乌干达",
"Ukraine": "乌克兰",
"United Arab Emirates": "阿拉伯联合酋长国",
"United Kingdom": "英国",
"United States": "美国",
"Uruguay": "乌拉圭",
"Uzbekistan": "乌兹别克斯坦",
"Vanuatu": "瓦努阿图",
"Vatican City": "梵蒂冈城",
"Venezuela": "委内瑞拉",
"Vietnam": "越南",
"W. Sahara": "西撒哈拉",
"Yemen": "也门",
"Yugoslavia": "南斯拉夫",
"Zaire": "扎伊尔",
"Zambia": "赞比亚",
"Zimbabwe": "津巴布韦"
}
This source diff could not be displayed because it is too large. You can view the blob instead.
@font-face {
font-family: "DIN";
src: url("din/DIN-Bold.eot"); /* IE9 */
src: url("din/DIN-Bold.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
url("din/DIN-Bold.woff") format("woff"), /* chromefirefox */
url("din/DIN-Bold.ttf") format("truetype");/* chrome、firefox、opera、Safari, Android, iOS 4.2+ */
font-style: normal;
font-weight: normal;
}
@font-face {
font-family: "DOUYU";
src: url("douyu/douyuFont-2.otf");
font-style: normal;
font-weight: normal;
}
.font_din {
font-family: DIN;
}
.font_douyu {
font-family: DOUYU;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"Afghanistan": "阿富汗",
"Albania": "阿尔巴尼亚",
"Algeria": "阿尔及利亚",
"Angola": "安哥拉",
"Argentina": "阿根廷",
"Armenia": "亚美尼亚",
"Australia": "澳大利亚",
"Austria": "奥地利",
"Azerbaijan": "阿塞拜疆",
"Bahamas": "巴哈马",
"Bahrain": "巴林",
"Bangladesh": "孟加拉国",
"Belarus": "白俄罗斯",
"Belgium": "比利时",
"Belize": "伯利兹",
"Benin": "贝宁",
"Bhutan": "不丹",
"Bolivia": "玻利维亚",
"Bosnia and Herz.": "波斯尼亚和黑塞哥维那",
"Botswana": "博茨瓦纳",
"Brazil": "巴西",
"British Virgin Islands": "英属维京群岛",
"Brunei": "文莱",
"Bulgaria": "保加利亚",
"Burkina Faso": "布基纳法索",
"Burundi": "布隆迪",
"Cambodia": "柬埔寨",
"Cameroon": "喀麦隆",
"Canada": "加拿大",
"Cape Verde": "佛得角",
"Cayman Islands": "开曼群岛",
"Central African Rep.": "中非共和国",
"Chad": "乍得",
"Chile": "智利",
"China": "中国",
"Colombia": "哥伦比亚",
"Comoros": "科摩罗",
"Congo": "刚果",
"Costa Rica": "哥斯达黎加",
"Croatia": "克罗地亚",
"Cuba": "古巴",
"Cyprus": "塞浦路斯",
"Czech Rep.": "捷克共和国",
"Côte d'Ivoire": "科特迪瓦",
"Dem. Rep. Congo": "刚果民主共和国",
"Dem. Rep. Korea": "朝鲜",
"Denmark": "丹麦",
"Djibouti": "吉布提",
"Dominican Rep.": "多米尼加共和国",
"Ecuador": "厄瓜多尔",
"Egypt": "埃及",
"El Salvador": "萨尔瓦多",
"Equatorial Guinea": "赤道几内亚",
"Eritrea": "厄立特里亚",
"Estonia": "爱沙尼亚",
"Ethiopia": "埃塞俄比亚",
"Falkland Is.": "福克兰群岛",
"Fiji": "斐济",
"Finland": "芬兰",
"Fr. S. Antarctic Lands": "所罗门群岛",
"France": "法国",
"Gabon": "加蓬",
"Gambia": "冈比亚",
"Georgia": "格鲁吉亚",
"Germany": "德国",
"Ghana": "加纳",
"Greece": "希腊",
"Greenland": "格陵兰",
"Guatemala": "危地马拉",
"Guinea": "几内亚",
"Guinea-Bissau": "几内亚比绍",
"Guyana": "圭亚那",
"Haiti": "海地",
"Honduras": "洪都拉斯",
"Hungary": "匈牙利",
"Iceland": "冰岛",
"India": "印度",
"Indonesia": "印度尼西亚",
"Iran": "伊朗",
"Iraq": "伊拉克",
"Ireland": "爱尔兰",
"Isle of Man": "马恩岛",
"Israel": "以色列",
"Italy": "意大利",
"Jamaica": "牙买加",
"Japan": "日本",
"Jordan": "约旦",
"Kazakhstan": "哈萨克斯坦",
"Kenya": "肯尼亚",
"Korea": "韩国",
"Kuwait": "科威特",
"Kyrgyzstan": "吉尔吉斯斯坦",
"Lao PDR": "老挝",
"Latvia": "拉脱维亚",
"Lebanon": "黎巴嫩",
"Lesotho": "莱索托",
"Liberia": "利比里亚",
"Libya": "利比亚",
"Lithuania": "立陶宛",
"Luxembourg": "卢森堡",
"Macedonia": "马其顿",
"Madagascar": "马达加斯加",
"Malawi": "马拉维",
"Malaysia": "马来西亚",
"Maldives": "马尔代夫",
"Mali": "马里",
"Malta": "马耳他",
"Mauritania": "毛利塔尼亚",
"Mauritius": "毛里求斯",
"Mexico": "墨西哥",
"Moldova": "摩尔多瓦",
"Monaco": "摩纳哥",
"Mongolia": "蒙古",
"Montenegro": "黑山共和国",
"Morocco": "摩洛哥",
"Mozambique": "莫桑比克",
"Myanmar": "缅甸",
"Namibia": "纳米比亚",
"Nepal": "尼泊尔",
"Netherlands": "荷兰",
"New Caledonia": "新喀里多尼亚",
"New Zealand": "新西兰",
"Nicaragua": "尼加拉瓜",
"Niger": "尼日尔",
"Nigeria": "尼日利亚",
"Norway": "挪威",
"Oman": "阿曼",
"Pakistan": "巴基斯坦",
"Panama": "巴拿马",
"Papua New Guinea": "巴布亚新几内亚",
"Paraguay": "巴拉圭",
"Peru": "秘鲁",
"Philippines": "菲律宾",
"Poland": "波兰",
"Portugal": "葡萄牙",
"Puerto Rico": "波多黎各",
"Qatar": "卡塔尔",
"Reunion": "留尼旺",
"Romania": "罗马尼亚",
"Russia": "俄罗斯",
"Rwanda": "卢旺达",
"S. Geo. and S. Sandw. Is.": "南乔治亚和南桑威奇群岛",
"S. Sudan": "南苏丹",
"San Marino": "圣马力诺",
"Saudi Arabia": "沙特阿拉伯",
"Senegal": "塞内加尔",
"Serbia": "塞尔维亚",
"Sierra Leone": "塞拉利昂",
"Singapore": "新加坡",
"Slovakia": "斯洛伐克",
"Slovenia": "斯洛文尼亚",
"Solomon Is.": "所罗门群岛",
"Somalia": "索马里",
"South Africa": "南非",
"Spain": "西班牙",
"Sri Lanka": "斯里兰卡",
"Sudan": "苏丹",
"Suriname": "苏里南",
"Swaziland": "斯威士兰",
"Sweden": "瑞典",
"Switzerland": "瑞士",
"Syria": "叙利亚",
"Taiwan": "中国台湾",
"Tajikistan": "塔吉克斯坦",
"Tanzania": "坦桑尼亚",
"Thailand": "泰国",
"Togo": "多哥",
"Tonga": "汤加",
"Trinidad and Tobago": "特立尼达和多巴哥",
"Tunisia": "突尼斯",
"Turkey": "土耳其",
"Turkmenistan": "土库曼斯坦",
"U.S. Virgin Islands": "美属维尔京群岛",
"Uganda": "乌干达",
"Ukraine": "乌克兰",
"United Arab Emirates": "阿拉伯联合酋长国",
"United Kingdom": "英国",
"United States": "美国",
"Uruguay": "乌拉圭",
"Uzbekistan": "乌兹别克斯坦",
"Vanuatu": "瓦努阿图",
"Vatican City": "梵蒂冈城",
"Venezuela": "委内瑞拉",
"Vietnam": "越南",
"W. Sahara": "西撒哈拉",
"Yemen": "也门",
"Yugoslavia": "南斯拉夫",
"Zaire": "扎伊尔",
"Zambia": "赞比亚",
"Zimbabwe": "津巴布韦"
}
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -30,7 +30,7 @@ watch( ...@@ -30,7 +30,7 @@ watch(
<template> <template>
<div <div
v-loading="loading" v-loading="loading"
class="w-full h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]" class="iframe-wrap w-full h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-2px)]"
> >
<iframe <iframe
ref="frameRef" ref="frameRef"
...@@ -39,9 +39,24 @@ watch( ...@@ -39,9 +39,24 @@ watch(
scrolling="auto" scrolling="auto"
height="100%" height="100%"
width="100%" width="100%"
allow="fullscreen"
allowfullscreen="true" allowfullscreen="true"
webkitallowfullscreen="true" webkitallowfullscreen="true"
mozallowfullscreen="true" mozallowfullscreen="true"
></iframe> ></iframe>
</div> </div>
</template> </template>
<style scoped>
/* 防止 iframe 自带的边框或轮廓导致内容裁切 */
.iframe-wrap {
overflow: hidden;
}
.iframe-wrap iframe {
border: none !important;
outline: none !important;
display: block;
width: 100%;
height: 100%;
}
</style>
<template>
<div class="top-header">
<!-- <div class="date-time">{{ nowTime }}</div>-->
<div class="title">普惠算力平台驾驶舱</div>
<!-- <div class="placeholder"></div>-->
</div>
</template>
<script>
export default {
name: 'TopHeader',
data () {
return {
nowTime: null
}
},
created () {
// this.nowTimes()
},
methods: {
// 显示当前时间(年月日时分秒)
timeFormate (timeStamp) {
const year = new Date(timeStamp).getFullYear()
const month = new Date(timeStamp).getMonth() + 1 < 10 ? '0' + (new Date(timeStamp).getMonth() + 1) : new Date(timeStamp).getMonth() + 1
const date = new Date(timeStamp).getDate() < 10 ? '0' + new Date(timeStamp).getDate() : new Date(timeStamp).getDate()
const hh = new Date(timeStamp).getHours() < 10 ? '0' + new Date(timeStamp).getHours() : new Date(timeStamp).getHours()
const mm = new Date(timeStamp).getMinutes() < 10 ? '0' + new Date(timeStamp).getMinutes() : new Date(timeStamp).getMinutes()
const ss = new Date(timeStamp).getSeconds() < 10 ? '0' + new Date(timeStamp).getSeconds() : new Date(timeStamp).getSeconds()
this.nowTime = year + '.' + month + '.' + date + ' ' + ' ' + hh + ':' + mm + ':' + ss
},
nowTimes () {
this.timeFormate(new Date())
setInterval(this.nowTimes, 1000)
this.clear()
},
clear () {
clearInterval(this.nowTimes)
this.nowTimes = null
}
}
}
</script>
<style scoped lang="scss">
.top-header {
position: absolute;
box-sizing: border-box;
left: 0;
top: 0;
width: 100%;
//height: 258px;
/* 铺满头部区域,避免在缩放/iframe 下出现拉伸或留白 */
background: url("@/assets/images/nav-back.gif") no-repeat center center / 100% 100%;
/* 缩小两侧内边距,避免在 iframe(可能有边框)或 "cover" 裁剪下被切边 */
padding: 0 80px;
pointer-events: none;
z-index: 9999;
}
//.date-time {
// font-size: 46px;
// font-family: DIN;
// font-weight: bold;
// color: #FFFFFF;
// text-shadow: 0 0 20px rgba(142, 200, 206, 0.67);
// margin-top: -90px;
// word-spacing: 6px;
// letter-spacing: 6px;
//}
.title {
line-height: 180px;
text-align: center;
/* 降低字号,避免标题过大导致在 iframe 或全屏缩放时压迫布局 */
font-size: 56px;
font-family: DOUYU;
font-weight: normal;
text-shadow: -6px 6px 4px rgba(5,25,32,0.28);
color: #FFFFFF;
}
//.placeholder {
// width: 600px;
//}
</style>
<template>
<count-to
ref="countTo"
:startVal='startVal'
:endVal='numValue'
:decimals="decimals ? 2 : 0 "
:useEasing="count !== 0 "/>
</template>
<script>
// import CountTo from 'vue3-count-to'
export default {
name: 'AnimationCount',
components: {
// CountTo
},
props: {
endVal: {
type: Number,
default: 0
},
rangeMax: Number,
rangeMin: Number,
decimals: {
type: Boolean,
default: false
}
},
data () {
return {
numValue: 0,
startVal: 0,
count: 0
}
},
mounted () {
this.numValue = this.endVal
setInterval(() => {
this.count += 1
if (this.count > 0) {
this.startVal = this.numValue
}
const rangeMax = this.decimals ? this.rangeMax * 100 : this.rangeMax - 1
const rangeMin = this.decimals ? this.rangeMin * 100 : this.rangeMin
const num = Math.floor(Math.random() * rangeMax + rangeMin)
this.numValue = this.decimals ? (this.numValue + (num / 100)) : this.numValue + num
}, 2000)
}
}
// import CountTo from 'vue-count-to-ts'
// import { defineProps, ref, onMounted } from 'vue'
// const props = defineProps({
// endVal: {
// type: Number,
// default: 0
// },
// rangeMax: Number,
// rangeMin: Number,
// decimals: {
// type: Boolean,
// default: false
// }
// })
// const numValue = ref(0)
// onMounted(() => {
// numValue.value = props.endVal
// const math = props.decimals ? Math.fround : Math.random
// const num = Math.floor(math() * (props.rangeMax - 1) + props.rangeMin)
// console.log(num)
// numValue.value = numValue.value + num
// })
</script>
<style scoped>
</style>
...@@ -60,7 +60,11 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -60,7 +60,11 @@ const remainingRouter: AppRouteRecordRaw[] = [
children: [ children: [
{ {
path: 'index', path: 'index',
component: () => import('@/views/Home/Index.vue'), // 使用 IFrame 承载去壳版 Home 页面,避免覆盖外层菜单布局
component: () => import('@/components/IFrame/src/IFrame.vue'),
// 直接使用 Vite 的 BASE_URL(等于 vite.config.ts 的 base)拼出去壳地址
// 确保在自定义部署路径下也能正确指向 /screen
props: { src: import.meta.env.BASE_URL + 'screen' },
name: 'Index', name: 'Index',
meta: { meta: {
title: t('router.home'), title: t('router.home'),
...@@ -71,6 +75,18 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -71,6 +75,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
} }
] ]
}, },
// 去壳版 Home:用于 iframe 纯展示,无 Layout 外壳
{
path: '/screen',
name: 'HomeScreen',
component: () => import('@/views/Home/Home.vue'),
meta: {
hidden: true,
noTagsView: true,
canTo: true,
title: '屏幕展示'
}
},
{ {
path: '/user', path: '/user',
component: Layout, component: Layout,
......
@use './var.css'; @use './var.css';
/* 全局引入字体,确保 DOUYU/DIN 可用 */
@use '@/assets/font/index.scss' as *;
@use './FormCreate/index.scss'; @use './FormCreate/index.scss';
@use './theme.scss'; @use './theme.scss';
@use 'element-plus/theme-chalk/dark/css-vars.css'; @use 'element-plus/theme-chalk/dark/css-vars.css';
......
<template>
<div ref="mapRef" class="map-main"></div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
import * as echarts from 'echarts/core'
import { TooltipComponent, VisualMapComponent, GeoComponent, GridComponent, LegendComponent } from 'echarts/components'
import {
MapChart,
ScatterChart
// LinesChart, EffectScatterChart
} from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import geoJson from '@/assets/mapJson/china.json'
import labelGreen from '@/assets/images/label-green.png'
import labelRed from '@/assets/images/label-red.png'
echarts.use([
GridComponent,
LegendComponent,
TooltipComponent,
VisualMapComponent,
GeoComponent,
CanvasRenderer,
UniversalTransition,
MapChart,
ScatterChart
// LinesChart,
// EffectScatterChart
])
const geoCoordMap = ref({
浙江省: [120.153576, 30.287459],
上海市: [121.472644, 31.231706],
江苏省: [87.617733, 43.792818],
辽宁省: [123.429096, 41.796767],
湖南省: [112.982279, 28.19409],
山西省: [112.549248, 37.857014],
甘肃省: [103.823557, 36.058039],
福建省: [119.306239, 26.075302]
})
const mapData = ref([
// {
// name: '浙江省',
// value: 5363200
// },
// {
// name: '上海市',
// value: 4472670
// },
// {
// name: '江苏省',
// value: 1550000
// },
// {
// name: '辽宁省',
// value: 265720
// }
])
const mapData2 = ref([
{
name: '浙江省',
value: 12.4920
},
{
name: '山西省',
value: 1.7948
},
{
name: '江苏省',
value: 19.0543
},
{
name: '福建省',
value: 7.4283
},
{
name: '甘肃省',
value: 3.5283
}
])
const convertData = function (data) {
const res = []
for (let i = 0; i < data.length; i++) {
const geoCoord = geoCoordMap.value[data[i].name]
if (geoCoord) {
res.push({
name: data[i].name,
value: geoCoord.concat(data[i].value)
})
}
}
return res
}
function initMapChart (el) {
const chartDom = el
const myChart = echarts.init(chartDom)
echarts.registerMap('china', geoJson)
const option = {
// backgroundColor: '#081627',
// darkMode: true,
// tooltip: {
// trigger: 'item',
// formatter: item => {
// const value = typeof (item.value) === 'number' ? item.value : item.value[2]
// return `${item.name}:${value || 0}`
// }
// },
geo: {
map: 'china',
zoom: 1.0,
roam: false,
tooltip: {
show: false
},
center: [106.321935, 23.474133],
scaleLimit: {
min: 1
},
label: {
show: false,
color: '#fff'
},
itemStyle: {
borderColor: '#00BDCA',
borderWidth: 2,
areaColor: {
type: 'linear',
x: 330,
y: 330,
x2: 1000,
y2: 1000,
colorStops: [
{
offset: 0,
color: '#0177d2' // 0% 处的颜色
},
{
offset: 1,
color: '#2BBAC7' // 100% 处的颜色
}
],
global: true
}
}
},
series: [
// {
// name: 'china',
// type: 'map',
// map: 'china',
// geoIndex: 0,
// roam: true,
// tooltip: {
// show: false
// }
// },
{
type: 'scatter',
coordinateSystem: 'geo',
data: convertData(mapData.value),
encode: {
value: 2
},
symbol: 'circle',
symbolSize: 10,
label: {
show: true,
position: [-25, -80],
width: 360,
height: 72,
color: '#FFFFFF',
backgroundColor: { image: labelGreen },
padding: [10, 50, 20, 40],
lineHeight: 25,
rich: {
txt: {
align: 'left',
padding: [40, 0, 0, 130],
fontSize: 22
},
txt2: {
fontSize: 34,
padding: [38, 0, 0, 0]
},
txt3: {
fontSize: 24,
padding: [44, 0, 0, 2]
}
},
formatter (params) {
const {
// dataIndex,
data
} = params
if (data && data.value) {
return `{txt|${'发电量'}}{txt2|${data.value[2]}}{txt3|${'GWh'}}`
}
return 'data'
}
}
},
{
type: 'scatter',
coordinateSystem: 'geo',
data: convertData(mapData2.value),
encode: {
value: 2
},
symbol: 'circle',
symbolSize: 0,
label: {
show: true,
position: [-25, -80],
width: 360,
height: 72,
color: '#FFFFFF',
backgroundColor: { image: labelRed },
padding: [10, 50, 20, 40],
lineHeight: 25,
rich: {
txt: {
align: 'left',
padding: [40, 0, 0, 130],
fontSize: 22
},
txt2: {
fontSize: 36,
padding: [38, 0, 0, 10]
},
txt3: {
fontSize: 24,
padding: [44, 0, 0, 7]
}
},
formatter (params) {
const {
// dataIndex,
data
} = params
if (data && data.value) {
return `{txt|${' 算力资源'}}{txt2|${data.value[2]}}{txt3|${'P'}}`
}
return 'data'
}
}
}
]
}
chartDom.setAttribute('_echarts_instance_', '')
myChart.setOption(option)
return myChart
}
const mapRef = ref(null)
let chartInst = null
const resize = () => {
if (!chartInst) return
chartInst.resize({ animation: { duration: 0 } })
}
onMounted(async () => {
await nextTick()
if (mapRef.value) {
chartInst = initMapChart(mapRef.value)
window.addEventListener('resize', resize)
document.addEventListener('fullscreenchange', resize)
}
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
document.removeEventListener('fullscreenchange', resize)
chartInst && chartInst.dispose?.()
chartInst = null
})
</script>
<style scoped>
.map-main {
width: 100%;
height: 100%;
min-height: 800px;
}
</style>
<template>
<div class="envc-wrap">
<div class="dim-switch">
<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>
</div>
<div ref="chartRef" class="echart-wrap"></div>
</div>
</template>
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue'
import * as echarts from 'echarts/core'
import { TitleComponent, TooltipComponent, LegendComponent } from 'echarts/components'
import { PieChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([TitleComponent, TooltipComponent, LegendComponent, PieChart, CanvasRenderer])
const chartRef = ref(null)
let chart = null
const dim = ref('gpu')
const datasets = {
gpu: [
{ value: 45, name: '4090' },
{ value: 30, 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 titles = {
gpu: 'GPU型号分布',
source: '算力来源占比',
resource: '计算资源占比'
}
const baseColors = ['#16FCFF', '#39E9D5', '#1DBAFF', '#77CCFF', '#E99102', '#3AEDCE']
const getOption = (which) => {
const data = datasets[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 {
color: baseColors,
title: {
text: titles[which],
left: 'center',
top: 12,
textStyle: { color: '#77CCFF', fontSize: 28, fontWeight: 600 }
},
tooltip: {
trigger: 'item',
formatter: ({ name, value, percent }) => `${name}: ${value} (${percent}%)`
},
legend: {
bottom: 8,
left: 'center',
itemWidth: 12,
itemHeight: 12,
textStyle: { color: '#DAF7FF', fontSize: 18 },
data: data.map((d) => d.name),
formatter: (name) => {
const v = dataMap[name] ?? 0
const p = total ? Math.round((v / total) * 100) : 0
return `${name} ${v} (${p}%)`
}
},
graphic: [
{
type: 'text',
left: '50%',
top: '55%',
style: {
text: `${titles[which]}\n总计: ${total}`,
fill: '#FFFFFF',
fontSize: 20,
fontWeight: 'bold',
textAlign: 'center',
textVerticalAlign: 'middle',
lineHeight: 28,
textShadowColor: 'rgba(5,25,32,0.6)',
textShadowBlur: 8
}
}
],
series: [
{
name: titles[which],
type: 'pie',
radius: '62%', // 实心饼图(非环形)
center: ['50%', '58%'],
avoidLabelOverlap: true,
selectedMode: false,
labelLayout: { hideOverlap: true },
label: {
color: '#FFFFFF',
fontSize: 20,
formatter: '{b}\n{d}%'
},
labelLine: { length: 18, length2: 10 },
emphasis: {
scale: true,
scaleSize: 8
},
data
}
]
}
}
const render = () => {
if (!chartRef.value) return
chart = chart || echarts.init(chartRef.value)
chart.setOption(getOption(dim.value), true)
}
const resize = () => chart && chart.resize()
const setDim = (which) => {
if (dim.value === which) return
dim.value = which
}
watch(dim, () => {
if (chart) chart.setOption(getOption(dim.value), true)
})
onMounted(() => {
render()
window.addEventListener('resize', resize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
if (chart) {
chart.dispose()
chart = null
}
})
</script>
<style scoped lang="scss">
.envc-wrap {
position: relative;
}
.dim-switch {
position: absolute;
right: 20px;
top: 20px;
z-index: 2;
}
.dim-switch button {
font-size: 26px;
color: #ffffff;
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);
}
.echart-wrap {
/* 给绝对定位的父级 .header-title 留出空间,避免重叠 */
height: 640px;
padding-top: 100px;
box-sizing: border-box;
}
</style>
<template>
<div id="nvestment" class="echart-wrap"></div>
</template>
<script setup>
import { onMounted } from 'vue'
import * as echarts from 'echarts/core'
import {
TitleComponent,
ToolboxComponent,
TooltipComponent,
GridComponent,
LegendComponent
} from 'echarts/components'
import { LineChart, BarChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
echarts.use([
TitleComponent,
ToolboxComponent,
TooltipComponent,
GridComponent,
LegendComponent,
LineChart,
BarChart,
CanvasRenderer,
UniversalTransition
])
function init () {
const chartDom = document.getElementById('nvestment')
const myChart = echarts.init(chartDom)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: ['水能消耗', '电能消耗'],
show: false,
right: '2%',
top: 30,
icon: 'pin',
itemGap: 40,
textStyle: {
color: '#77CCFF',
fontSize: 24
}
},
grid: {
// top: 100,
left: '3%',
right: '2%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
boundaryGap: true,
data: ['2018', '2019', '2020', '2021', '2022'],
axisLabel: {
fontSize: 24,
color: '#ffffff'
},
axisLine: {
lineStyle: {
color: '#105E86'
}
}
}
],
yAxis: [
{
type: 'value',
name: '(万元)',
// min: 0,
// interval: 2,
nameTextStyle: {
fontSize: 24,
color: '#77CCFF'
},
axisLabel: {
fontSize: 24,
color: '#77CCFF'
},
splitLine: {
lineStyle: {
color: '#105E86'
}
}
},
{
type: 'value',
name: '(万元)',
nameTextStyle: {
fontSize: 24,
color: '#77CCFF'
},
axisLabel: {
fontSize: 24,
color: '#77CCFF'
},
splitLine: {
show: false,
lineStyle: {
color: '#105E86'
}
}
}
],
series: [
{
name: '研发投入',
type: 'bar',
// showBackground: true,
barMaxWidth: 40,
itemStyle: {
borderRadius: [100, 100, 100, 100],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgb(252, 174, 24)'
},
{
offset: 1,
color: 'rgba(233, 145, 2,.1)'
}
])
},
data: [10.19, 189.61, 120.4, 75.14, '']
},
{
name: '历年营收',
yAxisIndex: 1,
type: 'line',
smooth: true,
lineStyle: {
width: 7
},
showSymbol: false,
itemStyle: {
color: '#3AEDCE'
},
emphasis: {
// focus: 'series'
},
data: [23.22, 12027.48, 75935.47, 195109.5381, '']
}
]
}
option && myChart.setOption(option)
}
onMounted(() => {
init()
})
</script>
<style scoped lang="scss">
.echart-wrap {
height: 500px;
}
</style>
<template>
<div ref="mapRef" class="map-main"></div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, nextTick } from 'vue'
import * as echarts from 'echarts/core'
import { TooltipComponent, VisualMapComponent, GeoComponent, GridComponent, LegendComponent } from 'echarts/components'
import { MapChart, LinesChart, EffectScatterChart } from 'echarts/charts'
import { UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import geoJson from '@/assets/mapJson/world.json'
import zhName from '@/assets/mapJson/country-name-zh.json'
echarts.use([
GridComponent,
LegendComponent,
TooltipComponent,
VisualMapComponent,
GeoComponent,
CanvasRenderer,
UniversalTransition,
MapChart,
LinesChart,
EffectScatterChart
])
// 初始化中间地图
// eslint-disable-next-line no-unused-vars
function initMapChart (DTData = [], el) {
// 给每个城市设置经纬度
const geoCoordMap = {
一道: [118.81033, 28.998632],
北美1: [-115.132541, 63.675557],
北美2: [-65.386447, 55.739546],
北美3: [-99.268283, 19.808162],
南美1: [-67.276096, 2.284665],
南美2: [-67.80344, -39.164052],
非洲1: [1.542264, 27.605773],
非洲2: [34.76492, 14.349659],
非洲3: [45.311795, 5.528625],
亚洲1: [70.975857, 37.090331],
亚洲2: [85.38992, 41.31091],
亚洲3: [116.854764, 42.098308],
大洋洲1: [135.311795, -24.447045]
}
const convertData = function (data) {
const res = []
for (let i = 0; i < data.length; i++) {
const dataItem = data[i]
const fromCoord = geoCoordMap[dataItem[0].name]
const toCoord = geoCoordMap[dataItem[1].name]
if (fromCoord && toCoord) {
res.push({
fromName: dataItem[0].name,
toName: dataItem[1].name,
coords: [fromCoord, toCoord],
value: dataItem[1].value
})
}
}
return res
}
const lines = [];
[
['一道', DTData]
].forEach(function (item, i) {
lines.push(
{
type: 'lines',
seriesIndex: '',
zlevel: 1,
symbol: ['none', 'none'],
symbolSize: 10,
effect: {
show: true,
color: '#fff',
period: 6,
trailLength: 0,
symbol: 'arrow',
symbolSize: 20
},
lineStyle: {
color: '#FFEA00',
type: 'dotted',
width: 4,
opacity: 0.6,
curveness: 0.2
},
tooltip: {
show: false
},
data: convertData(item[1])
},
{
type: 'effectScatter',
coordinateSystem: 'geo',
zlevel: 2,
rippleEffect: {
brushType: 'fill'
},
symbolSize: 20,
itemStyle: {
color: '#FFEA00'
},
tooltip: {
show: true
},
data: item[1].map(function (dataItem) {
return {
name: dataItem[1].name,
value: geoCoordMap[dataItem[1].name].concat([dataItem[1].value])
}
})
}
)
})
const chartDom = el
const myChart = echarts.init(chartDom)
echarts.registerMap('world', geoJson)
const option = {
// backgroundColor: '#081627',
// darkMode: true,
tooltip: {
trigger: 'item',
formatter: item => {
const value = typeof (item.value) === 'number' ? item.value : item.value[2]
return `${item.name}${value || 0}`
}
},
geo: {
map: 'world',
nameMap: zhName,
zoom: 1.3,
roam: true,
tooltip: {
show: false
},
center: [7.351834, 9.341271],
scaleLimit: {
min: 1
},
label: {
show: false,
color: '#fff'
},
itemStyle: {
// 地图区域的多边形 图形样式。
// borderColor: '#F3B152', // 边框颜色
borderWidth: 0,
areaColor: {
type: 'linear',
x: 330,
y: 330,
x2: 1000,
y2: 1000,
colorStops: [
{
offset: 0,
color: '#0177d2' // 0% 处的颜色
},
{
offset: 1,
color: '#2BBAC7' // 100% 处的颜色
}
],
global: true
}
}
},
series: [
{
name: 'world',
type: 'map',
map: 'world',
nameMap: zhName,
geoIndex: 0,
roam: true,
tooltip: {
show: false
}
},
...lines
]
}
chartDom.setAttribute('_echarts_instance_', '')
myChart.setOption(option)
return myChart
}
const mapRef = ref(null)
let chartInst = null
const resize = () => {
if (!chartInst) return
// 重新自适应视口,使缩放跟随容器变化
// 多次触发防抖后的最终 resize,确保全屏切换、容器缩放后尺寸正确
chartInst.resize({ animation: { duration: 0 } })
}
onMounted(async () => {
const DTData = [
[{ name: '一道' }, { name: '北美1', value: 782 }],
[{ name: '一道' }, { name: '北美2', value: 1427 }],
[{ name: '一道' }, { name: '北美3', value: 2197 }],
[{ name: '一道' }, { name: '南美1', value: 2197 }],
[{ name: '一道' }, { name: '南美2', value: 2197 }],
[{ name: '一道' }, { name: '非洲1', value: 2197 }],
[{ name: '一道' }, { name: '非洲2', value: 2197 }],
[{ name: '一道' }, { name: '非洲3', value: 2197 }],
[{ name: '一道' }, { name: '亚洲1', value: 2197 }],
[{ name: '一道' }, { name: '亚洲2', value: 2197 }],
[{ name: '一道' }, { name: '亚洲3', value: 2197 }],
[{ name: '一道' }, { name: '大洋洲1', value: 2197 }]
]
await nextTick()
if (mapRef.value) {
chartInst = initMapChart(DTData, mapRef.value)
window.addEventListener('resize', resize)
// 监听全屏变化,强制刷新尺寸
document.addEventListener('fullscreenchange', resize)
}
})
onBeforeUnmount(() => {
window.removeEventListener('resize', resize)
document.removeEventListener('fullscreenchange', resize)
chartInst && chartInst.dispose?.()
chartInst = null
})
</script>
<style scoped>
.map-main {
width: 100%;
height: 1300px;
}
</style>
<template>
<div
class="trend-wrap"
:class="{ 'is-iframe': inIframe, 'is-fullscreen': isFullscreen }"
:style="{ padding: outerPadding }"
>
<div class="trend-toolbar">
<div class="switcher">
<button
:class="{ active: rangeType === 'd' }"
@click="changeRange('d')"
type="button"
>日</button>
<button
:class="{ active: rangeType === 'm' }"
@click="changeRange('m')"
type="button"
>月</button>
<button
:class="{ active: rangeType === 'y' }"
@click="changeRange('y')"
type="button"
>年</button>
</div>
</div>
<div ref="chartRef" class="echart-wrap"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } 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'
echarts.use([GridComponent, TooltipComponent, LegendComponent, LineChart, CanvasRenderer])
const chartRef = ref(null)
let chart = null
// 维度切换:d=日, m=月, y=年
const rangeType = ref('m')
// 嵌入 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 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 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 getOption(x, y) {
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'line' },
formatter: (params) => {
const p = params && params[0]
if (!p) return ''
return `${p.axisValue}<br/>请求次数:${p.data.toLocaleString()}`
}
},
grid: { left: 90, right: 30, top: 30, bottom: 40, containLabel: true },
xAxis: {
type: 'category',
data: x,
boundaryGap: false,
axisLabel: { color: '#FFFFFF', fontSize: 22 },
axisLine: { lineStyle: { color: '#105E86' } }
},
yAxis: {
type: 'value',
name: '请求次数',
nameLocation: 'middle',
nameRotate: 90,
nameGap: 50,
nameTextStyle: { color: '#77CCFF', fontSize: 20 },
axisLabel: {
color: '#77CCFF', fontSize: 20,
formatter: (v) => v >= 10000 ? (v / 10000).toFixed(0) + '万' : v
},
splitLine: { lineStyle: { color: '#105E86' } }
},
series: [
{
name: 'API 请求次数',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 10,
lineStyle: { width: 5, color: '#3AEDCE' },
itemStyle: { color: '#16FCFF', borderColor: '#0B6065' },
areaStyle: {
opacity: 0.25,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(22, 252, 255, 0.35)' },
{ offset: 1, color: 'rgba(22, 252, 255, 0.05)' }
])
},
data: 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))
}
if (!chart) chart = echarts.init(chartRef.value)
chart.setOption(getOption(x, y), true)
}
const onResize = () => chart && chart.resize()
function changeRange(t) {
if (t === rangeType.value) return
rangeType.value = t
render()
}
function detectIframe() {
try {
inIframe.value = window.self !== window.top
} catch (e) {
inIframe.value = true
}
}
function onFsChange() {
const el = document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement
isFullscreen.value = !!el
// 全屏切换后主动调整图表
updateOuterPadding()
onResize()
}
function updateOuterPadding() {
// iframe 下留出边距避免裁切;全屏时尽量铺满
if (isFullscreen.value) {
outerPadding.value = '0px'
} else if (inIframe.value) {
outerPadding.value = '12px'
} else {
outerPadding.value = '0px'
}
}
onMounted(() => {
detectIframe()
render()
updateOuterPadding()
window.addEventListener('resize', onResize)
document.addEventListener('fullscreenchange', onFsChange)
document.addEventListener('webkitfullscreenchange', onFsChange)
document.addEventListener('mozfullscreenchange', onFsChange)
document.addEventListener('MSFullscreenChange', onFsChange)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
document.removeEventListener('fullscreenchange', onFsChange)
document.removeEventListener('webkitfullscreenchange', onFsChange)
document.removeEventListener('mozfullscreenchange', onFsChange)
document.removeEventListener('MSFullscreenChange', onFsChange)
if (chart) {
chart.dispose()
chart = null
}
})
</script>
<style scoped lang="scss">
.trend-wrap {
position: relative;
}
.trend-toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 8px;
padding: 0 6px;
}
.trend-toolbar .muted { color: rgba(218,247,255,0.7); font-size: 16px; }
.trend-toolbar .switcher {
display: flex;
gap: 10px;
}
.trend-toolbar .switcher button {
font-size: 18px;
line-height: 1;
padding: 6px 14px;
color: #A3E9FF;
background: rgba(16, 94, 134, 0.35);
border: 1px solid #1DBAFF;
border-radius: 2px;
cursor: pointer;
}
.trend-toolbar .switcher button.active {
color: #FFFFFF;
background: linear-gradient(90deg, rgba(19,101,106,0.9), rgba(38,169,163,0.9));
}
.echart-wrap {
height: 560px;
padding-top: 30px;
}
/* iframe 下在外层留出边距,避免缩放/裁切时被切边 */
.is-iframe {
box-sizing: border-box;
}
.is-fullscreen {
box-sizing: border-box;
}
</style>
<template>
<div class="order-manage-wrap">
<div class="toolbar">
<div class="switcher">
<button :class="{ active: dateType === 'd' }" @click="changeType('d')" type="button">日</button>
<button :class="{ active: dateType === 'm' }" @click="changeType('m')" type="button">月</button>
<button :class="{ active: dateType === 'y' }" @click="changeType('y')" type="button">年</button>
</div>
<!-- 全屏按钮 -->
<button
class="fs-float-btn"
type="button"
@click="toggleFs"
:title="fsIsFullscreen ? '退出全屏' : '全屏显示'"
>
<span v-if="!fsIsFullscreen"></span>
<span v-else>×</span>
</button>
</div>
<div id="energyManage" class="echart-wrap"></div>
</div>
</template>
<script setup lang="ts">
import { inject, computed, onMounted, onBeforeUnmount, ref } 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])
// 从父组件(Home.vue)注入全屏状态与方法
const fs = inject('fsState', null) as null | { isFullscreen: any; toggleFullscreen: () => void }
const fsIsFullscreen = computed(() => !!(fs && fs.isFullscreen && fs.isFullscreen.value))
const toggleFs = () => {
if (fs && typeof fs.toggleFullscreen === 'function') fs.toggleFullscreen()
}
const { t } = useI18n()
// 维度切换:d=日,m=月,y=年
const dateType = ref<'d' | 'm' | 'y'>('d')
let chart: echarts.ECharts | null = null
function getOption(xData: string[], seriesData: {
computeCount: number[]
apiCount: number[]
computeAmount: number[]
apiAmount: number[]
}): EChartsOption {
return {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['算力订单', 'API订单', '算力订单金额', 'API订单金额'],
right: '2%',
top: 0,
textStyle: { color: '#A3E9FF', fontSize: 18 }
},
grid: { left: 80, right: 80, top: 40, bottom: 30, containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: xData,
axisLabel: { color: '#FFFFFF', fontSize: 20 },
axisLine: { lineStyle: { color: '#105E86' } }
},
yAxis: [
{
type: 'value',
name: '数量',
nameTextStyle: { color: '#77CCFF', fontSize: 18 },
axisLabel: { color: '#77CCFF', fontSize: 18 },
splitLine: { lineStyle: { color: '#105E86' } }
},
{
type: 'value',
name: '金额(元)',
position: 'right',
nameTextStyle: { color: '#77CCFF', fontSize: 18 },
axisLabel: {
color: '#77CCFF', fontSize: 18,
formatter: (v: number) => Number(v).toLocaleString()
},
splitLine: { show: false }
}
],
series: [
{
name: '算力订单',
type: 'line',
smooth: false,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#67C23A' },
lineStyle: { width: 3, color: '#67C23A' },
data: seriesData.computeCount
},
{
name: 'API订单',
type: 'line',
smooth: false,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#409EFF' },
lineStyle: { width: 3, color: '#409EFF' },
data: seriesData.apiCount
},
{
name: '算力订单金额',
type: 'line',
smooth: false,
yAxisIndex: 1,
symbol: 'diamond',
symbolSize: 6,
itemStyle: { color: '#94D274' },
lineStyle: { width: 3, color: '#94D274', type: 'dashed' },
data: seriesData.computeAmount
},
{
name: 'API订单金额',
type: 'line',
smooth: false,
yAxisIndex: 1,
symbol: 'diamond',
symbolSize: 6,
itemStyle: { color: '#7CB6FF' },
lineStyle: { width: 3, color: '#7CB6FF', type: 'dashed' },
data: seriesData.apiAmount
}
]
} as EChartsOption
}
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)))
if (!chart) {
const el = document.getElementById('energyManage') as HTMLElement | null
if (!el) return
chart = echarts.init(el)
}
chart.setOption(getOption(x, { computeCount, apiCount, computeAmount, apiAmount }), true)
}
function changeType(t: 'd' | 'm' | 'y') {
if (t === dateType.value) return
dateType.value = t
render()
}
const onResize = () => chart && chart.resize()
onMounted(() => {
render()
window.addEventListener('resize', onResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
if (chart) {
chart.dispose()
chart = null
}
})
</script>
<style scoped lang="scss">
.order-manage-wrap { position: relative; }
.toolbar {
position: absolute;
right: 20px;
top: 20px;
z-index: 2;
display: flex;
align-items: center;
gap: 12px;
}
.switcher {
display: flex;
gap: 10px;
}
.switcher button {
font-size: 18px;
line-height: 1;
padding: 6px 14px;
color: #A3E9FF;
background: rgba(16, 94, 134, 0.35);
border: 1px solid #1DBAFF;
border-radius: 2px;
cursor: pointer;
}
.switcher button.active {
color: #FFFFFF;
background: linear-gradient(90deg, rgba(19,101,106,0.9), rgba(38,169,163,0.9));
}
.echart-wrap { height: 640px; padding-top: 100px; box-sizing: border-box; }
/* 全屏按钮样式,与项目现有风格一致 */
.fs-float-btn {
width: 36px;
height: 36px;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 14px;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 6px;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(4px);
cursor: pointer;
transition: all 0.2s ease;
}
.fs-float-btn:hover {
background: rgba(0, 0, 0, 0.45);
border-color: rgba(255, 255, 255, 0.5);
}
</style>
<template>
<div class="order-manage-wrap">
<div class="toolbar">
<div class="switcher">
<button :class="{ active: dateType === 'd' }" @click="changeType('d')" type="button">日</button>
<button :class="{ active: dateType === 'm' }" @click="changeType('m')" type="button">月</button>
<button :class="{ active: dateType === 'y' }" @click="changeType('y')" type="button">年</button>
</div>
</div>
<div id="userManageChart" class="echart-wrap"></div>
</div>
</template>
<script setup lang="ts">
import { inject, computed, onMounted, onBeforeUnmount, ref } 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])
// 从父组件(Home.vue)注入全屏状态与方法
const fs = inject('fsState', null) as null | { isFullscreen: any; toggleFullscreen: () => void }
const fsIsFullscreen = computed(() => !!(fs && fs.isFullscreen && fs.isFullscreen.value))
const toggleFs = () => {
if (fs && typeof fs.toggleFullscreen === 'function') fs.toggleFullscreen()
}
const { t } = useI18n()
// 维度切换:d=日,m=月,y=年
const dateType = ref<'d' | 'm' | 'y'>('d')
let chart: echarts.ECharts | null = null
function getOption(xData: string[], growth: number[], active: number[]): EChartsOption {
return {
tooltip: { trigger: 'axis' },
legend: {
data: ['增长用户数', '活跃用户数'],
right: '2%',
top: 0,
textStyle: { color: '#A3E9FF', fontSize: 18 }
},
grid: { left: 80, right: 40, top: 40, bottom: 30, containLabel: true },
xAxis: {
type: 'category',
boundaryGap: false,
data: xData,
axisLabel: { color: '#FFFFFF', fontSize: 20 },
axisLine: { lineStyle: { color: '#105E86' } }
},
yAxis: {
type: 'value',
name: '用户数',
nameTextStyle: { color: '#77CCFF', fontSize: 18 },
axisLabel: { color: '#77CCFF', fontSize: 18 },
splitLine: { lineStyle: { color: '#105E86' } }
},
series: [
{
name: '增长用户数',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
itemStyle: { color: '#67C23A' },
lineStyle: { width: 3, color: '#67C23A' },
areaStyle: { opacity: 0.08 },
data: growth
},
{
name: '活跃用户数',
type: 'line',
smooth: true,
symbol: 'diamond',
symbolSize: 6,
itemStyle: { color: '#409EFF' },
lineStyle: { width: 3, color: '#409EFF' },
areaStyle: { opacity: 0.06 },
data: active
}
]
} as EChartsOption
}
async function render() {
const res = await IndexCountApi.getUsersData(dateType.value)
const arr = Array.isArray(res) ? res : []
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) =>
item.activeUsersCount ?? item.visitUserCount ?? Math.round((item.usersCount ?? 0) * 1.4 + (idx % 3 === 0 ? 3 : 8))
)
if (!chart) {
const el = document.getElementById('userManageChart') as HTMLElement | null
if (!el) return
chart = echarts.init(el)
}
chart.setOption(getOption(x, growth, active), true)
}
function changeType(t: 'd' | 'm' | 'y') {
if (t === dateType.value) return
dateType.value = t
render()
}
const onResize = () => chart && chart.resize()
onMounted(() => {
render()
window.addEventListener('resize', onResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize)
if (chart) {
chart.dispose()
chart = null
}
})
</script>
<style scoped lang="scss">
.order-manage-wrap { position: relative; }
.toolbar {
position: absolute;
right: 20px;
top: 20px;
z-index: 2;
display: flex;
align-items: center;
gap: 12px;
}
.switcher {
display: flex;
gap: 10px;
}
.switcher button {
font-size: 18px;
line-height: 1;
padding: 6px 14px;
color: #A3E9FF;
background: rgba(16, 94, 134, 0.35);
border: 1px solid #1DBAFF;
border-radius: 2px;
cursor: pointer;
}
.switcher button.active {
color: #FFFFFF;
background: linear-gradient(90deg, rgba(19,101,106,0.9), rgba(38,169,163,0.9));
}
.echart-wrap { height: 560px; padding-top: 100px; box-sizing: border-box; }
</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