diff --git a/public/img/product_compare.png b/public/img/product_compare.png new file mode 100644 index 0000000..79ce24d Binary files /dev/null and b/public/img/product_compare.png differ diff --git a/src/admin/clients.tsx b/src/admin/clients.tsx index 0d54c36..944474f 100644 --- a/src/admin/clients.tsx +++ b/src/admin/clients.tsx @@ -59,7 +59,7 @@ export function Content(props: { children: ReactNode }) { "absolute inset-0 overflow-hidden", "mt-16", "md:ml-16", - "lg:data-[expand=true]:ml-50", + "md:data-[expand=true]:ml-50", )} > {props.children} @@ -119,8 +119,25 @@ export function Navbar() { `data-[expand=true]:basis-52 data-[expand=false]:basis-16`, )} > - - logo + + logo + + 聚IP +
{ console.log("保存的信息:", data) - // 这里调 API 保存数据 } + + // ========== 渲染 ========== return ( - {/* banner */} -
-
+
+ {/* 第一个卡片 */} +
@@ -117,7 +102,7 @@ export default function DashboardPage() {

- {balanceData.balance} + {balanceData.balance.toFixed(2)}

-
+ {/* 第二个卡片 */} +

可用H币(个)

- {balanceData.hCoin} + {balanceData.hCoin.toFixed(2)}

- {/* 最近订单 */} -
-

最近订单

-
- row.original.price.toFixed(2), - }, - { accessorKey: "connections", header: "总连接数" }, - { accessorKey: "account", header: "账号" }, - { - accessorKey: "orderAmount", - header: "订单金额(元)", - cell: ({ row }) => row.original.orderAmount.toFixed(2), - }, - { - accessorKey: "paidAmount", - header: "实付金额(元)", - cell: ({ row }) => row.original.paidAmount.toFixed(2), - }, - ]} - /> -
-
- - {/* 消费信息 */} -
-
-

消费信息

-
-
-
- 今日消费 -
-

今日消费

-

- {consumptionData.today} -

-
-
-
- 当月消费 -
-

当月消费

-

- {consumptionData.month} -

-
-
-
- 今日退款 -
-

今日退款

-

- {consumptionData.todayRefund} -

-
-
-
- 本年消费 -
-

本年消费

-

- {consumptionData.year} -

-
-
-
- 今日充值 -
-

今日充值

-

- {consumptionData.todayRecharge} -

-
-
-
- 当月充值 -
-

当月充值

-

- {consumptionData.monthRecharge} -

-
-
-
- 当月退款 -
-

当月退款

-

- {consumptionData.monthRefund} -

-
-
-
-
- - {/* 账号信息 */}

账号信息

@@ -374,45 +226,198 @@ export default function DashboardPage() {
- {/* IP账号 */} + {/* ====== 第二行:最近订单(占左侧3列) ====== */} +
+

最近订单

+
+ row.original.price.toFixed(2), + }, + { accessorKey: "connections", header: "总连接数" }, + { accessorKey: "account", header: "账号" }, + { + accessorKey: "orderAmount", + header: "订单金额(元)", + cell: ({ row }) => row.original.orderAmount.toFixed(2), + }, + { + accessorKey: "paidAmount", + header: "实付金额(元)", + cell: ({ row }) => row.original.paidAmount.toFixed(2), + }, + ]} + /> +
+
+ + {/* ====== 第三、四行:消费信息 + IP账号 ====== */} + {/* 消费信息 - 占左侧3列,跨2行 */} +
+
+

消费信息

+
+
+ {/* 今日消费 */} +
+ 今日消费 +
+

今日消费

+

+ {consumptionData.today} +

+
+
+ + {/* 当月消费 */} +
+ 当月消费 +
+

当月消费

+

+ {consumptionData.month} +

+
+
+ + {/* 今日退款 */} +
+ 今日退款 +
+

今日退款

+

+ {consumptionData.todayRefund} +

+
+
+ + {/* 本年消费 */} +
+ 本年消费 +
+

本年消费

+

+ {consumptionData.year} +

+
+
+ + {/* 今日充值 */} +
+ 今日充值 +
+

