limengbo hace 3 años
commit
9d8b88b06a

+ 3 - 0
.env.development

@@ -0,0 +1,3 @@
+NODE_ENV = 'development'
+VUE_APP_MODE = 'development'
+VUE_APP_BASE_API = 'http://asxx.efunbox.cn/'

+ 3 - 0
.env.production

@@ -0,0 +1,3 @@
+NODE_ENV = 'production'
+VUE_APP_MODE = 'production'
+VUE_APP_BASE_API = 'http://asxx.efunbox.cn/'

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+.DS_Store
+node_modules
+/dist
+
+
+# local env files
+.env.local
+.env.*.local
+
+# Log files
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 24 - 0
README.md

@@ -0,0 +1,24 @@
+# efunbox-yfxxt-statistics
+
+## Project setup
+```
+npm install
+```
+
+### Compiles and hot-reloads for development
+```
+npm serve
+```
+
+### Compiles and minifies for production
+```
+npm build
+```
+
+### Lints and fixes files
+```
+npm lint
+```
+
+### Customize configuration
+See [Configuration Reference](https://cli.vuejs.org/config/).

+ 5 - 0
babel.config.js

@@ -0,0 +1,5 @@
+module.exports = {
+  presets: [
+    '@vue/cli-plugin-babel/preset'
+  ]
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 13705 - 0
package-lock.json


+ 52 - 0
package.json

@@ -0,0 +1,52 @@
+{
+  "name": "efunbox-yfxxt-statistics",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "serve": "vue-cli-service serve",
+    "build": "vue-cli-service build",
+    "lint": "vue-cli-service lint"
+  },
+  "dependencies": {
+    "ant-design-vue": "^1.6.4",
+    "axios": "^0.19.2",
+    "core-js": "^3.6.5",
+    "echarts": "^4.8.0",
+    "vue": "^2.6.11",
+    "vue-router": "^3.4.2"
+  },
+  "devDependencies": {
+    "@vue/cli-plugin-babel": "~4.4.4",
+    "@vue/cli-plugin-eslint": "~4.4.4",
+    "@vue/cli-service": "~4.4.4",
+    "babel-eslint": "^10.1.0",
+    "compression-webpack-plugin": "^5.0.0",
+    "eslint": "^6.7.2",
+    "eslint-plugin-vue": "^6.2.2",
+    "sass": "^1.26.10",
+    "sass-loader": "^9.0.3",
+    "uglifyjs-webpack-plugin": "^2.2.0",
+    "vue-template-compiler": "^2.6.10",
+    "webpack-bundle-analyzer": "^3.8.0",
+    "webpack-report": "^1.2.0"
+  },
+  "eslintConfig": {
+    "root": true,
+    "env": {
+      "node": true
+    },
+    "extends": [
+      "plugin:vue/essential",
+      "eslint:recommended"
+    ],
+    "parserOptions": {
+      "parser": "babel-eslint"
+    },
+    "rules": {}
+  },
+  "browserslist": [
+    "> 1%",
+    "last 2 versions",
+    "not dead"
+  ]
+}

BIN
public/favicon.ico


+ 16 - 0
public/index.html

@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <title>义方小学堂v2.0数据分析后台</title>
+  </head>
+  <body>
+    <noscript>
+      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
+    </noscript>
+    <div id="app"></div>
+    <!-- built files will be auto injected -->
+  </body>
+</html>

+ 34 - 0
src/App.vue

@@ -0,0 +1,34 @@
+<template>
+  <div id="app">
+    <router-view />
+  </div>
+</template>
+
+<script>
+import Vue from 'vue'
+export default {
+  name: 'App',
+  created() {
+    //在页面加载时读取sessionStorage里的状态信息
+    if (sessionStorage.getItem("store") ) {
+        this.$store.states = Vue.observable(Object.assign({}, this.$store.states,JSON.parse(sessionStorage.getItem("store"))))
+    }
+
+    //在页面刷新时将vuex里的信息保存到sessionStorage里
+    window.addEventListener("beforeunload",()=>{
+        sessionStorage.setItem("store",JSON.stringify(this.$store.states))
+    })
+  }
+}
+</script>
+
+<style>
+#app {
+  font-family: Avenir, Helvetica, Arial, sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  width: 100%;
+  height: 100%;
+  background: #F7F7F7
+}
+</style>

+ 26 - 0
src/api/statistics.js

