面包屑导航

This commit is contained in:
gary.fu
2024-01-02 17:10:45 +08:00
parent cb4e3110ca
commit 7689b58b13
18 changed files with 165 additions and 48 deletions

20
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"element-plus": "^2.4.4", "element-plus": "^2.4.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mockjs": "^1.1.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.3.13", "vue": "^3.3.13",
@@ -865,6 +866,14 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true "dev": true
}, },
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
"integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
"engines": {
"node": ">=16"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2469,6 +2478,17 @@
"resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz",
"integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==" "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg=="
}, },
"node_modules/mockjs": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mockjs/-/mockjs-1.1.0.tgz",
"integrity": "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==",
"dependencies": {
"commander": "*"
},
"bin": {
"random": "bin/random"
}
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",

View File

@@ -15,6 +15,7 @@
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"element-plus": "^2.4.4", "element-plus": "^2.4.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mockjs": "^1.1.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1", "pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.3.13", "vue": "^3.3.13",

View File

@@ -0,0 +1,71 @@
<script setup>
import { computed } from 'vue'
import { useGlobalConfigStore } from '@/stores/GlobalConfigStore'
import { useTabsViewStore } from '@/stores/TabsViewStore'
import { useRoute } from 'vue-router'
import { useMenuInfo, useMenuName } from '@/components/utils'
const globalConfigStore = useGlobalConfigStore()
const tabsViewStore = useTabsViewStore()
const route = useRoute()
const breadcrumbs = computed(() => {
const exists = []
return route.matched.map(item => {
const menuInfo = useMenuInfo(item)
let icon = ''
if (menuInfo && menuInfo.icon) {
icon = menuInfo.icon
} else if (item.meta && item.meta.icon) {
icon = item.meta.icon
}
const result = {
path: item.path,
menuName: useMenuName(item),
icon
}
console.info(item, menuInfo)
return result
}).filter(item => {
const notExist = !exists.includes(item.menuName)
if (notExist) {
exists.push(item.menuName)
}
return notExist
})
})
</script>
<template>
<el-breadcrumb
v-if="globalConfigStore.isShowBreadcrumb"
v-bind="$attrs"
class="common-breadcrumb"
>
<el-breadcrumb-item
v-for="item in breadcrumbs"
:key="item.path"
:to="{ path: item.path }"
>
<common-icon
v-if="tabsViewStore.isShowTabIcon&&item.icon"
:icon="item.icon"
/>
{{ item.menuName }}
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<style scoped>
.common-breadcrumb {
padding-left: 15px;
padding-top: 10px;
height: 30px;
list-style: none;
border-radius: 4px;
}
.common-breadcrumb .el-icon {
vertical-align: bottom;
}
</style>

View File

@@ -27,7 +27,8 @@ const props = defineProps({
default: '100px' default: '100px'
}, },
model: { model: {
type: Object type: Object,
default: null
}, },
showButtons: { showButtons: {
type: Boolean, type: Boolean,
@@ -40,10 +41,6 @@ const props = defineProps({
showReset: { showReset: {
type: Boolean, type: Boolean,
default: true default: true
},
submitForm: {
type: Function,
default () {}
} }
}) })
@@ -77,6 +74,8 @@ const rules = computed(() => {
return ruleResult return ruleResult
}) })
defineEmits(['submitForm'])
const form = ref() const form = ref()
</script> </script>
@@ -99,7 +98,7 @@ const form = ref()
<el-button <el-button
v-if="showSubmit" v-if="showSubmit"
type="primary" type="primary"
@click="submitForm(form)" @click="$emit('submitForm', form)"
> >
{{ $t('common.label.submit') }} {{ $t('common.label.submit') }}
</el-button> </el-button>

View File

