first commit

This commit is contained in:
JaguarJack
2022-12-05 23:01:12 +08:00
commit 0024080c28
322 changed files with 27698 additions and 0 deletions

View File

@@ -0,0 +1,124 @@
<script lang="ts">
import { h, defineComponent, VNode } from 'vue'
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
import MenuItem from './item.vue'
import menus from './menus.vue'
import { useUserStore } from '/admin/stores/modules/user'
import { Menu } from '/admin/types/Menu'
/**
* 递归渲染 Menu 节点
*/
function getVNodes(menus: Menu[] | undefined, _subMenuClass: string | undefined): VNode[] {
const vnodes: VNode[] = []
menus?.forEach(menu => {
if (!menu.meta?.hidden) {
let vnode: VNode
const len = menu.children?.length
if (len) {
vnode = h(
MenuItem,
{
subMenuClass: _subMenuClass,
menu,
},
{
default: () => getVNodes(menu.children, 'children-menu'),
},
)
} else {
vnode = h(MenuItem, {
subMenuClass: _subMenuClass,
menu,
})
}
vnodes.push(vnode)
}
})
return vnodes
}
/**
* filter menus
*
* @param menus
*/
function filterMenus(menus: Menu[] | undefined): Menu[] {
const newMenus: Menu[] = []
menus?.forEach(m => {
if (m.meta?.hidden) {
return false
}
if (isHasOnlyChild(m) && m.children?.length) {
newMenus.push(
Object.assign({
path: m.children[0].path,
meta: m.children[0].meta,
name: m.name,
}),
)
} else {
newMenus.push(m)
}
})
return newMenus
}
/**
* is has only child
*
* @param menu
*/
function isHasOnlyChild(menu: Menu): boolean {
if (menu.children === undefined) {
return true
}
if (menu.children.length > 1 || !menu.children.length) {
return false
}
if (menu.children[0].children?.length) {
return false
}
return true
}
export default defineComponent({
props: {
subMenuClass: {
type: String,
require: true,
},
menuClass: {
type: String,
require: true,
},
},
setup(props, ctx) {
const permissionsStore = usePermissionsStore()
const userStore = useUserStore()
// 后端的 permissions 返回 undefined则认为该后端无权限系统
const permissions = userStore.getPermissions === undefined ? [] : userStore.getPermissions
const vnodes = getVNodes(filterMenus(permissionsStore.getMenusFrom(permissions)), props.subMenuClass)
return () => {
return h(
menus,
{
class: 'border-none side-menu ' + props.menuClass,
},
{
default: () => vnodes,
},
)
}
},
})
</script>

View File

