소스 검색

Merge branch 'template' of git.sagacloud.cn:web/wanda-adm into template

zhangyu 3 년 전
부모
커밋
8c85bbebe6

+ 1 - 1
.env.development

@@ -1,2 +1,2 @@
-NODE_ENV = development
+NODE_ENV = "development"
 VUE_APP_BASE_API = 'https://vue-typescript-admin-mock-server.armour.now.sh/mock-api/v1/'

+ 1 - 1
.env.production

@@ -1,2 +1,2 @@
-NODE_ENV = production
+NODE_ENV = "production"
 VUE_APP_BASE_API = 'https://vue-typescript-admin-mock-server.armour.now.sh/mock-api/v1/'

+ 1 - 1
.env.staging

@@ -1,4 +1,4 @@
-
+NODE_ENV = "production"
 # Base api
 VUE_APP_BASE_API = '/stage-api'
 

+ 2 - 1
README.md

@@ -6,12 +6,13 @@
 
 ├── public                     # 静态资源 (会被直接复制)
 │   │── favicon.ico            # favicon图标
-│   │── manifest.json          # PWA 配置文件
 │   └── index.html             # html模板
 ├── src                        # 源代码
 │   ├── api                    # 所有请求
 │   ├── assets                 # 主题 字体等静态资源 (由 webpack 处理加载)
 │   ├── components             # 全局组件
+│         │── public           # 公共组件
+│         └── business         # 业务组件
 │   ├── directive              # 全局指令
 │   ├── filters                # 全局过滤函数
 │   ├── icons                  # svg 图标

+ 70 - 71
package.json

