Bladeren bron

写了个有点厉害的菜单

xiongzhu 7 jaren geleden
bovenliggende
commit
00d802d754

+ 11 - 0
package-lock.json

@@ -0,0 +1,11 @@
+{
+  "requires": true,
+  "lockfileVersion": 1,
+  "dependencies": {
+    "tinycolor2": {
+      "version": "1.4.1",
+      "resolved": "http://registry.npm.taobao.org/tinycolor2/download/tinycolor2-1.4.1.tgz",
+      "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g="
+    }
+  }
+}

+ 55 - 0
src/main/vue/src/components/MenuComponent.vue

@@ -0,0 +1,55 @@
+<template>
+    <menu-item v-if="isLeaf" :id="menu.id" :icon="menu.icon">{{menu.name}}</menu-item>
+    <sub-menu v-else :id="menu.id" :icon="menu.icon" :title="menu.name">
+        <template slot-scope="{isOpen}">
+            <menu-component v-for="item in menu.children" :menu="item" :key="item.id" :parentIsOpen="isOpen"></menu-component>
+        </template>
+    </sub-menu>
+</template>
+<script>
+import SubMenu from './SubMenu'
+import MenuItem from './MenuItem'
+export default {
+    name: 'MenuComponent',
+    props: {
+        menu: {
+            type: Object,
+            required: true
+        },
+        parentIsOpen: {
+            type: Boolean
+        }
+    },
+    mounted() {
+        this.$on('onSelected', this.onSubMenuSelected)
+    },
+    destroyed() {
+        this.$off('onSelected')
+    },
+    data() {
+        return {}
+    },
+    methods: {
+        activeChange(activeId) {
+            this.$children.forEach(child => {
+                child.activeChange(activeId)
+            })
+        },
+        click(e) {
+            console.log(e);
+        },
+        onSubMenuSelected(e) {
+            this.$parent.$emit('onSelected', e)
+        }
+    },
+    computed: {
+        isLeaf() {
+            return !(this.menu.children instanceof Array)
+        }
+    },
+    components: {
+        SubMenu,
+        MenuItem
+    }
+}
+</script>

+ 112 - 0
src/main/vue/src/components/MenuItem.vue

@@ -0,0 +1,112 @@
+<template>
+    <li class="menu-item" :style="style" @click.stop="menuClick" @mouseenter="hover = true" @mouseleave="hover = false">
+        {{menu.name}}
+    </li>
+</template>
+
+<script>
+export default {
+    props: {
+        menu: {
+            type: Object,
+            required: true
+        },
+        depth: {
+            type: Number,
+            default: 0
+        },
+        collapse: {
+            type: Boolean,
+            default: false
+        },
+        width: {
+            type: Number
+        },
+        colors: {
+            type: Object
+        },
+        activeId: {}
+    },
+    data() {
+        return {
+            active: false,
+            hover: false
+        }
+    },
+    created() {
+        if (this.activeId === this.menu.id) {
+            this.active = true
+        }
+    },
+    computed: {
+        style() {
+            let style = {
+                color: this.colors.textColor
+            }
+            if (this.collapse) {
+                style.padding = '0 20px'
+                style.width = this.width + 'px'
+            } else {
+                style.padding = this.depth < 2 ? '0 45px' : '0 20px'
+            }
+
+            if (this.active) {
+                style.backgroundColor = this.colors.activeBackgroundColor
+                style.borderLeft = `2px solid ${this.colors.textColor}`
+            } else if (this.hover) {
+                style.backgroundColor = this.colors.hoverBackgroundColor
+            } else {
+                style.backgroundColor = this.colors.backgroundColor
+            }
+            return style
+        }
+    },
+    methods: {
+        activeChange(activeId) {
+            if (activeId instanceof Array && activeId.indexOf(this.menu.id) !== -1) {
+                this.active = true
+            } else if (activeId === this.menu.id) {
+                this.active = true
+            } else {
+                this.active = false
+            }
+        },
+        openedChange(openedId) {
+        },
+        menuClick() {
+            this.active = true
+            if (this.menu.url) {
+                if (/^\/http/.test(this.menu.url)) {
+                    window.open(url);
+                    return;
+                }
+            } else {
+                this.$router.push(this.menu.href)
+            }
+        }
+    },
+    watch: {
+        active(val) {
+            if (val) {
+                this.$emit('selected', [this.menu.id])
+            }
+        }
+    }
+}
+</script>
+<style lang="less" scoped>
+@menuColor: #6a90b8;
+@activeColor: #f2f6fc;
+.menu-item {
+    height: 56px;
+    line-height: 56px;
+    background-color: @menuColor;
+    font-size: 14px;
+    padding: 0 45px;
+    transition: background-color 0.2s, color 0.2s;
+    box-sizing: border-box;
+    * {
+        vertical-align: middle;
+    }
+}
+</style>

+ 61 - 0
src/main/vue/src/components/NavMenu.vue

