feat:vue-element-admin升级改造vue3
This commit is contained in:
@@ -433,12 +433,12 @@ export default service
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, computed} from 'vue'
|
import {defineComponent, computed} from 'vue'
|
||||||
import {userStore} from '@/store';
|
import {useStore} from '@/store';
|
||||||
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const store = userStore()
|
const store = useStore()
|
||||||
const num = computed(()=>{
|
const num = computed(()=>{
|
||||||
return store.state.count
|
return store.state.count
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^16.11.7",
|
"@types/node": "^16.11.7",
|
||||||
|
"@types/nprogress": "^0.2.0",
|
||||||
"@vitejs/plugin-vue": "^1.9.3",
|
"@vitejs/plugin-vue": "^1.9.3",
|
||||||
"sass": "^1.43.4",
|
"sass": "^1.43.4",
|
||||||
"typescript": "^4.4.3",
|
"typescript": "^4.4.3",
|
||||||
|
|||||||
8
src/api/system/menu.ts
Normal file
8
src/api/system/menu.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function getRouteList() {
|
||||||
|
return request({
|
||||||
|
url: '/youlai-admin/api/v1/menus/route',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -14,12 +14,11 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import {defineComponent, onBeforeMount, reactive, toRefs} from "vue";
|
import {defineComponent, onBeforeMount, reactive, toRefs,watch} from "vue";
|
||||||
import {compile} from 'path-to-regexp'
|
import {compile} from 'path-to-regexp'
|
||||||
import {useRoute, RouteLocationMatched} from "vue-router";
|
import {useRoute, RouteLocationMatched} from "vue-router";
|
||||||
import router from "@router";
|
import router from "@router";
|
||||||
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const currentRoute = useRoute();
|
const currentRoute = useRoute();
|
||||||
@@ -29,65 +28,50 @@ export default defineComponent({
|
|||||||
return toPath(params)
|
return toPath(params)
|
||||||
}
|
}
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
breadcrumbs: [] as Array<RouteLocationMatched>,
|
levelList: [] as Array<RouteLocationMatched>,
|
||||||
getBreadcrumb: () => {
|
getBreadcrumb: () => {
|
||||||
|
let matched = currentRoute.matched.filter((item) => item.meta && item.meta.title)
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
levelList: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
$route() {
|
|
||||||
this.getBreadcrumb()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.getBreadcrumb()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getBreadcrumb() {
|
|
||||||
// only show routes with meta.title
|
|
||||||
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
|
|
||||||
const first = matched[0]
|
const first = matched[0]
|
||||||
|
if (!state.isDashboard(first)) {
|
||||||
if (!this.isDashboard(first)) {
|
matched = [{path: '/dashboard', meta: {title: 'dashboard'}} as any].concat(matched)
|
||||||
matched = [{path: '/dashboard', meta: {title: 'Dashboard'}}].concat(matched)
|
|
||||||
}
|
}
|
||||||
|
state.levelList = matched.filter((item) => {
|
||||||
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
|
return item.mate && item.meta.title && item.meta.breadcrumb !== false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
isDashboard(route) {
|
isDashboard(route: RouteLocationMatched) {
|
||||||
const name = route && route.name
|
const name = route && route.name
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
|
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
|
||||||
},
|
},
|
||||||
pathCompile(path) {
|
handleLink(item: any) {
|
||||||
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
|
|
||||||
const {params} = this.$route
|
|
||||||
var toPath = pathToRegexp.compile(path)
|
|
||||||
return toPath(params)
|
|
||||||
},
|
|
||||||
handleLink(item) {
|
|
||||||
const {redirect, path} = item
|
const {redirect, path} = item
|
||||||
if (redirect) {
|
if (redirect) {
|
||||||
this.$router.push(redirect)
|
router.push(redirect)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$router.push(this.pathCompile(path))
|
router.push(pathCompile(path))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
watch(() => currentRoute.path, (path) => {
|
||||||
|
if (path.startsWith('/redirect/')) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
state.getBreadcrumb()
|
||||||
</script>
|
})
|
||||||
|
onBeforeMount(() => {
|
||||||
|
state.getBreadcrumb()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.app-breadcrumb.el-breadcrumb {
|
.app-breadcrumb.el-breadcrumb {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
175
src/components/ThemePicker/index.vue
Normal file
175
src/components/ThemePicker/index.vue
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
<template>
|
||||||
|
<el-color-picker
|
||||||
|
v-model="theme"
|
||||||
|
:predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
|
||||||
|
class="theme-picker"
|
||||||
|
popper-class="theme-picker-dropdown"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const version = require('element-ui/package.json').version // element-ui version from node_modules
|
||||||
|
const ORIGINAL_THEME = '#409EFF' // default color
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chalk: '', // content of theme-chalk css
|
||||||
|
theme: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
defaultTheme() {
|
||||||
|
return this.$store.state.settings.theme
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
defaultTheme: {
|
||||||
|
handler: function(val, oldVal) {
|
||||||
|
this.theme = val
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
async theme(val) {
|
||||||
|
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
|
||||||
|
if (typeof val !== 'string') return
|
||||||
|
const themeCluster = this.getThemeCluster(val.replace('#', ''))
|
||||||
|
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
|
||||||
|
console.log(themeCluster, originalCluster)
|
||||||
|
|
||||||
|
const $message = this.$message({
|
||||||
|
message: ' Compiling the theme',
|
||||||
|
customClass: 'theme-message',
|
||||||
|
type: 'success',
|
||||||
|
duration: 0,
|
||||||
|
iconClass: 'el-icon-loading'
|
||||||
|
})
|
||||||
|
|
||||||
|
const getHandler = (variable, id) => {
|
||||||
|
return () => {
|
||||||
|
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
|
||||||
|
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
|
||||||
|
|
||||||
|
let styleTag = document.getElementById(id)
|
||||||
|
if (!styleTag) {
|
||||||
|
styleTag = document.createElement('style')
|
||||||
|
styleTag.setAttribute('id', id)
|
||||||
|
document.head.appendChild(styleTag)
|
||||||
|
}
|
||||||
|
styleTag.innerText = newStyle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.chalk) {
|
||||||
|
const url = `https://unpkg.com/element-ui@${version}/lib/theme-chalk/index.css`
|
||||||
|
await this.getCSSString(url, 'chalk')
|
||||||
|
}
|
||||||
|
|
||||||
|
const chalkHandler = getHandler('chalk', 'chalk-style')
|
||||||
|
|
||||||
|
chalkHandler()
|
||||||
|
|
||||||
|
const styles = [].slice.call(document.querySelectorAll('style'))
|
||||||
|
.filter(style => {
|
||||||
|
const text = style.innerText
|
||||||
|
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
|
||||||
|
})
|
||||||
|
styles.forEach(style => {
|
||||||
|
const { innerText } = style
|
||||||
|
if (typeof innerText !== 'string') return
|
||||||
|
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('change', val)
|
||||||
|
|
||||||
|
$message.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateStyle(style, oldCluster, newCluster) {
|
||||||
|
let newStyle = style
|
||||||
|
oldCluster.forEach((color, index) => {
|
||||||
|
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
|
||||||
|
})
|
||||||
|
return newStyle
|
||||||
|
},
|
||||||
|
|
||||||
|
getCSSString(url, variable) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const xhr = new XMLHttpRequest()
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||||
|
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.open('GET', url)
|
||||||
|
xhr.send()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getThemeCluster(theme) {
|
||||||
|
const tintColor = (color, tint) => {
|
||||||
|
let red = parseInt(color.slice(0, 2), 16)
|
||||||
|
let green = parseInt(color.slice(2, 4), 16)
|
||||||
|
let blue = parseInt(color.slice(4, 6), 16)
|
||||||
|
|
||||||
|
if (tint === 0) { // when primary color is in its rgb space
|
||||||
|
return [red, green, blue].join(',')
|
||||||
|
} else {
|
||||||
|
red += Math.round(tint * (255 - red))
|
||||||
|
green += Math.round(tint * (255 - green))
|
||||||
|
blue += Math.round(tint * (255 - blue))
|
||||||
|
|
||||||
|
red = red.toString(16)
|
||||||
|
green = green.toString(16)
|
||||||
|
blue = blue.toString(16)
|
||||||
|
|
||||||
|
return `#${red}${green}${blue}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadeColor = (color, shade) => {
|
||||||
|
let red = parseInt(color.slice(0, 2), 16)
|
||||||
|
let green = parseInt(color.slice(2, 4), 16)
|
||||||
|
let blue = parseInt(color.slice(4, 6), 16)
|
||||||
|
|
||||||
|
red = Math.round((1 - shade) * red)
|
||||||
|
green = Math.round((1 - shade) * green)
|
||||||
|
blue = Math.round((1 - shade) * blue)
|
||||||
|
|
||||||
|
red = red.toString(16)
|
||||||
|
green = green.toString(16)
|
||||||
|
blue = blue.toString(16)
|
||||||
|
|
||||||
|
return `#${red}${green}${blue}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const clusters = [theme]
|
||||||
|
for (let i = 0; i <= 9; i++) {
|
||||||
|
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
|
||||||
|
}
|
||||||
|
clusters.push(shadeColor(theme, 0.1))
|
||||||
|
return clusters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.theme-message,
|
||||||
|
.theme-picker-dropdown {
|
||||||
|
z-index: 99999 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-picker .el-color-picker__trigger {
|
||||||
|
height: 26px !important;
|
||||||
|
width: 26px !important;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-picker-dropdown .el-color-dropdown__link-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,15 +6,24 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
export default {
|
|
||||||
name: 'AppMain',
|
import {useRoute} from "vue-router";
|
||||||
computed: {
|
import {defineComponent} from "vue";
|
||||||
key() {
|
|
||||||
return this.$route.path
|
export default defineComponent({
|
||||||
}
|
setup() {
|
||||||
|
const route = useRoute()
|
||||||
|
const key = () => {
|
||||||
|
return route.path
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -25,6 +34,7 @@ export default {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixed-header + .app-main {
|
.fixed-header + .app-main {
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import {reactive,computed,toRefs} from "vue";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {useRoute,useRouter} from "vue-router"
|
||||||
import Breadcrumb from '@/components/Breadcrumb/index.vue'
|
import Breadcrumb from '@/components/Breadcrumb/index.vue'
|
||||||
import Hamburger from '@/components/Hamburger/index.vue'
|
import Hamburger from '@/components/Hamburger/index.vue'
|
||||||
|
|
||||||
@@ -41,19 +43,35 @@ export default {
|
|||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
Hamburger
|
Hamburger
|
||||||
},
|
},
|
||||||
computed: {
|
setup() {
|
||||||
...mapGetters([
|
const store = useStore()
|
||||||
'sidebar',
|
const route = useRoute()
|
||||||
'avatar'
|
const router = useRouter()
|
||||||
])
|
|
||||||
|
const sidebar = computed(() => {
|
||||||
|
return store.state.app.sidebar
|
||||||
|
})
|
||||||
|
const device = computed(() => {
|
||||||
|
return store.state.app.device.toString()
|
||||||
|
})
|
||||||
|
const avatar = computed(() => {
|
||||||
|
return store.state.user.avatar
|
||||||
|
})
|
||||||
|
|
||||||
|
const state=reactive({
|
||||||
|
toggleSideBar:()=>{
|
||||||
|
store.dispatch('app/toggleSideBar', false)
|
||||||
},
|
},
|
||||||
methods: {
|
logout:()=>{
|
||||||
toggleSideBar() {
|
store.dispatch('user/logout')
|
||||||
this.$store.dispatch('app/toggleSideBar')
|
router.push(`/login?redirect=${route.fullPath}`)
|
||||||
},
|
}
|
||||||
async logout() {
|
})
|
||||||
await this.$store.dispatch('user/logout')
|
return{
|
||||||
this.$router.push(`/login?redirect=${this.$route.fullPath}`)
|
sidebar,
|
||||||
|
device,
|
||||||
|
avatar,
|
||||||
|
...toRefs(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
src/layout/components/Settings/index.vue
Normal file
97
src/layout/components/Settings/index.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div class="drawer-container">
|
||||||
|
<div>
|
||||||
|
<h3 class="drawer-title">系统布局配置</h3>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>主题色</span>
|
||||||
|
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>开启 Tags-View</span>
|
||||||
|
<el-switch v-model="tagsView" class="drawer-switch"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>固定 Header</span>
|
||||||
|
<el-switch v-model="fixedHeader" class="drawer-switch"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>侧边栏 Logo</span>
|
||||||
|
<el-switch v-model="sidebarLogo" class="drawer-switch"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ThemePicker from '@/components/ThemePicker/index.vue'
|
||||||
|
import {defineComponent, reactive, toRefs, watch} from "vue"
|
||||||
|
import {useStore} from '@/store'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {ThemePicker},
|
||||||
|
setup() {
|
||||||
|
const store = useStore()
|
||||||
|
const state = reactive({
|
||||||
|
fixedHeader:store.state.settings.fixedHeader,
|
||||||
|
tagsView:store.state.settings.tagsView,
|
||||||
|
sidebarLogo:store.state.settings.sidebarLogo,
|
||||||
|
themeChange: (val) => {
|
||||||
|
store.dispatch('settings/changeSetting', { key: 'theme', val })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watch(()=>state.fixedHeader,(value)=>{
|
||||||
|
store.dispatch('settings/changeSetting',{ key: 'fixedHeader', value })
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => state.tagsView, (value) => {
|
||||||
|
store.dispatch('settings/changeSetting', { key: 'showTagsView', value })
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => state.sidebarLogo, (value) => {
|
||||||
|
store.dispatch('settings/changeSetting', { key: 'showSidebarLogo', value })
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...toRefs(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.drawer-container {
|
||||||
|
padding: 24px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
.drawer-title {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: rgba(0, 0, 0, .85);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-item {
|
||||||
|
color: rgba(0, 0, 0, .65);
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-switch {
|
||||||
|
float: right
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-link {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,45 +12,56 @@
|
|||||||
:collapse-transition="false"
|
:collapse-transition="false"
|
||||||
mode="vertical"
|
mode="vertical"
|
||||||
>
|
>
|
||||||
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
|
<sidebar-item v-for="route in routes" :key="route.path" :item="route"
|
||||||
|
:base-path="route.path"/>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex'
|
import {computed, defineComponent} from "vue";
|
||||||
import Logo from './Logo.vue'
|
|
||||||
import SidebarItem from './SidebarItem.vue'
|
import SidebarItem from './SidebarItem.vue'
|
||||||
|
import SidebarLogo from './Logo.vue'
|
||||||
import variables from '@styles/variables.scss'
|
import variables from '@styles/variables.scss'
|
||||||
|
import {useStore} from '@/store'
|
||||||
|
import {useRoute} from 'vue-router'
|
||||||
|
|
||||||
export default {
|
|
||||||
components: { SidebarItem, Logo },
|
export default defineComponent({
|
||||||
computed: {
|
components: {
|
||||||
...mapGetters([
|
SidebarItem,
|
||||||
'sidebar'
|
SidebarLogo
|
||||||
]),
|
|
||||||
routes() {
|
|
||||||
return this.$router.options.routes
|
|
||||||
},
|
},
|
||||||
activeMenu() {
|
setup() {
|
||||||
const route = this.$route
|
const store = useStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const sidebar = computed(() => {
|
||||||
|
return store.state.app.sidebar
|
||||||
|
})
|
||||||
|
const showLogo = computed(() => {
|
||||||
|
return store.state.settings.sidebarLogo
|
||||||
|
})
|
||||||
|
const activeMenu = computed(() => {
|
||||||
const {meta, path} = route
|
const {meta, path} = route
|
||||||
// if set path, the sidebar will highlight the path you set
|
// if set path, the sidebar will highlight the path you set
|
||||||
if (meta.activeMenu) {
|
if (meta.activeMenu) {
|
||||||
return meta.activeMenu
|
return meta.activeMenu
|
||||||
}
|
}
|
||||||
return path
|
return path
|
||||||
},
|
})
|
||||||
showLogo() {
|
const isCollapse = computed(() => {
|
||||||
return this.$store.state.settings.sidebarLogo
|
return !sidebar.value.opened
|
||||||
},
|
})
|
||||||
variables() {
|
|
||||||
return variables
|
return {
|
||||||
},
|
sidebar,
|
||||||
isCollapse() {
|
route,
|
||||||
return !this.sidebar.opened
|
showLogo,
|
||||||
}
|
variables,
|
||||||
|
activeMenu,
|
||||||
|
isCollapse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
94
src/layout/components/TagsView/ScrollPane.vue
Normal file
94
src/layout/components/TagsView/ScrollPane.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
|
||||||
|
<slot />
|
||||||
|
</el-scrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const tagAndTagSpacing = 4 // tagAndTagSpacing
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ScrollPane',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
left: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
scrollWrapper() {
|
||||||
|
return this.$refs.scrollContainer.$refs.wrap
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleScroll(e) {
|
||||||
|
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||||
|
const $scrollWrapper = this.scrollWrapper
|
||||||
|
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||||
|
},
|
||||||
|
emitScroll() {
|
||||||
|
this.$emit('scroll')
|
||||||
|
},
|
||||||
|
moveToTarget(currentTag) {
|
||||||
|
const $container = this.$refs.scrollContainer.$el
|
||||||
|
const $containerWidth = $container.offsetWidth
|
||||||
|
const $scrollWrapper = this.scrollWrapper
|
||||||
|
const tagList = this.$parent.$refs.tag
|
||||||
|
|
||||||
|
let firstTag = null
|
||||||
|
let lastTag = null
|
||||||
|
|
||||||
|
// find first tag and last tag
|
||||||
|
if (tagList.length > 0) {
|
||||||
|
firstTag = tagList[0]
|
||||||
|
lastTag = tagList[tagList.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstTag === currentTag) {
|
||||||
|
$scrollWrapper.scrollLeft = 0
|
||||||
|
} else if (lastTag === currentTag) {
|
||||||
|
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||||
|
} else {
|
||||||
|
// find preTag and nextTag
|
||||||
|
const currentIndex = tagList.findIndex(item => item === currentTag)
|
||||||
|
const prevTag = tagList[currentIndex - 1]
|
||||||
|
const nextTag = tagList[currentIndex + 1]
|
||||||
|
|
||||||
|
// the tag's offsetLeft after of nextTag
|
||||||
|
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
|
||||||
|
|
||||||
|
// the tag's offsetLeft before of prevTag
|
||||||
|
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
|
||||||
|
|
||||||
|
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||||
|
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
|
||||||
|
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||||
|
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.scroll-container {
|
||||||
|
white-space: nowrap;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
::v-deep {
|
||||||
|
.el-scrollbar__bar {
|
||||||
|
bottom: 0px;
|
||||||
|
}
|
||||||
|
.el-scrollbar__wrap {
|
||||||
|
height: 49px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
294
src/layout/components/TagsView/index.vue
Normal file
294
src/layout/components/TagsView/index.vue
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<template>
|
||||||
|
<div id="tags-view-container" class="tags-view-container">
|
||||||
|
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
|
||||||
|
<router-link
|
||||||
|
v-for="tag in visitedViews"
|
||||||
|
ref="tag"
|
||||||
|
:key="tag.path"
|
||||||
|
:class="isActive(tag)?'active':''"
|
||||||
|
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||||
|
tag="span"
|
||||||
|
class="tags-view-item"
|
||||||
|
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
|
||||||
|
@contextmenu.prevent.native="openMenu(tag,$event)"
|
||||||
|
>
|
||||||
|
{{ generateTitle(tag.title) }}
|
||||||
|
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
|
||||||
|
</router-link>
|
||||||
|
</scroll-pane>
|
||||||
|
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
|
||||||
|
<li @click="refreshSelectedTag(selectedTag)">{{ $t('tagsView.refresh') }}</li>
|
||||||
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">{{ $t('tagsView.close') }}</li>
|
||||||
|
<li @click="closeOthersTags">{{ $t('tagsView.closeOthers') }}</li>
|
||||||
|
<li @click="closeAllTags(selectedTag)">{{ $t('tagsView.closeAll') }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ScrollPane from './ScrollPane'
|
||||||
|
import { generateTitle } from '@/utils/i18n'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ScrollPane },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
selectedTag: {},
|
||||||
|
affixTags: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
visitedViews() {
|
||||||
|
return this.$store.state.tagsView.visitedViews
|
||||||
|
},
|
||||||
|
routes() {
|
||||||
|
return this.$store.state.permission.routes
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
$route() {
|
||||||
|
this.addTags()
|
||||||
|
this.moveToCurrentTag()
|
||||||
|
},
|
||||||
|
visible(value) {
|
||||||
|
if (value) {
|
||||||
|
document.body.addEventListener('click', this.closeMenu)
|
||||||
|
} else {
|
||||||
|
document.body.removeEventListener('click', this.closeMenu)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initTags()
|
||||||
|
this.addTags()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
generateTitle, // generateTitle by vue-i18n
|
||||||
|
isActive(route) {
|
||||||
|
return route.path === this.$route.path
|
||||||
|
},
|
||||||
|
isAffix(tag) {
|
||||||
|
return tag.meta && tag.meta.affix
|
||||||
|
},
|
||||||
|
filterAffixTags(routes, basePath = '/') {
|
||||||
|
let tags = []
|
||||||
|
routes.forEach(route => {
|
||||||
|
if (route.meta && route.meta.affix) {
|
||||||
|
const tagPath = path.resolve(basePath, route.path)
|
||||||
|
tags.push({
|
||||||
|
fullPath: tagPath,
|
||||||
|
path: tagPath,
|
||||||
|
name: route.name,
|
||||||
|
meta: { ...route.meta }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (route.children) {
|
||||||
|
const tempTags = this.filterAffixTags(route.children, route.path)
|
||||||
|
if (tempTags.length >= 1) {
|
||||||
|
tags = [...tags, ...tempTags]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return tags
|
||||||
|
},
|
||||||
|
initTags() {
|
||||||
|
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
|
||||||
|
for (const tag of affixTags) {
|
||||||
|
// Must have tag name
|
||||||
|
if (tag.name) {
|
||||||
|
this.$store.dispatch('tagsView/addVisitedView', tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addTags() {
|
||||||
|
const { name } = this.$route
|
||||||
|
if (name) {
|
||||||
|
this.$store.dispatch('tagsView/addView', this.$route)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
moveToCurrentTag() {
|
||||||
|
const tags = this.$refs.tag
|
||||||
|
this.$nextTick(() => {
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (tag.to.path === this.$route.path) {
|
||||||
|
this.$refs.scrollPane.moveToTarget(tag)
|
||||||
|
// when query is different then update
|
||||||
|
if (tag.to.fullPath !== this.$route.fullPath) {
|
||||||
|
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshSelectedTag(view) {
|
||||||
|
this.$store.dispatch('tagsView/delCachedView', view).then(() => {
|
||||||
|
const { fullPath } = view
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$router.replace({
|
||||||
|
path: '/redirect' + fullPath
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeSelectedTag(view) {
|
||||||
|
this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => {
|
||||||
|
if (this.isActive(view)) {
|
||||||
|
this.toLastView(visitedViews, view)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeOthersTags() {
|
||||||
|
this.$router.push(this.selectedTag)
|
||||||
|
this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => {
|
||||||
|
this.moveToCurrentTag()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeAllTags(view) {
|
||||||
|
this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => {
|
||||||
|
if (this.affixTags.some(tag => tag.path === view.path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.toLastView(visitedViews, view)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
toLastView(visitedViews, view) {
|
||||||
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
|
if (latestView) {
|
||||||
|
this.$router.push(latestView.fullPath)
|
||||||
|
} else {
|
||||||
|
// now the default is to redirect to the home page if there is no tags-view,
|
||||||
|
// you can adjust it according to your needs.
|
||||||
|
if (view.name === 'Dashboard') {
|
||||||
|
// to reload home page
|
||||||
|
this.$router.replace({ path: '/redirect' + view.fullPath })
|
||||||
|
} else {
|
||||||
|
this.$router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openMenu(tag, e) {
|
||||||
|
const menuMinWidth = 105
|
||||||
|
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
|
||||||
|
const offsetWidth = this.$el.offsetWidth // container width
|
||||||
|
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
||||||
|
const left = e.clientX - offsetLeft + 15 // 15: margin right
|
||||||
|
|
||||||
|
if (left > maxLeft) {
|
||||||
|
this.left = maxLeft
|
||||||
|
} else {
|
||||||
|
this.left = left
|
||||||
|
}
|
||||||
|
|
||||||
|
this.top = e.clientY
|
||||||
|
this.visible = true
|
||||||
|
this.selectedTag = tag
|
||||||
|
},
|
||||||
|
closeMenu() {
|
||||||
|
this.visible = false
|
||||||
|
},
|
||||||
|
handleScroll() {
|
||||||
|
this.closeMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tags-view-container {
|
||||||
|
height: 34px;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #d8dce5;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||||
|
.tags-view-wrapper {
|
||||||
|
.tags-view-item {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
border: 1px solid #d8dce5;
|
||||||
|
color: #495060;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-top: 4px;
|
||||||
|
&:first-of-type {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
&:last-of-type {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: #42b983;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #42b983;
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
background: #fff;
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contextmenu {
|
||||||
|
margin: 0;
|
||||||
|
background: #fff;
|
||||||
|
z-index: 3000;
|
||||||
|
position: absolute;
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||||
|
li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 7px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
//reset element css of el-icon-close
|
||||||
|
.tags-view-wrapper {
|
||||||
|
.tags-view-item {
|
||||||
|
.el-icon-close {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
vertical-align: 2px;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
transition: all .3s cubic-bezier(.645, .045, .355, 1);
|
||||||
|
transform-origin: 100% 50%;
|
||||||
|
&:before {
|
||||||
|
transform: scale(.6);
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: -3px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: #b4bccc;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,54 +2,63 @@
|
|||||||
<div :class="classObj" class="app-wrapper">
|
<div :class="classObj" class="app-wrapper">
|
||||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||||
<sidebar class="sidebar-container" />
|
<sidebar class="sidebar-container" />
|
||||||
<div class="main-container">
|
<div :class="{hasTagsView:needTagsView}" class="main-container">
|
||||||
<div :class="{'fixed-header':fixedHeader}">
|
<div :class="{'fixed-header':fixedHeader}">
|
||||||
<navbar />
|
<navbar />
|
||||||
|
<tags-view v-if="needTagsView" />
|
||||||
</div>
|
</div>
|
||||||
<app-main />
|
<app-main />
|
||||||
|
<right-panel v-if="showSettings">
|
||||||
|
<settings />
|
||||||
|
</right-panel>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import { Navbar, Sidebar, AppMain } from './components'
|
|
||||||
import ResizeMixin from './mixin/ResizeHandler'
|
|
||||||
|
|
||||||
export default {
|
<script>
|
||||||
|
import {computed, defineComponent, onBeforeMount, onBeforeUnmount, onMounted, reactive, toRefs} from "vue";
|
||||||
|
import {Navbar, Sidebar, AppMain} from './components'
|
||||||
|
import {
|
||||||
|
sidebar,
|
||||||
|
device,
|
||||||
|
resizeMounted,
|
||||||
|
addEventListenerOnResize,
|
||||||
|
removeEventListenerResize,
|
||||||
|
watchRouter
|
||||||
|
} from './mixin/ResizeHandler'
|
||||||
|
import {useStore} from "@store";
|
||||||
|
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
name: 'Layout',
|
name: 'Layout',
|
||||||
components: {
|
components: {
|
||||||
Navbar,
|
Navbar,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
AppMain
|
AppMain
|
||||||
},
|
},
|
||||||
mixins: [ResizeMixin],
|
setup() {
|
||||||
computed: {
|
const store = useStore()
|
||||||
sidebar() {
|
|
||||||
return this.$store.state.app.sidebar
|
|
||||||
},
|
|
||||||
device() {
|
|
||||||
return this.$store.state.app.device
|
|
||||||
},
|
|
||||||
fixedHeader() {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
classObj() {
|
|
||||||
return {
|
|
||||||
hideSidebar: !this.sidebar.opened,
|
|
||||||
openSidebar: this.sidebar.opened,
|
|
||||||
withoutAnimation: this.sidebar.withoutAnimation,
|
|
||||||
mobile: this.device === 'mobile'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
handleClickOutside() {
|
|
||||||
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
handleClickOutside: () => {
|
||||||
|
store.dispatch('app/closeSideBar', false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const classObj = computed(() => {
|
||||||
|
return {
|
||||||
|
hideSidebar: !sidebar.opened,
|
||||||
|
openSidebar: sidebar.opened,
|
||||||
|
withoutAnimation: sidebar.withoutAnimation,
|
||||||
|
mobile: device === 'mobile'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "@styles/mixin.scss";
|
@import "@styles/mixin.scss";
|
||||||
@import "@styles/variables.scss";
|
@import "@styles/variables.scss";
|
||||||
@@ -59,11 +68,13 @@ export default {
|
|||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&.mobile.openSidebar {
|
&.mobile.openSidebar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer-bg {
|
.drawer-bg {
|
||||||
background: #000;
|
background: #000;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {userStore} from '@/store'
|
import {useStore} from '@/store'
|
||||||
import {computed, watch} from "vue";
|
import {computed, watch} from "vue";
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
const store = userStore()
|
const store = useStore()
|
||||||
|
|
||||||
const {body} = document
|
const {body} = document
|
||||||
const WIDTH = 992 // refer to Bootstrap's responsive design
|
const WIDTH = 992 // refer to Bootstrap's responsive design
|
||||||
|
|||||||
59
src/permission.ts
Normal file
59
src/permission.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import router from "@router";
|
||||||
|
import NProgress from 'nprogress';
|
||||||
|
import {Local} from "@utils/storage";
|
||||||
|
import {useStore} from "@store";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
|
||||||
|
NProgress.configure({showSpinner: false})
|
||||||
|
|
||||||
|
// 白名单
|
||||||
|
const whiteList = ['/login', '/auth-redirect']
|
||||||
|
|
||||||
|
router.beforeEach(async (to, form, next) => {
|
||||||
|
|
||||||
|
NProgress.start()
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const hasToken = Local.get(`token`)
|
||||||
|
if (hasToken) {
|
||||||
|
// 如果登录成功,跳转到首页
|
||||||
|
if (to.path === '/login') {
|
||||||
|
next({path: '/'})
|
||||||
|
NProgress.done()
|
||||||
|
} else {
|
||||||
|
const hasGetUserInfo = store.state.user.roles.length > 0
|
||||||
|
if (hasGetUserInfo) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await store.dispatch('user/getUserInfo')
|
||||||
|
const roles = store.state.user.roles
|
||||||
|
await store.dispatch('permission/generateRoutes', roles)
|
||||||
|
store.state.permission.addRoutes.forEach(route => {
|
||||||
|
router.addRoute(route)
|
||||||
|
})
|
||||||
|
next({...to, replace: true})
|
||||||
|
} catch (error) {
|
||||||
|
// remove token and go to login page to re-login
|
||||||
|
await store.dispatch('user/resetToken')
|
||||||
|
ElMessage.error(error as any || 'Has Error')
|
||||||
|
next(`/login?redirect=${to.path}`)
|
||||||
|
NProgress.done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* has no token*/
|
||||||
|
if (whiteList.indexOf(to.path) !== -1) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
next(`/login?redirect=${to.path}`)
|
||||||
|
NProgress.done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
NProgress.done()
|
||||||
|
})
|
||||||
@@ -1,13 +1,35 @@
|
|||||||
import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router'
|
import {createRouter, createWebHashHistory, RouteRecordRaw} from 'vue-router'
|
||||||
import Layout from '@/layout/index.vue'
|
import Layout from '@/layout/index.vue'
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
export const constantRoutes: Array<RouteRecordRaw> = [
|
||||||
|
{
|
||||||
|
path: '/redirect',
|
||||||
|
component: Layout,
|
||||||
|
meta: {hidden: true},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/redirect/:path(.*)',
|
||||||
|
component: () => import('@/views/redirect/index.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
component: () => import('@views/login/index.vue'),
|
component: () => import('@/views/login/index.vue'),
|
||||||
meta: {title: '登录'}
|
meta: {title: '登录'}
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
|
path: '/404',
|
||||||
|
component: () => import('@/views/error-page/404.vue'),
|
||||||
|
meta: {hidden: true}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/401',
|
||||||
|
component: () => import('@/views/error-page/401.vue'),
|
||||||
|
meta: {hidden: true}
|
||||||
|
},
|
||||||
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
redirect: '/dashboard',
|
redirect: '/dashboard',
|
||||||
@@ -22,9 +44,15 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(),
|
history: createWebHashHistory(),
|
||||||
routes: routes
|
routes: constantRoutes
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function resetRouter() {
|
||||||
|
const newRouter = router;
|
||||||
|
(router as any).matcher = (newRouter as any).matcher
|
||||||
|
}
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
20
src/settings.ts
Normal file
20
src/settings.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
interface DefaultSettings {
|
||||||
|
title: string,
|
||||||
|
showSettings: boolean,
|
||||||
|
tagsView: boolean,
|
||||||
|
fixedHeader: boolean,
|
||||||
|
sidebarLogo: boolean,
|
||||||
|
errorLog: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const defaultSettings: DefaultSettings = {
|
||||||
|
title: 'vue3-element-admin',
|
||||||
|
showSettings: false,
|
||||||
|
tagsView: true,
|
||||||
|
fixedHeader: false,
|
||||||
|
sidebarLogo: false,
|
||||||
|
errorLog: 'production'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultSettings
|
||||||
@@ -21,7 +21,7 @@ export const key: InjectionKey<Store<RootStateTypes>> = Symbol()
|
|||||||
|
|
||||||
export const store = createStore<RootStateTypes>({modules})
|
export const store = createStore<RootStateTypes>({modules})
|
||||||
|
|
||||||
export function userStore(){
|
export function useStore(){
|
||||||
return baseUseStore(key)
|
return baseUseStore(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import {RouteRecordRaw} from "vue-router";
|
||||||
|
|
||||||
// 接口类型声明
|
// 接口类型声明
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
token: string,
|
token: string,
|
||||||
@@ -16,8 +18,24 @@ export interface AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SettingState {
|
||||||
|
theme: string,
|
||||||
|
tagsView: boolean,
|
||||||
|
fixedHeader: boolean,
|
||||||
|
showSettings: boolean,
|
||||||
|
sidebarLogo: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface PermissionState{
|
||||||
|
routes:RouteRecordRaw[]
|
||||||
|
addRoutes: RouteRecordRaw[]
|
||||||
|
}
|
||||||
|
|
||||||
// 顶级类型声明
|
// 顶级类型声明
|
||||||
export interface RootStateTypes {
|
export interface RootStateTypes {
|
||||||
user: UserState,
|
user: UserState,
|
||||||
app:AppState
|
app: AppState,
|
||||||
|
setting: SettingState,
|
||||||
|
permission:PermissionState
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {Module} from "vuex";
|
import {Module} from "vuex";
|
||||||
import {RootStateTypes, AppState} from "@store/interface";
|
import {AppState,RootStateTypes} from "@store/interface";
|
||||||
import {Local} from "@utils/storage";
|
import {Local} from "@utils/storage";
|
||||||
|
|
||||||
const appModule: Module<AppState, RootStateTypes> = {
|
const appModule: Module<AppState, RootStateTypes> = {
|
||||||
|
|||||||
67
src/store/modules/permission.ts
Normal file
67
src/store/modules/permission.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {Module} from "vuex";
|
||||||
|
import {PermissionState, RootStateTypes} from "@store/interface";
|
||||||
|
import {RouteRecordRaw} from 'vue-router'
|
||||||
|
import {constantRoutes} from '@/router'
|
||||||
|
import {getRouteList} from "@api/system/menu";
|
||||||
|
|
||||||
|
|
||||||
|
const hasPermission = (roles: string[], route: RouteRecordRaw) => {
|
||||||
|
// 超级管理员放行
|
||||||
|
if (roles.includes('ROOT')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (route.meta && route.meta.roles) {
|
||||||
|
return roles.some(role => {
|
||||||
|
if (route.meta?.roles !== undefined) {
|
||||||
|
return (route.meta.roles as string[]).includes(role);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
|
||||||
|
const res: RouteRecordRaw[] = []
|
||||||
|
routes.forEach(route => {
|
||||||
|
const tmp = {...route}
|
||||||
|
if (hasPermission(roles, tmp)) {
|
||||||
|
if (tmp.children) {
|
||||||
|
tmp.children = filterAsyncRoutes(tmp.children, roles)
|
||||||
|
}
|
||||||
|
res.push(tmp)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const permissionModule: Module<PermissionState, RootStateTypes> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
routes: [],
|
||||||
|
addRoutes: []
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
SET_ROUTES: (state: PermissionState, routes: RouteRecordRaw[]) => {
|
||||||
|
state.addRoutes = routes
|
||||||
|
state.routes = constantRoutes.concat(routes)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
generateRoutes({commit}, roles: string[]) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getRouteList().then(response => {
|
||||||
|
const asyncRoutes = response.data
|
||||||
|
let accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
|
||||||
|
commit('SET_ROUTES', accessedRoutes)
|
||||||
|
resolve(accessedRoutes)
|
||||||
|
}).catch(error => {
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default permissionModule;
|
||||||
49
src/store/modules/settings.ts
Normal file
49
src/store/modules/settings.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import {Module} from "vuex";
|
||||||
|
import {SettingState, RootStateTypes} from "@store/interface";
|
||||||
|
import defaultSettings from '../../settings'
|
||||||
|
|
||||||
|
const {showSettings, tagsView, fixedHeader, sidebarLogo} = defaultSettings
|
||||||
|
|
||||||
|
const settingModule: Module<SettingState, RootStateTypes> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
theme: '',
|
||||||
|
showSettings: showSettings,
|
||||||
|
tagsView: tagsView,
|
||||||
|
fixedHeader: fixedHeader,
|
||||||
|
sidebarLogo: sidebarLogo,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
CHANGE_SETTING: (state: SettingState, payload: { key: string, value: any }) => {
|
||||||
|
const {key, value} = payload
|
||||||
|
switch (key) {
|
||||||
|
case 'theme':
|
||||||
|
state.theme = value
|
||||||
|
break
|
||||||
|
case 'showSettings':
|
||||||
|
state.showSettings = value
|
||||||
|
break
|
||||||
|
case 'fixedHeader':
|
||||||
|
state.fixedHeader = value
|
||||||
|
break
|
||||||
|
case 'tagsView':
|
||||||
|
state.tagsView = value
|
||||||
|
break
|
||||||
|
case 'sidebarLogo':
|
||||||
|
state.sidebarLogo = value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
changeSetting({commit}, data) {
|
||||||
|
commit('CHANGE_SETTING', data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default settingModule;
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import {Module} from "vuex";
|
import {Module} from "vuex";
|
||||||
import {UserState, RootStateTypes} from "@store/interface";
|
import {UserState, RootStateTypes} from "@store/interface";
|
||||||
import {Local} from "@utils/storage";
|
import {Local} from "@utils/storage";
|
||||||
import {getUserInfo, login, logout} from "@api/login"
|
import {getUserInfo, login, logout} from "@api/login";
|
||||||
|
import {resetRouter} from "@router";
|
||||||
|
|
||||||
|
|
||||||
const getDefaultState = () => {
|
const getDefaultState = () => {
|
||||||
return {
|
return {
|
||||||
@@ -107,13 +109,24 @@ const userModule: Module<UserState, RootStateTypes> = {
|
|||||||
logout().then(() => {
|
logout().then(() => {
|
||||||
Local.remove('token')
|
Local.remove('token')
|
||||||
commit('RESET_STATE')
|
commit('RESET_STATE')
|
||||||
|
resetRouter()
|
||||||
|
resolve(null)
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除 Token
|
||||||
|
*/
|
||||||
|
resetToken({commit}){
|
||||||
|
return new Promise(resolve=>{
|
||||||
|
Local.remove('token')
|
||||||
|
commit('RESET_STATE')
|
||||||
|
resolve(null)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
99
src/views/error-page/401.vue
Normal file
99
src/views/error-page/401.vue
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div class="errPage-container">
|
||||||
|
<el-button icon="el-icon-arrow-left" class="pan-back-btn" @click="back">
|
||||||
|
返回
|
||||||
|
</el-button>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="12">
|
||||||
|
<h1 class="text-jumbo text-ginormous">
|
||||||
|
Oops!
|
||||||
|
</h1>
|
||||||
|
gif来源<a href="https://zh.airbnb.com/" target="_blank">airbnb</a> 页面
|
||||||
|
<h2>你没有权限去该页面</h2>
|
||||||
|
<h6>如有不满请联系你领导</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>或者你可以去:</li>
|
||||||
|
<li class="link-type">
|
||||||
|
<router-link to="/dashboard">
|
||||||
|
回首页
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<li class="link-type">
|
||||||
|
<a href="https://www.taobao.com/">随便看看</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="#" @click.prevent="dialogVisible=true">点我看图</a></li>
|
||||||
|
</ul>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-dialog :visible.sync="dialogVisible" title="随便看">
|
||||||
|
<img :src="ewizardClap" class="pan-img">
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import errGif from '@/assets/401_images/401.gif'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Page401',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
errGif: errGif + '?' + +new Date(),
|
||||||
|
ewizardClap: 'https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646',
|
||||||
|
dialogVisible: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
back() {
|
||||||
|
if (this.$route.query.noGoBack) {
|
||||||
|
this.$router.push({ path: '/dashboard' })
|
||||||
|
} else {
|
||||||
|
this.$router.go(-1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.errPage-container {
|
||||||
|
width: 800px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 100px auto;
|
||||||
|
.pan-back-btn {
|
||||||
|
background: #008489;
|
||||||
|
color: #fff;
|
||||||
|
border: none!important;
|
||||||
|
}
|
||||||
|
.pan-gif {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.pan-img {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.text-jumbo {
|
||||||
|
font-size: 60px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #484848;
|
||||||
|
}
|
||||||
|
.list-unstyled {
|
||||||
|
font-size: 14px;
|
||||||
|
li {
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #008489;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
228
src/views/error-page/404.vue
Normal file
228
src/views/error-page/404.vue
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wscn-http404-container">
|
||||||
|
<div class="wscn-http404">
|
||||||
|
<div class="pic-404">
|
||||||
|
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
|
||||||
|
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||||
|
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||||
|
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
|
||||||
|
</div>
|
||||||
|
<div class="bullshit">
|
||||||
|
<div class="bullshit__oops">OOPS!</div>
|
||||||
|
<div class="bullshit__info">All rights reserved
|
||||||
|
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
|
||||||
|
</div>
|
||||||
|
<div class="bullshit__headline">{{ message }}</div>
|
||||||
|
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
|
||||||
|
<a href="" class="bullshit__return-home">Back to home</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Page404',
|
||||||
|
computed: {
|
||||||
|
message() {
|
||||||
|
return 'The webmaster said that you can not enter this page...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.wscn-http404-container{
|
||||||
|
transform: translate(-50%,-50%);
|
||||||
|
position: absolute;
|
||||||
|
top: 40%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
.wscn-http404 {
|
||||||
|
position: relative;
|
||||||
|
width: 1200px;
|
||||||
|
padding: 0 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
.pic-404 {
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
width: 600px;
|
||||||
|
overflow: hidden;
|
||||||
|
&__parent {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
&__child {
|
||||||
|
position: absolute;
|
||||||
|
&.left {
|
||||||
|
width: 80px;
|
||||||
|
top: 17px;
|
||||||
|
left: 220px;
|
||||||
|
opacity: 0;
|
||||||
|
animation-name: cloudLeft;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
&.mid {
|
||||||
|
width: 46px;
|
||||||
|
top: 10px;
|
||||||
|
left: 420px;
|
||||||
|
opacity: 0;
|
||||||
|
animation-name: cloudMid;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
animation-delay: 1.2s;
|
||||||
|
}
|
||||||
|
&.right {
|
||||||
|
width: 62px;
|
||||||
|
top: 100px;
|
||||||
|
left: 500px;
|
||||||
|
opacity: 0;
|
||||||
|
animation-name: cloudRight;
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-timing-function: linear;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
@keyframes cloudLeft {
|
||||||
|
0% {
|
||||||
|
top: 17px;
|
||||||
|
left: 220px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
top: 33px;
|
||||||
|
left: 188px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
top: 81px;
|
||||||
|
left: 92px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 97px;
|
||||||
|
left: 60px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes cloudMid {
|
||||||
|
0% {
|
||||||
|
top: 10px;
|
||||||
|
left: 420px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
top: 40px;
|
||||||
|
left: 360px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
top: 130px;
|
||||||
|
left: 180px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 160px;
|
||||||
|
left: 120px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes cloudRight {
|
||||||
|
0% {
|
||||||
|
top: 100px;
|
||||||
|
left: 500px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
top: 120px;
|
||||||
|
left: 460px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
top: 180px;
|
||||||
|
left: 340px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 200px;
|
||||||
|
left: 300px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.bullshit {
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
width: 300px;
|
||||||
|
padding: 30px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
&__oops {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 40px;
|
||||||
|
color: #1482f0;
|
||||||
|
opacity: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation-name: slideUp;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
&__headline {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 24px;
|
||||||
|
color: #222;
|
||||||
|
font-weight: bold;
|
||||||
|
opacity: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
animation-name: slideUp;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
&__info {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 21px;
|
||||||
|
color: grey;
|
||||||
|
opacity: 0;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
animation-name: slideUp;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
&__return-home {
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
width: 110px;
|
||||||
|
height: 36px;
|
||||||
|
background: #1482f0;
|
||||||
|
border-radius: 100px;
|
||||||
|
text-align: center;
|
||||||
|
color: #ffffff;
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 36px;
|
||||||
|
cursor: pointer;
|
||||||
|
animation-name: slideUp;
|
||||||
|
animation-duration: 0.5s;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
}
|
||||||
|
@keyframes slideUp {
|
||||||
|
0% {
|
||||||
|
transform: translateY(60px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
15
src/views/redirect/index.vue
Normal file
15
src/views/redirect/index.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent} from "vue";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
created() {
|
||||||
|
const {params, query} = useRoute()
|
||||||
|
const {path} = params
|
||||||
|
useRouter().replace({path: '/' + path, query})
|
||||||
|
},
|
||||||
|
render(h) {
|
||||||
|
return h()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user