diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..b10583c Binary files /dev/null and b/public/logo.png differ diff --git a/src/home/_components/header/common.tsx b/src/home/_components/header/common.tsx index b4bbe04..1196685 100644 --- a/src/home/_components/header/common.tsx +++ b/src/home/_components/header/common.tsx @@ -1,12 +1,15 @@ +import { ArrowUp, ChevronDown, ChevronUp } from "lucide-react" import { useState } from "react" +import { cn } from "@/lib/utils" 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" +import phoneImg from "../assets/phone.webp" +import qqImg from "../assets/qq.webp" +import topImg from "../assets/top.webp" +import wxImg from "../assets/wx.webp" export default function Common() { const [activePopup, setActivePopup] = useState<"gzh" | "tel" | null>(null) + const [isExpanded, setIsExpanded] = useState(false) // QQ点击跳转 const handleQQClick = () => { @@ -20,118 +23,277 @@ export default function Common() { } return ( -
- {/* QQ */} -
{ - 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" - > - QQ - QQ + <> + {/* ========== 桌面端 ========== */} +
+
{ + 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" + > + QQ + QQ +
+ +
{ + 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" + > + 微信 + 微信 +
+ +
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")) + } + }} + > + 公众号 + 公众号 + {activePopup === "gzh" && ( +
+ 公众号二维码 +

+ 实时响应 在线时间 8:30-23:00 +

+

+ 关注公众号可获取免费的到期提醒服务 +

+
+ )} +
+ +
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")) + } + }} + > + 电话 + 电话 + {activePopup === "tel" && ( +
+

400 800 9925

+

实时接通

+

+ 电话值班时间 8:30-23:00 +

+
+ )} +
+ +
- {/* 微信 */} -
{ - 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" - > - 微信 - 微信 -
- - {/* 公众号 */} -
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")) - } - }} - > - 公众号 - 公众号 - - {/* 公众号悬浮弹窗 */} - {activePopup === "gzh" && ( -
- 公众号二维码 -

- 实时响应 在线时间 8:30-23:00 -

-

- 关注公众号可获取免费的到期提醒服务 -

+ {/* ========== 移动端:底部折叠面板 ========== */} +
+ {/* 折叠面板 */} +
+ {/* 拖拽条*/} +
setIsExpanded(!isExpanded)} + className="flex flex-col items-center justify-center py-2.5 cursor-pointer hover:bg-gray-50 transition-colors rounded-t-2xl" + > +
+
+ + {isExpanded ? "收起" : "联系我们"} + + {isExpanded ? ( + + ) : ( + + )} +
- )} -
- {/* 电话 */} -
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")) - } - }} - > - 电话 - 电话 + {/* 展开内容 */} +
+
+ {/* QQ */} +
+
+ QQ +
+
+

QQ客服

+

点击立即咨询