@@ -0,0 +1,61 @@
+<template>
+    <ul class="nav-menu" :style="style">
+        <sub-menu v-for="item in menus" :key="item.id" :menu="item" :width="width" :collapse="collapse" :colors="colors" :activeId="activeId" @selected="onMenuSelected" @opened="onMenuOpened"></sub-menu>
+    </ul>
+</template>
+<script>
+import SubMenu from './SubMenu'
+import tinyColor from 'tinycolor2'
+
+export default {
+    props: ['menus', 'width', 'collapse', 'theme', 'activeId'],
+    computed: {
+        style() {
+            return {
+                width: this.collapse ? '60px' : (this.width + 'px')
+            }
+        },
+        colors() {
+            let luminance = tinyColor(this.theme).getLuminance()
+
+            let backgroundColor = tinyColor(this.theme).toHexString()
+            let activeBackgroundColor = tinyColor.mix(this.theme, '#000000', 20).toHexString()
+            let hoverBackgroundColor = tinyColor.mix(this.theme, '#000000', 10).toHexString()
+            let textColor = tinyColor.mix(this.theme, luminance > 0.6 ? '#000000' : '#ffffff', 90).toHexString()
+            return { backgroundColor, activeBackgroundColor, hoverBackgroundColor, textColor }
+        }
+    },
+    methods: {
+        onMenuSelected(activeId) {
+            this.$children.forEach(child => {
+                child.activeChange(activeId)
+            })
+        },
+        onMenuOpened(openedId) {
+            this.$children.forEach(child => {
+                child.openedChange(openedId)
+            })
+        }
+    },
+    watch: {
+        activeId(val) {
+            if (val) {
+                this.$children.forEach(child => {
+                    child.activeChange(val)
+                })
+            }
+        }
+    },
+    components: {
+        SubMenu
+    }
+}
+</script>
+<style lang="less" scoped>
+.nav-menu {
+    cursor: pointer;
+    transition: width 0.3s;
+    overflow: hidden;
+    font-size: 14px;
+}
+</style>

+ 261 - 0
src/main/vue/src/components/SubMenu.vue