@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 获取单日统计数据
+export function getDayData(params) {
+    return request({
+        url: 'dataAnalysis/day',
+        mothod: 'get',
+        params
+    })
+}
+// 获取时间段数据信息
+export function getDataAnalysis(params) {
+    return request({
+        url: 'dataAnalysis',
+        mothod: 'get',
+        params
+    })
+}
+// 获取热门课程
+export function getHotCourseData(params) {
+    return request({
+        url: 'dataAnalysis/hotCourse',
+        mothod: 'get',
+        params
+    })
+}

+ 18 - 0
src/api/user.js

@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 获取单日统计数据
+export function getUserInfo(params) {
+    return request({
+        url: 'dataAnalysis/member/info',
+        mothod: 'get',
+        params
+    })
+}
+// 获取在线时长
+export function getMemberData(params) {
+    return request({
+        url: 'dataAnalysis/member',
+        mothod: 'get',
+        params
+    })
+}

BIN
src/assets/logo.png


+ 60 - 0
src/components/BrokenLine.vue

@@ -0,0 +1,60 @@
+<template>
+    <div :id="id" :class="className" :style="{height:height, width:width, marginTop:'20px'}" />
+</template>
+<script>
+import echarts from 'echarts'
+export default {
+    name: 'BrokenLine',
+    props: {
+        id: {
+            type: String,
+            default: 'charts'
+        },
+        className: {
+            type: String,
+            default: 'charts'
+        },
+        width: {
+            type: String,
+            default: '200px'
+        },
+        height: {
+            type: String,
+            default: '200px'
+        },
+        dataOption: {
+            type: Object,
+            default: null
+        }
+    },
+    data() {
+        return {
+            chart: null
+        }
+    },
+    watch: {
+        dataOption () {
+            // 监听传值修改数据
+            this.initChart()
+        }
+    },
+    mounted() {
+        this.initChart()
+    },
+    beforeDestroy() {
+        if (!this.chart) {
+            return
+        }
+        this.chart.dispose()
+        this.chart = null
+    },
+    methods: {
+        // 初始化chart
+        initChart() {
+            this.chart = echarts.init(document.getElementById(this.id));
+            const option = this.dataOption
+            this.chart.setOption(option)
+        }
+    }
+}
+</script>

+ 101 - 0
src/components/DateTitle.vue

@@ -0,0 +1,101 @@
+<template>
+    <div class="date_title">
+        <p class="title">{{title}}</p>
+        <div class="date">
+            <span class="date_son_title">查询日期</span>
+            <a-date-picker
+            v-model="startValue"
+            :disabled-date="disabledStartDate"
+            format="YYYY-MM-DD"
+            placeholder="请选择日期"
+            />
+            <span v-if="!one">——</span>
+            <a-date-picker
+            v-if="!one"
+            v-model="endValue"
+            :disabled-date="disabledEndDate"
+            format="YYYY-MM-DD"
+            placeholder="请选择日期"
+            />
+        </div>
+    </div>
+</template>
+<script>
+export default {
+    name: 'DateTitle',
+    props: {
+        title: {
+            type: String,
+            default: ''
+        },
+        one: {
+            type: Boolean,
+            default: false
+        }
+    },
+    data() {
+        return {
+            startValue: null,
+            endValue: null
+        }
+    },
+    watch: {
+        startValue(val) {
+            console.log('startValue', val);
+            this.changeDate();
+        },
+        endValue(val) {
+            console.log('endValue', val);
+            this.changeDate();
+        },
+    },
+    methods: {
+        // 限定开始日期不能超出结束日期
+        disabledStartDate(startValue) {
+            const endValue = this.endValue;
+            if (!startValue || !endValue) {
+                return false;
+            }
+            return startValue.valueOf() > endValue.valueOf();
+        },
+        disabledEndDate(endValue) {
+            const startValue = this.startValue;
+            if (!endValue || !startValue) {
+                return false;
+            }
+            return startValue.valueOf() >= endValue.valueOf();
+        },
+        // 日期全部选中执行方法
+        changeDate() {
+            const endValue = this.endValue;
+            const startValue = this.startValue;
+            if (this.one && startValue) {
+                const satartTime = startValue.format('YYYY-MM-DD');
+                this.$emit('getChangeDate', satartTime)
+            }
+            if (startValue && endValue) {
+                const satartTime = startValue.format('YYYY-MM-DD');
+                const endTime = endValue.format('YYYY-MM-DD');
+                this.$emit('getChangeDate', satartTime, endTime)
+            }
+        }
+    }
+}
+</script>
+<style scope lang="scss">
+    .date_title {
+        .title {
+            margin: 33px 0 24px 29px;
+            display: inline-block;
+        }
+        .date {
+            span {
+                margin: 0 5px;
+                color: #AEAEAE;
+            }
+            .date_son_title {
+                margin-left: 25px;
+            }
+        }
+    }
+</style>

