Initial commit from Create vite
8
src/App.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { RouterProvider } from "react-router-dom"
|
||||
import { router } from "./routers"
|
||||
|
||||
function App() {
|
||||
return <RouterProvider router={router} />
|
||||
}
|
||||
|
||||
export default App
|
||||
9
src/admin/assets/logo-avatar.svg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
src/admin/assets/logo.webp
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
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
@@ -0,0 +1,5 @@
|
||||
import Page from "@/components/page"
|
||||
|
||||
export default function DashboardPage() {
|
||||
return <Page>账户总览页面</Page>
|
||||
}
|
||||
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>
|
||||
)
|
||||
}
|
||||
BIN
src/assets/hero.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
1
src/assets/vite.svg
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
67
src/components/composites/dialogs/AnnouncementModal.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
interface AnnouncementModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export default function AnnouncementModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: AnnouncementModalProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent
|
||||
className="sm:max-w-120 p-0 gap-0 overflow-hidden rounded-xl border-none shadow-2xl bg-white"
|
||||
showCloseButton
|
||||
>
|
||||
<div className="p-8 pb-4">
|
||||
<DialogHeader className="mb-4">
|
||||
<DialogTitle className="text-3xl font-bold text-center text-gray-800">
|
||||
公告
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="text-sm text-gray-600 space-y-4">
|
||||
<p className="text-xs text-gray-500 leading-relaxed">
|
||||
杜绝电信诈骗及任何违法行为,所有IP产品已记录日志,均保留6个月以上,发现违法行为直接提交网监部门处理
|
||||
</p>
|
||||
|
||||
<div className="pt-1">
|
||||
<span className="font-bold text-gray-700">重磅打造</span>
|
||||
<p>1. 极光IP(聚IP池子最大的动态产品),240城市纯净住宅IP</p>
|
||||
<p>2. 极狐IP(聚IP网速最快的动态产品),260城市纯净住宅IP</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-2">
|
||||
<p className="text-blue-500 font-medium text-[13px] leading-relaxed">
|
||||
稳定极速!视频直播无压力!且有不同宽带可选!承诺不好用48小时内免费退款
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-2">
|
||||
<DialogClose asChild className="flex-1">
|
||||
<Button className="w-full h-14 rounded-none bg-linear-to-r from-blue-500 to-cyan-400 hover:opacity-90 text-white text-lg font-medium border-0 rounded-bl-xl">
|
||||
查看活动
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<DialogClose asChild className="flex-1">
|
||||
<Button className="w-full h-14 rounded-none bg-linear-to-r from-cyan-400 to-blue-500 hover:opacity-90 text-white text-lg font-medium border-0 rounded-br-xl">
|
||||
联系客服
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
31
src/components/page.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { ComponentProps } from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type PageProps = {
|
||||
mode?: "full" | "blank"
|
||||
}
|
||||
|
||||
export default function Page(props: ComponentProps<"main"> & PageProps) {
|
||||
return (
|
||||
<main {...props} className={cn(`relative size-full`)}>
|
||||
{/* background */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute w-screen h-screen bg-gray-50"></div>
|
||||
<div className="absolute w-500 h-500 -left-250 -top-250 bg-radial from-blue-50/50 from-10% to-transparent to-50%"></div>
|
||||
<div className="absolute w-500 h-500 -right-250 -top-250 bg-radial from-blue-50/50 from-10% to-transparent to-50%"></div>
|
||||
<div className="absolute w-500 h-500 left-[calc(50%-1000px)] -bottom-250 bg-radial from-blue-50/50 from-10% to-transparent to-50%"></div>
|
||||
</div>
|
||||
|
||||
{/* content */}
|
||||
<div
|
||||
className={cn(
|
||||
`relative w-full h-full`,
|
||||
`flex flex-col gap-4 p-4 overflow-auto`,
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
49
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
67
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from "radix-ui"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
103
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-(--card-spacing)", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardAction,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
}
|
||||
29
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { Checkbox as CheckboxPrimitive } from "radix-ui"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer relative flex size-4 shrink-0 items-center justify-center rounded-lg border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
||||
>
|
||||
<CheckIcon />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
166
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import * as React from "react"
|
||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-none font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
19
src/components/ui/input.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
88
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Tabs as TabsPrimitive } from "radix-ui"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
className={cn(
|
||||
"group/tabs flex gap-2 data-horizontal:flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-horizontal/tabs:h-8 group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col data-[variant=line]:rounded-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-muted",
|
||||
line: "gap-1 bg-transparent",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||
VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 text-sm outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger, tabsListVariants }
|
||||
55
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as React from "react"
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
|
||||
14
src/components/wrap.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
export type WrapProps = {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Wrap(props: WrapProps) {
|
||||
return (
|
||||
<div className={`max-w-[1232px] w-full px-4 mx-auto ${props.className}`}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
src/home/_assets/banner.webp
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
src/home/_assets/s1.webp
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
src/home/_assets/s10.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/home/_assets/s11.webp
Normal file
|
After Width: | Height: | Size: 774 B |
BIN
src/home/_assets/s12.webp
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/home/_assets/s2.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/home/_assets/s3.webp
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src/home/_assets/s4.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/home/_assets/s5.webp
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
src/home/_assets/s6.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
src/home/_assets/s7.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/home/_assets/s8.webp
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/home/_assets/s9.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
src/home/_assets/vector.webp
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/home/_components/assets/gzh.webp
Normal file
|
After Width: | Height: | Size: 870 B |
BIN
src/home/_components/assets/phone.webp
Normal file
|
After Width: | Height: | Size: 740 B |
BIN
src/home/_components/assets/qq.webp
Normal file
|
After Width: | Height: | Size: 774 B |
BIN
src/home/_components/assets/top.webp
Normal file
|
After Width: | Height: | Size: 678 B |
BIN
src/home/_components/assets/wx.webp
Normal file
|
After Width: | Height: | Size: 840 B |
137
src/home/_components/header/common.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState } from "react"
|
||||
import gzh from "../assets/gzh.webp"
|
||||
import phone from "../assets/phone.webp"
|
||||
import qq from "../assets/qq.webp"
|
||||
import top from "../assets/top.webp"
|
||||
import wx from "../assets/wx.webp"
|
||||
|
||||
export default function Common() {
|
||||
const [activePopup, setActivePopup] = useState<"gzh" | "tel" | null>(null)
|
||||
|
||||
// QQ点击跳转
|
||||
const handleQQClick = () => {
|
||||
window.location.href =
|
||||
"https://admin.qidian.qq.com/template/blue/mp/menu/qr-code-jump.html?linkType=0&env=ol&kfuin=2852138148&fid=299&key=f377ec024ca45115a03a7632c7bda230&cate=1&source=&isLBS=&isCustomEntry=&type=16&ftype=1&_type=wpa&qidian=true"
|
||||
}
|
||||
|
||||
// 微信点击跳转
|
||||
const handleWxClick = () => {
|
||||
window.open("https://work.weixin.qq.com/kfid/kfc458bc58e79e5093f", "_blank")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed right-4 bottom-20 z-50 flex flex-col w-18 overflow-visible rounded-xl shadow-lg bg-linear-to-b from-blue-700 to-cyan-300">
|
||||
{/* QQ */}
|
||||
<div
|
||||
onClick={handleQQClick}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
handleQQClick()
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="flex flex-col items-center justify-center py-2 text-white border-b border-white/15 hover:bg-white/10 transition-colors cursor-pointer"
|
||||
>
|
||||
<img src={qq} alt="QQ" className="w-7 h-7 object-contain" />
|
||||
<span className="text-xs mt-1.5">QQ</span>
|
||||
</div>
|
||||
|
||||
{/* 微信 */}
|
||||
<div
|
||||
onClick={handleWxClick}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
handleWxClick()
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="flex flex-col items-center justify-center py-2 text-white border-b border-white/15 hover:bg-white/10 transition-colors cursor-pointer"
|
||||
>
|
||||
<img src={wx} alt="微信" className="w-7 h-7 object-contain" />
|
||||
<span className="text-xs mt-1.5">微信</span>
|
||||
</div>
|
||||
|
||||
{/* 公众号 */}
|
||||
<div
|
||||
className="relative flex flex-col items-center justify-center py-2 text-white border-b border-white/15 hover:bg-white/10 transition-colors cursor-pointer"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={activePopup === "gzh"}
|
||||
onMouseEnter={() => setActivePopup("gzh")}
|
||||
onMouseLeave={() => setActivePopup(null)}
|
||||
onFocus={() => setActivePopup("gzh")}
|
||||
onBlur={() => setActivePopup(null)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
// toggle popup with keyboard
|
||||
setActivePopup(prev => (prev === "gzh" ? null : "gzh"))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src={gzh} alt="公众号" className="w-7 h-7 object-contain" />
|
||||
<span className="text-xs mt-1.5">公众号</span>
|
||||
|
||||
{/* 公众号悬浮弹窗 */}
|
||||
{activePopup === "gzh" && (
|
||||
<div className="absolute right-full mr-3 bottom-0 bg-white rounded-lg shadow-xl p-4 w-48 text-center border border-gray-100">
|
||||
<img
|
||||
src="/img/ewm.png"
|
||||
alt="公众号二维码"
|
||||
className="w-full aspect-square object-contain"
|
||||
/>
|
||||
<p className="text-orange-500 text-xs mt-2">
|
||||
实时响应 在线时间 8:30-23:00
|
||||
</p>
|
||||
<p className="text-orange-500 text-xs">
|
||||
关注公众号可获取免费的到期提醒服务
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 电话 */}
|
||||
<div
|
||||
className="relative flex flex-col items-center justify-center py-2 text-white border-b border-white/15 hover:bg-white/10 transition-colors cursor-pointer"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={activePopup === "tel"}
|
||||
onMouseEnter={() => setActivePopup("tel")}
|
||||
onMouseLeave={() => setActivePopup(null)}
|
||||
onFocus={() => setActivePopup("tel")}
|
||||
onBlur={() => setActivePopup(null)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
setActivePopup(prev => (prev === "tel" ? null : "tel"))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src={phone} alt="电话" className="w-7 h-7 object-contain" />
|
||||
<span className="text-xs mt-1.5">电话</span>
|
||||
|
||||
{/* 电话悬浮弹窗 */}
|
||||
{activePopup === "tel" && (
|
||||
<div className="absolute right-full mr-3 bottom-0 bg-white rounded-lg shadow-xl p-4 w-48 text-center border border-gray-100">
|
||||
<p className="font-bold text-lg text-gray-800">400 800 9925</p>
|
||||
<p className="text-sm text-gray-600">实时接通</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
电话值班时间 8:30-23:00
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 回到顶部 */}
|
||||
<button
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
||||
className="flex flex-col items-center justify-center py-2 text-white hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<img src={top} alt="回到顶部" className="w-7 h-7 object-contain" />
|
||||
<span className="text-xs mt-1.5">回到顶部</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
src/home/_components/header/menu-client.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NavLink } from "react-router-dom"
|
||||
|
||||
export default function MenuClient() {
|
||||
return (
|
||||
<NavLink
|
||||
to="/client"
|
||||
className={({ isActive }) =>
|
||||
isActive
|
||||
? "text-blue-500 border-b-2 border-blue-500 py-1"
|
||||
: "text-slate-700 py-1"
|
||||
}
|
||||
>
|
||||
下载客户端
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
16
src/home/_components/header/menu-download.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NavLink } from "react-router-dom"
|
||||
|
||||
export default function MenuDownload() {
|
||||
return (
|
||||
<NavLink
|
||||
to="/soft-download"
|
||||
className={({ isActive }) =>
|
||||
isActive
|
||||
? "text-blue-500 border-b-2 border-blue-500 py-1"
|
||||
: "text-slate-700 py-1"
|
||||
}
|
||||
>
|
||||
软件下载
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
16
src/home/_components/header/menu-help.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NavLink } from "react-router-dom"
|
||||
|
||||
export default function MenuHelp() {
|
||||
return (
|
||||
<NavLink
|
||||
to="/help"
|
||||
className={({ isActive }) =>
|
||||
isActive
|
||||
? "text-blue-500 border-b-2 border-blue-500 py-1"
|
||||
: "text-slate-700 py-1"
|
||||
}
|
||||
>
|
||||
帮助/教程
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
16
src/home/_components/header/menu-ip.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NavLink } from "react-router-dom"
|
||||
|
||||
export default function MenuIp() {
|
||||
return (
|
||||
<NavLink
|
||||
to="/ip-line"
|
||||
className={({ isActive }) =>
|
||||
isActive
|
||||
? "text-blue-500 border-b-2 border-blue-500 py-1"
|
||||
: "text-slate-700 py-1"
|
||||
}
|
||||
>
|
||||
IP线路表
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
16
src/home/_components/header/menu-product.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NavLink } from "react-router-dom"
|
||||
|
||||
export default function MenuProduct() {
|
||||
return (
|
||||
<NavLink
|
||||
to="/product"
|
||||
className={({ isActive }) =>
|
||||
isActive
|
||||
? "text-blue-500 border-b-2 border-blue-500 py-1"
|
||||
: "text-slate-700 py-1"
|
||||
}
|
||||
>
|
||||
产品购买
|
||||
</NavLink>
|
||||
)
|
||||
}
|
||||
9
src/home/client/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import Wrap from "@/components/wrap"
|
||||
|
||||
export default function ClientPage() {
|
||||
return (
|
||||
<Wrap className="flex flex-col gap-16 mt-20">
|
||||
<h1>下载客户端页面内容</h1>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
132
src/home/foot.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import Wrap from "@/components/wrap"
|
||||
|
||||
export default function Foot() {
|
||||
return (
|
||||
<footer className="bg-gray-900 text-white z-20 overflow-hidden">
|
||||
<Wrap className="flex flex-col p-4 pt-12">
|
||||
<div className="flex-auto overflow-hidden flex flex-wrap justify-between">
|
||||
<div className="flex flex-col lg:items-center gap-2 max-lg:w-1/2">
|
||||
<img
|
||||
src="/img/qrcode.jpg"
|
||||
alt="logo"
|
||||
width={80}
|
||||
height={80}
|
||||
className="flex-none size-20 sm:size-44 bg-gray-100"
|
||||
/>
|
||||
<span className="text-sm text-gray-400">关注我们查看更多资讯</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 lg:gap-4 max-lg:w-1/2">
|
||||
<h3>商务合作</h3>
|
||||
<p className="text-sm text-gray-400">微信/电话:177 9666 8888</p>
|
||||
<p className="text-sm text-gray-400">QQ: 70177252</p>
|
||||
<h3 className="hidden sm:block">服务保障</h3>
|
||||
<a
|
||||
href="https://work.weixin.qq.com/kfid/kfc458bc58e79e5093f"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-400 hidden sm:block cursor-pointer hover:text-white transition-colors"
|
||||
>
|
||||
售前服务
|
||||
</a>
|
||||
<a
|
||||
href="https://work.weixin.qq.com/kfid/kfc458bc58e79e5093f"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-gray-400 hidden sm:block cursor-pointer hover:text-white transition-colors"
|
||||
>
|
||||
技术支持
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<SiteNavList
|
||||
title="站点导航"
|
||||
items={[
|
||||
{ name: `产品订购`, href: `/product` },
|
||||
{ name: `获取代理`, href: `/collect` },
|
||||
{ name: `帮助中心`, href: `/docs/faq-general` },
|
||||
{ name: `企业服务`, href: `/custom` },
|
||||
]}
|
||||
/>
|
||||
<SiteNavList
|
||||
title="国内代理"
|
||||
items={[
|
||||
{ name: `短效代理`, href: `/product?type=short` },
|
||||
{ name: `长效代理`, href: `/product?type=long` },
|
||||
{ name: `固定IP代理`, href: `/product?type=fixed` },
|
||||
]}
|
||||
/>
|
||||
<SiteNavList
|
||||
title="使用案例"
|
||||
items={[
|
||||
{ name: `数据采集`, href: `/data-capture` },
|
||||
{ name: `电商运营`, href: `/e-commerce` },
|
||||
{ name: `市场调研`, href: `/market-research` },
|
||||
{ name: `SEO优化`, href: `/seo-optimization` },
|
||||
{ name: `社交媒体`, href: `/social-media` },
|
||||
{ name: `广告投放`, href: `/advertising` },
|
||||
{ name: `账号管理`, href: `/account-management` },
|
||||
{ name: `网络测试`, href: `/network-testing` },
|
||||
]}
|
||||
/>
|
||||
<SiteNavList
|
||||
title="帮助文档"
|
||||
items={[
|
||||
{ name: `产品功能`, href: `/docs/product-overview` },
|
||||
{ name: `使用教程`, href: `/docs/browser-proxy` },
|
||||
{ name: `行业资讯`, href: `/docs/news-latest` },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-none mt-6 pt-6 border-t border-gray-700 flex text-center flex-col text-gray-300">
|
||||
<p className="text-xs">
|
||||
蓝狐代理仅提供IP服务,用户使用蓝狐代理IP从事的任何行为均不代表蓝狐代理IP的意志和观点,与蓝狐代理的立场无关。
|
||||
<br />
|
||||
严禁用户使用蓝狐代理从事任何违法犯罪行为,
|
||||
产生的相关责任用户自负,对此蓝狐代理不承担任何法律责任。
|
||||
</p>
|
||||
<p className={`text-xs mt-3 `}>
|
||||
版权所有 河南华连网络科技有限公司 |
|
||||
<a
|
||||
href="https://beian.miit.gov.cn/#/Integrated/index"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline"
|
||||
>
|
||||
豫ICP备17004061号-17
|
||||
</a>
|
||||
| 增值电信业务经营许可证:B1-20190663
|
||||
</p>
|
||||
</div>
|
||||
</Wrap>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
function SiteNavList(props: {
|
||||
title: string
|
||||
items: {
|
||||
name: string
|
||||
href: string
|
||||
}[]
|
||||
}) {
|
||||
return (
|
||||
<div className="max-lg:mt-4 w-full lg:w-auto">
|
||||
<h3>{props.title}</h3>
|
||||
<ul
|
||||
className={[
|
||||
`mt-1 flex flex-wrap gap-2 `,
|
||||
`lg:mt-4 lg:flex-col lg:gap-4 lg:gap-x-6`,
|
||||
].join(" ")}
|
||||
>
|
||||
{props.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<a href={item.href} className="text-sm text-gray-400">
|
||||
{item.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
91
src/home/header.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import { useSyncExternalStore } from "react"
|
||||
import { NavLink } from "react-router-dom"
|
||||
import Wrap from "@/components/wrap"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export default function Header() {
|
||||
const scroll = useSyncExternalStore(
|
||||
callback => {
|
||||
window.addEventListener("scroll", callback)
|
||||
return () => {
|
||||
window.removeEventListener("scroll", callback)
|
||||
}
|
||||
},
|
||||
() => window.scrollY > 48,
|
||||
() => false,
|
||||
)
|
||||
|
||||
return (
|
||||
<header className={cn("fixed top-0 left-0 w-screen z-10 flex flex-col ")}>
|
||||
<div
|
||||
className={cn(
|
||||
`flex-none pointer-events-auto`,
|
||||
`transition-[background,shadow] duration-200 ease-in-out`,
|
||||
scroll
|
||||
? `bg-[#fffe] backdrop-blur-sm shadow-lg`
|
||||
: `bg-transparent shadow-none`,
|
||||
)}
|
||||
>
|
||||
<Wrap className="h-20 max-md:h-16 flex justify-between pl-15 pr-15">
|
||||
<nav className="flex items-center justify-between gap-4 lg:gap-8">
|
||||
<ul className="h-full items-stretch hidden lg:flex">
|
||||
<NavItem to="/">首页</NavItem>
|
||||
<NavItem to="/product">产品购买</NavItem>
|
||||
<NavItem to="/ipline">IP线路表</NavItem>
|
||||
<NavItem to="/softDownload">软件下载</NavItem>
|
||||
<NavItem to="/help">帮助/教程</NavItem>
|
||||
<NavItem to="/client">下载客户端</NavItem>
|
||||
</ul>
|
||||
</nav>
|
||||
<ProfileOrLogin />
|
||||
</Wrap>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
const NavItem = ({
|
||||
to,
|
||||
children,
|
||||
}: {
|
||||
to: string
|
||||
children: React.ReactNode
|
||||
}) => (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
`h-full flex items-center relative px-3 ${
|
||||
isActive
|
||||
? "text-blue-500 after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-blue-500"
|
||||
: "text-slate-700"
|
||||
}`
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
)
|
||||
|
||||
function ProfileOrLogin() {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<NavLink
|
||||
to="/login"
|
||||
state={{ tab: "login" }}
|
||||
className="w-24 h-12 flex items-center justify-center lg:text-lg"
|
||||
>
|
||||
<span>登录</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/login"
|
||||
state={{ tab: "register" }}
|
||||
className={[
|
||||
`w-20 lg:w-24 h-10 lg:h-12 bg-linear-to-r rounded-full flex items-center justify-center lg:text-lg text-white`,
|
||||
`transition-colors duration-200 ease-in-out`,
|
||||
`from-blue-500 to-cyan-400 hover:from-blue-500 hover:to-cyan-300`,
|
||||
].join(" ")}
|
||||
>
|
||||
<span>注册</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
9
src/home/help/index.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import Wrap from "@/components/wrap"
|
||||
|
||||
export default function HelpPage() {
|
||||
return (
|
||||
<Wrap className="flex flex-col gap-16 mt-20">
|
||||
<h1>帮助中心和教程页面内容</h1>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
310
src/home/index.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
import type { ReactNode } from "react"
|
||||
import Wrap from "@/components/wrap"
|
||||
import banner from "./_assets/banner.webp"
|
||||
import s1 from "./_assets/s1.webp"
|
||||
import s2 from "./_assets/s2.webp"
|
||||
import s3 from "./_assets/s3.webp"
|
||||
import s4 from "./_assets/s4.webp"
|
||||
import s5 from "./_assets/s5.webp"
|
||||
import s6 from "./_assets/s6.webp"
|
||||
import s7 from "./_assets/s7.webp"
|
||||
import s8 from "./_assets/s8.webp"
|
||||
import s9 from "./_assets/s9.webp"
|
||||
import s10 from "./_assets/s10.webp"
|
||||
import s11 from "./_assets/s11.webp"
|
||||
import s12 from "./_assets/s12.webp"
|
||||
import Common from "./_components/header/common"
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<Common />
|
||||
<section className="w-full relative">
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
style={{
|
||||
backgroundImage: `url(${banner})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
|
||||
<Wrap className="relative pt-64 pb-48 max-md:pt-32 max-md:pb-24">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-center md:text-left">
|
||||
聚集优质IP产品,尽在聚IP
|
||||
</h1>
|
||||
<p className="mt-6 text-gray-600 text-center md:text-left">
|
||||
<span className="lg:text-lg">
|
||||
覆盖500+城市,超2000个服务器,亿量级IP
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div className="mt-8 max-w-3xl">
|
||||
<p className="text-gray-600 text-center md:text-left">
|
||||
<span className="lg:text-base">
|
||||
旗下有极狐、极光、蘑菇、猎豹、麒麟、水滴、火狐、无极等优质动态独享,
|
||||
静态独享,单窗口IP 产品
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-12 flex justify-center md:justify-start">
|
||||
<button
|
||||
type="button"
|
||||
className="w-36 h-14 rounded-full shadow-lg bg-linear-to-r from-blue-500 to-cyan-400 text-white text-xl font-medium hover:opacity-90 transition-opacity cursor-pointer"
|
||||
>
|
||||
免费试用
|
||||
</button>
|
||||
</div>
|
||||
</Wrap>
|
||||
</section>
|
||||
<section className="w-full relative shadow-blue-50 bg-white">
|
||||
<Wrap className="relative pt-32 pb-48 max-md:pt-32 max-md:pb-24">
|
||||
<PageSection title="为什么我们是你的最佳选择">
|
||||
<p className="text-gray-600 text-center text-sm">
|
||||
国内三大运营商正规授权,千万级代理IP资源,权威与品质的双重保障
|
||||
</p>
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6 pt-16">
|
||||
<ProductTypeCard icon={s1} title="全网产品最多,IP库最大" />
|
||||
<ProductTypeCard icon={s2} title="支持全设备及协议" />
|
||||
<ProductTypeCard icon={s3} title="用户独享宽带" />
|
||||
<ProductTypeCard icon={s4} title="价低质优,免费测试" />
|
||||
<ProductTypeCard icon={s5} title="专属化产品定制" />
|
||||
<ProductTypeCard icon={s6} title="专业的服务团队" />
|
||||
</ul>
|
||||
<div className="flex justify-between items-center gap-10 flex-wrap mt-16 p-20 shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white">
|
||||
<div className="flex flex-col">
|
||||
<h3 className="text-2xl font-bold mb-4">
|
||||
全网产品最多,IP库最大
|
||||
</h3>
|
||||
<p className="text-gray-600 w-150 text-sm">
|
||||
旗下有极狐、极光、蘑菇、猎豹、麒麟、水滴、火狐、无极等优质动态独享、静态独享、单窗口IP产品
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<img src={s7} alt="产品" className="w-20 h-20 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col pt-24 ">
|
||||
<h2 className="text-center text-3xl font-bold mb-8 lg:mb-24">
|
||||
海量地区,遍布全国的优质IP代理资源
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center text-sm">
|
||||
遍布全国的代理服务器节点为用户提供智能帮助
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-20">
|
||||
<ul className="shadow-[0_0_20px_4px] shadow-blue-50 p-8 flex max-lg:flex-col">
|
||||
<li className="flex-1 flex flex-col items-center justify-center max-lg:mb-4 ">
|
||||
<p className="mt-9 max-lg:mt-2 text-3xl bg-linear-to-t from-blue-500 to-cyan-400 bg-clip-text text-transparent font-bold pb-2 -mb-2">
|
||||
10万+
|
||||
</p>
|
||||
<p className="text-sm">全国节点覆盖</p>
|
||||
</li>
|
||||
<li className="flex-1 flex flex-col items-center justify-center max-lg:mb-4">
|
||||
<p className="mt-9 max-lg:mt-2 text-3xl bg-linear-to-t from-blue-500 to-cyan-400 bg-clip-text text-transparent font-bold pb-2 -mb-2">
|
||||
100万+
|
||||
</p>
|
||||
<p className="text-sm">全国独享IP池</p>
|
||||
</li>
|
||||
<li className="flex-1 flex flex-col items-center justify-center max-lg:mb-4">
|
||||
<p className="mt-9 max-lg:mt-2 text-3xl bg-linear-to-t from-blue-500 to-cyan-400 bg-clip-text text-transparent font-bold pb-2 -mb-2">
|
||||
10万+
|
||||
</p>
|
||||
<p className="text-sm">全国实时在线IP数</p>
|
||||
</li>
|
||||
<li className="flex-1 flex flex-col items-center justify-center">
|
||||
<p className="mt-9 max-lg:mt-2 text-3xl bg-linear-to-t from-blue-500 to-cyan-400 bg-clip-text text-transparent font-bold pb-2 -mb-2">
|
||||
99%
|
||||
</p>
|
||||
<p className="text-sm">IP可用率</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col pt-24 ">
|
||||
<h2 className="text-center text-3xl font-bold mb-8 lg:mb-24">
|
||||
满足您的多种业务需求
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center text-sm">
|
||||
更灵活自由的设计方式,适配多种业务需求
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-20 items-center justify-center">
|
||||
<img src={s9} alt="图片" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col pt-24 ">
|
||||
<h2 className="text-center text-3xl font-bold mb-8 lg:mb-24">
|
||||
为您提供质量保障
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center text-sm">
|
||||
国内三大运营商正规授权,千万级代理IP资源,权威与品质的双重保障
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 pt-20">
|
||||
<div className="flex flex-col shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-8">
|
||||
<div className="flex w-full mb-5 bg-blue-50 items-center justify-center">
|
||||
<img src={s10} alt="售前咨询" className="p-6" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold mb-8 lg:mb-24">售前咨询</h2>
|
||||
<p className="text-xs text-gray-600">
|
||||
注册即享新人福利,专业的客户经理,多维度为您提供在线代理相关
|
||||
答疑
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-8">
|
||||
<div className="flex w-full mb-5 bg-blue-50 items-center justify-center">
|
||||
<img src={s11} alt="售中服务" className="p-6" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold mb-8 lg:mb-24">售中服务</h2>
|
||||
<p className="text-xs text-gray-600">
|
||||
全国100万+动态IP代理资源免费测试,先测后买让您安心使用
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-8">
|
||||
<div className="flex w-full mb-5 bg-blue-50 items-center justify-center">
|
||||
<img src={s12} alt="售后保障" className="p-6" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold mb-8 lg:mb-24">售后保障</h2>
|
||||
<p className="text-xs text-gray-600">
|
||||
1V1专属售后答疑,技术团队7*24小时在线支持提供专属解决方案
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col pt-24 ">
|
||||
<h2 className="text-center text-3xl font-bold mb-8 lg:mb-24">
|
||||
为您推荐聚IP的产品
|
||||
</h2>
|
||||
<p className="text-gray-600 text-center text-sm">
|
||||
更灵活自由的设计方式,适配多种业务需求
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 pt-30">
|
||||
<div className="flex flex-col shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-8">
|
||||
<div className="mb-3">
|
||||
<h4 className="text-xl font-semibold mb-2 group-hover:text-blue-600 transition-colors">
|
||||
全国动态IP资源
|
||||
</h4>
|
||||
<p className="text-gray-500 text-xs leading-relaxed">
|
||||
覆盖全国100万+纯净动态代理IP资源,真实家庭在线代理IP
|
||||
,高度匿名,保护隐私
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<h4 className="text-xl font-semibold mb-2 group-hover:text-blue-600 transition-colors">
|
||||
精准城市定位
|
||||
</h4>
|
||||
<p className="text-gray-500 text-xs leading-relaxed">
|
||||
精准定位,高效稳定连接,助力轻松访问
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<h4 className="text-xl font-semibold mb-2 group-hover:text-blue-600 transition-colors">
|
||||
三大协议支持
|
||||
</h4>
|
||||
<p className="text-gray-500 text-xs leading-relaxed">
|
||||
分布式集群架构,同时支持 协议
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full h-12 mt-12 rounded-full shadow-lg bg-linear-to-r from-blue-500 to-cyan-400 text-white text-xl font-medium hover:opacity-90 transition-opacity cursor-pointer"
|
||||
>
|
||||
查看IP线路表
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-8">
|
||||
<h4 className="text-xl font-semibold mb-3 group-hover:text-blue-600 transition-colors">
|
||||
极光IP
|
||||
</h4>
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<p className="text-gray-500 text-xs leading-relaxed">
|
||||
首单低至{" "}
|
||||
<span className="text-green-500 text-xl font-bold">
|
||||
¥0.1
|
||||
</span>
|
||||
</p>
|
||||
<div className="bg-orange-100 text-orange-500 text-[10px] w-12 text-center mt-2">
|
||||
计时收费
|
||||
</div>
|
||||
</div>
|
||||
<img src={s8} alt="图片" className="w-20 h-20" />
|
||||
</div>
|
||||
<ul className="space-y-4 text-gray-600">
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mt-2.5 shrink-0"></span>
|
||||
<span className="text-xs leading-relaxed">
|
||||
全国240+城市,480+地区
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mt-2.5 shrink-0"></span>
|
||||
<span className="text-xs leading-relaxed">
|
||||
池子超大,高效稳定,三网混合真实IP
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500 mt-2.5 shrink-0"></span>
|
||||
<span className="text-xs leading-relaxed">
|
||||
多款套餐可选,满足多元业务需求
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full h-12 mt-14 rounded-full shadow-lg bg-linear-to-r from-blue-500 to-cyan-400 text-white text-xl font-medium hover:opacity-90 transition-opacity cursor-pointer"
|
||||
>
|
||||
立即查看
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-8 items-start justify-center ">
|
||||
<h4 className="text-xl font-semibold mb-3 group-hover:text-blue-600 transition-colors">
|
||||
极狐IP
|
||||
</h4>
|
||||
<p className="text-gray-500 text-xs leading-relaxed">
|
||||
网速超快,高效稳定,三网混合真实IP
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs leading-relaxed">
|
||||
全国260+城市,500+地区
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs leading-relaxed">
|
||||
多款套餐可选,满足多元业务需求
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageSection>
|
||||
</Wrap>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PageSection(props: { title: string; children: ReactNode }) {
|
||||
return (
|
||||
<section>
|
||||
<div className="max-w-308 mx-auto px-4 flex flex-col items-stretch">
|
||||
<h2 className="text-center text-3xl mb-8 lg:mb-24">{props.title}</h2>
|
||||
{props.children}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function ProductTypeCard(props: { icon: string; title: string }) {
|
||||
return (
|
||||
<li className="flex flex-col items-center gap-5rounded-lg text-center gap-6">
|
||||
<img
|
||||
src={props.icon}
|
||||
alt={props.title}
|
||||
className="w-20 h-20 object-cover"
|
||||
/>
|
||||
<p className="text-sm">{props.title}</p>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
346
src/home/ipLine/index.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Wrap from "@/components/wrap"
|
||||
|
||||
export default function IpLinePage() {
|
||||
return (
|
||||
<Wrap className="flex flex-col gap-4 mt-30 max-w-6xl mx-auto px-4 pb-20">
|
||||
<div className="grid grid-cols-3">
|
||||
<div></div>
|
||||
<h3 className="text-3xl font-bold text-gray-800 text-center">
|
||||
IP线路表
|
||||
</h3>
|
||||
<div className="flex gap-4 text-sm justify-end items-end text-blue-500">
|
||||
<a
|
||||
href="/softDownload"
|
||||
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
|
||||
>
|
||||
下载客户端
|
||||
</a>
|
||||
<a
|
||||
href="/help"
|
||||
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
|
||||
>
|
||||
教程&帮助
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="flex justify-between gap-4 text-xs text-gray-600 px-2">
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
线路表和账户必须为同一产品才能使用
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
请优先选择客户端连接
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
无对应客户端时,可通过线路表直连支持所有设备
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button className="px-6 py-1.5 rounded-full text-white bg-linear-to-r from-cyan-400 to-blue-500 shadow-md shadow-blue-200 font-medium">
|
||||
极狐IP
|
||||
</Button>
|
||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
||||
极光IP
|
||||
</Button>
|
||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
||||
蘑菇IP
|
||||
</Button>
|
||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
||||
麒麟IP
|
||||
</Button>
|
||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
||||
猎豹IP
|
||||
</Button>
|
||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
||||
水滴独享IP
|
||||
</Button>
|
||||
<Button className="px-6 py-1.5 rounded-full text-gray-600 border border-gray-200 hover:border-blue-400 transition-colors bg-white">
|
||||
火狐静态IP
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 w-full">
|
||||
<div className="relative w-1/2">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="混拨"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-full focus:outline-none focus:ring-2 focus:ring-cyan-400 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<button className="px-8 py-2 bg-linear-to-r from-cyan-400 to-blue-500 text-white rounded-full text-sm font-medium shadow-md shadow-blue-200">
|
||||
搜索当前线路表
|
||||
</button>
|
||||
<button className="flex items-center gap-1.5 px-4 py-2 border border-blue-400 text-blue-500 rounded text-sm bg-white hover:bg-blue-50 transition-colors">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
导出
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-orange-50 border border-orange-100 rounded-lg p-2 flex justify-center items-center gap-8 text-sm font-medium">
|
||||
<div className="flex items-center gap-2 text-orange-500">
|
||||
<span>L2TP密钥:</span>
|
||||
<span className="text-orange-600">1234</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-orange-500">
|
||||
<span>STTP端口:</span>
|
||||
<span className="text-orange-600">4430</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-xs text-gray-600 px-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
<span>
|
||||
实时总线路:<span className="font-bold text-gray-800">486条</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
<span>
|
||||
实时可用线路:<span className="font-bold text-gray-800">486条</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
<span className="text-gray-400">
|
||||
极狐IP- (每5分钟更新一次,禁止频繁访问!) :{" "}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-gray-200 rounded overflow-hidden">
|
||||
<table className="w-full text-center text-sm">
|
||||
<thead className="bg-gray-50 border-b border-gray-200 text-gray-600 font-medium">
|
||||
<tr>
|
||||
<th className="py-3 px-4 border-r border-gray-200 w-24">产品</th>
|
||||
<th className="py-3 px-4 border-r border-gray-200 w-32">城市</th>
|
||||
<th className="py-3 px-4 border-r border-gray-200 w-48">
|
||||
运营商
|
||||
</th>
|
||||
<th className="py-3 px-4 border-r border-gray-200">服务器域名</th>
|
||||
<th className="py-3 px-4 border-r border-gray-200 w-20">IP量</th>
|
||||
<th className="py-3 px-4 border-r border-gray-200 w-24">
|
||||
实时带宽
|
||||
</th>
|
||||
<th className="py-3 px-4 border-r border-gray-200 w-24">
|
||||
维护状态
|
||||
</th>
|
||||
<th className="py-3 px-4 w-24">负载状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-100">
|
||||
<tr>
|
||||
<td
|
||||
className="py-3 px-4 border-r border-gray-200 text-gray-500"
|
||||
rowSpan={10}
|
||||
>
|
||||
极狐
|
||||
</td>
|
||||
|
||||
<td
|
||||
className="py-3 px-4 border-r border-gray-200 text-green-600 font-medium"
|
||||
rowSpan={7}
|
||||
>
|
||||
全国
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>hb1.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>hb2.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>hb3.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>hb4.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>hb5.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>hbdx2.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>hbdx1.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
className="py-3 px-4 border-r border-gray-200 text-green-600 font-medium"
|
||||
rowSpan={2}
|
||||
>
|
||||
北京
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>bj1.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>bj2.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td
|
||||
className="py-3 px-4 border-r border-gray-200 text-green-600 font-medium"
|
||||
rowSpan={1}
|
||||
>
|
||||
上海
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200 text-gray-500">
|
||||
电信/联通/移动
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">
|
||||
<span>sh1.jhip.net</span>
|
||||
<span className="ml-2 text-green-500 cursor-pointer hover:text-green-700">
|
||||
复制
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">100M</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200">正常</td>
|
||||
<td className="py-3 px-4 border-r border-gray-200"></td>
|
||||
<td className="py-3 px-4"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
30
src/home/layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Outlet, useLocation } from "react-router-dom"
|
||||
import AnnouncementModal from "@/components/composites/dialogs/AnnouncementModal"
|
||||
import Foot from "./foot"
|
||||
import Header from "./header"
|
||||
|
||||
export default function LayoutPage() {
|
||||
const location = useLocation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname === "/") {
|
||||
setIsOpen(true)
|
||||
} else {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}, [location.pathname])
|
||||
|
||||
return (
|
||||
<div className="bg-blue-50 flex flex-col items-stretch relative min-h-screen">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Foot />
|
||||
|
||||
<AnnouncementModal open={isOpen} onOpenChange={setIsOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
BIN
src/home/login/_assets/bg.webp
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
172
src/home/login/index.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useLocation, useNavigate } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { cn } from "@/lib/utils"
|
||||
import bg from "./_assets/bg.webp"
|
||||
|
||||
export default function LoginPage() {
|
||||
const [tab, setTab] = useState<"login" | "register">("login")
|
||||
const location = useLocation()
|
||||
|
||||
useEffect(() => {
|
||||
const initTab = location.state?.tab
|
||||
if (initTab === "login" || initTab === "register") {
|
||||
setTab(initTab)
|
||||
}
|
||||
}, [location.state])
|
||||
|
||||
return (
|
||||
<main
|
||||
className={cn(
|
||||
`relative`,
|
||||
`h-screen w-screen xl:pr-64 bg-cover bg-left`,
|
||||
`flex justify-center xl:justify-end items-center`,
|
||||
)}
|
||||
style={{ backgroundImage: `url(${bg})` }}
|
||||
>
|
||||
<Card className="w-96 mx-4 shadow-lg relative z-20 py-8">
|
||||
<CardContent className="px-8">
|
||||
<div className="flex mb-6 rounded-lg bg-gray-100 p-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setTab("login")}
|
||||
className={`flex-1 py-2 rounded-md transition-all ${
|
||||
tab === "login"
|
||||
? "bg-white shadow-sm text-blue-600 font-medium"
|
||||
: "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setTab("register")}
|
||||
className={`flex-1 py-2 rounded-md transition-all ${
|
||||
tab === "register"
|
||||
? "bg-white shadow-sm text-blue-600 font-medium"
|
||||
: "text-gray-600 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</div>
|
||||
{tab === "login" ? <LoginForm /> : <RegisterForm />}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
function LoginForm() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
navigate("/admin")
|
||||
}
|
||||
return (
|
||||
<form className="space-y-4" onSubmit={handleSubmit}>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="account" className="text-sm font-medium text-gray-700">
|
||||
账号
|
||||
</label>
|
||||
<Input
|
||||
id="account"
|
||||
type="text"
|
||||
placeholder="请输入淘宝名/会员手机号码"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="password" className="text-sm font-medium text-gray-700">
|
||||
密码
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="请输入密码(8-12位数字或字母组合)"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox id="remember" />
|
||||
<label htmlFor="remember" className="text-gray-600 cursor-pointer">
|
||||
记住密码
|
||||
</label>
|
||||
</div>
|
||||
<span className="text-gray-400 text-xs">
|
||||
手机号不是IP账号,请登陆后开通IP账号
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<p className="text-center text-xs text-gray-400 mt-2">首次登录即注册</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function RegisterForm() {
|
||||
return (
|
||||
<form className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="reg-account"
|
||||
className="text-sm font-medium text-gray-700"
|
||||
>
|
||||
手机号
|
||||
</label>
|
||||
<Input
|
||||
id="reg-account"
|
||||
type="text"
|
||||
placeholder="请输入手机号码"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="reg-password"
|
||||
className="text-sm font-medium text-gray-700"
|
||||
>
|
||||
设置密码
|
||||
</label>
|
||||
<Input
|
||||
id="reg-password"
|
||||
type="password"
|
||||
placeholder="8-12位数字或字母组合"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="reg-confirm"
|
||||
className="text-sm font-medium text-gray-700"
|
||||
>
|
||||
确认密码
|
||||
</label>
|
||||
<Input
|
||||
id="reg-confirm"
|
||||
type="password"
|
||||
placeholder="再次输入密码"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
<p className="text-center text-xs text-gray-400 mt-2">
|
||||
注册即代表同意《用户协议》和《隐私政策》
|
||||
</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
133
src/home/product/index.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { useState } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
import Wrap from "@/components/wrap"
|
||||
import { PRODUCT_DATA } from "@/lib/models/product"
|
||||
import { cn } from "@/lib/utils"
|
||||
import ProductGrid from "./productGrid"
|
||||
import ProductSidebar from "./productSidebar"
|
||||
import ProductTabs from "./productTabs"
|
||||
import PurchaseInfo from "./purchaseInfo"
|
||||
|
||||
export default function ProductPage() {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [activeVersionId, setActiveVersionId] = useState<string>("v1")
|
||||
|
||||
const activeTabId = searchParams.get("tab") || "dynamic"
|
||||
const activeBrandId = searchParams.get("brand") || ""
|
||||
|
||||
const currentTab =
|
||||
PRODUCT_DATA.find(t => t.id === activeTabId) || PRODUCT_DATA[0]
|
||||
const currentBrand =
|
||||
currentTab.brands.find(b => b.id === activeBrandId) || currentTab.brands[0]
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
const targetTab = PRODUCT_DATA.find(t => t.id === tabId)
|
||||
const firstBrand = targetTab?.brands[0]
|
||||
setSearchParams({ tab: tabId, brand: firstBrand?.id || "" })
|
||||
setActiveVersionId("v1")
|
||||
}
|
||||
|
||||
const handleBrandSelect = (brandId: string) => {
|
||||
setSearchParams({ tab: activeTabId, brand: brandId })
|
||||
setActiveVersionId("v1")
|
||||
}
|
||||
|
||||
const getDisplayProducts = () => {
|
||||
if (!currentBrand) return []
|
||||
|
||||
if (currentBrand.versions && currentBrand.versions.length > 0) {
|
||||
return currentBrand.products.filter(p => p.version === activeVersionId)
|
||||
}
|
||||
return currentBrand.products
|
||||
}
|
||||
|
||||
const displayProducts = getDisplayProducts()
|
||||
const isPurchaseInfo = currentBrand?.id === "info"
|
||||
return (
|
||||
<Wrap className="flex flex-col gap-16 pt-30 bg-white">
|
||||
<div className="flex flex-col">
|
||||
<ProductTabs
|
||||
tabs={PRODUCT_DATA}
|
||||
activeId={activeTabId}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
|
||||
<div className="flex mt-8 mb-8 mx-auto w-full">
|
||||
<div className="shrink-0 border-r border-gray-200">
|
||||
<ProductSidebar
|
||||
brands={currentTab.brands}
|
||||
activeId={currentBrand?.id || ""}
|
||||
onSelect={handleBrandSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col">
|
||||
{!isPurchaseInfo ? (
|
||||
<>
|
||||
{" "}
|
||||
<div className="grid grid-cols-2 pl-3 text-sm gap-2 text-gray-600 leading-relaxed">
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
连接方式:电脑和安卓请优选客户端,苹果,路由器可通过线路表直连
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
本产品在个人中心签署协议后可联系客服解除限制
|
||||
</li>
|
||||
</ul>
|
||||
<ul className="space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
IP池超大且纯净,所有线路均为三网混合,更真实有效
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
230+城市,500+线路,有全国混拨和省混拨
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="bg-orange-50 border gap-3 border-orange-100 text-orange-400 p-3 text-center font-bold mt-3 mb-3 text-sm w-full">
|
||||
需求5个以上,可以联系客服设置优惠价
|
||||
</div>
|
||||
{currentBrand?.versions && currentBrand.versions.length > 0 && (
|
||||
<div className="w-full mt-2 pl-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{currentBrand.versions.map(v => (
|
||||
<button
|
||||
key={v.id}
|
||||
onClick={() => setActiveVersionId(v.id)}
|
||||
className={cn(
|
||||
"w-full py-2 rounded-full text-sm font-medium transition-all cursor-pointer text-center",
|
||||
activeVersionId === v.id
|
||||
? "bg-linear-to-r from-blue-500 to-cyan-400 text-white shadow-sm"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
||||
)}
|
||||
>
|
||||
{v.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<main className="pl-6">
|
||||
{displayProducts.length > 0 ? (
|
||||
<ProductGrid products={displayProducts} />
|
||||
) : (
|
||||
<div className="text-center py-20 text-gray-400">
|
||||
该版本暂无产品数据
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
) : (
|
||||
<div className="pl-6 pr-6">
|
||||
<PurchaseInfo />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
79
src/home/product/productGrid.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import type { Product } from "@/lib/models/product"
|
||||
import vector from "../_assets/vector.webp"
|
||||
|
||||
type ProductGridProps = {
|
||||
products: Product[]
|
||||
}
|
||||
|
||||
export default function ProductGrid({ products }: ProductGridProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-6 gap-6">
|
||||
{products.map(product => (
|
||||
<div
|
||||
key={product.id}
|
||||
className="relative overflow-visible rounded-xl shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-6 flex flex-col min-h-60"
|
||||
>
|
||||
{product.tag && (
|
||||
<div className="absolute -top-5 -right-5 flex items-center gap-1 text-xs font-bold px-3 py-1">
|
||||
<img src={vector} alt="打折" />
|
||||
{product.tag}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-lg font-bold text-center text-gray-700">
|
||||
{product.title}
|
||||
</div>
|
||||
|
||||
{product.desc ? (
|
||||
<div className="py-2 text-sm text-center text-orange-400 font-bold">
|
||||
{product.desc}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-2 text-sm text-center font-bold text-orange-400">
|
||||
¥ {product.price.toFixed(2)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-gray-400 flex flex-col space-y-0.5 mt-10">
|
||||
{product.card && (
|
||||
<div className="flex justify-between">
|
||||
<span>卡种:</span>
|
||||
<span>{product.card}</span>
|
||||
</div>
|
||||
)}
|
||||
{product.originalPrice !== undefined && (
|
||||
<div className="flex justify-between">
|
||||
<span>原价:</span>
|
||||
<span>¥{product.originalPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
{product.duration && (
|
||||
<div className="flex justify-between">
|
||||
<span>套餐有效期:</span>
|
||||
<span>{product.duration}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{product.features && product.features.length > 0 && (
|
||||
<div className="text-xs text-gray-400 text-center space-y-0.5 mt-1">
|
||||
{product.features.map((feature, index) => (
|
||||
<div key={index}>{feature}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4 mt-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full text-blue-500 border-blue-400 hover:bg-blue-50"
|
||||
>
|
||||
立即购买
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
src/home/product/productSidebar.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Brand } from "@/lib/models/product"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type ProductSidebarProps = {
|
||||
brands: Brand[]
|
||||
activeId: string
|
||||
onSelect: (id: string) => void
|
||||
}
|
||||
|
||||
export default function ProductSidebar({
|
||||
brands,
|
||||
activeId,
|
||||
onSelect,
|
||||
}: ProductSidebarProps) {
|
||||
return (
|
||||
<aside className="w-30">
|
||||
{brands.map(brand => (
|
||||
<button
|
||||
key={brand.id}
|
||||
onClick={() => onSelect(brand.id)}
|
||||
className={cn(
|
||||
"w-full text-left px-4 py-3 rounded-lg text-sm transition-colors",
|
||||
activeId === brand.id
|
||||
? "bg-blue-50 text-blue-600 font-semibold"
|
||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
)}
|
||||
>
|
||||
{brand.name}
|
||||
</button>
|
||||
))}
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
36
src/home/product/productTabs.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import type { Tab } from "@/lib/models/product"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type ProductTabsProps = {
|
||||
tabs: Tab[]
|
||||
activeId: string
|
||||
onTabChange: (id: string) => void
|
||||
}
|
||||
|
||||
export default function ProductTabs({
|
||||
tabs,
|
||||
activeId,
|
||||
onTabChange,
|
||||
}: ProductTabsProps) {
|
||||
return (
|
||||
<Tabs value={activeId} onValueChange={onTabChange} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3 bg-slate-100 rounded-xl h-14">
|
||||
{tabs.map(tab => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className={cn(
|
||||
"w-full py-2 rounded-full text-sm font-medium transition-all cursor-pointer text-center outline-none border-0 ring-0",
|
||||
activeId === tab.id
|
||||
? "bg-linear-to-r from-blue-500 to-cyan-400 text-white! shadow-none"
|
||||
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
85
src/home/product/purchaseInfo.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
export default function PurchaseInfo() {
|
||||
return (
|
||||
<div className="flex w-full max-w-6xl mx-auto">
|
||||
<div className="flex-1 space-y-3 pr-3 border-r">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-orange-400 mb-3">重磅打造:</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600">
|
||||
<li className="flex items-start gap-2 whitespace-nowrap">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
极光IP(聚IP池子最大的动态产品),240城市纯净住宅IP;现推广期,天卡1毛体验
|
||||
</li>
|
||||
<li className="flex items-start gap-2 whitespace-nowrap">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
极狐IP(聚IP网速最快的动态产品),260城市纯净住宅IP;现推广期,天卡1毛体验
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-orange-400 mb-3">购买须知:</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
需求5个以上,可以联系客服设置优惠价
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
退款规则:购买价格-使用天数*天卡价格
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
||||
本产品可解除微信QQ请到个人中心签订协议,云南以及非大陆地区禁用
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-orange-400 mb-3">产品对比:</h3>
|
||||
{/* 这里可以放入产品对比表格 */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-none w-90 pl-6 space-y-6">
|
||||
<h3 className="text-lg font-bold text-orange-400 mb-2">
|
||||
1V1定制化企业级业务场景
|
||||
</h3>
|
||||
<div className="">
|
||||
<span className="block text-sm font-medium text-gray-800 mb-2">
|
||||
定制化服务,满足个性化需求
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 m-0 leading-relaxed">
|
||||
为您提供灵活多样的代理IP服务,无论是大规模数据采集,还是AI大模型训练,我们的团队都能为您量身打造代理IP解决方案
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<span className="block text-sm font-medium text-gray-800 mb-2">
|
||||
高质量IP资源,保障业务稳定
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 m-0 leading-relaxed">
|
||||
我们精心挑选并维护着庞大的代理IP池,确保每个IP地址均经过严格筛选和测试,具备高度匿名性和稳定性
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<span className="block text-sm font-medium text-gray-800 mb-2">
|
||||
先进架构技术,保障业务稳定
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 m-0 leading-relaxed">
|
||||
聚IP致力于保护所有用户的隐私和数据安全,我们的团队采用先进的加密技术,确保您的业务活动在安全的环境下进行
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 pt-6">
|
||||
<span className="block text-sm font-medium text-gray-800 mb-2">
|
||||
专业技术支持,全天候响应
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 m-0 leading-relaxed">
|
||||
7×24小时技术支持深耕代理IP行业多年的专业人士,能迅速解决您在使用过程中遇到的各种问题,确保您的业务顺畅运行
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
248
src/home/softDownload/index.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import { Apple, Monitor, Smartphone } from "lucide-react"
|
||||
import type { ReactNode } from "react"
|
||||
import Wrap from "@/components/wrap"
|
||||
|
||||
export default function SoftDownloadPage() {
|
||||
const DownloadBtn = ({
|
||||
href,
|
||||
icon,
|
||||
label,
|
||||
}: {
|
||||
href: string
|
||||
icon: ReactNode
|
||||
label: string
|
||||
}) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 w-full py-2.5 px-4 bg-white border border-blue-400 text-blue-500 rounded-full text-sm font-medium hover:bg-blue-50 transition-colors"
|
||||
>
|
||||
{icon} {label}
|
||||
</a>
|
||||
)
|
||||
|
||||
return (
|
||||
<Wrap className="flex flex-col gap-6 mt-30">
|
||||
<div className="grid grid-cols-3">
|
||||
<div></div>
|
||||
<h3 className="text-3xl font-bold text-gray-800 text-center">
|
||||
软件下载
|
||||
</h3>
|
||||
<div className="flex gap-4 text-sm justify-end items-end text-blue-500">
|
||||
<a
|
||||
href="/softDownload"
|
||||
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
|
||||
>
|
||||
下载客户端
|
||||
</a>
|
||||
<a
|
||||
href="/help"
|
||||
className="no-underline hover:text-blue-700 active:no-underline focus:no-underline"
|
||||
>
|
||||
教程&帮助
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="flex justify-between gap-4 text-xs text-gray-600 px-2">
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
下载的软件需和购买的产品名一致才可使用
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
下载和使用时需关闭电脑管家,360等杀毒软件
|
||||
</li>
|
||||
<li className="flex items-center gap-1.5">
|
||||
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full" />
|
||||
无对应客户端时,可通过线路表直连支持所有设备
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="grid grid-cols-3 gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-center font-bold text-gray-700 text-base mb-1">
|
||||
极狐IP
|
||||
</h4>
|
||||
<div className="flex flex-col gap-3">
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/jh-dl.zip"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="Windows客户端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/dl.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="安卓APP"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://www.juip.com/article/index?Catalog=4"
|
||||
icon={<Apple className="w-4 h-4" />}
|
||||
label="苹果直连教程"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-center font-bold text-gray-700 text-base mb-1">
|
||||
极光IP
|
||||
</h4>
|
||||
<div className="flex flex-col gap-3">
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/jg-dl.zip"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="Windows客户端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/jgdl.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="安卓APP"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://www.juip.com/article/index?Catalog=4"
|
||||
icon={<Apple className="w-4 h-4" />}
|
||||
label="苹果直连教程"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-center font-bold text-gray-700 text-base mb-1">
|
||||
蘑菇IP
|
||||
</h4>
|
||||
<div className="flex flex-col gap-3">
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/%E8%98%91%E8%8F%87IP-2-17.zip"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="Windows客户端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/mogudl.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="安卓APP"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://www.juip.com/article/index?Catalog=4"
|
||||
icon={<Apple className="w-4 h-4" />}
|
||||
label="苹果直连教程"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-8">
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-center font-bold text-gray-700 text-base mb-1">
|
||||
麒麟IP
|
||||
</h4>
|
||||
<div className="flex flex-col gap-3">
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/%E9%BA%92%E9%BA%9F%E7%94%B5%E8%84%91HZ%E7%89%88.exe"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="Windows客户端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/%E9%BA%92%E9%BA%9F%E5%AE%89%E5%8D%93HZ%E7%89%88.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="安卓APP"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://www.juip.com/article/index?Catalog=4"
|
||||
icon={<Apple className="w-4 h-4" />}
|
||||
label="苹果直连教程"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<h4 className="text-center font-bold text-gray-700 text-base mb-1">
|
||||
猎豹IP
|
||||
</h4>
|
||||
<div className="flex flex-col gap-3">
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/lb-dl.zip"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="Windows客户端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/%E7%8C%8E%E8%B1%B9%E5%AE%89%E5%8D%93ikev2%E7%89%88.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="安卓APP"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/lbdlsstp.apk"
|
||||
icon={<Apple className="w-4 h-4" />}
|
||||
label="苹果直连教程"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6 mb-20">
|
||||
<h4 className="text-center font-bold text-gray-700 text-base">
|
||||
代理商专用客户端
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/jh-dl.zip"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="极狐IP电脑端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/dl.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="极狐IP安卓版"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/%E8%98%91%E8%8F%87IP-2-17.zip"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="蘑菇IP电脑端"
|
||||
/>
|
||||
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/mogudl.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="蘑菇IP安卓版"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/jg-dl.zip"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="极光IP电脑端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/jgdl.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="极光IP安卓版"
|
||||
/>
|
||||
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/%E9%BA%92%E9%BA%9F%E7%94%B5%E8%84%91HZ%E7%89%88.exe"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="麒麟IP电脑端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/%E9%BA%92%E9%BA%9F%E5%AE%89%E5%8D%93HZ%E7%89%88.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="麒麟IP安卓版"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/lb-dl.zip"
|
||||
icon={<Monitor className="w-4 h-4" />}
|
||||
label="猎豹IP电脑端"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/lbdlsstp.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="猎豹IP安卓ikev2"
|
||||
/>
|
||||
<DownloadBtn
|
||||
href="https://mp4.juip.com/lbdlsstp.apk"
|
||||
icon={<Smartphone className="w-4 h-4" />}
|
||||
label="猎豹IP安卓sstp"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
264
src/index.css
Normal file
@@ -0,0 +1,264 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
@import "@fontsource-variable/geist";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: oklch(0.922 0 0);
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
||||
--sans: system-ui, "Segoe UI", Roboto, sans-serif;
|
||||
--heading: system-ui, "Segoe UI", Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px / 145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
--background: oklch(1 0 0);
|
||||
|
||||
--foreground: oklch(0.145 0 0);
|
||||
|
||||
--card: oklch(1 0 0);
|
||||
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
|
||||
--popover: oklch(1 0 0);
|
||||
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
|
||||
--primary: oklch(0.205 0 0);
|
||||
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
|
||||
--secondary: oklch(0.97 0 0);
|
||||
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
|
||||
--muted: oklch(0.97 0 0);
|
||||
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
|
||||
--input: oklch(0.922 0 0);
|
||||
|
||||
--ring: oklch(0.708 0 0);
|
||||
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
|
||||
--radius: 0.625rem;
|
||||
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: "Geist Variable", sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
}
|
||||
295
src/lib/models/product.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
export type Version = {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type Product = {
|
||||
id: string
|
||||
title: string
|
||||
price: number
|
||||
desc?: string
|
||||
tag?: string
|
||||
card?: string
|
||||
originalPrice?: number
|
||||
duration?: string
|
||||
version?: string
|
||||
features?: string[]
|
||||
}
|
||||
|
||||
export type Brand = {
|
||||
id: string
|
||||
name: string
|
||||
versions?: Version[]
|
||||
products: Product[]
|
||||
}
|
||||
|
||||
export type Tab = {
|
||||
id: string
|
||||
label: string
|
||||
brands: Brand[]
|
||||
}
|
||||
|
||||
export const PRODUCT_DATA: Tab[] = [
|
||||
{
|
||||
id: "dynamic",
|
||||
label: "动态独享IP",
|
||||
brands: [
|
||||
{
|
||||
id: "fox",
|
||||
name: "极狐IP(推荐)",
|
||||
versions: [
|
||||
{ id: "v1", label: "尊享版-不限速" },
|
||||
{ id: "v2", label: "高级版-限速6M" },
|
||||
{ id: "v3", label: "普通版-限速2M" },
|
||||
],
|
||||
products: [
|
||||
{
|
||||
id: "1",
|
||||
title: "免费",
|
||||
price: 0,
|
||||
desc: "注册即送3次",
|
||||
card: "测试卡",
|
||||
duration: "1小时",
|
||||
version: "v1",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "首单低至",
|
||||
price: 0.1,
|
||||
originalPrice: 0.1,
|
||||
// card: "测试卡",
|
||||
duration: "24小时",
|
||||
version: "v1",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "周卡套餐",
|
||||
price: 37.5,
|
||||
tag: "8.5折",
|
||||
originalPrice: 37.5,
|
||||
// card: "周卡",
|
||||
duration: "7天",
|
||||
version: "v1",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "月卡套餐",
|
||||
price: 78,
|
||||
tag: "8.5折",
|
||||
originalPrice: 80.0,
|
||||
// card: "月卡",
|
||||
duration: "30天",
|
||||
version: "v2",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "季卡套餐",
|
||||
price: 208,
|
||||
tag: "8.5折",
|
||||
originalPrice: 220.0,
|
||||
// card: "季卡",
|
||||
duration: "90天",
|
||||
version: "v2",
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
title: "年卡套餐",
|
||||
price: 770,
|
||||
tag: "8.5折",
|
||||
originalPrice: 800.0,
|
||||
card: "年卡",
|
||||
duration: "365天",
|
||||
version: "v3",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "aurora",
|
||||
name: "极光IP(推荐)",
|
||||
versions: [
|
||||
{ id: "v1", label: "尊享版-不限速" },
|
||||
{ id: "v2", label: "高级版-限速6M" },
|
||||
{ id: "v3", label: "普通版-限速2M" },
|
||||
],
|
||||
products: [
|
||||
{
|
||||
id: "7",
|
||||
title: "周卡套餐",
|
||||
price: 37.5,
|
||||
originalPrice: 37.5,
|
||||
card: "周卡",
|
||||
duration: "7天",
|
||||
version: "v1",
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
title: "月卡套餐",
|
||||
price: 78,
|
||||
tag: "8.5折",
|
||||
originalPrice: 80.0,
|
||||
card: "月卡",
|
||||
duration: "30天",
|
||||
version: "v2",
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
title: "季卡套餐",
|
||||
price: 208,
|
||||
tag: "8.5折",
|
||||
originalPrice: 220.0,
|
||||
card: "季卡",
|
||||
duration: "90天",
|
||||
version: "v3",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "mushroom",
|
||||
name: "蘑菇IP",
|
||||
products: [
|
||||
{
|
||||
id: "10",
|
||||
title: "免费",
|
||||
price: 0,
|
||||
desc: "注册即送3次",
|
||||
card: "测试卡",
|
||||
duration: "1小时",
|
||||
},
|
||||
{
|
||||
id: "11",
|
||||
title: "首单低至",
|
||||
price: 0.1,
|
||||
originalPrice: 0.1,
|
||||
// card: "测试卡",
|
||||
duration: "24小时",
|
||||
},
|
||||
{
|
||||
id: "12",
|
||||
title: "周卡套餐",
|
||||
price: 37.5,
|
||||
originalPrice: 37.5,
|
||||
// card: "周卡",
|
||||
duration: "7天",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "kylin",
|
||||
name: "麒麟IP",
|
||||
products: [
|
||||
{
|
||||
id: "13",
|
||||
title: "测试卡",
|
||||
price: 0.1,
|
||||
card: "测试卡",
|
||||
duration: "1小时",
|
||||
},
|
||||
{
|
||||
id: "14",
|
||||
title: "24小时套餐",
|
||||
price: 37.5,
|
||||
originalPrice: 37.5,
|
||||
card: "24小时卡",
|
||||
duration: "24小时",
|
||||
},
|
||||
{
|
||||
id: "15",
|
||||
title: "周卡套餐",
|
||||
price: 37.5,
|
||||
originalPrice: 37.5,
|
||||
card: "周卡",
|
||||
duration: "7天",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "cheetah",
|
||||
name: "猎豹IP",
|
||||
products: [
|
||||
{
|
||||
id: "16",
|
||||
title: "月卡套餐",
|
||||
price: 78,
|
||||
tag: "8.5折",
|
||||
originalPrice: 80.0,
|
||||
card: "月卡",
|
||||
duration: "30天",
|
||||
},
|
||||
{
|
||||
id: "17",
|
||||
title: "季卡套餐",
|
||||
price: 208,
|
||||
tag: "8.5折",
|
||||
originalPrice: 220.0,
|
||||
card: "季卡",
|
||||
duration: "90天",
|
||||
},
|
||||
{
|
||||
id: "18",
|
||||
title: "年卡套餐",
|
||||
price: 770,
|
||||
tag: "8.5折",
|
||||
originalPrice: 800.0,
|
||||
card: "年卡",
|
||||
duration: "365天",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "info",
|
||||
name: "购买说明",
|
||||
products: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "static",
|
||||
label: "静态IP",
|
||||
brands: [
|
||||
{
|
||||
id: "drip",
|
||||
name: "水滴独享IP",
|
||||
products: [
|
||||
{
|
||||
id: "19",
|
||||
title: "纯住宅池",
|
||||
price: 50,
|
||||
desc: "分散式真实住宅IP",
|
||||
// card: "住宅卡",
|
||||
features: [
|
||||
"带宽充足,IP段优质分布广",
|
||||
"适合对IP要求高的业务",
|
||||
"IP 3-7天周期性变化",
|
||||
"线路切换每天20次",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "20",
|
||||
title: "多拨池",
|
||||
price: 60,
|
||||
desc: "集中拨号住宅,带宽较高",
|
||||
// card: "拨号卡",
|
||||
features: [
|
||||
"IP段较多,稳定性较高",
|
||||
"IP 3-7天周期性变化",
|
||||
"线路切换每天20次",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "21",
|
||||
title: "固定池",
|
||||
price: 70,
|
||||
desc: "天翼云节点",
|
||||
// card: "固定卡",
|
||||
features: ["IP段多IP不变化,稳定性高", "线路切换每日限制为20次"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "single",
|
||||
label: "单窗口单IP",
|
||||
brands: [],
|
||||
},
|
||||
]
|
||||
10
src/lib/utils/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { ReactNode } from "react"
|
||||
import { type ClassNameValue, twMerge } from "tailwind-merge"
|
||||
|
||||
// 通用组件类型
|
||||
export type Children = { children: ReactNode }
|
||||
|
||||
// 合并 className
|
||||
export function cn(...inputs: ClassNameValue[]) {
|
||||
return twMerge(inputs)
|
||||
}
|
||||
12
src/main.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { StrictMode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import "./index.css"
|
||||
import App from "./App.tsx"
|
||||
|
||||
const rootElement = document.getElementById("root")
|
||||
if (!rootElement) throw new Error("Failed to find root element")
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
34
src/routers/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { createBrowserRouter } from "react-router-dom"
|
||||
import Dashboard from "@/admin/dashboard/page"
|
||||
import AdminLayout from "@/admin/layout"
|
||||
import HomePage from "@/home"
|
||||
import ClientPage from "@/home/client"
|
||||
import HelpPage from "@/home/help"
|
||||
import IpLinePage from "@/home/ipLine"
|
||||
import LayoutPage from "@/home/layout"
|
||||
import LoginPage from "@/home/login"
|
||||
import ProductPage from "@/home/product"
|
||||
import SoftDownloadPage from "@/home/softDownload"
|
||||
export const router = createBrowserRouter([
|
||||
{
|
||||
path: "/",
|
||||
element: <LayoutPage />,
|
||||
children: [
|
||||
{ index: true, element: <HomePage /> },
|
||||
{ path: "product", element: <ProductPage /> },
|
||||
{ path: "ipline", element: <IpLinePage /> },
|
||||
{ path: "softdownload", element: <SoftDownloadPage /> },
|
||||
{ path: "help", element: <HelpPage /> },
|
||||
{ path: "client", element: <ClientPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
element: <LoginPage />,
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
element: <AdminLayout />,
|
||||
children: [{ index: true, element: <Dashboard /> }],
|
||||
},
|
||||
])
|
||||