mirror of
https://gitee.com/fantastic-admin/basic.git
synced 2024-12-03 12:38:34 +08:00
Merge branch 'template' of https://gitee.com/hooray/fantastic-admin into template
This commit is contained in:
commit
6f488fc706
38
package.json
38
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
831
pnpm-lock.yaml
831
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
15
src/menu/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
import MultilevelMenuExample from './modules/multilevel.menu.example'
|
||||
|
||||
const menu = [
|
||||
{
|
||||
meta: {
|
||||
title: '演示',
|
||||
icon: 'sidebar-default'
|
||||
},
|
||||
children: [
|
||||
MultilevelMenuExample
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export default menu
|
46
src/menu/modules/multilevel.menu.example.js
Normal file
46
src/menu/modules/multilevel.menu.example.js
Normal 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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@ -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 })
|
||||
|
@ -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 侧边栏模式(含主导航)
|
||||
|
@ -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') {
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,13 @@
|
||||
<route>
|
||||
{
|
||||
name: 'notFound',
|
||||
meta: {
|
||||
title: "找不到页面",
|
||||
layout: false
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<script setup>
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
const router = useRouter()
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<page-main>
|
||||
<router-link :to="{name: 'breadcrumbExampleDetail1'}">查看详情页</router-link>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<page-main>
|
||||
<router-link :to="{name: 'breadcrumbExampleDetail2'}">查看详情页</router-link>
|
||||
|
@ -1,3 +1,12 @@
|
||||
<route>
|
||||
{
|
||||
name: 'dashboard',
|
||||
meta: {
|
||||
title: "控制台"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<page-header title="欢迎使用 Fantastic-admin(基础版)">
|
||||
|
@ -1,3 +1,13 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
title: "登录",
|
||||
constant: true,
|
||||
layout: false
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<script setup name="Login">
|
||||
const { proxy } = getCurrentInstance()
|
||||
const route = useRoute(), router = useRouter()
|
||||
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-2-1'
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-2-2'
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
title: '导航2-1'
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
title: '导航1'
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<page-main>
|
||||
|
@ -1,3 +1,11 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
title: "修改密码"
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<script setup name="PersonalEditPassword">
|
||||
const route = useRoute(), router = useRouter()
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
@ -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">
|
||||
|
@ -1,3 +1,12 @@
|
||||
<route>
|
||||
{
|
||||
meta: {
|
||||
constant: true,
|
||||
layout: false
|
||||
}
|
||||
}
|
||||
</route>
|
||||
|
||||
<script setup>
|
||||
const router = useRouter()
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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
8
vite/plugins/layouts.js
Normal 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
10
vite/plugins/pages.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Pages from 'vite-plugin-pages'
|
||||
|
||||
export default function createPages() {
|
||||
return Pages({
|
||||
dirs: 'src/views',
|
||||
exclude: [
|
||||
'**/components/**/*.vue'
|
||||
]
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user