Compare commits

...

2 Commits

Author SHA1 Message Date
Eamon
ff56fbd7c3 动静态IP & 帮助教程 手机端适配 2026-08-26 16:55:22 +08:00
Eamon
5007cd9f51 调整首页布局 & 首页移动端适配 2026-08-25 18:32:22 +08:00
13 changed files with 845 additions and 201 deletions

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,5 +1,8 @@
import { useState } from "react" import { useState } from "react"
import { EditInfoDialog, type EditInfoValues } from "@/components/composites/dialogs/editInfoDialog" import {
EditInfoDialog,
type EditInfoValues,
} from "@/components/composites/dialogs/editInfoDialog"
import DataTable from "@/components/data-table" import DataTable from "@/components/data-table"
import Page from "@/components/page" import Page from "@/components/page"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"

View File

@@ -110,10 +110,7 @@ export function EditInfoDialog({
name="taobao" name="taobao"
control={form.control} control={form.control}
render={({ field }) => ( render={({ field }) => (
<Input <Input {...field} className="h-11 bg-gray-50 border-gray-200" />
{...field}
className="h-11 bg-gray-50 border-gray-200"
/>
)} )}
/> />
</div> </div>

View File

@@ -1,12 +1,15 @@
import { ArrowUp, ChevronDown, ChevronUp } from "lucide-react"
import { useState } from "react" import { useState } from "react"
import { cn } from "@/lib/utils"
import gzh from "../assets/gzh.webp" import gzh from "../assets/gzh.webp"
import phone from "../assets/phone.webp" import phoneImg from "../assets/phone.webp"
import qq from "../assets/qq.webp" import qqImg from "../assets/qq.webp"
import top from "../assets/top.webp" import topImg from "../assets/top.webp"
import wx from "../assets/wx.webp" import wxImg from "../assets/wx.webp"
export default function Common() { export default function Common() {
const [activePopup, setActivePopup] = useState<"gzh" | "tel" | null>(null) const [activePopup, setActivePopup] = useState<"gzh" | "tel" | null>(null)
const [isExpanded, setIsExpanded] = useState(false)
// QQ点击跳转 // QQ点击跳转
const handleQQClick = () => { const handleQQClick = () => {
@@ -20,118 +23,277 @@ export default function Common() {
} }
return ( 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 <div className="hidden md:flex fixed right-4 bottom-20 z-50 flex-col w-18 overflow-visible rounded-xl shadow-lg bg-linear-to-b from-blue-700 to-cyan-300">
onClick={handleQQClick} <div
onKeyDown={e => { onClick={handleQQClick}
if (e.key === "Enter" || e.key === " ") { onKeyDown={e => {
handleQQClick() if (e.key === "Enter" || e.key === " ") {
} handleQQClick()
}} }
role="button" }}
tabIndex={0} role="button"
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" 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> <img src={qqImg} 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={wxImg} 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 === " ") {
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={phoneImg} 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={topImg} alt="回到顶部" className="w-7 h-7 object-contain" />
<span className="text-xs mt-1.5">回到顶部</span>
</button>
</div> </div>
{/* 微信 */} {/* ========== 移动端:底部折叠面板 ========== */}
<div <div className="md:hidden fixed bottom-0 left-0 right-0 z-50">
onClick={handleWxClick} {/* 折叠面板 */}
onKeyDown={e => { <div
if (e.key === "Enter" || e.key === " ") { className={cn(
handleWxClick() "bg-white shadow-2xl rounded-t-2xl transition-all duration-300 ease-in-out",
} isExpanded ? "max-h-[80vh]" : "max-h-14",
}} )}
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" <div
> onClick={() => setIsExpanded(!isExpanded)}
<img src={wx} alt="微信" className="w-7 h-7 object-contain" /> className="flex flex-col items-center justify-center py-2.5 cursor-pointer hover:bg-gray-50 transition-colors rounded-t-2xl"
<span className="text-xs mt-1.5">微信</span> >
</div> <div className="w-10 h-1 bg-gray-300 rounded-full" />
<div className="flex items-center gap-2 mt-1">
{/* 公众号 */} <span className="text-xs text-gray-500">
<div {isExpanded ? "收起" : "联系我们"}
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" </span>
role="button" {isExpanded ? (
tabIndex={0} <ChevronDown className="w-4 h-4 text-gray-400" />
aria-haspopup="true" ) : (
aria-expanded={activePopup === "gzh"} <ChevronUp className="w-4 h-4 text-gray-400" />
onMouseEnter={() => setActivePopup("gzh")} )}
onMouseLeave={() => setActivePopup(null)} </div>
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>
{/* 电话 */} {/* 展开内容 */}
<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" className={cn(
role="button" "overflow-hidden transition-all duration-300",
tabIndex={0} isExpanded ? "max-h-[70vh] opacity-100" : "max-h-0 opacity-0",
aria-haspopup="true" )}
aria-expanded={activePopup === "tel"} >
onMouseEnter={() => setActivePopup("tel")} <div className="px-4 pb-6 pt-2 space-y-3">
onMouseLeave={() => setActivePopup(null)} {/* QQ */}
onFocus={() => setActivePopup("tel")} <div
onBlur={() => setActivePopup(null)} onClick={handleQQClick}
onKeyDown={e => { className="flex items-center gap-4 p-3 bg-blue-50 rounded-xl hover:bg-blue-100 transition-colors cursor-pointer"
if (e.key === "Enter" || e.key === " ") { >
setActivePopup(prev => (prev === "tel" ? null : "tel")) <div className="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center">
} <img
}} src={qqImg}
> alt="QQ"
<img src={phone} alt="电话" className="w-7 h-7 object-contain" /> className="w-6 h-6 object-contain"
<span className="text-xs mt-1.5">电话</span> />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-800">QQ客服</p>
<p className="text-xs text-gray-500">点击立即咨询</p>
</div>
</div>
{/* 电话悬浮弹窗 */} {/* 微信 */}
{activePopup === "tel" && ( <div
<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"> onClick={handleWxClick}
<p className="font-bold text-lg text-gray-800">400 800 9925</p> className="flex items-center gap-4 p-3 bg-green-50 rounded-xl hover:bg-green-100 transition-colors cursor-pointer"
<p className="text-sm text-gray-600">实时接通</p> >
<p className="text-xs text-gray-500 mt-1"> <div className="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center">
电话值班时间 8:30-23:00 <img
</p> src={wxImg}
alt="微信"
className="w-6 h-6 object-contain"
/>
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-800">微信客服</p>
<p className="text-xs text-gray-500">点击立即咨询</p>
</div>
</div>
{/* 公众号 */}
<div
onClick={() =>
setActivePopup(activePopup === "gzh" ? null : "gzh")
}
className="flex items-center gap-4 p-3 bg-purple-50 rounded-xl hover:bg-purple-100 transition-colors cursor-pointer"
>
<div className="w-10 h-10 rounded-full bg-purple-500 flex items-center justify-center">
<img
src={gzh}
alt="公众号"
className="w-6 h-6 object-contain"
/>
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-800">公众号</p>
<p className="text-xs text-gray-500">点击查看二维码</p>
</div>
<ChevronUp
className={cn(
"w-5 h-5 text-purple-500 transition-transform",
activePopup === "gzh" ? "rotate-180" : "",
)}
/>
</div>
{/* 公众号二维码展开 */}
{activePopup === "gzh" && (
<div className="p-4 bg-gray-50 rounded-xl text-center">
<img
src="/img/ewm.png"
alt="公众号二维码"
className="w-40 h-40 mx-auto 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
onClick={() =>
setActivePopup(activePopup === "tel" ? null : "tel")
}
className="flex items-center gap-4 p-3 bg-orange-50 rounded-xl hover:bg-orange-100 transition-colors cursor-pointer"
>
<div className="w-10 h-10 rounded-full bg-orange-500 flex items-center justify-center">
<img
src={phoneImg}
alt="电话"
className="w-6 h-6 object-contain"
/>
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-800">电话客服</p>
<p className="text-xs text-gray-500">点击查看号码</p>
</div>
<ChevronUp
className={cn(
"w-5 h-5 text-orange-500 transition-transform",
activePopup === "tel" ? "rotate-180" : "",
)}
/>
</div>
{/* 电话展开 */}
{activePopup === "tel" && (
<div className="p-4 bg-gray-50 rounded-xl text-center">
<p className="font-bold text-2xl text-gray-800">
400 800 9925
</p>
<p className="text-sm text-gray-600 mt-1">实时接通</p>
<p className="text-xs text-gray-500 mt-1">
电话值班时间 8:30-23:00
</p>
</div>
)}
{/* 回到顶部 */}
<button
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
className="flex items-center justify-center gap-2 w-full p-3 bg-gray-100 rounded-xl hover:bg-gray-200 transition-colors"
>
<ArrowUp className="w-4 h-4 text-gray-600" />
<span className="text-sm text-gray-600">回到顶部</span>
</button>
</div>
</div> </div>
)} </div>
</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>
) )
} }

View File

@@ -0,0 +1,182 @@
import { ChevronDown, X } from "lucide-react"
import { useState } from "react"
import { NavLink, useNavigate } from "react-router-dom"
import { logout } from "@/api/auth"
import { cn } from "@/lib/utils"
export default function MobileMenu({
authed,
onClose,
}: {
authed: boolean
onClose: () => void
}) {
const navigate = useNavigate()
const [productOpen, setProductOpen] = useState(false)
const handleLogout = async () => {
await logout()
}
const go = (path: string) => {
onClose()
navigate(path)
}
const openExternal = (url: string) => {
onClose()
window.open(url, "_blank")
}
return (
<div className="lg:hidden fixed inset-0 z-50 flex">
<button
type="button"
aria-label="关闭菜单"
className="flex-1 bg-black/40 cursor-pointer"
onClick={onClose}
/>
<div className="absolute left-0 top-0 bottom-0 w-72 max-w-[80vw] bg-white shadow-xl flex flex-col">
{/* 头部:logo + 关闭 */}
<div className="flex items-center justify-between px-4 h-16 border-b border-gray-100">
<NavLink to="/" onClick={onClose} className="flex items-center gap-2">
<img
src="/favicon.svg"
alt="聚IP"
className="w-8 h-8 object-contain"
/>
<span className="text-xl font-bold bg-linear-to-r from-blue-600 to-cyan-500 bg-clip-text text-transparent">
聚IP
</span>
</NavLink>
<button
type="button"
aria-label="关闭菜单"
className="rounded-md p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors"
onClick={onClose}
>
<X className="h-5 w-5" />
</button>
</div>
{/* 菜单项 */}
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-1">
<MobileNavLink to="/" onNavigate={onClose}>
首页
</MobileNavLink>
{/* 二级菜单 */}
<div>
<button
type="button"
onClick={() => setProductOpen(!productOpen)}
className={cn(
"w-full flex items-center justify-between px-3 py-2.5 rounded-lg text-sm transition-colors",
productOpen
? "text-blue-600 font-medium"
: "text-gray-700 hover:bg-gray-50",
)}
>
<span>产品订购</span>
<ChevronDown
className={cn(
"w-4 h-4 transition-transform duration-200",
productOpen && "rotate-180",
)}
/>
</button>
{productOpen && (
<div className="pl-4 space-y-1">
<SubItem onClick={() => go("/product")}>动静态IP</SubItem>
<SubItem onClick={() => openExternal("https://lanhuip.com")}>
新HTTP/S5
</SubItem>
<SubItem onClick={() => go("/product/http")}>HTTP/S5</SubItem>
<SubItem onClick={() => go("/product/routeros")}>
软路由成本价
</SubItem>
<SubItem onClick={() => openExternal("http://vps.juip.com")}>
动态Vps
</SubItem>
</div>
)}
</div>
<MobileNavLink to="/ipline" onNavigate={onClose}>
IP线路表
</MobileNavLink>
<MobileNavLink to="/softdownload" onNavigate={onClose}>
软件下载
</MobileNavLink>
<MobileNavLink to="/help" onNavigate={onClose}>
帮助/教程
</MobileNavLink>
{/* <MobileNavLink to="/client" onNavigate={onClose}>
下载客户端
</MobileNavLink> */}
{authed && (
<>
<button
type="button"
onClick={() => go("/admin")}
className="w-full flex items-center px-3 py-2.5 rounded-lg text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
个人中心
</button>
<button
type="button"
onClick={handleLogout}
className="w-full flex items-center px-3 py-2.5 rounded-lg text-sm text-gray-700 hover:bg-gray-50 transition-colors"
>
退出
</button>
</>
)}
</div>
</div>
</div>
)
}
const MobileNavLink = ({
to,
onNavigate,
children,
}: {
to: string
onNavigate: () => void
children: React.ReactNode
}) => (
<NavLink
to={to}
onClick={onNavigate}
className={({ isActive }) =>
cn(
"w-full flex items-center px-3 py-2.5 rounded-lg text-sm transition-colors",
isActive
? "bg-blue-50 text-blue-600 font-medium"
: "text-gray-700 hover:bg-gray-50",
)
}
>
{children}
</NavLink>
)
const SubItem = ({
onClick,
children,
}: {
onClick: () => void
children: React.ReactNode
}) => (
<button
type="button"
onClick={onClick}
className="w-full flex items-center px-3 py-2 rounded-lg text-xs text-gray-500 hover:bg-blue-50 hover:text-blue-600 transition-colors"
>
{children}
</button>
)

View File

@@ -1,11 +1,17 @@
import { useRef, useState, useSyncExternalStore } from "react" import { Menu, User } from "lucide-react"
import { NavLink } from "react-router-dom" import { useEffect, useRef, useState, useSyncExternalStore } from "react"
import { NavLink, useNavigate } from "react-router-dom"
import { AUTH_EVENT, isAuthed } from "@/api/token"
import ProductDropdown from "@/components/productDropdown" import ProductDropdown from "@/components/productDropdown"
import ProfileOrLogin from "@/components/profileOrLogin" import ProfileOrLogin from "@/components/profileOrLogin"
import Wrap from "@/components/wrap" import Wrap from "@/components/wrap"
import { useMediaQuery } from "@/lib/hooks/useMediaQuery"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import MobileMenu from "./_components/header/menu-mobile"
export default function Header() { export default function Header() {
const navigate = useNavigate()
const isWideDesktop = useMediaQuery("(min-width: 80rem)")
const scroll = useSyncExternalStore( const scroll = useSyncExternalStore(
callback => { callback => {
window.addEventListener("scroll", callback) window.addEventListener("scroll", callback)
@@ -22,6 +28,20 @@ export default function Header() {
const handleEnter = () => setOpenMenu(true) const handleEnter = () => setOpenMenu(true)
const handleLeave = () => setOpenMenu(false) const handleLeave = () => setOpenMenu(false)
// 移动端状态
const [mobileMenu, setMobileMenu] = useState(false)
const [authed, setAuthed] = useState(isAuthed)
useEffect(() => {
const update = () => setAuthed(isAuthed())
window.addEventListener("focus", update)
window.addEventListener(AUTH_EVENT, update)
return () => {
window.removeEventListener("focus", update)
window.removeEventListener(AUTH_EVENT, update)
}
}, [])
return ( return (
<header className={cn("fixed top-0 left-0 w-screen z-50 flex flex-col")}> <header className={cn("fixed top-0 left-0 w-screen z-50 flex flex-col")}>
<div <div
@@ -33,8 +53,19 @@ export default function Header() {
: `bg-transparent shadow-none`, : `bg-transparent shadow-none`,
)} )}
> >
<Wrap className="h-20 max-md:h-16 flex justify-between pl-15 pr-15"> <Wrap className="h-20 max-md:h-20 flex text-center justify-between max-md:pl-4 max-md:pr-4">
<nav className="flex items-center justify-between gap-4 lg:gap-8"> <nav className="flex items-center gap-4 lg:gap-8">
{/* logo + 网站名称 */}
<NavLink to="/" className="flex items-center gap-2 shrink-0">
<img
src="/favicon.svg"
alt="聚IP"
className="w-10 h-10 max-md:w-8 max-md:h-8 object-contain"
/>
<span className="text-2xl max-md:text-xl font-bold">聚IP</span>
</NavLink>
{/* 桌面端菜单 */}
<ul className="h-full items-stretch hidden lg:flex"> <ul className="h-full items-stretch hidden lg:flex">
<NavItem to="/">首页</NavItem> <NavItem to="/">首页</NavItem>
<MenuItem <MenuItem
@@ -43,16 +74,50 @@ export default function Header() {
onMouseEnter={handleEnter} onMouseEnter={handleEnter}
onMouseLeave={handleLeave} onMouseLeave={handleLeave}
> >
<ProductDropdown onNavigate={() => setOpenMenu(false)} /> <ProductDropdown
size={isWideDesktop ? "lg" : "sm"}
onNavigate={() => setOpenMenu(false)}
/>
</MenuItem> </MenuItem>
<NavItem to="/ipline">IP线路表</NavItem> <NavItem to="/ipline">IP线路表</NavItem>
<NavItem to="/softdownload">软件下载</NavItem> <NavItem to="/softdownload">软件下载</NavItem>
<NavItem to="/help">帮助/教程</NavItem> <NavItem to="/help">帮助/教程</NavItem>
</ul> </ul>
</nav> </nav>
<ProfileOrLogin />
{/* 桌面端登录/注册 */}
<div className="max-lg:hidden flex items-center">
<ProfileOrLogin />
</div>
{/* 移动端:未登录显示用户图标+汉堡菜单,已登录仅显示汉堡菜单 */}
<div className="lg:hidden flex items-center gap-2">
{!authed && (
<button
type="button"
aria-label="登录/注册"
onClick={() => navigate("/login")}
className="p-2 text-slate-700 hover:text-blue-500 transition-colors cursor-pointer"
>
<User className="w-6 h-6" />
</button>
)}
<button
type="button"
aria-label="打开菜单"
onClick={() => setMobileMenu(true)}
className="p-2 text-slate-700 hover:text-blue-500 transition-colors cursor-pointer"
>
<Menu className="w-6 h-6" />
</button>
</div>
</Wrap> </Wrap>
</div> </div>
{/* 移动端左侧菜单栏 */}
{mobileMenu && (
<MobileMenu authed={authed} onClose={() => setMobileMenu(false)} />
)}
</header> </header>
) )
} }
@@ -147,15 +212,11 @@ const MenuItem = ({
<div <div
className={cn( className={cn(
"absolute top-full left-0 pt-3 w-screen max-w-4xl transition-all duration-200 z-50", "fixed left-1/2 top-20 -translate-x-1/2 w-[min(56rem,calc(100vw-2rem))] transition-all duration-200 z-50",
isOpen isOpen
? "opacity-100 visible translate-y-0" ? "opacity-100 visible translate-y-0"
: "opacity-0 invisible -translate-y-2 pointer-events-none", : "opacity-0 invisible -translate-y-2 pointer-events-none",
)} )}
style={{
left: "50%",
transform: `translateX(-7%)`,
}}
> >
<div className="bg-white rounded-xl shadow-2xl border border-gray-100/80 p-6 overflow-visible"> <div className="bg-white rounded-xl shadow-2xl border border-gray-100/80 p-6 overflow-visible">
{children} {children}

View File

@@ -108,19 +108,19 @@ export default function HelpPage() {
</div> </div>
</div> </div>
<div className="flex gap-8"> <div className="flex flex-col md:flex-row gap-6 md:gap-8">
<aside className="w-52 shrink-0"> <aside className="w-full md:w-52 md:shrink-0">
<nav className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden sticky top-28"> <nav className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden md:sticky md:top-28">
<ul> <ul className="flex overflow-x-auto scrollbar-hide md:block">
{CATEGORIES.map(cat => ( {CATEGORIES.map(cat => (
<li key={cat.id}> <li key={cat.id} className="shrink-0">
<button <button
onClick={() => { onClick={() => {
setActiveCategoryId(cat.id) setActiveCategoryId(cat.id)
setSelectedArticle(null) setSelectedArticle(null)
}} }}
className={cn( className={cn(
"w-full text-left px-5 py-3 text-sm transition-colors flex items-center justify-between", "w-full text-left px-5 py-3 text-sm transition-colors flex items-center justify-between whitespace-nowrap",
activeCategoryId === cat.id activeCategoryId === cat.id
? "bg-blue-50 text-blue-600 font-medium cursor-pointer" ? "bg-blue-50 text-blue-600 font-medium cursor-pointer"
: "text-gray-600 hover:bg-gray-50 cursor-pointer", : "text-gray-600 hover:bg-gray-50 cursor-pointer",
@@ -194,7 +194,7 @@ function ArticleList({
onArticleClick: (article: Article) => void onArticleClick: (article: Article) => void
}) { }) {
return ( return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 md:p-8">
{articles.length === 0 ? ( {articles.length === 0 ? (
<div className="text-center py-16 text-gray-400"> <div className="text-center py-16 text-gray-400">
<SearchIcon className="size-12 mx-auto mb-4 opacity-30" /> <SearchIcon className="size-12 mx-auto mb-4 opacity-30" />
@@ -250,7 +250,7 @@ function ArticleDetail({
}, [article.content]) }, [article.content])
return ( return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-8"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 md:p-8">
<nav className="flex items-center gap-2 text-sm text-gray-400 mb-6"> <nav className="flex items-center gap-2 text-sm text-gray-400 mb-6">
{categoryName && ( {categoryName && (
<> <>
@@ -268,9 +268,8 @@ function ArticleDetail({
{/* biome-ignore lint/a11y/useMediaCaption: 教程视频无字幕文件 */} {/* biome-ignore lint/a11y/useMediaCaption: 教程视频无字幕文件 */}
<video <video
controls controls
height={500}
src={videoUrl} src={videoUrl}
className="max-w-full mx-auto rounded-lg" className="w-full h-auto max-w-full mx-auto rounded-lg"
/> />
</div> </div>
)} )}
@@ -278,8 +277,8 @@ function ArticleDetail({
ref={contentRef} ref={contentRef}
className="prose prose-sm max-w-none text-gray-700 leading-relaxed [&_h3]:text-lg [&_h3]:font-semibold [&_h3]:text-gray-800 [&_h3]:mt-6 [&_h3]:mb-3 [&_h4]:text-base [&_h4]:font-medium [&_h4]:text-gray-800 [&_h4]:mt-4 [&_h4]:mb-2 [&_p]:mb-3 [&_pre]:bg-gray-50 [&_pre]:p-4 [&_pre]:rounded-lg [&_pre]:text-sm [&_code]:bg-gray-100 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm [&_strong]:text-gray-800" className="prose prose-sm max-w-none text-gray-700 leading-relaxed [&_h3]:text-lg [&_h3]:font-semibold [&_h3]:text-gray-800 [&_h3]:mt-6 [&_h3]:mb-3 [&_h4]:text-base [&_h4]:font-medium [&_h4]:text-gray-800 [&_h4]:mt-4 [&_h4]:mb-2 [&_p]:mb-3 [&_pre]:bg-gray-50 [&_pre]:p-4 [&_pre]:rounded-lg [&_pre]:text-sm [&_code]:bg-gray-100 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-sm [&_strong]:text-gray-800"
/> />
<div className="mt-12 pt-6 border-t border-gray-100 flex items-center justify-between"> <div className="mt-12 pt-6 border-t border-gray-100 flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="flex-1 min-w-0"> <div className="min-w-0">
{prev ? ( {prev ? (
<button <button
onClick={onPrev} onClick={onPrev}
@@ -292,7 +291,7 @@ function ArticleDetail({
<span className="text-sm text-gray-300">已是第一篇</span> <span className="text-sm text-gray-300">已是第一篇</span>
)} )}
</div> </div>
<div className="flex-1 min-w-0 text-right"> <div className="min-w-0 md:text-right">
{next ? ( {next ? (
<button <button
onClick={onNext} onClick={onNext}

View File

@@ -1,6 +1,9 @@
import { ChevronLeft, ChevronRight } from "lucide-react"
import type { ReactNode } from "react" import type { ReactNode } from "react"
import { useRef, useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import Wrap from "@/components/wrap" import Wrap from "@/components/wrap"
import { useIsMobile } from "@/lib/hooks/useMediaQuery"
import banner from "./_assets/banner.webp" import banner from "./_assets/banner.webp"
import s1 from "./_assets/s1.webp" import s1 from "./_assets/s1.webp"
import s2 from "./_assets/s2.webp" import s2 from "./_assets/s2.webp"
@@ -22,6 +25,35 @@ export default function HomePage() {
const handleFreeTrial = () => { const handleFreeTrial = () => {
navigate("/login") navigate("/login")
} }
const scrollContainerRef = useRef<HTMLUListElement>(null)
const [showLeftArrow, setShowLeftArrow] = useState(false)
const [showRightArrow, setShowRightArrow] = useState(true)
const isMobile = useIsMobile()
const scroll = (direction: "left" | "right") => {
if (!scrollContainerRef.current) return
const container = scrollContainerRef.current
const scrollAmount = container.clientWidth / 2
const newScrollLeft =
direction === "left"
? container.scrollLeft - scrollAmount
: container.scrollLeft + scrollAmount
container.scrollTo({
left: newScrollLeft,
behavior: "smooth",
})
}
const handleScroll = () => {
if (!scrollContainerRef.current) return
const container = scrollContainerRef.current
setShowLeftArrow(container.scrollLeft > 10)
setShowRightArrow(
container.scrollLeft < container.scrollWidth - container.clientWidth - 10,
)
}
return ( return (
<> <>
<Common /> <Common />
@@ -37,7 +69,7 @@ export default function HomePage() {
/> />
<Wrap className="relative pt-64 pb-48 max-md:pt-32 max-md:pb-24"> <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"> <h1 className="text-4xl md:text-5xl font-bold text-center md:text-left sm:text-left">
聚集优质IP产品,尽在聚IP 聚集优质IP产品,尽在聚IP
</h1> </h1>
<p className="mt-6 text-gray-600 text-center md:text-left"> <p className="mt-6 text-gray-600 text-center md:text-left">
@@ -68,17 +100,69 @@ export default function HomePage() {
<section className="w-full relative shadow-blue-50 bg-white"> <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"> <Wrap className="relative pt-32 pb-48 max-md:pt-32 max-md:pb-24">
<PageSection title="为什么我们是你的最佳选择"> <PageSection title="为什么我们是你的最佳选择">
<p className="text-gray-600 text-center text-sm"> <p className="text-gray-600 text-center text-sm hidden md:block">
国内三大运营商正规授权,千万级代理IP资源,权威与品质的双重保障 国内三大运营商正规授权,千万级代理IP资源,权威与品质的双重保障
</p> </p>
<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6 pt-16"> <div className="relative pt-16">
<ProductTypeCard icon={s1} title="全网产品最多,IP库最大" /> {/* 左箭头 - 移动端显示 */}
<ProductTypeCard icon={s2} title="支持全设备及协议" /> {isMobile && showLeftArrow && (
<ProductTypeCard icon={s3} title="用户独享宽带" /> <button
<ProductTypeCard icon={s4} title="价低质优,免费测试" /> onClick={() => scroll("left")}
<ProductTypeCard icon={s5} title="专属化产品定制" /> className="absolute left-0 top-1/2 -translate-y-1/2 z-10 bg-white/90 hover:bg-white rounded-full shadow-lg p-2 border border-gray-200 transition-all md:hidden"
<ProductTypeCard icon={s6} title="专业的服务团队" /> >
</ul> <ChevronLeft className="w-5 h-5 text-gray-600" />
</button>
)}
{/* 右箭头 - 移动端显示 */}
{isMobile && showRightArrow && (
<button
onClick={() => scroll("right")}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 bg-white/90 hover:bg-white rounded-full shadow-lg p-2 border border-gray-200 transition-all md:hidden"
>
<ChevronRight className="w-5 h-5 text-gray-600" />
</button>
)}
{/* 左右渐变遮罩 - 移动端显示 */}
{isMobile && (
<>
<div className="absolute left-0 top-0 bottom-0 w-12 bg-linear-to-r from-white to-transparent pointer-events-none z-5 md:hidden" />
<div className="absolute right-0 top-0 bottom-0 w-12 bg-linear-to-r from-white to-transparent pointer-events-none z-5 md:hidden" />
</>
)}
{/* 滚动列表 */}
<ul
ref={scrollContainerRef}
onScroll={handleScroll}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6 overflow-x-auto scroll-smooth"
style={{
gridTemplateColumns: "repeat(6, 1fr)",
scrollbarWidth: "none",
msOverflowStyle: "none",
}}
>
<li className="min-w-37.5">
<ProductTypeCard icon={s1} title="全网产品最多,IP库最大" />
</li>
<li className="min-w-37.5">
<ProductTypeCard icon={s2} title="支持全设备及协议" />
</li>
<li className="min-w-37.5">
<ProductTypeCard icon={s3} title="用户独享宽带" />
</li>
<li className="min-w-37.5">
<ProductTypeCard icon={s4} title="价低质优,免费测试" />
</li>
<li className="min-w-37.5">
<ProductTypeCard icon={s5} title="专属化产品定制" />
</li>
<li className="min-w-37.5">
<ProductTypeCard icon={s6} title="专业的服务团队" />
</li>
</ul>
</div>
<div className="flex max-md:flex-col justify-between items-center gap-6 md:gap-10 mt-10 lg:mt-16 p-6 md:p-12 lg:p-20 shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white"> <div className="flex max-md:flex-col justify-between items-center gap-6 md:gap-10 mt-10 lg:mt-16 p-6 md:p-12 lg:p-20 shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white">
<div className="flex flex-col"> <div className="flex flex-col">
<h3 className="text-xl lg:text-2xl font-bold mb-4"> <h3 className="text-xl lg:text-2xl font-bold mb-4">
@@ -92,7 +176,7 @@ export default function HomePage() {
<img src={s7} alt="产品" className="w-20 h-20 rounded-lg" /> <img src={s7} alt="产品" className="w-20 h-20 rounded-lg" />
</div> </div>
</div> </div>
<div className="flex flex-col pt-12 lg:pt-24 "> <div className="flex flex-col pt-12 lg:pt-24">
<h2 className="text-center text-2xl lg:text-3xl font-bold mb-6 lg:mb-16"> <h2 className="text-center text-2xl lg:text-3xl font-bold mb-6 lg:mb-16">
海量地区,遍布全国的优质IP代理资源 海量地区,遍布全国的优质IP代理资源
</h2> </h2>
@@ -100,36 +184,37 @@ export default function HomePage() {
遍布全国的代理服务器节点为用户提供智能帮助 遍布全国的代理服务器节点为用户提供智能帮助
</p> </p>
</div> </div>
<div className="pt-10 lg:pt-20"> <div className="pt-10 lg:pt-20">
<ul className="shadow-[0_0_20px_4px] shadow-blue-50 p-8 flex max-lg:flex-col"> <ul className="grid grid-cols-2 lg:flex p-8 max-md:p-0 gap-4 lg:gap-0">
<li className="flex-1 flex flex-col items-center justify-center max-lg:mb-4 "> <li className="flex flex-col items-center justify-center lg:flex-1">
<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"> <p className="text-3xl lg:text-4xl bg-linear-to-t from-blue-500 to-cyan-400 bg-clip-text text-transparent font-bold">
10万+ 10万+
</p> </p>
<p className="text-sm">全国节点覆盖</p> <p className="text-sm text-gray-600">全国节点覆盖</p>
</li> </li>
<li className="flex-1 flex flex-col items-center justify-center max-lg:mb-4"> <li className="flex flex-col items-center justify-center lg:flex-1">
<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"> <p className="text-3xl lg:text-4xl bg-linear-to-t from-blue-500 to-cyan-400 bg-clip-text text-transparent font-bold">
100万+ 100万+
</p> </p>
<p className="text-sm">全国独享IP池</p> <p className="text-sm text-gray-600">全国独享IP池</p>
</li> </li>
<li className="flex-1 flex flex-col items-center justify-center max-lg:mb-4"> <li className="flex flex-col items-center justify-center lg:flex-1">
<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"> <p className="text-3xl lg:text-4xl bg-linear-to-t from-blue-500 to-cyan-400 bg-clip-text text-transparent font-bold">
10万+ 10万+
</p> </p>
<p className="text-sm">全国实时在线IP数</p> <p className="text-sm text-gray-600">全国实时在线IP数</p>
</li> </li>
<li className="flex-1 flex flex-col items-center justify-center"> <li className="flex flex-col items-center justify-center lg:flex-1">
<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"> <p className="text-3xl lg:text-4xl bg-linear-to-t from-blue-500 to-cyan-400 bg-clip-text text-transparent font-bold">
99% 99%
</p> </p>
<p className="text-sm">IP可用率</p> <p className="text-sm text-gray-600">IP可用率</p>
</li> </li>
</ul> </ul>
</div> </div>
<div className="flex flex-col pt-12 lg:pt-24 "> <div className="flex flex-col pt-12 lg:pt-24">
<h2 className="text-center text-2xl lg:text-3xl font-bold mb-6 lg:mb-16"> <h2 className="text-center text-2xl lg:text-3xl font-bold mb-6 lg:mb-16">
满足您的多种业务需求 满足您的多种业务需求
</h2> </h2>
@@ -138,7 +223,65 @@ export default function HomePage() {
</p> </p>
</div> </div>
<div className="flex flex-col mt-10 lg:mt-20"> {/* ========== 移动端 ========== */}
<div className="grid grid-cols-2 gap-4 mt-10 lg:hidden">
<div className="bg-white rounded-xl shadow-[0_0_20px_4px] shadow-blue-50 p-4">
<h4 className="text-sm font-semibold text-gray-800 mb-1">
爬虫抓取
</h4>
<p className="text-[10px] text-gray-500 leading-relaxed">
3000W高匿名IP,可以轻松抓取企业信息、分类信息、房地产信息、电商信息
</p>
</div>
<div className="bg-white rounded-xl shadow-[0_0_20px_4px] shadow-blue-50 p-4">
<h4 className="text-sm font-semibold text-gray-800 mb-1">
数据采集
</h4>
<p className="text-[10px] text-gray-500 leading-relaxed">
用于大数据采集的多样化利用场景,快速采集SEO数据优化、金融理财、地域信息数据
</p>
</div>
<div className="bg-white rounded-xl shadow-[0_0_20px_4px] shadow-blue-50 p-4">
<h4 className="text-sm font-semibold text-gray-800 mb-1">
营销推广
</h4>
<p className="text-[10px] text-gray-500 leading-relaxed">
论坛发帖、问答推广、网上购物、投票点赞等,根据渠道操作要求,定制IP完成营销推广
</p>
</div>
<div className="bg-white rounded-xl shadow-[0_0_20px_4px] shadow-blue-50 p-4">
<h4 className="text-sm font-semibold text-gray-800 mb-1">
电子商务
</h4>
<p className="text-[10px] text-gray-500 leading-relaxed">
可帮助用户切换网络,模拟多个独立账号,有助于用户参与限时促销活动
</p>
</div>
<div className="bg-white rounded-xl shadow-[0_0_20px_4px] shadow-blue-50 p-4">
<h4 className="text-sm font-semibold text-gray-800 mb-1">
游戏工作室
</h4>
<p className="text-[10px] text-gray-500 leading-relaxed">
保证IP稳定,不掉线,在有效时长内完成游戏试玩、升级、完成游戏工作室操作要求
</p>
</div>
<div className="bg-white rounded-xl shadow-[0_0_20px_4px] shadow-blue-50 p-4">
<h4 className="text-sm font-semibold text-gray-800 mb-1">
自媒体运营
</h4>
<p className="text-[10px] text-gray-500 leading-relaxed">
可帮助自媒体运营者维护账号独立性,为每个账号赋予独立IP,避免IP地址关联
</p>
</div>
</div>
{/* ========== 桌面端 ========== */}
<div className="hidden lg:flex flex-col mt-10 lg:mt-20">
<div className="relative flex items-center justify-center"> <div className="relative flex items-center justify-center">
<img src={s9} alt="图片" className="max-w-full h-auto" /> <img src={s9} alt="图片" className="max-w-full h-auto" />
<div className="absolute left-0 top-0 w-[30%] h-full flex flex-col justify-between"> <div className="absolute left-0 top-0 w-[30%] h-full flex flex-col justify-between">
@@ -197,16 +340,65 @@ export default function HomePage() {
</div> </div>
</div> </div>
<div className="flex flex-col pt-12 lg:pt-24 "> <div className="flex flex-col pt-12 lg:pt-24">
<h2 className="text-center text-2xl lg:text-3xl font-bold mb-6 lg:mb-16"> <h2 className="text-center text-2xl lg:text-3xl font-bold mb-6 lg:mb-16">
为您提供质量保障 为您提供质量保障
</h2> </h2>
<p className="text-gray-600 text-center text-sm"> <p className="text-gray-600 text-center text-sm">
国内三大运营商正规授权,千万级代理IP资源,权威与品质的双重保障 国内三大运营商正规授权,权威与品质的双重保障
</p> </p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 pt-20"> {/* 移动端 */}
<div className="flex flex-col gap-6 pt-10 md:hidden">
{/* 售前咨询 */}
<div className="flex items-start gap-4 shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white rounded-xl p-5">
<div className="w-16 h-16 bg-blue-50 rounded-xl flex items-center justify-center shrink-0">
<img src={s10} alt="售前咨询" className="w-10 h-10" />
</div>
<div>
<h3 className="text-base font-bold text-gray-800 mb-1">
售前咨询
</h3>
<p className="text-xs text-gray-600 leading-relaxed">
注册即享新人福利,专业的客户经理,多维度为您提供在线代理相关答疑
</p>
</div>
</div>
{/* 售中服务 */}
<div className="flex items-start gap-4 shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white rounded-xl p-5">
<div className="w-16 h-16 bg-blue-50 rounded-xl flex items-center justify-center shrink-0">
<img src={s11} alt="售中服务" className="w-10 h-10" />
</div>
<div>
<h3 className="text-base font-bold text-gray-800 mb-1">
售中服务
</h3>
<p className="text-xs text-gray-600 leading-relaxed">
全国100万+动态IP代理资源免费测试,先测后买让您安心使用
</p>
</div>
</div>
{/* 售后保障 */}
<div className="flex items-start gap-4 shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white rounded-xl p-5">
<div className="w-16 h-16 bg-blue-50 rounded-xl flex items-center justify-center shrink-0">
<img src={s12} alt="售后保障" className="w-10 h-10" />
</div>
<div>
<h3 className="text-base font-bold text-gray-800 mb-1">
售后保障
</h3>
<p className="text-xs text-gray-600 leading-relaxed">
1V1专属售后答疑,技术团队7*24小时在线支持提供专属解决方案
</p>
</div>
</div>
</div>
{/* 桌面端 */}
<div className="hidden md:grid 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 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"> <div className="flex w-full mb-5 bg-blue-50 items-center justify-center">
<img src={s10} alt="售前咨询" className="p-6" /> <img src={s10} alt="售前咨询" className="p-6" />
@@ -215,8 +407,7 @@ export default function HomePage() {
售前咨询 售前咨询
</h2> </h2>
<p className="text-xs text-gray-600"> <p className="text-xs text-gray-600">
注册即享新人福利,专业的客户经理,多维度为您提供在线代理相关 注册即享新人福利,专业的客户经理,多维度为您提供在线代理相关答疑
答疑
</p> </p>
</div> </div>
<div className="flex flex-col shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-8"> <div className="flex flex-col shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-8">
@@ -368,13 +559,13 @@ function PageSection(props: { title: string; children: ReactNode }) {
function ProductTypeCard(props: { icon: string; title: string }) { function ProductTypeCard(props: { icon: string; title: string }) {
return ( return (
<li className="flex flex-col items-center gap-5 rounded-lg text-center gap-6"> <div className="flex flex-col items-center gap-5 rounded-lg text-center">
<img <img
src={props.icon} src={props.icon}
alt={props.title} alt={props.title}
className="w-20 h-20 object-cover" className="w-20 h-20 object-cover"
/> />
<p className="text-sm">{props.title}</p> <p className="text-sm">{props.title}</p>
</li> </div>
) )
} }

View File

@@ -154,16 +154,18 @@ export default function ProductPage() {
} }
return ( return (
<Wrap className="flex flex-col gap-16 pt-30 bg-white"> <Wrap className="flex flex-col gap-16 pt-30 bg-white">
<div className="flex flex-col"> <div className="flex flex-col">
<ProductTabs <ProductTabs
tabs={tabs} tabs={tabs}
activeId={activeTabId} activeId={activeTabId}
onTabChange={handleTabChange} onTabChange={handleTabChange}
/> />
<div className="bg-orange-50 border block md:hidden border-orange-100 text-orange-400 p-3 text-center font-bold text-sm w-full mt-4">
<div className="flex mt-8 mb-8 mx-auto w-full"> 需求5个以上,可以联系客服设置优惠价
<div className="shrink-0 border-r border-gray-200"> </div>
<div className="flex flex-col md:flex-row md:mt-8 md:mb-8 mx-auto w-full">
<div className="shrink-0 md:border-r md:border-gray-200">
<ProductSidebar <ProductSidebar
brands={currentTab.brands} brands={currentTab.brands}
activeId={currentBrand?.id || ""} activeId={currentBrand?.id || ""}
@@ -174,7 +176,7 @@ export default function ProductPage() {
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{!isPurchaseInfo ? ( {!isPurchaseInfo ? (
<> <>
<div className="grid grid-cols-2 pl-3 text-sm gap-2 text-gray-600 leading-relaxed"> <div className="grid grid-cols-1 md:grid-cols-2 pl-3 text-sm gap-2 text-gray-600 leading-relaxed">
<ul className="space-y-2"> <ul className="space-y-2">
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" /> <span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
@@ -196,11 +198,11 @@ export default function ProductPage() {
</li> </li>
</ul> </ul>
</div> </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"> <div className="bg-orange-50 border gap-3 border-orange-100 hidden md:block text-orange-400 p-3 text-center font-bold mt-3 mb-3 text-sm w-full">
需求5个以上,可以联系客服设置优惠价 需求5个以上,可以联系客服设置优惠价
</div> </div>
{currentBrand?.versions && currentBrand.versions.length > 0 && ( {currentBrand?.versions && currentBrand.versions.length > 0 && (
<div className="w-full mt-2 pl-6"> <div className="w-full mt-2 px-3 md:pl-6 md:pr-0">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{currentBrand.versions.map(v => ( {currentBrand.versions.map(v => (
<button <button
@@ -219,7 +221,7 @@ export default function ProductPage() {
</div> </div>
</div> </div>
)} )}
<main className="pl-6"> <main className="pl-3 md:pl-6">
{displayProducts.length > 0 ? ( {displayProducts.length > 0 ? (
<ProductGrid products={displayProducts} /> <ProductGrid products={displayProducts} />
) : ( ) : (
@@ -230,7 +232,7 @@ export default function ProductPage() {
</main> </main>
</> </>
) : ( ) : (
<div className="pl-6 pr-6"> <div className="px-3 md:pl-6 md:pr-6">
<PurchaseInfo /> <PurchaseInfo />
</div> </div>
)} )}

View File

@@ -13,21 +13,24 @@ export default function ProductSidebar({
onSelect, onSelect,
}: ProductSidebarProps) { }: ProductSidebarProps) {
return ( return (
<aside className="w-30"> <aside className="w-full md:w-30 overflow-visible">
{brands.map(brand => ( <div className="flex gap-2 overflow-x-auto px-4 py-3 scrollbar-hide md:flex-col md:gap-0 md:overflow-visible md:px-0 md:py-0">
<button {brands.map(brand => (
key={brand.id} <button
onClick={() => onSelect(brand.id)} key={brand.id}
className={cn( onClick={() => onSelect(brand.id)}
"w-full text-left px-4 py-3 rounded-lg text-sm transition-colors", className={cn(
activeId === brand.id "shrink-0 whitespace-nowrap px-5 py-1.5 rounded-full text-sm font-medium transition-colors",
? "bg-blue-50 text-blue-600 font-semibold" "md:w-full md:text-left md:px-4 md:py-3 md:rounded-lg md:whitespace-normal",
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900", 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> >
))} {brand.name}
</button>
))}
</div>
</aside> </aside>
) )
} }

View File

@@ -1,15 +1,15 @@
export default function PurchaseInfo() { export default function PurchaseInfo() {
return ( return (
<div className="flex w-full max-w-6xl mx-auto"> <div className="flex flex-col md:flex-row w-full max-w-6xl mx-auto">
<div className="flex-1 space-y-3 pr-3 border-r"> <div className="flex-1 space-y-3 md:pr-3 md:border-r">
<div> <div>
<h3 className="text-lg font-bold text-orange-400 mb-3">重磅打造:</h3> <h3 className="text-lg font-bold text-orange-400 mb-3">重磅打造:</h3>
<ul className="space-y-2 text-sm text-gray-600"> <ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-start gap-2 whitespace-nowrap"> <li className="flex items-start gap-2">
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" /> <span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
极光IP(聚IP池子最大的动态产品),240城市纯净住宅IP;现推广期,天卡1毛体验 极光IP(聚IP池子最大的动态产品),240城市纯净住宅IP;现推广期,天卡1毛体验
</li> </li>
<li className="flex items-start gap-2 whitespace-nowrap"> <li className="flex items-start gap-2">
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" /> <span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
极狐IP(聚IP网速最快的动态产品),260城市纯净住宅IP;现推广期,天卡1毛体验 极狐IP(聚IP网速最快的动态产品),260城市纯净住宅IP;现推广期,天卡1毛体验
</li> </li>
@@ -40,7 +40,7 @@ export default function PurchaseInfo() {
</div> </div>
</div> </div>
<div className="flex-none w-90 pl-6 space-y-6"> <div className="flex-none md:w-90 md:pl-6 space-y-6 mt-6 md:mt-0">
<h3 className="text-lg font-bold text-orange-400 mb-2"> <h3 className="text-lg font-bold text-orange-400 mb-2">
1V1定制化企业级业务场景 1V1定制化企业级业务场景
</h3> </h3>

View File

@@ -262,3 +262,11 @@ code {
@apply font-sans; @apply font-sans;
} }
} }
@utility scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}

View File

@@ -0,0 +1,36 @@
import { useSyncExternalStore } from "react"
const subscribeMediaQuery = (query: string) => (callback: () => void) => {
if (typeof window === "undefined") return () => {}
const mql = window.matchMedia(query)
mql.addEventListener("change", callback)
return () => mql.removeEventListener("change", callback)
}
const getMediaQuerySnapshot = (query: string) => () => {
if (typeof window === "undefined") return false
return window.matchMedia(query).matches
}
export function useMediaQuery(query: string): boolean {
return useSyncExternalStore(
subscribeMediaQuery(query),
getMediaQuerySnapshot(query),
() => false,
)
}
// 移动端:< 768px(对应 Tailwind md 断点以下)
export function useIsMobile(): boolean {
return useMediaQuery("(max-width: 47.999rem)")
}
// 桌面端:>= 768px
export function useIsDesktop(): boolean {
return useMediaQuery("(min-width: 48rem)")
}
// 窄桌面/平板横屏:< 1280px(对应 Tailwind xl 断点以下)
export function useIsNarrowDesktop(): boolean {
return useMediaQuery("(max-width: 79.999rem)")
}