Merge remote-tracking branch 'origin/main' into main

# Conflicts:
#	dataease-commons/dataease-common-auth/src/main/java/io/dataease/commons/auth/config/F2cRealm.java
#	frontend/src/api/user-token.js
#	frontend/src/store/modules/user-token.js
This commit is contained in:
fit2cloud-chenyw
2021-02-19 17:08:56 +08:00
155 changed files with 315 additions and 181 deletions

12
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>

View File

@@ -0,0 +1,7 @@
import {post} from "@/plugins/request"
export function saveLicense(data) {
return post("/samples/license/save", data)
}

View File

@@ -0,0 +1,21 @@
/* 前后端分离的登录方式 */
import {get, post, put} from "@/plugins/request"
export function login(data) {
return post("/login", data)
}
export function logout() {
return post("/logout")
}
export function getCurrentUser() {
return get("/info")
}
export function updateInfo(data) {
return put("/update", data)
}

21
frontend/src/api/user.js Normal file
View File

@@ -0,0 +1,21 @@
/* 前后端分离的登录方式 */
import {get, post, put} from "@/plugins/request"
export function login(data) {
return post("/login", data)
}
export function logout() {
return post("/logout")
}
export function getCurrentUser() {
return get("/info")
}
export function updateInfo(data) {
return put("/update", data)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

View File

@@ -0,0 +1,57 @@
/* cyrillic-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
src: url(Roboto-Regular.ttf) format('truetype');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
src: url(Roboto-Regular.ttf) format('truetype');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
src: url(Roboto-Regular.ttf) format('truetype');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
src: url(Roboto-Regular.ttf) format('truetype');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
src: url(Roboto-Regular.ttf) format('truetype');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
src: url(Roboto-Regular.ttf) format('truetype');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-display: swap;
src: url(Roboto-Regular.ttf) format('truetype');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

View File

@@ -0,0 +1,62 @@
<template>
<el-menu :unique-opened="true"
:default-active="language"
class="header-menu"
text-color="inherit"
mode="horizontal">
<el-submenu index="1" popper-class="header-menu-popper">
<template slot="title">
<font-awesome-icon class="language-icon" :icon="['fas', 'globe']"/>
<span>{{ languageMap[language] }}</span>
</template>
<el-menu-item v-for="(value, key) in languageMap" :key="key" :index="key" @click="setLanguage(key)">
<span>{{ value }}</span>
<i class="el-icon-check" v-if="key === language"/>
</el-menu-item>
</el-submenu>
</el-menu>
</template>
<script>
export default {
name: "LanguageSwitch",
data() {
return {
languageMap: {
"zh-CN": "中文(简体)",
"en-US": "English",
}
};
},
computed: {
language() {
return this.$store.getters.language
}
},
methods: {
setLanguage(lang) {
this.$store.dispatch('user/setLanguage', lang).then(() => {
// do something
})
}
}
}
</script>
<style lang="scss">
@import "~@/styles/business/header-menu.scss";
.header-menu {
.language-icon {
width: 24px;
}
}
.header-menu-popper {
.el-icon-check {
margin-left: 10px;
color: $--color-primary;
}
}
</style>

View File

@@ -0,0 +1,48 @@
<template>
<el-menu class="header-menu" text-color="inherit" mode="horizontal">
<el-submenu index="none" popper-class="header-menu-popper">
<template slot="title">
<span>{{ name }}</span>
</template>
<el-menu-item @click="toPersonal">
<span>{{ $t('commons.personal.personal_information') }}</span>
</el-menu-item>
<el-menu-item @click="toHelp">
<span>{{ $t('commons.personal.help_documentation') }}</span>
</el-menu-item>
<el-menu-item @click="logout">
<span>{{ $t('commons.personal.exit_system') }}</span>
</el-menu-item>
</el-submenu>
</el-menu>
</template>
<script>
import {mapGetters} from "vuex";
export default {
name: "PersonalSetting",
computed: {
...mapGetters([
'name'
])
},
methods: {
toPersonal() {
},
toHelp() {
window.open("https://github.com/fit2cloud-ui/samples", "_blank");
},
logout() {
this.$store.dispatch("user/logout").then(() => {
location.reload()
})
}
}
}
</script>
<style lang="scss">
@import "~@/styles/business/header-menu.scss";
</style>

View File

@@ -0,0 +1,67 @@
<template>
<div class="horizontal-header">
<div class="header-left">
<sidebar-toggle-button/>
</div>
<div class="header-right">
<div class="navbar-item">
<language-switch/>
</div>
<div class="navbar-item">
<personal-setting/>
</div>
</div>
</div>
</template>
<script>
import SidebarToggleButton from "@/components/layout/sidebar/SidebarToggleButton";
import LanguageSwitch from "@/business/app-layout/header-components/LanguageSwitch";
import PersonalSetting from "@/business/app-layout/header-components/PersonalSetting";
export default {
name: "HorizontalHeader",
components: {PersonalSetting, LanguageSwitch, SidebarToggleButton}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/common/mixins";
.horizontal-header {
@include flex-row(flex-start, center);
position: relative;
height: 100%;
// 先去掉横线看看
//&:after {
// content: "";
// position: absolute;
// bottom: 0;
// left: 0;
// height: 1px;
// width: 100%;
// background-color: #D5D5D5;
//}
.header-left {
@include flex-row(flex-start, center);
position: relative;
height: 100%;
}
.header-right {
@include flex-row(flex-end, center);
flex: auto;
height: 100%;
.navbar-item {
color: #2E2E2E;
}
.navbar-item + .navbar-item {
margin-left: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<layout>
<template v-slot:header>
<horizontal-header/>
</template>
</layout>
</template>
<script>
import Layout from "@/components/layout";
import HorizontalHeader from "@/business/app-layout/horizontal-layout/HorizontalHeader";
export default {
name: "HorizontalLayout",
components: {HorizontalHeader, Layout},
}
</script>

View File

@@ -0,0 +1,15 @@
<template>
<div>
{{ $t('commons.message_box.prompt') }}
</div>
</template>
<script>
export default {
name: "dashboard"
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div></div>
</template>
<script>
export default {
name: "ClickOutsideDemo"
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,49 @@
<template>
<div class="app-container">
<h2>切换admineditorreadonly用户看到不同的内容</h2>
<br/>
<div class="permission-block admin" v-permission="['admin']">
需要admin角色才能看到, 指令设置v-permission="['admin']"
</div>
<div class="permission-block editor" v-permission="['editor']">
需要editor角色才能看到, 指令设置v-permission="['editor']"
</div>
<div class="permission-block admin-editor" v-permission="['admin', 'editor']">
需要admin或者editor角色才能看到, 指令设置v-permission="['admin', 'editor']"
</div>
<div class="permission-block">
任何人都能看到
</div>
</div>
</template>
<script>
export default {
name: "PermissionDemo"
}
</script>
<style lang="scss" scoped>
.permission-block {
width: 100%;
height: 40px;
line-height: 40px;
font-size: 18px;
&.admin {
color: #2D61A2;
}
&.admin-editor {
color: mix(#2D61A2, #FFBA00)
}
&.editor {
color: #FFBA00;
}
}
</style>

View File

@@ -0,0 +1,245 @@
<template>
<div class="login-background">
<div class="login-container">
<el-row type="flex" v-loading="loading">
<el-col :span="12">
<el-form :model="form" :rules="rules" ref="form" size="default">
<div class="login-logo">
<img src="../../assets/RackShift-black.png" alt="">
</div>
<div class="login-title">
{{ $t('login.title') }}
</div>
<div class="login-border"></div>
<div class="login-welcome">
{{ $t('login.welcome') }}
</div>
<div class="login-form">
<el-form-item prop="username">
<el-input v-model="form.username" :placeholder="$t('login.username')" autofocus/>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" :placeholder="$t('login.password')"
show-password maxlength="30" show-word-limit
autocomplete="new-password"/>
</el-form-item>
</div>
<div class="login-btn">
<el-button type="primary" class="submit" @click="submit('form')" size="default">
{{ $t('commons.button.login') }}
</el-button>
</div>
<div class="login-msg">
{{ msg }}
</div>
</el-form>
</el-col>
<el-col :span="12">
<div class="login-image"></div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import { encrypt } from '@/utils/rsaEncrypt'
export default {
name: "Login",
data() {
return {
loading: false,
form: {
username: 'admin',
password: '123456'
},
rules: {
username: [
{required: true, message: this.$tm('commons.validate.input', 'login.username'), trigger: 'blur'},
],
password: [
// 先去掉方便测试
{required: true, message: this.$tm('commons.validate.input', 'login.password'), trigger: 'blur'},
{min: 6, max: 30, message: this.$t('commons.validate.limit', [6, 30]), trigger: 'blur'}
]
},
msg: '',
redirect: undefined,
otherQuery: {}
}
},
watch: {
$route: {
handler: function (route) {
const query = route.query
if (query) {
this.redirect = query.redirect
this.otherQuery = this.getOtherQuery(query)
}
},
immediate: true
}
},
created: function () {
document.addEventListener("keydown", this.watchEnter);
},
destroyed() {
document.removeEventListener("keydown", this.watchEnter);
},
methods: {
watchEnter(e) {
let keyCode = e.keyCode;
if (keyCode === 13) {
this.submit('form');
}
},
submit(form) {
this.$refs[form].validate((valid) => {
if (valid) {
const user = {
username: this.form.username,
password: this.form.password
}
user.password = encrypt(user.password)
this.loading = true;
this.$store.dispatch('user/login', user).then(() => {
this.$router.push({path: this.redirect || '/', query: this.otherQuery})
this.loading = false
}).catch(error => {
this.msg = error.message
this.loading = false
})
} else {
return false;
}
});
},
getOtherQuery(query) {
return Object.keys(query).reduce((acc, cur) => {
if (cur !== 'redirect') {
acc[cur] = query[cur]
}
return acc
}, {})
}
}
}
</script>
<style lang="scss" scoped>
@import "../../styles/common/variables";
@mixin login-center {
display: flex;
justify-content: center;
align-items: center;
}
.login-background {
background-color: $--background-color-base;
height: 100%;
@include login-center;
}
.login-container {
min-width: 900px;
width: 1280px;
height: 520px;
background-color: #FFFFFF;
@media only screen and (max-width: 1280px) {
width: 900px;
height: 380px;
}
.login-logo {
margin-top: 30px;
margin-left: 30px;
@media only screen and (max-width: 1280px) {
margin-top: 20px;
}
img {
height: 45px;
}
}
.login-title {
margin-top: 50px;
font-size: 32px;
letter-spacing: 0;
text-align: center;
color: #999999;
@media only screen and (max-width: 1280px) {
margin-top: 20px;
}
}
.login-border {
height: 2px;
margin: 20px auto 20px;
position: relative;
width: 80px;
background: $--color-primary;
@media only screen and (max-width: 1280px) {
margin: 10px auto 10px;
}
}
.login-welcome {
margin-top: 50px;
font-size: 14px;
color: #999999;
letter-spacing: 0;
line-height: 18px;
text-align: center;
@media only screen and (max-width: 1280px) {
margin-top: 20px;
}
}
.login-form {
margin-top: 30px;
padding: 0 40px;
@media only screen and (max-width: 1280px) {
margin-top: 10px;
}
& ::v-deep .el-input__inner {
border-radius: 0;
}
}
.login-btn {
margin-top: 40px;
padding: 0 40px;
@media only screen and (max-width: 1280px) {
margin-top: 20px;
}
.submit {
width: 100%;
border-radius: 0;
}
}
.login-msg {
margin-top: 10px;
padding: 0 40px;
color: $--color-danger;
text-align: center;
}
.login-image {
background: url(../../assets/login-desc.png) no-repeat;
background-size: cover;
width: 100%;
height: 520px;
@media only screen and (max-width: 1280px) {
height: 380px;
}
}
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div>参数设置</div>
</template>
<script>
export default {
name: "ParamsSetting"
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,46 @@
<template>
<layout-content>
<dynamic-table>
<fu-search-bar quick-placeholder=" 姓名/邮箱 搜索" :components="components" @exec="search"/>
</dynamic-table>
</layout-content>
</template>
<script>
import DynamicTable from "@/components/dynamic-table";
import LayoutContent from "@/components/layout/LayoutContent";
export default {
name: "UserManagement",
components: {LayoutContent, DynamicTable},
data() {
return {
components: [
{field: "name", label: "姓名", component: "FuInputComponent", defaultOperator: "eq"},
{field: "email", label: "Email", component: "FuInputComponent"},
{
field: "status",
label: "状态",
component: "FuSelectComponent",
options: [
{label: "运行中", value: "Running"},
{label: "成功", value: "Success"},
{label: "失败", value: "Fail"}
],
multiple: true
},
{field: "create_time", label: "创建时间", component: "FuDateTimeComponent"},
]
}
},
methods: {
search(condition) {
console.log(condition)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,59 @@
<template>
<el-row type="flex" justify="end">
<div class="table-pagination">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pageSizes"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</el-row>
</template>
<script>
export default {
name: "TablePagination",
props: {
page: Object,
currentPage: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 5
},
pageSizes: {
type: Array,
default: function () {
return [5, 10, 20, 50, 100]
}
},
total: {
type: Number,
default: 0
},
change: Function
},
methods: {
handleSizeChange: function (size) {
this.$emit('update:pageSize', size)
this.change();
},
handleCurrentChange(current) {
this.$emit('update:currentPage', current)
this.change();
}
}
}
</script>
<style lang="scss" scoped>
.table-pagination {
padding-top: 20px;
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="dynamic-table">
<div class="dynamic-table__header" v-if="$slots.header || header">
<slot name="header">{{ header }}</slot>
</div>
<div class="dynamic-table__body">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "DynamicTable",
props: {
header: {},
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/common/mixins.scss";
.dynamic-table {
}
.dynamic-table__header {
@include flex-row(flex-start, center);
height: 60px;
font-size: 20px;
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<div class="content-container">
<slot></slot>
</div>
</template>
<script>
export default {
name: "LayoutContent"
}
</script>
<style lang="scss">
@import "~@/styles/common/variables";
.content-container {
transition: 0.3s;
color: $--color-text-primary;
background-color: #FFFFFF;
overflow: auto;
height: 100%;
padding: 20px;
border-radius: 4px;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 14%);
box-sizing: border-box;
}
</style>

View File

@@ -0,0 +1,11 @@
<template>
<header class="header-container">
<slot></slot>
</header>
</template>
<script>
export default {
name: "LayoutHeader",
}
</script>

View File

@@ -0,0 +1,16 @@
<template>
<el-container class="main-container" direction="vertical">
<slot></slot>
</el-container>
</template>
<script>
export default {
name: "LayoutMain",
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,25 @@
<template>
<aside :class="['sidebar-container', {'is-collapse': isCollapse}]">
<slot>
<sidebar/>
</slot>
</aside>
</template>
<script>
import {mapGetters} from "vuex";
import Sidebar from "@/components/layout/sidebar";
export default {
name: "LayoutSidebar",
components: {Sidebar},
computed: {
...mapGetters([
'sidebar'
]),
isCollapse() {
return !this.sidebar.opened
},
}
}
</script>

View File

@@ -0,0 +1,20 @@
<template>
<main class="view-container">
<transition name="el-fade-in" mode="out-in">
<keep-alive>
<router-view :key="key"/>
</keep-alive>
</transition>
</main>
</template>
<script>
export default {
name: "LayoutView",
computed: {
key() {
return this.$route.path
}
}
}
</script>

View File

@@ -0,0 +1,66 @@
<template>
<el-container class="layout-container">
<slot>
<layout-sidebar/>
<layout-main>
<layout-header>
<slot name="header"></slot>
</layout-header>
<layout-view/>
</layout-main>
</slot>
</el-container>
</template>
<script>
import LayoutSidebar from "@/components/layout/LayoutSidebar";
import LayoutMain from "@/components/layout/LayoutMain";
import LayoutHeader from "@/components/layout/LayoutHeader";
import LayoutView from "@/components/layout/LayoutView";
export default {
name: "Layout",
components: {LayoutView, LayoutHeader, LayoutMain, LayoutSidebar},
}
</script>
<style lang="scss">
@import "~@/styles/common/variables";
.layout-container {
min-width: 1024px;
height: 100%;
background-color: $layout-bg-color;
}
.main-container {
position: relative;
}
.sidebar-container {
position: relative;
transition: width 0.28s;
width: $sidebar-open-width;
min-width: $sidebar-open-width;
background-color: $sidebar-bg-color;
background-image: $sidebar-bg-gradient;
&.is-collapse {
width: $sidebar-close-width;
min-width: $sidebar-close-width;
}
}
.header-container {
height: $header-height;
padding: 0 $header-padding;
}
.view-container {
display: block;
flex: auto;
overflow: auto;
box-sizing: border-box;
padding: $view-padding;
}
</style>

View File

@@ -0,0 +1,26 @@
export default {
computed: {
device() {
return this.$store.state.app.device
}
},
mounted() {
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
this.fixBugIniOS()
},
methods: {
fixBugIniOS() {
const $subMenu = this.$refs.subMenu
if ($subMenu) {
const handleMouseleave = $subMenu.handleMouseleave
$subMenu.handleMouseleave = (e) => {
if (this.device === 'mobile') {
return
}
handleMouseleave(e)
}
}
}
}
}

View File

@@ -0,0 +1,37 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const {icon, title} = context.props
const vnodes = []
if (icon) {
vnodes.push(<i class={[icon, 'sub-el-icon']}/>)
}
if (title) {
vnodes.push(<span slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>
<style scoped>
.sub-el-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
</style>

View File

@@ -0,0 +1,43 @@
<template>
<component :is="type" v-bind="linkProps(to)">
<slot/>
</component>
</template>
<script>
import {isExternal} from '@/utils/validate'
export default {
props: {
to: {
type: String,
required: true
}
},
computed: {
isExternal() {
return isExternal(this.to)
},
type() {
if (this.isExternal) {
return 'a'
}
return 'router-link'
}
},
methods: {
linkProps(to) {
if (this.isExternal) {
return {
href: to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: to
}
}
}
}
</script>

View File

@@ -0,0 +1,85 @@
<template>
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebar-logo-fade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="collapseLogo" :src="collapseLogo" class="sidebar-logo" alt="Sidebar Logo">
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo" alt="Sidebar Logo">
</router-link>
</transition>
</div>
</template>
<script>
export default {
name: 'SidebarLogo',
props: {
collapse: {
type: Boolean,
required: true
}
},
data() {
return {
title: 'FIT2CLOUD',
logo: require('@/assets/RackShift-white.png'),
collapseLogo: require('@/assets/RackShift-assist-white.png')
}
}
}
</script>
<style lang="scss">
@import "~@/styles/common/variables";
.sidebar-logo-container {
position: relative;
height: $header-height;
line-height: $header-height;
overflow: hidden;
&:after {
content: "";
position: absolute;
bottom: 0;
right: #{$sidebar-close-width / 4};
height: 1px;
width: calc(100% - #{$sidebar-close-width / 2});
background-color: hsla(0, 0%, 100%, .5);
}
& .sidebar-logo-link {
display: flex;
align-items: center;
height: 100%;
width: 100%;
& .sidebar-logo {
margin-left: #{$sidebar-close-width / 4};
height: $logo-height;
vertical-align: middle;
}
}
&.collapse {
.sidebar-logo-link {
justify-content: center;
}
.sidebar-logo {
margin: 0;
}
}
}
.sidebar-logo-fade-enter-active {
transition: opacity 0.3s;
transition-delay: 0.1s
}
.sidebar-logo-fade-enter,
.sidebar-logo-fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div v-if="!item.hidden">
<template
v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-no-dropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="$tk(onlyOneChild.meta.title)"/>
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body
popper-class="sidebar-popper">
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="$tk(item.meta.title)"/>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import {isExternal} from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: {Item, AppLink},
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = {...parent, path: '', noShowingChildren: true}
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<el-button circle :class="['sidebar-toggle-button', icon]" @click="toggle"></el-button>
</template>
<script>
import {mapGetters} from "vuex";
export default {
name: "SidebarToggleButton",
methods: {
toggle() {
this.$store.dispatch('app/toggleSideBar');
}
},
computed: {
...mapGetters([
'sidebar'
]),
icon() {
return this.sidebar.opened ? "el-icon-s-fold" : "el-icon-s-unfold"
},
}
}
</script>
<style lang="scss" scoped>
.sidebar-toggle-button.el-button {
font-size: 18px;
}
</style>

View File

@@ -0,0 +1,266 @@
<template>
<div class="sidebar">
<logo :collapse="isCollapse"/>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:collapse-transition="false"
:unique-opened="false"
mode="vertical">
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path"/>
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import {mapGetters} from 'vuex'
import SidebarItem from './SidebarItem'
import Logo from "@/components/layout/sidebar/Logo";
export default {
name: "Sidebar",
components: {Logo, SidebarItem},
computed: {
...mapGetters([
'permission_routes',
'sidebar'
]),
activeMenu() {
const route = this.$route
const {meta, path} = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
isCollapse() {
return !this.sidebar.opened
},
}
}
</script>
<style lang="scss">
@import "~@/styles/common/variables";
@mixin sidebar-base-item {
padding-left: 10px !important;
border-radius: 2px;
color: $menu-color;
}
@mixin menu-item {
@include sidebar-base-item;
margin: 2px 10px;
line-height: $menu-height;
height: $menu-height;
}
@mixin submenu-item {
@include sidebar-base-item;
margin: 2px 10px;
line-height: $submenu-height;
height: $submenu-height;
}
@mixin popper-submenu-item {
@include sidebar-base-item;
margin: 2px 0;
line-height: $submenu-height;
height: $submenu-height;
}
@mixin menu-item-active {
color: $menu-active-color;
background-color: $menu-active-bg-color;
}
@mixin submenu-item-active {
color: $submenu-active-color;
background-color: $submenu-active-bg-color;
}
.sidebar {
height: 100%;
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.el-scrollbar {
box-sizing: border-box;
padding: 10px 0;
height: calc(100% - #{$header-height});
.el-scrollbar__bar {
&.is-vertical {
right: 0;
}
&.is-horizontal {
display: none;
}
}
.scrollbar-wrapper {
height: 100%;
overflow-x: hidden;
}
}
a {
width: 100%;
overflow: hidden;
}
.el-menu {
border: none;
height: 100%;
width: 100%;
background-color: $menu-bg-color;
.submenu-title-no-dropdown {
@include menu-item;
&:hover {
background-color: $menu-bg-color-hover;
}
&.is-active {
@include menu-item-active;
}
}
.el-submenu {
.el-submenu__title {
@include menu-item;
&:hover {
background-color: $menu-bg-color-hover;
}
}
&.is-active {
.el-submenu__title, {
@include menu-item-active;
.sub-el-icon, span {
color: #FFF;
}
}
}
.el-menu-item {
@include submenu-item;
&:hover {
background-color: $menu-bg-color-hover;
}
&.is-active {
@include submenu-item-active
}
}
}
.nest-menu, .el-submenu__title, .submenu-title-no-dropdown {
span {
padding-left: 30px;
}
.sub-el-icon {
margin-right: 10px;
+ span {
padding-left: 0;
}
}
}
&.el-menu--collapse {
.el-tooltip {
padding: 0 !important;
text-align: center;
line-height: $menu-height;
}
.el-submenu__title {
padding-left: 20px !important;
}
.submenu-title-no-dropdown, .el-submenu__title {
max-width: 60px;
text-align: center;
span {
display: none;
}
.sub-el-icon {
margin: 0;
}
.el-submenu__icon-arrow {
display: none;
}
}
}
}
}
.sidebar-popper {
& > .el-menu {
display: block;
background-color: $sidebar-bg-color;
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
}
.nest-menu .el-submenu > .el-submenu__title, .el-menu-item {
&.is-active {
@include submenu-item-active
}
@include popper-submenu-item;
span {
padding-left: 30px;
}
.sub-el-icon {
margin-right: 10px;
+ span {
padding-left: 0;
}
}
&:hover {
background-color: $menu-bg-color-hover;
}
}
> .el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}
</style>

View File

@@ -0,0 +1,12 @@
<script>
export default {
created() {
const { params, query } = this.$route
const { path } = params
this.$router.replace({ path: '/' + path, query })
},
render: function(h) {
return h() // avoid warning message
}
}
</script>

View File

@@ -0,0 +1,8 @@
import ClickOutside from "element-ui/src/utils/clickoutside";
const install = function (Vue) {
Vue.directive("click-outside", ClickOutside)
}
ClickOutside.install = install
export default ClickOutside

View File

@@ -0,0 +1,11 @@
import ClickOutside from "element-ui/src/utils/clickoutside";
import permission from "@/directive/permission";
export default {
install(Vue) {
Vue.directive('click-outside', ClickOutside);
Vue.directive('permission', permission);
}
}

View File

@@ -0,0 +1,29 @@
import store from '@/store'
function checkPermission(el, binding) {
const {value} = binding
const roles = store.getters && store.getters.roles
if (value && value instanceof Array) {
if (value.length > 0) {
const permissionRoles = value
const hasPermission = roles.some(role => {
return permissionRoles.includes(role)
})
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el)
}
}
}
}
export default {
inserted(el, binding) {
checkPermission(el, binding)
},
update(el, binding) {
checkPermission(el, binding)
}
}

View File

@@ -0,0 +1,75 @@
import Vue from 'vue';
import VueI18n from "vue-i18n";
Vue.use(VueI18n);
// 直接加载翻译的语言文件
const LOADED_LANGUAGES = ['zh-CN', 'en-US'];
const LANG_FILES = require.context('./lang', true, /\.js$/)
// 自动加载lang目录下语言文件默认只加载LOADED_LANGUAGES中规定的语言文件其他的语言动态加载
const messages = LANG_FILES.keys().reduce((messages, path) => {
const value = LANG_FILES(path)
const lang = path.replace(/^\.\/(.*)\.\w+$/, '$1');
if (LOADED_LANGUAGES.includes(lang)) {
messages[lang] = value.default
}
return messages;
}, {})
export const getLanguage = () => {
let language = localStorage.getItem('language')
if (!language) {
language = (navigator.language || navigator.browserLanguage).toLowerCase()
}
return language;
}
const i18n = new VueI18n({
locale: getLanguage(),
messages,
});
const importLanguage = lang => {
if (!LOADED_LANGUAGES.includes(lang)) {
return import(`./lang/${lang}`).then(response => {
i18n.mergeLocaleMessage(lang, response.default);
LOADED_LANGUAGES.push(lang);
return Promise.resolve(lang)
})
}
return Promise.resolve(lang)
}
const setLang = lang => {
localStorage.setItem('language', lang)
i18n.locale = lang;
}
export const setLanguage = lang => {
if (i18n.locale !== lang) {
importLanguage(lang).then(setLang)
}
}
// 组合翻译例如key为'请输入{0}'keys为login.username则自动将keys翻译并替换到{0} {1}...
Vue.prototype.$tm = function (key, ...keys) {
let values = [];
for (const k of keys) {
values.push(i18n.t(k))
}
return i18n.t(key, values);
};
// 忽略警告不存在Key直接返回Key
Vue.prototype.$tk = function (key) {
const hasKey = i18n.te(key)
if (hasKey) {
return i18n.t(key)
}
return key
};
// 设置当前语言LOADED_LANGUAGES以外的翻译文件会自动从lang目录获取(如果有的话), 如果不需要动态加载语言文件直接用setLang
Vue.prototype.$setLang = setLanguage;
export default i18n;

View File

@@ -0,0 +1,13 @@
import el from "element-ui/lib/locale/lang/en";
import fu from "fit2cloud-ui/src/locale/lang/en_US"; // 加载fit2cloud的内容
const message = {
// TODO
}
export default {
...el,
...fu,
...message
};

View File

@@ -0,0 +1,54 @@
import el from "element-ui/lib/locale/lang/zh-CN"; // 加载element的内容
import fu from "fit2cloud-ui/src/locale/lang/zh-CN"; // 加载fit2cloud的内容
const message = {
commons: {
message_box: {
alert: "警告",
confirm: "确认",
prompt: "提示",
},
button: {
login: "登录",
ok: "确定",
save: "保存",
delete: "删除",
cancel: "取消",
return: "返回",
},
msg: {
success: "{0}成功",
op_success: "操作成功",
save_success: "保存成功",
delete_success: "删除成功",
},
validate: {
limit: '长度在 {0} 到 {1} 个字符',
input: "请输入{0}",
select: "请选择{0}",
},
personal: {
personal_information: "个人信息",
help_documentation: "帮助文档",
exit_system: "退出系统",
}
},
login: {
username: "用户名",
password: "密码",
title: "登录 FIT2CLOUD",
welcome: "欢迎回来,请输入用户名和密码登录",
expires: '认证信息已过期,请重新登录',
},
route: {
system_setting: "系统设置",
user_management: "用户管理",
params_setting: "参数设置",
},
}
export default {
...el,
...fu,
...message
};

View File

@@ -0,0 +1,10 @@
import el from "element-ui/lib/locale/lang/zh-TW";
const message = {
// TODO
}
export default {
...el,
...message
};

View File

@@ -0,0 +1,12 @@
import {library} from '@fortawesome/fontawesome-svg-core'
import {fas} from '@fortawesome/free-solid-svg-icons'
import {far} from '@fortawesome/free-regular-svg-icons'
import {fab} from '@fortawesome/free-brands-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
export default {
install(Vue) {
library.add(fas, far, fab);
Vue.component('font-awesome-icon', FontAwesomeIcon);
}
}

33
frontend/src/main.js Normal file
View File

@@ -0,0 +1,33 @@
import Vue from 'vue'
import "@/styles/index.scss"
import Fit2CloudUI from 'fit2cloud-ui';
import ElementUI from 'element-ui';
import App from './App.vue'
import i18n from "./i18n";
import router from './router'
import store from './store'
import icons from './icons'
import plugins from "./plugins";
import directives from "./directive";
import "./permission"
Vue.config.productionTip = false
Vue.use(ElementUI, {
size: 'small',
i18n: (key, value) => i18n.t(key, value)
});
Vue.use(Fit2CloudUI, {
i18n: (key, value) => i18n.t(key, value)
});
Vue.use(icons);
Vue.use(plugins);
Vue.use(directives);
new Vue({
el: '#app',
i18n,
router,
store,
render: h => h(App),
})

View File

@@ -0,0 +1,56 @@
import router from './router'
import store from './store'
import NProgress from 'nprogress'
import { getToken } from '@/utils/token'
import 'nprogress/nprogress.css'
NProgress.configure({showSpinner: false}) // NProgress Configuration
const whiteList = ['/login'] // no redirect whitelist
const generateRoutes = async (to, from, next) => {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
const {roles} = await store.dispatch('user/getCurrentUser')
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
router.addRoutes(accessRoutes)
next({...to, replace: true})
} catch (error) {
await store.dispatch('user/logout')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
// 路由前置钩子,根据实际需求修改
router.beforeEach(async (to, from, next) => {
NProgress.start()
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({path: '/'})
NProgress.done()
} else {
await generateRoutes(to, from, next)
}
} else {
/* has not login*/
if (whiteList.indexOf(to.path) !== -1) {
// in the free login whitelist, go directly
next()
} else {
// other pages that do not have permission to access are redirected to the login page.
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})