今日充值

+

+ {consumptionData.todayRecharge} +

+
+
+ + {/* 当月充值 */} +
+ 当月充值 +
+

当月充值

+

+ {consumptionData.monthRecharge} +

+
+
+ + {/* 当月退款 */} +
+ 当月退款 +
+

当月退款

+

+ {consumptionData.monthRefund} +

+
+
+
+
+ + {/* ====== IP账号(占右侧1列,跨2行) ====== */}

IP账号

+ {/* 使用中 */}
使用中图标

{ipAccountData.using}

-

使用中

+

使用中

+ + {/* 总个数 */}
总个数图标

{ipAccountData.total}

-

总个数

+

总个数

+ + {/* 已过期 */}
已过期图标

{ipAccountData.expired}

-

已过期

+

已过期

+
- {!isTest && ( void +}) { + return ( +
+ + +
+ ) +} + +// ============ 运营商选择 ============ +function OperatorSelect({ + value, + onChange, +}: { + value: string + onChange: (val: string) => void +}) { + const operators = ["电信", "联通", "移动"] + + return ( +
+ {operators.map((op) => ( + + ))} +
+ ) +} + +function RegionGrid({ + selected, + onSelect, +}: { + selected: string + onSelect: (region: string) => void +}) { + // 模拟地区数据,实际应从接口获取 + const regions = [ + { id: "A", name: "郑州电信", stock: 20 }, + { id: "B", name: "郑州电信", stock: 20 }, + { id: "C", name: "天津", stock: 20 }, + { id: "D", name: "上海", stock: 20 }, + { id: "E", name: "安徽", stock: 20 }, + { id: "G", name: "山东", stock: 20 }, + { id: "H", name: "湖南", stock: 20 }, + { id: "I", name: "陕西", stock: 20 }, + ] + + // 按首字母分组 + const groupedRegions: Record = {} + const letterMap: Record = { + A: "A", + B: "B", + C: "C", + D: "D", + E: "E", + G: "G", + H: "H", + I: "I", + } + regions.forEach((r) => { + const key = letterMap[r.id] || "其他" + if (!groupedRegions[key]) groupedRegions[key] = [] + groupedRegions[key].push(r) + }) + + return ( +
+ {Object.entries(groupedRegions).map(([letter, items]) => ( +
+ + {letter} + +
+ {items.map((region) => ( + + ))} +
+
+ ))} +
+ ) +} + +// ============ 带宽选择 ============ +function BandwidthSelect({ + value, + onChange, +}: { + value: string + onChange: (val: string) => void +}) { + const options = ["2M", "4M", "10M", "20M"] + + return ( +
+ {options.map((bw) => ( + + ))} +
+ ) +} + +// ============ 时长选择 ============ +function DurationSelect({ + value, + onChange, +}: { + value: string + onChange: (val: string) => void +}) { + const options = [ + { label: "测试", value: "test" }, + { label: "天卡", value: "day" }, + { label: "月卡", value: "month" }, + ] + + return ( +
+ {options.map((opt) => ( + + ))} +
+ ) +} + +// ============ 项目选择下拉 ============ +function ProjectSelect({ + value, + onChange, +}: { + value: string + onChange: (val: string) => void +}) { + const projects = ["请选择", "项目A", "项目B", "项目C"] + + return ( + + ) +} + +// ============ 主页面 ============ +export default function BuyStaticPage() { + const location = useLocation() + const navigate = useNavigate() + const state = (location.state ?? {}) as BuyStaticState + + const poolTitle = state.pool?.title || "多拨池" + + const [type, setType] = useState<"project" | "line">("project") + const [operator, setOperator] = useState("电信") + const [project, setProject] = useState("请选择") + const [region, setRegion] = useState("") + const [account, setAccount] = useState("") + const [pwd, setPwd] = useState("") + const [bandwidth, setBandwidth] = useState("2M") + const [duration, setDuration] = useState("test") + const [payType, setPayType] = useState("100") + + useEffect(() => { + setAccount(randomChars(2) + randomDigits(4)) + setPwd(randomDigits(3)) + }, []) + + const handleBack = () => navigate(-1) + + // 根据类型计算价格 + const getPrice = () => { + const bwMap: Record = { "2M": 10, "4M": 20, "10M": 40, "20M": 80 } + const durationMap: Record = { test: 0.1, day: 1, month: 10 } + const basePrice = bwMap[bandwidth] || 10 + const durationPrice = durationMap[duration] || 0.1 + return (basePrice * durationPrice).toFixed(2) + } + const [regionSearch, setRegionSearch] = useState("") + return ( + +
+ {/* 标题 */} +
+