+ 22 - 0
src/main.js

@@ -0,0 +1,22 @@
+import Vue from 'vue'
+import App from './App.vue'
+import { Button, Input, DatePicker, Icon, Table, message  } from 'ant-design-vue';
+
+import 'ant-design-vue/dist/antd.css';
+
+import router from './router'
+import store from '@/store'
+
+Vue.config.productionTip = false
+Vue.use(Button);
+Vue.use(Input);
+Vue.use(DatePicker);
+Vue.use(Icon);
+Vue.use(Table)
+Vue.prototype.$message = message
+Vue.prototype.$store = store
+
+new Vue({
+  render: h => h(App),
+  router
+}).$mount('#app')

+ 43 - 0
src/page/components/Header.vue

@@ -0,0 +1,43 @@
+<template>
+    <div class="header">
+        <h3>义方小学堂v2.0数据分析后台</h3>
+        <a-input-search placeholder="" style="width: 220px; position: absolute; right: 87px; top: 15px;" @search="onSearch" />
+    </div>
+</template>
+<script>
+export default {
+    name: 'Header',
+    methods: {
+        onSearch(value) {
+            console.log(value);
+            const phone = /^1[3456789]\d{9}$/
+            if (!phone.test(value)) {
+                this.$message.error('手机号错误')
+                return false
+            }
+            this.$store.dispatch('getUserInfoList', {
+                mobileNo: value
+            }).then(() => {
+                if (this.$route.path !== '/user') {
+                    this.$router.push('/user')
+                }
+            })
+        }
+    },
+}
+</script>
+<style scoped lang="scss">
+    .header {
+        flex-shrink: 0;
+        position: relative;
+        width: 100%;
+        height: 62px;
+        background: #fff;
+        h3 {
+            position: absolute;
+            left: 85px;
+            top: 19px;
+            color: #0086D8;
+        }
+    }
+</style>

+ 8 - 0
src/page/components/Main.vue

@@ -0,0 +1,8 @@
+<template>
+    <router-view />
+</template>
+<script>
+export default {
+    name: 'Main'
+}
+</script>

+ 26 - 0
src/page/index.vue

@@ -0,0 +1,26 @@
+<template>
+    <div class="big_container">
+        <Header />
+        <Main />
+    </div>
+</template>
+<script>
+import Header from './components/Header.vue'
+import Main from './components/Main.vue'
+export default {
+    name: 'page',
+    components: {
+        Header,
+        Main
+    }
+}
+</script>
+<style scoped lang="scss">
+    .big_container {
+        display: flex;
+        flex-direction: column;
+        width: 100%;
+        height: 100%;
+        overflow: hidden;
+    }
+</style>

+ 134 - 0
src/page/views/statistics/index.vue