View File

@@ -0,0 +1,9 @@
import message from "@/plugins/message";
import request from "@/plugins/request";
export default {
install(Vue) {
Vue.use(message);
Vue.use(request);
}
}

View File

@@ -0,0 +1,71 @@
import {MessageBox, Message} from 'element-ui';
import i18n from "@/i18n";
export const $alert = (message, callback, options) => {
let title = i18n.t("common.message_box.alert");
MessageBox.alert(message, title, options).then(() => {
callback();
});
}
export const $confirm = (message, callback, options = {}) => {
let defaultOptions = {
confirmButtonText: i18n.t("common.button.ok"),
cancelButtonText: i18n.t("common.button.cancel"),
type: 'warning',
...options
}
let title = i18n.t("common.message_box.confirm");
MessageBox.confirm(message, title, defaultOptions).then(() => {
callback();
});
}
export const $success = (message, duration) => {
Message.success({
message: message,
type: "success",
showClose: true,
duration: duration || 1500
})
}
export const $info = (message, duration) => {
Message.info({
message: message,
type: "info",
showClose: true,
duration: duration || 3000
})
}
export const $warning = (message, duration) => {
Message.warning({
message: message,
type: "warning",
showClose: true,
duration: duration || 5000
})
}
export const $error = (message, duration) => {
Message.error({
message: message,
type: "error",
showClose: true,
duration: duration || 10000
})
}
export default {
install(Vue) {
// 使用$$前缀避免与Element UI的冲突
Vue.prototype.$$confirm = $confirm;
Vue.prototype.$$alert = $alert;
Vue.prototype.$success = $success;
Vue.prototype.$info = $info;
Vue.prototype.$warning = $warning;
Vue.prototype.$error = $error;
}
}

