2026-08-13 18:28:20 +08:00
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
2026-07-24 18:00:22 +08:00
|
|
|
|
import { useSearchParams } from "react-router-dom"
|
2026-08-18 13:33:22 +08:00
|
|
|
|
import {
|
|
|
|
|
|
fetchProducts,
|
|
|
|
|
|
fetchStaticProducts,
|
|
|
|
|
|
fetchWindowProducts,
|
|
|
|
|
|
} from "@/api/product"
|
2026-07-24 18:00:22 +08:00
|
|
|
|
import Wrap from "@/components/wrap"
|
2026-08-18 13:33:22 +08:00
|
|
|
|
import type { ApiResponse } from "@/lib/api"
|
|
|
|
|
|
import type { ProductItem, Tab, TabSource } from "@/lib/models/product"
|
2026-08-13 18:28:20 +08:00
|
|
|
|
import { transformToTabs } from "@/lib/models/product"
|
2026-07-24 18:00:22 +08:00
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
|
|
import ProductGrid from "./productGrid"
|
|
|
|
|
|
import ProductSidebar from "./productSidebar"
|
|
|
|
|
|
import ProductTabs from "./productTabs"
|
|
|
|
|
|
import PurchaseInfo from "./purchaseInfo"
|
|
|
|
|
|
|
2026-08-07 16:54:21 +08:00
|
|
|
|
const TAB_ALIAS: Record<string, string> = {
|
|
|
|
|
|
short: "dynamic",
|
|
|
|
|
|
long: "dynamic",
|
|
|
|
|
|
tunnel: "dynamic",
|
|
|
|
|
|
exclusive: "static",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-18 13:33:22 +08:00
|
|
|
|
const TAB_SOURCES: {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
fetch: () => Promise<ApiResponse<ProductItem[]>>
|
|
|
|
|
|
}[] = [
|
|
|
|
|
|
{ id: "dynamic", label: "动态独享IP", fetch: fetchProducts },
|
|
|
|
|
|
{ id: "static", label: "静态IP", fetch: fetchStaticProducts },
|
|
|
|
|
|
{ id: "window", label: "单窗口单IP", fetch: fetchWindowProducts },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-07-24 18:00:22 +08:00
|
|
|
|
export default function ProductPage() {
|
|
|
|
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
2026-08-13 18:28:20 +08:00
|
|
|
|
const [tabs, setTabs] = useState<Tab[]>([])
|
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const [activeBrandId, setActiveBrandId] = useState("")
|
|
|
|
|
|
const [selectedVersionId, setSelectedVersionId] = useState("")
|
|
|
|
|
|
const initializedRef = useRef(false)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-08-18 13:33:22 +08:00
|
|
|
|
let cancelled = false
|
|
|
|
|
|
|
|
|
|
|
|
const load = async () => {
|
|
|
|
|
|
const results = await Promise.allSettled(
|
|
|
|
|
|
TAB_SOURCES.map(source => source.fetch()),
|
|
|
|
|
|
)
|
|
|
|
|
|
console.log(results, "resultsresultsresults")
|
|
|
|
|
|
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
|
|
|
|
|
|
|
const sources: TabSource[] = []
|
|
|
|
|
|
const failedLabels: string[] = []
|
|
|
|
|
|
|
|
|
|
|
|
TAB_SOURCES.forEach((source, index) => {
|
|
|
|
|
|
const result = results[index]
|
|
|
|
|
|
if (result?.status === "fulfilled" && result.value.success) {
|
|
|
|
|
|
sources.push({
|
|
|
|
|
|
id: source.id,
|
|
|
|
|
|
label: source.label,
|
|
|
|
|
|
data: result.value.data,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
failedLabels.push(source.label)
|
2026-08-13 18:28:20 +08:00
|
|
|
|
}
|
2026-08-18 13:33:22 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
if (sources.length === 0) {
|
|
|
|
|
|
setError("产品列表加载失败,请稍后重试")
|
|
|
|
|
|
setLoading(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (failedLabels.length > 0) {
|
|
|
|
|
|
console.warn(`部分产品列表加载失败: ${failedLabels.join("、")}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const newTabs = transformToTabs(sources)
|
|
|
|
|
|
setTabs(newTabs)
|
|
|
|
|
|
if (!initializedRef.current && newTabs[0]?.brands[0]) {
|
|
|
|
|
|
setActiveBrandId(newTabs[0].brands[0].id)
|
|
|
|
|
|
initializedRef.current = true
|
2026-08-13 18:28:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
setLoading(false)
|
2026-08-18 13:33:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
load()
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
2026-08-13 18:28:20 +08:00
|
|
|
|
}, [])
|
2026-07-24 18:00:22 +08:00
|
|
|
|
|
2026-08-07 16:54:21 +08:00
|
|
|
|
const urlTab = searchParams.get("tab") || "dynamic"
|
|
|
|
|
|
const activeTabId = TAB_ALIAS[urlTab] || urlTab
|
2026-07-24 18:00:22 +08:00
|
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
|
const currentTab = tabs.find(t => t.id === activeTabId) || tabs[0]
|
2026-07-24 18:00:22 +08:00
|
|
|
|
const currentBrand =
|
2026-08-13 18:28:20 +08:00
|
|
|
|
currentTab?.brands.find(b => b.id === activeBrandId) ||
|
|
|
|
|
|
currentTab?.brands[0]
|
|
|
|
|
|
|
|
|
|
|
|
const effectiveVersionId =
|
|
|
|
|
|
selectedVersionId || currentBrand?.versions?.[0]?.id || ""
|
2026-07-24 18:00:22 +08:00
|
|
|
|
|
|
|
|
|
|
const handleTabChange = (tabId: string) => {
|
2026-08-13 18:28:20 +08:00
|
|
|
|
setSearchParams({ tab: tabId })
|
|
|
|
|
|
const targetTab = tabs.find(t => t.id === tabId)
|
2026-07-24 18:00:22 +08:00
|
|
|
|
const firstBrand = targetTab?.brands[0]
|
2026-08-13 18:28:20 +08:00
|
|
|
|
setActiveBrandId(firstBrand?.id || "")
|
|
|
|
|
|
setSelectedVersionId("")
|
2026-07-24 18:00:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleBrandSelect = (brandId: string) => {
|
2026-08-13 18:28:20 +08:00
|
|
|
|
setActiveBrandId(brandId)
|
|
|
|
|
|
setSelectedVersionId("")
|
2026-07-24 18:00:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-13 18:28:20 +08:00
|
|
|
|
const displayProducts = (() => {
|
2026-07-24 18:00:22 +08:00
|
|
|
|
if (!currentBrand) return []
|
|
|
|
|
|
if (currentBrand.versions && currentBrand.versions.length > 0) {
|
2026-08-13 18:28:20 +08:00
|
|
|
|
return currentBrand.products.filter(p => p.version === effectiveVersionId)
|
2026-07-24 18:00:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
return currentBrand.products
|
2026-08-13 18:28:20 +08:00
|
|
|
|
})()
|
2026-07-24 18:00:22 +08:00
|
|
|
|
|
|
|
|
|
|
const isPurchaseInfo = currentBrand?.id === "info"
|
2026-08-13 18:28:20 +08:00
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Wrap className="flex items-center justify-center pt-30 bg-white min-h-96">
|
|
|
|
|
|
<div className="text-gray-400">加载中...</div>
|
|
|
|
|
|
</Wrap>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Wrap className="flex items-center justify-center pt-30 bg-white min-h-96">
|
|
|
|
|
|
<div className="text-red-400">加载失败:{error}</div>
|
|
|
|
|
|
</Wrap>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (tabs.length === 0) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Wrap className="flex items-center justify-center pt-30 bg-white min-h-96">
|
|
|
|
|
|
<div className="text-gray-400">暂无产品数据</div>
|
|
|
|
|
|
</Wrap>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 18:00:22 +08:00
|
|
|
|
return (
|
2026-08-26 16:55:22 +08:00
|
|
|
|
<Wrap className="flex flex-col gap-16 pt-30 bg-white">
|
2026-07-24 18:00:22 +08:00
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<ProductTabs
|
2026-08-13 18:28:20 +08:00
|
|
|
|
tabs={tabs}
|
2026-07-24 18:00:22 +08:00
|
|
|
|
activeId={activeTabId}
|
|
|
|
|
|
onTabChange={handleTabChange}
|
|
|
|
|
|
/>
|
2026-08-26 16:55:22 +08:00
|
|
|
|
<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">
|
|
|
|
|
|
需求5个以上,可以联系客服设置优惠价
|
|
|
|
|
|
</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">
|
2026-07-24 18:00:22 +08:00
|
|
|
|
<ProductSidebar
|
|
|
|
|
|
brands={currentTab.brands}
|
|
|
|
|
|
activeId={currentBrand?.id || ""}
|
|
|
|
|
|
onSelect={handleBrandSelect}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex-1 flex flex-col">
|
|
|
|
|
|
{!isPurchaseInfo ? (
|
|
|
|
|
|
<>
|
2026-08-26 16:55:22 +08:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 pl-3 text-sm gap-2 text-gray-600 leading-relaxed">
|
2026-07-24 18:00:22 +08:00
|
|
|
|
<ul className="space-y-2">
|
|
|
|
|
|
<li className="flex items-start gap-2">
|
|
|
|
|
|
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
|
|
|
|
|
连接方式:电脑和安卓请优选客户端,苹果,路由器可通过线路表直连
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li className="flex items-start gap-2">
|
|
|
|
|
|
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
|
|
|
|
|
本产品在个人中心签署协议后可联系客服解除限制
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<ul className="space-y-2">
|
|
|
|
|
|
<li className="flex items-start gap-2">
|
|
|
|
|
|
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
|
|
|
|
|
IP池超大且纯净,所有线路均为三网混合,更真实有效
|
|
|
|
|
|
</li>
|
|
|
|
|
|
<li className="flex items-start gap-2">
|
|
|
|
|
|
<span className="w-2 h-2 mt-1.5 bg-emerald-400 rounded-full shrink-0" />
|
|
|
|
|
|
230+城市,500+线路,有全国混拨和省混拨
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-08-26 16:55:22 +08:00
|
|
|
|
<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">
|
2026-07-24 18:00:22 +08:00
|
|
|
|
需求5个以上,可以联系客服设置优惠价
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{currentBrand?.versions && currentBrand.versions.length > 0 && (
|
2026-08-26 16:55:22 +08:00
|
|
|
|
<div className="w-full mt-2 px-3 md:pl-6 md:pr-0">
|
2026-07-24 18:00:22 +08:00
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
|
|
|
|
{currentBrand.versions.map(v => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={v.id}
|
2026-08-13 18:28:20 +08:00
|
|
|
|
onClick={() => setSelectedVersionId(v.id)}
|
2026-07-24 18:00:22 +08:00
|
|
|
|
className={cn(
|
|
|
|
|
|
"w-full py-2 rounded-full text-sm font-medium transition-all cursor-pointer text-center",
|
2026-08-13 18:28:20 +08:00
|
|
|
|
effectiveVersionId === v.id
|
2026-07-24 18:00:22 +08:00
|
|
|
|
? "bg-linear-to-r from-blue-500 to-cyan-400 text-white shadow-sm"
|
|
|
|
|
|
: "bg-gray-100 text-gray-600 hover:bg-gray-200",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{v.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-08-26 16:55:22 +08:00
|
|
|
|
<main className="pl-3 md:pl-6">
|
2026-07-24 18:00:22 +08:00
|
|
|
|
{displayProducts.length > 0 ? (
|
|
|
|
|
|
<ProductGrid products={displayProducts} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="text-center py-20 text-gray-400">
|
|
|
|
|
|
该版本暂无产品数据
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
2026-08-26 16:55:22 +08:00
|
|
|
|
<div className="px-3 md:pl-6 md:pr-6">
|
2026-07-24 18:00:22 +08:00
|
|
|
|
<PurchaseInfo />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Wrap>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|