Initial commit from Create vite

This commit is contained in:
Eamon
2026-07-24 18:00:22 +08:00
commit 0361853732
79 changed files with 4803 additions and 0 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

254
src/admin/clients.tsx Normal file
View 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>
)
}

View File

@@ -0,0 +1,5 @@
import Page from "@/components/page"
export default function DashboardPage() {
return <Page>账户总览页面</Page>
}

50
src/admin/layout.tsx Normal file
View 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>
)
}