1
0
mirror of https://github.com/zclzone/vue-naive-admin.git synced 2026-01-02 14:20:21 +08:00

feat: 添加simple、normal、full 等layout布局

This commit is contained in:
zclzone
2023-12-16 18:56:59 +08:00
parent 3f1e92e066
commit b4ebae4f42
35 changed files with 270 additions and 970 deletions

View File

@@ -0,0 +1,80 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:50:10
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<n-breadcrumb>
<n-breadcrumb-item v-if="!breadItems?.length" :clickable="false">
{{ route.meta.title }}
</n-breadcrumb-item>
<n-breadcrumb-item
v-for="item of breadItems"
v-else
:key="item.code"
:clickable="!!item.path"
@click="handleItemClick(item)"
>
<n-dropdown :options="getDropOptions(item.children)" @select="handleDropSelect">
<div class="flex items-center">
<i :class="item.icon" class="mr-8" />
{{ item.name }}
</div>
</n-dropdown>
</n-breadcrumb-item>
</n-breadcrumb>
</template>
<script setup>
import { usePermissionStore } from '@/store'
const router = useRouter()
const route = useRoute()
const permissionStore = usePermissionStore()
const breadItems = ref([])
watch(
() => route.name,
(v) => {
breadItems.value = findMatchs(permissionStore.permissions, v)
},
{ immediate: true }
)
function findMatchs(tree, code, parents = []) {
for (const item of tree) {
if (item.code === code) {
return [...parents, item]
}
if (item.children?.length) {
const found = findMatchs(item.children, code, [...parents, item])
if (found) {
return found
}
}
}
return null
}
function handleItemClick(item) {
if (item.path && item.code !== route.name) {
router.push(item.path)
}
}
function getDropOptions(list = []) {
return list.map((child) => ({
label: child.name,
key: child.code,
icon: () => h('i', { class: child.icon }),
}))
}
function handleDropSelect(item) {
if (item.path && item.code !== route.name) {
router.push(item.path)
}
}
</script>

View File

@@ -0,0 +1,22 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:50:18
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<div
class="f-c-c cursor-pointer rounded-4 p-6 text-22 transition-all-300 auto-bg-hover"
@click="appStore.switchCollapsed"
>
<i :class="appStore.collapsed ? 'i-line-md-menu-unfold-left' : 'i-line-md-menu-fold-left'" />
</div>
</template>
<script setup>
import { useAppStore } from '@/store'
const appStore = useAppStore()
</script>

View File

@@ -0,0 +1,26 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:50:28
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<router-link class="h-60 f-c-c" to="/">
<img src="@/assets/images/logo.png" class="h-40" />
<h2
v-show="!appStore.collapsed"
class="ml-10 max-w-140 flex-shrink-0 text-16 color-primary font-bold"
>
{{ title }}
</h2>
</router-link>
</template>
<script setup>
import { useAppStore } from '@/store'
const title = import.meta.env.VITE_TITLE
const appStore = useAppStore()
</script>

View File

@@ -0,0 +1,62 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:50:35
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<n-menu
ref="menu"
class="side-menu"
accordion
:indent="18"
:collapsed-icon-size="22"
:collapsed-width="64"
:collapsed="appStore.collapsed"
:options="permissionStore.menus"
:value="activeKey"
@update:value="handleMenuSelect"
/>
</template>
<script setup>
import { useAppStore, usePermissionStore } from '@/store'
import { isExternal } from '@/utils'
const router = useRouter()
const route = useRoute()
const appStore = useAppStore()
const permissionStore = usePermissionStore()
const activeKey = computed(() => route.meta?.parentKey || route.name)
const menu = ref(null)
watch(route, async () => {
await nextTick()
menu.value?.showOption()
})
function handleMenuSelect(key, item) {
if (isExternal(item.path)) {
window.open(item.path)
} else {
router.push(item.path)
}
}
</script>
<style lang="scss">
.side-menu:not(.n-menu--collapsed) {
.n-menu-item-content {
&::before {
left: 8px;
right: 8px;
}
&.n-menu-item-content--selected::before {
border-left: 4px solid var(--primary-color);
}
}
}
</style>

