Initial commit from Create vite
This commit is contained in:
9
src/admin/assets/logo-avatar.svg
Normal file
9
src/admin/assets/logo-avatar.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 106 KiB |
BIN
src/admin/assets/logo.webp
Normal file
BIN
src/admin/assets/logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
254
src/admin/clients.tsx
Normal file
254
src/admin/clients.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import {
|
||||
IdCard,
|
||||
MessageCircleMoreIcon,
|
||||
PanelLeftCloseIcon,
|
||||
PanelLeftOpenIcon,
|
||||
UserRound,
|
||||
} from "lucide-react"
|
||||
import { type ReactNode, useState } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import logoAvatar from "./assets/logo-avatar.svg"
|
||||
|
||||
export function Shell(props: { children: ReactNode; navbar: boolean }) {
|
||||
const { navbar } = props
|
||||
return (
|
||||
<div
|
||||
data-expand={navbar}
|
||||
className={cn(
|
||||
`transition-[grid-template-columns] duration-300 ease-in-out`,
|
||||
`w-full h-full grid`,
|
||||
`grid-rows-[64px_1fr]`,
|
||||
`data-[expand=true]:grid-cols-[200px_1fr]`,
|
||||
`data-[expand=false]:grid-cols-[0px_1fr]`,
|
||||
`md:data-[expand=false]:grid-cols-[64px_1fr]`,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Mask(props: {
|
||||
navbar: boolean
|
||||
setNavbar: (v: boolean) => void
|
||||
}) {
|
||||
const { navbar, setNavbar } = props
|
||||
|
||||
return (
|
||||
<button
|
||||
data-expand={navbar}
|
||||
className={cn(
|
||||
`lg:hidden`,
|
||||
`transition-opacity duration-300 ease-in-out`,
|
||||
`col-start-1 row-start-1 col-span-2 row-span-2 bg-black/50 z-10`,
|
||||
`data-[expand=true]:opacity-100 data-[expand=false]:opacity-0`,
|
||||
`data-[expand=true]:pointer-events-auto data-[expand=false]:pointer-events-none`,
|
||||
)}
|
||||
onClick={() => setNavbar(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Content(props: { children: ReactNode; navbar: boolean }) {
|
||||
const { navbar } = props
|
||||
return (
|
||||
<div
|
||||
data-expand={navbar}
|
||||
className={cn(
|
||||
`transition-[margin] duration-300 ease-in-out`,
|
||||
`absolute inset-0 overflow-hidden`,
|
||||
`mt-16`,
|
||||
`md:ml-16`,
|
||||
`lg:data-[expand=true]:ml-50`,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Header(props: { navbar: boolean; toggleNavbar: () => void }) {
|
||||
const { navbar, toggleNavbar } = props
|
||||
return (
|
||||
<header className={`flex-none h-16 overflow-hidden flex items-stretch`}>
|
||||
<div className="flex-auto flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-9 ml-4 md:ml-0"
|
||||
onClick={toggleNavbar}
|
||||
>
|
||||
{navbar ? <PanelLeftCloseIcon /> : <PanelLeftOpenIcon />}
|
||||
</Button>
|
||||
<div className="max-md:hidden h-5 w-px bg-gray-300 mx-2" />
|
||||
|
||||
<Link
|
||||
to="/admin/identify"
|
||||
className="max-md:hidden flex items-center gap-1.5 text-sm text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
<IdCard size={16} />
|
||||
<span>实名认证</span>
|
||||
</Link>
|
||||
|
||||
<div className="max-md:hidden h-5 w-px bg-gray-300 mx-2" />
|
||||
<a
|
||||
href="https://wpa1.qq.com/K0s0cvwf?_type=wpa&qidian=true"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="max-md:hidden flex items-center gap-1.5 text-sm text-blue-600 hover:text-blue-800 transition-colors mr-2"
|
||||
>
|
||||
<MessageCircleMoreIcon size={16} />
|
||||
<span>联系专属客服</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex-none flex items-center justify-end pr-4 max-md:hidden gap-3">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex-none h-16 flex items-center justify-center text-sm"
|
||||
>
|
||||
返回首页
|
||||
</Link>
|
||||
<UserCenter />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function UserCenter() {
|
||||
return <div className="flex items-center gap-2">{"用户"}</div>
|
||||
}
|
||||
|
||||
export function Navbar(props: {
|
||||
navbar: boolean
|
||||
setNavbar: (v: boolean) => void
|
||||
}) {
|
||||
const { navbar, setNavbar } = props
|
||||
const closeNavBarIfMobile = () => {
|
||||
if (window.innerWidth < 768) {
|
||||
setNavbar(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<nav
|
||||
data-expand={navbar}
|
||||
className={cn(
|
||||
`transition-[flex-basis] duration-300 ease-in-out`,
|
||||
`h-full flex flex-col overflow-hidden group`,
|
||||
`data-[expand=true]:basis-52 data-[expand=false]:basis-16`,
|
||||
)}
|
||||
>
|
||||
{/* logo */}
|
||||
<Link
|
||||
to="/"
|
||||
className={`flex-none h-16 flex items-center justify-center`}
|
||||
onClick={closeNavBarIfMobile}
|
||||
>
|
||||
<img src={logoAvatar} alt="logo" className="w-10 h-10 object-contain" />
|
||||
</Link>
|
||||
|
||||
{/* routes */}
|
||||
<section
|
||||
className={cn(
|
||||
`transition-[padding] duration-300 ease-in-out`,
|
||||
`flex-auto overflow-x-hidden overflow-y-auto pb-4`,
|
||||
`group-data-[expand=true]:px-4 group-data-[expand=false]:px-3`,
|
||||
)}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<NavItem
|
||||
href="/admin"
|
||||
icon={<UserRound size={20} />}
|
||||
label="账户总览"
|
||||
expand={navbar}
|
||||
/>
|
||||
<NavTitle label="快速开始" />
|
||||
</TooltipProvider>
|
||||
</section>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
function NavTitle(props: { label: string }) {
|
||||
return (
|
||||
<p
|
||||
className={cn(
|
||||
`transition-[height] duration-300 ease-in-out`,
|
||||
`text-sm text-gray-500 whitespace-nowrap flex items-center relative`,
|
||||
`group-data-[expand=true]:h-9`,
|
||||
`group-data-[expand=false]:h-4`,
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
`transition-opacity duration-200 ease-in-out absolute mx-4`,
|
||||
`group-data-[expand=true]:delay-100 group-data-[expand=true]:opacity-100 group-data-[expand=false]:opacity-0`,
|
||||
)}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
`transition-opacity duration-200 ease-in-out absolute w-full border-b block`,
|
||||
`group-data-[expand=false]:delay-100 group-data-[expand=false]:opacity-100 group-data-[expand=true]:opacity-0`,
|
||||
)}
|
||||
></span>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
function NavItem(props: {
|
||||
href: string
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
expand?: boolean
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (!props.expand) {
|
||||
setOpen(open)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip open={open} onOpenChange={handleOpenChange}>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
className={cn(
|
||||
`transition-[padding,background] duration-300 ease-in-out`,
|
||||
`flex items-center rounded-md gap-2 whitespace-nowrap`,
|
||||
`hover:bg-gray-100`,
|
||||
`group-data-[expand=true]:px-4`,
|
||||
)}
|
||||
to={props.href}
|
||||
>
|
||||
<span className="flex-none w-10 h-10 flex items-center justify-center">
|
||||
{props.icon}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
`flex-auto`,
|
||||
`transition-[width,opacity] duration-300 ease-in-out`,
|
||||
`group-data-[expand=true]:w-auto group-data-[expand=true]:opacity-100`,
|
||||
`group-data-[expand=false]:w-0 group-data-[expand=false]:opacity-0`,
|
||||
)}
|
||||
>
|
||||
{props.label}
|
||||
</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={16}>
|
||||
<p>{props.label}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
5
src/admin/dashboard/page.tsx
Normal file
5
src/admin/dashboard/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import Page from "@/components/page"
|
||||
|
||||
export default function DashboardPage() {
|
||||
return <Page>账户总览页面</Page>
|
||||
}
|
||||
50
src/admin/layout.tsx
Normal file
50
src/admin/layout.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { useState } from "react"
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { Content, Header, Mask, Navbar, Shell } from "./clients"
|
||||
|
||||
export default function AdminLayout() {
|
||||
const [navbar, setNavbar] = useState(true)
|
||||
|
||||
const toggleNavbar = () => setNavbar(!navbar)
|
||||
return (
|
||||
<div className="relative h-dvh overflow-hidden">
|
||||
<Shell navbar={navbar}>
|
||||
<div className="col-start-1 row-start-1 row-span-2 bg-card overflow-hidden relative z-20">
|
||||
<Navbar navbar={navbar} setNavbar={setNavbar} />
|
||||
</div>
|
||||
|
||||
<div className="col-start-2 row-start-1 bg-card overflow-hidden relative z-20">
|
||||
<Header navbar={navbar} toggleNavbar={toggleNavbar} />
|
||||
</div>
|
||||
|
||||
<svg
|
||||
className="col-start-2 row-start-2 w-full h-full z-20 pointer-events-none"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
<defs>
|
||||
<mask id="top-left-rounded-mask">
|
||||
<rect width="100%" height="100%" fill="white" />
|
||||
<circle cx="16" cy="16" r="16" fill="black" />
|
||||
<rect x="16" y="0" width="100%" height="32" fill="black" />
|
||||
<rect x="0" y="16" width="32" height="100%" fill="black" />
|
||||
<rect x="16" y="16" width="100%" height="100%" fill="black" />
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
width="100%"
|
||||
height="100%"
|
||||
className="fill-card"
|
||||
mask="url(#top-left-rounded-mask)"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<Mask navbar={navbar} setNavbar={setNavbar} />
|
||||
</Shell>
|
||||
|
||||
{/* 用 Outlet 替代 {props.children} */}
|
||||
<Content navbar={navbar}>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user