Initial commit: 网络验证平台

This commit is contained in:
Admin
2026-04-27 17:22:56 +08:00
commit afe67d704e
780 changed files with 88960 additions and 0 deletions
@@ -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!,
}
@@ -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>
@@ -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>
@@ -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>
@@ -0,0 +1,136 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import {
ChevronRight,
} from 'lucide-vue-next'
import { useRoute } from 'vue-router'
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 v-if="state !== 'collapsed'">{{ group.title }}</UiSidebarGroupLabel>
<UiSidebarMenu>
<template v-for="menu in group.items" :key="menu.title">
<UiSidebarMenuItem v-if="!menu.items">
<UiTooltip v-if="state === 'collapsed' && !isMobile">
<UiTooltipTrigger as-child>
<UiSidebarMenuButton as-child :is-active="isActive(menu)">
<router-link :to="menu.url">
<component :is="menu.icon" v-if="typeof menu.icon === 'function'" class="size-4" />
<Icon v-else-if="menu.icon" :icon="menu.icon" />
<span>{{ menu.title }}</span>
</router-link>
</UiSidebarMenuButton>
</UiTooltipTrigger>
<UiTooltipContent side="right">
{{ menu.title }}
</UiTooltipContent>
</UiTooltip>
<UiSidebarMenuButton v-else as-child :is-active="isActive(menu)">
<router-link :to="menu.url">
<component :is="menu.icon" v-if="typeof menu.icon === 'function'" class="size-4" />
<Icon v-else-if="menu.icon" :icon="menu.icon" />
<span>{{ menu.title }}</span>
</router-link>
</UiSidebarMenuButton>
</UiSidebarMenuItem>
<UiSidebarMenuItem v-else>
<UiCollapsible
v-if="state !== 'collapsed' || isMobile"
as-child
:default-open="isCollapsed(menu)"
class="group/collapsible"
>
<UiSidebarMenuItem>
<UiCollapsibleTrigger as-child>
<UiSidebarMenuButton>
<component :is="menu.icon" v-if="typeof menu.icon === 'function'" class="size-4" />
<Icon v-else-if="menu.icon" :icon="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="typeof subItem.icon === 'function'" class="size-4" />
<Icon v-else-if="subItem.icon" :icon="subItem.icon" />
<span>{{ subItem.title }}</span>
</router-link>
</UiSidebarMenuSubButton>
</UiSidebarMenuSubItem>
</UiSidebarMenuSub>
</UiCollapsibleContent>
</UiCollapsible>
<UiTooltip v-else>
<UiTooltipTrigger as-child>
<UiSidebarMenuButton>
<component :is="menu.icon" v-if="typeof menu.icon === 'function'" class="size-4" />
<Icon v-else-if="menu.icon" :icon="menu.icon" />
<span class="sr-only">{{ menu.title }}</span>
</UiSidebarMenuButton>
</UiTooltipTrigger>
<UiTooltipContent side="right">
{{ menu.title }}
</UiTooltipContent>
</UiTooltip>
<UiDropdownMenu v-if="state === 'collapsed' && !isMobile">
<UiDropdownMenuTrigger as-child>
<span />
</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="typeof subItem.icon === 'function'" class="size-4" />
<Icon v-else-if="subItem.icon" :icon="subItem.icon" />
<span>{{ subItem.title }}</span>
</router-link>
</UiDropdownMenuItem>
</UiDropdownMenuContent>
</UiDropdownMenu>
</UiSidebarMenuItem>
</template>
</UiSidebarMenu>
</UiSidebarGroup>
</template>
@@ -0,0 +1,31 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue'
import { ref } from 'vue'
import type { Team } from './types'
const { teams } = defineProps<{
teams: Team[]
}>()
const activeTeam = ref<Team>(teams[0])
</script>
<template>
<UiSidebarMenu>
<UiSidebarMenuItem>
<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" v-if="typeof activeTeam.logo === 'function'" class="size-4" />
<Icon v-else :icon="activeTeam.logo" class="size-4" />
</div>
<div class="grid flex-1 text-sm leading-tight">
<span class="font-semibold truncate">{{ activeTeam.name }}</span>
<span class="text-xs truncate text-muted-foreground">{{ activeTeam.plan }}</span>
</div>
</UiSidebarMenuButton>
</UiSidebarMenuItem>
</UiSidebarMenu>
</template>
@@ -0,0 +1,43 @@
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
role?: 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>