@@ -113,8 +113,8 @@ const selectIcon = icon => {
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-header> </el-header>
<el-main> <el-main class="icon-area">
<RecycleScroller <recycle-scroller
v-slot="{ item }" v-slot="{ item }"
class="scroller icon-list" class="scroller icon-list"
:items="filterIcons" :items="filterIcons"
@@ -142,7 +142,7 @@ const selectIcon = icon => {
</a> </a>
</el-col> </el-col>
</el-row> </el-row>
</RecycleScroller> </recycle-scroller>
</el-main> </el-main>
</el-container> </el-container>
</el-dialog> </el-dialog>
@@ -164,4 +164,7 @@ const selectIcon = icon => {
.el-radio { .el-radio {
margin-right: 10px; margin-right: 10px;
} }
.icon-area {
padding: 0;
}
</style> </style>

View File

@@ -28,7 +28,7 @@ const props = defineProps({
required: true required: true
}, },
index: { index: {
type: Number, type: [Number, String],
required: false required: false
} }
}) })
@@ -65,7 +65,7 @@ const dropdownClick = menuItem => {
:key="menuItem.index||index" :key="menuItem.index||index"
:class="menuCls" :class="menuCls"
> >
{{ menuItem.splitText }} <slot name="split" />
</div> </div>
<el-sub-menu <el-sub-menu
v-else-if="isSubMenu" v-else-if="isSubMenu"

View File

@@ -32,7 +32,11 @@ const activeRoutePath = computed(() => {
<common-menu-item <common-menu-item
:menu-item="menuItem" :menu-item="menuItem"
:index="index" :index="index"
/> >
<template #split>
<slot name="split" />
</template>
</common-menu-item>
</template> </template>
<slot name="default" /> <slot name="default" />
</el-menu> </el-menu>

View File

@@ -81,12 +81,12 @@ const onDropdownVisibleChange = (visible, tab) => {
v-for="item in tabsViewStore.historyTabs" v-for="item in tabsViewStore.historyTabs"
ref="tabItems" ref="tabItems"
:key="item.path" :key="item.path"
:refresh-history-tab="refreshHistoryTab"
:remove-history-tab="removeHistoryTab"
:remove-other-history-tabs="removeOtherHistoryTabs"
:remove-history-tabs="removeHistoryTabs"
:on-dropdown-visible-change="onDropdownVisibleChange"
:tab-item="item" :tab-item="item"
@refresh-history-tab="refreshHistoryTab"
@remove-other-history-tabs="removeOtherHistoryTabs"
@remove-history-tabs="removeHistoryTabs"
@on-dropdown-visible-change="onDropdownVisibleChange"
@remove-history-tab="removeHistoryTab"
/> />
</el-tabs> </el-tabs>
</template> </template>

View File

@@ -12,29 +12,11 @@ const props = defineProps({
tabItem: { tabItem: {
type: Object, type: Object,
required: true required: true
},
removeHistoryTab: {
type: Function,
required: true
},
removeOtherHistoryTabs: {
type: Function,
required: true
},
removeHistoryTabs: {
type: Function,
required: true
},
refreshHistoryTab: {
type: Function,
required: true
},
onDropdownVisibleChange: {
type: Function,
required: true
} }
}) })
defineEmits(['removeHistoryTab', 'removeOtherHistoryTabs', 'removeHistoryTabs', 'refreshHistoryTab', 'onDropdownVisibleChange'])
const menuName = computed(() => { const menuName = computed(() => {
return useMenuName(props.tabItem) return useMenuName(props.tabItem)
}) })
@@ -69,7 +51,7 @@ defineExpose({
:id="tabItem.path" :id="tabItem.path"
ref="dropdownRef" ref="dropdownRef"
trigger="contextmenu" trigger="contextmenu"
@visible-change="onDropdownVisibleChange($event, tabItem)" @visible-change="$emit('onDropdownVisibleChange', $event, tabItem)"
> >
<span class="custom-tabs-label"> <span class="custom-tabs-label">
<common-icon <common-icon
@@ -81,35 +63,35 @@ defineExpose({
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item <el-dropdown-item
@click="refreshHistoryTab(tabItem)" @click="$emit('refreshHistoryTab', tabItem)"
> >
<common-icon icon="refresh" /> <common-icon icon="refresh" />
{{ $t('common.label.refresh') }} {{ $t('common.label.refresh') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
v-if="tabsViewStore.hasCloseDropdown(tabItem, 'close')" v-if="tabsViewStore.hasCloseDropdown(tabItem, 'close')"
@click="removeHistoryTab(tabItem.path)" @click="$emit('removeHistoryTab', tabItem.path)"
> >
<common-icon icon="close" /> <common-icon icon="close" />
{{ $t('common.label.close') }} {{ $t('common.label.close') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
v-if="tabsViewStore.hasCloseDropdown(tabItem, 'left')" v-if="tabsViewStore.hasCloseDropdown(tabItem, 'left')"
@click="removeHistoryTabs(tabItem, 'left')" @click="$emit('removeHistoryTabs', tabItem, 'left')"
> >
<common-icon icon="KeyboardDoubleArrowLeftFilled" /> <common-icon icon="KeyboardDoubleArrowLeftFilled" />
{{ $t('common.label.closeLeft') }} {{ $t('common.label.closeLeft') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
v-if="tabsViewStore.hasCloseDropdown(tabItem, 'right')" v-if="tabsViewStore.hasCloseDropdown(tabItem, 'right')"
@click="removeHistoryTabs(tabItem, 'right')" @click="$emit('removeHistoryTabs', tabItem, 'right')"
> >
<common-icon icon="KeyboardDoubleArrowRightFilled" /> <common-icon icon="KeyboardDoubleArrowRightFilled" />
{{ $t('common.label.closeRight') }} {{ $t('common.label.closeRight') }}
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item <el-dropdown-item
v-if="tabsViewStore.hasCloseDropdown(tabItem, 'other')" v-if="tabsViewStore.hasCloseDropdown(tabItem, 'other')"
@click="removeOtherHistoryTabs(tabItem)" @click="$emit('removeOtherHistoryTabs', tabItem)"
> >
<common-icon icon="PlaylistRemoveFilled" /> <common-icon icon="PlaylistRemoveFilled" />
{{ $t('common.label.closeOther') }} {{ $t('common.label.closeOther') }}

View File

@@ -6,6 +6,7 @@ import CommonMenu from '@/components/common-menu/index.vue'
import CommonMenuItem from '@/components/common-menu-item/index.vue' import CommonMenuItem from '@/components/common-menu-item/index.vue'
import CommonTabsView from '@/components/common-tabs-view/index.vue' import CommonTabsView from '@/components/common-tabs-view/index.vue'
import CommonTable from '@/components/common-table/index.vue' import CommonTable from '@/components/common-table/index.vue'
import CommonBreadcrumb from '@/components/common-breadcrumb/index.vue'
/** /**
* 自定义通用组件自动注册 * 自定义通用组件自动注册
@@ -23,5 +24,6 @@ export default {
Vue.component('CommonMenuItem', CommonMenuItem) Vue.component('CommonMenuItem', CommonMenuItem)
Vue.component('CommonTabsView', CommonTabsView) Vue.component('CommonTabsView', CommonTabsView)
Vue.component('CommonTable', CommonTable) Vue.component('CommonTable', CommonTable)
Vue.component('CommonBreadcrumb', CommonBreadcrumb)
} }
} }

View File

@@ -22,7 +22,14 @@ const allMenus = computed(() => {
mode="horizontal" mode="horizontal"
:ellipsis="false" :ellipsis="false"
:menus="allMenus" :menus="allMenus"
/> >
<template
v-if="globalConfigStore.layoutMode === GlobalLayoutMode.LEFT && globalConfigStore.isShowBreadcrumb"
#split
>
<common-breadcrumb :style="{'padding-left': '0','padding-top': '22px'}" />
</template>
</common-menu>
</template> </template>
<style scoped> <style scoped>

View File

@@ -39,6 +39,7 @@ common.label.tabMode = '多标签模式'
common.label.cachedTabMode = '缓存标签页' common.label.cachedTabMode = '缓存标签页'
common.label.showTabIcon = '标签图标' common.label.showTabIcon = '标签图标'
common.label.keywords = '关键字' common.label.keywords = '关键字'
common.label.breadcrumb = '面包屑导航'
//* =======================msg=====================// //* =======================msg=====================//
common.msg.nonNull = '{0}不能为空' common.msg.nonNull = '{0}不能为空'

View File

@@ -39,6 +39,7 @@ common.label.tabMode = 'Tabs Mode'
common.label.cachedTabMode = 'Cache Tabs' common.label.cachedTabMode = 'Cache Tabs'
common.label.showTabIcon = 'Tab Icon' common.label.showTabIcon = 'Tab Icon'
common.label.keywords = 'Keywords' common.label.keywords = 'Keywords'
common.label.breadcrumb = 'Breadcrumb'
//* =======================msg=====================// //* =======================msg=====================//
common.msg.nonNull = '{0} is required.' common.msg.nonNull = '{0} is required.'

View File

@@ -8,13 +8,18 @@ const router = createRouter({
routes: [ routes: [
{ {
path: '/', path: '/',
name: 'home', name: 'Home',
component: HomeView, component: HomeView,
meta: {
icon: 'HomeFilled',
labelKey: 'common.label.index'
},
children: [{ children: [{
path: '', path: '',
name: 'index', name: 'index',
component: () => import('@/views/Index.vue'), component: () => import('@/views/Index.vue'),
meta: { meta: {
icon: 'HomeFilled',
labelKey: 'common.label.index' labelKey: 'common.label.index'
} }
}, { }, {
@@ -28,8 +33,12 @@ const router = createRouter({
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'notFound', name: 'NotFound',
component: () => import('@/views/404.vue') component: () => import('@/views/404.vue'),
meta: {
icon: 'QuestionFilled',
label: 'Not Found'
}
}, },
...AdminRoutes, ...AdminRoutes,
...ToolsRoutes ...ToolsRoutes

View File

@@ -9,12 +9,14 @@ export const useGlobalConfigStore = defineStore('globalConfig', () => {
const isDarkTheme = useDark() const isDarkTheme = useDark()
const isCollapseLeft = ref(false) const isCollapseLeft = ref(false)
const isShowSettings = ref(false) const isShowSettings = ref(false)
const isShowBreadcrumb = ref(true)
const layoutMode = ref(GlobalLayoutMode.LEFT) const layoutMode = ref(GlobalLayoutMode.LEFT)
return { return {
currentLocale, currentLocale,
isDarkTheme, isDarkTheme,
isCollapseLeft, isCollapseLeft,
isShowSettings, isShowSettings,
isShowBreadcrumb,
layoutMode, layoutMode,
changeLocale (locale) { changeLocale (locale) {
if (Object.values(GlobalLocales).includes(locale)) { if (Object.values(GlobalLocales).includes(locale)) {

View File

@@ -30,6 +30,12 @@ menuStore.loadBusinessMenus()
<el-header> <el-header>
<top-nav /> <top-nav />
</el-header> </el-header>
<el-header
v-if="globalConfigStore.layoutMode === GlobalLayoutMode.TOP && globalConfigStore.isShowBreadcrumb"
class="tabs-header"
>
<common-breadcrumb />
</el-header>
<el-header <el-header
v-if="tabsViewStore.isTabMode" v-if="tabsViewStore.isTabMode"
class="tabs-header" class="tabs-header"

View File

@@ -50,6 +50,15 @@ const options = [
value: GlobalLayoutMode.TOP value: GlobalLayoutMode.TOP
}] }]
}, },
{
labelKey: 'common.label.breadcrumb',
prop: 'isShowBreadcrumb',
type: 'switch',
model: globalConfigStore,
change (val) {
globalConfigStore.isShowBreadcrumb = val
}
},
{ {
labelKey: 'common.label.tabMode', labelKey: 'common.label.tabMode',
prop: 'isTabMode', prop: 'isTabMode',

View File

@@ -9,7 +9,7 @@ const router = useRouter()
<br> <br>
<el-link <el-link
type="primary" type="primary"
@click="router.back()" @click="router.replace('/tables')"
> >
{{ $t('common.label.back') }} {{ $t('common.label.back') }}
</el-link> </el-link>