mirror of
https://github.com/un-pany/v3-admin-vite.git
synced 2025-04-21 03:19:19 +08:00
refactor: 重构为更推荐的 vue3 代码组织方式
This commit is contained in:
parent
a7297af892
commit
19b377651c
23
README.md
23
README.md
@ -2,9 +2,9 @@
|
||||
|
||||
一个中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element-Plus、Pinia 和 Vite.
|
||||
|
||||
模板代码是从 [v3-admin v3.1.3](https://github.com/un-pany/v3-admin) 迁移而来,只是脚手架从 vue-cli 5.x 切换到了 vite,并作了一些繁琐我适配.
|
||||
模板代码是从 [v3-admin v3.1.3](https://github.com/un-pany/v3-admin) 迁移而来,只是脚手架从 vue-cli 5.x 切换到了 vite,并作了一些繁琐的适配.
|
||||
|
||||
现在还是预发布 **3.1.3-rc2** 版本,正在努力重构中,等待正式版(v3.1.3)发布.
|
||||
现在还是预发布 **3.1.3-rc3** 版本,正在努力重构中,等待 vite 版的 v3.1.3 发布.
|
||||
|
||||
文档暂无,可以先用到 v3-admin 的文档,基本上是适用的.
|
||||
|
||||
@ -16,6 +16,25 @@
|
||||
- pnpm 版本 6.x
|
||||
- 安装依赖: pnpm i
|
||||
- 运行项目: pnpm dev
|
||||
- 预览测试环境: preview:stage
|
||||
- 预览正式环境: preview:prod
|
||||
- 打包测试环境: pnpm build:stage
|
||||
- 打包正式环境: pnpm build:prod
|
||||
- 代码检测: pnpm lint
|
||||
|
||||
## Git 提交规范
|
||||
|
||||
- `feat` 增加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关无影响运行结果的
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 重构
|
||||
- `revert` 撤销修改
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `workflow` 工作流改进
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件更改
|
||||
- `wip` 开发中
|
||||
- `mod` 不确定分类的修改
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "v3-admin-vite",
|
||||
"private": true,
|
||||
"version": "3.1.3-rc2",
|
||||
"version": "3.1.3-rc3",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:stage": "vue-tsc --noEmit && vite build --mode staging",
|
||||
|
19
src/App.vue
19
src/App.vue
@ -1,15 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { ElConfigProvider } from "element-plus"
|
||||
import zhCn from "element-plus/lib/locale/lang/zh-cn"
|
||||
|
||||
useAppStore().initTheme() // 初始化 theme
|
||||
const locale = zhCn // element-plus 设置为中文
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElConfigProvider :locale="locale">
|
||||
<router-view />
|
||||
</ElConfigProvider>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ElConfigProvider } from "element-plus"
|
||||
import zhCn from "element-plus/lib/locale/lang/zh-cn"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
|
||||
const locale = zhCn // element-plus 设置为中文
|
||||
|
||||
useAppStore().initTheme() // 初始化 Theme
|
||||
</script>
|
||||
|
@ -1,16 +1,6 @@
|
||||
<template>
|
||||
<div @click="click">
|
||||
<el-tooltip effect="dark" content="全屏" placement="bottom">
|
||||
<el-icon :size="20">
|
||||
<full-screen />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { FullScreen } from "@element-plus/icons-vue"
|
||||
import { ElMessage } from "element-plus"
|
||||
import { FullScreen } from "@element-plus/icons-vue"
|
||||
import screenfull from "screenfull"
|
||||
|
||||
const click = () => {
|
||||
@ -21,3 +11,13 @@ const click = () => {
|
||||
screenfull.toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click="click">
|
||||
<el-tooltip effect="dark" content="全屏" placement="bottom">
|
||||
<el-icon :size="20">
|
||||
<FullScreen />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,9 +1,3 @@
|
||||
<template>
|
||||
<svg class="svg-icon" aria-hidden="true">
|
||||
<use :href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
|
||||
@ -21,6 +15,12 @@ const props = defineProps({
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg class="svg-icon" aria-hidden="true">
|
||||
<use :href="symbolId" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
|
@ -1,8 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { MagicStick } from "@element-plus/icons-vue"
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const themeList = computed(() => {
|
||||
return appStore.themeList
|
||||
})
|
||||
const activeThemeName = computed(() => {
|
||||
return appStore.activeThemeName
|
||||
})
|
||||
const handleSetTheme = (name: string) => {
|
||||
appStore.setTheme(name)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dropdown trigger="click" @command="handleSetTheme">
|
||||
<el-tooltip effect="dark" content="主题模式" placement="bottom">
|
||||
<el-icon :size="20">
|
||||
<magic-stick />
|
||||
<MagicStick />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<template #dropdown>
|
||||
@ -19,20 +37,3 @@
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { MagicStick } from "@element-plus/icons-vue"
|
||||
import { computed } from "vue"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
|
||||
const appStore = useAppStore()
|
||||
const themeList = computed(() => {
|
||||
return appStore.themeList
|
||||
})
|
||||
const activeThemeName = computed(() => {
|
||||
return appStore.activeThemeName
|
||||
})
|
||||
const handleSetTheme = (name: string) => {
|
||||
appStore.setTheme(name)
|
||||
}
|
||||
</script>
|
||||
|
21
src/config/async-route.ts
Normal file
21
src/config/async-route.ts
Normal file
@ -0,0 +1,21 @@
|
||||
/** 动态路由配置 */
|
||||
interface IAsyncRouteSettings {
|
||||
/**
|
||||
* 是否开启动态路由功能?
|
||||
* 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
|
||||
* 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 open: false
|
||||
*/
|
||||
open: boolean
|
||||
/** 当动态路由功能关闭时:
|
||||
* 1. 应该将所有路由都写到常驻路由里面(表明所有登陆的用户能访问的页面都是一样的)
|
||||
* 2. 系统自动给当前登录用户赋值一个默认的角色(默认为 admin,拥有所有页面权限)
|
||||
*/
|
||||
defaultRoles: Array<string>
|
||||
}
|
||||
|
||||
const asyncRouteSettings: IAsyncRouteSettings = {
|
||||
open: true,
|
||||
defaultRoles: ["admin"]
|
||||
}
|
||||
|
||||
export default asyncRouteSettings
|
@ -1,14 +0,0 @@
|
||||
/** 角色配置 */
|
||||
interface IRolesSettings {
|
||||
/** 是否开启角色功能(开启后需要后端配合,在查询用户详情接口返回当前用户的所属角色) */
|
||||
openRoles: boolean
|
||||
/** 当角色功能关闭时,当前登录用户的默认角色将生效(默认为admin,拥有所有权限) */
|
||||
defaultRoles: Array<string>
|
||||
}
|
||||
|
||||
const rolesSettings: IRolesSettings = {
|
||||
openRoles: true,
|
||||
defaultRoles: ["admin"]
|
||||
}
|
||||
|
||||
export default rolesSettings
|
@ -1,8 +1,7 @@
|
||||
class Keys {
|
||||
static sidebarStatus = "v3-admin-sidebar-status-key"
|
||||
static language = "v3-admin-language-key"
|
||||
static token = "v3-admin-token-key"
|
||||
static activeThemeName = "v3-admin-active-theme-name"
|
||||
static sidebarStatus = "v3-admin-vite-sidebar-status-key"
|
||||
static token = "v3-admin-vite-token-key"
|
||||
static activeThemeName = "v3-admin-vite-active-theme-name-key"
|
||||
}
|
||||
|
||||
export default Keys
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useUserStoreHook } from "@/store/modules/user"
|
||||
import { Directive } from "vue"
|
||||
import { useUserStoreHook } from "@/store/modules/user"
|
||||
|
||||
/** 权限指令 */
|
||||
export const permission: Directive = {
|
||||
|
@ -1,4 +1,13 @@
|
||||
<!-- 主视图 -->
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
const route = useRoute()
|
||||
const key = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view v-slot="{ Component }">
|
||||
@ -11,16 +20,6 @@
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
|
||||
const route = useRoute()
|
||||
const key = computed(() => {
|
||||
return route.path
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
/* 50 = navbar height */
|
||||
|
@ -1,28 +1,13 @@
|
||||
<!-- 面包屑组件 -->
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in state.breadcrumbs" :key="item.path">
|
||||
<span v-if="item.redirect === 'noRedirect' || index === state.breadcrumbs.length - 1" class="no-redirect">{{
|
||||
item.meta.title
|
||||
}}</span>
|
||||
<a v-else @click.prevent="state.handleLink(item)">
|
||||
{{ item.meta.title }}
|
||||
</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, reactive, watch } from "vue"
|
||||
import { useRoute, useRouter, RouteLocationMatched } from "vue-router"
|
||||
import { compile } from "path-to-regexp"
|
||||
|
||||
const currentRoute = useRoute()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const pathCompile = (path: string) => {
|
||||
const { params } = currentRoute
|
||||
const { params } = route
|
||||
const toPath = compile(path)
|
||||
return toPath(params)
|
||||
}
|
||||
@ -30,7 +15,7 @@ const pathCompile = (path: string) => {
|
||||
const state = reactive({
|
||||
breadcrumbs: [] as Array<RouteLocationMatched>,
|
||||
getBreadcrumb: () => {
|
||||
const matched = currentRoute.matched.filter((item) => item.meta && item.meta.title)
|
||||
const matched = route.matched.filter((item) => item.meta && item.meta.title)
|
||||
state.breadcrumbs = matched.filter((item) => {
|
||||
return item.meta && item.meta.title && item.meta.breadcrumb !== false
|
||||
})
|
||||
@ -50,7 +35,7 @@ const state = reactive({
|
||||
})
|
||||
|
||||
watch(
|
||||
() => currentRoute.path,
|
||||
() => route.path,
|
||||
(path) => {
|
||||
if (path.startsWith("/redirect/")) {
|
||||
return
|
||||
@ -64,6 +49,21 @@ onBeforeMount(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-breadcrumb class="app-breadcrumb">
|
||||
<transition-group name="breadcrumb">
|
||||
<el-breadcrumb-item v-for="(item, index) in state.breadcrumbs" :key="item.path">
|
||||
<span v-if="item.redirect === 'noRedirect' || index === state.breadcrumbs.length - 1" class="no-redirect">{{
|
||||
item.meta.title
|
||||
}}</span>
|
||||
<a v-else @click.prevent="state.handleLink(item)">
|
||||
{{ item.meta.title }}
|
||||
</a>
|
||||
</el-breadcrumb-item>
|
||||
</transition-group>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-breadcrumb__inner,
|
||||
.el-breadcrumb__inner a {
|
||||
|
@ -1,13 +1,3 @@
|
||||
<!-- 折叠边栏按钮 -->
|
||||
<template>
|
||||
<div @click="toggleClick">
|
||||
<el-icon :size="20" class="icon">
|
||||
<fold v-if="isActive" />
|
||||
<expand v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Expand, Fold } from "@element-plus/icons-vue"
|
||||
|
||||
@ -25,6 +15,15 @@ const toggleClick = () => {
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div @click="toggleClick">
|
||||
<el-icon :size="20" class="icon">
|
||||
<fold v-if="isActive" />
|
||||
<expand v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
|
@ -1,4 +1,43 @@
|
||||
<!-- 导航栏 -->
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import { UserFilled } from "@element-plus/icons-vue"
|
||||
import BreadCrumb from "../BreadCrumb/index.vue"
|
||||
import Hamburger from "../Hamburger/index.vue"
|
||||
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
|
||||
import Screenfull from "@/components/Screenfull/index.vue"
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const sidebar = computed(() => {
|
||||
return appStore.sidebar
|
||||
})
|
||||
const showThemeSwitch = computed(() => {
|
||||
return settingsStore.showThemeSwitch
|
||||
})
|
||||
const showScreenfull = computed(() => {
|
||||
return settingsStore.showScreenfull
|
||||
})
|
||||
|
||||
const state = reactive({
|
||||
toggleSideBar: () => {
|
||||
appStore.toggleSidebar(false)
|
||||
},
|
||||
logout: () => {
|
||||
userStore.logout()
|
||||
router.push("/login").catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<Hamburger :is-active="sidebar.opened" class="hamburger" @toggle-click="state.toggleSideBar" />
|
||||
@ -35,44 +74,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from "@element-plus/icons-vue"
|
||||
import { computed, reactive } from "vue"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import BreadCrumb from "../BreadCrumb/index.vue"
|
||||
import Hamburger from "../Hamburger/index.vue"
|
||||
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
|
||||
import Screenfull from "@/components/Screenfull/index.vue"
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const userStore = useUserStore()
|
||||
const sidebar = computed(() => {
|
||||
return appStore.sidebar
|
||||
})
|
||||
const showThemeSwitch = computed(() => {
|
||||
return settingsStore.showThemeSwitch
|
||||
})
|
||||
const showScreenfull = computed(() => {
|
||||
return settingsStore.showScreenfull
|
||||
})
|
||||
const state = reactive({
|
||||
toggleSideBar: () => {
|
||||
appStore.toggleSidebar(false)
|
||||
},
|
||||
logout: () => {
|
||||
userStore.logout()
|
||||
router.push("/login").catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 50px;
|
||||
|
@ -1,15 +1,3 @@
|
||||
<!-- 右侧悬浮设置面板 -->
|
||||
<template>
|
||||
<div class="handle-button" :style="{ top: buttonTop + 'px' }" @click="show = true">
|
||||
<el-icon :size="24">
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-drawer v-model="show" size="300px" :with-header="false">
|
||||
<slot />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { Setting } from "@element-plus/icons-vue"
|
||||
@ -24,6 +12,17 @@ defineProps({
|
||||
const show = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="handle-button" :style="{ top: buttonTop + 'px' }" @click="show = true">
|
||||
<el-icon :size="24">
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-drawer v-model="show" size="300px" :with-header="false">
|
||||
<slot />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.handle-button {
|
||||
width: 48px;
|
||||
|
@ -1,4 +1,64 @@
|
||||
<!-- 设置页面 -->
|
||||
<script lang="ts" setup>
|
||||
import { reactive, watch } from "vue"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const state = reactive({
|
||||
fixedHeader: settingsStore.fixedHeader,
|
||||
showTagsView: settingsStore.showTagsView,
|
||||
showSidebarLogo: settingsStore.showSidebarLogo,
|
||||
showThemeSwitch: settingsStore.showThemeSwitch,
|
||||
showScreenfull: settingsStore.showScreenfull
|
||||
})
|
||||
|
||||
watch(
|
||||
() => state.fixedHeader,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "fixedHeader",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => state.showTagsView,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showTagsView",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => state.showSidebarLogo,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showSidebarLogo",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => state.showThemeSwitch,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showThemeSwitch",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
watch(
|
||||
() => state.showScreenfull,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showScreenfull",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="drawer-container">
|
||||
<div>
|
||||
@ -27,71 +87,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { reactive, watch } from "vue"
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const state = reactive({
|
||||
fixedHeader: settingsStore.fixedHeader,
|
||||
showTagsView: settingsStore.showTagsView,
|
||||
showSidebarLogo: settingsStore.showSidebarLogo,
|
||||
showThemeSwitch: settingsStore.showThemeSwitch,
|
||||
showScreenfull: settingsStore.showScreenfull
|
||||
})
|
||||
|
||||
watch(
|
||||
() => state.fixedHeader,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "fixedHeader",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.showTagsView,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showTagsView",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.showSidebarLogo,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showSidebarLogo",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.showThemeSwitch,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showThemeSwitch",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => state.showScreenfull,
|
||||
(value) => {
|
||||
settingsStore.changeSetting({
|
||||
key: "showScreenfull",
|
||||
value
|
||||
})
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drawer-container {
|
||||
padding: 24px;
|
||||
|
@ -1,4 +1,68 @@
|
||||
<!-- 侧边栏 Item -->
|
||||
<script lang="ts" setup>
|
||||
import { computed, PropType } from "vue"
|
||||
import { RouteRecordRaw } from "vue-router"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
import path from "path-browserify"
|
||||
import SidebarItemLink from "./SidebarItemLink.vue"
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as PropType<RouteRecordRaw>,
|
||||
required: true
|
||||
},
|
||||
isCollapse: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
isFirstLevel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const alwaysShowRootMenu = computed(() => {
|
||||
return !!(props.item.meta && props.item.meta.alwaysShow)
|
||||
})
|
||||
const showingChildNumber = computed(() => {
|
||||
if (props.item.children) {
|
||||
const showingChildren = props.item.children.filter((item) => {
|
||||
return !(item.meta && item.meta.hidden)
|
||||
})
|
||||
return showingChildren.length
|
||||
}
|
||||
return 0
|
||||
})
|
||||
const theOnlyOneChild = computed(() => {
|
||||
if (showingChildNumber.value > 1) {
|
||||
return null
|
||||
}
|
||||
if (props.item.children) {
|
||||
for (const child of props.item.children) {
|
||||
if (!child.meta || !child.meta.hidden) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there is no children, return itself with path removed,
|
||||
// because this.basePath already contains item's path information
|
||||
return { ...props.item, path: "" }
|
||||
})
|
||||
|
||||
const resolvePath = (routePath: string) => {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(props.basePath)) {
|
||||
return props.basePath
|
||||
}
|
||||
return path.resolve(props.basePath, routePath)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!item.meta || !item.meta.hidden" :class="{ 'simple-mode': isCollapse, 'first-level': isFirstLevel }">
|
||||
<template v-if="!alwaysShowRootMenu && theOnlyOneChild && !theOnlyOneChild.children">
|
||||
@ -30,72 +94,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import path from "path-browserify"
|
||||
import { computed, PropType } from "vue"
|
||||
import { RouteRecordRaw } from "vue-router"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
import SidebarItemLink from "./SidebarItemLink.vue"
|
||||
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object as PropType<RouteRecordRaw>,
|
||||
required: true
|
||||
},
|
||||
isCollapse: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
isFirstLevel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const alwaysShowRootMenu = computed(() => {
|
||||
return !!(props.item.meta && props.item.meta.alwaysShow)
|
||||
})
|
||||
|
||||
const showingChildNumber = computed(() => {
|
||||
if (props.item.children) {
|
||||
const showingChildren = props.item.children.filter((item) => {
|
||||
return !(item.meta && item.meta.hidden)
|
||||
})
|
||||
return showingChildren.length
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const theOnlyOneChild = computed(() => {
|
||||
if (showingChildNumber.value > 1) {
|
||||
return null
|
||||
}
|
||||
if (props.item.children) {
|
||||
for (const child of props.item.children) {
|
||||
if (!child.meta || !child.meta.hidden) {
|
||||
return child
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there is no children, return itself with path removed,
|
||||
// because this.basePath already contains item's path information
|
||||
return { ...props.item, path: "" }
|
||||
})
|
||||
|
||||
const resolvePath = (routePath: string) => {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(props.basePath)) {
|
||||
return props.basePath
|
||||
}
|
||||
return path.resolve(props.basePath, routePath)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.svg-icon {
|
||||
margin-right: 20px;
|
||||
|
@ -1,15 +1,6 @@
|
||||
<template>
|
||||
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
|
||||
<slot />
|
||||
</a>
|
||||
<div v-else @click="push">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isExternal } from "@/utils/validate"
|
||||
import { useRouter } from "vue-router"
|
||||
import { isExternal } from "@/utils/validate"
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
@ -19,9 +10,19 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const push = () => {
|
||||
router.push(props.to).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
|
||||
<slot />
|
||||
</a>
|
||||
<div v-else @click="push">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,4 +1,12 @@
|
||||
<!-- 侧边栏 Logo(需要跟随侧边栏折叠) -->
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sidebar-logo-container" :class="{ collapse: collapse }">
|
||||
<transition name="sidebarLogoFade">
|
||||
@ -12,15 +20,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebarLogoFade-enter-active,
|
||||
.sidebarLogoFade-leave-active {
|
||||
|
@ -1,3 +1,37 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import SidebarItem from "./SidebarItem.vue"
|
||||
import SidebarLogo from "./SidebarLogo.vue"
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const sidebar = computed(() => {
|
||||
return useAppStore().sidebar
|
||||
})
|
||||
const routes = computed(() => {
|
||||
return usePermissionStore().routes
|
||||
})
|
||||
const showLogo = computed(() => {
|
||||
return useSettingsStore().showSidebarLogo
|
||||
})
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route
|
||||
if (meta !== null || meta !== undefined) {
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
}
|
||||
return path
|
||||
})
|
||||
const isCollapse = computed(() => {
|
||||
return !sidebar.value.opened
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'has-logo': showLogo }">
|
||||
<SidebarLogo v-if="showLogo" :collapse="isCollapse" />
|
||||
@ -23,39 +57,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { useAppStore } from "@/store/modules/app"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import SidebarItem from "./SidebarItem.vue"
|
||||
import SidebarLogo from "./SidebarLogo.vue"
|
||||
|
||||
const route = useRoute()
|
||||
const sidebar = computed(() => {
|
||||
return useAppStore().sidebar
|
||||
})
|
||||
const routes = computed(() => {
|
||||
return usePermissionStore().routes
|
||||
})
|
||||
const showLogo = computed(() => {
|
||||
return useSettingsStore().showSidebarLogo
|
||||
})
|
||||
const activeMenu = computed(() => {
|
||||
const { meta, path } = route
|
||||
if (meta !== null || meta !== undefined) {
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu
|
||||
}
|
||||
}
|
||||
return path
|
||||
})
|
||||
const isCollapse = computed(() => {
|
||||
return !sidebar.value.opened
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.sidebar-container {
|
||||
// 重置当前页面的 element-plus css, ,注意,虽然没有加 scoped 标识,但是被该页面的 sidebar-container 类名包裹,所以不会影响其他页面
|
||||
|
@ -1,46 +1,17 @@
|
||||
<template>
|
||||
<div class="tags-view-container">
|
||||
<ScrollPane class="tags-view-wrapper">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
ref="tag"
|
||||
:key="tag.path"
|
||||
:class="state.isActive(tag) ? 'active' : ''"
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
class="tags-view-item"
|
||||
@click.middle="!state.isAffix(tag) ? state.closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="state.openMenu(tag, $event)"
|
||||
>
|
||||
{{ tag.meta?.title }}
|
||||
<el-icon v-if="!state.isAffix(tag)" :size="12" @click.prevent.stop="state.closeSelectedTag(tag)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</router-link>
|
||||
</ScrollPane>
|
||||
<ul v-show="state.visible" :style="{ left: state.left + 'px', top: state.top + 'px' }" class="contextmenu">
|
||||
<li @click="state.refreshSelectedTag(state.selectedTag)">刷新</li>
|
||||
<li v-if="!state.isAffix(state.selectedTag)" @click="state.closeSelectedTag(state.selectedTag)">关闭</li>
|
||||
<li @click="state.closeOthersTags">关闭其它</li>
|
||||
<li @click="state.closeAllTags(state.selectedTag)">关闭所有</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import path from "path-browserify"
|
||||
import { useTagsViewStore, ITagView } from "@/store/modules/tags-view"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import { computed, getCurrentInstance, nextTick, onBeforeMount, reactive, watch } from "vue"
|
||||
import { RouteRecordRaw, useRoute, useRouter } from "vue-router"
|
||||
import ScrollPane from "./ScrollPane.vue"
|
||||
import { useTagsViewStore, ITagView } from "@/store/modules/tags-view"
|
||||
import { usePermissionStore } from "@/store/modules/permission"
|
||||
import { Close } from "@element-plus/icons-vue"
|
||||
import path from "path-browserify"
|
||||
import ScrollPane from "./ScrollPane.vue"
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
const permissionStore = usePermissionStore()
|
||||
const router = useRouter()
|
||||
const instance = getCurrentInstance()
|
||||
const currentRoute = useRoute()
|
||||
const { proxy } = instance as any
|
||||
|
||||
const toLastView = (visitedViews: ITagView[], view: ITagView) => {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
@ -70,7 +41,7 @@ const state = reactive({
|
||||
selectedTag: {} as ITagView,
|
||||
affixTags: [] as ITagView[],
|
||||
isActive: (route: ITagView) => {
|
||||
return route.path === currentRoute.path
|
||||
return route.path === route.path
|
||||
},
|
||||
isAffix: (tag: ITagView) => {
|
||||
return tag.meta && tag.meta.affix
|
||||
@ -90,7 +61,7 @@ const state = reactive({
|
||||
}
|
||||
},
|
||||
closeOthersTags: () => {
|
||||
if (state.selectedTag.fullPath !== currentRoute.path && state.selectedTag.fullPath !== undefined) {
|
||||
if (state.selectedTag.fullPath !== route.path && state.selectedTag.fullPath !== undefined) {
|
||||
router.push(state.selectedTag.fullPath).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
@ -99,15 +70,15 @@ const state = reactive({
|
||||
},
|
||||
closeAllTags: (view: ITagView) => {
|
||||
tagsViewStore.delAllVisitedViews()
|
||||
if (state.affixTags.some((tag) => tag.path === currentRoute.path)) {
|
||||
if (state.affixTags.some((tag) => tag.path === route.path)) {
|
||||
return
|
||||
}
|
||||
toLastView(tagsViewStore.visitedViews, view)
|
||||
},
|
||||
openMenu: (tag: ITagView, e: MouseEvent) => {
|
||||
const menuMinWidth = 105
|
||||
const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
|
||||
const offsetWidth = proxy.$el.offsetWidth // container width
|
||||
const offsetLeft = instance!.proxy!.$el.getBoundingClientRect().left // container margin left
|
||||
const offsetWidth = instance!.proxy!.$el.offsetWidth // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
||||
const left = e.clientX - offsetLeft + 15 // 15: margin right
|
||||
if (left > maxLeft) {
|
||||
@ -131,7 +102,6 @@ const routes = computed(() => permissionStore.routes)
|
||||
|
||||
const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
||||
let tags: ITagView[] = []
|
||||
|
||||
routes.forEach((route) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path)
|
||||
@ -142,7 +112,6 @@ const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
||||
meta: { ...route.meta }
|
||||
})
|
||||
}
|
||||
|
||||
if (route.children) {
|
||||
const childTags = filterAffixTags(route.children, route.path)
|
||||
if (childTags.length >= 1) {
|
||||
@ -164,8 +133,8 @@ const initTags = () => {
|
||||
}
|
||||
|
||||
const addTags = () => {
|
||||
if (currentRoute.name) {
|
||||
tagsViewStore.addVisitedView(currentRoute)
|
||||
if (route.name) {
|
||||
tagsViewStore.addVisitedView(route)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -176,17 +145,17 @@ const moveToCurrentTag = () => {
|
||||
return
|
||||
}
|
||||
for (const tag of tags) {
|
||||
if ((tag.to as ITagView).path === currentRoute.path) {
|
||||
if ((tag.to as ITagView).path === route.path) {
|
||||
// When query is different then update
|
||||
if ((tag.to as ITagView).fullPath !== currentRoute.fullPath) {
|
||||
tagsViewStore.updateVisitedView(currentRoute)
|
||||
if ((tag.to as ITagView).fullPath !== route.fullPath) {
|
||||
tagsViewStore.updateVisitedView(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => currentRoute.name,
|
||||
() => route.name,
|
||||
() => {
|
||||
addTags()
|
||||
moveToCurrentTag()
|
||||
@ -211,6 +180,34 @@ onBeforeMount(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tags-view-container">
|
||||
<ScrollPane class="tags-view-wrapper">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
ref="tag"
|
||||
:key="tag.path"
|
||||
:class="state.isActive(tag) ? 'active' : ''"
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
class="tags-view-item"
|
||||
@click.middle="!state.isAffix(tag) ? state.closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="state.openMenu(tag, $event)"
|
||||
>
|
||||
{{ tag.meta?.title }}
|
||||
<el-icon v-if="!state.isAffix(tag)" :size="12" @click.prevent.stop="state.closeSelectedTag(tag)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</router-link>
|
||||
</ScrollPane>
|
||||
<ul v-show="state.visible" :style="{ left: state.left + 'px', top: state.top + 'px' }" class="contextmenu">
|
||||
<li @click="state.refreshSelectedTag(state.selectedTag)">刷新</li>
|
||||
<li v-if="!state.isAffix(state.selectedTag)" @click="state.closeSelectedTag(state.selectedTag)">关闭</li>
|
||||
<li @click="state.closeOthersTags">关闭其它</li>
|
||||
<li @click="state.closeAllTags(state.selectedTag)">关闭所有</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-container {
|
||||
height: 34px;
|
||||
@ -264,7 +261,6 @@ onBeforeMount(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
|
@ -1,38 +1,19 @@
|
||||
<!-- 布局入口 -->
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<div v-if="classObj.mobile && sidebar.opened" class="drawer-bg" @click="state.handleClickOutside" />
|
||||
<Sidebar class="sidebar-container" />
|
||||
<div :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<NavigationBar />
|
||||
<TagsView v-if="showTagsView" />
|
||||
</div>
|
||||
<AppMain />
|
||||
<RightPanel v-if="showSettings">
|
||||
<Settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeMount, onBeforeUnmount, onMounted, reactive } from "vue"
|
||||
import { useAppStore, DeviceType } from "@/store/modules/app"
|
||||
import { useSettingsStore } from "@/store/modules/settings"
|
||||
import { computed, onBeforeMount, onBeforeUnmount, onMounted, reactive } from "vue"
|
||||
import { AppMain, NavigationBar, Settings, Sidebar, TagsView, RightPanel } from "./components"
|
||||
import useResize from "./useResize"
|
||||
|
||||
const { sidebar, device, addEventListenerOnResize, resizeMounted, removeEventListenerResize, watchRouter } = useResize()
|
||||
const appStore = useAppStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const { sidebar, device, addEventListenerOnResize, resizeMounted, removeEventListenerResize, watchRouter } = useResize()
|
||||
|
||||
const state = reactive({
|
||||
handleClickOutside: () => {
|
||||
appStore.closeSidebar(false)
|
||||
}
|
||||
})
|
||||
|
||||
const classObj = computed(() => {
|
||||
return {
|
||||
hideSidebar: !sidebar.value.opened,
|
||||
@ -63,6 +44,23 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper">
|
||||
<div v-if="classObj.mobile && sidebar.opened" class="drawer-bg" @click="state.handleClickOutside" />
|
||||
<Sidebar class="sidebar-container" />
|
||||
<div :class="{ hasTagsView: showTagsView }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }">
|
||||
<NavigationBar />
|
||||
<TagsView v-if="showTagsView" />
|
||||
</div>
|
||||
<AppMain />
|
||||
<RightPanel v-if="showSettings">
|
||||
<Settings />
|
||||
</RightPanel>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/mixins.scss";
|
||||
$sideBarWidth: 220px;
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { useAppStore, DeviceType } from "@/store/modules/app"
|
||||
import { computed, watch } from "vue"
|
||||
import { useRoute } from "vue-router"
|
||||
import { useAppStore, DeviceType } from "@/store/modules/app"
|
||||
|
||||
/** 参考 Bootstrap 的响应式设计 width = 992 */
|
||||
const WIDTH = 992
|
||||
|
||||
/** 根据大小变化重新布局 */
|
||||
export default function () {
|
||||
export default () => {
|
||||
const route = useRoute()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const device = computed(() => {
|
||||
@ -17,10 +18,8 @@ export default function () {
|
||||
return appStore.sidebar
|
||||
})
|
||||
|
||||
const currentRoute = useRoute()
|
||||
|
||||
const watchRouter = watch(
|
||||
() => currentRoute.name,
|
||||
() => route.name,
|
||||
() => {
|
||||
if (appStore.device === DeviceType.Mobile && appStore.sidebar.opened) {
|
||||
appStore.closeSidebar(false)
|
||||
|
10
src/main.ts
10
src/main.ts
@ -1,15 +1,15 @@
|
||||
import { createApp, Directive } from "vue"
|
||||
import App from "./App.vue"
|
||||
import router from "./router"
|
||||
import "@/router/permission"
|
||||
import store from "./store"
|
||||
import App from "./App.vue"
|
||||
import * as directives from "@/directives"
|
||||
import loadSvg from "@/icons"
|
||||
import "@/styles/index.scss"
|
||||
import "normalize.css"
|
||||
import * as directives from "@/directives"
|
||||
import "@/router/permission"
|
||||
import loadSvg from "@/icons"
|
||||
|
||||
const app = createApp(App)
|
||||
// 加载全局 SVG
|
||||
// 加载全局 svg
|
||||
loadSvg(app)
|
||||
// 自定义指令
|
||||
Object.keys(directives).forEach((key) => {
|
||||
|
@ -44,7 +44,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
|
||||
|
||||
/**
|
||||
* 动态路由
|
||||
* 用来放置有权限的路由
|
||||
* 用来放置有权限(roles 属性)的路由
|
||||
* 必须带有 name 属性
|
||||
*/
|
||||
export const asyncRoutes: Array<RouteRecordRaw> = [
|
||||
|
@ -1,13 +1,13 @@
|
||||
import NProgress from "nprogress"
|
||||
import "nprogress/nprogress.css"
|
||||
import router from "@/router"
|
||||
import { RouteLocationNormalized } from "vue-router"
|
||||
import { useUserStoreHook } from "@/store/modules/user"
|
||||
import { usePermissionStoreHook } from "@/store/modules/permission"
|
||||
import { ElMessage } from "element-plus"
|
||||
import { whiteList } from "@/config/white-list"
|
||||
import rolesSettings from "@/config/roles"
|
||||
import { getToken } from "@/utils/cookies"
|
||||
import asyncRouteSettings from "@/config/async-route"
|
||||
import NProgress from "nprogress"
|
||||
import "nprogress/nprogress.css"
|
||||
|
||||
const userStore = useUserStoreHook()
|
||||
const permissionStore = usePermissionStoreHook()
|
||||
@ -25,19 +25,18 @@ router.beforeEach(async (to: RouteLocationNormalized, _: RouteLocationNormalized
|
||||
// 检查用户是否已获得其权限角色
|
||||
if (userStore.roles.length === 0) {
|
||||
try {
|
||||
if (rolesSettings.openRoles) {
|
||||
if (asyncRouteSettings.open) {
|
||||
// 注意:角色必须是一个数组! 例如: ['admin'] 或 ['developer', 'editor']
|
||||
await userStore.getInfo()
|
||||
// 获取接口返回的 roles
|
||||
const roles = userStore.roles
|
||||
// 根据角色生成可访问的 routes
|
||||
// 根据角色生成可访问的 routes(可访问路由 = 常驻路由 + 有访问权限的动态路由)
|
||||
permissionStore.setRoutes(roles)
|
||||
} else {
|
||||
// 没有开启角色功能,则启用默认角色
|
||||
userStore.setRoles(rolesSettings.defaultRoles)
|
||||
permissionStore.setRoutes(rolesSettings.defaultRoles)
|
||||
// 没有开启动态路由功能,则启用默认角色
|
||||
userStore.setRoles(asyncRouteSettings.defaultRoles)
|
||||
permissionStore.setRoutes(asyncRouteSettings.defaultRoles)
|
||||
}
|
||||
// 动态地添加可访问的 routes
|
||||
// 将'有访问权限的动态路由' 添加到 router 中
|
||||
permissionStore.dynamicRoutes.forEach((route) => {
|
||||
router.addRoute(route)
|
||||
})
|
||||
@ -45,9 +44,9 @@ router.beforeEach(async (to: RouteLocationNormalized, _: RouteLocationNormalized
|
||||
// 设置 replace: true, 因此导航将不会留下历史记录
|
||||
next({ ...to, replace: true })
|
||||
} catch (err: any) {
|
||||
// 删除 token,并重定向到登录页面
|
||||
// 过程中发生任何错误,都直接重置 token,并重定向到登录页面
|
||||
userStore.resetToken()
|
||||
ElMessage.error(err.message || "Has Error")
|
||||
ElMessage.error(err.message || "路由守卫过程发生错误")
|
||||
next("/login")
|
||||
NProgress.done()
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
.app-wrapper {
|
||||
background-color: $theme-bg-color;
|
||||
color: $font-color;
|
||||
|
||||
// 侧边栏
|
||||
.sidebar-container {
|
||||
.sidebar-logo-container {
|
||||
|
@ -1,4 +1,3 @@
|
||||
<!-- admin 权限主页 -->
|
||||
<template>
|
||||
<div class="app-container">Admin 权限可见</div>
|
||||
</template>
|
||||
|
@ -1,4 +1,3 @@
|
||||
<!-- editor 权限主页 -->
|
||||
<template>
|
||||
<div class="app-container">Editor 权限可见</div>
|
||||
</template>
|
||||
|
@ -1,10 +1,6 @@
|
||||
<template>
|
||||
<component :is="currentRole === 'admin' ? AdminDashboard : EditorDashboard" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import { computed, onBeforeMount, ref } from "vue"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import AdminDashboard from "./admin/index.vue"
|
||||
import EditorDashboard from "./editor/index.vue"
|
||||
|
||||
@ -18,3 +14,7 @@ onBeforeMount(() => {
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="currentRole === 'admin' ? AdminDashboard : EditorDashboard" />
|
||||
</template>
|
||||
|
@ -1,57 +1,7 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<ThemeSwitch class="theme-switch" />
|
||||
<div class="login-card">
|
||||
<div class="title">
|
||||
<img src="@/assets/layout/logo-text-2.png" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-form ref="loginFormDom" :model="loginForm" :rules="loginRules" @keyup.enter="handleLogin">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
type="text"
|
||||
tabindex="1"
|
||||
:prefix-icon="User"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
placeholder="密码"
|
||||
type="password"
|
||||
tabindex="2"
|
||||
:prefix-icon="Lock"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
placeholder="验证码"
|
||||
type="text"
|
||||
tabindex="3"
|
||||
:prefix-icon="Key"
|
||||
maxlength="4"
|
||||
size="large"
|
||||
/>
|
||||
<span class="show-code">
|
||||
<img :src="codeUrl" @click="createCode" />
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-button :loading="loading" type="primary" size="large" @click.prevent="handleLogin"> 登 录 </el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import { useRouter } from "vue-router"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import { User, Lock, Key } from "@element-plus/icons-vue"
|
||||
import ThemeSwitch from "@/components/ThemeSwitch/index.vue"
|
||||
|
||||
@ -128,6 +78,56 @@ const createCode: () => void = () => {
|
||||
// createCode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<ThemeSwitch class="theme-switch" />
|
||||
<div class="login-card">
|
||||
<div class="title">
|
||||
<img src="@/assets/layout/logo-text-2.png" />
|
||||
</div>
|
||||
<div class="content">
|
||||
<el-form ref="loginFormDom" :model="loginForm" :rules="loginRules" @keyup.enter="handleLogin">
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
v-model="loginForm.username"
|
||||
placeholder="用户名"
|
||||
type="text"
|
||||
tabindex="1"
|
||||
:prefix-icon="User"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
placeholder="密码"
|
||||
type="password"
|
||||
tabindex="2"
|
||||
:prefix-icon="Lock"
|
||||
size="large"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="code">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
placeholder="验证码"
|
||||
type="text"
|
||||
tabindex="3"
|
||||
:prefix-icon="Key"
|
||||
maxlength="4"
|
||||
size="large"
|
||||
/>
|
||||
<span class="show-code">
|
||||
<img :src="codeUrl" @click="createCode" />
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-button :loading="loading" type="primary" size="large" @click.prevent="handleLogin"> 登 录 </el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
|
@ -1,4 +1,19 @@
|
||||
<!-- 切换角色 -->
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const emit = defineEmits(["change"])
|
||||
|
||||
const roles = computed(() => userStore.roles)
|
||||
const currentRole = ref(roles.value[0])
|
||||
watch(currentRole, async (value) => {
|
||||
await userStore.changeRoles(value)
|
||||
emit("change")
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div style="margin-bottom: 15px">你的权限:{{ roles }}</div>
|
||||
@ -11,17 +26,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from "@/store/modules/user"
|
||||
import { computed, ref, watch } from "vue"
|
||||
|
||||
const userStore = useUserStore()
|
||||
const emit = defineEmits(["change"])
|
||||
const roles = computed(() => userStore.roles)
|
||||
const currentRole = ref(roles.value[0])
|
||||
watch(currentRole, async (value) => {
|
||||
await userStore.changeRoles(value)
|
||||
emit("change")
|
||||
})
|
||||
</script>
|
@ -1,4 +1,16 @@
|
||||
<!-- 指令权限、权限判断函数的使用 -->
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from "vue"
|
||||
import { checkPermission } from "@/utils/permission" // 权限判断函数
|
||||
import SwitchRoles from "./components/SwitchRoles.vue"
|
||||
|
||||
const state = reactive({
|
||||
key: 1,
|
||||
handleRolesChange: () => {
|
||||
state.key++
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<SwitchRoles @change="state.handleRolesChange" />
|
||||
@ -56,19 +68,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive } from "vue"
|
||||
import { checkPermission } from "@/utils/permission" // 权限判断函数
|
||||
import SwitchRoles from "./components/switch-roles.vue"
|
||||
|
||||
const state = reactive({
|
||||
key: 1,
|
||||
handleRolesChange: () => {
|
||||
state.key++
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.permission-alert {
|
||||
width: 320px;
|
||||
|
@ -1,19 +1,19 @@
|
||||
<!-- 页面权限测试页 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-tag type="success" size="large" style="margin-bottom: 15px"> 当前页面只有 admin 权限可见 </el-tag>
|
||||
<SwitchRoles @change="handleRolesChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from "vue-router"
|
||||
import SwitchRoles from "./components/switch-roles.vue"
|
||||
import SwitchRoles from "./components/SwitchRoles.vue"
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleRolesChange = () => {
|
||||
router.push({ path: "/401" }).catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-tag type="success" size="large" style="margin-bottom: 15px"> 当前页面只有 admin 权限可见 </el-tag>
|
||||
<SwitchRoles @change="handleRolesChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -1,16 +1,16 @@
|
||||
<!-- 重定向 -->
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
|
||||
const { params, query } = useRoute()
|
||||
const { path } = params
|
||||
|
||||
useRouter()
|
||||
.replace({ path: "/" + path, query })
|
||||
.catch((err) => {
|
||||
console.warn(err)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
Loading…
x
Reference in New Issue
Block a user