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" "preinstall": "npx only-allow pnpm"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^0.2.7", "@element-plus/icons-vue": "^1.0.0",
"@tinymce/tinymce-vue": "^4.0.5", "@tinymce/tinymce-vue": "^4.0.6",
"@vueuse/core": "^7.6.2", "@vueuse/core": "^7.7.0",
"@vueuse/integrations": "^7.6.2", "@vueuse/integrations": "^7.7.0",
"axios": "^0.26.0", "axios": "^0.26.0",
"dayjs": "^1.10.7", "dayjs": "^1.10.8",
"element-plus": "^2.0.2", "element-plus": "^2.0.4",
"hotkeys-js": "^3.8.7", "hotkeys-js": "^3.8.7",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"mitt": "^3.0.0", "mitt": "^3.0.0",
@ -33,38 +33,40 @@
"qs": "^6.10.3", "qs": "^6.10.3",
"tinymce": "^5.10.3", "tinymce": "^5.10.3",
"vue": "^3.2.31", "vue": "^3.2.31",
"vue-router": "^4.0.12" "vue-router": "^4.0.13"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^2.2.0", "@vitejs/plugin-vue": "^2.2.4",
"@vitejs/plugin-vue-jsx": "^1.3.7", "@vitejs/plugin-vue-jsx": "^1.3.8",
"@vue/compiler-sfc": "^3.2.31", "@vue/compiler-sfc": "^3.2.31",
"eslint": "^8.9.0", "eslint": "^8.10.0",
"eslint-plugin-vue": "^8.4.1", "eslint-plugin-vue": "^8.5.0",
"http-server": "^14.1.0", "http-server": "^14.1.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"lint-staged": "^12.3.4", "lint-staged": "^12.3.4",
"plop": "^3.0.5", "plop": "^3.0.5",
"postcss-html": "^1.3.0", "postcss-html": "^1.3.0",
"postcss-scss": "^4.0.3", "postcss-scss": "^4.0.3",
"sass": "^1.49.7", "sass": "^1.49.9",
"stylelint": "^14.5.0", "stylelint": "^14.5.3",
"stylelint-config-recommended-scss": "^5.0.2", "stylelint-config-recommended-scss": "^5.0.2",
"stylelint-config-recommended-vue": "^1.3.0", "stylelint-config-recommended-vue": "^1.3.0",
"stylelint-config-standard": "^25.0.0", "stylelint-config-standard": "^25.0.0",
"stylelint-scss": "^4.1.0", "stylelint-scss": "^4.1.0",
"svgo": "^2.8.0", "svgo": "^2.8.0",
"unplugin-auto-import": "^0.5.11", "unplugin-auto-import": "^0.6.1",
"unplugin-vue-components": "^0.17.18", "unplugin-vue-components": "^0.17.21",
"vite": "^2.8.2", "vite": "^2.8.6",
"vite-plugin-banner": "^0.2.0", "vite-plugin-banner": "^0.2.0",
"vite-plugin-compression": "^0.5.1", "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-mock": "^2.9.6",
"vite-plugin-pages": "^0.21.0",
"vite-plugin-restart": "^0.1.1", "vite-plugin-restart": "^0.1.1",
"vite-plugin-spritesmith": "^0.1.1", "vite-plugin-spritesmith": "^0.1.1",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-layouts": "^0.6.0",
"vite-plugin-vue-setup-extend": "^0.4.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_url: 'tinymce/langs/zh_CN.js',
language: 'zh_CN', language: 'zh_CN',
skin_url: 'tinymce/skins/ui/oxide', skin_url: 'tinymce/skins/ui/oxide',
content_css: 'tinymce/skins/content/default/content.min.css',
min_height: 250, min_height: 250,
max_height: 600, max_height: 600,
selector: 'textarea', 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-alert title="应用配置可实时预览效果,但仅是临时生效,要想真正作用于项目,可以点击下方的“复制配置”按钮,将配置粘贴到 src/settings.custom.json 中即可,或者也可在 src/settings.js 中直接修改默认配置。同时建议在生产环境隐藏应用配置功能。" type="error" :closable="false" />
<el-divider v-if="settingsStore.mode === 'pc'">导航栏模式</el-divider> <el-divider v-if="settingsStore.mode === 'pc'">导航栏模式</el-divider>
<div v-if="settingsStore.mode === 'pc'" class="menu-mode"> <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'"> <div class="mode mode-side" :class="{'active': settings.menu.menuMode === 'side'}" @click="settings.menu.menuMode = 'side'">
<svg-icon name="el-icon-check" /> <svg-icon name="el-icon-check" />
</div> </div>
</el-tooltip> </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'"> <div class="mode mode-head" :class="{'active': settings.menu.menuMode === 'head'}" @click="settings.menu.menuMode = 'head'">
<svg-icon name="el-icon-check" /> <svg-icon name="el-icon-check" />
</div> </div>
</el-tooltip> </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'"> <div class="mode mode-single" :class="{'active': settings.menu.menuMode === 'single'}" @click="settings.menu.menuMode = 'single'">
<svg-icon name="el-icon-check" /> <svg-icon name="el-icon-check" />
</div> </div>
@ -71,7 +71,7 @@ function handleCopy() {
<div class="setting-item"> <div class="setting-item">
<div class="label"> <div class="label">
切换跳转 切换跳转
<el-tooltip content="开启该功能后,切换侧边栏时,页面自动跳转至该侧边栏导航下第一个路由地址" placement="top" :append-to-body="false"> <el-tooltip content="开启该功能后,切换侧边栏时,页面自动跳转至该侧边栏导航下第一个路由地址" placement="top">
<svg-icon name="el-icon-question-filled" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>
@ -80,7 +80,7 @@ function handleCopy() {
<div class="setting-item"> <div class="setting-item">
<div class="label"> <div class="label">
保持展开一个 保持展开一个
<el-tooltip content="开启该功能后,侧边栏只保持一个子菜单的展开" placement="top" :append-to-body="false"> <el-tooltip content="开启该功能后,侧边栏只保持一个子菜单的展开" placement="top">
<svg-icon name="el-icon-question-filled" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>
@ -102,7 +102,7 @@ function handleCopy() {
<div class="setting-item"> <div class="setting-item">
<div class="label"> <div class="label">
导航栏搜索 导航栏搜索
<el-tooltip content="对导航栏进行快捷搜索" placement="top" :append-to-body="false"> <el-tooltip content="对导航栏进行快捷搜索" placement="top">
<svg-icon name="el-icon-question-filled" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>
@ -111,7 +111,7 @@ function handleCopy() {
<div v-if="settingsStore.mode === 'pc'" class="setting-item"> <div v-if="settingsStore.mode === 'pc'" class="setting-item">
<div class="label"> <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" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>
@ -120,7 +120,7 @@ function handleCopy() {
<div class="setting-item"> <div class="setting-item">
<div class="label"> <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" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>
@ -147,7 +147,7 @@ function handleCopy() {
<div class="setting-item"> <div class="setting-item">
<div class="label"> <div class="label">
是否开启 是否开启
<el-tooltip content="该功能开启时,登录成功默认进入控制台页面,反之则默认进入导航栏里第一个导航页面" placement="top" :append-to-body="false"> <el-tooltip content="该功能开启时,登录成功默认进入控制台页面,反之则默认进入导航栏里第一个导航页面" placement="top">
<svg-icon name="el-icon-question-filled" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>
@ -161,7 +161,7 @@ function handleCopy() {
<div class="setting-item"> <div class="setting-item">
<div class="label"> <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" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>
@ -175,14 +175,10 @@ function handleCopy() {
<div class="label">是否启用权限</div> <div class="label">是否启用权限</div>
<el-switch v-model="settings.app.enablePermission" /> <el-switch v-model="settings.app.enablePermission" />
</div> </div>
<div class="setting-item">
<div class="label">是否开启后端返回路由数据</div>
<el-switch v-model="settings.app.enableBackendReturnRoute" />
</div>
<div class="setting-item"> <div class="setting-item">
<div class="label"> <div class="label">
载入进度条 载入进度条
<el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top" :append-to-body="false"> <el-tooltip content="该功能开启时,跳转路由会看到页面顶部有进度条" placement="top">
<svg-icon name="el-icon-question-filled" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>
@ -191,7 +187,7 @@ function handleCopy() {
<div class="setting-item"> <div class="setting-item">
<div class="label"> <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" /> <svg-icon name="el-icon-question-filled" />
</el-tooltip> </el-tooltip>
</div> </div>

View File

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

View File

@ -27,13 +27,15 @@ const hasChildren = computed(() => {
</script> </script>
<template> <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)"> <el-sub-menu v-if="item.path == undefined" :title="item.meta.title" :index="JSON.stringify(item)">
<template #title> <template #title>
<svg-icon v-if="item.meta.icon" :name="item.meta.icon" /> <svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
<span class="title">{{ item.meta.title }}</span> <span class="title">{{ item.meta.title }}</span>
</template> </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> </el-sub-menu>
<router-link v-else-if="!hasChildren" v-slot="{ href, navigate, isActive, isExactActive }" custom :to="resolveRoutePath(basePath, item.path)"> <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"> <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" /> <svg-icon v-if="item.meta.icon" :name="item.meta.icon" />
<span class="title">{{ item.meta.title }}</span> <span class="title">{{ item.meta.title }}</span>
</template> </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> </el-sub-menu>
</div> </div>
</template> </template>
@ -66,6 +70,7 @@ const hasChildren = computed(() => {
:deep(.el-sub-menu__title) { :deep(.el-sub-menu__title) {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
} }
:deep(.el-sub-menu), :deep(.el-sub-menu),
:deep(.el-menu-item) { :deep(.el-menu-item) {

View File

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

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()"> <div v-if="enableSidebarCollapse" class="sidebar-collapse" :class="{'is-collapse': settingsStore.menu.subMenuCollapse}" @click="settingsStore.toggleSidebarCollapse()">
<svg-icon name="toolbar-collapse" /> <svg-icon name="toolbar-collapse" />
</div> </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"> <transition-group name="breadcrumb">
<template v-for="(item, index) in breadcrumbList"> <el-breadcrumb-item v-for="(item, index) in breadcrumbList" :key="item.path" :to="index < breadcrumbList.length - 1 ? pathCompile(item.path) : ''">
<el-breadcrumb-item v-if="index < breadcrumbList.length - 1" :key="item.path" :to="pathCompile(item.path)"> {{ item.title }}
{{ item.title }} </el-breadcrumb-item>
</el-breadcrumb-item>
<el-breadcrumb-item v-else :key="item.path">
{{ item.title }}
</el-breadcrumb-item>
</template>
</transition-group> </transition-group>
</el-breadcrumb> </el-breadcrumb>
</div> </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 { isLoading } = useNProgress()
// 固定路由 // 固定路由
const constantRoutes = [ let constantRoutes = [
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
@ -71,7 +71,7 @@ import MultilevelMenuExample from './modules/multilevel.menu.example'
import BreadcrumbExample from './modules/breadcrumb.example' import BreadcrumbExample from './modules/breadcrumb.example'
// 动态路由(异步路由、导航栏路由) // 动态路由(异步路由、导航栏路由)
const asyncRoutes = [ let asyncRoutes = [
{ {
meta: { meta: {
title: '演示', title: '演示',
@ -85,13 +85,26 @@ const asyncRoutes = [
] ]
const lastRoute = { const lastRoute = {
path: '/:pathMatch(.*)*', path: '/:all(.*)*',
component: () => import('@/views/404.vue'), name: 'notFound',
component: () => import('@/views/[...all].vue'),
meta: { meta: {
title: '找不到页面' 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({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes: constantRoutes routes: constantRoutes
@ -140,10 +153,24 @@ router.beforeEach(async(to, from, next) => {
next() next()
} }
} else { } else {
if (!settingsOutsideStore.app.enableBackendReturnRoute) { switch (settingsOutsideStore.app.routeBaseOn) {
await routeOutsideStore.generateRoutesAtFront(asyncRoutes) case 'frontend':
} else { await routeOutsideStore.generateRoutesAtFront(asyncRoutes)
await routeOutsideStore.generateRoutesAtBack() 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 = [] let removeRoutes = []
routeOutsideStore.flatRoutes.forEach(route => { routeOutsideStore.flatRoutes.forEach(route => {
@ -151,7 +178,12 @@ router.beforeEach(async(to, from, next) => {
removeRoutes.push(router.addRoute(route)) 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() 返回的回调进行删除 // 记录的 accessRoutes 路由数据,在登出时会使用到,不使用 router.removeRoute 是考虑配置的路由可能不一定有设置 name ,则通过调用 router.addRoute() 返回的回调进行删除
routeOutsideStore.setCurrentRemoveRoutes(removeRoutes) routeOutsideStore.setCurrentRemoveRoutes(removeRoutes)
next({ ...to, replace: true }) next({ ...to, replace: true })

View File

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

View File

@ -1,9 +1,12 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { piniaStore } from '@/store' import { piniaStore } from '@/store'
import { resolveRoutePath } from '@/util' import { deepClone, resolveRoutePath } from '@/util'
import path from 'path-browserify' import path from 'path-browserify'
import api from '@/api'
import menu from '@/menu'
import { useSettingsStore } from './settings' import { useSettingsStore } from './settings'
import { useUserStore } from './user'
import { useRouteStore } from './route' import { useRouteStore } from './route'
function getDeepestPath(routes, rootPath = '') { function getDeepestPath(routes, rootPath = '') {
@ -29,28 +32,67 @@ function getDeepestPath(routes, rootPath = '') {
return retnPath 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( export const useMenuStore = defineStore(
// 唯一ID // 唯一ID
'menu', 'menu',
{ {
state: () => ({ state: () => ({
menus: [],
actived: 0 actived: 0
}), }),
getters: { getters: {
// 完整导航数据 // 完整导航数据
allMenus() { allMenus() {
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const routeStore = useRouteStore() let menus
let routes if (settingsStore.app.routeBaseOn !== 'filesystem') {
if (settingsStore.menu.menuMode === 'single') { const routeStore = useRouteStore()
routes = [{ children: [] }] if (settingsStore.menu.menuMode === 'single') {
routeStore.routes.map(item => { menus = [{ children: [] }]
routes[0].children.push(...item.children) routeStore.routes.map(item => {
}) menus[0].children.push(...item.children)
})
} else {
menus = routeStore.routes
}
} else { } else {
routes = routeStore.routes menus = this.menus
} }
return routes return menus
}, },
// 次导航数据 // 次导航数据
sidebarMenus() { sidebarMenus() {
@ -61,18 +103,60 @@ export const useMenuStore = defineStore(
return this.allMenus.length > 0 ? getDeepestPath(this.sidebarMenus[0]) : '/' return this.allMenus.length > 0 ? getDeepestPath(this.sidebarMenus[0]) : '/'
}, },
defaultOpenedPaths() { defaultOpenedPaths() {
const routeStore = useRouteStore() const settingsStore = useSettingsStore()
let defaultOpenedPaths = [] let defaultOpenedPaths = []
routeStore.routes.map(item => { if (settingsStore.app.routeBaseOn !== 'filesystem') {
item.meta.defaultOpened && defaultOpenedPaths.push(item.path) const routeStore = useRouteStore()
item.children && item.children.map(child => { routeStore.routes.map(item => {
child.meta.defaultOpened && defaultOpenedPaths.push(path.resolve(item.path, child.path)) 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 return defaultOpenedPaths
} }
}, },
actions: { 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) { setActived(data) {
if (typeof data === 'number') { if (typeof data === 'number') {

View File

@ -130,19 +130,26 @@ export const useRouteStore = defineStore(
getters: { getters: {
// 扁平化路由(将三级及以上路由数据拍平成二级) // 扁平化路由(将三级及以上路由数据拍平成二级)
flatRoutes: state => { flatRoutes: state => {
const settingsStore = useSettingsStore()
let routes = [] let routes = []
if (state.routes) { if (state.routes) {
state.routes.map(item => { if (settingsStore.app.routeBaseOn !== 'filesystem') {
routes.push(...deepClone(item.children)) state.routes.map(item => {
}) routes.push(...deepClone(item.children))
routes.map(item => { })
if (item.children) { routes.map(item => {
item.children = flatAsyncRoutes(item.children, [{ if (item.children) {
path: item.path, item.children = flatAsyncRoutes(item.children, [{
title: item.meta.title path: item.path,
}], item.path) title: item.meta.title
} }], item.path)
}) }
})
} else {
state.routes.map(item => {
routes.push(deepClone(item))
})
}
} }
return routes 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 路由,用于登出时删除路由 // 记录 accessRoutes 路由,用于登出时删除路由
setCurrentRemoveRoutes(routes) { setCurrentRemoveRoutes(routes) {
this.currentRemoveRoutes = routes this.currentRemoveRoutes = routes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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