首页页面
BIN
src/admin/assets/b10.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/admin/assets/b11.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/admin/assets/b12.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/admin/assets/b3.webp
Normal file
|
After Width: | Height: | Size: 884 B |
BIN
src/admin/assets/b4.webp
Normal file
|
After Width: | Height: | Size: 980 B |
BIN
src/admin/assets/b5.webp
Normal file
|
After Width: | Height: | Size: 958 B |
BIN
src/admin/assets/b6.webp
Normal file
|
After Width: | Height: | Size: 996 B |
BIN
src/admin/assets/b7.webp
Normal file
|
After Width: | Height: | Size: 1008 B |
BIN
src/admin/assets/b8.webp
Normal file
|
After Width: | Height: | Size: 964 B |
BIN
src/admin/assets/b9.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/admin/assets/banner.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/admin/assets/banner2.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
5
src/admin/channels/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import Page from "@/components/page"
|
||||
|
||||
export default function ChannelsPage() {
|
||||
return <Page>IP管理</Page>
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
5
src/admin/coupon/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import Page from "@/components/page"
|
||||
|
||||
export default function CouponPage() {
|
||||
return <Page>优惠券</Page>
|
||||
}
|
||||
@@ -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
@@ -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
@@ -0,0 +1,5 @@
|
||||
import Page from "@/components/page"
|
||||
|
||||
export default function FundsPage() {
|
||||
return <Page>资金明细</Page>
|
||||
}
|
||||
27
src/admin/layout-context.tsx
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
5
src/admin/whitelist/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import Page from "@/components/page"
|
||||
|
||||
export default function WhitelistPage() {
|
||||
return <Page>白名单</Page>
|
||||
}
|
||||