View File

@@ -0,0 +1,83 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:50:42
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<n-dropdown :options="options" @select="handleSelect">
<div class="flex cursor-pointer items-center">
<n-avatar round :size="36" :src="userStore.avatar" />
<div v-if="userStore.userInfo" class="ml-12 flex-col flex-shrink-0 items-center">
<span class="text-14">{{ userStore.nickName ?? userStore.username }}</span>
<span class="text-12 opacity-50">[{{ userStore.currentRole?.name }}]</span>
</div>
</div>
</n-dropdown>
<RoleSelect ref="roleSelectRef" />
</template>
<script setup>
import { useUserStore, useAuthStore, usePermissionStore } from '@/store'
import { RoleSelect } from '@/layouts/components'
import { initUserAndPermissions } from '@/router'
import api from '@/api'
const router = useRouter()
const userStore = useUserStore()
const authStore = useAuthStore()
const permissionStore = usePermissionStore()
const options = reactive([
{
label: '个人资料',
key: 'profile',
icon: () => h('i', { class: 'i-material-symbols:person-outline text-14' }),
show: computed(() => permissionStore.accessRoutes?.some((item) => item.path === '/profile')),
},
{
label: '切换角色',
key: 'toggleRole',
icon: () => h('i', { class: 'i-basil:exchange-solid text-14' }),
show: computed(() => userStore.roles.length > 1),
},
{
label: '退出登录',
key: 'logout',
icon: () => h('i', { class: 'i-mdi:exit-to-app text-14' }),
},
])
const roleSelectRef = ref(null)
function handleSelect(key) {
switch (key) {
case 'profile':
router.push('/profile')
break
case 'toggleRole':
roleSelectRef.value?.open({
onOk() {
initUserAndPermissions().then(() => {
router.replace('/')
})
},
})
break
case 'logout':
$dialog.confirm({
title: '提示',
type: 'info',
content: '确认退出?',
async confirm() {
await api.logout()
authStore.logout()
$message.success('已退出登录')
},
})
break
}
}
</script>

View File

@@ -1 +1,7 @@
export { default as RoleSelect } from './RoleSelect.vue'
export { default as UserAvatar } from './UserAvatar.vue'
export { default as MenuCollapse } from './MenuCollapse.vue'
export { default as BreadCrumb } from './BreadCrumb.vue'
export { default as AppTab } from './tab/index.vue'
export { default as SideLogo } from './SideLogo.vue'
export { default as SideMenu } from './SideMenu.vue'

View File

