Merge branch 'template' of https://gitee.com/hooray/fantastic-admin into template

This commit is contained in:
hooray 2022-03-05 05:21:22 +08:00
commit 6f488fc706
34 changed files with 1048 additions and 505 deletions

View File

@ -15,13 +15,13 @@
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@element-plus/icons-vue": "^0.2.7",
"@tinymce/tinymce-vue": "^4.0.5",
"@vueuse/core": "^7.6.2",
"@vueuse/integrations": "^7.6.2",
"@element-plus/icons-vue": "^1.0.0",
"@tinymce/tinymce-vue": "^4.0.6",
"@vueuse/core": "^7.7.0",
"@vueuse/integrations": "^7.7.0",
"axios": "^0.26.0",
"dayjs": "^1.10.7",
"element-plus": "^2.0.2",
"dayjs": "^1.10.8",
"element-plus": "^2.0.4",
"hotkeys-js": "^3.8.7",
"js-cookie": "^3.0.1",
"mitt": "^3.0.0",
@ -33,38 +33,40 @@
"qs": "^6.10.3",
"tinymce": "^5.10.3",
"vue": "^3.2.31",
"vue-router": "^4.0.12"
"vue-router": "^4.0.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.2.0",
"@vitejs/plugin-vue-jsx": "^1.3.7",
"@vitejs/plugin-vue": "^2.2.4",
"@vitejs/plugin-vue-jsx": "^1.3.8",
"@vue/compiler-sfc": "^3.2.31",
"eslint": "^8.9.0",
"eslint-plugin-vue": "^8.4.1",
"eslint": "^8.10.0",
"eslint-plugin-vue": "^8.5.0",
"http-server": "^14.1.0",
"husky": "^7.0.4",
"lint-staged": "^12.3.4",
"plop": "^3.0.5",
"postcss-html": "^1.3.0",
"postcss-scss": "^4.0.3",
"sass": "^1.49.7",
"stylelint": "^14.5.0",
"sass": "^1.49.9",
"stylelint": "^14.5.3",
"stylelint-config-recommended-scss": "^5.0.2",
"stylelint-config-recommended-vue": "^1.3.0",
"stylelint-config-standard": "^25.0.0",
"stylelint-scss": "^4.1.0",
"svgo": "^2.8.0",
"unplugin-auto-import": "^0.5.11",
"unplugin-vue-components": "^0.17.18",
"vite": "^2.8.2",
"unplugin-auto-import": "^0.6.1",
"unplugin-vue-components": "^0.17.21",
"vite": "^2.8.6",
"vite-plugin-banner": "^0.2.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.0.6",
"vite-plugin-html": "^3.1.0",
"vite-plugin-mock": "^2.9.6",
"vite-plugin-pages": "^0.21.0",
"vite-plugin-restart": "^0.1.1",
"vite-plugin-spritesmith": "^0.1.1",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-layouts": "^0.6.0",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-eslint-parser": "^8.2.0"
"vue-eslint-parser": "^8.3.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -43,6 +43,7 @@ const defaultSetting = ref({
language_url: 'tinymce/langs/zh_CN.js',
language: 'zh_CN',
skin_url: 'tinymce/skins/ui/oxide',
content_css: 'tinymce/skins/content/default/content.min.css',
min_height: 250,
max_height: 600,
selector: 'textarea',

View File