View File

@@ -0,0 +1,108 @@
import axios from 'axios'
import {$alert, $error} from "./message"
import store from '@/store'
import i18n from "@/i18n";
import {TokenKey, getToken} from '@/utils/token'
const instance = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
withCredentials: true,
timeout: 60000 // request timeout, default 1 min
})
// 每次请求加上Token。如果没用使用Token删除这个拦截器
instance.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers[TokenKey] = getToken()
}
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
}
)
const checkAuth = response => {
// 请根据实际需求修改
if (response.headers["authentication-status"] === "invalid" || response.status === 401) {
let message = i18n.t('login.expires');
$alert(message, () => {
store.dispatch('user/logout').then(() => {
location.reload()
})
});
}
}
const checkPermission = response => {
// 请根据实际需求修改
if (response.status === 403) {
location.href = "/403";
}
}
// 请根据实际需求修改
instance.interceptors.response.use(response => {
checkAuth(response);
return response;
}, error => {
let msg;
if (error.response) {
checkAuth(error.response);
checkPermission(error.response);
msg = error.response.data.message || error.response.data;
} else {
console.log('error: ' + error) // for debug
msg = error.message;
}
$error(msg)
return Promise.reject(error);
});
export const request = instance
/* 简化请求方法统一处理返回结果并增加loading处理这里以{success,data,message}格式的返回值为例,具体项目根据实际需求修改 */
const promise = (request, loading = {}) => {
return new Promise((resolve, reject) => {
loading.status = true;
request.then(response => {
if (response.data.success) {
resolve(response.data);
} else {
reject(response.data)
}
loading.status = false;
}).catch(error => {
reject(error)
loading.status = false;
})
})
}
export const get = (url, data, loading) => {
return promise(request({url: url, method: "get", params: data}), loading)
};
export const post = (url, data, loading) => {
return promise(request({url: url, method: "post", data}), loading)
};
export const put = (url, data, loading) => {
return promise(request({url: url, method: "put", data}), loading)
};
export const del = (url, loading) => {
return promise(request({url: url, method: "delete"}), loading)
};
export default {
install(Vue) {
Vue.prototype.$get = get;
Vue.prototype.$post = post;
Vue.prototype.$put = put;
Vue.prototype.$delete = del;
Vue.prototype.$request = request;
}
}