@@ -0,0 +1,134 @@
+<template>
+    <div class="statistics">
+        <div class="data_look">
+            <DateTitle title="数据概览" :one="true" @getChangeDate="dataChange"/>
+            <ul class="data_container">
+                <li v-for="item in dataContainer" :key="item.value">
+                    <span>{{item.name}}</span>
+                    <span class="container_number">{{item.value}}</span>
+                </li>
+            </ul>
+        </div>
+        <div class="online_time">
+            <DateTitle title="用户单日平均在线时长" @getChangeDate="onlineChange"/>
+            <BrokenLine id="onlineChart" className="online_chart" width="100%" height="338px" :dataOption="onlineOption"/>
+        </div>
+        <div class="hot_course">
+            <DateTitle title="热门课程排行" @getChangeDate="hotChange"/>
+            <BrokenLine id="courseChart" class="course_chart" width="100%" height="505px" :dataOption="hotOption"/>
+        </div>
+        <div class="page_time">
+            <DateTitle title="主要功能模块访问数据" @getChangeDate="pageChange"/>
+            <BrokenLine id="pageChart" className="page_chart" width="100%" height="338px" :dataOption="pageOption"/>
+        </div>
+    </div>
+</template>
+<script>
+import DateTitle from '../../../components/DateTitle.vue'
+import BrokenLine from '../../../components/BrokenLine.vue'
+export default {
+    name: 'Statistics',
+    components: {
+        DateTitle,
+        BrokenLine
+    },
+    data() {
+        return {
+        }
+    },
+    computed: {
+        dataContainer() {
+            return this.$store.states.baydataList
+        },
+        onlineOption() {
+            return this.$store.states.dataAnalysisList
+        },
+        hotOption() {
+            return this.$store.states.hotCourseList
+        },
+        pageOption() {
+            console.log(this.$store.states.pageOptionData)
+            return this.$store.states.pageOptionData
+        },
+    },
+    created() {
+    },
+    methods: {
+        // 获取数据概览数据
+        dataChange(satartTime) {
+            console.log(satartTime)
+            this.$store.dispatch('getBayDataList', {
+                day: satartTime
+            })
+        },
+        // 获取用户平均在线时长
+        onlineChange(startDay, endDay) {
+            console.log(startDay, endDay)
+            this.$store.dispatch('getDataAnalysisList', {
+               startDay,
+               endDay
+            })
+        },
+        // 获取热门课程排行
+        hotChange(startDay, endDay) {
+            console.log(startDay, endDay)
+            this.$store.dispatch('getHotCourseList', {
+               startDay,
+               endDay
+            })
+        },
+        // 获取主要功能访问数据
+        pageChange(startDay, endDay) {
+            console.log(startDay, endDay)
+            this.$store.dispatch('getPageOption', {
+               startDay,
+               endDay
+            })
+        }
+    }
+}
+</script>
+<style scoped lang="scss">
+.statistics {
+    flex: 1;
+    overflow: scroll;
+    width: 100%;
+    padding: 40px 85px;
+    box-sizing: border-box;
+    .data_look,
+    .online_time,
+    .hot_course,
+    .page_time {
+        width: 100%;
+        height: auto;
+        background: #fff;
+    }
+    .data_container {
+        display: flex;
+        padding: 62px 0;
+        box-sizing: border-box;
+        list-style:none;
+        li {
+            flex: 1;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            height: 90px;
+            border-right: 1px solid #ADADAD;
+            &:last-child {
+                border: 0px;
+            }
+            span {
+                font-size: 20px;
+                color: #252525;
+            }
+            .container_number {
+                color: #108EE9;
+                font-size: 40px;
+            }
+        }
+    }
+}
+
+</style>

+ 189 - 0
src/page/views/user/index.vue