@ -1,84 +0,0 @@
<script setup name="Result">
defineProps({
type: {
type: String,
validator: val => ['success', 'warning', 'error'].includes(val),
required: true
},
title: {
type: String,
required: true
},
desc: {
type: String,
default: ''
}
})
</script>
<template>
<div class="result">
<div v-if="type === 'success'" class="icon icon-success">
<el-icon><el-icon-success-filled /></el-icon>
</div>
<div v-else-if="type === 'warning'" class="icon icon-warning">
<el-icon><el-icon-warning-filled /></el-icon>
</div>
<div v-else class="icon icon-error">
<el-icon><el-icon-circle-close-filled /></el-icon>
</div>
<h1>{{ title }}</h1>
<div v-if="desc" class="desc">{{ desc }}</div>
<div v-if="$slots.extra" class="extra">
<slot name="extra" />
</div>
<div v-if="$slots.default" class="actions">
<slot />
</div>
</div>
</template>
<style lang="scss" scoped>
$success: #67c23a;
$warning: #e6a23c;
$error: #f56c6c;
.result {
width: 72%;
margin: 20px auto 0;
text-align: center;
.icon {
i {
font-size: 80px;
}
&-success i {
color: $success;
}
&-warning i {
color: $warning;
}
&-error i {
color: $error;
}
}
h1 {
margin: 20px 0;
font-size: 24px;
font-weight: normal;
}
.desc {
color: #909399;
margin-bottom: 20px;
}
.extra {
margin: 50px 0;
padding: 24px 40px;
text-align: left;
color: #606266;
background: #f8f8f9;
border-radius: 4px;
}
.actions {
margin-bottom: 20px;
}
}
</style>

View File

