Template
1
0
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:
pany 2022-04-22 12:47:04 +08:00
parent a7297af892
commit 19b377651c
35 changed files with 548 additions and 540 deletions

View File

@ -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` 不确定分类的修改

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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
View 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

View File

@ -1,14 +0,0 @@
/** 角色配置 */
interface IRolesSettings {
/** 是否开启角色功能(开启后需要后端配合,在查询用户详情接口返回当前用户的所属角色) */
openRoles: boolean
/** 当角色功能关闭时当前登录用户的默认角色将生效默认为admin拥有所有权限 */
defaultRoles: Array<string>
}
const rolesSettings: IRolesSettings = {
openRoles: true,
defaultRoles: ["admin"]
}
export default rolesSettings

View File

@ -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

View File

@ -1,5 +1,5 @@
import { useUserStoreHook } from "@/store/modules/user"
import { Directive } from "vue"
import { useUserStoreHook } from "@/store/modules/user"
/** 权限指令 */
export const permission: Directive = {

View File

@ -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 */

View File

@ -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 {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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 {

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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)

View File

@ -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) => {

View File

@ -44,7 +44,7 @@ export const constantRoutes: Array<RouteRecordRaw> = [
/**
*
*
* roles
* name
*/
export const asyncRoutes: Array<RouteRecordRaw> = [

View File

@ -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()
}

View File

@ -4,6 +4,7 @@
.app-wrapper {
background-color: $theme-bg-color;
color: $font-color;
// 侧边栏
.sidebar-container {
.sidebar-logo-container {

View File

@ -1,4 +1,3 @@
<!-- admin 权限主页 -->
<template>
<div class="app-container">Admin 权限可见</div>
</template>

View File

@ -1,4 +1,3 @@
<!-- editor 权限主页 -->
<template>
<div class="app-container">Editor 权限可见</div>
</template>

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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>