View File

@@ -0,0 +1,69 @@
import Vue from 'vue'
import Router from 'vue-router'
// 加载modules中的路由
const modules = require.context('./modules', true, /\.js$/)
// 修复路由变更后报错的问题
const routerPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return routerPush.call(this, location).catch(error => error)
}
Vue.use(Router)
import Layout from '@/business/app-layout/horizontal-layout'
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/components/redirect')
}
]
},
{
path: '/login',
component: () => import('@/business/login'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/business/dashboard'),
name: 'Dashboard',
meta: {title: 'Dashboard', icon: 'el-icon-s-marketing', affix: true}
}
]
}
]
/**
* 用户登录后根据角色加载的路由
*/
export const rolesRoutes = [
...modules.keys().map(key => modules(key).default),
{path: '*', redirect: '/', hidden: true}
]
const createRouter = () => new Router({
scrollBehavior: () => ({y: 0}),
routes: constantRoutes
})
const router = createRouter()
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router

View File

@@ -0,0 +1,30 @@
import Layout from "@/business/app-layout/horizontal-layout";
const Directive = {
path: '/directive',
component: Layout,
name: 'Directive',
meta: {
title: "指令示例",
icon: 'el-icon-setting',
},
children: [
{
path: 'click-outside',
component: () => import('@/business/directive/ClickOutsideDemo'),
name: "ClickOutside",
meta: {
title: "点击外部指令"
}
},
{
path: 'permission',
component: () => import('@/business/directive/PermissionDemo'),
name: "Permission",
meta: {
title: "权限指令"
}
}
]
}
export default Directive