+
+
- {/* 电话悬浮弹窗 */} - {activePopup === "tel" && ( -
-

400 800 9925

-

实时接通

-

- 电话值班时间 8:30-23:00 -

+ {/* 微信 */} +
+
+ 微信 +
+
+

微信客服

+

点击立即咨询

+
+
+ + {/* 公众号 */} +
+ 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" + > +
+ 公众号 +
+
+

公众号

+

点击查看二维码

+
+ +
+ + {/* 公众号二维码展开 */} + {activePopup === "gzh" && ( +
+ 公众号二维码 +

+ 实时响应 在线时间 8:30-23:00 +

+

+ 关注公众号可获取免费的到期提醒服务 +

+
+ )} + + {/* 电话 */} +
+ 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" + > +
+ 电话 +
+
+

电话客服

+

点击查看号码

+
+ +
+ + {/* 电话展开 */} + {activePopup === "tel" && ( +
+

+ 400 800 9925 +

+

实时接通

+

+ 电话值班时间 8:30-23:00 +

+
+ )} + + {/* 回到顶部 */} + +
- )} +
- - {/* 回到顶部 */} - -
+ ) } diff --git a/src/home/_components/header/menu-mobile.tsx b/src/home/_components/header/menu-mobile.tsx new file mode 100644 index 0000000..18fe543 --- /dev/null +++ b/src/home/_components/header/menu-mobile.tsx @@ -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 ( +
+ +
+ + {/* 菜单项 */} +
+ + 首页 + + + {/* 二级菜单 */} +
+ + {productOpen && ( +
+ go("/product")}>动静态IP + openExternal("https://lanhuip.com")}> + 新HTTP/S5 + + go("/product/http")}>HTTP/S5 + go("/product/routeros")}> + 软路由成本价 + + openExternal("http://vps.juip.com")}> + 动态Vps + +
+ )} +
+ + + IP线路表 + + + 软件下载 + + + 帮助/教程 + + + 下载客户端 + + + {authed && ( + <> + + + + )} +
+
+
+ ) +} + +const MobileNavLink = ({ + to, + onNavigate, + children, +}: { + to: string + onNavigate: () => void + children: React.ReactNode +}) => ( + + 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} + +) + +const SubItem = ({ + onClick, + children, +}: { + onClick: () => void + children: React.ReactNode +}) => ( + +) diff --git a/src/home/header.tsx b/src/home/header.tsx index a287846..4f402bd 100644 --- a/src/home/header.tsx +++ b/src/home/header.tsx @@ -1,11 +1,17 @@ -import { useRef, useState, useSyncExternalStore } from "react" -import { NavLink } from "react-router-dom" +import { Menu, User } from "lucide-react" +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 ProfileOrLogin from "@/components/profileOrLogin" import Wrap from "@/components/wrap" +import { useMediaQuery } from "@/lib/hooks/useMediaQuery" import { cn } from "@/lib/utils" +import MobileMenu from "./_components/header/menu-mobile" export default function Header() { + const navigate = useNavigate() + const isWideDesktop = useMediaQuery("(min-width: 80rem)") const scroll = useSyncExternalStore( callback => { window.addEventListener("scroll", callback) @@ -22,6 +28,20 @@ export default function Header() { const handleEnter = () => setOpenMenu(true) 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 (
- -
+ + {/* 移动端左侧菜单栏 */} + {mobileMenu && ( + setMobileMenu(false)} /> + )}
) } @@ -147,15 +214,11 @@ const MenuItem = ({
{children} diff --git a/src/home/index.tsx b/src/home/index.tsx index 6fb2809..6920201 100644 --- a/src/home/index.tsx +++ b/src/home/index.tsx @@ -1,6 +1,9 @@ +import { ChevronLeft, ChevronRight } from "lucide-react" import type { ReactNode } from "react" +import { useRef, useState } from "react" import { useNavigate } from "react-router-dom" import Wrap from "@/components/wrap" +import { useIsMobile } from "@/lib/hooks/useMediaQuery" import banner from "./_assets/banner.webp" import s1 from "./_assets/s1.webp" import s2 from "./_assets/s2.webp" @@ -22,6 +25,35 @@ export default function HomePage() { const handleFreeTrial = () => { navigate("/login") } + + const scrollContainerRef = useRef(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 ( <> @@ -37,7 +69,7 @@ export default function HomePage() { /> -

+

聚集优质IP产品,尽在聚IP

@@ -68,17 +100,69 @@ export default function HomePage() {

-

+

国内三大运营商正规授权,千万级代理IP资源,权威与品质的双重保障

-
    - - - - - - -
+
+ {/* 左箭头 - 移动端显示 */} + {isMobile && showLeftArrow && ( + + )} + + {/* 右箭头 - 移动端显示 */} + {isMobile && showRightArrow && ( + + )} + + {/* 左右渐变遮罩 - 移动端显示 */} + {isMobile && ( + <> +
+
+ + )} + + {/* 滚动列表 */} +
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+

@@ -92,7 +176,7 @@ export default function HomePage() { 产品

-
+

海量地区,遍布全国的优质IP代理资源

@@ -100,36 +184,37 @@ export default function HomePage() { 遍布全国的代理服务器节点为用户提供智能帮助

+
-
    -
  • -

    +

      +
    • +

      10万+

      -

      全国节点覆盖

      +

      全国节点覆盖

    • -
    • -

      +

    • +

      100万+

      -

      全国独享IP池

      +

      全国独享IP池

    • -
    • -

      +

    • +

      10万+

      -

      全国实时在线IP数

      +

      全国实时在线IP数

    • -
    • -

      +

    • +

      99%

      -

      IP可用率

      +

      IP可用率

-
+

满足您的多种业务需求

@@ -138,7 +223,65 @@ export default function HomePage() {

-
+ {/* ========== 移动端 ========== */} +
+
+

+ 爬虫抓取 +

+

+ 3000W高匿名IP,可以轻松抓取企业信息、分类信息、房地产信息、电商信息 +

+
+ +
+

+ 数据采集 +

+

+ 用于大数据采集的多样化利用场景,快速采集SEO数据优化、金融理财、地域信息数据 +

+
+ +
+

+ 营销推广 +

+

+ 论坛发帖、问答推广、网上购物、投票点赞等,根据渠道操作要求,定制IP完成营销推广 +

+
+ +
+

+ 电子商务 +

+

+ 可帮助用户切换网络,模拟多个独立账号,有助于用户参与限时促销活动 +

+
+ +
+

+ 游戏工作室 +

+

+ 保证IP稳定,不掉线,在有效时长内完成游戏试玩、升级、完成游戏工作室操作要求 +

+
+ +
+

+ 自媒体运营 +

+

+ 可帮助自媒体运营者维护账号独立性,为每个账号赋予独立IP,避免IP地址关联 +

+
+
+ + {/* ========== 桌面端 ========== */} +
图片
@@ -197,16 +340,65 @@ export default function HomePage() {
-
+

为您提供质量保障

- 国内三大运营商正规授权,千万级代理IP资源,权威与品质的双重保障 + 国内三大运营商正规授权,权威与品质的双重保障

-
+ {/* 移动端 */} +
+ {/* 售前咨询 */} +
+
+ 售前咨询 +
+
+

+ 售前咨询 +

+

+ 注册即享新人福利,专业的客户经理,多维度为您提供在线代理相关答疑 +

+
+
+ + {/* 售中服务 */} +
+
+ 售中服务 +
+
+

+ 售中服务 +

+

+ 全国100万+动态IP代理资源免费测试,先测后买让您安心使用 +

+
+
+ + {/* 售后保障 */} +
+
+ 售后保障 +
+
+

+ 售后保障 +

+

+ 1V1专属售后答疑,技术团队7*24小时在线支持提供专属解决方案 +

+
+
+
+ + {/* 桌面端 */} +
售前咨询 @@ -215,8 +407,7 @@ export default function HomePage() { 售前咨询

- 注册即享新人福利,专业的客户经理,多维度为您提供在线代理相关 - 答疑 + 注册即享新人福利,专业的客户经理,多维度为您提供在线代理相关答疑

@@ -368,13 +559,13 @@ function PageSection(props: { title: string; children: ReactNode }) { function ProductTypeCard(props: { icon: string; title: string }) { return ( -
  • +
    {props.title}

    {props.title}

    -
  • +
    ) } diff --git a/src/lib/hooks/useMediaQuery.ts b/src/lib/hooks/useMediaQuery.ts new file mode 100644 index 0000000..fd7c032 --- /dev/null +++ b/src/lib/hooks/useMediaQuery.ts @@ -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)") +}