mirror of
https://github.com/dataease/dataease.git
synced 2026-05-20 02:58:10 +08:00
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:
12
frontend/src/App.vue
Normal file
12
frontend/src/App.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
}
|
||||
</script>
|
||||
7
frontend/src/api/license.js
Normal file
7
frontend/src/api/license.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import {post} from "@/plugins/request"
|
||||
|
||||
export function saveLicense(data) {
|
||||
return post("/samples/license/save", data)
|
||||
}
|
||||
|
||||
|
||||
21
frontend/src/api/user-token.js
Normal file
21
frontend/src/api/user-token.js
Normal 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
21
frontend/src/api/user.js
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
BIN
frontend/src/assets/RackShift-assist-white.png
Normal file
BIN
frontend/src/assets/RackShift-assist-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
frontend/src/assets/RackShift-black.png
Normal file
BIN
frontend/src/assets/RackShift-black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
BIN
frontend/src/assets/RackShift-white.png
Normal file
BIN
frontend/src/assets/RackShift-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
BIN
frontend/src/assets/font/Roboto/Roboto-Regular.ttf
Executable file
BIN
frontend/src/assets/font/Roboto/Roboto-Regular.ttf
Executable file
Binary file not shown.
57
frontend/src/assets/font/Roboto/index.css
Normal file
57
frontend/src/assets/font/Roboto/index.css
Normal 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;
|
||||
}
|
||||
|
||||
BIN
frontend/src/assets/login-desc.png
Normal file
BIN
frontend/src/assets/login-desc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 355 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
18
frontend/src/business/app-layout/horizontal-layout/index.vue
Normal file
18
frontend/src/business/app-layout/horizontal-layout/index.vue
Normal 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>
|
||||
15
frontend/src/business/dashboard/index.vue
Normal file
15
frontend/src/business/dashboard/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
{{ $t('commons.message_box.prompt') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "dashboard"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
13
frontend/src/business/directive/ClickOutsideDemo.vue
Normal file
13
frontend/src/business/directive/ClickOutsideDemo.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ClickOutsideDemo"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
49
frontend/src/business/directive/PermissionDemo.vue
Normal file
49
frontend/src/business/directive/PermissionDemo.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<h2>切换admin、editor、readonly用户看到不同的内容</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>
|
||||
245
frontend/src/business/login/index.vue
Normal file
245
frontend/src/business/login/index.vue
Normal 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>
|
||||
13
frontend/src/business/system-setting/ParamsSetting.vue
Normal file
13
frontend/src/business/system-setting/ParamsSetting.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>参数设置</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ParamsSetting"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
46
frontend/src/business/system-setting/UserManagement.vue
Normal file
46
frontend/src/business/system-setting/UserManagement.vue
Normal 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>
|
||||
59
frontend/src/components/dynamic-table/TablePagination.vue
Normal file
59
frontend/src/components/dynamic-table/TablePagination.vue
Normal 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>
|
||||
33
frontend/src/components/dynamic-table/index.vue
Normal file
33
frontend/src/components/dynamic-table/index.vue
Normal 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>
|
||||
27
frontend/src/components/layout/LayoutContent.vue
Normal file
27
frontend/src/components/layout/LayoutContent.vue
Normal 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>
|
||||
11
frontend/src/components/layout/LayoutHeader.vue
Normal file
11
frontend/src/components/layout/LayoutHeader.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<header class="header-container">
|
||||
<slot></slot>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "LayoutHeader",
|
||||
}
|
||||
</script>
|
||||
16
frontend/src/components/layout/LayoutMain.vue
Normal file
16
frontend/src/components/layout/LayoutMain.vue
Normal 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>
|
||||
25
frontend/src/components/layout/LayoutSidebar.vue
Normal file
25
frontend/src/components/layout/LayoutSidebar.vue
Normal 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>
|
||||
20
frontend/src/components/layout/LayoutView.vue
Normal file
20
frontend/src/components/layout/LayoutView.vue
Normal 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>
|
||||
66
frontend/src/components/layout/index.vue
Normal file
66
frontend/src/components/layout/index.vue
Normal 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>
|
||||
26
frontend/src/components/layout/sidebar/FixiOSBug.js
Normal file
26
frontend/src/components/layout/sidebar/FixiOSBug.js
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
frontend/src/components/layout/sidebar/Item.vue
Normal file
37
frontend/src/components/layout/sidebar/Item.vue
Normal 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>
|
||||
43
frontend/src/components/layout/sidebar/Link.vue
Normal file
43
frontend/src/components/layout/sidebar/Link.vue
Normal 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>
|
||||
85
frontend/src/components/layout/sidebar/Logo.vue
Normal file
85
frontend/src/components/layout/sidebar/Logo.vue
Normal 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>
|
||||
97
frontend/src/components/layout/sidebar/SidebarItem.vue
Normal file
97
frontend/src/components/layout/sidebar/SidebarItem.vue
Normal 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>
|
||||
@@ -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>
|
||||
266
frontend/src/components/layout/sidebar/index.vue
Normal file
266
frontend/src/components/layout/sidebar/index.vue
Normal 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>
|
||||
12
frontend/src/components/redirect/index.vue
Normal file
12
frontend/src/components/redirect/index.vue
Normal 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>
|
||||
8
frontend/src/directive/click-outside/index.js
Normal file
8
frontend/src/directive/click-outside/index.js
Normal 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
|
||||
11
frontend/src/directive/index.js
Normal file
11
frontend/src/directive/index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
frontend/src/directive/permission/index.js
Normal file
29
frontend/src/directive/permission/index.js
Normal 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)
|
||||
}
|
||||
}
|
||||
75
frontend/src/i18n/index.js
Normal file
75
frontend/src/i18n/index.js
Normal 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;
|
||||
13
frontend/src/i18n/lang/en-US.js
Normal file
13
frontend/src/i18n/lang/en-US.js
Normal 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
|
||||
};
|
||||
|
||||
54
frontend/src/i18n/lang/zh-CN.js
Normal file
54
frontend/src/i18n/lang/zh-CN.js
Normal 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
|
||||
};
|
||||
10
frontend/src/i18n/lang/zh-TW.js
Normal file
10
frontend/src/i18n/lang/zh-TW.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import el from "element-ui/lib/locale/lang/zh-TW";
|
||||
|
||||
const message = {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export default {
|
||||
...el,
|
||||
...message
|
||||
};
|
||||
12
frontend/src/icons/index.js
Normal file
12
frontend/src/icons/index.js
Normal 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
33
frontend/src/main.js
Normal 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),
|
||||
})
|
||||
56
frontend/src/permission.js
Normal file
56
frontend/src/permission.js
Normal 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()
|
||||
})
|
||||
9
frontend/src/plugins/index.js
Normal file
9
frontend/src/plugins/index.js
Normal 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);
|
||||
}
|
||||
}
|
||||
71
frontend/src/plugins/message.js
Normal file
71
frontend/src/plugins/message.js
Normal 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;
|
||||
}
|
||||
}
|
||||
108
frontend/src/plugins/request.js
Normal file
108
frontend/src/plugins/request.js
Normal 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;
|
||||
}
|
||||
}
|
||||
69
frontend/src/router/index.js
Normal file
69
frontend/src/router/index.js
Normal 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
|
||||
30
frontend/src/router/modules/directives.js
Normal file
30
frontend/src/router/modules/directives.js
Normal 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
|
||||
12
frontend/src/router/modules/filters.js
Normal file
12
frontend/src/router/modules/filters.js
Normal 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
|
||||
33
frontend/src/router/modules/system-setting.js
Normal file
33
frontend/src/router/modules/system-setting.js
Normal 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
|
||||
11
frontend/src/store/getters.js
Normal file
11
frontend/src/store/getters.js
Normal 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
|
||||
23
frontend/src/store/index.js
Normal file
23
frontend/src/store/index.js
Normal 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
|
||||
50
frontend/src/store/modules/app.js
Normal file
50
frontend/src/store/modules/app.js
Normal 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
|
||||
}
|
||||
64
frontend/src/store/modules/license.js
Normal file
64
frontend/src/store/modules/license.js
Normal 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
|
||||
}
|
||||
61
frontend/src/store/modules/permission.js
Normal file
61
frontend/src/store/modules/permission.js
Normal 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
|
||||
}
|
||||
97
frontend/src/store/modules/user-token.js
Normal file
97
frontend/src/store/modules/user-token.js
Normal 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
|
||||
}
|
||||
103
frontend/src/store/modules/user.js
Normal file
103
frontend/src/store/modules/user.js
Normal 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
|
||||
}
|
||||
54
frontend/src/styles/business/app.scss
Normal file
54
frontend/src/styles/business/app.scss
Normal 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;
|
||||
}
|
||||
|
||||
39
frontend/src/styles/business/header-menu.scss
Normal file
39
frontend/src/styles/business/header-menu.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
frontend/src/styles/common/mixins.scss
Normal file
15
frontend/src/styles/common/mixins.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
48
frontend/src/styles/common/variables.scss
Normal file
48
frontend/src/styles/common/variables.scss
Normal 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;
|
||||
}
|
||||
|
||||
4
frontend/src/styles/index.scss
Normal file
4
frontend/src/styles/index.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
@import '~normalize.css/normalize.css';
|
||||
@import "./common/variables";
|
||||
@import "~fit2cloud-ui/src/styles";
|
||||
@import "./business/app";
|
||||
15
frontend/src/utils/token.js
Normal file
15
frontend/src/utils/token.js
Normal 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)
|
||||
}
|
||||
3
frontend/src/utils/validate.js
Normal file
3
frontend/src/utils/validate.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isExternal(path) {
|
||||
return /^(https?:|mailto:|tel:)/.test(path)
|
||||
}
|
||||
Reference in New Issue
Block a user