View File

@@ -0,0 +1,12 @@
import Layout from "@/business/app-layout/horizontal-layout";
const Filters = {
path: '/filters',
component: Layout,
name: 'Filters',
meta: {
title: "过滤器示例",
icon: 'el-icon-setting',
}
}
export default Filters

View File

@@ -0,0 +1,33 @@
import Layout from "@/business/app-layout/horizontal-layout";
const SystemSetting = {
path: '/system-setting',
component: Layout,
name: 'SystemSetting',
meta: {
title: "route.system_setting",
icon: 'el-icon-setting',
roles: ['admin']
},
children: [
{
path: 'user-management',
component: () => import('@/business/system-setting/UserManagement'),
name: "UserManagement",
meta: {
title: "route.user_management",
roles: ['admin']
}
},
{
path: 'params-setting',
component: () => import('@/business/system-setting/ParamsSetting'),
name: "ParamsSetting",
meta: {
title: "route.params_setting",
roles: ['admin']
}
}
]
}
export default SystemSetting

View File

@@ -0,0 +1,11 @@
// 根据实际需要修改
const getters = {
sidebar: state => state.app.sidebar,
name: state => state.user.name,
language: state => state.user.language,
roles: state => state.user.roles,
permission_routes: state => state.permission.routes,
license: state => state.license,
token: state => state.user.token,
}
export default getters