@@ -0,0 +1,189 @@
+<template>
+    <div class="user">
+        <div class="user_message">
+            <div class="message_title">
+                <p class="title">用户信息</p>
+            </div>
+            <ul class="message_container">
+                <li>
+                    <span>用户ID:</span>
+                    <a-input :placeholder="userInfoList.member.eid" disabled/>
+                </li>
+                <li>
+                    <span>手机号:</span>
+                    <a-input :placeholder="userInfoList.member.mobile" disabled/>
+                </li>
+                <li>
+                   <span>微信号:</span>
+                    <a-input placeholder="" disabled/>
+                </li>
+                <li>
+                    <span>注册时间:</span>
+                    <a-input :placeholder="time" disabled/>
+                </li>
+            </ul>
+        </div>
+        <a-button type="primary" @click="orderHide">订购信息 <a-icon :type="show ? 'up' : 'down'" /></a-button>
+        <div class="order_table" v-show="show">
+            <a-table :columns="columns" :data-source="data" :bordered="true" :pagination="false">
+                <a slot="name" slot-scope="text">{{ text }}</a>
+            </a-table>
+        </div>
+        <div class="online_time">
+            <DateTitle title="用户单日平均在线时长" @getChangeDate="onlineChange"/>
+            <BrokenLine id="onlineChart" className="online_chart" width="100%" height="338px" :dataOption="onlineOption"/>
+        </div>
+    </div>
+</template>
+<script>
+import DateTitle from '../../../components/DateTitle.vue'
+import BrokenLine from '../../../components/BrokenLine.vue'
+export default {
+    name: 'User',
+    components: {
+        DateTitle,
+        BrokenLine
+    },
+    data() {
+        return {
+            show: false,
+            columns: [
+                {
+                    title: '购买产品',
+                    dataIndex: 'name',
+                    key: 'name',
+                    width: 430,
+                    align: 'center'
+                },
+                {
+                    title: '购买时间',
+                    dataIndex: 'time',
+                    key: 'time',
+                    width: 430,
+                    align: 'center'
+                },
+                {
+                    title: '有效期',
+                    dataIndex: 'valid',
+                    key: 'valid',
+                    width: 430,
+                    align: 'center'
+                },
+                {
+                    title: '价格',
+                    dataIndex: 'money',
+                    key: 'money',
+                    width: 430,
+                    align: 'center'
+                }
+            ]
+        }
+    },
+    computed: {
+        userInfoList() {
+            return this.$store.states.userInfoList
+        },
+        data() {
+            const orderInfoList = this.userInfoList.orderInfoList
+            const arr = []
+            orderInfoList.forEach((item, index) => {
+                const product = item.product
+                const orderInfo = item.orderInfo
+                const time = new Date(orderInfo.gmtCreated).toLocaleDateString()
+                const valid = new Date(orderInfo.gmtModified).toLocaleDateString()
+                console.log(item)
+                arr.push({
+                    key: index,
+                    name: product.title,
+                    time,
+                    valid,
+                    money: `${product.firstMonthPrice / 100}元`
+                })
+            });
+            console.log(arr)
+            return arr
+        },
+        time() {
+            const date = this.userInfoList.member.gmtCreated
+            return new Date(date).toLocaleDateString()
+        },
+        // 用户在线时长数据折线图
+        onlineOption() {
+            return this.$store.states.memberList
+        }
+    },
+    created() {
+    },
+    methods: {
+        // 订购消息展开和闭合
+        orderHide() {
+            this.show = !this.show
+        },
+        // 获取用户平均在线时长
+        onlineChange(startDay, endDay) {
+            console.log(startDay, endDay)
+            const uid = this.userInfoList.member.uid
+            this.$store.dispatch('getMemberList', {
+               uid,
+               startDay,
+               endDay
+            })
+        }
+    }
+}
+</script>
+<style scoped lang="scss">
+.user {
+    flex: 1;
+    overflow: scroll;
+    width: 100%;
+    padding: 40px 85px;
+    box-sizing: border-box;
+    .user_message,
+    .order_table,
+    .online_time {
+        width: 100%;
+        height: auto;
+        background: #fff;
+        margin-bottom: 20px;
+    }
+    .message_title {
+        .title {
+            margin: 33px 0 24px 29px;
+            display: inline-block;
+        }
+    }
+    .message_container {
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: space-between;
+        padding: 0 164px 36px 27px;
+        box-sizing: border-box;
+        li {
+            display: flex;
+            align-items: center;
+            height: 60px;
+            width: 40%;
+            margin-top: 30px;
+            span {
+                font-size: 20px;
+                white-space: nowrap;
+                margin-right: 20px;
+            }
+            .ant-input {
+                width: 498px;
+                height: 100%;
+            }
+        }
+    }
+    .ant-btn {
+        width: 140px;
+        height: 50px;
+        background: #fb9b2e;
+        border-color: #fb9b2e;
+        margin: 16px 0;
+        font-size: 18px;
+    }
+}
+
+</style>

+ 26 - 0
src/router/index.js

@@ -0,0 +1,26 @@
+import vue from 'vue';
+import Router from 'vue-router';
+vue.use(Router);
+import page from '../page/index.vue'
+const routes =[
+    {
+        path: '/',
+        component: page,
+        children: [
+            {
+                path: '/statistics',
+                component:  () => import('../page/views/statistics/index.vue'),
+            },
+            {
+                path: '/user',
+                component:  () => import('../page/views/user/index.vue'),
+            },
+            { path: '', component:  () => import('../page/views/statistics/index.vue') },
+        ]
+    }
+]
+const createRoute = () => new Router({
+    routes
+})
+const route = createRoute()
+export default route;

+ 46 - 0
src/store/actions.js