@@ -1,73 +1,72 @@
 {
-  "name": "adm",
-  "version": "1.0.0",
-  "private": true,
-  "author": "hao jie <haojie@persagy.com>",
-  "scripts": {
-    "serve": "vue-cli-service serve",
-    "build:dev": "vue-cli-service build --mode development",
-    "build:prod": "vue-cli-service build --mode production",
-    "build:stage": "vue-cli-service build --mode staging",
-    "lint": "vue-cli-service lint",
-    "svg": "vsvg -s ./src/icons/svg -t ./src/icons/components --ext ts --es6",
-    "test:unit": "jest --clearCache && vue-cli-service test:unit"
-  },
-  "dependencies": {
-    "@babel/runtime": "^7.12.5",
-    "axios": "^0.21.1",
-    "core-js": "^3.6.5",
-    "element-ui": "^2.14.1",
-    "js-cookie": "^2.2.1",
-    "normalize.css": "^8.0.1",
-    "nprogress": "^0.2.0",
-    "path-to-regexp": "^6.2.0",
-    "register-service-worker": "^1.7.1",
-    "vue": "^2.6.12",
-    "vue-class-component": "^7.2.6",
-    "vue-property-decorator": "^9.0.2",
-    "vue-router": "^3.4.8",
-    "vue-svgicon": "^3.2.9",
-    "vuex": "^3.5.1",
-    "vuex-module-decorators": "^1.0.1"
-  },
-  "devDependencies": {
-    "@babel/core": "^7.12.10",
-    "@babel/plugin-transform-runtime": "^7.12.10",
-    "@babel/preset-env": "^7.12.11",
-    "@types/jest": "^26.0.15",
-    "@types/js-cookie": "^2.2.6",
-    "@types/node": "^14.14.6",
-    "@types/nprogress": "^0.2.0",
-    "@types/webpack-env": "^1.15.3",
-    "@typescript-eslint/eslint-plugin": "^4.6.0",
-    "@typescript-eslint/parser": "^4.6.0",
-    "@vue/cli-plugin-babel": "^4.5.8",
-    "@vue/cli-plugin-eslint": "^4.5.8",
-    "@vue/cli-plugin-router": "^4.5.8",
-    "@vue/cli-plugin-typescript": "^4.5.8",
-    "@vue/cli-plugin-unit-jest": "^4.5.8",
-    "@vue/cli-plugin-vuex": "^4.5.8",
-    "@vue/cli-service": "^4.5.8",
-    "@vue/eslint-config-standard": "^5.1.2",
-    "@vue/eslint-config-typescript": "^7.0.0",
-    "@vue/test-utils": "^1.1.1",
-    "babel-core": "^7.0.0-bridge.0",
-    "babel-eslint": "^10.1.0",
-    "babel-loader": "^8.1.0",
-    "eslint": "^7.12.1",
-    "eslint-plugin-import": "^2.22.1",
-    "eslint-plugin-node": "^11.1.0",
-    "eslint-plugin-promise": "^4.2.1",
-    "eslint-plugin-standard": "^4.0.2",
-    "eslint-plugin-vue": "^7.1.0",
-    "fibers": "^5.0.0",
-    "sass": "^1.28.0",
-    "sass-loader": "^10.0.4",
-    "style-resources-loader": "^1.3.3",
-    "typescript": "^4.0.5",
-    "vue-cli-plugin-element": "^1.0.1",
-    "vue-cli-plugin-style-resources-loader": "^0.1.4",
-    "vue-template-compiler": "^2.6.12",
-    "webpack": "^4.46.0"
-  }
+    "name": "adm",
+    "version": "1.0.0",
+    "private": true,
+    "author": "hao jie <haojie@persagy.com>",
+    "scripts": {
+        "serve": "vue-cli-service serve",
+        "build:dev": "vue-cli-service build --mode development",
+        "build:prod": "vue-cli-service build --mode production",
+        "build:stage": "vue-cli-service build --mode staging",
+        "lint": "vue-cli-service lint",
+        "svg": "vsvg -s ./src/icons/svg -t ./src/icons/components --ext ts --es6",
+        "test:unit": "jest --clearCache && vue-cli-service test:unit"
+    },
+    "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "axios": "^0.21.1",
+        "core-js": "^3.6.5",
+        "element-ui": "^2.14.0",
+        "js-cookie": "^2.2.1",
+        "normalize.css": "^8.0.1",
+        "nprogress": "^0.2.0",
+        "path-to-regexp": "^6.2.0",
+        "register-service-worker": "^1.7.1",
+        "vue": "^2.6.12",
+        "vue-class-component": "^7.2.6",
+        "vue-property-decorator": "^9.0.2",
+        "vue-router": "^3.4.8",
+        "vue-svgicon": "^3.2.9",
+        "vuex": "^3.5.1",
+        "vuex-module-decorators": "^1.0.1"
+    },
+    "devDependencies": {
+        "@babel/core": "^7.12.10",
+        "@babel/plugin-transform-runtime": "^7.12.10",
+        "@babel/preset-env": "^7.12.11",
+        "@types/js-cookie": "^2.2.6",
+        "@types/node": "^14.14.6",
+        "@types/nprogress": "^0.2.0",
+        "@types/webpack-env": "^1.15.3",
+        "@typescript-eslint/eslint-plugin": "^4.6.0",
+        "@typescript-eslint/parser": "^4.6.0",
+        "@vue/cli-plugin-babel": "^4.5.8",
+        "@vue/cli-plugin-eslint": "^4.5.8",
+        "@vue/cli-plugin-router": "^4.5.8",
+        "@vue/cli-plugin-typescript": "^4.5.8",
+        "@vue/cli-plugin-unit-jest": "^4.5.8",
+        "@vue/cli-plugin-vuex": "^4.5.8",
+        "@vue/cli-service": "^4.5.8",
+        "@vue/eslint-config-standard": "^5.1.2",
+        "@vue/eslint-config-typescript": "^7.0.0",
+        "@vue/test-utils": "^1.1.1",
+        "babel-core": "^7.0.0-bridge.0",
+        "babel-eslint": "^10.1.0",
+        "babel-loader": "^8.1.0",
+        "eslint": "^7.12.1",
+        "eslint-plugin-import": "^2.22.1",
+        "eslint-plugin-node": "^11.1.0",
+        "eslint-plugin-promise": "^4.2.1",
+        "eslint-plugin-standard": "^4.0.2",
+        "eslint-plugin-vue": "^7.1.0",
+        "fibers": "^5.0.0",
+        "sass": "^1.28.0",
+        "sass-loader": "^10.0.4",
+        "style-resources-loader": "^1.3.3",
+        "typescript": "^4.0.5",
+        "vue-cli-plugin-element": "^1.0.1",
+        "vue-cli-plugin-style-resources-loader": "^0.1.4",
+        "vue-template-compiler": "^2.6.12",
+        "webpack": "^4.46.0"
+    }
 }

