修复测试1小时应付金额计算 & 水滴独享IP立即购买页面

This commit is contained in:
Eamon
2026-10-19 16:00:13 +08:00
parent 65e1eca221
commit c295f15724
3 changed files with 169 additions and 66 deletions

View File

@@ -55,8 +55,7 @@ function randomDigits(length: number): string {
return result
}
// 静态产品 product_id 固定为 1;线路品质固定为优质尊享(等级 100)
const PRODUCT_ID = 1
// 线路品质固定为优质尊享(等级 100)
const LINE_LEVEL = "100"
function ispLabel(isp: number): string {
@@ -139,7 +138,13 @@ function provinceLetter(name: string): string {
}
type BuyStaticState = {
pool?: { id?: string; title?: string; packageId?: number }
pool?: {
id?: string
title?: string
packageId?: number
// static-拨号线路(静态):product_id=1、type=0;solid-水滴专线(固态):product_id=2、type=1
productType?: "static" | "solid"
}
}
// ============ 独享类型切换 Tab ============
@@ -283,7 +288,13 @@ export default function BuyStaticPage() {
const navigate = useNavigate()
const state = (location.state ?? {}) as BuyStaticState
const poolTitle = state.pool?.title || "多拨池"
const poolTitle = state.pool?.title || "拨号线路(静态)"
// 产品类型:static-拨号线路(静态) product_id=1/type=0;solid-水滴专线(固态) product_id=2/type=1
const isSolid = state.pool?.productType === "solid"
const productId = isSolid ? 2 : 1
const packageType: 1 | 2 = isSolid ? 2 : 1
const orderType: 0 | 1 = isSolid ? 1 : 0
// 三个后端接口数据:游戏列表 / 套餐列表 / 线路列表
const [games, setGames] = useState<ShuidiGame[]>([])
@@ -320,10 +331,12 @@ export default function BuyStaticPage() {
const [wechatOpen, setWechatOpen] = useState(false)
const [wechatPayUrl, setWechatPayUrl] = useState("")
// 账户余额(.NET /user/ApiIndex -> UserModel.RestAmount)
// 账户余额与剩余免费测试次数(.NET /user/ApiIndex -> UserModel)
const [restAmount, setRestAmount] = useState(0)
const [restTest, setRestTest] = useState<number | null>(null)
// 初始加载:游戏列表 + 静态套餐(type=1)+ 账户余额,并生成随机账号密码
// 初始加载:游戏列表 + 套餐(静态type=1/固态type=2)+ 账户余额/测试次数,
// 并生成随机账号密码
useEffect(() => {
setAccount(randomChars(2) + randomDigits(4))
setPwd(randomDigits(3))
@@ -332,18 +345,26 @@ export default function BuyStaticPage() {
if (res.success) setGames(res.data)
else toast.error(res.message)
})
fetchShuidiPackages(1).then(res => {
fetchShuidiPackages(packageType).then(res => {
if (res.success) setPackages(res.data)
else toast.error(res.message)
})
fetchApiIndex().then(res => {
if (!res.success || !res.data) return
const amount = res.data.UserModel?.RestAmount
if (typeof amount === "number" && Number.isFinite(amount)) {
setRestAmount(amount)
const user = res.data.UserModel
if (user) {
const amount = user.RestAmount
if (typeof amount === "number" && Number.isFinite(amount)) {
setRestAmount(amount)
}
const limit = user.TestCountLimit
const used = user.UseTestCount
if (typeof limit === "number" && typeof used === "number") {
setRestTest(Math.max(0, limit - used))
}
}
})
}, [])
}, [packageType])
// 线路列表:跟随独享类型/项目变化重新拉取
// 序列号防止快速切换时旧响应覆盖新数据
@@ -352,7 +373,7 @@ export default function BuyStaticPage() {
const seq = ++regionSeqRef.current
setRegionLoading(true)
const res = await fetchShuidiRegions({
product_id: PRODUCT_ID,
product_id: productId,
is_unshar: type === "project" ? 0 : 1,
game_id: type === "project" ? gameId : "",
})
@@ -369,7 +390,7 @@ export default function BuyStaticPage() {
toast.error(res.message)
}
setRegionLoading(false)
}, [type, gameId])
}, [type, gameId, productId])
useEffect(() => {
loadRegions()
@@ -538,7 +559,20 @@ export default function BuyStaticPage() {
setBandwidth(key)
setPackageId("")
const group = type === "project" ? packages?.game : packages?.line
setPackageInfoList(group?.[key] ?? [])
const list = group?.[key] ?? []
// 免费测试次数用完时,测试套餐按 1 元计费
if (
key === "test" &&
restTest !== null &&
restTest < 1 &&
list.length > 0
) {
setPackageInfoList(
list.map((item, i) => (i === 0 ? { ...item, Price: "1" } : item)),
)
} else {
setPackageInfoList(list)
}
}
const handleDurationClick = (item: ShuidiPackage) => {
@@ -558,7 +592,7 @@ export default function BuyStaticPage() {
setBuyNum(Math.max(Math.floor(value), sum))
}
// 校验与 juipnet pay_page 一致;下单与 juipnet pay() 一致
// 校验与 pay_page 一致;下单与 pay() 一致
const handlePay = async () => {
if (surplusOutTotal < buyNum) {
toast.error("节点数量不足!")
@@ -587,10 +621,10 @@ export default function BuyStaticPage() {
productInfo[key] = String(count)
}
// 免费(价格0)时固定余额支付,与 juipnet pay_page 一致
// 免费(价格0)时固定余额支付
const orderInfo: ShuidiOrderInfo = {
pay_type: totalPrice <= 0 ? 10 : Number(payType),
type: 0,
type: orderType,
package_id: packageId,
game_id: type === "project" ? gameId : "",
buy_num: buyNum,
@@ -679,6 +713,26 @@ export default function BuyStaticPage() {
</div>
)}
{/* 静态/固态说明 */}
<div className="mb-4 rounded-lg bg-blue-50 px-4 py-3 text-xs text-gray-600 leading-relaxed">
{isSolid ? (
<p>
水滴专线(固态):IP永久固定不变、更稳定,上下行同速,项目/直播首选。
<span className="text-red-500">
线路切换单日限制为20次,如余量不足请联系客服上架。
</span>
</p>
) : (
<p>
拨号线路(静态):运营商专线宽带、自营机房,高带宽出口,IP 3-7
天周期性变化。
<span className="text-red-500">
线路切换单日限制为20次,如余量不足请联系客服上架。
</span>
</p>
)}
</div>
<div className="space-y-5 mb-20">
{/* 项目选择(可搜索下拉) */}
<div className="flex flex-col gap-2 md:flex-row md:items-start md:gap-6">
@@ -933,7 +987,7 @@ export default function BuyStaticPage() {
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
带宽选择:
</Label>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 items-center">
{bandwidthKeys.map(key => (
<button
key={key}
@@ -948,6 +1002,13 @@ export default function BuyStaticPage() {
{key === "test" ? "测试" : key}
</button>
))}
{bandwidth === "test" && restTest !== null && (
<span className="text-xs text-amber-600">
{restTest > 0
? `剩余免费测试次数:${restTest}次`
: "免费测试次数已用完,测试套餐按1元计费"}
</span>
)}
</div>
</div>
@@ -1073,7 +1134,7 @@ export default function BuyStaticPage() {
</div>
</div>
{/* 微信扫码支付弹窗(与 juipnet staticBackdrop 一致) */}
{/* 微信扫码支付弹窗 */}
<Dialog open={wechatOpen} onOpenChange={setWechatOpen}>
<DialogContent className="sm:max-w-sm">
<DialogHeader>

View File

@@ -132,7 +132,7 @@ export default function ProductPage() {
const isShuidi = currentBrand?.id === "水滴独享IP"
const handlePoolBuy = (pool: PoolModule) => {
if (pool.id !== "dial") return
if (!pool.canBuy) return
navigate("/product/buy-static", { state: { pool } })
}

View File

@@ -1,43 +1,52 @@
import { Button } from "@/components/ui/button"
export type PoolModule = {
id: string
title: string
features: string[]
mainTitle: string
leftTitle: string
rightTitle: string
features: {
title: string
desc: string
}[]
canBuy?: boolean
productType?: "static" | "solid"
}
const POOLS: PoolModule[] = [
{
id: "house",
title: "纯住宅池",
features: [
"分散式真实住宅IP",
"带宽充足,IP段优质分布广",
"适合对IP要求高的业务",
"IP3-7天周期性变化",
"线路切换每天20次",
],
},
{
id: "dial",
title: "多拨池",
mainTitle: "拨号线路(静态)",
leftTitle: "静态专线介绍",
rightTitle: "传统socks5",
features: [
"集中拨号住宅,带宽较高",
"IP段较多、稳定性较高",
"IP3-7天周期性变化",
"线路切换每天20次",
{ title: "运营商专线宽带,自营机房", desc: "天翼云,各种云线路" },
{ title: "大出口流量,直播/项目首选", desc: "低带宽出口,共享严重,线路拥挤" },
{ title: "全新vpn隧道,安全可靠", desc: "传统协议,波动大,延迟大" },
{ title: "全局代理不易发现", desc: "系统局部代理,易发现,易封号" },
{ title: "IP周期性变化", desc: "-" },
{ title: "精选超20000条不同线路", desc: "容量一般只有2-3000线路" },
{ title: "Ros/爱快/路由器/手机/电脑", desc: "电脑/手机" },
{ title: "华为级维护专员7*24小时服务", desc: "售后困难,处理时间漫长" },
],
canBuy: true,
productType: "static",
},
{
id: "fixed",
title: "固定池",
mainTitle: "水滴专线(固态)",
leftTitle: "固态专线介绍",
rightTitle: "传统socks5",
features: [
"天翼云节点",
"IP长久有效IP不变化,稳定性高",
"线路切换单日限制为20次",
{ title: "运营商专线宽带,更稳定", desc: "天翼云,各种云线路" },
{ title: "全新vpn隧道,安全可靠", desc: "传统socks5协议,波动大,延迟大" },
{ title: "全局代理,为项目而生", desc: "系统局部代理,易发现,易封号" },
{ title: "IP永久固定不变", desc: "-" },
{ title: "精选超10000条不同线路", desc: "容量一般只有2-3000线路" },
{ title: "Ros/爱快/路由器/手机/电脑", desc: "电脑/手机" },
{ title: "华为级维护专员7*24小时服务", desc: "售后困难,处理时间漫长" },
],
canBuy: true,
productType: "solid",
},
]
@@ -47,39 +56,72 @@ type ShuidiPoolsProps = {
export default function ShuidiPools({ onBuy }: ShuidiPoolsProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-6 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-2 mt-6 gap-8">
{POOLS.map(pool => (
<div
key={pool.id}
className="rounded-xl shadow-[4px_4px_20px_4px] shadow-blue-50 bg-white p-6 flex flex-col min-h-60"
className="rounded-xl shadow-xl shadow-slate-200/50 bg-white flex flex-col overflow-hidden border border-slate-200"
>
<div className="text-lg font-bold text-center text-gray-700">
{pool.title}
<div className="bg-[#e6f0fa] py-4">
<div className="text-lg sm:text-xl font-bold text-center text-gray-800">
{pool.mainTitle}
</div>
</div>
<div className="text-sm text-gray-500 space-y-1.5 mt-5">
{pool.features.map((feature, index) => (
<div
key={index}
className="flex items-center justify-center gap-2 text-amber-500"
>
{feature}
<div className="flex grow">
<div className="w-1/2 bg-white py-6 pl-4 pr-1 flex flex-col">
<div className="text-center font-bold text-emerald-600 text-base sm:text-lg h-8 flex items-center justify-center">
{pool.leftTitle}
</div>
))}
<div className="flex flex-col grow">
{pool.features.map((feature, index) => (
<div
key={index}
className="grid grid-cols-[1fr_auto] gap-2 items-center py-2"
>
<div className="text-sm text-gray-800 font-medium text-center">
{feature.title}
</div>
<div className="text-base font-bold leading-none text-[#10b981]">
✓
</div>
</div>
))}
</div>
</div>
<div className="w-1/2 bg-[#e5e7eb] py-6 pr-4 pl-1 flex flex-col">
<div className="text-center font-bold text-emerald-600 text-base sm:text-lg h-8 flex items-center justify-center">
{pool.rightTitle}
</div>
<div className="flex flex-col grow">
{pool.features.map((feature, index) => (
<div
key={index}
className="grid grid-cols-[auto_1fr] gap-2 items-center py-2"
>
<div className="text-base font-bold leading-none text-[#ef4444]">
✗
</div>
<div className="text-sm text-gray-800 font-medium text-center ">
{feature.desc}
</div>
</div>
))}
</div>
</div>
</div>
<div className="pt-6 mt-auto">
<Button
variant="outline"
className="w-full text-blue-500 border-blue-400 hover:bg-blue-50"
onClick={() => pool.canBuy && onBuy(pool)}
disabled={!pool.canBuy}
>
立即购买
</Button>
</div>
<button
className="w-full bg-[#119a49] hover:bg-[#0d7a39] text-white font-bold text-lg py-4 transition-colors"
onClick={() => pool.canBuy && onBuy(pool)}
disabled={!pool.canBuy}
>
立即购买
</button>
</div>
))}
</div>
)
}
}