Files
juip-web/src/home/product/index.tsx
2026-08-26 16:55:22 +08:00

245 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from "react"
import { useSearchParams } from "react-router-dom"
import {
fetchProducts,
fetchStaticProducts,
fetchWindowProducts,
} from "@/api/product"
import Wrap from "@/components/wrap"
import type { ApiResponse } from "@/lib/api"
import type { ProductItem, Tab, TabSource } from "@/lib/models/product"
import { transformToTabs } from "@/lib/models/product"
import { cn } from "@/lib/utils"
import ProductGrid from "./productGrid"
import ProductSidebar from "./productSidebar"
import ProductTabs from "./productTabs"
import PurchaseInfo from "./purchaseInfo"
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 },
]
export default function ProductPage() {
const [searchParams, setSearchParams] = useSearchParams()
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)
}
})
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
}
setLoading(false)
}
load()
return () => {
cancelled = true
}
}, [])
const urlTab = searchParams.get("tab") || "dynamic"
const activeTabId = TAB_ALIAS[urlTab] || urlTab
const currentTab = tabs.find(t => t.id === activeTabId) || tabs[0]
const currentBrand =
currentTab?.brands.find(b => b.id === activeBrandId) ||
currentTab?.brands[0]
const effectiveVersionId =
selectedVersionId || currentBrand?.versions?.[0]?.id || ""
const handleTabChange = (tabId: string) => {
setSearchParams({ tab: tabId })
const targetTab = tabs.find(t => t.id === tabId)
const firstBrand = targetTab?.brands[0]
setActiveBrandId(firstBrand?.id || "")
setSelectedVersionId("")
}
const handleBrandSelect = (brandId: string) => {
setActiveBrandId(brandId)
setSelectedVersionId("")
}
const displayProducts = (() => {
if (!currentBrand) return []
if (currentBrand.versions && currentBrand.versions.length > 0) {
return currentBrand.products.filter(p => p.version === effectiveVersionId)
}
return currentBrand.products
})()
const isPurchaseInfo = currentBrand?.id === "info"
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>
)
}
return (
<Wrap className="flex flex-col gap-16 pt-30 bg-white">
<div className="flex flex-col">
<ProductTabs
tabs={tabs}
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">
<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">
<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">
需求5个以上,可以联系客服设置优惠价
</div>
{currentBrand?.versions && currentBrand.versions.length > 0 && (
<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">
{currentBrand.versions.map(v => (
<button
key={v.id}
onClick={() => setSelectedVersionId(v.id)}
className={cn(
"w-full py-2 rounded-full text-sm font-medium transition-all cursor-pointer text-center",
effectiveVersionId === v.id
? "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">
{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">
<PurchaseInfo />
</div>
)}
</div>
</div>
</div>
</Wrap>
)
}