+ 151 - 0
src/components/RightPanel/index.vue

@@ -0,0 +1,151 @@
+<template>
+    <div
+        ref="rightPanel"
+        :class="{ show: show  }"
+        class="rightPanel-container"
+    >
+        <div class="rightPanel-background"/>
+        <div class="rightPanel">
+            <div
+                class="handle-button"
+                :style="{ 'top': buttonTop+'px','background-color': theme || '#409EFF' }"
+                @click="show=!show"
+            >
+                <i :class="show?'el-icon-close':'el-icon-setting'"/>
+            </div>
+            <div class="rightPanel-items">
+                <slot/>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts">
+import { Component, Prop, Vue, Watch } from 'vue-property-decorator'
+import { addClass, removeClass } from '@/utils'
+import { SettingsModule } from '@/store/modules/settings.ts'
+
+@Component({
+    name: 'RightPanel'
+})
+export default class extends Vue {
+    @Prop({ default: false }) private clickNotClose!: boolean
+    @Prop({ default: 250 }) private buttonTop!: number
+
+    private show = false
+
+    get theme() {
+        return SettingsModule.theme
+    }
+
+    @Watch('show')
+    private onShowChange(value: boolean) {
+        if (value && !this.clickNotClose) {
+            this.addEventClick()
+        }
+        if (value) {
+            addClass(document.body, 'showRightPanel')
+        } else {
+            removeClass(document.body, 'showRightPanel')
+        }
+    }
+
+    mounted() {
+        this.insertToBody()
+    }
+
+    beforeDestroy() {
+        const elx = this.$refs.rightPanel as Element
+        elx.remove()
+    }
+
+    private addEventClick() {
+        window.addEventListener('click', this.closeSidebar)
+    }
+
+    private closeSidebar(ev: MouseEvent) {
+        const parent = (ev.target as HTMLElement).closest('.rightPanel')
+        if (!parent) {
+            this.show = false
+            window.removeEventListener('click', this.closeSidebar)
+        }
+    }
+
+    private insertToBody() {
+        const elx = this.$refs.rightPanel as Element
+        const body = document.querySelector('body')
+        if (body) {
+            body.insertBefore(elx, body.firstChild)
+        }
+    }
+}
+</script>
+
+<style lang="scss">
+.showRightPanel {
+    overflow: hidden;
+    position: relative;
+    width: calc(100% - 15px);
+}
+</style>
+
+<style lang="scss" scoped>
+.rightPanel-background {
+    position: fixed;
+    top: 0;
+    left: 0;
+    opacity: 0;
+    transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
+    background: rgba(0, 0, 0, .2);
+    z-index: -1;
+}
+
+.rightPanel {
+    width: 100%;
+    max-width: 260px;
+    height: 100vh;
+    position: fixed;
+    top: 0;
+    right: 0;
+    box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
+    transition: all .25s cubic-bezier(.7, .3, .1, 1);
+    transform: translate(100%);
+    background: #fff;
+    z-index: 40000;
+}
+
+.show {
+    transition: all .3s cubic-bezier(.7, .3, .1, 1);
+
+    .rightPanel-background {
+        z-index: 20000;
+        opacity: 1;
+        width: 100%;
+        height: 100%;
+    }
+
+    .rightPanel {
+        transform: translate(0);
+    }
+}
+
+.handle-button {
+    width: 48px;
+    height: 48px;
+    position: absolute;
+    left: -48px;
+    text-align: center;
+    font-size: 24px;
+    border-radius: 6px 0 0 6px !important;
+    z-index: 0;
+    cursor: pointer;
+    pointer-events: auto;
+    color: #fff;
+    line-height: 48px;
+
+    i {
+        font-size: 24px;
+        line-height: 48px;
+    }
+}
+</style>

