@ -0,0 +1,26 @@
|
||||
package dto
|
||||
|
||||
type DashboardBase struct {
|
||||
HaloEnabled bool `json:"haloEnabled"`
|
||||
DateeaseEnabled bool `json:"dateeaseEnabled"`
|
||||
JumpServerEnabled bool `json:"jumpserverEnabled"`
|
||||
MeterSphereEnabled bool `json:"metersphereEnabled"`
|
||||
|
||||
WebsiteNumber int `json:"websiteNumber"`
|
||||
DatabaseNumber int `json:"databaseNumber"`
|
||||
CronjobNumber int `json:"cronjobNumber"`
|
||||
AppInstalldNumber int `json:"appInstalldNumber"`
|
||||
|
||||
HostName string `json:"hostname"`
|
||||
Os string `json:"os"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformFamily string `json:"platformFamily"`
|
||||
PlatformVersion string `json:"platformVersion"`
|
||||
KernelArch string `json:"kernelArch"`
|
||||
VirtualizationSystem string `json:"virtualizationSystem"`
|
||||
|
||||
CPUCores int `json:"cpuCores"`
|
||||
CPULogicalCores int `json:"cpuLogicalCores"`
|
||||
CPUModelName string `json:"cpuModelName"`
|
||||
CPUPercent float64 `json:"cpuPercent"`
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/backend/app/dto"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
)
|
||||
|
||||
type DashboardService struct{}
|
||||
|
||||
type IDashboardService interface {
|
||||
LoadBaseInfo() (*dto.DashboardBase, error)
|
||||
}
|
||||
|
||||
func NewIDashboardService() IDashboardService {
|
||||
return &DashboardService{}
|
||||
}
|
||||
func (u *DashboardService) LoadBaseInfo() (*dto.DashboardBase, error) {
|
||||
var baseInfo dto.DashboardBase
|
||||
hostInfo, err := host.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := copier.Copy(baseInfo, hostInfo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appInstall, err := appInstallRepo.GetBy()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, app := range appInstall {
|
||||
switch app.App.Key {
|
||||
case "dateease":
|
||||
baseInfo.DateeaseEnabled = true
|
||||
case "halo":
|
||||
baseInfo.HaloEnabled = true
|
||||
case "metersphere":
|
||||
baseInfo.MeterSphereEnabled = true
|
||||
case "jumpserver":
|
||||
baseInfo.JumpServerEnabled = true
|
||||
}
|
||||
}
|
||||
baseInfo.AppInstalldNumber = len(appInstall)
|
||||
dbs, err := mysqlRepo.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseInfo.DatabaseNumber = len(dbs)
|
||||
cornjobs, err := cronjobRepo.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseInfo.DatabaseNumber = len(cornjobs)
|
||||
|
||||
cpuInfo, err := cpu.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseInfo.CPUModelName = cpuInfo[0].ModelName
|
||||
baseInfo.CPUCores, _ = cpu.Counts(false)
|
||||
baseInfo.CPULogicalCores, _ = cpu.Counts(true)
|
||||
totalPercent, _ := cpu.Percent(1*time.Second, false)
|
||||
if len(totalPercent) == 1 {
|
||||
baseInfo.CPUPercent = totalPercent[0]
|
||||
}
|
||||
|
||||
return &baseInfo, nil
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package configs
|
||||
|
||||
type GeneralDB struct {
|
||||
Path string `mapstructure:"path"` // 服务器地址
|
||||
Port string `mapstructure:"port"` //:端口
|
||||
Config string `mapstructure:"config"` // 高级配置
|
||||
Dbname string `mapstructure:"db_name"` // 数据库名
|
||||
Username string `mapstructure:"username"` // 数据库用户名
|
||||
Password string `mapstructure:"password"` // 数据库密码
|
||||
MaxIdleConns int `mapstructure:"max_idle_conns"` // 空闲中的最大连接数
|
||||
MaxOpenConns int `mapstructure:"max_open_conns"` // 打开到数据库的最大连接数
|
||||
DbFile string `mapstructure:"db_file"`
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package configs
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Mysql struct {
|
||||
GeneralDB `yaml:",inline" mapstructure:",squash"`
|
||||
}
|
||||
|
||||
func (m *Mysql) Dsn() string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Asia%%2FShanghai", m.Username, m.Password, m.Path, m.Port, m.Dbname)
|
||||
}
|
@ -1,9 +1,25 @@
|
||||
package configs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Sqlite struct {
|
||||
GeneralDB `yaml:",inline" mapstructure:",squash"`
|
||||
Path string `mapstructure:"path"`
|
||||
DbFile string `mapstructure:"db_file"`
|
||||
}
|
||||
|
||||
func (s *Sqlite) Dsn() string {
|
||||
if _, err := os.Stat(s.Path); err != nil {
|
||||
if err := os.MkdirAll(s.Path, os.ModePerm); err != nil {
|
||||
panic(fmt.Errorf("init db dir falied, err: %v", err))
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(s.Path + "/" + s.DbFile); err != nil {
|
||||
if _, err := os.Create(s.Path + "/" + s.DbFile); err != nil {
|
||||
panic(fmt.Errorf("init db file falied, err: %v", err))
|
||||
}
|
||||
}
|
||||
return s.Path + "/" + s.DbFile
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
package db
|
||||
|
||||
import "github.com/1Panel-dev/1Panel/backend/global"
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
switch global.CONF.System.DbType {
|
||||
case "mysql":
|
||||
global.DB = MysqlGorm()
|
||||
case "sqlite":
|
||||
global.DB = SqliteGorm()
|
||||
default:
|
||||
global.DB = MysqlGorm()
|
||||
s := global.CONF.Sqlite
|
||||
db, err := gorm.Open(sqlite.Open(s.Dsn()), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
global.DB = db
|
||||
}
|
||||
|
@ -1,28 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func MysqlGorm() *gorm.DB {
|
||||
m := global.CONF.Mysql
|
||||
if m.Dbname == "" {
|
||||
return nil
|
||||
}
|
||||
mysqlConfig := mysql.Config{
|
||||
DSN: m.Dsn(),
|
||||
DefaultStringSize: 191,
|
||||
SkipInitializeWithVersion: false,
|
||||
}
|
||||
if db, err := gorm.Open(mysql.New(mysqlConfig), &gorm.Config{}); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
|
||||
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
|
||||
return db
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/backend/global"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SqliteGorm() *gorm.DB {
|
||||
s := global.CONF.Sqlite
|
||||
if db, err := gorm.Open(sqlite.Open(s.Dsn()), &gorm.Config{}); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return db
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { Layout } from '@/routers/constant';
|
||||
|
||||
const toolBoxRouter = {
|
||||
sort: 9,
|
||||
path: '/toolbox',
|
||||
component: Layout,
|
||||
redirect: '/toolbox',
|
||||
meta: {
|
||||
icon: 'p-toolbox',
|
||||
title: 'menu.toolbox',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/toolbox',
|
||||
name: 'ToolBox',
|
||||
component: () => import('@/views/toolbox/index.vue'),
|
||||
meta: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default toolBoxRouter;
|
@ -1,153 +0,0 @@
|
||||
<template>
|
||||
<!-- 数据来源 -->
|
||||
<div class="echarts" id="curve"></div>
|
||||
</template>
|
||||
<script setup lang="ts" name="cure">
|
||||
import { ECharts, init } from 'echarts';
|
||||
const initChart = (data: any): ECharts => {
|
||||
const echartsBox = document.getElementById('curve') as HTMLElement;
|
||||
const echarts: ECharts = init(echartsBox);
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'transparent',
|
||||
axisPointer: {
|
||||
type: 'none',
|
||||
},
|
||||
padding: 0,
|
||||
formatter: (p: any) => {
|
||||
let dom = `<div style="width:100%; height: 70px !important; display:flex;flex-direction: column;justify-content: space-between;padding:10px;box-sizing: border-box;
|
||||
color:#fff; background: #6B9DFE;border-radius: 4px;font-size:14px; ">
|
||||
<div style="display: flex; align-items: center;"> <div style="width:5px;height:5px;background:#ffffff;border-radius: 50%;margin-right:5px"></div>平台 : ${p[0].name}</div>
|
||||
<div style="display: flex;align-items: center;"><div style="width:5px;height:5px;background:#ffffff;border-radius: 50%;margin-right:5px"></div>数据量 : ${p[0].value}</div>
|
||||
</div>`;
|
||||
return dom;
|
||||
},
|
||||
},
|
||||
toolbox: {
|
||||
show: true,
|
||||
orient: 'horizontal',
|
||||
},
|
||||
grid: {
|
||||
left: '5%',
|
||||
right: '6%',
|
||||
},
|
||||
dataZoom: [
|
||||
{
|
||||
show: false,
|
||||
height: 10,
|
||||
xAxisIndex: [0],
|
||||
bottom: 0,
|
||||
startValue: 0, //数据窗口范围的起始数值
|
||||
endValue: 9, //数据窗口范围的结束数值
|
||||
handleStyle: {
|
||||
color: '#6b9dfe',
|
||||
},
|
||||
textStyle: {
|
||||
color: 'transparent',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'inside',
|
||||
show: true,
|
||||
height: 0,
|
||||
zoomLock: true, //控制伸缩
|
||||
},
|
||||
],
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category',
|
||||
data: data.map((val: any) => {
|
||||
return {
|
||||
value: val.spotName,
|
||||
};
|
||||
}),
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
axisLabel: {
|
||||
// interval: time > 4 ? 27 : 0,
|
||||
margin: 20,
|
||||
interval: 0,
|
||||
color: '#a1a1a1',
|
||||
fontSize: 14,
|
||||
formatter: function (name: string) {
|
||||
undefined;
|
||||
return name.length > 8 ? name.slice(0, 8) + '...' : name;
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#F6F6F7',
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
min: 0,
|
||||
axisLine: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#edeff5',
|
||||
width: 2,
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: '#a1a1a1',
|
||||
fontSize: 16,
|
||||
fontWeight: 400,
|
||||
formatter: function (value: number) {
|
||||
if (value === 0) {
|
||||
return value;
|
||||
} else if (value >= 10000) {
|
||||
return value / 10000 + 'w';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: 'Direct',
|
||||
type: 'bar',
|
||||
data: data.map((val: any) => {
|
||||
return {
|
||||
value: val.value,
|
||||
};
|
||||
}),
|
||||
barWidth: '45px',
|
||||
itemStyle: {
|
||||
color: '#C5D8FF',
|
||||
borderRadius: [12, 12, 0, 0],
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: '#6B9DFE',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
echarts.setOption(option);
|
||||
return echarts;
|
||||
};
|
||||
defineExpose({
|
||||
initChart,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.echarts {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -1,130 +0,0 @@
|
||||
<template>
|
||||
<!-- Gitee / GitHub 访问量占比 -->
|
||||
<div class="echarts" id="pie"></div>
|
||||
</template>
|
||||
<script setup lang="ts" name="pie">
|
||||
import { ECharts, init } from 'echarts';
|
||||
const initChart = (data: any): ECharts => {
|
||||
const echartsBox = document.getElementById('pie') as HTMLElement;
|
||||
const echarts: ECharts = init(echartsBox);
|
||||
const option = {
|
||||
title: {
|
||||
text: 'Gitee / GitHub',
|
||||
subtext: '访问占比',
|
||||
left: '56%',
|
||||
top: '45%',
|
||||
textAlign: 'center',
|
||||
textStyle: {
|
||||
fontSize: 18,
|
||||
color: '#767676',
|
||||
},
|
||||
subtextStyle: {
|
||||
fontSize: 15,
|
||||
color: '#a1a1a1',
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
top: '4%',
|
||||
left: '2%',
|
||||
orient: 'vertical',
|
||||
icon: 'circle', //图例形状
|
||||
align: 'left',
|
||||
itemGap: 20,
|
||||
textStyle: {
|
||||
fontSize: 13,
|
||||
color: '#a1a1a1',
|
||||
fontWeight: 500,
|
||||
},
|
||||
formatter: function (name: string) {
|
||||
let dataCopy = '';
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].name == name && data[i].value >= 10000) {
|
||||
dataCopy = (data[i].value / 10000).toFixed(2);
|
||||
return name + ' ' + dataCopy + 'w';
|
||||
} else if (data[i].name == name) {
|
||||
dataCopy = data[i].value;
|
||||
return name + ' ' + dataCopy;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['70%', '40%'],
|
||||
center: ['57%', '52%'],
|
||||
silent: true,
|
||||
clockwise: true,
|
||||
startAngle: 150,
|
||||
data: data,
|
||||
labelLine: {
|
||||
length: 80,
|
||||
length2: 30,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
},
|
||||
},
|
||||
label: {
|
||||
position: 'outside',
|
||||
show: true,
|
||||
formatter: '{d}%',
|
||||
fontWeight: 400,
|
||||
fontSize: 19,
|
||||
color: '#a1a1a1',
|
||||
},
|
||||
color: [
|
||||
{
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0.5,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#feb791', // 0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#fe8b4c', // 100% 处的颜色
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 1,
|
||||
y2: 0.5,
|
||||
colorStops: [
|
||||
{
|
||||
offset: 0,
|
||||
color: '#b898fd', // 0% 处的颜色
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: '#8347fd', // 100% 处的颜色
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
echarts.setOption(option);
|
||||
return echarts;
|
||||
};
|
||||
defineExpose({
|
||||
initChart,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.echarts {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 780 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 859 B |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 704 B |
@ -1,181 +0,0 @@
|
||||
.dataVisualize-box {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 650px;
|
||||
padding: 10px 15px;
|
||||
.top-box {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 20px 50px 30px;
|
||||
margin-bottom: 40px;
|
||||
border-radius: 35px;
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
.top-title {
|
||||
margin-bottom: 10px;
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.top-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
.item-left {
|
||||
box-sizing: border-box;
|
||||
width: 22%;
|
||||
padding: 40px 0 120px 30px;
|
||||
overflow: hidden;
|
||||
color: #ffffff;
|
||||
background: url('./images/book-bg.png');
|
||||
background-size: 100% 100%;
|
||||
border-radius: 20px;
|
||||
.left-title {
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.img-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin: 40px 0 20px;
|
||||
background: #ffffff;
|
||||
background-color: #ffffff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 20px rgb(0 0 0 / 14%);
|
||||
img {
|
||||
width: 60px;
|
||||
height: 65px;
|
||||
}
|
||||
}
|
||||
.left-number {
|
||||
overflow: hidden;
|
||||
font-family: DIN;
|
||||
font-size: 62px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.item-center {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
align-content: space-between;
|
||||
justify-content: space-between;
|
||||
min-width: 250px;
|
||||
padding: 0 50px;
|
||||
.traffic-box {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 47%;
|
||||
height: 48%;
|
||||
padding: 25px;
|
||||
border-radius: 30px;
|
||||
.traffic-img {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 19px;
|
||||
}
|
||||
}
|
||||
img {
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
}
|
||||
.item-value {
|
||||
margin-bottom: 4px;
|
||||
font-family: DIN;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #1a1a37;
|
||||
}
|
||||
.traffic-name {
|
||||
overflow: hidden;
|
||||
font-family: DIN;
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
color: #1a1a37;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.gitee-traffic {
|
||||
background: url('./images/1-bg.png');
|
||||
background-color: #e8faea;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.gitHub-traffic {
|
||||
background: url('./images/2-bg.png');
|
||||
background-color: #e7e1fb;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.today-traffic {
|
||||
background: url('./images/3-bg.png');
|
||||
background-color: #fdf3e9;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.yesterday-traffic {
|
||||
background: url('./images/4-bg.png');
|
||||
background-color: #f0f5fb;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
.item-right {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 40%;
|
||||
height: 430px;
|
||||
margin-right: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 25px;
|
||||
.echarts-title {
|
||||
padding: 15px 20px;
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.book-echarts {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bottom-box {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
padding: 20px 0;
|
||||
border-radius: 35px;
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
.bottom-title {
|
||||
position: absolute;
|
||||
top: 16%;
|
||||
left: 3%;
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bottom-tabs {
|
||||
padding: 0 50px;
|
||||
}
|
||||
.curve-echarts {
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|