feat:v1.0.0
This commit is contained in:
4
shadcn-admin/.vercel/project.json
Normal file
4
shadcn-admin/.vercel/project.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"orgId": "team_k7XASAgXeoo4C8RJJzrkEMK3",
|
||||
"projectId": "prj_E7eJmO5ydZwNTgA5OH1ECjr4JnO9"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
@@ -29,37 +29,37 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>Shadcn Admin</title>
|
||||
<meta name="title" content="Shadcn Admin" />
|
||||
<title>WEIAI 角色卡</title>
|
||||
<meta name="title" content="WEIAI 角色卡" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Admin Dashboard UI built with Shadcn and Vite."
|
||||
content="WEIAI 角色卡管理与配置控制台。"
|
||||
/>
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://shadcn-admin.netlify.app" />
|
||||
<meta property="og:title" content="Shadcn Admin" />
|
||||
<meta property="og:url" content="/" />
|
||||
<meta property="og:title" content="WEIAI 角色卡" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Admin Dashboard UI built with Shadcn and Vite."
|
||||
content="WEIAI 角色卡管理与配置控制台。"
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://shadcn-admin.netlify.app/images/shadcn-admin.png"
|
||||
content="/images/favicon.png"
|
||||
/>
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://shadcn-admin.netlify.app" />
|
||||
<meta property="twitter:title" content="Shadcn Admin" />
|
||||
<meta property="twitter:url" content="/" />
|
||||
<meta property="twitter:title" content="WEIAI 角色卡" />
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="Admin Dashboard UI built with Shadcn and Vite."
|
||||
content="WEIAI 角色卡管理与配置控制台。"
|
||||
/>
|
||||
<meta
|
||||
property="twitter:image"
|
||||
content="https://shadcn-admin.netlify.app/images/shadcn-admin.png"
|
||||
content="/images/favicon.png"
|
||||
/>
|
||||
|
||||
<!-- font family -->
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cn } from '@/lib/utils'
|
||||
export function Logo({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
id='shadcn-admin-logo'
|
||||
id='weiai-logo'
|
||||
viewBox='0 0 24 24'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
height='24'
|
||||
@@ -17,7 +17,7 @@ export function Logo({ className, ...props }: SVGProps<SVGSVGElement>) {
|
||||
className={cn('size-6', className)}
|
||||
{...props}
|
||||
>
|
||||
<title>Shadcn-Admin</title>
|
||||
<title>WEIAI</title>
|
||||
<path d='M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3' />
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -30,10 +30,10 @@ export function CommandMenu() {
|
||||
|
||||
return (
|
||||
<CommandDialog modal open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder='Type a command or search...' />
|
||||
<CommandInput placeholder='输入命令或搜索...' />
|
||||
<CommandList>
|
||||
<ScrollArea type='hover' className='h-72 pe-1'>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandEmpty>没有找到结果。</CommandEmpty>
|
||||
{sidebarData.navGroups.map((group) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
{group.items.map((navItem, i) => {
|
||||
@@ -71,17 +71,17 @@ export function CommandMenu() {
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading='Theme'>
|
||||
<CommandGroup heading='主题'>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('light'))}>
|
||||
<Sun /> <span>Light</span>
|
||||
<Sun /> <span>浅色</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('dark'))}>
|
||||
<Moon className='scale-90' />
|
||||
<span>Dark</span>
|
||||
<span>深色</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => setTheme('system'))}>
|
||||
<Laptop />
|
||||
<span>System</span>
|
||||
<span>跟随系统</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -51,14 +51,14 @@ export function ConfirmDialog(props: ConfirmDialogProps) {
|
||||
{children}
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isLoading}>
|
||||
{cancelBtnText ?? 'Cancel'}
|
||||
{cancelBtnText ?? '取消'}
|
||||
</AlertDialogCancel>
|
||||
<Button
|
||||
variant={destructive ? 'destructive' : 'default'}
|
||||
onClick={handleConfirm}
|
||||
disabled={disabled || isLoading}
|
||||
>
|
||||
{confirmText ?? 'Continue'}
|
||||
{confirmText ?? '继续'}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
|
||||
@@ -52,18 +52,18 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
<DropdownMenuContent align='start'>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||
<ArrowUpIcon className='size-3.5 text-muted-foreground/70' />
|
||||
Asc
|
||||
升序
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||
<ArrowDownIcon className='size-3.5 text-muted-foreground/70' />
|
||||
Desc
|
||||
降序
|
||||
</DropdownMenuItem>
|
||||
{column.getCanHide() && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||
<EyeNoneIcon className='size-3.5 text-muted-foreground/70' />
|
||||
Hide
|
||||
隐藏
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -59,7 +59,7 @@ export function DataTableFacetedFilter<TData, TValue>({
|
||||
variant='secondary'
|
||||
className='rounded-sm px-1 font-normal'
|
||||
>
|
||||
{selectedValues.size} selected
|
||||
已选 {selectedValues.size} 项
|
||||
</Badge>
|
||||
) : (
|
||||
options
|
||||
@@ -83,7 +83,7 @@ export function DataTableFacetedFilter<TData, TValue>({
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandEmpty>没有找到结果。</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedValues.has(option.value)
|
||||
@@ -133,7 +133,7 @@ export function DataTableFacetedFilter<TData, TValue>({
|
||||
onSelect={() => column?.setFilterValue(undefined)}
|
||||
className='justify-center text-center'
|
||||
>
|
||||
Clear filters
|
||||
清除筛选
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function DataTablePagination<TData>({
|
||||
>
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<div className='flex w-[100px] items-center justify-center text-sm font-medium @2xl/content:hidden'>
|
||||
Page {currentPage} of {totalPages}
|
||||
第 {currentPage} / {totalPages} 页
|
||||
</div>
|
||||
<div className='flex items-center gap-2 @max-2xl/content:flex-row-reverse'>
|
||||
<Select
|
||||
@@ -59,13 +59,13 @@ export function DataTablePagination<TData>({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className='hidden text-sm font-medium sm:block'>Rows per page</p>
|
||||
<p className='hidden text-sm font-medium sm:block'>每页行数</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center sm:space-x-6 lg:space-x-8'>
|
||||
<div className='flex w-[100px] items-center justify-center text-sm font-medium @max-3xl/content:hidden'>
|
||||
Page {currentPage} of {totalPages}
|
||||
第 {currentPage} / {totalPages} 页
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<Button
|
||||
@@ -74,7 +74,7 @@ export function DataTablePagination<TData>({
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to first page</span>
|
||||
<span className='sr-only'>跳到第一页</span>
|
||||
<DoubleArrowLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
@@ -83,7 +83,7 @@ export function DataTablePagination<TData>({
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to previous page</span>
|
||||
<span className='sr-only'>上一页</span>
|
||||
<ChevronLeftIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
|
||||
@@ -98,7 +98,7 @@ export function DataTablePagination<TData>({
|
||||
className='h-8 min-w-8 px-2'
|
||||
onClick={() => table.setPageIndex((pageNumber as number) - 1)}
|
||||
>
|
||||
<span className='sr-only'>Go to page {pageNumber}</span>
|
||||
<span className='sr-only'>跳到第 {pageNumber} 页</span>
|
||||
{pageNumber}
|
||||
</Button>
|
||||
)}
|
||||
@@ -111,7 +111,7 @@ export function DataTablePagination<TData>({
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to next page</span>
|
||||
<span className='sr-only'>下一页</span>
|
||||
<ChevronRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
<Button
|
||||
@@ -120,7 +120,7 @@ export function DataTablePagination<TData>({
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<span className='sr-only'>Go to last page</span>
|
||||
<span className='sr-only'>跳到最后一页</span>
|
||||
<DoubleArrowRightIcon className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ type DataTableToolbarProps<TData> = {
|
||||
|
||||
export function DataTableToolbar<TData>({
|
||||
table,
|
||||
searchPlaceholder = 'Filter...',
|
||||
searchPlaceholder = '筛选...',
|
||||
searchKey,
|
||||
filters = [],
|
||||
}: DataTableToolbarProps<TData>) {
|
||||
@@ -74,7 +74,7 @@ export function DataTableToolbar<TData>({
|
||||
}}
|
||||
className='h-8 px-2 lg:px-3'
|
||||
>
|
||||
Reset
|
||||
重置
|
||||
<Cross2Icon className='ms-2 h-4 w-4' />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -26,11 +26,11 @@ export function DataTableViewOptions<TData>({
|
||||
className='ms-auto hidden h-8 lg:flex'
|
||||
>
|
||||
<MixerHorizontalIcon className='size-4' />
|
||||
View
|
||||
视图
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-[150px]'>
|
||||
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
||||
<DropdownMenuLabel>显示列</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
|
||||
@@ -6,22 +6,17 @@ import {
|
||||
SidebarHeader,
|
||||
SidebarRail,
|
||||
} from '@/components/ui/sidebar'
|
||||
// import { AppTitle } from './app-title'
|
||||
import { AppTitle } from './app-title'
|
||||
import { sidebarData } from './data/sidebar-data'
|
||||
import { NavGroup } from './nav-group'
|
||||
import { NavUser } from './nav-user'
|
||||
import { TeamSwitcher } from './team-switcher'
|
||||
|
||||
export function AppSidebar() {
|
||||
const { collapsible, variant } = useLayout()
|
||||
return (
|
||||
<Sidebar collapsible={collapsible} variant={variant}>
|
||||
<SidebarHeader>
|
||||
<TeamSwitcher teams={sidebarData.teams} />
|
||||
|
||||
{/* Replace <TeamSwitch /> with the following <AppTitle />
|
||||
/* if you want to use the normal app title instead of TeamSwitch dropdown */}
|
||||
{/* <AppTitle /> */}
|
||||
<AppTitle />
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{sidebarData.navGroups.map((props) => (
|
||||
|
||||
@@ -25,8 +25,8 @@ export function AppTitle() {
|
||||
onClick={() => setOpenMobile(false)}
|
||||
className='grid flex-1 text-start text-sm leading-tight'
|
||||
>
|
||||
<span className='truncate font-bold'>Shadcn-Admin</span>
|
||||
<span className='truncate text-xs'>Vite + ShadcnUI</span>
|
||||
<span className='truncate font-bold'>WEIAI</span>
|
||||
<span className='truncate text-xs'>角色卡管理</span>
|
||||
</Link>
|
||||
<ToggleSidebar />
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@ function ToggleSidebar({
|
||||
>
|
||||
<X className='md:hidden' />
|
||||
<Menu className='max-md:hidden' />
|
||||
<span className='sr-only'>Toggle Sidebar</span>
|
||||
<span className='sr-only'>切换侧边栏</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,209 +1,28 @@
|
||||
import {
|
||||
Construction,
|
||||
LayoutDashboard,
|
||||
Monitor,
|
||||
Bug,
|
||||
ListTodo,
|
||||
FileX,
|
||||
HelpCircle,
|
||||
Lock,
|
||||
Bell,
|
||||
Package,
|
||||
Palette,
|
||||
ServerOff,
|
||||
Settings,
|
||||
Wrench,
|
||||
UserCog,
|
||||
UserX,
|
||||
Users,
|
||||
MessagesSquare,
|
||||
ShieldCheck,
|
||||
AudioWaveform,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
} from 'lucide-react'
|
||||
import { ClerkLogo } from '@/assets/clerk-logo'
|
||||
import { Command, Users } from 'lucide-react'
|
||||
import { type SidebarData } from '../types'
|
||||
|
||||
export const sidebarData: SidebarData = {
|
||||
user: {
|
||||
name: 'satnaing',
|
||||
email: 'satnaingdev@gmail.com',
|
||||
avatar: '/avatars/shadcn.jpg',
|
||||
name: 'WEIAI',
|
||||
email: 'weiai@example.com',
|
||||
avatar: '/avatars/01.png',
|
||||
},
|
||||
teams: [
|
||||
{
|
||||
name: 'Shadcn Admin',
|
||||
name: 'WEIAI',
|
||||
logo: Command,
|
||||
plan: 'Vite + ShadcnUI',
|
||||
},
|
||||
{
|
||||
name: 'Acme Inc',
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: 'Enterprise',
|
||||
},
|
||||
{
|
||||
name: 'Acme Corp.',
|
||||
logo: AudioWaveform,
|
||||
plan: 'Startup',
|
||||
plan: '角色卡',
|
||||
},
|
||||
],
|
||||
navGroups: [
|
||||
{
|
||||
title: 'General',
|
||||
title: '功能',
|
||||
items: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
url: '/',
|
||||
icon: LayoutDashboard,
|
||||
},
|
||||
{
|
||||
title: 'Tasks',
|
||||
url: '/tasks',
|
||||
icon: ListTodo,
|
||||
},
|
||||
{
|
||||
title: 'Characters',
|
||||
title: '角色卡',
|
||||
url: '/characters',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: 'Apps',
|
||||
url: '/apps',
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: 'Chats',
|
||||
url: '/chats',
|
||||
badge: '3',
|
||||
icon: MessagesSquare,
|
||||
},
|
||||
{
|
||||
title: 'Users',
|
||||
url: '/users',
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
title: 'Secured by Clerk',
|
||||
icon: ClerkLogo,
|
||||
items: [
|
||||
{
|
||||
title: 'Sign In',
|
||||
url: '/clerk/sign-in',
|
||||
},
|
||||
{
|
||||
title: 'Sign Up',
|
||||
url: '/clerk/sign-up',
|
||||
},
|
||||
{
|
||||
title: 'User Management',
|
||||
url: '/clerk/user-management',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Pages',
|
||||
items: [
|
||||
{
|
||||
title: 'Auth',
|
||||
icon: ShieldCheck,
|
||||
items: [
|
||||
{
|
||||
title: 'Sign In',
|
||||
url: '/sign-in',
|
||||
},
|
||||
{
|
||||
title: 'Sign In (2 Col)',
|
||||
url: '/sign-in-2',
|
||||
},
|
||||
{
|
||||
title: 'Sign Up',
|
||||
url: '/sign-up',
|
||||
},
|
||||
{
|
||||
title: 'Forgot Password',
|
||||
url: '/forgot-password',
|
||||
},
|
||||
{
|
||||
title: 'OTP',
|
||||
url: '/otp',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Errors',
|
||||
icon: Bug,
|
||||
items: [
|
||||
{
|
||||
title: 'Unauthorized',
|
||||
url: '/errors/unauthorized',
|
||||
icon: Lock,
|
||||
},
|
||||
{
|
||||
title: 'Forbidden',
|
||||
url: '/errors/forbidden',
|
||||
icon: UserX,
|
||||
},
|
||||
{
|
||||
title: 'Not Found',
|
||||
url: '/errors/not-found',
|
||||
icon: FileX,
|
||||
},
|
||||
{
|
||||
title: 'Internal Server Error',
|
||||
url: '/errors/internal-server-error',
|
||||
icon: ServerOff,
|
||||
},
|
||||
{
|
||||
title: 'Maintenance Error',
|
||||
url: '/errors/maintenance-error',
|
||||
icon: Construction,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Other',
|
||||
items: [
|
||||
{
|
||||
title: 'Settings',
|
||||
icon: Settings,
|
||||
items: [
|
||||
{
|
||||
title: 'Profile',
|
||||
url: '/settings',
|
||||
icon: UserCog,
|
||||
},
|
||||
{
|
||||
title: 'Account',
|
||||
url: '/settings/account',
|
||||
icon: Wrench,
|
||||
},
|
||||
{
|
||||
title: 'Appearance',
|
||||
url: '/settings/appearance',
|
||||
icon: Palette,
|
||||
},
|
||||
{
|
||||
title: 'Notifications',
|
||||
url: '/settings/notifications',
|
||||
icon: Bell,
|
||||
},
|
||||
{
|
||||
title: 'Display',
|
||||
url: '/settings/display',
|
||||
icon: Monitor,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Help Center',
|
||||
url: '/help-center',
|
||||
icon: HelpCircle,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
} from 'lucide-react'
|
||||
import { ChevronsUpDown, LogOut } from 'lucide-react'
|
||||
import useDialogState from '@/hooks/use-dialog-state'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
@@ -50,7 +41,7 @@ export function NavUser({ user }: NavUserProps) {
|
||||
>
|
||||
<Avatar className='h-8 w-8 rounded-lg'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
|
||||
<AvatarFallback className='rounded-lg'>WA</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grid flex-1 text-start text-sm leading-tight'>
|
||||
<span className='truncate font-semibold'>{user.name}</span>
|
||||
@@ -69,7 +60,7 @@ export function NavUser({ user }: NavUserProps) {
|
||||
<div className='flex items-center gap-2 px-1 py-1.5 text-start text-sm'>
|
||||
<Avatar className='h-8 w-8 rounded-lg'>
|
||||
<AvatarImage src={user.avatar} alt={user.name} />
|
||||
<AvatarFallback className='rounded-lg'>SN</AvatarFallback>
|
||||
<AvatarFallback className='rounded-lg'>WA</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className='grid flex-1 text-start text-sm leading-tight'>
|
||||
<span className='truncate font-semibold'>{user.name}</span>
|
||||
@@ -78,40 +69,12 @@ export function NavUser({ user }: NavUserProps) {
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings/account'>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings/notifications'>
|
||||
<Bell />
|
||||
Notifications
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
variant='destructive'
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<LogOut />
|
||||
Sign out
|
||||
退出登录
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import useDialogState from '@/hooks/use-dialog-state'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
@@ -23,45 +21,23 @@ export function ProfileDropdown() {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
|
||||
<Avatar className='h-8 w-8'>
|
||||
<AvatarImage src='/avatars/01.png' alt='@shadcn' />
|
||||
<AvatarFallback>SN</AvatarFallback>
|
||||
<AvatarImage src='/avatars/01.png' alt='@weiai' />
|
||||
<AvatarFallback>WA</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className='w-56' align='end' forceMount>
|
||||
<DropdownMenuLabel className='font-normal'>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<p className='text-sm leading-none font-medium'>satnaing</p>
|
||||
<p className='text-sm leading-none font-medium'>WEIAI</p>
|
||||
<p className='text-xs leading-none text-muted-foreground'>
|
||||
satnaingdev@gmail.com
|
||||
weiai@example.com
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Profile
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Billing
|
||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to='/settings'>
|
||||
Settings
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant='destructive' onClick={() => setOpen(true)}>
|
||||
Sign out
|
||||
退出登录
|
||||
<DropdownMenuShortcut className='text-current'>
|
||||
⇧⌘Q
|
||||
</DropdownMenuShortcut>
|
||||
|
||||
@@ -11,7 +11,7 @@ type SearchProps = {
|
||||
|
||||
export function Search({
|
||||
className = '',
|
||||
placeholder = 'Search',
|
||||
placeholder = '搜索',
|
||||
}: SearchProps) {
|
||||
const { setOpen } = useSearch()
|
||||
return (
|
||||
|
||||
@@ -27,9 +27,9 @@ export function SignOutDialog({ open, onOpenChange }: SignOutDialogProps) {
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title='Sign out'
|
||||
desc='Are you sure you want to sign out? You will need to sign in again to access your account.'
|
||||
confirmText='Sign out'
|
||||
title='退出登录'
|
||||
desc='确定要退出登录吗?退出后需要重新登录才能访问账号。'
|
||||
confirmText='退出登录'
|
||||
destructive
|
||||
handleConfirm={handleSignOut}
|
||||
className='sm:max-w-sm'
|
||||
|
||||
@@ -27,26 +27,26 @@ export function ThemeSwitch() {
|
||||
<Button variant='ghost' size='icon' className='scale-95 rounded-full'>
|
||||
<Sun className='size-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90' />
|
||||
<Moon className='absolute size-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0' />
|
||||
<span className='sr-only'>Toggle theme</span>
|
||||
<span className='sr-only'>切换主题</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>
|
||||
Light{' '}
|
||||
浅色{' '}
|
||||
<Check
|
||||
size={14}
|
||||
className={cn('ms-auto', theme !== 'light' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
||||
Dark
|
||||
深色
|
||||
<Check
|
||||
size={14}
|
||||
className={cn('ms-auto', theme !== 'dark' && 'hidden')}
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>
|
||||
System
|
||||
跟随系统
|
||||
<Check
|
||||
size={14}
|
||||
className={cn('ms-auto', theme !== 'system' && 'hidden')}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function AuthLayout({ children }: AuthLayoutProps) {
|
||||
<div className='mx-auto flex w-full flex-col justify-center space-y-2 py-8 sm:w-[480px] sm:p-8'>
|
||||
<div className='mb-4 flex items-center justify-center'>
|
||||
<Logo className='me-2' />
|
||||
<h1 className='text-xl font-medium'>Shadcn Admin</h1>
|
||||
<h1 className='text-xl font-medium'>WEIAI</h1>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,8 @@ import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { Link, useNavigate } from '@tanstack/react-router'
|
||||
import { Loader2, LogIn } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { IconFacebook, IconGithub, IconGoogle } from '@/assets/brand-icons'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import { sleep, cn } from '@/lib/utils'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { supabase } from '@/lib/supabase'
|
||||
import {
|
||||
@@ -85,22 +84,6 @@ export function UserAuthForm({
|
||||
}
|
||||
}
|
||||
|
||||
const handleGoogleLogin = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google',
|
||||
options: {
|
||||
redirectTo: `${window.location.origin}/`,
|
||||
},
|
||||
})
|
||||
if (error) throw error
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || 'Google login failed')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
@@ -113,7 +96,7 @@ export function UserAuthForm({
|
||||
name='email'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormLabel>邮箱</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='name@example.com' {...field} />
|
||||
</FormControl>
|
||||
@@ -126,7 +109,7 @@ export function UserAuthForm({
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem className='relative'>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormLabel>密码</FormLabel>
|
||||
<FormControl>
|
||||
<PasswordInput placeholder='********' {...field} />
|
||||
</FormControl>
|
||||
@@ -135,14 +118,14 @@ export function UserAuthForm({
|
||||
to='/forgot-password'
|
||||
className='absolute end-0 -top-0.5 text-sm font-medium text-muted-foreground hover:opacity-75'
|
||||
>
|
||||
Forgot password?
|
||||
忘记密码?
|
||||
</Link>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button className='mt-2' disabled={isLoading}>
|
||||
{isLoading ? <Loader2 className='animate-spin' /> : <LogIn />}
|
||||
Sign in
|
||||
登录
|
||||
</Button>
|
||||
|
||||
<div className='relative my-2'>
|
||||
@@ -151,7 +134,7 @@ export function UserAuthForm({
|
||||
</div>
|
||||
<div className='relative flex justify-center text-xs uppercase'>
|
||||
<span className='bg-background px-2 text-muted-foreground'>
|
||||
Or continue with
|
||||
或使用以下方式
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,10 +17,9 @@ export function SignIn() {
|
||||
<AuthLayout>
|
||||
<Card className='gap-4'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-lg tracking-tight'>Sign in</CardTitle>
|
||||
<CardTitle className='text-lg tracking-tight'>登录</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your email and password below to <br />
|
||||
log into your account
|
||||
请输入邮箱和密码登录账号
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -28,19 +27,19 @@ export function SignIn() {
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<p className='px-8 text-center text-sm text-muted-foreground'>
|
||||
By clicking sign in, you agree to our{' '}
|
||||
点击登录即表示你同意我们的{' '}
|
||||
<a
|
||||
href='/terms'
|
||||
className='underline underline-offset-4 hover:text-primary'
|
||||
>
|
||||
Terms of Service
|
||||
服务条款
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
和{' '}
|
||||
<a
|
||||
href='/privacy'
|
||||
className='underline underline-offset-4 hover:text-primary'
|
||||
>
|
||||
Privacy Policy
|
||||
隐私政策
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
|
||||
@@ -11,32 +11,31 @@ export function SignIn2() {
|
||||
<div className='mx-auto flex w-full flex-col justify-center space-y-2 py-8 sm:w-[480px] sm:p-8'>
|
||||
<div className='mb-4 flex items-center justify-center'>
|
||||
<Logo className='me-2' />
|
||||
<h1 className='text-xl font-medium'>Shadcn Admin</h1>
|
||||
<h1 className='text-xl font-medium'>WEIAI</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto flex w-full max-w-sm flex-col justify-center space-y-2'>
|
||||
<div className='flex flex-col space-y-2 text-start'>
|
||||
<h2 className='text-lg font-semibold tracking-tight'>Sign in</h2>
|
||||
<h2 className='text-lg font-semibold tracking-tight'>登录</h2>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Enter your email and password below <br />
|
||||
to log into your account
|
||||
请输入邮箱和密码登录账号
|
||||
</p>
|
||||
</div>
|
||||
<UserAuthForm />
|
||||
<p className='px-8 text-center text-sm text-muted-foreground'>
|
||||
By clicking sign in, you agree to our{' '}
|
||||
点击登录即表示你同意我们的{' '}
|
||||
<a
|
||||
href='/terms'
|
||||
className='underline underline-offset-4 hover:text-primary'
|
||||
>
|
||||
Terms of Service
|
||||
服务条款
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
和{' '}
|
||||
<a
|
||||
href='/privacy'
|
||||
className='underline underline-offset-4 hover:text-primary'
|
||||
>
|
||||
Privacy Policy
|
||||
隐私政策
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
@@ -54,14 +53,14 @@ export function SignIn2() {
|
||||
className='dark:hidden'
|
||||
width={1024}
|
||||
height={1151}
|
||||
alt='Shadcn-Admin'
|
||||
alt='WEIAI'
|
||||
/>
|
||||
<img
|
||||
src={dashboardDark}
|
||||
className='hidden dark:block'
|
||||
width={1024}
|
||||
height={1138}
|
||||
alt='Shadcn-Admin'
|
||||
alt='WEIAI'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,13 +82,13 @@ export function CharacterDialog() {
|
||||
console.log('🔧 处理后的数据:', sanitizedValues);
|
||||
|
||||
if (isEdit) {
|
||||
console.log('🔄 更新角色,ID:', character.id);
|
||||
console.log('🔄 更新角色卡,ID:', character.id);
|
||||
await updateCharacter({ ...sanitizedValues, id: character.id } as any);
|
||||
toast.success('角色更新成功');
|
||||
toast.success('角色卡更新成功');
|
||||
} else {
|
||||
console.log('➕ 创建新角色');
|
||||
console.log('➕ 创建新角色卡');
|
||||
await createCharacter(sanitizedValues as any);
|
||||
toast.success('角色创建成功');
|
||||
toast.success('角色卡创建成功');
|
||||
}
|
||||
setOpen(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['characters'] });
|
||||
@@ -100,7 +100,7 @@ export function CharacterDialog() {
|
||||
? error.message
|
||||
: JSON.stringify(error);
|
||||
|
||||
toast.error(`保存角色失败: ${errorMessage}`);
|
||||
toast.error(`保存角色卡失败: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -108,11 +108,11 @@ export function CharacterDialog() {
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className='max-h-[85vh] overflow-y-auto sm:max-w-2xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? '编辑角色' : '创建角色'}</DialogTitle>
|
||||
<DialogTitle>{isEdit ? '编辑角色卡' : '创建角色卡'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit
|
||||
? '在下方修改角色详情。'
|
||||
: '填写详情以创建新角色。'}
|
||||
? '在下方修改角色卡详情。'
|
||||
: '填写详情以创建新角色卡。'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
@@ -140,7 +140,7 @@ export function CharacterDialog() {
|
||||
<FormItem>
|
||||
<FormLabel>名称</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder='角色名称' {...field} />
|
||||
<Input placeholder='角色卡名称' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -150,13 +150,13 @@ export function CharacterDialog() {
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='avatar_path'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>角色头像</FormLabel>
|
||||
<FormControl>
|
||||
<ImageUpload
|
||||
value={field.value}
|
||||
name='avatar_path'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>角色卡头像</FormLabel>
|
||||
<FormControl>
|
||||
<ImageUpload
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -172,7 +172,7 @@ export function CharacterDialog() {
|
||||
<FormItem>
|
||||
<FormLabel>描述</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder='完整角色描述' {...field} value={field.value || ''} />
|
||||
<Textarea placeholder='完整角色卡描述' {...field} value={field.value || ''} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -241,7 +241,7 @@ export function CharacterDialog() {
|
||||
name='ai_system_prompt'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>系统提示词 (System Prompt)</FormLabel>
|
||||
<FormLabel>系统提示词</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder='你是一个乐于助人的助手...'
|
||||
|
||||
@@ -11,7 +11,7 @@ export function CharactersPrimaryButtons() {
|
||||
|
||||
return (
|
||||
<div className='flex gap-2'>
|
||||
<Button onClick={handleCreate}>创建角色</Button>
|
||||
<Button onClick={handleCreate}>创建角色卡</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,11 +96,11 @@ export function CharactersTable({ data }: DataTableProps) {
|
||||
<div className='flex flex-1 flex-col gap-4'>
|
||||
<DataTableToolbar
|
||||
table={table}
|
||||
searchPlaceholder='Filter by name...'
|
||||
searchPlaceholder='按名称搜索...'
|
||||
filters={[
|
||||
{
|
||||
columnId: 'status',
|
||||
title: 'Status',
|
||||
title: '状态',
|
||||
options: statuses,
|
||||
},
|
||||
]}
|
||||
@@ -159,7 +159,7 @@ export function CharactersTable({ data }: DataTableProps) {
|
||||
colSpan={columns.length}
|
||||
className='h-24 text-center'
|
||||
>
|
||||
No results.
|
||||
暂无结果
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@@ -31,7 +31,7 @@ export function DataTableRowActions<TData>({
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (confirm('确认删除该角色吗?')) {
|
||||
if (confirm('确认删除该角色卡吗?')) {
|
||||
try {
|
||||
if (character.id) {
|
||||
await deleteCharacter(character.id);
|
||||
|
||||
@@ -3,17 +3,17 @@ import { Signal, User, UserX } from 'lucide-react';
|
||||
export const statuses = [
|
||||
{
|
||||
value: 'online',
|
||||
label: 'Online',
|
||||
label: '在线',
|
||||
icon: Signal,
|
||||
},
|
||||
{
|
||||
value: 'busy',
|
||||
label: 'Busy',
|
||||
label: '忙碌',
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
value: 'offline',
|
||||
label: 'Offline',
|
||||
label: '离线',
|
||||
icon: UserX,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function Characters() {
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <div>Error loading characters: {(error as Error).message}</div>;
|
||||
return <div>加载角色卡失败:{(error as Error).message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -33,16 +33,16 @@ export default function Characters() {
|
||||
<Main className='flex flex-1 flex-col gap-4 sm:gap-6'>
|
||||
<div className='flex flex-wrap items-end justify-between gap-2'>
|
||||
<div>
|
||||
<h2 className='text-2xl font-bold tracking-tight'>Characters</h2>
|
||||
<h2 className='text-2xl font-bold tracking-tight'>角色卡</h2>
|
||||
<p className='text-muted-foreground'>
|
||||
Manage your AI characters and their configurations.
|
||||
管理你的 AI 角色卡与相关配置。
|
||||
</p>
|
||||
</div>
|
||||
<CharactersPrimaryButtons />
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div>Loading...</div>
|
||||
<div>加载中...</div>
|
||||
) : (
|
||||
<CharactersTable data={characters || []} />
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { Dashboard } from '@/features/dashboard'
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/')({
|
||||
component: Dashboard,
|
||||
beforeLoad: () => {
|
||||
throw redirect({ to: '/characters' })
|
||||
},
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ function ClerkAuthLayout() {
|
||||
className='relative z-20 flex items-center text-lg font-medium'
|
||||
>
|
||||
<Logo className='me-2' />
|
||||
Shadcn Admin
|
||||
WEIAI
|
||||
</Link>
|
||||
|
||||
<ClerkFullLogo className='relative m-auto size-96' />
|
||||
@@ -42,13 +42,13 @@ function ClerkAuthLayout() {
|
||||
}}
|
||||
contentProps={{ side: 'top', align: 'end', className: 'w-auto' }}
|
||||
>
|
||||
Welcome to the example Clerk auth page. <br />
|
||||
Back to{' '}
|
||||
欢迎使用示例 Clerk 登录页。<br />
|
||||
返回{' '}
|
||||
<Link
|
||||
to='/'
|
||||
className='underline decoration-dashed underline-offset-2'
|
||||
>
|
||||
Dashboard
|
||||
角色卡
|
||||
</Link>{' '}
|
||||
?
|
||||
</LearnMore>
|
||||
|
||||
8
shadcn-admin/vercel.json
Normal file
8
shadcn-admin/vercel.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user