Files
juip-web/src/home/product/index.tsx

245 lines
8.4 KiB
TypeScript
Raw Normal View History

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"
import {
fetchProducts,
fetchStaticProducts,
fetchWindowProducts,
} from "@/api/product"
2026-07-24 18:00:22 +08:00
import Wrap from "@/components/wrap"
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",
}
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(() => {
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
}
})
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)
}
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 (
<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}
/>
<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 ? (
<>
<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>
<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 && (
<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>
)}
<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>
</>
) : (
<div className="px-3 md:pl-6 md:pr-6">
2026-07-24 18:00:22 +08:00
<PurchaseInfo />
</div>
)}
</div>
</div>
</div>
</Wrap>
)
}