优化
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
AudioWaveform,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/composables/use-sidebar'
|
||||
|
||||
import type { SidebarData, Team, User } from '../types'
|
||||
|
||||
const user: User = {
|
||||
name: 'shadcn',
|
||||
email: 'm@example.com',
|
||||
avatar: '/avatars/shadcn.jpg',
|
||||
}
|
||||
|
||||
const teams: Team[] = [
|
||||
{
|
||||
name: 'Acme Inc',
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: 'Enterprise',
|
||||
},
|
||||
{
|
||||
name: 'Acme Corp.',
|
||||
logo: AudioWaveform,
|
||||
plan: 'Startup',
|
||||
},
|
||||
{
|
||||
name: 'Evil Corp.',
|
||||
logo: Command,
|
||||
plan: 'Free',
|
||||
},
|
||||
]
|
||||
|
||||
const { navData } = useSidebar()
|
||||
|
||||
export const sidebarData: SidebarData = {
|
||||
user,
|
||||
teams,
|
||||
navMain: navData.value!,
|
||||
}
|
||||
24
monisuo-admin/src/components/app-sidebar/index.vue
Normal file
24
monisuo-admin/src/components/app-sidebar/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { sidebarData } from './data/sidebar-data'
|
||||
import NavFooter from './nav-footer.vue'
|
||||
import NavTeam from './nav-team.vue'
|
||||
import TeamSwitcher from './team-switcher.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebar collapsible="icon" class="z-50">
|
||||
<UiSidebarHeader>
|
||||
<TeamSwitcher :teams="sidebarData.teams" />
|
||||
</UiSidebarHeader>
|
||||
|
||||
<UiSidebarContent>
|
||||
<NavTeam :nav-main="sidebarData.navMain" />
|
||||
</UiSidebarContent>
|
||||
|
||||
<UiSidebarFooter>
|
||||
<NavFooter :user="sidebarData.user" />
|
||||
</UiSidebarFooter>
|
||||
|
||||
<UiSidebarRail />
|
||||
</UiSidebar>
|
||||
</template>
|
||||
108
monisuo-admin/src/components/app-sidebar/nav-footer.vue
Normal file
108
monisuo-admin/src/components/app-sidebar/nav-footer.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
UserRoundCog,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { User } from './types'
|
||||
|
||||
const { user } = defineProps<
|
||||
{ user: User }
|
||||
>()
|
||||
|
||||
const { logout } = useAuth()
|
||||
const { isMobile, open } = useSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarMenu>
|
||||
<UiSidebarMenuItem>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<UiAvatar class="size-8 rounded-lg">
|
||||
<UiAvatarImage :src="user.avatar" :alt="user.name" />
|
||||
<UiAvatarFallback class="rounded-lg">
|
||||
CN
|
||||
</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ user.name }}</span>
|
||||
<span class="text-xs truncate">{{ user.email }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent
|
||||
class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
:side="(isMobile || open) ? 'bottom' : 'right'"
|
||||
align="start"
|
||||
:side-offset="4"
|
||||
>
|
||||
<UiDropdownMenuLabel class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<UiAvatar class="size-8 rounded-lg">
|
||||
<UiAvatarImage :src="user.avatar" :alt="user.name" />
|
||||
<UiAvatarFallback class="rounded-lg">
|
||||
CN
|
||||
</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ user.name }}</span>
|
||||
<span class="text-xs truncate">{{ user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiDropdownMenuLabel>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/billing/')">
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/billing?type=billing')">
|
||||
<CreditCard />
|
||||
Billing
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/')">
|
||||
<UserRoundCog />
|
||||
Profile
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/account')">
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/notifications')">
|
||||
<Bell />
|
||||
Notifications
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem @click="logout">
|
||||
<LogOut />
|
||||
{{ $t('logout') }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</UiSidebarMenuItem>
|
||||
</UiSidebarMenu>
|
||||
</template>
|
||||
91
monisuo-admin/src/components/app-sidebar/nav-team-add.vue
Normal file
91
monisuo-admin/src/components/app-sidebar/nav-team-add.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
|
||||
import { teamAddValidator } from './validators/team.validator'
|
||||
|
||||
const emits = defineEmits(['close'])
|
||||
|
||||
const teamAddFormSchema = toTypedSchema(teamAddValidator)
|
||||
|
||||
const { handleSubmit } = useForm({
|
||||
validationSchema: teamAddFormSchema,
|
||||
initialValues: {},
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit((values) => {
|
||||
toast('You submitted the following values:', {
|
||||
position: 'top-center',
|
||||
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
|
||||
})
|
||||
|
||||
emits('close')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>
|
||||
Add New Team
|
||||
</UiDialogTitle>
|
||||
<UiDialogDescription>
|
||||
Add a new team by your self.
|
||||
</UiDialogDescription>
|
||||
</UiDialogHeader>
|
||||
|
||||
<form class="space-y-4" @submit="onSubmit">
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the name for the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="slug">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Slug
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the slug for the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="logo">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Logo
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the logo of the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<div class="flex justify-start mt-4">
|
||||
<UiButton type="submit">
|
||||
Add team
|
||||
</UiButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
108
monisuo-admin/src/components/app-sidebar/nav-team.vue
Normal file
108
monisuo-admin/src/components/app-sidebar/nav-team.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ChevronRight,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { NavGroup, NavItem } from './types'
|
||||
|
||||
const { navMain } = defineProps<{
|
||||
navMain: NavGroup[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { state, isMobile } = useSidebar()
|
||||
|
||||
function isCollapsed(menu: NavItem): boolean {
|
||||
const pathname = route.path
|
||||
navMain.forEach((group) => {
|
||||
group.items.forEach((item) => {
|
||||
if (item.url === pathname) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
return !!menu.items?.some(item => item.url === pathname)
|
||||
}
|
||||
|
||||
function isActive(menu: NavItem): boolean {
|
||||
const pathname = route.path
|
||||
if (menu.url) {
|
||||
return pathname === menu.url
|
||||
}
|
||||
return !!menu.items?.some(item => item.url === pathname)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarGroup v-for="group in navMain" :key="group.title">
|
||||
<UiSidebarGroupLabel>{{ group.title }}</UiSidebarGroupLabel>
|
||||
<UiSidebarMenu>
|
||||
<template v-for="menu in group.items" :key="menu.title">
|
||||
<UiSidebarMenuItem v-if="!menu.items">
|
||||
<UiSidebarMenuButton as-child :is-active="isActive(menu)" :tooltip="menu.title">
|
||||
<router-link :to="menu.url">
|
||||
<component :is="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
</router-link>
|
||||
</UiSidebarMenuButton>
|
||||
</UiSidebarMenuItem>
|
||||
|
||||
<UiSidebarMenuItem v-else>
|
||||
<!-- sidebar expanded -->
|
||||
<UiCollapsible
|
||||
v-if="state !== 'collapsed' || isMobile"
|
||||
as-child :default-open="isCollapsed(menu)"
|
||||
class="group/collapsible"
|
||||
>
|
||||
<UiSidebarMenuItem>
|
||||
<UiCollapsibleTrigger as-child>
|
||||
<UiSidebarMenuButton :tooltip="menu.title">
|
||||
<component :is="menu.icon" v-if="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</UiSidebarMenuButton>
|
||||
</UiCollapsibleTrigger>
|
||||
</UiSidebarMenuItem>
|
||||
<UiCollapsibleContent>
|
||||
<UiSidebarMenuSub>
|
||||
<UiSidebarMenuSubItem v-for="subItem in menu.items" :key="subItem.title">
|
||||
<UiSidebarMenuSubButton as-child :is-active="isActive(subItem as NavItem)">
|
||||
<router-link :to="subItem?.url || '/'">
|
||||
<component :is="subItem.icon" v-if="subItem.icon" />
|
||||
<span>{{ subItem.title }}</span>
|
||||
</router-link>
|
||||
</UiSidebarMenuSubButton>
|
||||
</UiSidebarMenuSubItem>
|
||||
</UiSidebarMenuSub>
|
||||
</UiCollapsibleContent>
|
||||
</UiCollapsible>
|
||||
|
||||
<!-- sidebar collapsed -->
|
||||
<UiDropdownMenu v-else>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton :tooltip="menu.title">
|
||||
<component :is="menu.icon" v-if="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" side="right">
|
||||
<UiDropdownMenuLabel>{{ menu.title }}</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem v-for="subItem in menu.items" :key="subItem.title" as-child>
|
||||
<router-link :to="subItem?.url || '/'">
|
||||
<component :is="subItem.icon" v-if="subItem.icon" />
|
||||
<span>{{ subItem.title }}</span>
|
||||
</router-link>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</UiSidebarMenuItem>
|
||||
</template>
|
||||
</UiSidebarMenu>
|
||||
</UiSidebarGroup>
|
||||
</template>
|
||||
100
monisuo-admin/src/components/app-sidebar/team-switcher.vue
Normal file
100
monisuo-admin/src/components/app-sidebar/team-switcher.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ChevronsUpDown,
|
||||
Plus,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { Team } from './types'
|
||||
|
||||
const { teams } = defineProps<{
|
||||
teams: Team[]
|
||||
}>()
|
||||
|
||||
const { isMobile, open } = useSidebar()
|
||||
|
||||
const activeTeam = ref<Team>(teams[0])
|
||||
function setActiveTeam(team: Team) {
|
||||
activeTeam.value = team
|
||||
}
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const showComponent = shallowRef<Component | null>(null)
|
||||
type TComponent = 'team-add'
|
||||
|
||||
function handleSelect(command: TComponent) {
|
||||
switch (command) {
|
||||
case 'team-add':
|
||||
showComponent.value = defineAsyncComponent(() => import('./nav-team-add.vue'))
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarMenu>
|
||||
<UiSidebarMenuItem>
|
||||
<UiDialog v-model:open="isOpen">
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center rounded-lg aspect-square size-8 bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<component :is="activeTeam.logo" class="size-4" />
|
||||
</div>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ activeTeam.name }}</span>
|
||||
<span class="text-xs truncate">{{ activeTeam.plan }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto" />
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent
|
||||
class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
align="start"
|
||||
:side="(isMobile || open) ? 'bottom' : 'right'"
|
||||
:side-offset="4"
|
||||
>
|
||||
<UiDropdownMenuLabel class="text-xs text-muted-foreground">
|
||||
Teams
|
||||
</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuItem
|
||||
v-for="(team, index) in teams"
|
||||
:key="team.name"
|
||||
class="gap-2 p-2"
|
||||
@click="setActiveTeam(team)"
|
||||
>
|
||||
<div class="flex items-center justify-center border rounded-sm size-6">
|
||||
<component :is="team.logo" class="size-4 shrink-0" />
|
||||
</div>
|
||||
{{ team.name }}
|
||||
<UiDropdownMenuShortcut>⌘{{ index + 1 }}</UiDropdownMenuShortcut>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuSeparator />
|
||||
|
||||
<UiDialogTrigger as-child>
|
||||
<UiDropdownMenuItem class="gap-2 p-2" @click.stop="handleSelect('team-add')">
|
||||
<div class="flex items-center justify-center border rounded-md size-6 bg-background">
|
||||
<Plus class="size-4" />
|
||||
</div>
|
||||
<div class="font-medium text-muted-foreground">
|
||||
Add team
|
||||
</div>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDialogTrigger>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<UiDialogContent>
|
||||
<component :is="showComponent" @close="isOpen = false" />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</UiSidebarMenuItem>
|
||||
</UiSidebarMenu>
|
||||
</template>
|
||||
42
monisuo-admin/src/components/app-sidebar/types.ts
Normal file
42
monisuo-admin/src/components/app-sidebar/types.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { LucideProps } from 'lucide-vue-next'
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
|
||||
type NavIcon = FunctionalComponent<LucideProps, Record<any, any>, any, Record<any, any>>
|
||||
|
||||
interface BaseNavItem {
|
||||
title: string
|
||||
icon?: NavIcon
|
||||
}
|
||||
|
||||
export type NavItem
|
||||
= | BaseNavItem & {
|
||||
items: (BaseNavItem & { url?: string })[]
|
||||
url?: never
|
||||
isActive?: boolean
|
||||
} | BaseNavItem & {
|
||||
url: string
|
||||
items?: never
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
title: string
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
export interface User {
|
||||
name: string
|
||||
avatar: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
name: string
|
||||
logo: NavIcon
|
||||
plan: string
|
||||
}
|
||||
|
||||
export interface SidebarData {
|
||||
user: User
|
||||
teams: Team[]
|
||||
navMain: NavGroup[]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const teamAddValidator = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, { error: 'Group name is required' })
|
||||
.max(50, { error: 'Group name must be less than 50 characters' }),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, { error: 'Group name is required' })
|
||||
.max(50, { error: 'Group name must be less than 50 characters' }),
|
||||
logo: z
|
||||
.string()
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type TeamAddValidator = z.infer<typeof teamAddValidator>
|
||||
Reference in New Issue
Block a user