@ -47,17 +47,17 @@ function handleCopy() {
<el-alert title="应用配置可实时预览效果,但仅是临时生效,要想真正作用于项目,可以点击下方的“复制配置”按钮,将配置粘贴到 src/settings.custom.json 中即可,或者也可在 src/settings.js 中直接修改默认配置。同时建议在生产环境隐藏应用配置功能。" type="error" :closable="false" />
<el-divider v-if="settingsStore.mode === 'pc'">导航栏模式</el-divider>
<div v-if="settingsStore.mode === 'pc'" class="menu-mode">
<el-tooltip content="侧边栏模式(含主导航)" placement="top" :show-after="500" :append-to-body="false">
<el-tooltip content="侧边栏模式(含主导航)" placement="top" :show-after="500">
<div class="mode mode-side" :class="{'active': settings.menu.menuMode === 'side'}" @click="settings.menu.menuMode = 'side'">
<svg-icon name="el-icon-check" />
</div>
</el-tooltip>
<el-tooltip content="顶部模式" placement="top" :show-after="500" :append-to-body="false">
<el-tooltip content="顶部模式" placement="top" :show-after="500">
<div class="mode mode-head" :class="{'active': settings.menu.menuMode === 'head'}" @click="settings.menu.menuMode = 'head'">
<svg-icon name="el-icon-check" />
</div>
</el-tooltip>
<el-tooltip content="侧边栏模式(不含主导航)" placement="top" :show-after="500" :append-to-body="false">
<el-tooltip content="侧边栏模式(不含主导航)" placement="top" :show-after="500">
<div class="mode mode-single" :class="{'active': settings.menu.menuMode === 'single'}" @click="settings.menu.menuMode = 'single'">
<svg-icon name="el-icon-check" />
</div>
@ -71,7 +71,7 @@ function handleCopy() {
<div class="setting-item">
<div class="label">
切换跳转
<el-tooltip content="开启该功能后,切换侧边栏时,页面自动跳转至该侧边栏导航下第一个路由地址" placement="top" :append-to-body="false">
<el-tooltip content="开启该功能后,切换侧边栏时,页面自动跳转至该侧边栏导航下第一个路由地址" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
@ -80,7 +80,7 @@ function handleCopy() {
<div class="setting-item">
<div class="label">
保持展开一个
<el-tooltip content="开启该功能后,侧边栏只保持一个子菜单的展开" placement="top" :append-to-body="false">
<el-tooltip content="开启该功能后,侧边栏只保持一个子菜单的展开" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
@ -102,7 +102,7 @@ function handleCopy() {
<div class="setting-item">
<div class="label">
导航栏搜索
<el-tooltip content="对导航栏进行快捷搜索" placement="top" :append-to-body="false">
<el-tooltip content="对导航栏进行快捷搜索" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
@ -111,7 +111,7 @@ function handleCopy() {
<div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label">
全屏
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top" :append-to-body="false">
<el-tooltip content="该功能使用场景极少,用户习惯于通过窗口“最大化”功能来扩大显示区域,以显示更多内容,并且使用 F11 键也可以进入全屏效果" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
@ -120,7 +120,7 @@ function handleCopy() {
<div class="setting-item">
<div class="label">
页面刷新
<el-tooltip content="开启时会阻止原生 F5 键刷新功能,并采用框架提供的刷新模式进行页面刷新" placement="top" :append-to-body="false">
<el-tooltip content="开启时会阻止原生 F5 键刷新功能,并采用框架提供的刷新模式进行页面刷新" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
@ -147,7 +147,7 @@ function handleCopy() {
<div class="setting-item">
<div class="label">
是否开启
<el-tooltip content="该功能开启时,登录成功默认进入控制台页面,反之则默认进入导航栏里第一个导航页面" placement="top" :append-to-body="false">
<el-tooltip content="该功能开启时,登录成功默认进入控制台页面,反之则默认进入导航栏里第一个导航页面" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
@ -161,7 +161,7 @@ function handleCopy() {
<div class="setting-item">
<div class="label">
组件尺寸
<el-tooltip content="全局设置 Element Plus 组件的默认尺寸大小" placement="top" :append-to-body="false">
<el-tooltip content="全局设置 Element Plus 组件的默认尺寸大小" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
@ -175,14 +175,10 @@ function handleCopy() {
<div class="label">是否启用权限</div>
<el-switch v-model="settings.app.enablePermission" />
</div>
<div class="setting-item">
<div class="label">是否开启后端返回路由数据</div>
<el-switch v-model="settings.app.enableBackendReturnRoute" />
</div>
<div class="setting-item">
<div class="label">
载入进度条
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top" :append-to-body="false">
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>
@ -191,7 +187,7 @@ function handleCopy() {
<div class="setting-item">
<div class="label">
动态标题
<el-tooltip content="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" placement="top" :append-to-body="false">
<el-tooltip content="该功能开启时,页面标题会显示当前路由标题,格式为“页面标题 - 网站名称”;关闭时则显示网站名称,网站名称在项目根目录下 .env.* 文件里配置" placement="top">
<svg-icon name="el-icon-question-filled" />
</el-tooltip>
</div>

View File

@ -7,6 +7,8 @@ import { useSettingsStore } from '@/store/modules/settings'
const settingsStore = useSettingsStore()
import { useRouteStore } from '@/store/modules/route'
const routeStore = useRouteStore()
import { useMenuStore } from '@/store/modules/menu'
const menuStore = useMenuStore()
const isShow = ref(false)
const searchInput = ref('')
@ -68,9 +70,15 @@ onMounted(() => {
isShow.value = true
}
})
routeStore.routes.map(item => {
getSourceList(item.children)
})
if (settingsStore.app.routeBaseOn !== 'filesystem') {
routeStore.routes.map(item => {
getSourceList(item.children)
})
} else {
menuStore.menus.map(item => {
getSourceList(item.children)
})
}
})
function hasChildren(item) {

View File

@ -27,13 +27,15 @@ const hasChildren = computed(() => {
</script>
<template>
<div v-if="item.meta.sidebar !== false" class="sidebar-item">
<div class="sidebar-item">
<el-sub-menu v-if="item.path == undefined" :title="item.meta.title" :index="JSON.stringify(item)">
<template #title>
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
<span class="title">{{ item.meta.title }}</span>
</template>
<SidebarItem v-for="route in item.children" :key="route.path" :item="route" />
<template v-for="route in item.children">
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" />
</template>
</el-sub-menu>
<router-link v-else-if="!hasChildren" v-slot="{ href, navigate, isActive, isExactActive }" custom :to="resolveRoutePath(basePath, item.path)">
<a :href="isExternalLink(resolveRoutePath(basePath, item.path)) ? resolveRoutePath(basePath, item.path) : href" :class="[isActive && 'router-link-active', isExactActive && 'router-link-exact-active']" :target="isExternalLink(resolveRoutePath(basePath, item.path)) ? '_blank' : '_self'" @click="navigate">
@ -48,7 +50,9 @@ const hasChildren = computed(() => {
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
<span class="title">{{ item.meta.title }}</span>
</template>
<SidebarItem v-for="route in item.children" :key="route.path" :item="route" :base-path="resolveRoutePath(basePath, item.path)" />
<template v-for="route in item.children">
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="resolveRoutePath(basePath, item.path)" />
</template>
</el-sub-menu>
</div>
</template>
@ -66,6 +70,7 @@ const hasChildren = computed(() => {
:deep(.el-sub-menu__title) {
display: flex;
align-items: center;
justify-content: center;
}
:deep(.el-sub-menu),
:deep(.el-menu-item) {

View File

@ -30,8 +30,8 @@ function onSidebarScroll(e) {
}"
>
<transition-group name="sub-sidebar">
<template v-for="route in menuStore.sidebarMenus">
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path" :item="route" :base-path="route.path" />
<template v-for="(route, index) in menuStore.sidebarMenus">
<SidebarItem v-if="route.meta.sidebar !== false" :key="route.path || index" :item="route" :base-path="route.path" />
</template>
</transition-group>
</el-menu>
@ -113,12 +113,9 @@ function onSidebarScroll(e) {
}
:deep(.el-menu-item),
:deep(.el-sub-menu__title) {
span {
display: none;
}
span,
.el-sub-menu__icon-arrow {
right: 7px;
margin-top: -5px;
display: none;
}
}
}

View File

@ -57,16 +57,11 @@ function pathCompile(path) {
<div v-if="enableSidebarCollapse" class="sidebar-collapse" :class="{'is-collapse': settingsStore.menu.subMenuCollapse}" @click="settingsStore.toggleSidebarCollapse()">
<svg-icon name="toolbar-collapse" />
</div>
<el-breadcrumb v-if="settingsStore.topbar.enableBreadcrumb && settingsStore.mode === 'pc'" separator-class="el-icon-arrow-right">
<el-breadcrumb v-if="settingsStore.topbar.enableBreadcrumb && settingsStore.mode === 'pc' && settingsStore.app.routeBaseOn !== 'filesystem'" separator-class="el-icon-arrow-right">
<transition-group name="breadcrumb">
<template v-for="(item, index) in breadcrumbList">
<el-breadcrumb-item v-if="index < breadcrumbList.length - 1" :key="item.path" :to="pathCompile(item.path)">
{{ item.title }}
</el-breadcrumb-item>
<el-breadcrumb-item v-else :key="item.path">
{{ item.title }}
</el-breadcrumb-item>
</template>
<el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path" :to="index < breadcrumbList.length - 1 ? pathCompile(item.path) : ''">
{{ item.title }}
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</div>

15
src/menu/index.js Normal file
View File

@ -0,0 +1,15 @@
import MultilevelMenuExample from './modules/multilevel.menu.example'
const menu = [
{
meta: {
title: '演示',
icon: 'sidebar-default'
},
children: [
MultilevelMenuExample
]
}
]
export default menu

View File

@ -0,0 +1,46 @@
export default {
meta: {
title: '多级导航',
icon: 'sidebar-menu'
},
children: [
{
path: '/multilevel_menu_example/page',
meta: {
title: '导航1'
}
},
{
meta: {
title: '导航2'
},
children: [
{
path: '/multilevel_menu_example/level2/page',
meta: {
title: '导航2-1'
}
},
{
meta: {
title: '导航2-2'
},
children: [
{
path: '/multilevel_menu_example/level2/level3/page1',
meta: {
title: '导航2-2-1'
}
},
{
path: '/multilevel_menu_example/level2/level3/page2',
meta: {
title: '导航2-2-2'
}
}
]
}
]
}
]
}

View File

@ -1,86 +0,0 @@
export default [
{
url: '/mock/route/list',
method: 'get',
response: () => {
return {
error: '',
status: 1,
data: [
{
meta: {
title: '演示',
icon: 'sidebar-default'
},
children: [
{
path: '/multilevel_menu_example',
component: 'Layout',
redirect: '/multilevel_menu_example/page',
name: 'multilevelMenuExample',
meta: {
title: '多级导航',
icon: 'sidebar-menu'
},
children: [
{
path: 'page',
name: 'multilevelMenuExample1',
component: 'multilevel_menu_example/page.vue',
meta: {
title: '导航1'
}
},
{
path: 'level2',
name: 'multilevelMenuExample2',
redirect: '/multilevel_menu_example/level2/page',
meta: {
title: '导航2'
},
children: [
{
path: 'page',
name: 'multilevelMenuExample2-1',
component: 'multilevel_menu_example/level2/page.vue',
meta: {
title: '导航2-1'
}
},
{
path: 'level3',
name: 'multilevelMenuExample2-2',
redirect: '/multilevel_menu_example/level2/level3/page1',
meta: {
title: '导航2-2'
},
children: [
{
path: 'page1',
name: 'multilevelMenuExample2-2-1',
component: 'multilevel_menu_example/level2/level3/page1.vue',
meta: {
title: '导航2-2-1'
}
},
{
path: 'page2',
name: 'multilevelMenuExample2-2-2',
component: 'multilevel_menu_example/level2/level3/page2.vue',
meta: {
title: '导航2-2-2'
}
}
]
}
]
}
]
}
]
}
]
}
}
}
]

View File

@ -10,7 +10,7 @@ import { useNProgress } from '@vueuse/integrations/useNProgress'
const { isLoading } = useNProgress()
// 固定路由
const constantRoutes = [
let constantRoutes = [
{
path: '/login',
name: 'login',
@ -71,7 +71,7 @@ import MultilevelMenuExample from './modules/multilevel.menu.example'
import BreadcrumbExample from './modules/breadcrumb.example'
// 动态路由(异步路由、导航栏路由)
const asyncRoutes = [
let asyncRoutes = [
{
meta: {
title: '演示',
@ -85,13 +85,26 @@ const asyncRoutes = [
]
const lastRoute = {
path: '/:pathMatch(.*)*',
component: () => import('@/views/404.vue'),
path: '/:all(.*)*',
name: 'notFound',
component: () => import('@/views/[...all].vue'),
meta: {
title: '找不到页面'
}
}
import { setupLayouts } from 'virtual:generated-layouts'
import generatedRoutes from 'virtual:generated-pages'
if (useSettingsOutsideStore().app.routeBaseOn === 'filesystem') {
constantRoutes = generatedRoutes.filter(item => {
return item.meta?.enabled !== false && item.meta?.constant === true
})
asyncRoutes = setupLayouts(generatedRoutes.filter(item => {
return item.meta?.enabled !== false && item.meta?.constant !== true && item.meta?.layout !== false
}))
}
const router = createRouter({
history: createWebHashHistory(),
routes: constantRoutes
@ -140,10 +153,24 @@ router.beforeEach(async(to, from, next) => {
next()
}
} else {
if (!settingsOutsideStore.app.enableBackendReturnRoute) {
await routeOutsideStore.generateRoutesAtFront(asyncRoutes)
} else {
await routeOutsideStore.generateRoutesAtBack()
switch (settingsOutsideStore.app.routeBaseOn) {
case 'frontend':
await routeOutsideStore.generateRoutesAtFront(asyncRoutes)
break
case 'backend':
await routeOutsideStore.generateRoutesAtBack()
break
case 'filesystem':
await routeOutsideStore.generateRoutesAtFilesystem(asyncRoutes)
switch (settingsOutsideStore.menu.baseOn) {
case 'frontend':
await menuOutsideStore.generateMenusAtFront()
break
case 'backend':
await menuOutsideStore.generateMenusAtBack()
break
}
break
}
let removeRoutes = []
routeOutsideStore.flatRoutes.forEach(route => {
@ -151,7 +178,12 @@ router.beforeEach(async(to, from, next) => {
removeRoutes.push(router.addRoute(route))
}
})
removeRoutes.push(router.addRoute(lastRoute))
if (settingsOutsideStore.app.routeBaseOn === 'filesystem') {
const otherRoutes = generatedRoutes.filter(item => item.meta?.constant !== true && item.meta?.layout === false)
otherRoutes.length && removeRoutes.push(router.addRoute(...otherRoutes))
} else {
removeRoutes.push(router.addRoute(lastRoute))
}
// 记录的 accessRoutes 路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
routeOutsideStore.setCurrentRemoveRoutes(removeRoutes)
next({ ...to, replace: true })

View File

@ -10,12 +10,17 @@ let globalSettings = {
* 4鉴权函数this.$auth()this.$authAll()
*/
enablePermission: false,
// 是否开启后端返回路由数据
enableBackendReturnRoute: false,
// 是否开启载入进度条
enableProgress: true,
// 是否开启动态标题
enableDynamicTitle: false
enableDynamicTitle: false,
/**
* 路由数据来源
* frontend 前端
* backend 后端
* filesystem 文件系统
*/
routeBaseOn: 'frontend'
},
// 控制台
dashboard: {
@ -31,6 +36,12 @@ let globalSettings = {
},
// 导航栏
menu: {
/**
* 数据来源 app.routeBaseOn filesystem 时生效
* frontend 前端
* backend 后端
*/
baseOn: 'frontend',
/**
* 导航栏模式
* side 侧边栏模式含主导航

View File

@ -1,9 +1,12 @@
import { defineStore } from 'pinia'
import { piniaStore } from '@/store'
import { resolveRoutePath } from '@/util'
import { deepClone, resolveRoutePath } from '@/util'
import path from 'path-browserify'
import api from '@/api'
import menu from '@/menu'
import { useSettingsStore } from './settings'
import { useUserStore } from './user'
import { useRouteStore } from './route'
function getDeepestPath(routes, rootPath = '') {
@ -29,28 +32,67 @@ function getDeepestPath(routes, rootPath = '') {
return retnPath
}
function hasPermission(permissions, route) {
let isAuth = false
if (route.meta && route.meta.auth) {
isAuth = permissions.some(auth => {
if (typeof route.meta.auth == 'string') {
return route.meta.auth === auth
} else {
return route.meta.auth.some(routeAuth => {
return routeAuth === auth
})
}
})
} else {
isAuth = true
}
return isAuth
}
function filterAsyncMenus(menus, permissions) {
const res = []
menus.forEach(menu => {
let tmpMenu = deepClone(menu)
if (hasPermission(permissions, tmpMenu)) {
if (tmpMenu.children) {
tmpMenu.children = filterAsyncMenus(tmpMenu.children, permissions)
tmpMenu.children.length && res.push(tmpMenu)
} else {
res.push(tmpMenu)
}
}
})
return res
}
export const useMenuStore = defineStore(
// 唯一ID
'menu',
{
state: () => ({
menus: [],
actived: 0
}),
getters: {
// 完整导航数据
allMenus() {
const settingsStore = useSettingsStore()
const routeStore = useRouteStore()
let routes
if (settingsStore.menu.menuMode === 'single') {
routes = [{ children: [] }]
routeStore.routes.map(item => {
routes[0].children.push(...item.children)
})
let menus
if (settingsStore.app.routeBaseOn !== 'filesystem') {
const routeStore = useRouteStore()
if (settingsStore.menu.menuMode === 'single') {
menus = [{ children: [] }]
routeStore.routes.map(item => {
menus[0].children.push(...item.children)
})
} else {
menus = routeStore.routes
}
} else {
routes = routeStore.routes
menus = this.menus
}
return routes
return menus
},
// 次导航数据
sidebarMenus() {
@ -61,18 +103,60 @@ export const useMenuStore = defineStore(
return this.allMenus.length > 0 ? getDeepestPath(this.sidebarMenus[0]) : '/'
},
defaultOpenedPaths() {
const routeStore = useRouteStore()
const settingsStore = useSettingsStore()
let defaultOpenedPaths = []
routeStore.routes.map(item => {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path)
item.children && item.children.map(child => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path))
if (settingsStore.app.routeBaseOn !== 'filesystem') {
const routeStore = useRouteStore()
routeStore.routes.map(item => {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path)
item.children && item.children.map(child => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path))
})
})
})
}
return defaultOpenedPaths
}
},
actions: {
// 生成导航(前端生成)
generateMenusAtFront() {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedMenus
// 如果权限功能开启,则需要对导航数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedMenus = filterAsyncMenus(menu, permissions)
} else {
accessedMenus = deepClone(menu)
}
this.menus = accessedMenus.filter(item => item.children.length != 0)
resolve()
})
},
// 生成导航(后端生成)
generateMenusAtBack() {
return new Promise(resolve => {
api.get('menu/list', {
baseURL: '/mock/'
}).then(async res => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedMenus
// 如果权限功能开启,则需要对导航数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedMenus = filterAsyncMenus(res.data, permissions)
} else {
accessedMenus = deepClone(res.data)
}
this.menus = accessedMenus.filter(item => item.children.length != 0)
resolve()
})
})
},
// 切换主导航
setActived(data) {
if (typeof data === 'number') {

View File

@ -130,19 +130,26 @@ export const useRouteStore = defineStore(
getters: {
// 扁平化路由(将三级及以上路由数据拍平成二级)
flatRoutes: state => {
const settingsStore = useSettingsStore()
let routes = []
if (state.routes) {
state.routes.map(item => {
routes.push(...deepClone(item.children))
})
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
if (settingsStore.app.routeBaseOn !== 'filesystem') {
state.routes.map(item => {
routes.push(...deepClone(item.children))
})
routes.map(item => {
if (item.children) {
item.children = flatAsyncRoutes(item.children, [{
path: item.path,
title: item.meta.title
}], item.path)
}
})
} else {
state.routes.map(item => {
routes.push(deepClone(item))
})
}
}
return routes
}
@ -192,6 +199,26 @@ export const useRouteStore = defineStore(
})
})
},
// 根据权限动态生成路由(文件系统生成)
generateRoutesAtFilesystem(asyncRoutes) {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async resolve => {
const settingsStore = useSettingsStore()
const userStore = useUserStore()
let accessedRoutes
// 如果权限功能开启,则需要对路由数据进行筛选过滤
if (settingsStore.app.enablePermission) {
const permissions = await userStore.getPermissions()
accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
} else {
accessedRoutes = deepClone(asyncRoutes)
}
// 设置 routes 数据
this.isGenerate = true
this.routes = accessedRoutes.filter(item => item.children.length != 0)
resolve()
})
},
// 记录 accessRoutes 路由,用于登出时删除路由
setCurrentRemoveRoutes(routes) {
this.currentRemoveRoutes = routes

View File

@ -1,3 +1,13 @@
<route>
{
name: 'notFound',
meta: {
title: "找不到页面",
layout: false
}
}
</route>
<script setup>
import { onBeforeRouteLeave } from 'vue-router'
const router = useRouter()

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
enabled: false
}
}
</route>
<template>
<div>
<page-main>

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
enabled: false
}
}
</route>
<template>
<div>
<page-main>

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
enabled: false
}
}
</route>
<template>
<page-main>
<router-link :to="{name: 'breadcrumbExampleDetail1'}">查看详情页</router-link>

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
enabled: false
}
}
</route>
<template>
<page-main>
<router-link :to="{name: 'breadcrumbExampleDetail2'}">查看详情页</router-link>

View File

@ -1,3 +1,12 @@
<route>
{
name: 'dashboard',
meta: {
title: "控制台"
}
}
</route>
<template>
<div>
<page-header title="欢迎使用 Fantastic-admin基础版">

View File

@ -1,3 +1,13 @@
<route>
{
meta: {
title: "登录",
constant: true,
layout: false
}
}
</route>
<script setup name="Login">
const { proxy } = getCurrentInstance()
const route = useRoute(), router = useRouter()

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
title: '导航2-2-1'
}
}
</route>
<template>
<div>
<page-main>

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
title: '导航2-2-2'
}
}
</route>
<template>
<div>
<page-main>

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
title: '导航2-1'
}
}
</route>
<template>
<div>
<page-main>

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
title: '导航1'
}
}
</route>
<template>
<div>
<page-main>

View File

@ -1,3 +1,11 @@
<route>
{
meta: {
title: "修改密码"
}
}
</route>
<script setup name="PersonalEditPassword">
const route = useRoute(), router = useRouter()
const { proxy } = getCurrentInstance()

View File

@ -1,3 +1,12 @@
<route>
{
meta: {
title: "个人设置",
cache: "personal-edit.password"
}
}
</route>
<script setup name="PersonalSetting">
const router = useRouter()
const { proxy } = getCurrentInstance()
@ -26,7 +35,6 @@ function editPassword() {
<template>
<div>
<!-- 页面Setting -->
<page-main>
<el-tabs tab-position="left" style="height: 600px;">
<el-tab-pane label="基本设置" class="basic">

View File

@ -1,3 +1,12 @@
<route>
{
meta: {
constant: true,
layout: false
}
}
</route>
<script setup>
const router = useRouter()

View File

@ -39,6 +39,7 @@ export default ({ mode, command }) => {
build: {
outDir: mode == 'production' ? 'dist' : `dist-${mode}`,
sourcemap: env.VITE_BUILD_SOURCEMAP == 'true',
minify: 'terser',
terserOptions: {
compress: {
drop_console: env.VITE_BUILD_DROP_CONSOLE == 'true'

View File

@ -8,6 +8,8 @@ import createComponents from './components'
import createSetupExtend from './setup-extend'
import createSvgIcon from './svg-icon'
import createMock from './mock'
import createLayouts from './layouts'
import createPages from './pages'
import createCompression from './compression'
import createSpritesmith from './spritesmith'
import createBanner from './banner'
@ -22,6 +24,8 @@ export default function createVitePlugins(viteEnv, isBuild = false) {
vitePlugins.push(createSetupExtend())
vitePlugins.push(createSvgIcon(isBuild))
vitePlugins.push(createMock())
vitePlugins.push(createLayouts())
vitePlugins.push(createPages())
isBuild && vitePlugins.push(...createCompression(viteEnv))
vitePlugins.push(...createSpritesmith(isBuild))
vitePlugins.push(createBanner())

8
vite/plugins/layouts.js Normal file
View File

@ -0,0 +1,8 @@
import Layouts from 'vite-plugin-vue-layouts'
export default function createPages() {
return Layouts({
layoutsDirs: 'src/layout',
defaultLayout: 'index'
})
}

10
vite/plugins/pages.js Normal file
View File

@ -0,0 +1,10 @@
import Pages from 'vite-plugin-pages'
export default function createPages() {
return Pages({
dirs: 'src/views',
exclude: [
'**/components/**/*.vue'
]
})
}