@@ -0,0 +1,46 @@
+import { getDayData, getDataAnalysis, getHotCourseData } from '@/api/statistics'
+import { getUserInfo, getMemberData } from '@/api/user'
+const actions = {
+    getBayDataList: (commit, params) => {
+        getDayData(params).then(res => {
+            const data = res.data
+            commit('setBayDataList', data)
+        })
+    },
+    getDataAnalysisList: (commit, params) => {
+        getDataAnalysis(params).then(res => {
+            const data = res.data
+            commit('setDataAnalysisList', data)
+        })
+    },
+    getHotCourseList: (commit, params) => {
+        getHotCourseData(params).then(res => {
+            const data = res.data
+            commit('setHotCourseList', data)
+        })
+    },
+    getPageOption: (commit, params) => {
+        getDataAnalysis(params).then(res => {
+            const data = res.data
+            commit('setPageOption', data)
+        })
+    },
+    getUserInfoList: (commit, params) => {
+        return new Promise((resolve, reject) => {
+            getUserInfo(params).then(res => {
+                const data = res.data
+                commit('setUserInfoList', data)
+                resolve(data)
+            }).catch(error => {
+                reject(error)
+            })
+        })
+    },
+    getMemberList: (commit, params) => {
+        getMemberData(params).then(res => {
+            const data = res.data
+            commit('setMemberList', data)
+        })
+    },
+}
+export default actions

+ 10 - 0
src/store/index.js

@@ -0,0 +1,10 @@
+import Store from './store';
+import states from './states';
+import actions from './actions';
+import mutations from './mutations';
+const store = new Store({
+    states,
+    actions,
+    mutations
+})
+export default store

+ 243 - 0
src/store/mutations.js