@@ -0,0 +1,261 @@
+<template>
+    <menu-item v-if="isLeaf" :menu="menu" ref="title" @selected="onSubMenuSelected" :depth="depth" :collapse="collapse" :width="width" :colors="colors" :activeId="activeId"></menu-item>
+    <li v-else @mouseenter="enterBody" @mouseleave="leaveBody">
+        <div class="sub-menu-title" :style="titelStyle" ref="title" @click.stop="isOpen=!isOpen" @mouseenter="hover = true" @mouseleave="hover = false">
+            <i class="icon fa-fw" :class="menu.icon" v-if="menu.icon"></i>
+            <transition name="el-fade-in">
+                <span v-if="!collapse || depth > 0">{{menu.name}}</span>
+            </transition>
+            <transition name="el-fade-in">
+                <i class="el-icon-arrow-left" v-if="!collapse || depth > 0" :class="{open:isOpen}"></i>
+            </transition>
+        </div>
+        <transition :name="transitionName">
+            <div class="sub-menu-wrapper" :style="menuStyle" v-show="isOpen">
+                <ul class="sub-menu" :style="subMenuStyle" ref="child">
+                    <!-- <slot :isOpen="isOpen" @onSelected="onSubMenuSelected"></slot> -->
+                    <sub-menu v-for="item in menu.children" :key="item.id" :menu="item" :depth="depth+1" :width="width" :collapse="collapse" :colors="colors" :activeId="activeId" @selected="onSubMenuSelected" @opened="onSubMenuOpened"></sub-menu>
+                </ul>
+            </div>
+        </transition>
+    </li>
+</template>
+<script>
+import tinyColor from 'tinycolor2'
+import MenuItem from './MenuItem'
+export default {
+    name: 'SubMenu',
+    props: {
+        menu: {
+            type: Object,
+            required: true
+        },
+        depth: {
+            type: Number,
+            default: 0
+        },
+        parentIsOpen: {
+            type: Boolean,
+            default: false
+        },
+        collapse: {
+            type: Boolean,
+            default: false
+        },
+        width: {
+            type: Number
+        },
+        colors: {
+            type: Object
+        },
+        activeId: {}
+    },
+    data() {
+        return {
+            x: 0,
+            y: 0,
+            w: 0,
+            h: 0,
+            c: 0,
+            isOpen: false,
+            active: false,
+            collapsing: false,
+            hover: false
+        }
+    },
+    mounted() {
+        this.reCale()
+    },
+    computed: {
+        isLeaf() {
+            return !(this.menu.children instanceof Array)
+        },
+        titelStyle() {
+            let style = {
+                paddingLeft: this.depth === 1 && !this.collapse ? '45px ' : '',
+                color: this.colors.textColor
+            }
+            if (this.active && (this.collapse || (this.depth > 0))) {
+                style.backgroundColor = this.colors.activeBackgroundColor
+                style.borderLeft = `2px solid ${this.colors.textColor}`
+            } else if (this.hover) {
+                style.backgroundColor = this.colors.hoverBackgroundColor
+            } else {
+                style.backgroundColor = this.colors.backgroundColor
+            }
+            return style
+        },
+        menuStyle() {
+            if ((this.collapse || (this.depth > 0)) && !this.collapsing) {
+                let style = {
+                    position: 'absolute',
+                    left: this.x + this.w + 'px',
+                    height: 56 * this.c + 10 + 'px',
+                    paddingLeft: '5px',
+                    zIndex: 3
+                }
+                if (this.y + 56 * this.c + 10 > window.innerHeight) {
+                    style.top = window.innerHeight - 56 * this.c - 15 + 'px'
+                } else {
+                    style.top = this.y + 'px'
+                }
+                return style
+            }
+            return {
+                height: 56 * this.c + 'px'
+            }
+        },
+        subMenuStyle() {
+            let style = {
+                backgroundColor: this.colors.backgroundColor
+            }
+            if ((this.depth > 0 || this.collapse) && !this.collapsing) {
+                style = {
+                    ...style,
+                    width: this.width + 'px',
+                    boxSizing: 'border-box',
+                    padding: '5px 0',
+                    borderRadius: '2px',
+                    boxShadow: '0 2px 12px 0 rgba(0, 0, 0, 0.1)'
+                }
+            }
+            return style
+        },
+        transitionName() {
+            if (this.collapsing) {
+                return 'menu-collapse'
+            }
+            return this.collapse || (this.depth > 0) ? 'menu-zoom' : 'menu-collapse'
+        }
+    },
+    methods: {
+        activeChange(activeId) {
+            if (activeId instanceof Array && activeId.indexOf(this.menu.id) === -1) {
+                this.active = false
+            }
+            this.$children.forEach(child => {
+                child.activeChange(activeId)
+            })
+        },
+        openedChange(openedId) {
+            if (openedId.indexOf(this.menu.id) === -1) {
+                this.isOpen = false
+            }
+            this.$children.forEach(child => {
+                child.openedChange(openedId)
+            })
+        },
+        reCale() {
+            this.x = this.$refs.title.offsetLeft
+            this.y = this.$refs.title.offsetTop
+            this.w = this.$refs.title.offsetWidth
+            this.h = this.$refs.title.offsetHeight
+            if (!this.isLeaf) {
+                this.c = this.$refs.child.childElementCount
+            }
+        },
+        enterBody(e) {
+            if (this.collapse || (this.depth > 0)) {
+                this.isOpen = true
+            }
+        },
+        leaveBody(e) {
+            if (this.collapse || (this.depth > 0)) {
+                this.isOpen = false
+            }
+        },
+        onSubMenuSelected(e) {
+            this.active = true
+            if (this.depth === 0 && !this.collapse) {
+                this.isOpen = true
+            }
+            let selected = [this.menu.id, ...e]
+            if (this.isLeaf) {
+                this.$emit('selected', e)
+            } else {
+                this.$emit('selected', selected)
+            }
+        },
+        onSubMenuOpened(e) {
+            this.$emit('opened', [this.menu.id, ...e])
+        }
+    },
+    watch: {
+        parentIsOpen(val) {
+            if (!val) this.isOpen = false
+        },
+        isOpen(val) {
+            this.reCale()
+            if (val) {
+                this.$emit('opened', [this.menu.id])
+            }
+        },
+        collapse(val) {
+            if (val) {
+                this.collapsing = true
+                setTimeout(() => {
+                    this.collapsing = false
+                }, 300);
+                this.isOpen = false
+            } else {
+                this.isOpen = this.active && this.depth === 0 ? true : false
+            }
+        }
+    },
+    components: {
+        MenuItem
+    }
+}
+</script>
+<style lang="less" scoped>
+.sub-menu-title {
+    height: 56px;
+    line-height: 56px;
+    font-size: 14px;
+    padding: 0 20px;
+    box-sizing: border-box;
+    transition: background-color 0.2s, color 0.2s;
+    position: relative;
+    .icon {
+        margin-right: 6px;
+    }
+    .el-icon-arrow-left {
+        position: absolute;
+        right: 20px;
+        top: 50%;
+        margin-top: -7px;
+        font-size: 12px;
+        transition: all 0.3s;
+        &.open {
+            transform: rotate(-90deg);
+        }
+    }
+    * {
+        vertical-align: middle;
+    }
+}
+
+.menu-collapse-enter,
+.menu-collapse-leave-active {
+    height: 0 !important;
+}
+.menu-collapse-enter-active,
+.menu-collapse-leave-active {
+    transition: height 0.3s ease;
+    transform: translate3d(0, 0, 0);
+    overflow: hidden;
+}
+
+.menu-zoom-enter,
+.menu-zoom-leave-active {
+    transform: scale3d(0.2, 0.2, 1);
+    opacity: 0;
+}
+.menu-zoom-enter-active,
+.menu-zoom-leave-active {
+    transform-origin: 0 0;
+    transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
+        opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
+    overflow: hidden;
+}
+</style>

+ 22 - 21
src/main/vue/src/components/SysMenu.vue

@@ -1,6 +1,7 @@
 <template>
     <el-menu-item v-if="isLeaf" :index="''+menu.id" :route="{path:menu.href}">
-        <i class="fa-fw" :class="menu.icon" v-if="menu.icon"></i><span slot="title">{{menu.name}}</span>
+        <i class="fa-fw" :class="menu.icon" v-if="menu.icon"></i>
+        <span slot="title">{{menu.name}}</span>
     </el-menu-item>
     <el-submenu v-else :index="''+menu.id">
         <template slot="title">
@@ -11,26 +12,26 @@
     </el-submenu>
 </template>
 <script>