@@ -0,0 +1,125 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:50:48
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<n-dropdown
:show="show"
:options="options"
:x="x"
:y="y"
placement="bottom-start"
@clickoutside="handleHideDropdown"
@select="handleSelect"
/>
</template>
<script setup>
import { useTabStore } from '@/store'
const props = defineProps({
show: {
type: Boolean,
default: false,
},
currentPath: {
type: String,
default: '',
},
x: {
type: Number,
default: 0,
},
y: {
type: Number,
default: 0,
},
})
const emit = defineEmits(['update:show'])
const tabStore = useTabStore()
const options = computed(() => [
{
label: '重新加载',
key: 'reload',
disabled: props.currentPath !== tabStore.activeTab,
icon: () => h('i', { class: 'i-mdi:refresh text-14' }),
},
{
label: '关闭',
key: 'close',
disabled: tabStore.tabs.length <= 1,
icon: () => h('i', { class: 'i-mdi:close text-14' }),
},
{
label: '关闭其他',
key: 'close-other',
disabled: tabStore.tabs.length <= 1,
icon: () => h('i', { class: 'i-mdi:arrow-expand-horizontal text-14' }),
},
{
label: '关闭左侧',
key: 'close-left',
disabled: tabStore.tabs.length <= 1 || props.currentPath === tabStore.tabs[0].path,
icon: () => h('i', { class: 'i-mdi:arrow-expand-left text-14' }),
},
{
label: '关闭右侧',
key: 'close-right',
disabled:
tabStore.tabs.length <= 1 ||
props.currentPath === tabStore.tabs[tabStore.tabs.length - 1].path,
icon: () => h('i', { class: 'i-mdi:arrow-expand-right text-14' }),
},
])
const route = useRoute()
const actionMap = new Map([
[
'reload',
() => {
tabStore.reloadTab(route.fullPath, route.meta?.keepAlive)
},
],
[
'close',
() => {
tabStore.removeTab(props.currentPath)
},
],
[
'close-other',
() => {
tabStore.removeOther(props.currentPath)
},
],
[
'close-left',
() => {
tabStore.removeLeft(props.currentPath)
},
],
[
'close-right',
() => {
tabStore.removeRight(props.currentPath)
},
],
])
function handleHideDropdown() {
emit('update:show', false)
}
function handleSelect(key) {
const actionFn = actionMap.get(key)
actionFn && actionFn()
handleHideDropdown()
}
</script>

View File

@@ -0,0 +1,101 @@
<!--------------------------------
- @Author: Ronnie Zhang
- @LastEditor: Ronnie Zhang
- @LastEditTime: 2023/12/16 18:50:54
- @Email: zclzone@outlook.com
- Copyright © 2023 Ronnie Zhang(大脸怪) | https://isme.top
--------------------------------->
<template>
<div>
<n-tabs
:value="tabStore.activeTab"
:closable="tabStore.tabs.length > 1"
:style="`--selected-bg: ${appStore.isDark ? '#1b2429' : '#eaf0f1'}`"
type="card"
@close="(path) => tabStore.removeTab(path)"
>
<n-tab
v-for="item in tabStore.tabs"
:key="item.path"
:name="item.path"
@click="handleItemClick(item.path)"
@contextmenu.prevent="handleContextMenu($event, item)"
>
{{ item.title }}
</n-tab>
</n-tabs>
<ContextMenu
v-if="contextMenuOption.show"
v-model:show="contextMenuOption.show"
:current-path="contextMenuOption.currentPath"
:x="contextMenuOption.x"
:y="contextMenuOption.y"
/>
</div>
</template>
<script setup>
import ContextMenu from './ContextMenu.vue'
import { useTabStore, useAppStore } from '@/store'
const router = useRouter()
const appStore = useAppStore()
const tabStore = useTabStore()
const contextMenuOption = reactive({
show: false,
x: 0,
y: 0,
currentPath: '',
})
const handleItemClick = (path) => {
tabStore.setActiveTab(path)
router.push(path)
}
function showContextMenu() {
contextMenuOption.show = true
}
function hideContextMenu() {
contextMenuOption.show = false
}
function setContextMenu(x, y, currentPath) {
Object.assign(contextMenuOption, { x, y, currentPath })
}
// 右击菜单
async function handleContextMenu(e, tagItem) {
const { clientX, clientY } = e
hideContextMenu()
setContextMenu(clientX, clientY, tagItem.path)
await nextTick()
showContextMenu()
}
</script>
<style scoped lang="scss">
:deep(.n-tabs) {
.n-tabs-tab {
padding-left: 16px;
height: 36px;
background: transparent !important;
border-radius: 4px !important;
margin-right: 4px;
&:hover {
border: 1px solid var(--primary-color) !important;
}
}
.n-tabs-tab--active {
border: 1px solid var(--primary-color) !important;
background-color: var(--selected-bg) !important;
}
.n-tabs-pad,
.n-tabs-tab-pad,
.n-tabs-scroll-padding {
border: none !important;
}
}
</style>