首页页面

This commit is contained in:
Eamon
2026-07-30 13:55:28 +08:00
parent 0361853732
commit 0d5e28c785
48 changed files with 2141 additions and 294 deletions

BIN
src/admin/assets/b10.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/admin/assets/b11.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/admin/assets/b12.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/admin/assets/b3.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

BIN
src/admin/assets/b4.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

BIN
src/admin/assets/b5.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

BIN
src/admin/assets/b6.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 996 B

BIN
src/admin/assets/b7.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

BIN
src/admin/assets/b8.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

BIN
src/admin/assets/b9.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,5 @@
import Page from "@/components/page"
export default function ChannelsPage() {
return <Page>IP管理</Page>
}

View File

@@ -1,9 +1,19 @@
import {
IdCard,
MessageCircleMoreIcon,
Archive,
CalendarClock,
CircleDollarSignIcon,
ClipboardClock,
ClipboardList,
Clock1,
FolderCode,
LayoutDashboard,
PanelLeftCloseIcon,
PanelLeftOpenIcon,
UserRound,
ReceiptJapaneseYenIcon,
Router,
ShoppingBag,
TicketPercent,
UserCheck,
} from "lucide-react"
import { type ReactNode, useState } from "react"
import { Link } from "react-router-dom"
@@ -16,19 +26,20 @@ import {
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
import logoAvatar from "./assets/logo-avatar.svg"
import { useLayout } from "./layout-context"
export function Shell(props: { children: ReactNode; navbar: boolean }) {
const { navbar } = props
export function Shell(props: { children: ReactNode }) {
const { navbar } = useLayout()
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]`,
"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}
@@ -36,49 +47,45 @@ export function Shell(props: { children: ReactNode; navbar: boolean }) {
)
}
export function Mask(props: {
navbar: boolean
setNavbar: (v: boolean) => void
}) {
const { navbar, setNavbar } = props
export function Content(props: { children: ReactNode }) {
const { navbar } = useLayout()
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-[200px]",
)}
>
{props.children}
</div>
)
}
export function Mask() {
const { navbar, setNavbar } = useLayout()
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`,
"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
export function Header() {
const { navbar, toggleNavbar } = useLayout()
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`}>
<header className="flex-none h-16 overflow-hidden flex items-stretch">
<div className="flex-auto flex items-center gap-2">
<Button
variant="ghost"
@@ -87,26 +94,6 @@ export function Header(props: { navbar: boolean; toggleNavbar: () => void }) {
>
{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">
@@ -123,19 +110,11 @@ export function Header(props: { navbar: boolean; toggleNavbar: () => void }) {
}
function UserCenter() {
return <div className="flex items-center gap-2">{"用户"}</div>
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)
}
}
export function Navbar() {
const { navbar } = useLayout()
return (
<nav
@@ -146,65 +125,102 @@ export function Navbar(props: {
`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}
>
<Link to="/" className="flex-none h-16 flex items-center justify-center">
<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`,
"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} />}
icon={<LayoutDashboard size={20} />}
label="账户总览"
expand={navbar}
/>
<NavTitle label="快速开始" />
<NavItem
href="/admin/whitelist"
icon={<UserCheck size={20} />}
label="白名单"
expand={navbar}
/>
<NavItem
href="/admin/channels"
icon={<FolderCode size={20} />}
label="IP账号管理"
expand={navbar}
/>
<NavItem
href="/admin/funds"
icon={<CircleDollarSignIcon size={20} />}
label="资金明细"
expand={navbar}
/>
<NavItem
href="/admin/channels"
icon={<ShoppingBag size={20} />}
label="订单管理"
expand={navbar}
/>
<NavItem
href="/admin/channels"
icon={<ClipboardList size={20} />}
label="退货管理"
expand={navbar}
/>
<NavItem
href="/admin/coupon"
icon={<TicketPercent size={20} />}
label="优惠券"
expand={navbar}
/>
<NavItem
href="/admin/channels"
icon={<ReceiptJapaneseYenIcon size={20} />}
label="提现管理"
expand={navbar}
/>
<NavItem
href="/admin/record"
icon={<Archive size={20} />}
label="充值记录"
expand={navbar}
/>
<NavItem
href="/admin/resources_short"
icon={<Clock1 size={20} />}
label="短效套餐"
expand={navbar}
/>
<NavItem
href="/admin/resources_long"
icon={<CalendarClock size={20} />}
label="长效管理"
expand={navbar}
/>
<NavItem
href="/admin/channels"
icon={<Router size={20} />}
label="软路由"
expand={navbar}
/>
<NavItem
href="/admin/channels"
icon={<ClipboardClock size={20} />}
label="使用记录"
expand={navbar}
/>
</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
@@ -212,6 +228,7 @@ function NavItem(props: {
expand?: boolean
}) {
const [open, setOpen] = useState(false)
const { setNavbar } = useLayout()
const handleOpenChange = (open: boolean) => {
if (!props.expand) {
@@ -219,6 +236,12 @@ function NavItem(props: {
}
}
const closeNavBarIfMobile = () => {
if (window.innerWidth < 768) {
setNavbar(false)
}
}
return (
<Tooltip open={open} onOpenChange={handleOpenChange}>
<TooltipTrigger asChild>
@@ -230,6 +253,7 @@ function NavItem(props: {
`group-data-[expand=true]:px-4`,
)}
to={props.href}
onClick={closeNavBarIfMobile}
>
<span className="flex-none w-10 h-10 flex items-center justify-center">
{props.icon}

View File

@@ -0,0 +1,5 @@
import Page from "@/components/page"
export default function CouponPage() {
return <Page>优惠券</Page>
}

View File

@@ -1,5 +1,415 @@
import { useState } from "react"
import { EditInfoDialog } from "@/components/composites/dialogs/editInfoDialog"
import DataTable from "@/components/data-table"
import Page from "@/components/page"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import b3 from "../assets/b3.webp"
import b5 from "../assets/b5.webp"
import b6 from "../assets/b6.webp"
import b7 from "../assets/b7.webp"
import b8 from "../assets/b8.webp"
import b9 from "../assets/b9.webp"
import b10 from "../assets/b10.webp"
import b11 from "../assets/b11.webp"
import b12 from "../assets/b12.webp"
import banner from "../assets/banner.webp"
import banner2 from "../assets/banner2.webp"
export default function DashboardPage() {
return <Page>账户总览页面</Page>
const balanceData = {
balance: 68.6,
hCoin: 68.6,
}
const accountInfo = {
phone: "191****1234",
qq: "123456123",
wechat: "123456123",
email: "123456123@qq.com",
taobao: "123456123",
}
const recentOrders = [
{
date: "2025.04.01",
product: "极光IP",
plan: "天卡(尊享)",
price: 3.0,
connections: 1,
account: "jx1234",
orderAmount: 3.0,
paidAmount: 3.0,
},
{
date: "2025.04.01",
product: "极光IP",
plan: "天卡(尊享)",
price: 3.0,
connections: 1,
account: "jx1234",
orderAmount: 3.0,
paidAmount: 3.0,
},
{
date: "2025.04.01",
product: "极光IP",
plan: "天卡(尊享)",
price: 3.0,
connections: 1,
account: "jx1234",
orderAmount: 3.0,
paidAmount: 3.0,
},
{
date: "2025.04.01",
product: "极光IP",
plan: "天卡(尊享)",
price: 3.0,
connections: 1,
account: "jx1234",
orderAmount: 3.0,
paidAmount: 3.0,
},
]
const consumptionData = {
today: 2365,
month: 312365,
todayRefund: 2365,
year: 256562,
monthRefund: 30,
todayRecharge: 50,
monthRecharge: 20,
}
const ipAccountData = {
using: 30,
total: 50,
expired: 20,
yearConsumption: 2365,
}
const [editDialogOpen, setEditDialogOpen] = useState(false)
const handleSaveInfo = (data: any) => {
console.log("保存的信息:", data)
// 这里调用你的 API 保存数据
}
return (
<Page
className={cn(
"grid gap-4 p-4",
"grid-cols-1",
"md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_300px]",
"md:grid-rows-[150px_200px_minmax(150px,1fr)_minmax(150px,1fr)]",
)}
>
{/* banner */}
<section className="flex md:row-start-1 md:col-start-1 md:col-span-3 gap-4 max-md:flex-col">
<div className="flex flex-1 min-w-0 items-center justify-between overflow-hidden rounded-lg p-5 bg-linear-to-b from-blue-200 to-blue-50">
<div className="flex flex-col gap-4 min-w-0">
<div>
<span className="text-sm font-medium text-gray-500">
可用余额(元)
</span>
</div>
<div className="flex flex-wrap items-center gap-4 sm:gap-8">
<p className="text-2xl sm:text-3xl font-bold">
{balanceData.balance}
</p>
<div className="flex gap-2 sm:gap-4">
<Button
size="sm"
variant="outline"
className="text-xs bg-white text-blue-600 hover:bg-white/90 hover:text-blue-700"
>
去提现
</Button>
<Button
size="sm"
className="text-xs bg-white text-blue-600 hover:bg-white/90 hover:text-blue-700"
>
去充值
</Button>
</div>
</div>
</div>
<img
src={banner}
alt="banner"
className="w-16 h-16 sm:w-24 sm:h-24 object-contain shrink-0"
/>
</div>
<div className="flex flex-1 min-w-0 items-center justify-between overflow-hidden rounded-lg p-5 bg-linear-to-b from-cyan-200 to-cyan-50">
<div className="flex flex-col gap-4 min-w-0">
<div>
<p className="text-sm font-medium text-gray-500">可用H币(个)</p>
</div>
<div className="flex flex-wrap items-center gap-4 sm:gap-8">
<p className="text-2xl sm:text-3xl font-bold">
{balanceData.hCoin}
</p>
<Button
size="sm"
variant="outline"
className="text-xs bg-white text-blue-600 hover:bg-white/90 hover:text-blue-700"
>
去充值
</Button>
</div>
</div>
<img
src={banner2}
alt="banner2"
className="w-16 h-16 sm:w-24 sm:h-24 object-contain shrink-0"
/>
</div>
</section>
{/* 最近订单 */}
<div className="md:row-start-2 md:col-start-1 md:col-span-3 bg-white rounded-lg p-4 flex flex-col min-h-0">
<h3 className="font-semibold text-lg mb-4 shrink-0">最近订单</h3>
<div className="flex-1 overflow-auto min-h-0">
<DataTable
data={recentOrders}
status="done"
columns={[
{ accessorKey: "date", header: "日期" },
{ accessorKey: "product", header: "产品" },
{ accessorKey: "plan", header: "套餐" },
{
accessorKey: "price",
header: "单价(元)",
cell: ({ row }) => row.original.price.toFixed(2),
},
{ accessorKey: "connections", header: "总连接数" },
{ accessorKey: "account", header: "账号" },
{
accessorKey: "orderAmount",
header: "订单金额(元)",
cell: ({ row }) => row.original.orderAmount.toFixed(2),
},
{
accessorKey: "paidAmount",
header: "实付金额(元)",
cell: ({ row }) => row.original.paidAmount.toFixed(2),
},
]}
/>
</div>
</div>
{/* 消费信息 */}
<div className="md:col-start-1 md:row-start-3 md:col-span-3 md:row-span-2 bg-white rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-lg">消费信息</h3>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-blue-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
<img
src={b3}
alt="今日消费"
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
/>
<div className="min-w-0">
<p className="text-xs sm:text-sm text-gray-500">今日消费</p>
<p className="text-lg sm:text-2xl font-bold text-blue-600">
{consumptionData.today}
</p>
</div>
</div>
<div className="bg-cyan-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
<img
src={b5}
alt="当月消费"
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
/>
<div className="min-w-0">
<p className="text-xs sm:text-sm text-gray-500">当月消费</p>
<p className="text-lg sm:text-2xl font-bold text-cyan-600">
{consumptionData.month}
</p>
</div>
</div>
<div className="bg-orange-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
<img
src={b6}
alt="今日退款"
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
/>
<div className="min-w-0">
<p className="text-xs sm:text-sm text-gray-500">今日退款</p>
<p className="text-lg sm:text-2xl font-bold text-orange-600">
{consumptionData.todayRefund}
</p>
</div>
</div>
<div className="bg-fuchsia-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
<img
src={b7}
alt="本年消费"
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
/>
<div className="min-w-0">
<p className="text-xs sm:text-sm text-gray-500">本年消费</p>
<p className="text-lg sm:text-2xl font-bold text-fuchsia-600">
{consumptionData.year}
</p>
</div>
</div>
<div className="bg-amber-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
<img
src={b8}
alt="今日充值"
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
/>
<div className="min-w-0">
<p className="text-xs sm:text-sm text-gray-500">今日充值</p>
<p className="text-lg sm:text-2xl font-bold text-amber-600">
{consumptionData.todayRecharge}
</p>
</div>
</div>
<div className="bg-emerald-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
<img
src={b9}
alt="当月充值"
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
/>
<div className="min-w-0">
<p className="text-xs sm:text-sm text-gray-500">当月充值</p>
<p className="text-lg sm:text-2xl font-bold text-emerald-600">
{consumptionData.monthRecharge}
</p>
</div>
</div>
<div className="bg-purple-50 rounded-lg p-3 sm:p-4 flex items-center gap-2 sm:gap-3">
<img
src={b6}
alt="当月退款"
className="w-8 h-8 sm:w-10 sm:h-10 shrink-0"
/>
<div className="min-w-0">
<p className="text-xs sm:text-sm text-gray-500">当月退款</p>
<p className="text-lg sm:text-2xl font-bold text-purple-600">
{consumptionData.monthRefund}
</p>
</div>
</div>
</div>
</div>
{/* 账号信息 */}
<div className="md:col-start-4 md:row-start-1 md:row-span-2 bg-white rounded-lg p-4 flex flex-col">
<h3 className="font-semibold text-lg shrink-0">账号信息</h3>
<div className="mt-4 flex justify-between items-start gap-3 flex-auto">
<div className="flex flex-col items-center gap-4 shrink-0">
<span className="text-base font-medium">{accountInfo.phone}</span>
<Button
variant="outline"
size="sm"
className="w-20 text-xs bg-white text-blue-600 hover:bg-white/90 hover:text-blue-700 border border-blue-600"
>
去实名
</Button>
<div className="w-20 h-20 border border-gray-200 rounded">
<img
src="/qrcode-placeholder.png"
alt="账号二维码"
className="w-full h-full object-contain"
/>
</div>
</div>
<div className="flex flex-col gap-2.5 text-sm flex-1">
<div className="flex items-start py-1 border-b border-gray-50 last:border-0">
<span className="text-gray-400 w-20 shrink-0">密码</span>
<span className="text-gray-700 font-mono tracking-wide">
********
</span>
</div>
<div className="flex items-start py-1 border-b border-gray-50 last:border-0">
<span className="text-gray-400 w-20 shrink-0">QQ</span>
<span className="text-gray-700">
{accountInfo.qq || "未绑定"}
</span>
</div>
<div className="flex items-start py-1 border-b border-gray-50 last:border-0">
<span className="text-gray-400 w-20 shrink-0">微信号</span>
<span className="text-gray-700">
{accountInfo.wechat || "未绑定"}
</span>
</div>
<div className="flex items-start py-1 border-b border-gray-50 last:border-0">
<span className="text-gray-400 w-20 shrink-0">淘宝会员</span>
<span className="text-gray-700">
{accountInfo.taobao || "未绑定"}
</span>
</div>
<div className="flex items-start py-1 border-b border-gray-50 last:border-0">
<span className="text-gray-400 w-20 shrink-0">邮箱</span>
<span className="text-gray-700 truncate">
{accountInfo.email || "未绑定"}
</span>
</div>
</div>
</div>
<div className="mt-5 flex justify-end shrink-0">
<Button
variant="outline"
size="sm"
className="text-xs bg-white text-blue-600 hover:bg-white/90 hover:text-blue-700 border-transparent hover:border-blue-200"
onClick={() => setEditDialogOpen(true)}
>
编辑信息
</Button>
</div>
</div>
{/* IP账号 */}
<div className="md:col-start-4 md:row-start-3 md:row-span-2 bg-white rounded-lg p-4">
<h3 className="font-semibold text-lg mb-6">IP账号</h3>
<div className="grid grid-cols-3 gap-4 text-center">
<div className="flex flex-col items-center gap-3">
<div className="w-16 h-16 flex items-center justify-center">
<img
src={b10}
alt="使用中图标"
className="w-10 h-10 object-contain"
/>
</div>
<p className="text-3xl font-bold">{ipAccountData.using}</p>
<p className="text-gray-500">使用中</p>
</div>
<div className="flex flex-col items-center gap-3">
<div className="w-16 h-16 flex items-center justify-center">
<img
src={b12}
alt="总个数图标"
className="w-10 h-10 object-contain"
/>
</div>
<p className="text-3xl font-bold">{ipAccountData.total}</p>
<p className="text-gray-500">总个数</p>
</div>
<div className="flex flex-col items-center gap-3">
<div className="w-16 h-16 flex items-center justify-center">
<img
src={b11}
alt="已过期图标"
className="w-10 h-10 object-contain"
/>
</div>
<p className="text-3xl font-bold">{ipAccountData.expired}</p>
<p className="text-gray-500">已过期</p>
</div>
</div>
</div>
<EditInfoDialog
open={editDialogOpen}
onOpenChange={setEditDialogOpen}
onSave={handleSaveInfo}
/>
</Page>
)
}

59
src/admin/effects.tsx Normal file
View File

@@ -0,0 +1,59 @@
import { type ReactNode, useEffect } from "react"
import { useLayout } from "./layout-context"
type Breakpoints = {
sm: boolean
md: boolean
lg: boolean
xl: boolean
}
const breakpoints: Breakpoints = { sm: false, md: false, lg: false, xl: false }
export function useBreakpoint() {
return breakpoints
}
export const bp = breakpoints
export default function Effects(props: { children?: ReactNode }) {
const { setNavbar } = useLayout()
useEffect(() => {
const sm = window.matchMedia("(width >= 40rem)")
const md = window.matchMedia("(width >= 48rem)")
const lg = window.matchMedia("(width >= 64rem)")
const xl = window.matchMedia("(width >= 80rem)")
breakpoints.sm = sm.matches
breakpoints.md = md.matches
breakpoints.lg = lg.matches
breakpoints.xl = xl.matches
setNavbar(md.matches)
sm.addEventListener("change", (e) => {
breakpoints.sm = e.matches
})
md.addEventListener("change", (e) => {
breakpoints.md = e.matches
setNavbar(e.matches)
})
lg.addEventListener("change", (e) => {
breakpoints.lg = e.matches
})
xl.addEventListener("change", (e) => {
breakpoints.xl = e.matches
})
const html = document.documentElement
const originalFontSize = html.style.fontSize
html.style.fontSize = "16px"
return () => {
html.style.fontSize = originalFontSize
}
}, [setNavbar])
return <>{props.children}</>
}

5
src/admin/funds/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import Page from "@/components/page"
export default function FundsPage() {
return <Page>资金明细</Page>
}

View File

@@ -0,0 +1,27 @@
import { createContext, type ReactNode, useContext, useState } from "react"
type LayoutState = {
navbar: boolean
setNavbar: (navbar: boolean) => void
toggleNavbar: () => void
}
const LayoutContext = createContext<LayoutState | null>(null)
export function LayoutProvider(props: { children: ReactNode }) {
const [navbar, setNavbar] = useState(false)
const toggleNavbar = () => setNavbar(!navbar)
return (
<LayoutContext value={{ navbar, setNavbar, toggleNavbar }}>
{props.children}
</LayoutContext>
)
}
export function useLayout() {
const ctx = useContext(LayoutContext)
if (!ctx) throw new Error("useLayout must be used within LayoutProvider")
return ctx
}

View File

@@ -1,50 +1,50 @@
import { useState } from "react"
import { Outlet } from "react-router-dom"
import { Content, Header, Mask, Navbar, Shell } from "./clients"
import Effects from "./effects"
import { LayoutProvider } from "./layout-context"
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>
<LayoutProvider>
<Effects />
<div className="relative h-dvh overflow-hidden">
<Shell>
<div className="col-start-1 row-start-1 row-span-2 bg-card overflow-hidden relative z-20">
<Navbar />
</div>
<div className="col-start-2 row-start-1 bg-card overflow-hidden relative z-20">
<Header navbar={navbar} toggleNavbar={toggleNavbar} />
</div>
<div className="col-start-2 row-start-1 bg-card overflow-hidden relative z-20">
<Header />
</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>
<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>
<Mask />
</Shell>
{/* 用 Outlet 替代 {props.children} */}
<Content navbar={navbar}>
<Outlet />
</Content>
</div>
<Content>
<Outlet />
</Content>
</div>
</LayoutProvider>
)
}

View File

@@ -0,0 +1,5 @@
import Page from "@/components/page"
export default function WhitelistPage() {
return <Page>白名单</Page>
}