Selaa lähdekoodia

[frontend-next] Refactored dashboard

0xJacky 2 vuotta sitten
vanhempi
commit
3b49053525

+ 1 - 6
frontend-next/components.d.ts

@@ -26,20 +26,15 @@ declare module '@vue/runtime-core' {
     ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
+    AreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
     ARow: typeof import('ant-design-vue/es')['Row']
     ASelect: typeof import('ant-design-vue/es')['Select']
     ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
     AStatistic: typeof import('ant-design-vue/es')['Statistic']
     ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
     Breadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
-    CPUChart: typeof import('./src/components/Chart/CPUChart.vue')['default']
-    DiskChart: typeof import('./src/components/Chart/DiskChart.vue')['default']
-    DynamicIcon: typeof import('./src/components/DynamicIcon/DynamicIcon.vue')['default']
     FooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
-    HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
-    LineChart: typeof import('./src/components/Chart/LineChart.vue')['default']
     Logo: typeof import('./src/components/Logo/Logo.vue')['default']
-    NetChart: typeof import('./src/components/Chart/NetChart.vue')['default']
     PageHeader: typeof import('./src/components/PageHeader/PageHeader.vue')['default']
     RadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']

+ 2 - 1
frontend-next/package.json

@@ -13,6 +13,7 @@
     "dependencies": {
         "@ant-design/icons-vue": "^6.1.0",
         "ant-design-vue": "^3.2.10",
+        "apexcharts": "^3.35.4",
         "axios": "^0.27.2",
         "lodash": "^4.17.21",
         "moment": "^2.29.4",
@@ -21,9 +22,9 @@
         "pinia-plugin-persistedstate": "^1.6.3",
         "reconnecting-websocket": "^4.4.0",
         "vue": "^3.2.37",
-        "vue-apexcharts": "^1.6.2",
         "vue-chartjs": "^4.1.1",
         "vue-router": "4",
+        "vue3-apexcharts": "^1.4.1",
         "vue3-gettext": "^2.3.0",
         "vuex": "^4.0.2",
         "xterm": "^4.19.0",

+ 8 - 7
frontend-next/src/App.vue

@@ -1,18 +1,19 @@
 <script setup lang="ts">
 // This starter template is using Vue 3 <script setup> SFCs
 // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
-import {toggleTheme} from '@zougt/vite-plugin-theme-preprocessor/dist/browser-utils.js'
+//@ts-ignore
+import {useSettingsStore} from "@/pinia/settings"
+import {dark_mode} from "@/lib/theme"
 
 let media = window.matchMedia('(prefers-color-scheme: dark)')
 const callback = (media: { matches: any; }) => {
+    const settings = useSettingsStore()
     if (media.matches) {
-        toggleTheme({
-            scopeName: 'theme-dark'
-        })
+        dark_mode(true)
+        settings.set_theme('dark')
     } else {
-        toggleTheme({
-            scopeName: 'theme-default'
-        })
+        dark_mode(false)
+        settings.set_theme('default')
     }
 }
 callback(media)

+ 9 - 0
frontend-next/src/api/analytic.ts

@@ -0,0 +1,9 @@
+import http from "@/lib/http"
+
+const analytic = {
+    init() {
+        return http.get('/analytic/init')
+    }
+}
+
+export default analytic

+ 1 - 1
frontend-next/src/api/auth.ts

@@ -10,7 +10,7 @@ const auth = {
             name: name,
             password: password
         }).then(r => {
-            login(r.data.token)
+            login(r.token)
         })
     },
     logout() {

+ 138 - 0
frontend-next/src/components/Chart/AreaChart.vue

@@ -0,0 +1,138 @@
+<script setup lang="ts">
+import VueApexCharts from 'vue3-apexcharts'
+import {ref, watch} from "vue"
+import {useSettingsStore} from "@/pinia/settings"
+import {storeToRefs} from "pinia"
+
+const {series, max, y_formatter} = defineProps(['series', 'max', 'y_formatter'])
+
+const settings = useSettingsStore()
+const {theme} = storeToRefs(settings)
+
+const fontColor = () => {
+    return theme.value === 'dark' ? '#b4b4b4' : undefined
+}
+
+const chart = ref(null)
+
+let chartOptions = {
+    chart: {
+        type: 'area',
+        zoom: {
+            enabled: false
+        },
+        animations: {
+            enabled: false,
+        },
+        toolbar: {
+            show: false
+        },
+    },
+    colors: ['#ff6385', '#36a3eb'],
+    fill: {
+        // type: ['solid', 'gradient'],
+        gradient: {
+            shade: 'light'
+        }
+        //colors:  ['#ff6385', '#36a3eb'],
+    },
+    dataLabels: {
+        enabled: false
+    },
+    stroke: {
+        curve: 'smooth',
+        width: 0,
+    },
+    xaxis: {
+        type: 'datetime',
+        labels: {
+            datetimeUTC: false,
+            style: {
+                colors: fontColor()
+            }
+        }
+    },
+    tooltip: {
+        enabled: false
+    },
+    yaxis: {
+        max: max,
+        tickAmount: 4,
+        min: 0,
+        labels: {
+            style: {
+                colors: fontColor()
+            },
+            formatter: y_formatter
+        }
+    },
+    legend: {
+        labels: {
+            colors: fontColor()
+        },
+        onItemClick: {
+            toggleDataSeries: false
+        },
+        onItemHover: {
+            highlightDataSeries: false
+        },
+    }
+}
+
+let instance: ApexCharts | null = chart.value
+
+const callback = () => {
+    chartOptions = {
+        ...chartOptions,
+        ...{
+            xaxis: {
+                type: 'datetime',
+                labels: {
+                    datetimeUTC: false,
+                    style: {
+                        colors: fontColor()
+                    }
+                }
+            },
+            yaxis: {
+                max: max,
+                tickAmount: 4,
+                min: 0,
+                labels: {
+                    style: {
+                        colors: fontColor()
+                    },
+                    formatter: y_formatter
+                }
+            },
+            legend: {
+                labels: {
+                    colors: fontColor()
+                },
+                onItemClick: {
+                    toggleDataSeries: false
+                },
+                onItemHover: {
+                    highlightDataSeries: false
+                },
+            }
+        }
+    };
+    instance!.updateOptions(chartOptions)
+}
+
+
+watch(theme, callback)
+// watch(series, () => {
+//     instance?.updateSeries(series)
+// })
+</script>
+
+<template>
+    <VueApexCharts type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
+</template>
+
+
+<style scoped>
+
+</style>

+ 0 - 140
frontend-next/src/components/Chart/CPUChart.vue

@@ -1,140 +0,0 @@
-<template>
-    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
-</template>
-
-<script>
-import VueApexCharts from 'vue-apexcharts'
-import Vue from 'vue'
-
-Vue.use(VueApexCharts)
-Vue.component('apexchart', VueApexCharts)
-const fontColor = () => {
-    return window.matchMedia('(prefers-color-scheme: dark)').matches ? '#b4b4b4' : undefined
-}
-export default {
-    name: 'CPUChart',
-    props: {
-        series: Array
-    },
-    watch: {
-        series: {
-            deep: true,
-            handler() {
-                this.$refs.chart.updateSeries(this.series)
-            }
-        }
-    },
-    mounted() {
-        let media = window.matchMedia('(prefers-color-scheme: dark)')
-        let callback = () => {
-            this.chartOptions.xaxis = {
-                type: 'datetime',
-                labels: {
-                    datetimeUTC: false,
-                    style: {
-                        colors: fontColor()
-                    }
-                }
-            }
-            this.chartOptions.yaxis = {
-                max: 100,
-                tickAmount: 4,
-                min: 0,
-                labels: {
-                    style: {
-                        colors: fontColor()
-                    }
-                }
-            }
-            this.chartOptions.legend = {
-                labels: {
-                    colors: fontColor()
-                },
-                onItemClick: {
-                    toggleDataSeries: false
-                },
-                onItemHover: {
-                    highlightDataSeries: false
-                },
-            }
-            this.$refs.chart.updateOptions(this.chartOptions)
-        }
-        if (typeof media.addEventListener === 'function') {
-            media.addEventListener('change', callback)
-        } else if (typeof media.addListener === 'function') {
-            media.addListener(callback)
-        }
-    },
-    data() {
-        return {
-            chartOptions: {
-                series: this.series,
-                chart: {
-                    type: 'area',
-                    zoom: {
-                        enabled: false
-                    },
-                    animations: {
-                        enabled: false,
-                    },
-                    toolbar: {
-                        show: false
-                    },
-                },
-                colors: ['#ff6385', '#36a3eb'],
-                fill: {
-                    // type: ['solid', 'gradient'],
-                    gradient: {
-                        shade: 'light'
-                    }
-                    //colors:  ['#ff6385', '#36a3eb'],
-                },
-                dataLabels: {
-                    enabled: false
-                },
-                stroke: {
-                    curve: 'smooth',
-                    width: 0,
-                },
-                xaxis: {
-                    type: 'datetime',
-                    labels: {
-                        datetimeUTC: false,
-                        style: {
-                            colors: fontColor()
-                        }
-                    }
-                },
-                tooltip: {
-                    enabled: false
-                },
-                yaxis: {
-                    max: 100,
-                    tickAmount: 4,
-                    min: 0,
-                    labels: {
-                        style: {
-                            colors: fontColor()
-                        }
-                    }
-                },
-                legend: {
-                    labels: {
-                        colors: fontColor()
-                    },
-                    onItemClick: {
-                        toggleDataSeries: false
-                    },
-                    onItemHover: {
-                        highlightDataSeries: false
-                    },
-                }
-            },
-        }
-    },
-}
-</script>
-
-<style scoped>
-
-</style>

+ 0 - 139
frontend-next/src/components/Chart/DiskChart.vue

@@ -1,139 +0,0 @@
-<template>
-    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
-</template>
-
-<script>
-import VueApexCharts from 'vue-apexcharts'
-import Vue from 'vue'
-
-Vue.use(VueApexCharts)
-Vue.component('apexchart', VueApexCharts)
-
-const fontColor = () => {
-    return window.matchMedia('(prefers-color-scheme: dark)').matches ? '#b4b4b4' : null
-}
-export default {
-    name: 'DiskChart',
-    props: {
-        series: Array
-    },
-    watch: {
-        series: {
-            deep: true,
-            handler() {
-                this.$refs.chart.updateSeries(this.series)
-            }
-        },
-    },
-    mounted() {
-        let media = window.matchMedia('(prefers-color-scheme: dark)')
-        let callback = () => {
-            this.chartOptions.xaxis = {
-                type: 'datetime',
-                    labels: {
-                    datetimeUTC: false,
-                        style: {
-                        colors: fontColor()
-                    }
-                }
-            }
-            this.chartOptions.yaxis = {
-                tickAmount: 3,
-                    min: 0,
-                    labels: {
-                    style: {
-                        colors: fontColor()
-                    }
-                }
-            }
-            this.chartOptions.legend = {
-                labels: {
-                    colors: fontColor()
-                },
-                onItemClick: {
-                    toggleDataSeries: false
-                },
-                onItemHover: {
-                    highlightDataSeries: false
-                },
-            }
-            this.$refs.chart.updateOptions(this.chartOptions)
-        }
-        if (typeof media.addEventListener === 'function') {
-            media.addEventListener('change', callback)
-        } else if (typeof media.addListener === 'function') {
-            media.addListener(callback)
-        }
-    },
-    data() {
-        return {
-            chartOptions: {
-                series: this.series,
-                chart: {
-                    type: 'area',
-                    zoom: {
-                        enabled: false
-                    },
-                    animations: {
-                        enabled: false,
-                    },
-                    toolbar: {
-                        show: false
-                    },
-                },
-                colors: ['#ff6385', '#36a3eb'],
-                fill: {
-                    // type: ['solid', 'gradient'],
-                    gradient: {
-                        shade: 'light'
-                    }
-                    //colors:  ['#ff6385', '#36a3eb'],
-                },
-                dataLabels: {
-                    enabled: false
-                },
-                stroke: {
-                    curve: 'smooth',
-                    width: 0,
-                },
-                xaxis: {
-                    type: 'datetime',
-                    labels: {
-                        datetimeUTC: false,
-                        style: {
-                            colors: fontColor()
-                        }
-                    }
-                },
-                tooltip: {
-                    enabled: false
-                },
-                yaxis: {
-                    tickAmount: 3,
-                    min: 0,
-                    labels: {
-                        style: {
-                            colors: fontColor()
-                        }
-                    }
-                },
-                legend: {
-                    labels: {
-                        colors: fontColor()
-                    },
-                    onItemClick: {
-                        toggleDataSeries: false
-                    },
-                    onItemHover: {
-                        highlightDataSeries: false
-                    },
-                }
-            },
-        }
-    },
-}
-</script>
-
-<style scoped>
-
-</style>

+ 0 - 37
frontend-next/src/components/Chart/LineChart.vue

@@ -1,37 +0,0 @@
-<script>
-import {Line, mixins} from 'vue-chartjs'
-
-const {reactiveProp} = mixins
-
-export default {
-    name: 'LineChart',
-    extends: Line,
-    mixins: [reactiveProp],
-    props: ['options'],
-    data() {
-        return {
-            updating: false
-        }
-    },
-    mounted() {
-        this.renderChart(this.chartData, this.options)
-    },
-    watch: {
-        chartData: {
-            deep: true,
-            handler() {
-                if (!this.updating && this.$data && this.$data._chart) {
-                    // Update the chart
-                    this.updating = true
-                    this.$data._chart.update()
-                    this.$nextTick(() => this.updating = false)
-                }
-            }
-        }
-    }
-}
-</script>
-
-<style lang="less" scoped>
-
-</style>

+ 0 - 144
frontend-next/src/components/Chart/NetChart.vue

@@ -1,144 +0,0 @@
-<template>
-    <apexchart type="area" height="200" :options="chartOptions" :series="series" ref="chart"/>
-</template>
-
-<script>
-import VueApexCharts from 'vue-apexcharts'
-import Vue from 'vue'
-
-Vue.use(VueApexCharts)
-Vue.component('apexchart', VueApexCharts)
-const fontColor = () => {
-    return window.matchMedia('(prefers-color-scheme: dark)').matches ? '#b4b4b4' : undefined
-}
-export default {
-    name: 'NetChart',
-    props: {
-        series: Array
-    },
-    watch: {
-        series: {
-            deep: true,
-            handler() {
-                this.$refs.chart.updateSeries(this.series)
-            }
-        }
-    },
-    mounted() {
-        let media = window.matchMedia('(prefers-color-scheme: dark)')
-        let callback = () => {
-            this.chartOptions.xaxis = {
-                type: 'datetime',
-                labels: {
-                    datetimeUTC: false,
-                    style: {
-                        colors: fontColor()
-                    }
-                }
-            }
-            this.chartOptions.yaxis = {
-                tickAmount: 3,
-                min: 0,
-                labels: {
-                    style: {
-                        colors: fontColor()
-                    },
-                    formatter: (bytes) => {
-                        return this.bytesToSize(bytes) + '/s'
-                    }
-                }
-            }
-            this.chartOptions.legend = {
-                labels: {
-                    colors: fontColor()
-                },
-                onItemClick: {
-                    toggleDataSeries: false
-                },
-                onItemHover: {
-                    highlightDataSeries: false
-                },
-            }
-            this.$refs.chart.updateOptions(this.chartOptions)
-        }
-        if (typeof media.addEventListener === 'function') {
-            media.addEventListener('change', callback)
-        } else if (typeof media.addListener === 'function') {
-            media.addListener(callback)
-        }
-    },
-    data() {
-        return {
-            chartOptions: {
-                series: this.series,
-                chart: {
-                    type: 'area',
-                    zoom: {
-                        enabled: false
-                    },
-                    animations: {
-                        enabled: false,
-                    },
-                    toolbar: {
-                        show: false
-                    },
-                },
-                colors: ['#ff6385', '#36a3eb'],
-                fill: {
-                    // type: ['solid', 'gradient'],
-                    gradient: {
-                        shade: 'light'
-                    }
-                    //colors:  ['#ff6385', '#36a3eb'],
-                },
-                dataLabels: {
-                    enabled: false
-                },
-                stroke: {
-                    curve: 'smooth',
-                    width: 0,
-                },
-                xaxis: {
-                    type: 'datetime',
-                    labels: {
-                        datetimeUTC: false,
-                        style: {
-                            colors: fontColor()
-                        }
-                    }
-                },
-                tooltip: {
-                    enabled: false
-                },
-                yaxis: {
-                    tickAmount: 3,
-                    min: 0,
-                    labels: {
-                        style: {
-                            colors: fontColor()
-                        },
-                        formatter: (bytes) => {
-                            return this.bytesToSize(bytes) + '/s'
-                        }
-                    }
-                },
-                legend: {
-                    labels: {
-                        colors: fontColor()
-                    },
-                    onItemClick: {
-                        toggleDataSeries: false
-                    },
-                    onItemHover: {
-                        highlightDataSeries: false
-                    },
-                }
-            },
-        }
-    },
-}
-</script>
-
-<style scoped>
-
-</style>

+ 60 - 72
frontend-next/src/components/Chart/RadialBarChart.vue

@@ -1,102 +1,90 @@
-<template>
-    <div class="container">
-        <p class="text">{{ centerText }}</p>
-        <p class="bottom_text">{{ bottomText }}</p>
-        <apexchart class="radialBar" type="radialBar" height="205" :options="chartOptions" :series="series" ref="chart"/>
-    </div>
-</template>
+<script setup lang="ts">
+import VueApexCharts from 'vue3-apexcharts'
+import app from '@/main'
+import {reactive} from "vue";
 
-<script>
-import VueApexCharts from 'vue-apexcharts'
-import Vue from 'vue'
+const {series, centerText, colors, name, bottomText}
+    = defineProps(['series', 'centerText', 'colors', 'name', 'bottomText'])
 
-Vue.use(VueApexCharts)
-Vue.component('apexchart', VueApexCharts)
-export default {
-    name: 'RadialBarChart',
-    props: {
-        series: Array,
-        centerText: String,
-        colors: String,
-        name: String,
-        bottomText: String,
-    },
-    watch: {
-        series: {
-            deep: true,
-            handler() {
-                this.$refs.chart.updateSeries(this.series)
-            }
-        }
+const chartOptions = reactive({
+    series: series,
+    chart: {
+        type: 'radialBar',
+        offsetY: 0
     },
-    data() {
-        return {
-            chartOptions: {
-                series: this.series,
-                chart: {
-                    type: 'radialBar',
-                    offsetY: 0
-                },
-                plotOptions: {
-                    radialBar: {
-                        startAngle: -135,
-                        endAngle: 135,
-                        dataLabels: {
-                            name: {
-                                fontSize: '14px',
-                                color: this.colors,
-                                offsetY: 36
-                            },
-                            value: {
-                                offsetY: 50,
-                                fontSize: '14px',
-                                color: undefined,
-                                formatter: () => {return ''}
-                            }
-                        }
-                    }
-                },
-                fill: {
-                    colors: this.colors
+    plotOptions: {
+        radialBar: {
+            startAngle: -135,
+            endAngle: 135,
+            dataLabels: {
+                name: {
+                    fontSize: '14px',
+                    color: colors,
+                    offsetY: 36
                 },
-                labels: [this.name],
-                states: {
-                    hover: {
-                        filter: {
-                            type: 'none'
-                        }
-                    },
-                    active: {
-                        filter: {
-                            type: 'none'
-                        }
+                value: {
+                    offsetY: 50,
+                    fontSize: '14px',
+                    color: undefined,
+                    formatter: () => {
+                        return ''
                     }
                 }
             }
         }
+    },
+    fill: {
+        colors: colors
+    },
+    labels: [name],
+    states: {
+        hover: {
+            filter: {
+                type: 'none'
+            }
+        },
+        active: {
+            filter: {
+                type: 'none'
+            }
+        }
     }
-}
+})
 </script>
 
+<template>
+    <div class="radial-bar-container">
+        <p class="text">{{ centerText }}</p>
+        <p class="bottom_text">{{ bottomText }}</p>
+        <VueApexCharts v-if="centerText" class="radialBar" type="radialBar" height="205" :options="chartOptions"
+                       :series="series"
+                       ref="chart"/>
+    </div>
+</template>
+
+
 <style lang="less" scoped>
-.container {
+.radial-bar-container {
     position: relative;
     margin: 0 auto;
-    height: 112px!important;
+    height: 112px !important;
+
     .radialBar {
         position: absolute;
         top: -30px;
-        @media(max-width: 768px) and (min-width: 290px) {
+        @media (max-width: 768px) and (min-width: 290px) {
             left: 50%;
             transform: translateX(-50%);
         }
     }
+
     .text {
         position: absolute;
         top: calc(50% - 5px);
         width: 100%;
         text-align: center;
     }
+
     .bottom_text {
         position: absolute;
         top: calc(106px);

+ 1 - 0
frontend-next/src/gettext.ts

@@ -9,4 +9,5 @@ export default createGettext({
     },
     defaultLanguage: "en",
     translations: translations,
+    silent: true
 })

+ 3 - 6
frontend-next/src/layouts/HeaderLayout.vue

@@ -3,7 +3,7 @@ import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import gettext from '@/gettext'
 import {message} from "ant-design-vue"
 import auth from '@/api/auth'
-import {HomeOutlined, LogoutOutlined} from "@ant-design/icons-vue"
+// import {HomeOutlined, LogoutOutlined} from "@ant-design/icons-vue"
 
 const {$gettext} = gettext
 import {useRouter} from "vue-router"
@@ -21,18 +21,15 @@ function logout() {
 
 <template>
     <div class="header">
-        <div class="tool">
-            <a-icon type="menu-unfold" @click="$emit('clickUnFold')"/>
-        </div>
         <div class="user-wrapper">
             <set-language class="set_lang"/>
 
             <a href="/">
-                <HomeOutlined/>
+                <!--                <HomeOutlined/>-->
             </a>
 
             <a @click="logout" style="margin-left: 20px">
-                <LogoutOutlined/>
+                <!--                <LogoutOutlined/>-->
             </a>
         </div>
     </div>

+ 15 - 24
frontend-next/src/layouts/SideBar.vue

@@ -4,14 +4,14 @@ import {routes} from '@/routes'
 import {useRoute} from "vue-router"
 import {computed, ref, watch} from "vue"
 import {useGettext} from "vue3-gettext"
+
 const {$gettext} = useGettext()
 
 const route = useRoute()
 
 let openKeys = [openSub()]
 
-const selectedKey = ref()
-
+const selectedKey = ref([route.name])
 
 function openSub() {
     let path = route.path
@@ -19,23 +19,14 @@ function openSub() {
     return path.substring(1, lastSepIndex)
 }
 
-function onOpenChange(_openKeys: Array<any>) {
-    const latestOpenKey = openKeys.find(key => openKeys.indexOf(key) === -1) || ''
-    if ((sidebars.value||[]).indexOf(latestOpenKey) === -1) {
-        openKeys = _openKeys
-    } else {
-        openKeys = latestOpenKey ? [latestOpenKey] : []
-    }
-}
-
-watch(route, ()=>{
-    const selectedKey = [route.name]
+watch(route, () => {
+    selectedKey.value = [route.name]
     const sub = openSub()
     const p = openKeys.indexOf(sub)
     if (p === -1) openKeys.push(sub)
 })
 
-const sidebars = computed(()=>{
+const sidebars = computed(() => {
     return routes[0]['children']
 })
 
@@ -51,11 +42,11 @@ interface sidebar {
     children: sidebar[]
 }
 
-const visible = computed(()=>{
+const visible = computed(() => {
 
     const res: sidebar[] = [];
 
-    (sidebars.value||[]).forEach((s)=> {
+    (sidebars.value || []).forEach((s) => {
         if (s.meta && s.meta.hiddenInSidebar) {
             return
         }
@@ -66,7 +57,7 @@ const visible = computed(()=>{
             children: []
         };
 
-        (s.children||[]).forEach(c => {
+        (s.children || []).forEach(c => {
             if (c.meta && c.meta.hiddenInSidebar) {
                 return
             }
@@ -76,9 +67,8 @@ const visible = computed(()=>{
     })
 
 
-   return res
+    return res
 })
-
 </script>
 
 <template>
@@ -87,19 +77,20 @@ const visible = computed(()=>{
         <a-menu
             :openKeys="openKeys"
             mode="inline"
-            @openChange="onOpenChange"
-            v-model="selectedKey"
+            v-model:openKeys="openKeys"
+            v-model:selectedKeys="selectedKey"
         >
             <template v-for="sidebar in visible">
-                <a-menu-item v-if="sidebar.children.length===0 || sidebar.meta.hideChildren === true" :key="sidebar.name"
+                <a-menu-item v-if="sidebar.children.length===0 || sidebar.meta.hideChildren === true"
+                             :key="sidebar.name"
                              @click="$router.push('/'+sidebar.path).catch(() => {})">
-                    <component :is="sidebar.meta.icon" />
+                    <component :is="sidebar.meta.icon"/>
                     <span>{{ $gettext(sidebar.name) }}</span>
                 </a-menu-item>
 
                 <a-sub-menu v-else :key="sidebar.path">
                     <template #title>
-                        <component :is="sidebar.meta.icon" />
+                        <component :is="sidebar.meta.icon"/>
                         <span>{{ $gettext(sidebar.name) }}</span>
                     </template>
                     <a-menu-item v-for="child in sidebar.children" :key="child.name">

+ 14 - 0
frontend-next/src/lib/helper/index.ts

@@ -0,0 +1,14 @@
+function bytesToSize(bytes: number) {
+    if (bytes === 0) return '0 B'
+
+    const k = 1024
+
+    const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
+
+    const i = Math.floor(Math.log(bytes) / Math.log(k))
+    return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]
+}
+
+export {
+    bytesToSize
+}

+ 29 - 9
frontend-next/src/lib/http/index.ts

@@ -1,4 +1,4 @@
-import axios from 'axios'
+import axios, {AxiosRequestConfig} from 'axios'
 import {useUserStore} from "@/pinia/user"
 import {storeToRefs} from "pinia";
 
@@ -6,8 +6,12 @@ const user = useUserStore()
 
 const {token} = storeToRefs(user)
 
-/* 创建 axios 实例 */
-let http = axios.create({
+declare module 'axios' {
+    export interface AxiosResponse<T = any> extends Promise<T> {
+    }
+}
+
+let instance = axios.create({
     baseURL: import.meta.env.VITE_API_ROOT,
     timeout: 50000,
     headers: {'Content-Type': 'application/json'},
@@ -21,8 +25,8 @@ let http = axios.create({
     }],
 })
 
-/* http request 拦截器 */
-http.interceptors.request.use(
+
+instance.interceptors.request.use(
     config => {
         if (token) {
             (config.headers || {}).Authorization = token.value
@@ -34,10 +38,10 @@ http.interceptors.request.use(
     }
 )
 
-/* response 拦截器 */
-http.interceptors.response.use(
-    response =>{
-        return Promise.resolve(response)
+
+instance.interceptors.response.use(
+    response => {
+        return Promise.resolve(response.data)
     },
     async error => {
         switch (error.response.status) {
@@ -49,4 +53,20 @@ http.interceptors.response.use(
     }
 )
 
+const http = {
+    get(url: string, config: AxiosRequestConfig = {}) {
+        return instance.get<any, any>(url, config)
+    },
+    post(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
+        return instance.post<any, any>(url, data, config)
+    },
+    put(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
+        return instance.put<any, any>(url, data, config)
+    },
+    delete(url: string, config: AxiosRequestConfig = {}) {
+        return instance.delete<any, any>(url, config)
+    }
+}
+
+
 export default http

+ 31 - 0
frontend-next/src/lib/theme/index.ts

@@ -0,0 +1,31 @@
+function changeCss(css: string, value: string) {
+    const body = document.body.style
+    body.setProperty(css, value)
+}
+
+function changeTheme(theme: string) {
+    const head = document.head
+    document.getElementById("theme")?.remove()
+    const styleDom = document.createElement("style")
+    styleDom.id = "theme"
+    styleDom.innerHTML = theme
+    head.appendChild(styleDom)
+}
+
+export const dark_mode = async (enabled: Boolean) => {
+    if (enabled) {
+        changeTheme((await import("@/dark.less?inline")).default)
+        changeCss("--page-bg-color", "#141414");
+        changeCss("--head-bg-color", "rgba(0, 0, 0, 0.5)")
+        changeCss("--line-color", "#2e2e2e")
+        changeCss("--content-bg-color", "rgb(255 255 255 / 4%)")
+        changeCss("--text-color", "rgba(255, 255, 255, 0.85)")
+    } else {
+        changeTheme((await import("@/style.less?inline")).default)
+        changeCss("--page-bg-color", "white")
+        changeCss("--head-bg-color", "rgba(255, 255, 255, 0.7)")
+        changeCss("--line-color", "#e8e8e8")
+        changeCss("--content-bg-color", "#f0f2f5")
+        changeCss("--text-color", "rgba(0, 0, 0, 0.85)")
+    }
+}

+ 16 - 0
frontend-next/src/lib/websocket/index.ts

@@ -0,0 +1,16 @@
+import ReconnectingWebSocket from "reconnecting-websocket"
+import {useUserStore} from "@/pinia/user"
+import {storeToRefs} from "pinia"
+
+
+function ws(url: string): ReconnectingWebSocket {
+    const user = useUserStore()
+    const {token} = storeToRefs(user)
+
+    const protocol = location.protocol === 'https:' ? 'wss://' : 'ws://'
+
+    return new ReconnectingWebSocket(
+        protocol + window.location.host + url + '?token=' + btoa(token.value))
+}
+
+export default ws

+ 7 - 5
frontend-next/src/pinia/settings.ts

@@ -1,16 +1,18 @@
-import { defineStore } from "pinia"
+import {defineStore} from "pinia"
 
 export const useSettingsStore = defineStore('settings', {
     state: () => ({
         language: '',
+        theme: 'light',
     }),
-    getters: {
-
-    },
+    getters: {},
     actions: {
-        set_language(lang:string) {
+        set_language(lang: string) {
             this.language = lang
         },
+        set_theme(t: string) {
+            this.theme = t
+        }
     },
     persist: true
 })

+ 6 - 5
frontend-next/src/pinia/user.ts

@@ -1,15 +1,16 @@
-import { defineStore } from "pinia"
+import {defineStore} from "pinia"
 
 export const useUserStore = defineStore('user', {
     state: () => ({
-        token: '',
+        token: ''
     }),
     getters: {
-        is_login(state): boolean
-        {return !!state.token}
+        is_login(state): boolean {
+            return !!state.token
+        }
     },
     actions: {
-        login(token:string) {
+        login(token: string) {
             this.token = token
         },
         logout() {

+ 1 - 0
frontend-next/src/routes/index.ts

@@ -4,6 +4,7 @@ import gettext from "../gettext"
 const {$gettext} = gettext
 
 import {useUserStore} from "@/pinia/user"
+
 import {
     HomeOutlined,
     UserOutlined,

+ 152 - 188
frontend-next/src/views/dashboard/DashBoard.vue

@@ -1,8 +1,138 @@
+<script setup lang="ts">
+import AreaChart from '@/components/Chart/AreaChart.vue'
+
+import RadialBarChart from '@/components/Chart/RadialBarChart.vue'
+import {useGettext} from 'vue3-gettext'
+import {onMounted, onUnmounted, reactive, ref} from "vue"
+import analytic from "@/api/analytic"
+import ws from "@/lib/websocket"
+import {bytesToSize} from "@/lib/helper"
+
+const {$gettext} = useGettext()
+
+const websocket = ws('/api/analytic')
+websocket.onmessage = wsOnMessage
+
+const host = reactive({})
+const cpu = ref('0.0')
+const cpu_info = reactive([])
+const cpu_analytic_series = reactive([{name: 'User', data: <any>[]}, {name: 'Total', data: <any>[]}])
+const net_analytic = reactive([{name: $gettext('Receive'), data: <any>[]},
+    {name: $gettext('Send'), data: <any>[]}])
+const disk_io_analytic = reactive([{name: $gettext('Writes'), data: <any>[]},
+    {name: $gettext('Writes'), data: <any>[]}])
+const memory = reactive({})
+const disk = reactive({})
+const disk_io = reactive({writes: 0, reads: 0})
+const uptime = ref('')
+const loadavg = reactive({})
+const net = reactive({recv: 0, sent: 0, last_recv: 0, last_sent: 0})
+
+const net_formatter = (bytes: number) => {
+    return bytesToSize(bytes) + '/s'
+}
+
+interface Usage {
+    x: number
+    y: number
+}
+
+onMounted(() => {
+    analytic.init().then(r => {
+        Object.assign(host, r.host)
+        Object.assign(cpu_info, r.cpu.info)
+        Object.assign(memory, r.memory)
+        Object.assign(disk, r.disk)
+
+        net.last_recv = r.network.init.bytesRecv
+        net.last_sent = r.network.init.bytesSent
+        r.cpu.user.forEach((u: Usage) => {
+            cpu_analytic_series[0].data.push([u.x, u.y.toFixed(2)])
+        })
+        r.cpu.total.forEach((u: Usage) => {
+            cpu_analytic_series[1].data.push([u.x, u.y.toFixed(2)])
+        })
+        r.network.bytesRecv.forEach((u: Usage) => {
+            net_analytic[0].data.push([u.x, u.y.toFixed(2)])
+        })
+        r.network.bytesSent.forEach((u: Usage) => {
+            net_analytic[1].data.push([u.x, u.y.toFixed(2)])
+        })
+        disk_io_analytic[0].data = disk_io_analytic[0].data.concat(r.disk_io.writes)
+        disk_io_analytic[1].data = disk_io_analytic[1].data.concat(r.disk_io.reads)
+
+    })
+})
+
+onUnmounted(() => {
+    websocket.close()
+})
+
+function wsOnMessage(m: { data: any }) {
+    const r = JSON.parse(m.data)
+
+    const cpu_usage = r.cpu.system + r.cpu.user
+    cpu.value = cpu_usage.toFixed(2)
+
+    const time = new Date().getTime()
+
+    cpu_analytic_series[0].data.push([time, r.cpu.user.toFixed(2)])
+    cpu_analytic_series[1].data.push([time, cpu.value])
+
+    if (cpu_analytic_series[0].data.length > 100) {
+        cpu_analytic_series[0].data.shift()
+        cpu_analytic_series[1].data.shift()
+    }
+
+    // mem
+    Object.assign(memory, r.memory)
+
+    // disk
+    Object.assign(disk, r.disk)
+    disk_io.writes = r.disk.writes.y
+    disk_io.reads = r.disk.reads.y
+
+    // uptime
+    let _uptime = Math.floor(r.uptime)
+    let uptime_days = Math.floor(_uptime / 86400)
+    _uptime -= uptime_days * 86400
+    let uptime_hours = Math.floor(_uptime / 3600)
+    _uptime -= uptime_hours * 3600
+    uptime.value = uptime_days + 'd ' + uptime_hours + 'h ' + Math.floor(_uptime / 60) + 'm'
+
+    // loadavg
+    Object.assign(loadavg, r.loadavg)
+
+    // network
+    Object.assign(net, r.network)
+    net.recv = r.network.bytesRecv - net.last_recv
+    net.sent = r.network.bytesSent - net.last_sent
+    net.last_recv = r.network.bytesRecv
+    net.last_sent = r.network.bytesSent
+
+    net_analytic[0].data.push([time, net.recv])
+    net_analytic[1].data.push([time, net.sent])
+
+    if (net_analytic[0].data.length > 100) {
+        net_analytic[0].data.shift()
+        net_analytic[1].data.shift()
+    }
+
+    disk_io_analytic[0].data.push(r.disk.writes)
+    disk_io_analytic[1].data.push(r.disk.reads)
+
+    if (disk_io_analytic[0].data.length > 100) {
+        disk_io_analytic[0].data.shift()
+        disk_io_analytic[1].data.shift()
+    }
+}
+</script>
+
 <template>
     <div>
         <a-row :gutter="[16,16]" class="first-row">
             <a-col :xl="7" :lg="24" :md="24">
-                <a-card :title="$gettext('Server Info')">
+                <a-card :title="$gettext('Server Info')" :bordered="false">
                     <p>
                         <translate>Uptime:</translate>
                         {{ uptime }}
@@ -26,26 +156,26 @@
                 </a-card>
             </a-col>
             <a-col :xl="10" :lg="16" :md="24" class="chart_dashboard">
-                <a-card :title="$gettext('Memory and Storage')">
+                <a-card :title="$gettext('Memory and Storage')" :bordered="false">
                     <a-row :gutter="[0,16]">
                         <a-col :xs="24" :sm="24" :md="8">
-                            <radial-bar-chart :name="$gettext('Memory')" :series="[memory_pressure]"
-                                              :centerText="memory_used" :bottom-text="memory_total" colors="#36a3eb"/>
+                            <radial-bar-chart :name="$gettext('Memory')" :series="[memory.pressure]"
+                                              :centerText="memory.used" :bottom-text="memory.total" colors="#36a3eb"/>
                         </a-col>
                         <a-col :xs="24" :sm="12" :md="8">
-                            <radial-bar-chart :name="$gettext('Swap')" :series="[memory_swap_percent]"
-                                              :centerText="memory_swap_used"
-                                              :bottom-text="memory_swap_total" colors="#ff6385"/>
+                            <radial-bar-chart :name="$gettext('Swap')" :series="[memory.swap_percent]"
+                                              :centerText="memory.swap_used"
+                                              :bottom-text="memory.swap_total" colors="#ff6385"/>
                         </a-col>
                         <a-col :xs="24" :sm="12" :md="8">
-                            <radial-bar-chart :name="$gettext('Storage')" :series="[disk_percentage]"
-                                              :centerText="disk_used" :bottom-text="disk_total" colors="#87d068"/>
+                            <radial-bar-chart :name="$gettext('Storage')" :series="[disk.percentage]"
+                                              :centerText="disk.used" :bottom-text="disk.total" colors="#87d068"/>
                         </a-col>
                     </a-row>
                 </a-card>
             </a-col>
             <a-col :xl="7" :lg="8" :sm="24" class="chart_dashboard">
-                <a-card :title="$gettext('Network Statistics')">
+                <a-card :title="$gettext('Network Statistics')" :bordered="false">
                     <a-row :gutter="16">
                         <a-col :span="12">
                             <a-statistic :value="bytesToSize(net.last_recv)"
@@ -61,13 +191,13 @@
         </a-row>
         <a-row class="row-two" :gutter="[16,32]">
             <a-col :xl="8" :lg="24" :md="24" :sm="24">
-                <a-card :title="$gettext('CPU Status')">
+                <a-card :title="$gettext('CPU Status')" :bordered="false">
                     <a-statistic :value="cpu" title="CPU">
                         <template v-slot:suffix>
                             <span>%</span>
                         </template>
                     </a-statistic>
-                    <c-p-u-chart :series="cpu_analytic_series"/>
+                    <area-chart :series="cpu_analytic_series" :max="100"/>
                 </a-card>
             </a-col>
             <a-col :xl="8" :lg="12" :md="24" :sm="24">
@@ -89,14 +219,14 @@
                             </a-statistic>
                         </a-col>
                     </a-row>
-                    <net-chart :series="net_analytic"/>
+                    <area-chart :series="net_analytic" :y_formatter="net_formatter"/>
                 </a-card>
             </a-col>
             <a-col :xl="8" :lg="12" :md="24" :sm="24">
                 <a-card :title="$gettext('Disk IO')">
                     <a-row :gutter="16">
                         <a-col :span="12">
-                            <a-statistic :value="diskIO.writes"
+                            <a-statistic :value="disk_io.writes"
                                          :title="$gettext('Writes')">
                                 <template v-slot:suffix>
                                     <span>/s</span>
@@ -104,197 +234,31 @@
                             </a-statistic>
                         </a-col>
                         <a-col :span="12">
-                            <a-statistic :value="diskIO.reads" :title="$gettext('Reads')">
+                            <a-statistic :value="disk_io.reads" :title="$gettext('Reads')">
                                 <template v-slot:suffix>
                                     <span>/s</span>
                                 </template>
                             </a-statistic>
                         </a-col>
                     </a-row>
-                    <disk-chart :series="diskIO_analytic"/>
+                    <area-chart :series="disk_io_analytic"/>
                 </a-card>
             </a-col>
         </a-row>
-
     </div>
 </template>
 
-<script>
-import ReconnectingWebSocket from 'reconnecting-websocket'
-import CPUChart from '@/components/Chart/CPUChart.vue'
-import NetChart from '@/components/Chart/NetChart.vue'
-import $gettext from '@/lib/translate/gettext.vue'
-import RadialBarChart from '@/components/Chart/RadialBarChart.vue'
-import DiskChart from '@/components/Chart/DiskChart.vue'
-
-export default {
-    name: 'DashBoard',
-    components: {
-        DiskChart,
-        RadialBarChart,
-        NetChart,
-        CPUChart,
-    },
-    data() {
-        return {
-            websocket: null,
-            loading: true,
-            stat: {},
-            memory_pressure: 0,
-            memory_used: '',
-            memory_cached: '',
-            memory_free: '',
-            memory_total: '',
-            cpu_analytic_series: [{
-                name: 'CPU User',
-                data: []
-            }, {
-                name: 'CPU Total',
-                data: []
-            }],
-            cpu: 0,
-            memory_swap_used: '',
-            memory_swap_total: '',
-            memory_swap_percent: 0,
-            disk_percentage: 0,
-            disk_total: '',
-            disk_used: '',
-            net: {
-                recv: 0,
-                sent: 0,
-                last_recv: 0,
-                last_sent: 0,
-            },
-            diskIO: {
-                writes: 0,
-                reads: 0,
-            },
-            net_analytic: [{
-                name: $gettext('Receive'),
-                data: []
-            }, {
-                name: $gettext('Send'),
-                data: []
-            }],
-            diskIO_analytic: [{
-                name: $gettext('Writes'),
-                data: []
-            }, {
-                name: $gettext('Reads'),
-                data: []
-            }],
-            uptime: '',
-            loadavg: {},
-            cpu_info: [],
-            host: {}
-        }
-    },
-    created() {
-        this.websocket = new ReconnectingWebSocket(this.getWebSocketRoot() + '/analytic?token='
-            + btoa(this.$store.state.user.token))
-        this.websocket.onmessage = this.wsOnMessage
-        this.websocket.onopen = this.wsOpen
-        this.$api.analytic.init().then(r => {
-            this.cpu_info = r.cpu.info
-            this.net.last_recv = r.network.init.bytesRecv
-            this.net.last_sent = r.network.init.bytesSent
-            this.host = r.host
-            r.cpu.user.forEach(u => {
-                this.cpu_analytic_series[0].data.push([u.x, u.y.toFixed(2)])
-            })
-            r.cpu.total.forEach(u => {
-                this.cpu_analytic_series[1].data.push([u.x, u.y.toFixed(2)])
-            })
-            r.network.bytesRecv.forEach(u => {
-                this.net_analytic[0].data.push([u.x, u.y.toFixed(2)])
-            })
-            r.network.bytesSent.forEach(u => {
-                this.net_analytic[1].data.push([u.x, u.y.toFixed(2)])
-            })
-            this.diskIO_analytic[0].data = this.diskIO_analytic[0].data.concat(r.diskIO.writes)
-            this.diskIO_analytic[1].data = this.diskIO_analytic[1].data.concat(r.diskIO.reads)
-        })
-    },
-    destroyed() {
-        this.websocket.close()
-    },
-    methods: {
-        wsOpen() {
-            this.websocket.send('ping')
-        },
-        wsOnMessage(m) {
-            const r = JSON.parse(m.data)
-            // console.log(r)
-            this.cpu = r.cpu_system + r.cpu_user
-            this.cpu = this.cpu.toFixed(2)
-            const time = new Date().getTime()
-
-            this.cpu_analytic_series[0].data.push([time, r.cpu_user.toFixed(2)])
-            this.cpu_analytic_series[1].data.push([time, this.cpu])
-
-            if (this.cpu_analytic_series[0].data.length > 100) {
-                this.cpu_analytic_series[0].data.shift()
-                this.cpu_analytic_series[1].data.shift()
-            }
-
-            // mem
-            this.memory_pressure = r.memory_pressure
-            this.memory_used = r.memory_used
-            this.memory_cached = r.memory_cached
-            this.memory_free = r.memory_free
-            this.memory_total = r.memory_total
-            this.memory_swap_percent = r.memory_swap_percent
-            this.memory_swap_used = r.memory_swap_used
-            this.memory_swap_total = r.memory_swap_total
-
-            // disk
-            this.disk_percentage = r.disk_percentage
-            this.disk_used = r.disk_used
-            this.disk_total = r.disk_total
-
-            let uptime = Math.floor(r.uptime)
-            let uptime_days = Math.floor(uptime / 86400)
-            uptime -= uptime_days * 86400
-            let uptime_hours = Math.floor(uptime / 3600)
-            uptime -= uptime_hours * 3600
-            this.uptime = uptime_days + 'd ' + uptime_hours + 'h ' + Math.floor(uptime / 60) + 'm'
-            this.loadavg = r.loadavg
-
-            // net
-            this.net.recv = r.network.bytesRecv - this.net.last_recv
-            this.net.sent = r.network.bytesSent - this.net.last_sent
-            this.net.last_recv = r.network.bytesRecv
-            this.net.last_sent = r.network.bytesSent
-
-            this.net_analytic[0].data.push([time, this.net.recv])
-            this.net_analytic[1].data.push([time, this.net.sent])
-
-            if (this.net_analytic[0].data.length > 100) {
-                this.net_analytic[0].data.shift()
-                this.net_analytic[1].data.shift()
-            }
-
-            // diskIO
-            this.diskIO.writes = r.diskIO.writes.y
-            this.diskIO.reads = r.diskIO.reads.y
-
-            this.diskIO_analytic[0].data.push(r.diskIO.writes)
-            this.diskIO_analytic[1].data.push(r.diskIO.reads)
-
-            if (this.diskIO_analytic[0].data.length > 100) {
-                this.diskIO_analytic[0].data.shift()
-                this.diskIO_analytic[1].data.shift()
-            }
-        }
-    }
-}
-</script>
-
 <style lang="less" scoped>
 .first-row {
     .ant-card {
         min-height: 227px;
+
+        p {
+            margin-bottom: 8px;
+        }
     }
+
+    margin-bottom: 20px;
 }
 
 .ant-card {

+ 2 - 10
frontend-next/src/views/pty/Terminal.vue

@@ -1,24 +1,16 @@
 <script setup lang="ts">
-import ReconnectingWebSocket from 'reconnecting-websocket'
 import 'xterm/css/xterm.css'
 import {Terminal} from 'xterm'
 import {FitAddon} from 'xterm-addon-fit'
 import {onMounted, onUnmounted} from "vue"
-import {useUserStore} from "@/pinia/user"
-import {storeToRefs} from "pinia"
 import _ from 'lodash'
-
-const user = useUserStore()
-const {token} = storeToRefs(user)
+import ws from "@/lib/websocket"
 
 let term: Terminal | null
 let ping: null | NodeJS.Timer
 
-const protocol = location.protocol === 'https:' ? 'wss://' : 'ws://'
 
-const websocket = new ReconnectingWebSocket(
-    protocol + window.location.host + '/api/pty?token='
-    + btoa(token.value))
+const websocket = ws('/api/pty')
 
 onMounted(() => {
     initTerm()

+ 2 - 34
frontend-next/vite.config.ts

@@ -3,9 +3,7 @@ import vue from '@vitejs/plugin-vue'
 import {createHtmlPlugin} from 'vite-plugin-html'
 import Components from 'unplugin-vue-components/vite'
 import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers'
-import {themePreprocessorPlugin, themePreprocessorHmrPlugin} from "@zougt/vite-plugin-theme-preprocessor";
-import { fileURLToPath, URL } from "url"
-import path from 'path'
+import {fileURLToPath, URL} from "url"
 
 // https://vitejs.dev/config/
 export default defineConfig({
@@ -28,43 +26,13 @@ export default defineConfig({
         Components({
             resolvers: [AntDesignVueResolver({importStyle: false})]
         }),
-        themePreprocessorPlugin({
-            less: {
-                multipleScopeVars: [
-                    {
-                        scopeName: "theme-default",
-                        path: path.resolve("./src/style.less"),
-                    },
-                    {
-                        scopeName: "theme-dark",
-                        path: path.resolve("./src/dark.less"),
-                    },
-                ],
-                // css中不是由主题色变量生成的颜色,也让它抽取到主题css内,可以提高权重
-                includeStyleWithColors: [
-                    {
-                        color: "#ffffff",
-                        // 排除属性
-                        // excludeCssProps:["background","background-color"]
-                        // 排除选择器
-                        // excludeSelectors: [
-                        //   ".ant-btn-link:hover, .ant-btn-link:focus, .ant-btn-link:active",
-                        // ],
-                    },
-                    {
-                        color: ["transparent","none"],
-                    },
-                ],
-            },
-        }),
-        themePreprocessorHmrPlugin(),
         createHtmlPlugin({
             minify: true,
             /**
              * After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
              * @default src/main.ts
              */
-            entry: 'src/main.ts',
+            entry: '/src/main.ts',
             /**
              * If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
              * @default index.html

+ 72 - 5
frontend-next/yarn.lock

@@ -389,6 +389,18 @@ anymatch@~3.1.2:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
+apexcharts@^3.35.4:
+  version "3.35.4"
+  resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-3.35.4.tgz#347140240fad6e646a76c7bc8e7e75294c2206b2"
+  integrity sha512-dsXjETHF2OmKtxNv66wBeFGU2qtZQnr6kp/vcNY05GWs4vcBepg54qNgOJ2Gp/gXskiGw/frrmIKGi8lJ/UDnQ==
+  dependencies:
+    svg.draggable.js "^2.2.2"
+    svg.easing.js "^2.0.0"
+    svg.filter.js "^2.0.2"
+    svg.pathmorphing.js "^0.1.3"
+    svg.resize.js "^1.4.3"
+    svg.select.js "^3.0.1"
+
 array-back@^3.0.1, array-back@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
@@ -2037,6 +2049,61 @@ supports-preserve-symlinks-flag@^1.0.0:
   resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
   integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
 
+svg.draggable.js@^2.2.2:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz#c514a2f1405efb6f0263e7958f5b68fce50603ba"
+  integrity sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==
+  dependencies:
+    svg.js "^2.0.1"
+
+svg.easing.js@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/svg.easing.js/-/svg.easing.js-2.0.0.tgz#8aa9946b0a8e27857a5c40a10eba4091e5691f12"
+  integrity sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==
+  dependencies:
+    svg.js ">=2.3.x"
+
+svg.filter.js@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/svg.filter.js/-/svg.filter.js-2.0.2.tgz#91008e151389dd9230779fcbe6e2c9a362d1c203"
+  integrity sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==
+  dependencies:
+    svg.js "^2.2.5"
+
+svg.js@>=2.3.x, svg.js@^2.0.1, svg.js@^2.2.5, svg.js@^2.4.0, svg.js@^2.6.5:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/svg.js/-/svg.js-2.7.1.tgz#eb977ed4737001eab859949b4a398ee1bb79948d"
+  integrity sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==
+
+svg.pathmorphing.js@^0.1.3:
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz#c25718a1cc7c36e852ecabc380e758ac09bb2b65"
+  integrity sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==
+  dependencies:
+    svg.js "^2.4.0"
+
+svg.resize.js@^1.4.3:
+  version "1.4.3"
+  resolved "https://registry.yarnpkg.com/svg.resize.js/-/svg.resize.js-1.4.3.tgz#885abd248e0cd205b36b973c4b578b9a36f23332"
+  integrity sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==
+  dependencies:
+    svg.js "^2.6.5"
+    svg.select.js "^2.1.2"
+
+svg.select.js@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-2.1.2.tgz#e41ce13b1acff43a7441f9f8be87a2319c87be73"
+  integrity sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==
+  dependencies:
+    svg.js "^2.2.5"
+
+svg.select.js@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/svg.select.js/-/svg.select.js-3.0.1.tgz#a4198e359f3825739226415f82176a90ea5cc917"
+  integrity sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==
+  dependencies:
+    svg.js "^2.6.5"
+
 svgo@^2.7.0:
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
@@ -2168,11 +2235,6 @@ vite@^3.0.0:
   optionalDependencies:
     fsevents "~2.3.2"
 
-vue-apexcharts@^1.6.2:
-  version "1.6.2"
-  resolved "https://registry.yarnpkg.com/vue-apexcharts/-/vue-apexcharts-1.6.2.tgz#0547826067f97e8ea67ca9423e524eb6669746ad"
-  integrity sha512-9HS3scJwWgKjmkcWIf+ndNDR0WytUJD8Ju0V2ZYcjYtlTLwJAf2SKUlBZaQTkDmwje/zMgulvZRi+MXmi+WkKw==
-
 vue-chartjs@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/vue-chartjs/-/vue-chartjs-4.1.1.tgz#b1ffc2845e09d14cb5255305b11bd3e8df8058ab"
@@ -2204,6 +2266,11 @@ vue-types@^3.0.0:
   dependencies:
     is-plain-object "3.0.1"
 
+vue3-apexcharts@^1.4.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d"
+  integrity sha512-96qP8JDqB9vwU7bkG5nVU+E0UGQn7yYQVqUUCLQMYWDuQyu2vE77H/UFZ1yI+hwzlSTBKT9BqnNG8JsFegB3eg==
+
 vue3-gettext@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/vue3-gettext/-/vue3-gettext-2.3.0.tgz#5825949e3978aa576e035128de46c97e59b65a5b"

+ 8 - 8
server/analytic/analytic.go

@@ -6,18 +6,18 @@ import (
 	"time"
 )
 
-type usage struct {
+type Usage struct {
 	Time  time.Time   `json:"x"`
 	Usage interface{} `json:"y"`
 }
 
 var (
-	CpuUserRecord   []usage
-	CpuTotalRecord  []usage
-	NetRecvRecord   []usage
-	NetSentRecord   []usage
-	DiskWriteRecord []usage
-	DiskReadRecord  []usage
+	CpuUserRecord   []Usage
+	CpuTotalRecord  []Usage
+	NetRecvRecord   []Usage
+	NetSentRecord   []Usage
+	DiskWriteRecord []Usage
+	DiskReadRecord  []Usage
 	LastDiskWrites  uint64
 	LastDiskReads   uint64
 	LastNetSent     uint64
@@ -37,7 +37,7 @@ func init() {
 	now := time.Now()
 	// init record slices
 	for i := 100; i > 0; i-- {
-		u := usage{Time: now.Add(time.Duration(-i) * time.Second), Usage: 0}
+		u := Usage{Time: now.Add(time.Duration(-i) * time.Second), Usage: 0}
 		CpuUserRecord = append(CpuUserRecord, u)
 		CpuTotalRecord = append(CpuTotalRecord, u)
 		NetRecvRecord = append(NetRecvRecord, u)

+ 89 - 89
server/analytic/record.go

@@ -1,115 +1,115 @@
 package analytic
 
 import (
-    "github.com/go-acme/lego/v4/log"
-    "github.com/shirou/gopsutil/v3/cpu"
-    "github.com/shirou/gopsutil/v3/disk"
-    "github.com/shirou/gopsutil/v3/net"
-    "runtime"
-    "time"
+	"github.com/go-acme/lego/v4/log"
+	"github.com/shirou/gopsutil/v3/cpu"
+	"github.com/shirou/gopsutil/v3/disk"
+	"github.com/shirou/gopsutil/v3/net"
+	"runtime"
+	"time"
 )
 
 func getTotalDiskIO() (read, write uint64) {
-    diskIOCounters, err := disk.IOCounters()
-    if err != nil {
-        log.Println("getTotalDiskIO: get diskIOCounters err", err)
-        return
-    }
-    for _, v := range diskIOCounters {
-        write += v.WriteCount
-        read += v.ReadCount
-    }
-    return
+	diskIOCounters, err := disk.IOCounters()
+	if err != nil {
+		log.Println("getTotalDiskIO: get diskIOCounters err", err)
+		return
+	}
+	for _, v := range diskIOCounters {
+		write += v.WriteCount
+		read += v.ReadCount
+	}
+	return
 }
 
 func recordCpu(now time.Time) {
-    cpuTimesBefore, err := cpu.Times(false)
-    if err != nil {
-        log.Println("recordCpu: get cpuTimesBefore err", err)
-        return
-    }
-    time.Sleep(1000 * time.Millisecond)
-    cpuTimesAfter, err := cpu.Times(false)
-    if err != nil {
-        log.Println("recordCpu: get cpuTimesAfter err", err)
-        return
-    }
-    threadNum := runtime.GOMAXPROCS(0)
+	cpuTimesBefore, err := cpu.Times(false)
+	if err != nil {
+		log.Println("recordCpu: get cpuTimesBefore err", err)
+		return
+	}
+	time.Sleep(1000 * time.Millisecond)
+	cpuTimesAfter, err := cpu.Times(false)
+	if err != nil {
+		log.Println("recordCpu: get cpuTimesAfter err", err)
+		return
+	}
+	threadNum := runtime.GOMAXPROCS(0)
 
-    cpuUserUsage := (cpuTimesAfter[0].User - cpuTimesBefore[0].User) / (float64(1000*threadNum) / 1000)
-    cpuUserUsage *= 100
-    cpuSystemUsage := (cpuTimesAfter[0].System - cpuTimesBefore[0].System) / (float64(1000*threadNum) / 1000)
-    cpuSystemUsage *= 100
+	cpuUserUsage := (cpuTimesAfter[0].User - cpuTimesBefore[0].User) / (float64(1000*threadNum) / 1000)
+	cpuUserUsage *= 100
+	cpuSystemUsage := (cpuTimesAfter[0].System - cpuTimesBefore[0].System) / (float64(1000*threadNum) / 1000)
+	cpuSystemUsage *= 100
 
-    u := usage{
-        Time:  now,
-        Usage: cpuUserUsage,
-    }
+	u := Usage{
+		Time:  now,
+		Usage: cpuUserUsage,
+	}
 
-    CpuUserRecord = append(CpuUserRecord, u)
+	CpuUserRecord = append(CpuUserRecord, u)
 
-    s := usage{
-        Time:  now,
-        Usage: cpuUserUsage + cpuSystemUsage,
-    }
+	s := Usage{
+		Time:  now,
+		Usage: cpuUserUsage + cpuSystemUsage,
+	}
 
-    CpuTotalRecord = append(CpuTotalRecord, s)
+	CpuTotalRecord = append(CpuTotalRecord, s)
 
-    if len(CpuUserRecord) > 100 {
-        CpuUserRecord = CpuUserRecord[1:]
-    }
+	if len(CpuUserRecord) > 100 {
+		CpuUserRecord = CpuUserRecord[1:]
+	}
 
-    if len(CpuTotalRecord) > 100 {
-        CpuTotalRecord = CpuTotalRecord[1:]
-    }
+	if len(CpuTotalRecord) > 100 {
+		CpuTotalRecord = CpuTotalRecord[1:]
+	}
 }
 
 func recordNetwork(now time.Time) {
-    network, err := net.IOCounters(false)
+	network, err := net.IOCounters(false)
 
-    if err != nil {
-        log.Println("recordNetwork: get network err", err)
-        return
-    }
+	if err != nil {
+		log.Println("recordNetwork: get network err", err)
+		return
+	}
 
-    if len(network) == 0 {
-        return
-    }
-    NetRecvRecord = append(NetRecvRecord, usage{
-        Time:  now,
-        Usage: network[0].BytesRecv - LastNetRecv,
-    })
-    NetSentRecord = append(NetSentRecord, usage{
-        Time:  now,
-        Usage: network[0].BytesSent - LastNetSent,
-    })
-    LastNetRecv = network[0].BytesRecv
-    LastNetSent = network[0].BytesSent
-    if len(NetRecvRecord) > 100 {
-        NetRecvRecord = NetRecvRecord[1:]
-    }
-    if len(NetSentRecord) > 100 {
-        NetSentRecord = NetSentRecord[1:]
-    }
+	if len(network) == 0 {
+		return
+	}
+	NetRecvRecord = append(NetRecvRecord, Usage{
+		Time:  now,
+		Usage: network[0].BytesRecv - LastNetRecv,
+	})
+	NetSentRecord = append(NetSentRecord, Usage{
+		Time:  now,
+		Usage: network[0].BytesSent - LastNetSent,
+	})
+	LastNetRecv = network[0].BytesRecv
+	LastNetSent = network[0].BytesSent
+	if len(NetRecvRecord) > 100 {
+		NetRecvRecord = NetRecvRecord[1:]
+	}
+	if len(NetSentRecord) > 100 {
+		NetSentRecord = NetSentRecord[1:]
+	}
 }
 
 func recordDiskIO(now time.Time) {
-    readCount, writeCount := getTotalDiskIO()
+	readCount, writeCount := getTotalDiskIO()
 
-    DiskReadRecord = append(DiskReadRecord, usage{
-        Time:  now,
-        Usage: readCount - LastDiskReads,
-    })
-    DiskWriteRecord = append(DiskWriteRecord, usage{
-        Time:  now,
-        Usage: writeCount - LastDiskWrites,
-    })
-    if len(DiskReadRecord) > 100 {
-        DiskReadRecord = DiskReadRecord[1:]
-    }
-    if len(DiskWriteRecord) > 100 {
-        DiskWriteRecord = DiskWriteRecord[1:]
-    }
-    LastDiskWrites = writeCount
-    LastDiskReads = readCount
+	DiskReadRecord = append(DiskReadRecord, Usage{
+		Time:  now,
+		Usage: readCount - LastDiskReads,
+	})
+	DiskWriteRecord = append(DiskWriteRecord, Usage{
+		Time:  now,
+		Usage: writeCount - LastDiskWrites,
+	})
+	if len(DiskReadRecord) > 100 {
+		DiskReadRecord = DiskReadRecord[1:]
+	}
+	if len(DiskWriteRecord) > 100 {
+		DiskWriteRecord = DiskWriteRecord[1:]
+	}
+	LastDiskWrites = writeCount
+	LastDiskReads = readCount
 }

+ 128 - 64
server/api/analytic.go

@@ -1,19 +1,20 @@
 package api
 
 import (
-	"encoding/json"
 	"fmt"
 	"github.com/0xJacky/Nginx-UI/server/analytic"
+	"github.com/pkg/errors"
 	"github.com/shirou/gopsutil/v3/cpu"
 	"github.com/shirou/gopsutil/v3/disk"
 	"github.com/shirou/gopsutil/v3/host"
 	"github.com/shirou/gopsutil/v3/load"
 	"github.com/shirou/gopsutil/v3/mem"
 	"github.com/shirou/gopsutil/v3/net"
+	"github.com/spf13/cast"
+	"log"
 	"math"
 	"net/http"
 	"runtime"
-	"strconv"
 	"time"
 
 	"github.com/dustin/go-humanize"
@@ -21,6 +22,77 @@ import (
 	"github.com/gorilla/websocket"
 )
 
+type CPUStat struct {
+	User   float64 `json:"user"`
+	System float64 `json:"system"`
+	Idle   float64 `json:"idle"`
+	Total  float64 `json:"total"`
+}
+
+type MemStat struct {
+	Total       string  `json:"total"`
+	Used        string  `json:"used"`
+	Cached      string  `json:"cached"`
+	Free        string  `json:"free"`
+	SwapUsed    string  `json:"swap_used"`
+	SwapTotal   string  `json:"swap_total"`
+	SwapCached  string  `json:"swap_cached"`
+	SwapPercent float64 `json:"swap_percent"`
+	Pressure    float64 `json:"pressure"`
+}
+
+type DiskStat struct {
+	Total      string         `json:"total"`
+	Used       string         `json:"used"`
+	Percentage float64        `json:"percentage"`
+	Writes     analytic.Usage `json:"writes"`
+	Reads      analytic.Usage `json:"reads"`
+}
+
+type Stat struct {
+	Uptime  uint64             `json:"uptime"`
+	LoadAvg *load.AvgStat      `json:"loadavg"`
+	CPU     CPUStat            `json:"cpu"`
+	Memory  MemStat            `json:"memory"`
+	Disk    DiskStat           `json:"disk"`
+	Network net.IOCountersStat `json:"network"`
+}
+
+func getMemoryStat() (MemStat, error) {
+	memoryStat, err := mem.VirtualMemory()
+	if err != nil {
+		return MemStat{}, errors.Wrap(err, "error analytic getMemoryStat")
+	}
+	return MemStat{
+		Total:      humanize.Bytes(memoryStat.Total),
+		Used:       humanize.Bytes(memoryStat.Used),
+		Cached:     humanize.Bytes(memoryStat.Cached),
+		Free:       humanize.Bytes(memoryStat.Free),
+		SwapUsed:   humanize.Bytes(memoryStat.SwapTotal - memoryStat.SwapFree),
+		SwapTotal:  humanize.Bytes(memoryStat.SwapTotal),
+		SwapCached: humanize.Bytes(memoryStat.SwapCached),
+		SwapPercent: cast.ToFloat64(fmt.Sprintf("%.2f",
+			float64(memoryStat.SwapFree)/math.Max(float64(memoryStat.SwapTotal), 1))),
+		Pressure: cast.ToFloat64(fmt.Sprintf("%.2f", memoryStat.UsedPercent)),
+	}, nil
+}
+
+func getDiskStat() (DiskStat, error) {
+	diskUsage, err := disk.Usage(".")
+
+	if err != nil {
+		return DiskStat{}, errors.Wrap(err, "error analytic getDiskStat")
+	}
+
+	return DiskStat{
+		Used:       humanize.Bytes(diskUsage.Used),
+		Total:      humanize.Bytes(diskUsage.Total),
+		Percentage: cast.ToFloat64(fmt.Sprintf("%.2f", diskUsage.UsedPercent)),
+		Writes:     analytic.DiskWriteRecord[len(analytic.DiskWriteRecord)-1],
+		Reads:      analytic.DiskReadRecord[len(analytic.DiskReadRecord)-1],
+	}, nil
+}
+
 func Analytic(c *gin.Context) {
 	var upGrader = websocket.Upgrader{
 		CheckOrigin: func(r *http.Request) bool {
@@ -30,91 +102,81 @@ func Analytic(c *gin.Context) {
 	// upgrade http to websocket
 	ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
 	if err != nil {
+		log.Println("[Error] Analytic Upgrade", err)
 		return
 	}
 
 	defer ws.Close()
 
-	response := make(gin.H)
+	var stat Stat
 
 	for {
-		// read
-		mt, message, err := ws.ReadMessage()
+		stat.Memory, err = getMemoryStat()
+
 		if err != nil {
-			break
+			log.Println(err)
+			return
 		}
-		for {
-
-			memoryStat, err := mem.VirtualMemory()
-			if err != nil {
-				fmt.Println(err)
-				return
-			}
-
-			response["memory_total"] = humanize.Bytes(memoryStat.Total)
-			response["memory_used"] = humanize.Bytes(memoryStat.Used)
-			response["memory_cached"] = humanize.Bytes(memoryStat.Cached)
-			response["memory_free"] = humanize.Bytes(memoryStat.Free)
-			response["memory_swap_used"] = humanize.Bytes(memoryStat.SwapTotal - memoryStat.SwapFree)
-			response["memory_swap_total"] = humanize.Bytes(memoryStat.SwapTotal)
-			response["memory_swap_cached"] = humanize.Bytes(memoryStat.SwapCached)
-			response["memory_swap_percent"] = float64(memoryStat.SwapFree) / math.Max(float64(memoryStat.SwapTotal), 1)
-
-			response["memory_pressure"], _ = strconv.ParseFloat(fmt.Sprintf("%.2f", memoryStat.UsedPercent), 64)
 
-			cpuTimesBefore, _ := cpu.Times(false)
-			time.Sleep(1000 * time.Millisecond)
-			cpuTimesAfter, _ := cpu.Times(false)
-			threadNum := runtime.GOMAXPROCS(0)
-
-			cpuUserUsage := (cpuTimesAfter[0].User - cpuTimesBefore[0].User) / (float64(1000*threadNum) / 1000)
-			cpuSystemUsage := (cpuTimesAfter[0].System - cpuTimesBefore[0].System) / (float64(1000*threadNum) / 1000)
-
-			response["cpu_user"], _ = strconv.ParseFloat(fmt.Sprintf("%.2f",
-				cpuUserUsage*100), 64)
-
-			response["cpu_system"], _ = strconv.ParseFloat(fmt.Sprintf("%.2f",
-				cpuSystemUsage*100), 64)
-
-			response["cpu_idle"], _ = strconv.ParseFloat(fmt.Sprintf("%.2f",
-				(1-cpuUserUsage+cpuSystemUsage)*100), 64)
-
-			response["uptime"], _ = host.Uptime()
-			response["loadavg"], _ = load.Avg()
+		cpuTimesBefore, _ := cpu.Times(false)
+		time.Sleep(1000 * time.Millisecond)
+		cpuTimesAfter, _ := cpu.Times(false)
+		threadNum := runtime.GOMAXPROCS(0)
+		cpuUserUsage := (cpuTimesAfter[0].User - cpuTimesBefore[0].User) / (float64(1000*threadNum) / 1000)
+		cpuSystemUsage := (cpuTimesAfter[0].System - cpuTimesBefore[0].System) / (float64(1000*threadNum) / 1000)
+
+		stat.CPU = CPUStat{
+			User:   cast.ToFloat64(fmt.Sprintf("%.2f", cpuUserUsage*100)),
+			System: cast.ToFloat64(fmt.Sprintf("%.2f", cpuSystemUsage*100)),
+			Idle:   cast.ToFloat64(fmt.Sprintf("%.2f", (1-cpuUserUsage-cpuSystemUsage)*100)),
+			Total:  cast.ToFloat64(fmt.Sprintf("%.2f", (cpuUserUsage+cpuSystemUsage)*100)),
+		}
 
-			diskUsage, _ := disk.Usage(".")
+		stat.Uptime, _ = host.Uptime()
 
-			response["disk_used"] = humanize.Bytes(diskUsage.Used)
-			response["disk_total"] = humanize.Bytes(diskUsage.Total)
-			response["disk_percentage"], _ = strconv.ParseFloat(fmt.Sprintf("%.2f", diskUsage.UsedPercent), 64)
+		stat.LoadAvg, _ = load.Avg()
 
-			response["diskIO"] = gin.H{
-				"writes": analytic.DiskWriteRecord[len(analytic.DiskWriteRecord)-1],
-				"reads":  analytic.DiskReadRecord[len(analytic.DiskReadRecord)-1],
-			}
+		stat.Disk, err = getDiskStat()
 
-			network, _ := net.IOCounters(false)
+		if err != nil {
+			log.Println(err)
+			return
+		}
 
-			if len(network) > 0 {
-				response["network"] = network[0]
-			}
+		network, _ := net.IOCounters(false)
 
-			m, _ := json.Marshal(response)
-			message = m
+		if len(network) > 0 {
+			stat.Network = network[0]
+		}
 
-			// write
-			err = ws.WriteMessage(mt, message)
-			if err != nil {
-				break
-			}
-			time.Sleep(800 * time.Microsecond)
+		// write
+		err = ws.WriteJSON(stat)
+		if err != nil {
+			log.Println("[Error] analytic WriteJSON", err)
+			break
 		}
+		time.Sleep(800 * time.Microsecond)
 	}
+
 }
 
 func GetAnalyticInit(c *gin.Context) {
 	cpuInfo, _ := cpu.Info()
 	network, _ := net.IOCounters(false)
+	memory, err := getMemoryStat()
+
+	if err != nil {
+		log.Println(err)
+		return
+	}
+
+	disk, err := getDiskStat()
+
+	if err != nil {
+		log.Println(err)
+		return
+	}
+
 	var _net net.IOCountersStat
 	if len(network) > 0 {
 		_net = network[0]
@@ -133,9 +195,11 @@ func GetAnalyticInit(c *gin.Context) {
 			"bytesRecv": analytic.NetRecvRecord,
 			"bytesSent": analytic.NetSentRecord,
 		},
-		"diskIO": gin.H{
+		"disk_io": gin.H{
 			"writes": analytic.DiskWriteRecord,
 			"reads":  analytic.DiskReadRecord,
 		},
+		"memory": memory,
+		"disk":   disk,
 	})
 }