+ {poolTitle} +

+
+ + {/* Tab 切换 */} +
+ +
+ + {/* 独享说明 */} + {type === "project" ? ( +
+ + + 您所选项目分配的IP保证在同一项目内不会出现。 + +
+ ) : ( +
+

专线独享:个人独享整个线路,真实独享,永远不重复。

+

线路切换单日限制为20次

+

+ 如果没有自己想用的地区线路或线路余量不足,请联系客服上架 +

+
+ )} + +
+ {/* 运营商 */} +
+ + +
+ + {/* 项目选择 */} +
+ +
+ + + 选择自己所做项目可避免与同项目的其他客户分配至同一IP,如没有可不选 + +
+
+ + {/* 地区选择 */} +
+ +
+
+ setRegionSearch(e.target.value)} + /> + +
+

+ 如果没有自己想用的地区线路或线路余量不足,请联系客服上架哦 +

+ +
+
+ + {/* 线路品质 */} +
+ + 优质尊享 +
+ + {/* 购买数量 */} +
+ + 1 +
+ + {/* IP产品账号 */} +
+ + setAccount(e.target.value)} + placeholder="4-10位字母或数字" + className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1" + /> + + 4至10位字母或数字或组合 + +
+ + {/* IP产品密码 */} +
+ + setPwd(e.target.value)} + placeholder="1-10位字母或数字" + className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1" + /> + + 1至10位字母或数字或组合 + +
+ + {/* 带宽选择 */} +
+ + +
+ + {/* 时长选择 */} +
+ + +
+ + {/* 应付金额 */} +
+ + ¥{getPrice()} +
+ + {/* 余额 */} +
+ +
+ + 当前账户余额 + ¥28.00 + + +
+
+ + {/* 支付方式 */} +
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/home/product/index.tsx b/src/home/product/index.tsx index aae834f..5d886b2 100644 --- a/src/home/product/index.tsx +++ b/src/home/product/index.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react" -import { useSearchParams } from "react-router-dom" +import { useNavigate, useSearchParams } from "react-router-dom" import { fetchProducts, fetchStaticProducts, @@ -14,6 +14,7 @@ import ProductGrid from "./productGrid" import ProductSidebar from "./productSidebar" import ProductTabs from "./productTabs" import PurchaseInfo from "./purchaseInfo" +import ShuidiPools, { type PoolModule } from "./shuidiPools" const TAB_ALIAS: Record = { short: "dynamic", @@ -41,6 +42,7 @@ export default function ProductPage() { const [activeBrandId, setActiveBrandId] = useState("") const [selectedVersionId, setSelectedVersionId] = useState("") const initializedRef = useRef(false) + const navigate = useNavigate() useEffect(() => { let cancelled = false @@ -128,6 +130,12 @@ export default function ProductPage() { })() const isPurchaseInfo = currentBrand?.id === "info" + const isShuidi = currentBrand?.id === "水滴独享IP" + + const handlePoolBuy = (pool: PoolModule) => { + if (pool.id !== "dial") return + navigate("/product/buy-static", { state: { pool } }) + } if (loading) { return ( @@ -201,35 +209,44 @@ export default function ProductPage() {
需求5个以上,可以联系客服设置优惠价
- {currentBrand?.versions && currentBrand.versions.length > 0 && ( -
-
- {currentBrand.versions.map(v => ( - - ))} -
-
+ {isShuidi ? ( +
+ +
+ ) : ( + <> + {currentBrand?.versions && + currentBrand.versions.length > 0 && ( +
+
+ {currentBrand.versions.map(v => ( + + ))} +
+
+ )} +
+ {displayProducts.length > 0 ? ( + + ) : ( +
+ 该版本暂无产品数据 +
+ )} +
+ )} -
- {displayProducts.length > 0 ? ( - - ) : ( -
- 该版本暂无产品数据 -
- )} -
) : (
diff --git a/src/home/product/purchaseInfo.tsx b/src/home/product/purchaseInfo.tsx index 83b4488..dbe5315 100644 --- a/src/home/product/purchaseInfo.tsx +++ b/src/home/product/purchaseInfo.tsx @@ -1,4 +1,7 @@ +import { useState } from "react" + export default function PurchaseInfo() { + const [isImageOpen, setIsImageOpen] = useState(false) return (
@@ -36,8 +39,52 @@ export default function PurchaseInfo() {

产品对比:

- {/* 这里可以放入产品对比表格 */} +
setIsImageOpen(true)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + setIsImageOpen(true) + } + }} + > + 产品对比图,点击可放大查看 +
+ {isImageOpen && ( + +
+ + )}
diff --git a/src/home/product/shuidiPools.tsx b/src/home/product/shuidiPools.tsx new file mode 100644 index 0000000..2ebfc39 --- /dev/null +++ b/src/home/product/shuidiPools.tsx @@ -0,0 +1,82 @@ +import { Button } from "@/components/ui/button" + +export type PoolModule = { + id: string + title: string + features: string[] + canBuy?: boolean +} + +const POOLS: PoolModule[] = [ + { + id: "house", + title: "纯住宅池", + features: [ + "分散式真实住宅IP", + "带宽充足,IP段优质分布广", + "适合对IP要求高的业务", + "IP3-7天周期性变化", + "线路切换每天20次", + ], + }, + { + id: "dial", + title: "多拨池", + features: [ + "集中拨号住宅,带宽较高", + "IP段较多、稳定性较高", + "IP3-7天周期性变化", + "线路切换每天20次", + ], + canBuy: true, + }, + { + id: "fixed", + title: "固定池", + features: [ + "天翼云节点", + "IP长久有效IP不变化,稳定性高", + "线路切换单日限制为20次", + ], + }, +] + +type ShuidiPoolsProps = { + onBuy: (pool: PoolModule) => void +} + +export default function ShuidiPools({ onBuy }: ShuidiPoolsProps) { + return ( +
+ {POOLS.map(pool => ( +
+
+ {pool.title} +
+ +
+ {pool.features.map((feature, index) => ( +
+ {feature} +
+ ))} +
+ +
+ +
+
+ ))} +
+ ) +} diff --git a/src/lib/models/product.ts b/src/lib/models/product.ts index 7bad630..88ceef0 100644 --- a/src/lib/models/product.ts +++ b/src/lib/models/product.ts @@ -136,11 +136,10 @@ export function transformToTabs(sources: TabSource[]): Tab[] { tabs.push({ id: source.id, label: source.label, brands }) } - const firstTab = tabs[0] - if (firstTab) { - firstTab.brands.push({ + for (const tab of tabs) { + tab.brands.push({ id: "info", - name: "购买须知", + name: "购买说明", products: [], }) } diff --git a/src/routers/index.tsx b/src/routers/index.tsx index 30aefa9..069062c 100644 --- a/src/routers/index.tsx +++ b/src/routers/index.tsx @@ -21,6 +21,7 @@ import LayoutPage from "@/home/layout" import LoginPage from "@/home/login" import ProductPage from "@/home/product" import BuyPage from "@/home/product/buy" +import BuyStaticPage from "@/home/product/buyStatic" import HttpPage from "@/home/product/http" import RouterosPage from "@/home/product/routeros" import SoftDownloadPage from "@/home/softDownload" @@ -32,6 +33,7 @@ export const router = createBrowserRouter([ { index: true, element: }, { path: "product", element: }, { path: "product/buy", element: }, + { path: "product/buy-static", element: }, { path: "product/http", element: }, { path: "product/routeros", element: }, { path: "ipline", element: },