修复测试1小时应付金额计算 & 水滴独享IP立即购买页面
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 } })
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user