@@ -0,0 +1,243 @@
+const mutations = {
+    setBayDataList: (state, data) => {
+        // 新增用户
+        const appendUser = data && data.appendUser;
+        // 用户
+        const uv = data && data.uv;
+        // 付费用户
+        const vipUv = data && data.vipUv;
+        // 累计用户
+        const totalUser = data && data.totalUser;
+        // 累计付费用户
+        const totalVipUser = data && data.totalVipUser;
+        const arrDatd = [
+            {
+                name: '新增用户',
+                value: appendUser
+            },
+            {
+                name: '用户数',
+                value: uv
+            },
+            {
+                name: '付费用户',
+                value: vipUv
+            },
+            {
+                name: '累计用户',
+                value: totalUser
+            },
+            {
+                name: '累计付费用户',
+                value: totalVipUser
+            }
+        ]
+        state.baydataList = arrDatd
+    },
+    setDataAnalysisList: (state, data) => {
+        const dataDay = [];
+        const dataDate = [];
+        data.forEach(item => {
+            dataDay.push(item.day)
+            dataDate.push(item.avgOnlineTime)
+        });
+        const onlineOption = {
+            tooltip: {
+                trigger: 'axis',
+            },
+            xAxis: {
+                type: 'category',
+                data: dataDay
+            },
+            yAxis: {
+                type: 'value',
+                name: '分钟'
+            },
+            series: [{
+                data: dataDate,
+                type: 'line',
+                itemStyle: {
+                    color: '#2BCF82'
+                }
+            }]
+        }
+        state.dataAnalysisList = onlineOption
+    },
+    setHotCourseList: (state, data) => {
+        const courseData = []
+        const hotcourse = Object.keys(data)
+        hotcourse.forEach(key => {
+            courseData.push(data[key])
+        })
+        // 热门课程柱状图
+        const hotOption = {
+            tooltip: {
+                trigger: 'axis',
+                axisPointer: {
+                    type: 'shadow'
+                }
+            },
+            grid: {
+                left: '3%',
+                right: '4%',
+                bottom: '3%',
+                containLabel: true
+            },
+            xAxis: {
+                type: 'value',
+                boundaryGap: [0, 0.01],
+                name: '次'
+            },
+            yAxis: {
+                type: 'category',
+                data: ['语文 一年级 上册', '语文 一年级 下册', '语文 二年级 上册', '语文 二年级 下册', '语文 三年级 上册', '语文 三年级 下册', '语文 四年级 上册', '语文 四年级 下册']
+            },
+            series: {
+                type: 'bar',
+                data: courseData,
+                barWidth : 18,//柱图宽度
+                itemStyle: {
+                    color: '#03ABEF'
+                }
+            }
+        }
+        state.hotCourseList = hotOption
+    },
+    setPageOption: (state, data) => {
+        const dataDay = [];
+        const review = [];
+        const prepare = [];
+        const schoolStudy = [];
+        const wrongQuestions = [];
+        const memberRead = [];
+        const learnReport = [];
+        data.forEach(item => {
+            dataDay.push(item.day)
+            review.push(item.review)
+            prepare.push(item.prepare)
+            schoolStudy.push(item.schoolStudy)
+            wrongQuestions.push(item.wrongQuestions)
+            memberRead.push(item.memberRead)
+            learnReport.push(item.learnReport)
+        });
+        const pageOption =  {
+            tooltip: {
+                trigger: 'axis'
+            },
+            legend: {
+                data: ['错题本', '配音秀', '学习报告', '课前预习', '学校上课', '复习测试']
+            },
+            grid: {
+                left: '3%',
+                right: '4%',
+                bottom: '3%',
+                containLabel: true
+            },
+            toolbox: {
+                feature: {
+                    saveAsImage: {}
+                }
+            },
+            xAxis: {
+                type: 'category',
+                boundaryGap: false,
+                data: dataDay
+            },
+            yAxis: {
+                type: 'value',
+                name: '次'
+            },
+            series: [
+                {
+                    name: '错题本',
+                    type: 'line',
+                    stack: '总量',
+                    data: wrongQuestions,
+                    itemStyle: {
+                        color: '#FF900D'
+                    }
+                },
+                {
+                    name: '配音秀',
+                    type: 'line',
+                    stack: '总量',
+                    data: memberRead,
+                    itemStyle: {
+                        color: '#03ABEF'
+                    }
+                },
+                {
+                    name: '学习报告',
+                    type: 'line',
+                    stack: '总量',
+                    data: learnReport,
+                    itemStyle: {
+                        color: '#DC0072'
+                    }
+                },
+                {
+                    name: '课前预习',
+                    type: 'line',
+                    stack: '总量',
+                    data: prepare,
+                    itemStyle: {
+                        color: '#05AC22'
+                    }
+                },
+                {
+                    name: '学校上课',
+                    type: 'line',
+                    stack: '总量',
+                    data: schoolStudy,
+                    itemStyle: {
+                        color: '#C30DFF'
+                    }
+                },
+                {
+                    name: '复习测试',
+                    type: 'line',
+                    stack: '总量',
+                    data: review,
+                    itemStyle: {
+                        color: '#00F2FF'
+                    }
+                },
+            ]
+        }
+        state.pageOptionData = pageOption
+    },
+    setUserInfoList: (state, data) => {
+        state.userInfoList = data
+    },
+    setMemberList: (state, data) => {
+        const date = []
+        const container = []
+        data.forEach(item => {
+            date.push(item.day)
+            container.push(item.memberOnlineTime.timeAcc / 1000 /60)
+        })
+        const onlineOption = {
+            tooltip: {
+                trigger: 'axis',
+            },
+            xAxis: {
+                type: 'category',
+                data: date
+            },
+            yAxis: {
+                type: 'value',
+                name: '分钟'
+            },
+            series: [{
+                data: container,
+                type: 'line',
+                itemStyle: {
+                    color: '#2BCF82'
+                }
+            }]
+        }
+        state.memberList = onlineOption
+
+    }
+}
+export default mutations

+ 9 - 0
src/store/states.js

@@ -0,0 +1,9 @@
+const states = {
+    baydataList: [],
+    dataAnalysisList: {},
+    hotCourseList: {},
+    pageOptionData: {},
+    userInfoList: {},
+    memberList: {}
+}
+export default states

+ 14 - 0
src/store/store.js

@@ -0,0 +1,14 @@
+import Vue from 'vue'
+export default class Store {
+    constructor({states, actions, mutations}) {
+        this.states = Vue.observable(states || {})
+        this.actions = Vue.observable(actions || {})
+        this.mutations = Vue.observable(mutations || {})
+    }
+    commit = (fun, data) => {
+        this.mutations[fun](this.states, data)
+    }
+    dispatch(fun, params) {
+        return this.actions[fun](this.commit, params)
+    }
+}

+ 28 - 0
src/utils/request.js

@@ -0,0 +1,28 @@
+import axios from 'axios'
+import { message  } from 'ant-design-vue'
+
+// create an axios instance
+const service = axios.create({
+  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
+  timeout: 5000 // request timeout
+})
+// response interceptor
+service.interceptors.response.use(
+  response => {
+    const res = response.data
+    // if the custom code is not 20000, it is judged as an error.
+    if (res.code !== 200) {
+      message.error(res.message || 'Error')
+      return Promise.reject(res)
+    } else {
+      return res
+    }
+  },
+  error => {
+    console.log('err' + JSON.stringify(error)) // for debug
+    message.error(error.message)
+    return Promise.reject(error)
+  }
+)
+
+export default service