+ 159 - 0
src/components/ThemePicker/index.vue

@@ -0,0 +1,159 @@
+<template>
+    <el-color-picker
+        v-model="theme"
+        :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d']"
+        class="theme-picker"
+        popper-class="theme-picker-dropdown"
+    />
+</template>
+
+<script lang="ts">
+import { Component, Vue, Watch } from 'vue-property-decorator'
+import { SettingsModule } from '@/store/modules/settings'
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const version = require('element-ui/package.json').version // element-ui version from node_modules
+const ORIGINAL_THEME = '#409EFF' // default color
+
+@Component({
+    name: 'ThemePicker'
+})
+export default class extends Vue {
+    private chalk = '' // The content of theme-chalk css
+    private theme = ''
+
+    get defaultTheme() {
+        return SettingsModule.theme
+    }
+
+    @Watch('defaultTheme', { immediate: true })
+    private onDefaultThemeChange(value: string) {
+        this.theme = value
+    }
+
+    @Watch('theme')
+    private async onThemeChange(value: string) {
+        if (!value) return
+        const oldValue = this.chalk ? this.theme : ORIGINAL_THEME
+        const themeCluster = this.getThemeCluster(value.replace('#', ''))
+        const originalCluster = this.getThemeCluster(oldValue.replace('#', ''))
+        const message = this.$message({
+            message: '  Compiling the theme',
+            customClass: 'theme-message',
+            type: 'success',
+            duration: 0,
+            iconClass: 'el-icon-loading'
+        })
+
+        if (!this.chalk) {
+            const url = `https://unpkg.com/element-ui@${ version }/lib/theme-chalk/index.css`
+            await this.getCSSString(url, 'chalk')
+        }
+
+        const getHandler = (variable: string, id: string) => {
+            return () => {
+                const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
+                const newStyle = this.updateStyle((this as any)[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')
+        chalkHandler()
+
+        let styles: HTMLElement[] = [].slice.call(document.querySelectorAll('style'))
+        styles = styles
+            .filter(style => {
+                const text = style.innerText
+                return new RegExp(oldValue, '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.$emit('change', value)
+        message.close()
+    }
+
+    private updateStyle(style: string, oldCluster: string[], newCluster: string[]) {
+        let newStyle = style
+        oldCluster.forEach((color, index) => {
+            newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
+        })
+        return newStyle
+    }
+
+    private getCSSString(url: string, variable: string) {
+        console.log(url, variable, '------------')
+        return new Promise(resolve => {
+            const xhr = new XMLHttpRequest()
+            xhr.onreadystatechange = () => {
+                if (xhr.readyState === 4 && xhr.status === 200) {
+                    (this as any)[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
+                    resolve()
+                }
+            }
+            xhr.open('GET', url)
+            xhr.send()
+        })
+    }
+
+    private getThemeCluster(theme: string) {
+        const tintColor = (color: string, tint: number) => {
+            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))
+                return `#${ red.toString(16) }${ green.toString(16) }${ blue.toString(16) }`
+            }
+        }
+
+        const shadeColor = (color: string, shade: number) => {
+            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)
+            return `#${ red.toString(16) }${ green.toString(16) }${ blue.toString(16) }`
+        }
+
+        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 lang="scss">
+.theme-message,
+.theme-picker-dropdown {
+    z-index: 99999 !important;
+}
+
+.theme-picker .el-color-picker__trigger {
+    height: 26px !important;
+    width: 26px !important;
+    padding: 2px;
+}
+
+.theme-picker-dropdown .el-color-dropdown__link-btn {
+    display: none;
+}
+</style>

+ 125 - 0
src/layout/components/Settings/index.vue

@@ -0,0 +1,125 @@
+<template>
+  <div class="drawer-container">
+    <div>
+      <h3 class="drawer-title">
+          系统布局配置
+      </h3>
+
+      <div class="drawer-item">
+        <span>主题色</span>
+        <theme-picker
+          style="float: right;height: 26px;margin: -3px 8px 0 0;"
+          @change="themeChange"
+        />
+      </div>
+
+      <div class="drawer-item">
+        <span>显示 Tags-View</span>
+        <el-switch
+          v-model="showTagsView"
+          class="drawer-switch"
+        />
+      </div>
+
+      <div class="drawer-item">
+        <span>显示侧边栏 Logo</span>
+        <el-switch
+          v-model="showSidebarLogo"
+          class="drawer-switch"
+        />
+      </div>
+
+      <div class="drawer-item">
+        <span>固定 Header</span>
+        <el-switch
+          v-model="fixedHeader"
+          class="drawer-switch"
+        />
+      </div>
+
+      <div class="drawer-item">
+        <span>侧边栏文字主题色</span>
+        <el-switch
+          v-model="sidebarTextTheme"
+          class="drawer-switch"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Vue } from 'vue-property-decorator'
+import { SettingsModule } from '@/store/modules/settings'
+import ThemePicker from '@/components/ThemePicker/index.vue'
+
+@Component({
+  name: 'Settings',
+  components: {
+    ThemePicker
+  }
+})
+export default class extends Vue {
+  get fixedHeader() {
+    return SettingsModule.fixedHeader
+  }
+
+  set fixedHeader(value) {
+    SettingsModule.ChangeSetting({ key: 'fixedHeader', value })
+  }
+
+  get showTagsView() {
+    return SettingsModule.showTagsView
+  }
+
+  set showTagsView(value) {
+    SettingsModule.ChangeSetting({ key: 'showTagsView', value })
+  }
+
+  get showSidebarLogo() {
+    return SettingsModule.showSidebarLogo
+  }
+
+  set showSidebarLogo(value) {
+    SettingsModule.ChangeSetting({ key: 'showSidebarLogo', value })
+  }
+
+  get sidebarTextTheme() {
+    return SettingsModule.sidebarTextTheme
+  }
+
+  set sidebarTextTheme(value) {
+    SettingsModule.ChangeSetting({ key: 'sidebarTextTheme', value })
+  }
+
+  private themeChange(value: string) {
+    SettingsModule.ChangeSetting({ key: 'theme', value })
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.drawer-container {
+  padding: 24px;
+  font-size: 14px;
+  line-height: 1.5;
+  word-wrap: break-word;
+
+  .drawer-title {
+    margin-bottom: 12px;
+    color: rgba(0, 0, 0, .85);
+    font-size: 14px;
+    line-height: 22px;
+  }
+
+  .drawer-item {
+    color: rgba(0, 0, 0, .65);
+    font-size: 14px;
+    padding: 12px 0;
+  }
+
+  .drawer-switch {
+    float: right
+  }
+}
+</style>

+ 1 - 0
src/layout/components/index.ts

@@ -1,4 +1,5 @@
 export { default as AppMain } from "./AppMain.vue";
 export { default as Navbar } from "./Navbar/index.vue";
 export { default as Sidebar } from "./Sidebar/index.vue";
+export { default as Settings } from "./Settings/index.vue";
 export { default as Breadcrumb } from "@/components/Breadcrumb/index.vue";

+ 18 - 7
src/layout/index.vue

@@ -1,13 +1,16 @@
 <template>
     <div :class="classObj" class="app-wrapper">
-        <div v-if="classObj.mobile && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
-        <sidebar class="sidebar-container" />
-        <navbar />
+        <div v-if="classObj.mobile && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
+        <sidebar class="sidebar-container"/>
+        <navbar/>
         <div class="main-container">
-            <breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
-            <hr />
-            <app-main />
+            <breadcrumb id="breadcrumb-container" class="breadcrumb-container"/>
+            <hr/>
+            <app-main/>
         </div>
+        <right-panel v-if="showSettings">
+            <Settings/>
+        </right-panel>
     </div>
 </template>
 
@@ -15,8 +18,10 @@
 import { Component } from "vue-property-decorator";
 import { mixins } from "vue-class-component";
 import { DeviceType, AppModule } from "@/store/modules/app";
-import { AppMain, Navbar, Sidebar, Breadcrumb } from "./components";
+import { AppMain, Navbar, Sidebar, Breadcrumb, Settings } from "./components";
 import ResizeMixin from "./mixin/resize";
+import RightPanel from '@/components/RightPanel/index.vue'
+import { SettingsModule } from "@/store/modules/settings";
 
 @Component({
     name: "Layout",
@@ -25,6 +30,8 @@ import ResizeMixin from "./mixin/resize";
         Navbar,
         Sidebar,
         Breadcrumb,
+        RightPanel,
+        Settings
     },
 })
 export default class extends mixins(ResizeMixin) {
@@ -37,6 +44,10 @@ export default class extends mixins(ResizeMixin) {
         };
     }
 
+    get showSettings() {
+        return SettingsModule.showSettings;
+    }
+
     private handleClickOutside() {
         AppModule.CloseSideBar(false);
     }

+ 26 - 0
src/settings.ts

@@ -0,0 +1,26 @@
+interface ISettings {
+  title: string // Overrides the default title
+  showSettings: boolean // Controls settings panel display
+  showTagsView: boolean // Controls tagsview display
+  showSidebarLogo: boolean // Controls siderbar logo display
+  fixedHeader: boolean // If true, will fix the header component
+  errorLog: string[] // The env to enable the errorlog component, default 'production' only
+  sidebarTextTheme: boolean // If true, will change active text color for sidebar based on theme
+  devServerPort: number // Port number for webpack-dev-server
+  mockServerPort: number // Port number for mock server
+}
+
+// You can customize below settings :)
+const settings: ISettings = {
+  title: 'wanda-adm',
+  showSettings: true,
+  showTagsView: true,
+  fixedHeader: false,
+  showSidebarLogo: false,
+  errorLog: ['production'],
+  sidebarTextTheme: true,
+  devServerPort: 28888,
+  mockServerPort: 28888
+}
+
+export default settings

+ 38 - 0
src/store/modules/settings.ts

@@ -0,0 +1,38 @@
+import { VuexModule, Module, Mutation, Action, getModule } from 'vuex-module-decorators'
+import store from '@/store'
+import elementVariables from '@/styles/element-variables.scss'
+import defaultSettings from '@/settings'
+
+export interface ISettingsState {
+    theme: string
+    fixedHeader: boolean
+    showSettings: boolean
+    showTagsView: boolean
+    showSidebarLogo: boolean
+    sidebarTextTheme: boolean
+}
+
+@Module({ dynamic: true, store, name: 'settings' })
+class Settings extends VuexModule implements ISettingsState {
+    public theme = elementVariables.theme
+    public fixedHeader = defaultSettings.fixedHeader
+    public showSettings = defaultSettings.showSettings
+    public showTagsView = defaultSettings.showTagsView
+    public showSidebarLogo = defaultSettings.showSidebarLogo
+    public sidebarTextTheme = defaultSettings.sidebarTextTheme
+
+    @Mutation
+    private CHANGE_SETTING(payload: { key: string, value: any }) {
+        const { key, value } = payload
+        if (Object.prototype.hasOwnProperty.call(this, key)) {
+            (this as any)[key] = value
+        }
+    }
+
+    @Action
+    public ChangeSetting(payload: { key: string, value: any }) {
+        this.CHANGE_SETTING(payload)
+    }
+}
+
+export const SettingsModule = getModule(Settings)

+ 7 - 0
src/styles/element-variables.scss.d.ts

@@ -0,0 +1,7 @@
+export interface IScssVariables {
+  theme: string
+}
+
+export const variables: IScssVariables
+
+export default variables

+ 8 - 0
src/styles/styles/_mixins.scss

@@ -0,0 +1,8 @@
+/* Mixins */
+@mixin clearfix {
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+}

+ 31 - 0
src/styles/styles/_svgicon.scss

@@ -0,0 +1,31 @@
+/* Recommended css code for vue-svgicon */
+.svg-icon {
+    display: inline-block;
+    width: 16px;
+    height: 16px;
+    color: inherit;
+    fill: none;
+    stroke: currentColor;
+    vertical-align: -0.15em;
+}
+
+.svg-fill {
+    fill: currentColor;
+    stroke: none;
+}
+
+.svg-up {
+    transform: rotate(0deg);
+}
+
+.svg-right {
+    transform: rotate(90deg);
+}
+
+.svg-down {
+    transform: rotate(180deg);
+}
+
+.svg-left {
+    transform: rotate(-90deg);
+}

+ 49 - 0
src/styles/styles/_transition.scss

@@ -0,0 +1,49 @@
+/* Global transition */
+// See https://vuejs.org/v2/guide/transitions.html for detail
+
+// fade
+.fade-enter-active,
+.fade-leave-active {
+  transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+  opacity: 0;
+}
+
+// fade-transform
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+  transition: all .5s;
+}
+
+.fade-transform-enter {
+  opacity: 0;
+  transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+  opacity: 0;
+  transform: translateX(30px);
+}
+
+// breadcrumb
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+  transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+.breadcrumb-move {
+  transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+  position: absolute;
+}

+ 34 - 0
src/styles/styles/_variables.scss

@@ -0,0 +1,34 @@
+/* Variables */
+
+// Base color
+$blue:#324157;
+$light-blue:#3A71A8;
+$red:#C03639;
+$pink: #E65D6E;
+$green: #30B08F;
+$tiffany: #4AB7BD;
+$yellow:#FEC171;
+$panGreen: #30B08F;
+
+// Sidebar
+$sideBarWidth: 210px;
+$subMenuBg:#1f2d3d;
+$subMenuHover:#001528;
+$subMenuActiveText:#f4f4f5;
+$menuBg:#304156;
+$menuText:#bfcbd9;
+$menuActiveText:#409EFF; // Also see settings.sidebarTextTheme
+
+// Login page
+$lightGray: #eee;
+$darkGray:#889aa4;
+$loginBg: #2d3a4b;
+$loginCursorColor: #fff;
+
+// The :export directive is the magic sauce for webpack
+// https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
+:export {
+  menuBg: $menuBg;
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+}

+ 9 - 0
src/styles/styles/_variables.scss.d.ts

@@ -0,0 +1,9 @@
+export interface IScssVariables {
+  menuBg: string
+  menuText: string
+  menuActiveText: string
+}
+
+export const variables: IScssVariables
+
+export default variables

+ 25 - 0
src/styles/styles/element-variables.scss

@@ -0,0 +1,25 @@
+/* Element Variables */
+
+// Override Element UI variables
+$--color-primary: #1890ff;
+$--color-success: #13ce66;
+$--color-warning: #ffba00;
+$--color-danger: #ff4949;
+$--color-info: #5d5d5d;
+$--button-font-weight: 400;
+$--color-text-regular: #1f2d3d;
+$--border-color-light: #dfe4ed;
+$--border-color-lighter: #e6ebf5;
+$--table-border: 1px solid#dfe6ec;
+
+// Icon font path, required
+$--font-path: '~element-ui/lib/theme-chalk/fonts';
+
+// Apply overrided variables in Element UI
+@import '~element-ui/packages/theme-chalk/src/index';
+
+// The :export directive is the magic sauce for webpack
+// https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
+:export {
+  theme: $--color-primary;
+}

+ 7 - 0
src/styles/styles/element-variables.scss.d.ts

@@ -0,0 +1,7 @@
+export interface IScssVariables {
+  theme: string
+}
+
+export const variables: IScssVariables
+
+export default variables

+ 156 - 0
src/styles/styles/index.scss

@@ -0,0 +1,156 @@
+// @import './variables.scss'; // Already imported in style-resources-loader
+// @import './mixins.scss'; // Already imported in style-resources-loader
+@import './transition.scss';
+@import './svgicon.scss';
+
+/* Global scss */
+
+body {
+    height: 100%;
+    -moz-osx-font-smoothing: grayscale;
+    -webkit-font-smoothing: antialiased;
+    font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+html {
+    height: 100%;
+}
+
+#app {
+    height: 100%;
+}
+
+*,
+*:before,
+*:after {
+    box-sizing: border-box;
+}
+
+a,
+a:focus,
+a:hover {
+    color: inherit;
+    outline: none;
+    text-decoration: none;
+}
+
+div:focus {
+    outline: none;
+}
+
+.clearfix {
+    @include clearfix;
+}
+
+label {
+    font-weight: 700;
+}
+
+.app-container {
+    padding: 20px;
+}
+
+.components-container {
+    margin: 30px 50px;
+    position: relative;
+}
+
+// Refine element ui upload
+.upload-container {
+    .el-upload {
+        width: 100%;
+
+        .el-upload-dragger {
+            width: 100%;
+            height: 200px;
+        }
+    }
+}
+
+aside {
+    background: #eef1f6;
+    color: #2c3e50;
+    padding: 8px 24px;
+    margin-bottom: 20px;
+    border-radius: 2px;
+    display: block;
+    line-height: 32px;
+    font-size: 16px;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+
+    a {
+        color: #337ab7;
+        cursor: pointer;
+
+        &:hover {
+            color: rgb(32, 160, 255);
+        }
+    }
+}
+
+.fixed-width {
+    .el-button--mini {
+        padding: 7px 10px;
+        min-width: 60px;
+    }
+}
+
+.filter-container {
+    padding-bottom: 10px;
+
+    .filter-item {
+        display: inline-block;
+        vertical-align: middle;
+        margin-bottom: 10px;
+    }
+}
+
+.link-type,
+.link-type:focus {
+    color: #337ab7;
+    cursor: pointer;
+
+    &:hover {
+        color: rgb(32, 160, 255);
+    }
+}
+
+.status-col {
+    .cell {
+        padding: 0 10px;
+        text-align: center;
+    }
+}
+
+.text-center {
+    text-align: center
+}
+
+.sub-navbar {
+    height: 50px;
+    line-height: 50px;
+    position: relative;
+    width: 100%;
+    text-align: right;
+    padding-right: 20px;
+    transition: 600ms ease position;
+    background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
+    .subtitle {
+        font-size: 20px;
+        color: #fff;
+    }
+
+    &.draft {
+        background: #d0d0d0;
+    }
+
+    &.deleted {
+        background: #d0d0d0;
+    }
+}
+
+.no-padding {
+    padding: 0px !important;
+}

+ 17 - 0
src/utils/index.ts

@@ -0,0 +1,17 @@
+// Check if an element has a class
+export const hasClass = (ele: HTMLElement, className: string) => {
+    return !!ele.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'))
+}
+
+// Add class to element
+export const addClass = (ele: HTMLElement, className: string) => {
+    if (!hasClass(ele, className)) ele.className += ' ' + className
+}
+
+// Remove class from element
+export const removeClass = (ele: HTMLElement, className: string) => {
+    if (hasClass(ele, className)) {
+        const reg = new RegExp('(\\s|^)' + className + '(\\s|$)')
+        ele.className = ele.className.replace(reg, ' ')
+    }
+}