-    export default {
-        name: 'SysMenu',
-        props: {
-            menu: {
-                type: Object,
-                required: true
-            }
-        },
-        data() {
-            return {}
-        },
-        methods: {
-            click(e) {
-                console.log(e);
-            }
-        },
-        computed: {
-            isLeaf() {
-                return !(this.menu.children instanceof Array)
-            }
+export default {
+    name: 'SysMenu',
+    props: {
+        menu: {
+            type: Object,
+            required: true
+        }
+    },
+    data() {
+        return {}
+    },
+    methods: {
+        click(e) {
+            console.log(e);
+        }
+    },
+    computed: {
+        isLeaf() {
+            return !(this.menu.children instanceof Array)
         }
     }
+}
 </script>

+ 123 - 0
src/main/vue/src/components/ThemePicker.vue

@@ -0,0 +1,123 @@
+<template>
+    <el-color-picker class="theme-picker" popper-class="theme-picker-dropdown" v-model="theme"></el-color-picker>
+</template>
+
+<script>
+const version = require('element-ui/package.json').version // element-ui version from node_modules
+const ORIGINAL_THEME = '#409EFF' // default color
+export default {
+    data() {
+        return {
+            chalk: '', // content of theme-chalk css
+            theme: ORIGINAL_THEME
+        }
+    },
+    watch: {
+        theme(val, oldVal) {
+            if (typeof val !== 'string') return
+            const themeCluster = this.getThemeCluster(val.replace('#', ''))
+            const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
+            console.log(themeCluster, originalCluster)
+            const getHandler = (variable, id) => {
+                return () => {
+                    const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
+                    const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
+                    let styleTag = document.getElementById(id)
+                    if (!styleTag) {
+                        styleTag = document.createElement('style')
+                        styleTag.setAttribute('id', id)
+                        document.head.appendChild(styleTag)
+                    }
+                    styleTag.innerText = newStyle
+                }
+            }
+            const chalkHandler = getHandler('chalk', 'chalk-style')
+            if (!this.chalk) {
+                const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
+                this.getCSSString(url, chalkHandler, 'chalk')
+            } else {
+                chalkHandler()
+            }
+            const styles = [].slice.call(document.querySelectorAll('style'))
+                .filter(style => {
+                    const text = style.innerText
+                    return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
+                })
+            styles.forEach(style => {
+                const { innerText } = style
+                if (typeof innerText !== 'string') return
+                style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
+            })
+            this.$message({
+                message: '换肤成功',
+                type: 'success'
+            })
+        }
+    },
+    methods: {
+        updateStyle(style, oldCluster, newCluster) {
+            let newStyle = style
+            oldCluster.forEach((color, index) => {
+                newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
+            })
+            return newStyle
+        },
+        getCSSString(url, callback, variable) {
+            const xhr = new XMLHttpRequest()
+            xhr.onreadystatechange = () => {
+                if (xhr.readyState === 4 && xhr.status === 200) {
+                    this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
+                    callback()
+                }
+            }
+            xhr.open('GET', url)
+            xhr.send()
+        },
+        getThemeCluster(theme) {
+            const tintColor = (color, tint) => {
+                let red = parseInt(color.slice(0, 2), 16)
+                let green = parseInt(color.slice(2, 4), 16)
+                let blue = parseInt(color.slice(4, 6), 16)
+                if (tint === 0) { // when primary color is in its rgb space
+                    return [red, green, blue].join(',')
+                } else {
+                    red += Math.round(tint * (255 - red))
+                    green += Math.round(tint * (255 - green))
+                    blue += Math.round(tint * (255 - blue))
+                    red = red.toString(16)
+                    green = green.toString(16)
+                    blue = blue.toString(16)
+                    return `#${red}${green}${blue}`
+                }
+            }
+            const shadeColor = (color, shade) => {
+                let red = parseInt(color.slice(0, 2), 16)
+                let green = parseInt(color.slice(2, 4), 16)
+                let blue = parseInt(color.slice(4, 6), 16)
+                red = Math.round((1 - shade) * red)
+                green = Math.round((1 - shade) * green)
+                blue = Math.round((1 - shade) * blue)
+                red = red.toString(16)
+                green = green.toString(16)
+                blue = blue.toString(16)
+                return `#${red}${green}${blue}`
+            }
+            const clusters = [theme]
+            for (let i = 0; i <= 9; i++) {
+                clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
+            }
+            clusters.push(shadeColor(theme, 0.1))
+            return clusters
+        }
+    }
+}
+</script>
+
+<style>
+.theme-picker .el-color-picker__trigger {
+    vertical-align: middle;
+}
+.theme-picker-dropdown .el-color-dropdown__link-btn {
+    display: none;
+}
+</style>

+ 1 - 1
src/main/vue/src/main.js

@@ -154,7 +154,7 @@ Vue.mixin({
             return format(new Date(cellValue), 'HH:mm', { locale: zh })
         },
         datetimeFormatter(row, column, cellValue, index) {
-            if (!cellValue) return ''
+            if (!cellValue) return '';
             return format(new Date(cellValue), 'YYYY/MM/DD HH:mm', { locale: zh })
         }
     }

+ 155 - 159
src/main/vue/src/pages/App.vue

@@ -1,19 +1,12 @@
 <template>
     <el-container id="app">
-        <el-aside :width="collapse ? '65px' : '200px'" class="aside">
+        <el-aside class="aside">
             <div class="logo-wrapper">Logo</div>
-            <el-menu
-                :collapse="collapse"
-                background-color="#324157"
-                text-color="#BFCBD9"
-                active-text-color="#20A0FF"
-                :unique-opened="true"
-                :router="true"
-                :default-active="activeMenu"
-                style="border-right: 1px solid #545c64"
-                class="el-menu-vertical-demo">
+            <!-- <el-menu :collapse="collapse" background-color="#324157" text-color="#BFCBD9" active-text-color="#20A0FF" :unique-opened="true" :router="true" :default-active="activeMenu" style="border-right: 1px solid #545c64" class="el-menu-vertical-demo">
                 <sys-menu v-for="item in menus" :menu="item" :key="item.id"></sys-menu>
-            </el-menu>
+            </el-menu> -->
+            <nav-menu :menus="menus" :activeId="activeMenu" :collapse="collapse" :width="220" theme="#324157">
+            </nav-menu>
         </el-aside>
         <el-container>
             <el-header class="header">
@@ -33,7 +26,7 @@
                 </el-tooltip>
 
                 <el-dropdown @command="onCommand" style="margin-left: 20px;">
-                    <img :src="userInfo ? userInfo.icon || '' : ''" class="avatar"/>
+                    <img :src="userInfo ? userInfo.icon || '' : ''" class="avatar" />
                     <el-dropdown-menu slot="dropdown">
                         <el-dropdown-item command="logout">退出登录</el-dropdown-item>
                     </el-dropdown-menu>
@@ -48,181 +41,184 @@
 </template>
 
 <script>
-    import SysMenu from '../components/SysMenu'
-    import {mapState} from 'vuex'
+import SysMenu from '../components/SysMenu'
+import NavMenu from '../components/NavMenu'
+import { mapState } from 'vuex'
 
-    export default {
-        name: 'App',
-        created() {
-            this.getMenus();
-            let fn = () => {
-                this.isFullscreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
+export default {
+    name: 'App',
+    mounted() {
+        this.getMenus();
+        let fn = () => {
+            this.isFullscreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
 
-            };
-            document.addEventListener('fullscreenchange', fn);
-            document.addEventListener('webkitfullscreenchange', fn);
-            document.addEventListener('mozfullscreenchange', fn);
-            document.addEventListener('MSFullscreenChange', fn);
-        },
-        data() {
-            return {
-                rawMenus: [],
-                menus: [],
-                activeMenu: '',
-                menuPath: [],
-                collapse: false,
-                isFullscreen: false
-            }
-        },
-        computed: {
-            ...mapState(['userInfo'])
-        },
-        methods: {
-            findActiveMenu() {
-                this.activeMenu = '';
-                this.menuPath = [];
-                let path = this.$route.path;
-                const findActiveMenu = (parents, childMenus) => {
-                    childMenus.forEach(i => {
-                        let parents_copy = [...parents];
-                        if (i.href === path) {
+        };
+        document.addEventListener('fullscreenchange', fn);
+        document.addEventListener('webkitfullscreenchange', fn);
+        document.addEventListener('mozfullscreenchange', fn);
+        document.addEventListener('MSFullscreenChange', fn);
+    },
+    data() {
+        return {
+            rawMenus: [],
+            menus: [],
+            activeMenu: '',
+            menuPath: [],
+            collapse: false,
+            isFullscreen: false
+        }
+    },
+    computed: {
+        ...mapState(['userInfo'])
+    },
+    methods: {
+        findActiveMenu() {
+            this.activeMenu = '';
+            this.menuPath = [];
+            let path = this.$route.path;
+            const findActiveMenu = (parents, childMenus) => {
+                childMenus.forEach(i => {
+                    let parents_copy = [...parents];
+                    if (i.href === path) {
+                        parents_copy.push(i);
+                        this.menuPath = parents_copy.map(i => i.name);
+                        this.activeMenu = i.id;
+                    } else {
+                        if (i.children) {
                             parents_copy.push(i);
-                            this.menuPath = parents_copy.map(i => i.name);
-                            this.activeMenu = '' + i.id;
-                        } else {
-                            if (i.children) {
-                                parents_copy.push(i);
-                                findActiveMenu(parents_copy, i.children);
-                            }
+                            findActiveMenu(parents_copy, i.children);
                         }
-                    })
-                };
-                findActiveMenu([], this.rawMenus);
-            },
-            getMenus() {
-                this.$http.get({
-                    url: '/sysMenu/userMenuTree',
-                    data: {
-                        userId: this.userInfo.id
-                    }
-                }).then(res => {
-                    if (res.success) {
-                        this.rawMenus = res.data;
-                        this.menus = res.data;
-                        this.findActiveMenu();
                     }
                 })
-            },
-            toggleFullScreen() {
-                this.isFullscreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
-                let element = document.body;
-                let requestMethod;
-                if (this.isFullscreen) {
-                    requestMethod = document.exitFullscreen || document.mozCancelFullScreen || document.webkitExitFullscreen;
-                } else {
-                    requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;
-                }
-                if (requestMethod) {
-                    requestMethod.call(this.isFullscreen ? document : element);
+            };
+            findActiveMenu([], this.rawMenus);
+        },
+        getMenus() {
+            this.$http.get({
+                url: '/sysMenu/userMenuTree',
+                data: {
+                    userId: this.userInfo.id
                 }
-            },
-            onCommand(command) {
-                if (command === 'logout') {
-                    this.$http.post({
-                        url: '/auth/logout'
-                    }).then(res => {
-                        if (res.success) {
-                            this.$store.commit('updateUserInfo', null);
-                            this.$router.replace('/login');
-                        }
-                    })
+            }).then(res => {
+                if (res.success) {
+                    this.rawMenus = res.data;
+                    this.menus = res.data;
+                    this.findActiveMenu();
                 }
+            })
+        },
+        toggleFullScreen() {
+            this.isFullscreen = document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen;
+            let element = document.body;
+            let requestMethod;
+            if (this.isFullscreen) {
+                requestMethod = document.exitFullscreen || document.mozCancelFullScreen || document.webkitExitFullscreen;
+            } else {
+                requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen;
+            }
+            if (requestMethod) {
+                requestMethod.call(this.isFullscreen ? document : element);
             }
         },
-        watch: {
-            $route(val) {
-                this.findActiveMenu(this.rawMenus);
-            },
-            isFullscreen(val) {
-                this.$refs.fullscreen.innerHTML = '';
-                let i = document.createElement('i');
-                i.style.fontSize = '20px';
-                i.className = val ? 'fas fa-compress' : 'fas fa-expand';
-                this.$refs.fullscreen.append(i);
-                FontAwesome.dom.i2svg();
+        onCommand(command) {
+            if (command === 'logout') {
+                this.$http.post({
+                    url: '/auth/logout'
+                }).then(res => {
+                    if (res.success) {
+                        this.$store.commit('updateUserInfo', null);
+                        this.$router.replace('/login');
+                    }
+                })
             }
+        }
+    },
+    watch: {
+        $route(val) {
+            this.findActiveMenu(this.rawMenus);
         },
-        components: {
-            SysMenu
+        isFullscreen(val) {
+            this.$refs.fullscreen.innerHTML = '';
+            let i = document.createElement('i');
+            i.style.fontSize = '20px';
+            i.className = val ? 'fas fa-compress' : 'fas fa-expand';
+            this.$refs.fullscreen.append(i);
+            FontAwesome.dom.i2svg();
         }
+    },
+    components: {
+        SysMenu,
+        NavMenu
     }
+}
 </script>
 <style lang="less">
-    .aside {
-        .el-menu {
-            border: none !important;
-            &:not(.el-menu--collapse) {
-                width: 200px;
-            }
+.aside {
+    width: auto !important;
+    .el-menu {
+        border: none !important;
+        &:not(.el-menu--collapse) {
+            width: 200px;
         }
     }
+}
 </style>
 <style lang="less" scoped>
-    #app {
-        height: 100%;
-    }
+#app {
+    height: 100%;
+}
 
-    .header {
-        color: #303133;
-        background: #fff;
+.header {
+    color: #303133;
+    background: #fff;
+    display: flex;
+    align-items: center;
+    padding-left: 0;
+    border-bottom: 1px solid #dcdfe6;
+    .header-btn {
+        width: 60px;
+        height: 60px;
         display: flex;
         align-items: center;
-        padding-left: 0;
-        border-bottom: 1px solid #DCDFE6;
-        .header-btn {
-            width: 60px;
-            height: 60px;
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            cursor: pointer;
+        justify-content: center;
+        cursor: pointer;
+        transition: all 0.3s;
+        & > div {
             transition: all 0.3s;
-            & > div {
-                transition: all 0.3s;
-            }
-            &:hover {
-                background: fade(black, 10%);
-            }
         }
-        .avatar {
-            width: 40px;
-            height: 40px;
-            border: 1px solid #ebebeb;
-            border-radius: 50%;
-        }
-        a {
-            &:visited {
-                color: #303133;
-            }
+        &:hover {
+            background: fade(black, 10%);
         }
     }
-
-    .aside {
-        background: #324157;
-        transition: all 0.4s ease;
-        .logo-wrapper {
-            display: flex;
-            align-items: center;
-            justify-content: center;
-            height: 60px;
-            color: white;
-            font-size: 20px;
-            font-weight: 700;
-            background: fade(black, 20%);
+    .avatar {
+        width: 40px;
+        height: 40px;
+        border: 1px solid #ebebeb;
+        border-radius: 50%;
+    }
+    a {
+        &:visited {
+            color: #303133;
         }
     }
+}
 
-    .aside::-webkit-scrollbar {
-        display: none
+.aside {
+    background: #324157;
+    transition: all 0.4s ease;
+    .logo-wrapper {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 60px;
+        color: white;
+        font-size: 20px;
+        font-weight: 700;
+        background: fade(black, 20%);
     }
+}
+
+.aside::-webkit-scrollbar {
+    display: none;
+}
 </style>

+ 1 - 2
src/main/vue/src/pages/Login.vue

@@ -41,8 +41,7 @@
                             data: {
                                 username: this.userInfo.username,
                                 password: this.userInfo.password,
-                                remember: this.rememberMe,
-                                requireToken: true
+                                remember: this.rememberMe
                             }
                         }).then(res => {
                             this.loading = false;

+ 85 - 0
src/main/vue/src/pages/TestMenu.vue

@@ -0,0 +1,85 @@
+<template>
+    <div>
+        <theme-picker></theme-picker>
+        <el-button type="primary" @click="activeId=13">test</el-button>
+        <el-button type="primary" plain @click="activeId=37">test</el-button>
+        <el-button @click="collapse=!collapse">test</el-button>
+        <!-- <nav-menu style="width:200px">
+            <sub-menu icon="fas fa-bars" title="菜单1" id="1" :depth="0">
+                <template slot-scope="{isOpen}">
+                    <menu-item id="1-1">菜单1-1</menu-item>
+                    <menu-item id="1-2">菜单1-2</menu-item>
+                    <menu-item id="1-3">菜单1-3</menu-item>
+                    <sub-menu icon="fas fa-bars" title="菜单1-4" id="1-4" :depth="1" :parentIsOpen="isOpen">
+                        <template slot-scope="{isOpen}">
+                            <menu-item id="1-4-1">菜单1-4-1</menu-item>
+                            <menu-item id="1-4-2">菜单1-4-2</menu-item>
+                            <menu-item id="1-4-3">菜单1-4-3</menu-item>
+                            <sub-menu icon="fas fa-bars" title="菜单1-4-4" id="1-4-4" :depth="1" :parentIsOpen="isOpen">
+                                <menu-item id="1-4-4-1">菜单1-4-4-1</menu-item>
+                                <menu-item id="1-4-4-2">菜单1-4-4-2</menu-item>
+                                <menu-item id="1-4-4-3">菜单1-4-4-3</menu-item>
+                            </sub-menu>
+                        </template>
+                    </sub-menu>
+                </template>
+            </sub-menu>
+            <sub-menu icon="fas fa-bars" title="菜单2" id="2" :depth="0">
+                <menu-item id="2-1">菜单2-1</menu-item>
+                <menu-item id="2-2">菜单2-2</menu-item>
+                <menu-item id="2-3">菜单2-3</menu-item>
+            </sub-menu>
+        </nav-menu> -->
+
+        <nav-menu :activeId="activeId" :menus="menus" :collapse="collapse" :width="220" theme="#6a90b8">
+        </nav-menu>
+    </div>
+</template>
+<script>
+import ThemePicker from '../components/ThemePicker'
+import NavMenu from '../components/NavMenu'
+import SubMenu from '../components/SubMenu'
+import MenuItem from '../components/MenuItem'
+import MenuComponent from '../components/MenuComponent'
+import tinyColor from 'tinycolor2'
+export default {
+    created() {
+        this.$http.get({
+            url: '/sysMenu/userMenuTree',
+            data: {
+                userId: 84664
+            }
+        }).then(res => {
+            if (res.success) {
+                this.menus = res.data;
+            }
+        })
+    },
+    data() {
+        return {
+            menus: [],
+            collapse: true,
+            activeId: ''
+        }
+    },
+    computed: {
+        style() {
+            return {
+                width: '150px',
+                height: '56px',
+                backgroundColor: this.colors[0]
+            }
+        }
+    },
+    components: {
+        ThemePicker,
+        NavMenu,
+        SubMenu,
+        MenuItem,
+        MenuComponent
+    }
+}
+</script>
+
+<style lang="less" scoped>
+</style>

+ 6 - 1
src/main/vue/src/router/index.js

@@ -269,6 +269,11 @@ const router = new Router({
                     path: '/testCaidans',
                     name: 'TestCaidans',
                     component: () => import('../pages/TestCaidans')
+                },
+                {
+                    path: '/testMenu',
+                    name: 'TestMenu',
+                    component: () => import('../pages/TestMenu')
                 }
                 /**INSERT_LOCATION**/
             ]
@@ -322,4 +327,4 @@ router.afterEach((to, from) => {
     window.onresize();
 });
 
-export default router;
+export default router;

+ 107 - 105
src/main/vue/src/widgets/WeatherWidget.vue

@@ -1,5 +1,5 @@
 <template>
-    <widget-card :bodyStyle="bodyStyle">
+    <widget-card :bodyStyle="bodyStyle" class="weather-widget">
         <div slot="header" class="date">
             {{date}}
         </div>
@@ -15,117 +15,118 @@
 </template>
 
 <script>
-    import WidgetCard from './WidgetCard'
-    import {format} from 'date-fns'
-    import zh from 'date-fns/locale/zh_cn'
-    import axios from 'axios'
+import WidgetCard from './WidgetCard'
+import { format } from 'date-fns'
+import zh from 'date-fns/locale/zh_cn'
+import axios from 'axios'
 
-    export default {
-        created() {
-            axios.get('https://free-api.heweather.com/s6/weather/now', {
-                params: {
-                    location: 'auto_ip',
-                    key: 'cd39b8d532784c5b881ed41eaea72e6d',
-                },
-                withCredentials: false
-            }).then(res => {
-                this.weather = res.data.HeWeather6[0]
-            })
-        },
-        data() {
-            return {
-                date: format(new Date(), 'YYYY/MM/DD dddd', {locale: zh}),
-                weather: null,
-                iconMap: {
-                    '100': 'wi-day-sunny',
-                    '100n': 'wi-night-clear',
-                    '101': 'wi-cloudy',
-                    '102': 'wi-cloud',
-                    '103': 'wi-day-cloudy',
-                    '103n': 'wi-night-alt-cloudy',
-                    '104': 'wi-cloud',
-                    '104n': 'wi-cloud',
-                    '200': 'wi-windy',
-                    '201': 'wi-day-sunny',
-                    '202': 'wi-windy',
-                    '203': 'wi-windy',
-                    '204': 'wi-windy',
-                    '205': 'wi-strong-wind',
-                    '206': 'wi-strong-wind',
-                    '207': 'wi-strong-wind',
-                    '208': 'wi-strong-wind',
-                    '209': 'wi-storm-warning',
-                    '210': 'wi-storm-warning',
-                    '211': 'wi-hurricane',
-                    '212': 'wi-tornado',
-                    '213': 'wi-hurricane',
-                    '300': 'wi-day-showers',
-                    '300n': 'wi-night-alt-showers',
-                    '301': 'wi-day-showers',
-                    '301n': 'wi-night-alt-showers',
-                    '302': 'wi-day-storm-showers',
-                    '302n': 'wi-night-alt-storm-showers',
-                    '303': 'wi-day-storm-showers',
-                    '303n': 'wi-night-alt-storm-showers',
-                    '304': 'wi-day-snow-thunderstorm',
-                    '304n': 'wi-night-alt-snow-thunderstorm',
-                    '305': 'wi-rain',
-                    '306': 'wi-rain',
-                    '307': 'wi-rain',
-                    '308': 'wi-rain',
-                    '309': 'wi-rain',
-                    '310': 'wi-rain',
-                    '311': 'wi-rain',
-                    '312': 'wi-rain',
-                    '313': 'wi-sleet',
-                    '314': 'wi-rain',
-                    '315': 'wi-rain',
-                    '316': 'wi-rain',
-                    '317': 'wi-rain',
-                    '318': 'wi-rain',
-                    '399': 'wi-rain',
-                    '400': 'wi-snow',
-                    '401': 'wi-snow',
-                    '402': 'wi-snow',
-                    '403': 'wi-snow',
-                    '404': 'wi-sleet',
-                    '405': 'wi-rain-mix',
-                    '406': 'wi-rain-mix',
-                    '407': 'wi-day-snow',
-                    '407n': 'wi-night-alt-snow',
-                    '408': 'wi-snow',
-                    '409': 'wi-snow',
-                    '410': 'wi-snow',
-                    '499': 'wi-snow',
-                    '500': 'wi-fog',
-                    '501': 'wi-fog',
-                    '502': 'wi-day-haze',
-                    '503': 'wi-dust',
-                    '504': 'wi-dust',
-                    '507': 'wi-sandstorm',
-                    '509': 'wi-fog',
-                    '510': 'wi-fog',
-                    '511': 'wi-fog',
-                    '512': 'wi-fog',
-                    '513': 'wi-fog',
-                    '514': 'wi-fog',
-                    '515': 'wi-fog',
-                },
-                bodyStyle: {
-                    display: 'flex',
-                    alignItems: 'center'
-                }
+export default {
+    created() {
+        axios.get('https://free-api.heweather.com/s6/weather/now', {
+            params: {
+                location: 'auto_ip',
+                key: 'cd39b8d532784c5b881ed41eaea72e6d',
+            },
+            withCredentials: false
+        }).then(res => {
+            this.weather = res.data.HeWeather6[0]
+        })
+    },
+    data() {
+        return {
+            date: format(new Date(), 'YYYY/MM/DD dddd', { locale: zh }),
+            weather: null,
+            iconMap: {
+                '100': 'wi-day-sunny',
+                '100n': 'wi-night-clear',
+                '101': 'wi-cloudy',
+                '102': 'wi-cloud',
+                '103': 'wi-day-cloudy',
+                '103n': 'wi-night-alt-cloudy',
+                '104': 'wi-cloud',
+                '104n': 'wi-cloud',
+                '200': 'wi-windy',
+                '201': 'wi-day-sunny',
+                '202': 'wi-windy',
+                '203': 'wi-windy',
+                '204': 'wi-windy',
+                '205': 'wi-strong-wind',
+                '206': 'wi-strong-wind',
+                '207': 'wi-strong-wind',
+                '208': 'wi-strong-wind',
+                '209': 'wi-storm-warning',
+                '210': 'wi-storm-warning',
+                '211': 'wi-hurricane',
+                '212': 'wi-tornado',
+                '213': 'wi-hurricane',
+                '300': 'wi-day-showers',
+                '300n': 'wi-night-alt-showers',
+                '301': 'wi-day-showers',
+                '301n': 'wi-night-alt-showers',
+                '302': 'wi-day-storm-showers',
+                '302n': 'wi-night-alt-storm-showers',
+                '303': 'wi-day-storm-showers',
+                '303n': 'wi-night-alt-storm-showers',
+                '304': 'wi-day-snow-thunderstorm',
+                '304n': 'wi-night-alt-snow-thunderstorm',
+                '305': 'wi-rain',
+                '306': 'wi-rain',
+                '307': 'wi-rain',
+                '308': 'wi-rain',
+                '309': 'wi-rain',
+                '310': 'wi-rain',
+                '311': 'wi-rain',
+                '312': 'wi-rain',
+                '313': 'wi-sleet',
+                '314': 'wi-rain',
+                '315': 'wi-rain',
+                '316': 'wi-rain',
+                '317': 'wi-rain',
+                '318': 'wi-rain',
+                '399': 'wi-rain',
+                '400': 'wi-snow',
+                '401': 'wi-snow',
+                '402': 'wi-snow',
+                '403': 'wi-snow',
+                '404': 'wi-sleet',
+                '405': 'wi-rain-mix',
+                '406': 'wi-rain-mix',
+                '407': 'wi-day-snow',
+                '407n': 'wi-night-alt-snow',
+                '408': 'wi-snow',
+                '409': 'wi-snow',
+                '410': 'wi-snow',
+                '499': 'wi-snow',
+                '500': 'wi-fog',
+                '501': 'wi-fog',
+                '502': 'wi-day-haze',
+                '503': 'wi-dust',
+                '504': 'wi-dust',
+                '507': 'wi-sandstorm',
+                '509': 'wi-fog',
+                '510': 'wi-fog',
+                '511': 'wi-fog',
+                '512': 'wi-fog',
+                '513': 'wi-fog',
+                '514': 'wi-fog',
+                '515': 'wi-fog',
+            },
+            bodyStyle: {
+                display: 'flex',
+                alignItems: 'center'
             }
-        },
-        components: {
-            WidgetCard
         }
+    },
+    components: {
+        WidgetCard
     }
+}
 </script>
 <style lang="less">
-    @import "../../static/weather_icons/less/weather-icons.less";
+@import '../../static/weather_icons/less/weather-icons.less';
 </style>
-<style lang="less">
+<style lang="less" >
+.weather-widget {
     .date {
         color: #666;
         font-size: 14px;
@@ -150,4 +151,5 @@
             margin-top: 8px;
         }
     }
+}
 </style>