+ 129 - 0
vue.config.js

@@ -0,0 +1,129 @@
+/**
+ * 配置文件地址
+ * https://cli.vuejs.org/zh/config/#vue-config-js
+ */
+const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);
+const path = require('path');
+const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
+const CompressionWebpackPlugin = require("compression-webpack-plugin");
+const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
+module.exports = {
+    /**
+     * 部署应用包时基本的URL,与webpack中的output.publicPath一致
+     * 默认情况下是部署到一个域名的根路径上,
+     * 也可以设置成'',或者相对路径'./';这样所有的资源都会被链接相对路径,就可以部署到任意路径
+     */
+    publicPath: './',
+    /**
+     * 生产环境打包生成的文件夹,构建之前会清除构建目录,与webpack中的output.path一致
+     */
+    outputDir: 'dist',
+    /**
+     * 放置生成的静态文件
+     */
+    assetsDir: 'assets',
+    /**
+     * 默认情况下生成的静态文件包含hash值更好的控制缓存,html必须是动态生成,如果不是动态生成可以置为false
+     * 与webpack中的output.filename中的名字加hash一样
+     */
+    filenameHashing: true,
+    /**
+     * 如果不需要生产环境的source map,可以设置为false加快构建
+     */
+    productionSourceMap: false,
+    /**
+     * 所有webpack-dev-server的选项都支持
+     * https://webpack.docschina.org/configuration/dev-server/#devserveroverlay
+     */
+    devServer: {
+        port: '8897',
+        open: true,
+        progress: true,
+        overlay: {
+            warnings: false,
+            errors: true
+        }
+    },
+    // webpack配置
+    chainWebpack: config => {
+        // 是否将符号链接(symlink)解析到它们的符号链接位置
+        config.resolve.symlinks(true)
+        if (IS_PROD) {
+            config.plugin("webpack-report").use(BundleAnalyzerPlugin, [
+                {
+                analyzerMode: "static"
+                }
+            ]);
+            config.optimization.delete("splitChunks");
+        }
+    },
+    configureWebpack: config => {
+        if (process.env.VUE_APP_MODE === 'production') {
+            // 生产环境
+            config.mode = 'production'
+        } else {
+            // 开发环境
+            config.mode = 'development'
+        }
+        Object.assign(config, {
+            resolve: {
+                alias: {
+                    '@': path.resolve(__dirname, './src')
+                }
+            }
+        })
+        const plugins = []
+        if (IS_PROD) {
+            config.optimization ={
+                splitChunks: {
+                    cacheGroups: {
+                        common: {
+                            name: "chunk-common",
+                            chunks: "all",
+                            minChunks: 2,
+                            maxInitialRequests: 5,
+                            minSize: 0,
+                            priority: 1,
+                            reuseExistingChunk: true,
+                            enforce: true
+                        },
+                        vendors: {
+                            name: "chunk-vendors",
+                            test: /[\\/]node_modules[\\/]/,
+                            chunks: "initial",
+                            priority: 2,
+                            reuseExistingChunk: true,
+                            enforce: true
+                        },
+                        antDesignVueUI: {
+                            name: "chunk-antdesignvueUI",
+                            test: /[\\/]node_modules[\\/]ant-design-vue[\\/]/,
+                            chunks: "all",
+                            priority: 3,
+                            reuseExistingChunk: true,
+                            enforce: true
+                        },
+                        echarts: {
+                            name: "chunk-echarts",
+                            test: /[\\/]node_modules[\\/](vue-)?echarts[\\/]/,
+                            chunks: "all",
+                            priority: 4,
+                            reuseExistingChunk: true,
+                            enforce: true
+                        }
+                    }
+                }
+            }
+            plugins.push(
+                new CompressionWebpackPlugin({
+                    filename: "[path].gz[query]",
+                    algorithm: "gzip",
+                    test: productionGzipExtensions,
+                    threshold: 10240,
+                    minRatio: 0.8
+                })
+            )
+        }
+        config.plugins = [...config.plugins, ...plugins];
+    }
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 8459 - 0
yarn.lock