@@ -0,0 +1,58 @@
<template>
<el-sub-menu :index="menu?.path" :class="subMenuClass" v-if="menu?.children?.length">
<template #title>
<el-icon>
<Icon :name="menu?.meta?.icon" v-if="menu?.meta?.icon" class="text-sm"/>
</el-icon>
<span>{{ menu?.meta?.title }}</span>
</template>
<slot/>
</el-sub-menu>
<el-menu-item v-else class="ct-menu-item" :index="menu?.path" @click="isMiniScreen() && store.changeExpaned()">
<el-icon>
<Icon :name="menu?.meta?.icon" v-if="menu?.meta?.icon" class="text-sm"/>
</el-icon>
<span>{{ menu?.meta?.title }}</span>
</el-menu-item>
</template>
<script lang="ts" name="MenuItem" setup>
import { Menu } from '/admin/types/Menu'
import { PropType } from 'vue'
import { useAppStore } from '/admin/stores/modules/app'
import { isMiniScreen } from '/admin/support/Helper'
const store = useAppStore()
defineProps({
subMenuClass: {
type: String,
require: true,
default: '',
},
menu: {
type: Object as PropType<Menu>,
require: true,
},
})
</script>
<style scoped lang="scss">
:deep(.el-menu) {
background-color: var(--sider-sub-menu-bg-color);
}
.ct-menu-item:hover {
background-color: var(--sider-sub-menu-hover-bg-color) !important;
}
:deep(.children-menu .el-sub-menu__title) {
background-color: var(--sider-sub-menu-bg-color) !important;
}
:deep(.el-menu-item-group__title) {
padding: 0;
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<div class="block sm:hidden z-40 w-screen h-full absolute mask-bg left-0 top-0" />
</template>
<style scoped>
.mask-bg {
background-color: #00000080;
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<el-menu
:default-active="appStore.getActiveMenu"
background-color="var(--sider-menu-bg-color)"
active-text-color="var(--sider-ment-active-text-color)"
text-color="var(--sider-menu-text-color)"
:collapse="!appStore.isExpand"
:collapse-transition="false"
:router="true"
@select="selectMenu"
:unique-opened="true"
>
<slot/>
</el-menu>
</template>
<script lang="ts" setup name="menus">
import { useAppStore } from '/admin/stores/modules/app'
const appStore = useAppStore()
const selectMenu = (index: string) => {
if (index.startsWith('http') || index.startsWith('https')) {
window.open(index)
}
}
</script>
<style scoped lang="scss">
.el-menu {
border-right: none;
}
:deep(.el-menu--inline) {
@apply pt-1 pb-2;
}
:deep(.ct-menu-item) {
@apply mt-1;
}
:deep(.is-active) {
background-color: var(--side-active-menu-bg-color) !important;
}
:deep(.el-sub-menu__title) {
padding-left: calc(calc(var(--el-menu-base-level-padding) + var(--el-menu-level) * var(--el-menu-level-padding)));
color: var(--sider-menu-text-color);
}
:deep(.el-sub-menu) {
color: var(--sider-sub-menu-bg-color);
}
:deep(.el-sub-menu__title:hover) {
background-color: var(--sider-menu-bg-color);
}
:deep(.el-menu--popup .el-menu-item:hover) {
background-color: var(--sider-menu-bg-color) !important;
}
:deep(.el-menu-item:hover) {
background-color: var(--sider-sub-menu-hover-bg-color) !important;
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<div :class="'w-full h-screen flex flex-col transition-spacing duration-300 ease-linear overflow-hidden ' + mlClass">
<!-- Header -->
<Header />
<!-- Tag view -->
<!--<div class=""></div>-->
<!-- Container -->
<div class="p-1 sm:p-4 max-w-full h-screen overflow-auto sm:overflow-x-hidden">
<router-view />
<!--<div class="w-full text-center text-gray-400 h-10 leading-10 mt-2">CatchAdmin 管理系统 @copyright 2018 ~ {{ year }}</div>-->
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
import { useAppStore } from '/admin/stores/modules/app'
const appStore = useAppStore()
const mlClass = computed(() => {
return appStore.isExpand ? 'ml-0 sm:ml-56' : 'ml-0 sm:ml-16'
})
const year = computed(() => {
const date = new Date()
return date.getFullYear()
})
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div class="flex flex-row h-16 w-full drop-shadow border-l dark:border-l-0 border-gray-200" style="background-color: var(--header-bg-color)">
<div class="flex flex-row justify-between w-full">
<div class="flex flex-row min-w-[17rem]">
<div class="h-full flex items-center w-8 ml-2 hover:cursor-pointer" @click="store.changeExpaned">
<Icon name="list-bullet" class="w-6 h-8" />
</div>
<div class="w-96 flex items-center pl-3 sm:pl-0">
<Breadcrumbs />
</div>
</div>
<div class="flex w-52 sm:min-w-[18rem] flex-row item-center pl-1 sm:pl-0 justify-end sm:justify-between mr-4">
<div class="w-3/5 hidden sm:flex">
<!-- 搜索 -->
<Search />
<!-- 多语言 -->
<Lang />
<!-- 暗黑主题 -->
<Theme />
<Notification />
</div>
<Profile />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useAppStore } from '/admin/stores/modules/app'
import Notification from './notification.vue'
const store = useAppStore()
</script>

View File

@@ -0,0 +1,38 @@
<template>
<div class="w-10 h-10 grid place-items-center rounded-full mt-3 hover:cursor-pointer">
<div class="flex hover:cursor-pointer pl-1 pr-1">
<el-dropdown size="large" class="flex items-center justify-center hover:cursor-pointer w-full" @command="selectLanguage">
<Icon name="language" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="lang in langs" :key="lang.value" :command="lang.value" :disabled="lang.value == defaultLang">
{{ $t('system.' + lang.label) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, computed } from 'vue'
import { useAppStore } from '/admin/stores/modules/app'
const langs = reactive([
{ label: 'chinese', value: 'zh' },
{ label: 'english', value: 'en' },
])
const appStore = useAppStore()
// select default languages
const defaultLang = computed(() => {
return appStore.getLocale
})
// select language
const selectLanguage = (value: 'zh' | 'en') => {
appStore.changeLocale(value)
location.reload()
}
</script>

View File

@@ -0,0 +1,22 @@
<template>
<div class="h-16 flex items-center justify-center logo-bg">
<img :src="logo" class="h-9 w-9" />
<div class="text-md logo-text pl-3" v-if="store.isExpand">CatchAdmin</div>
</div>
</template>
<script lang="ts" setup>
import { useAppStore } from '/admin/stores/modules/app'
import logo from '/admin/assets/logo.png'
const store = useAppStore()
</script>
<style scoped lang="scss">
.logo-bg {
background-color: var(--header-logo-bg-color);
}
.logo-text {
color: var(--header-logo-text-color);
}
</style>

View File

@@ -0,0 +1,49 @@
<template>
<!-- 通知 -->
<div class="w-10 h-10 grid place-items-center rounded-full mt-3 hover:cursor-pointer" ref="messageRef" v-click-outside="onClickOutside">
<el-badge :value="3">
<Icon name="bell" />
</el-badge>
<el-popover ref="popoverRef" :virtual-ref="messageRef" trigger="hover" virtual-triggering :width="300">
<el-tabs model-value="message">
<el-tab-pane label="消息(8)" name="message">
<div>
<div class="flex flex-row w-full border-b border-b-slate-300 dark:border-b-slate-500 mt-2" v-for="(message, key) in messages" :key="key">
<div>
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" class="w-2 h-2" />
</div>
<div class="ml-2 h-10 mt-2">
{{ message }}
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="通知(1)" name="notice">
<div>
<div class="flex flex-row w-full border-b border-b-slate-300 dark:border-b-slate-500 mt-2" v-for="(message, key) in messages" :key="key">
<div>
<el-avatar src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" class="w-2 h-2" />
</div>
<div class="ml-2 h-10 mt-2">
{{ message }}
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-popover>
</div>
</template>
<script setup lang="ts">
import { ref, unref } from 'vue'
import { ClickOutside as vClickOutside } from 'element-plus'
const messageRef = ref()
const popoverRef = ref()
const onClickOutside = () => {
unref(popoverRef).popperRef?.delayHide?.()
}
const messages = ref()
messages.value = ['你收到 catchadmin 的好友申请', '你收到 catchadmin pro 的 license 授权', '你收到 catchadmin 通知']
</script>

View File

@@ -0,0 +1,34 @@
<template>
<div class="flex w-2/5 hover:cursor-pointer pl-1 pr-1">
<el-dropdown size="large" placement="bottom-end" class="flex items-center justify-center hover:cursor-pointer w-full">
<div class="flex lg:items-center">
<img :src="userStore.getAvatar" class="w-7 h-7 rounded-full" />
<div class="ml-2 hidden lg:block">{{ userStore.getNickname }}</div>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Action 1</el-dropdown-item>
<el-dropdown-item>Action 2</el-dropdown-item>
<el-dropdown-item>Action 3</el-dropdown-item>
<el-dropdown-item divided @click="logout">
<Icon name="logout" class="mr-1" />
退
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script lang="ts" setup>
import { useUserStore } from '/admin/stores/modules/user'
import Message from '/admin/support/message'
const userStore = useUserStore()
const logout = () => {
Message.confirm('确定退出系统吗?', () => {
userStore.logout()
})
}
</script>

View File

@@ -0,0 +1,58 @@
<template>
<div class="w-10 h-10 grid place-items-center rounded-full mt-3 hover:cursor-pointer">
<div class="flex flex-row w-96">
<Icon name="magnifying-glass" class="hidden sm:block" @click="serachMenuVisable = true" />
<Teleport to="body">
<el-dialog v-model="serachMenuVisable" width="30%" draggable>
<el-cascader :filterable="true" :options="options" @change="toWhere" placeholder="请输入菜单名称" clearable class="w-full" :show-all-levels="false" />
</el-dialog>
</Teleport>
</div>
</div>
</template>
<script lang="ts" setup>
import { usePermissionsStore } from '/admin/stores/modules/user/permissions'
import { Menu } from '/admin/types/Menu'
import router from '/admin/router'
import { ref, computed } from 'vue'
const serachMenuVisable = ref(false)
const permissionStore = usePermissionsStore()
const options = computed(() => {
return filterMenus(permissionStore.getMenus)
})
const toWhere = (value: string[]) => {
if (value.length) {
router.push({ path: value[value.length - 1] })
}
serachMenuVisable.value = false
}
/**
* filter menus
*
* @param menus
*/
function filterMenus(menus: Menu[] | undefined): Object[] {
const cascaderMenus: Object[] = []
menus?.forEach(menu => {
if (menu.meta === undefined) {
const child = menu.children?.pop()
cascaderMenus.push(Object.assign({ label: child?.meta?.title, value: child?.path }))
} else {
const cascaderMenu = Object.assign({ label: menu.meta?.title, value: menu.path, children: [] })
cascaderMenu.children = filterMenus(menu.children)
cascaderMenus.push(cascaderMenu)
}
})
return cascaderMenus
}
</script>

View File

@@ -0,0 +1,22 @@
<template>
<div class="w-10 h-10 grid place-items-center rounded-full mt-3 hover:cursor-pointer">
<Icon name="moon" @click="changeTheme()" v-if="isDark" />
<Icon name="sun" @click="changeTheme()" v-else />
</div>
</template>
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
import { useAppStore } from '/admin/stores/modules/app'
import { unref } from 'vue'
const appStore = useAppStore()
const isDark = useDark()
const toggleDark = useToggle(isDark)
function changeTheme() {
appStore.setDarkMode(!unref(isDark))
toggleDark()
}
</script>

View File

@@ -0,0 +1,60 @@
<template>
<div>
<div :class="sideClass + ' drop-shadow-md overflow-y'">
<!--logo -->
<Logo />
<!-- menu item -->
<Menu :menu-class="menuClass" />
</div>
<Mask v-if="isMobile && appStore.isExpand" @click="appStore.changeExpaned()" />
</div>
</template>
<script lang="ts" setup>
import { useAppStore } from '/admin/stores/modules/app'
import { computed, onMounted, ref, watch } from 'vue'
import { isMiniScreen } from '/admin/support/Helper'
const isMobile = ref(isMiniScreen())
const layoutSide = ' h-screen z-[1000] sm:z-0 absolute top-0 left-0 sm:fixed transition-width duration-300 ease-linear sider-bg'
const layoutSideOpenClass = 'w-56' + layoutSide
const layoutSideHiddenClass = 'w-0 sm:w-16' + layoutSide
// 是否是小屏幕
const sideClass = computed(() => {
return appStore.isExpand ? layoutSideOpenClass : layoutSideHiddenClass
})
// menu class
const menuClass = ref<string>()
// 判断展开状态
const appStore = useAppStore()
watch(appStore.$state, state => {
// 如果切换到小屏幕,并且是菜单是收缩状态
menuClass.value = isExpandWhenInMobile()
})
// 监控屏幕大小
onMounted(() => {
window.onresize = () => {
return (() => {
// 如果切换到小屏幕,并且是菜单是收缩状态,则隐藏子菜单
isMobile.value = isMiniScreen()
menuClass.value = isExpandWhenInMobile()
})()
}
// 刷新或者 go back 的时候默认展开菜单
appStore.isExpand = true
})
function isExpandWhenInMobile(): string {
return !appStore.isExpand && isMobile.value ? 'hidden' : ''
}
</script>
<style scoped>
.sider-bg {
background-color: var(--sider-menu-bg-color);
}
</style>