Compare commits
2 Commits
9f917a39f9
...
ff56fbd7c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff56fbd7c3 | ||
|
|
5007cd9f51 |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
182
src/home/_components/header/menu-mobile.tsx
Normal file
182
src/home/_components/header/menu-mobile.tsx
Normal 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>
|
||||||
|
)
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
36
src/lib/hooks/useMediaQuery.ts
Normal file
36
src/lib/hooks/useMediaQuery.ts
Normal 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)")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user