View File

@@ -0,0 +1,23 @@
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
Vue.use(Vuex)
// 自动从modules目录下获取模块
const MODULES_FILES = require.context('./modules', true, /\.js$/)
// 模块名为js文件名例如user.js 则模块名为user
const modules = MODULES_FILES.keys().reduce((modules, modulePath) => {
const value = MODULES_FILES(modulePath)
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
modules[moduleName] = value.default
return modules
}, {})
const store = new Vuex.Store({
modules,
getters
})
export default store

View File

@@ -0,0 +1,50 @@
const get = () => {
return localStorage.getItem('sidebarStatus')
}
const set = value => {
localStorage.setItem('sidebarStatus', value)
}
const state = {
sidebar: {
opened: get() ? !!+get() : true
},
device: 'desktop'
}
const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
if (state.sidebar.opened) {
set(1)
} else {
set(0)
}
},
OPEN_SIDEBAR: (state) => {
set('sidebarStatus', 1)
state.sidebar.opened = true
},
CLOSE_SIDEBAR: (state) => {
set('sidebarStatus', 0)
state.sidebar.opened = false
}
}
const actions = {
toggleSideBar({commit}) {
commit('TOGGLE_SIDEBAR')
},
openSideBar({commit}) {
commit('OPEN_SIDEBAR')
},
closeSideBar({commit}) {
commit('CLOSE_SIDEBAR')
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@@ -0,0 +1,64 @@
import {saveLicense} from "@/api/license"
const LicenseKey = "X-License";
const Status = {
valid: "valid",
invalid: "invalid",
expired: "expired",
}
const get = () => {
return localStorage.getItem(LicenseKey)
}
const set = value => {
localStorage.setItem(LicenseKey, value)
}
const state = {
status: get(),
license: {},
message: ""
}
const mutations = {
SET_STATUS: (state, status) => {
set(LicenseKey, status)
state.status = status;
},
SET_LICENSE: (state, license) => {
state.license = license;
},
SET_MESSAGE: (state, message) => {
state.message = message;
}
}
const actions = {
saveLicense({commit}, content) {
return new Promise((resolve, reject) => {
saveLicense({license: content}).then(response => {
const {status, license, message} = response.data;
commit('SET_STATUS', status)
commit('SET_LICENSE', license)
commit('SET_MESSAGE', message)
resolve(status)
}).catch(error => {
commit('SET_STATUS', Status.invalid)
reject(error)
})
})
},
isValid({state}) {
return state.status === Status.valid
},
isExpired({state}) {
return state.status === Status.expired
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@@ -0,0 +1,61 @@
import {rolesRoutes, constantRoutes} from '@/router'
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}
export function filterRolesRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = {...route}
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterRolesRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({commit}, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {
// admin角色加载所有路由
accessedRoutes = rolesRoutes || []
} else {
// 其他角色加载对应角色的路由
accessedRoutes = filterRolesRoutes(rolesRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)
resolve(accessedRoutes)
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@@ -0,0 +1,97 @@
import {login, getCurrentUser, updateInfo, logout} from '@/api/user-token'
import {resetRouter} from '@/router'
import {getToken, setToken, removeToken} from '@/utils/token'
import {getLanguage, setLanguage} from "@/i18n";
/* 前后端不分离的登录办法*/
const state = {
token: getToken(),
name: "",
language: getLanguage(),
roles: []
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_LANGUAGE: (state, language) => {
state.language = language
setLanguage(language)
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
}
const actions = {
login({commit}, userInfo) {
const {username, password} = userInfo
return new Promise((resolve, reject) => {
login({username: username.trim(), password: password}).then(response => {
let token = response.data
commit('SET_TOKEN', token)
setToken(token)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
isLogin({commit}) {
return new Promise((resolve, reject) => {
let token = getToken()
if (token) {
commit('SET_TOKEN', token);
resolve(true)
} else {
reject(false)
}
});
},
getCurrentUser({commit}) {
return new Promise((resolve, reject) => {
getCurrentUser().then(response => {
const {name, roles, language} = response.data
commit('SET_NAME', name)
commit('SET_ROLES', roles)
commit('SET_LANGUAGE', language)
resolve(response.data)
}).catch(error => {
reject(error)
})
});
},
setLanguage({commit, state}, language) {
commit('SET_LANGUAGE', language)
return new Promise((resolve, reject) => {
updateInfo(state.id, {language: language}).then(response => {
resolve(response)
}).catch(error => {
reject(error)
})
})
},
logout({commit}) {
logout().then(() => {
commit('SET_TOKEN', "");
commit('SET_ROLES', [])
removeToken()
resetRouter()
})
},
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@@ -0,0 +1,103 @@
import {login, getCurrentUser, updateInfo, logout} from '@/api/user'
import {resetRouter} from '@/router'
import {getToken, setToken, removeToken} from '@/utils/token'
import {getLanguage, setLanguage} from "@/i18n";
/* 前后端不分离的登录办法*/
const getDefaultState = () => {
return {
token: getToken(),
name: "",
language: getLanguage(),
roles: []
}
}
const state = getDefaultState()
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
SET_NAME: (state, name) => {
state.name = name
},
SET_LANGUAGE: (state, language) => {
state.language = language
setLanguage(language)
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
}
const actions = {
login({commit}, userInfo) {
const {username, password} = userInfo
return new Promise((resolve, reject) => {
login({username: username.trim(), password: password}).then(response => {
let token = response.data.token
commit('SET_TOKEN', token)
setToken(token)
resolve(response)
}).catch(error => {
reject(error)
})
})
},
isLogin({commit}) {
return new Promise((resolve, reject) => {
let token = getToken()
if (token) {
commit('SET_TOKEN', token);
resolve(true)
} else {
reject(false)
}
});
},
getCurrentUser({commit}) {
return new Promise((resolve, reject) => {
getCurrentUser().then(response => {
const {name, roles, language} = response.data
commit('SET_NAME', name)
commit('SET_ROLES', roles)
commit('SET_LANGUAGE', language)
resolve(response.data)
}).catch(error => {
reject(error)
})
});
},
setLanguage({commit, state}, language) {
commit('SET_LANGUAGE', language)
return new Promise((resolve, reject) => {
updateInfo(state.id, {language: language}).then(response => {
resolve(response)
}).catch(error => {
reject(error)
})
})
},
logout({commit}) {
logout().then(() => {
commit('SET_TOKEN', "");
commit('SET_ROLES', [])
removeToken()
resetRouter()
})
},
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@@ -0,0 +1,54 @@
@import "~@/assets/font/Roboto/index.css";
html {
height: 100%;
box-sizing: border-box;
}
body {
font-family: Roboto, Helvetica, PingFang SC, Arial, sans-serif;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
height: 100%;
}
#app {
height: 100%;
}
:focus {
outline: none;
}
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
// 滚动条整体部分
::-webkit-scrollbar {
width: 6px; // 纵向滚动条宽度
height: 6px; // 横向滚动条高度
}
// 滑块
::-webkit-scrollbar-thumb {
border-radius: 5px;
background-color: #4A4B4D;
}
// 轨道
::-webkit-scrollbar-track {
border-radius: 5px;
background-color: transparent;
}

View File

@@ -0,0 +1,39 @@
@import "~@/styles/common/variables.scss";
.header-menu {
min-width: 150px;
color: #3E3E3D;
&.el-menu {
background-color: transparent;
&.el-menu--horizontal {
border: none;
.el-submenu__title {
border: none;
min-width: 150px;
height: 40px;
line-height: 40px;
}
}
}
}
.header-menu-popper {
color: #3E3E3D;
.el-menu--popup {
min-width: 150px;
}
.el-menu-item {
&.is-active {
color: $--color-primary;
}
&:hover {
background-color: #D5D5D5;
}
}
}

View File

@@ -0,0 +1,15 @@
@mixin flex-row($justify: flex-start, $align: stretch) {
display: flex;
@if $justify != flex-start {
justify-content: $justify;
}
@if $align != stretch {
align-items: $align;
}
}
@mixin click-active-scale($scale:0.95) {
&:active {
transform: scale($scale);
}
}

View File

@@ -0,0 +1,48 @@
/* Element 变量 */
$--color-primary: #447DF7;
$--color-success: #87CB16;
$--color-warning: #FFA534;
$--color-danger: #FB404B;
$--box-shadow-light: 0 1px 4px 0 rgb(0 0 0 / 14%);
$--color-text-primary: #3c4858;
/* layout */
$layout-bg-color: #F2F2F2;
/* sidebar */
$sidebar-open-width: 260px;
$sidebar-close-width: 80px;
$sidebar-bg-color: #30373d;
$sidebar-bg-gradient: linear-gradient(to bottom right, #30373D, #3E3E3D);
/* menu */
$menu-color: #BFCBD9;
$menu-active-color: #FFF;
$menu-active-bg-color: rgb(200 200 200 / 20%);
$menu-bg-color: transparent;
$menu-bg-color-hover: #4A4B4D;
$menu-height: 50px;
$submenu-height: 40px;
$submenu-active-color: #FFF;
$submenu-active-bg-color: $menu-active-bg-color;
/* logo */
$logo-height: 40px;
$logo-bg-color: #4E5051;
/* header */
$header-height: 60px;
$header-padding: 30px;
/* main */
$view-padding: 15px;
/* fit2cloud-ui的variables加载了element-ui的变量 */
@import "~fit2cloud-ui/src/styles/common/variables";
:export {
theme: $--color-primary;
}

View File

@@ -0,0 +1,4 @@
@import '~normalize.css/normalize.css';
@import "./common/variables";
@import "~fit2cloud-ui/src/styles";
@import "./business/app";

View File

@@ -0,0 +1,15 @@
import Cookies from 'js-cookie'
export const TokenKey = 'Authorization' // 自行修改
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}

View File

@@ -0,0 +1,3 @@
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}