diff --git a/src/api/http.ts b/src/api/http.ts index 480af03..12e7e75 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -1,6 +1,8 @@ import type { ApiResponse } from "@/lib/api" import type { CreateOrderResult, + DxblData, + DxbtData, DxwxlData, GameCity, GameItem, @@ -31,28 +33,68 @@ const DEFAULT_TIMEOUT = 30_000 type RawHttpResponse = Record -// ==================== 统一 PHP 请求底层 ==================== // 所有 /http/* 接口都带 cookie 走同一出口:超时、错误归一化在此层处理, // 各接口函数只关心业务数据,不再各自实现 fetch。 // credentials 仅对需要登录态的 POST 开启;公开脚本 GET 接口(如线路表) // 不带 cookie,避免跨域时服务端 Access-Control-Allow-Origin 配置不匹配。 +type PhpFetchOptions = { + credentials?: RequestCredentials + cache?: RequestCache + encoding?: "json" | "form" +} + +// 表单编码:嵌套对象/数组展开为 data[key] 结构(PHP $_POST 规则) +function toFormParams(params: URLSearchParams, prefix: string, value: unknown) { + if (value === null || value === undefined) return + if (Array.isArray(value)) { + value.forEach((v, i) => { + toFormParams(params, `${prefix}[${i}]`, v) + }) + return + } + if (typeof value === "object") { + for (const [k, v] of Object.entries(value as Record)) { + toFormParams(params, `${prefix}[${k}]`, v) + } + return + } + params.set(prefix, String(value)) +} + export async function phpFetch( method: "GET" | "POST", path: string, body?: unknown, - options: { credentials?: RequestCredentials } = {}, + options: PhpFetchOptions = {}, ): Promise> { - const { credentials = "same-origin" } = options + const { credentials = "same-origin", cache, encoding = "json" } = options + const isForm = encoding === "form" && body !== undefined const controller = new AbortController() const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT) try { + let payload: string | undefined + if (body !== undefined) { + if (isForm) { + const params = new URLSearchParams() + for (const [k, v] of Object.entries(body as Record)) { + toFormParams(params, k, v) + } + payload = params.toString() + } else { + payload = JSON.stringify(body) + } + } const response = await fetch(`${PHP_API_BASE_URL}${path}`, { method, - headers: - body !== undefined ? { "Content-Type": "application/json" } : undefined, + headers: isForm + ? { "Content-Type": "application/x-www-form-urlencoded" } + : body !== undefined + ? { "Content-Type": "application/json" } + : undefined, credentials, - body: body !== undefined ? JSON.stringify(body) : undefined, + ...(cache ? { cache } : {}), + body: payload, signal: controller.signal, }) if (!response.ok) { @@ -87,8 +129,9 @@ export async function phpFetch( export async function phpPost( path: string, body?: unknown, + options: PhpFetchOptions = {}, ): Promise> { - return phpFetch("POST", path, body, { credentials: "include" }) + return phpFetch("POST", path, body, { credentials: "include", ...options }) } // multipart 上传(手持身份证等文件场景),不设 Content-Type,由浏览器自动补全 boundary @@ -140,7 +183,7 @@ export async function phpGet( // ==================== 响应解析工具 ==================== -// 老 PHP 接口习惯把业务数据包在 d 字段里 +// 接口习惯把业务数据包在 d 字段里 export function unwrapD( raw: RawHttpResponse | undefined, ): Record { @@ -177,7 +220,9 @@ export function parsePager(raw: unknown, itemCount: number): Pager { export async function fetchGameCities( game: GameOrderData, ): Promise> { - const res = await phpPost("/http/product/city", game) + const res = await phpPost("/http/product/city", game, { + encoding: "form", + }) if (!res.success) return res const cities = unwrapD(res.data).cities return { @@ -189,7 +234,9 @@ export async function fetchGameCities( export async function fetchGameList( game: GameOrderData, ): Promise> { - const res = await phpPost("/http/product/game", game) + const res = await phpPost("/http/product/game", game, { + encoding: "form", + }) if (!res.success) return res const games = unwrapD(res.data).games return { @@ -201,7 +248,9 @@ export async function fetchGameList( export async function fetchGameLineCount( game: GameOrderData, ): Promise> { - const res = await phpPost("/http/product/linecount", game) + const res = await phpPost("/http/product/linecount", game, { + encoding: "form", + }) if (!res.success) return res const count = unwrapD(res.data).count return { success: true, data: toNumber(count) } @@ -216,37 +265,83 @@ export async function calcGamePrice( export async function calcUnlimitedPrice( data: DxwxlData, ): Promise> { - const res = await phpPost("/http/product/calc_price", { - order_type: 2, - money: 1, - pay_type: 1, - data, - }) + const res = await phpPost( + "/http/product/calc_price", + { + order_type: 2, + money: 1, + pay_type: 1, + data, + }, + { encoding: "form" }, + ) if (!res.success) return res const raw = res.data ?? {} - const priceValue = - typeof raw.price === "number" ? raw.price : unwrapD(raw).price + const d = unwrapD(raw) + return { success: true, data: toNumber(raw.price ?? d.price) } +} + +// calc_price 原始返回:每 ipAmount 个 IP 价格为 price +export type CalcPriceResult = { + ipAmount: number + price: number +} + +async function calcPriceWithIp( + orderType: number, + data: unknown, +): Promise> { + const res = await phpPost( + "/http/product/calc_price", + { + order_type: orderType, + money: 1, + pay_type: 1, + data, + }, + { encoding: "form" }, + ) + if (!res.success) return res + const raw = res.data ?? {} + const d = unwrapD(raw) + const rawIp = raw.ipAmount ?? d.ipAmount + // ipAmount 为空时按 1 处理(每 1 个 IP 价格为 price) return { success: true, - data: typeof priceValue === "number" ? priceValue : 0, + data: { + ipAmount: rawIp === null || rawIp === undefined ? 1 : toNumber(rawIp), + price: toNumber(raw.price ?? d.price), + }, } } +// 短效包天:返回每 IP 每天单价(总价 = periodAmount * price * ipAmount) +export async function calcDxbtPrice( + data: DxbtData, +): Promise> { + return calcPriceWithIp(3, data) +} + +// 短效包量:返回每 1 元可购 IP 数量(总 IP = money * ipAmount) +export async function calcDxblPrice( + data: DxblData, +): Promise> { + return calcPriceWithIp(4, data) +} + export async function calcOrderPrice( orderInfo: PackageOrderInfo, ): Promise> { const res = await phpPost( "/http/product/calc_price", orderInfo, + { encoding: "form" }, ) if (!res.success) return res const raw = res.data ?? {} - const priceValue = - typeof raw.price === "number" ? raw.price : unwrapD(raw).price - return { - success: true, - data: typeof priceValue === "number" ? priceValue : 0, - } + const d = unwrapD(raw) + // price 可能为字符串(如 "9.9000"),统一 toNumber 解析 + return { success: true, data: toNumber(raw.price ?? d.price) } } // ==================== 订单 ==================== @@ -390,7 +485,6 @@ export async function fetchCxOrders(): Promise> { export async function fetchLangIpList( query: Record, ): Promise> { - const res = await phpPost("/http/user/lang_ip_list", { cookie: document.cookie, ...query, @@ -407,7 +501,6 @@ export async function fetchLangIpList( export async function fetchSwitchList( query: Record, ): Promise> { - const res = await phpPost("/http/user/cx_switch", { cookie: document.cookie, ...query, diff --git a/src/api/shuidi.ts b/src/api/shuidi.ts index 79285db..401ab58 100644 --- a/src/api/shuidi.ts +++ b/src/api/shuidi.ts @@ -1,10 +1,12 @@ import type { ApiResponse } from "@/lib/api" import type { ShuidiGame, + ShuidiOrderInfo, + ShuidiOrderResult, ShuidiPackageList, ShuidiRegion, } from "@/lib/models/shuidi" -import { phpFetch, phpGet, phpPost } from "./http" +import { phpFetch, phpPost } from "./http" // ==================== 水滴静态/固态(jinyou) ==================== // - 游戏列表:POST /jinyou/api/gameList(不带 cookie) @@ -63,8 +65,12 @@ export async function fetchShuidiRegions(params: { is_unshar: String(params.is_unshar), game_id: String(params.game_id), }) - const res = await phpGet>( + // 余量数据时效性强,禁止浏览器缓存 + const res = await phpFetch>( + "GET", `/jinyou/api/regionSurplusOuts?${query.toString()}`, + undefined, + { cache: "no-store" }, ) if (!res.success) return res const region = res.data?.data?.region @@ -82,3 +88,26 @@ export async function fetchShuidiRegions(params: { }, } } + +// ==================== 下单 ==================== +// 与 juipnet Index.cshtml pay() 一致:body 为 { cookie, order_info } +// code 1-开通成功 2-返回支付宝支付主体 3-返回微信支付主体 -1-错误 + +export async function createShuidiOrder( + orderInfo: ShuidiOrderInfo, +): Promise> { + const res = await phpPost>( + "/jinyou/jinyoujt/create_order", + { cookie: document.cookie, order_info: orderInfo }, + ) + if (!res.success) return res + const raw = res.data ?? {} + return { + success: true, + data: { + code: Number(raw.code) || 0, + msg: typeof raw.msg === "string" ? raw.msg : undefined, + data: typeof raw.data === "string" ? raw.data : undefined, + }, + } +} diff --git a/src/home/product/buyStatic.tsx b/src/home/product/buyStatic.tsx index 84e383e..d867b20 100644 --- a/src/home/product/buyStatic.tsx +++ b/src/home/product/buyStatic.tsx @@ -1,15 +1,25 @@ import { ChevronDownIcon } from "lucide-react" import { Popover } from "radix-ui" -import { useCallback, useEffect, useMemo, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useLocation, useNavigate } from "react-router-dom" import { toast } from "sonner" +import { fetchApiIndex } from "@/api/dashboard" import { + createShuidiOrder, fetchShuidiGameList, fetchShuidiPackages, fetchShuidiRegions, } from "@/api/shuidi" +import { QrCodeImage } from "@/components/qrCodeImage" import RealNameCheck from "@/components/realNameCheck" import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" @@ -17,6 +27,7 @@ import Wrap from "@/components/wrap" import type { ShuidiCity, ShuidiGame, + ShuidiOrderInfo, ShuidiPackage, ShuidiPackageList, ShuidiRegion, @@ -305,7 +316,14 @@ export default function BuyStaticPage() { const [payType, setPayType] = useState("100") const [submitting, setSubmitting] = useState(false) - // 初始加载:游戏列表 + 静态套餐(type=1),并生成随机账号密码 + // 微信支付二维码弹窗 + const [wechatOpen, setWechatOpen] = useState(false) + const [wechatPayUrl, setWechatPayUrl] = useState("") + + // 账户余额(.NET /user/ApiIndex -> UserModel.RestAmount) + const [restAmount, setRestAmount] = useState(0) + + // 初始加载:游戏列表 + 静态套餐(type=1)+ 账户余额,并生成随机账号密码 useEffect(() => { setAccount(randomChars(2) + randomDigits(4)) setPwd(randomDigits(3)) @@ -318,16 +336,27 @@ export default function BuyStaticPage() { 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) + } + }) }, []) - // 线路列表:跟随独享类型/项目变化重新拉取(与 juipnet select_game 逻辑一致) + // 线路列表:跟随独享类型/项目变化重新拉取 + // 序列号防止快速切换时旧响应覆盖新数据 + const regionSeqRef = useRef(0) const loadRegions = useCallback(async () => { + const seq = ++regionSeqRef.current setRegionLoading(true) const res = await fetchShuidiRegions({ product_id: PRODUCT_ID, is_unshar: type === "project" ? 0 : 1, game_id: type === "project" ? gameId : "", }) + if (seq !== regionSeqRef.current) return if (res.success) { setRegion(res.data) const codes = Object.keys(res.data.province_surplus_outs) @@ -346,7 +375,7 @@ export default function BuyStaticPage() { loadRegions() }, [loadRegions]) - // 切换项目独享/专线独享:重置套餐与分配选择(与 juipnet game()/line() 逻辑一致) + // 切换项目独享/专线独享:重置套餐与分配选择 const handleTypeChange = (t: "project" | "line") => { if (t === type) return setType(t) @@ -363,6 +392,24 @@ export default function BuyStaticPage() { [region], ) + // 可用节点总数:优先接口返回的 surplus_out_total,缺失时按城市余量求和兜底 + const surplusOutTotal = useMemo(() => { + if (!region) return 0 + if ( + typeof region.surplus_out_total === "number" && + region.surplus_out_total >= 0 + ) { + return region.surplus_out_total + } + return provinceCodes.reduce((sum, code) => { + const cities = region.province_surplus_outs[code]?.citys ?? [] + return ( + sum + + cities.reduce((s, c) => s + (Number.isFinite(c.total) ? c.total : 0), 0) + ) + }, 0) + }, [region, provinceCodes]) + // 省市导航行:按拼音首字母 + 名称排序,附带字母索引 const provinceRows = useMemo(() => { if (!region) return [] @@ -407,8 +454,8 @@ export default function BuyStaticPage() { () => keyword ? provinceRows.filter(r => - r.province.en_name.toLowerCase().includes(keyword), - ) + r.province.en_name.toLowerCase().includes(keyword), + ) : provinceRows, [provinceRows, keyword], ) @@ -511,9 +558,9 @@ export default function BuyStaticPage() { setBuyNum(Math.max(Math.floor(value), sum)) } - // 校验与 juipnet pay_page 一致;下单接口暂未提供,校验通过后提示联系客服 + // 校验与 juipnet pay_page 一致;下单与 juipnet pay() 一致 const handlePay = async () => { - if ((region?.surplus_out_total ?? 0) < buyNum) { + if (surplusOutTotal < buyNum) { toast.error("节点数量不足!") return } @@ -533,9 +580,60 @@ export default function BuyStaticPage() { toast.error("密码为1-10位字母或数字!") return } + + // product_info:省-市-rid-100 -> 数量(字符串),未选择时为空数组 + const productInfo: Record = {} + for (const [key, count] of Object.entries(cityCounts)) { + productInfo[key] = String(count) + } + + // 免费(价格0)时固定余额支付,与 juipnet pay_page 一致 + const orderInfo: ShuidiOrderInfo = { + pay_type: totalPrice <= 0 ? 10 : Number(payType), + type: 0, + package_id: packageId, + game_id: type === "project" ? gameId : "", + buy_num: buyNum, + price: totalPrice, + account: account.trim(), + password: pwd.trim(), + product_info: Object.keys(productInfo).length > 0 ? productInfo : [], + } + setSubmitting(true) try { - toast.info("该产品暂未开放线上购买,请联系客服购买") + const result = await createShuidiOrder(orderInfo) + if (!result.success) { + toast.error(result.message) + return + } + const data = result.data + if (data.code === 1) { + toast.success(data.msg || "账号开通成功!") + navigate("/admin/channels") + } else if (data.code === 2 && data.data) { + // 支付宝:data 为支付宝支付主体(HTML 表单),挂载后自动提交 + const container = document.createElement("div") + container.innerHTML = data.data + const form = container.querySelector("form") + if (form) { + form.style.display = "none" + document.body.appendChild(form) + form.submit() + } else if (/^https?:\/\//i.test(data.data)) { + window.open(data.data, "_blank", "noopener,noreferrer") + } else { + toast.error("支付宝支付发起失败,请重试") + } + } else if (data.code === 3 && data.data) { + // 微信:data 为支付二维码内容 + setWechatPayUrl(data.data) + setWechatOpen(true) + } else { + toast.error(data.msg || "下单失败,请重试") + } + } catch { + toast.error("网络错误,请重试") } finally { setSubmitting(false) } @@ -637,7 +735,7 @@ export default function BuyStaticPage() {

可用节点总数: - {region?.surplus_out_total ?? 0} + {surplusOutTotal}

@@ -899,7 +997,9 @@ export default function BuyStaticPage() {
当前账户余额 - ¥28.00 + + ¥{restAmount.toFixed(2)} +
- +
+ + {/* 微信扫码支付弹窗(与 juipnet staticBackdrop 一致) */} + + + + 微信扫码支付 + + 请使用微信扫描下方二维码完成支付 + + +
+ {wechatPayUrl && ( +
+ +
+ )} +

+ 应付金额: + + ¥{totalPrice.toFixed(2)} + +

+ +
+
+
) } diff --git a/src/home/product/http.tsx b/src/home/product/http.tsx index 5de5ae9..8820c55 100644 --- a/src/home/product/http.tsx +++ b/src/home/product/http.tsx @@ -1,7 +1,10 @@ import { useEffect, useRef, useState } from "react" import { useNavigate } from "react-router-dom" import { Toaster, toast } from "sonner" +import { fetchApiIndex } from "@/api/dashboard" import { + calcDxblPrice, + calcDxbtPrice, calcGamePrice, calcUnlimitedPrice, createHttpOrder, @@ -69,6 +72,13 @@ const PERIOD_OPTIONS = [ { value: "90", label: "按季" }, ] +const PERIOD_UNIT: Record = { + "1": "天", + "7": "周", + "30": "月", + "90": "季", +} + // 短效无限量:周期含按小时(1060),默认按小时 const UNLIMITED_PERIOD_OPTIONS = [ { value: "1060", label: "按小时", unit: "小时" }, @@ -139,14 +149,29 @@ export default function HttpPage() { const [volumeAmount, setVolumeAmount] = useState(1) // 短效无限量:周期类型/购买时长/白名单数量 + 服务端单价 - const [unlimitedPeriod, setUnlimitedPeriod] = useState( - UNLIMITED_PERIOD_OPTIONS[0].value, - ) + // 默认"按天":当前后端不支持按小时档位(返回默认桩价格),按天/周/月/季均为有效档位 + const [unlimitedPeriod, setUnlimitedPeriod] = useState("1") const [unlimitedPeriodCount, setUnlimitedPeriodCount] = useState(1) const [unlimitedUnitPrice, setUnlimitedUnitPrice] = useState(0) const [unlimitedPriceLoading, setUnlimitedPriceLoading] = useState(false) const unlimitedPriceSeqRef = useRef(0) + // 短效包天:服务端每IP每天单价 + const [dxbtUnitPrice, setDxbtUnitPrice] = useState(0) + const [dxbtPriceLoading, setDxbtPriceLoading] = useState(false) + const dxbtPriceSeqRef = useRef(0) + + // 短效包量:服务端每1元可购IP数量 + const [dxblIpsPerYuan, setDxblIpsPerYuan] = useState(0) + const [dxblPriceLoading, setDxblPriceLoading] = useState(false) + const dxblPriceSeqRef = useRef(0) + + // 短效包天/包量 H币支付弹窗 + const [dxPayOpen, setDxPayOpen] = useState(false) + const [dxPayKind, setDxPayKind] = useState<"daily" | "volume">("daily") + const [dxPaying, setDxPaying] = useState(false) + const [dxBalance, setDxBalance] = useState(0) + // 长效游戏 const [game, _setGame] = useState(DEFAULT_GAME) const gameRef = useRef(game) @@ -158,6 +183,9 @@ export default function HttpPage() { const [gamePriceLoading, setGamePriceLoading] = useState(false) const gameSeqRef = useRef(0) + // IP数量滑杆:拖动即时更新数值,停止 300ms 后才触发算价(防抖) + const ipSliderTimerRef = useRef(null) + // 支付弹窗状态 const [payDialogOpen, setPayDialogOpen] = useState(false) const [payAmount, setPayAmount] = useState(0) @@ -307,6 +335,33 @@ export default function HttpPage() { applyGame({ ...gameRef.current, gameId }) } + // IP数量滑杆变化:即时更新显示,防抖后重新算价 + const handleIpSliderChange = (value: number) => { + setGame({ ...gameRef.current, ipAmount: value }) + if (ipSliderTimerRef.current !== null) { + window.clearTimeout(ipSliderTimerRef.current) + } + ipSliderTimerRef.current = window.setTimeout(() => { + ipSliderTimerRef.current = null + const seq = gameSeqRef.current + 1 + gameSeqRef.current = seq + loadGamePrice(gameRef.current, seq) + }, 300) + } + + // H币余额:优先走 .NET /user/ApiIndex 的 ju_money(登录会话内稳定可用), + // 失败时回退 php-api 的 get_balance(依赖 php-api 域名下的 cookie) + const loadHCoinBalance = async (): Promise => { + const apiRes = await fetchApiIndex() + if (apiRes.success && apiRes.data) { + const m = apiRes.data.UserModel?.ju_money + if (typeof m === "number" && Number.isFinite(m)) return m + } + const phpRes = await fetchHttpBalance() + if (phpRes.success) return phpRes.data + return null + } + const openGamePay = async () => { if (!ensureLogin()) return if (lineCountRef.current <= 0) { @@ -317,12 +372,12 @@ export default function HttpPage() { toast.error("价格获取失败,请稍后重试") return } - const res = await fetchHttpBalance() - if (!res.success) { - toast.error(res.message) + const hCoin = await loadHCoinBalance() + if (hCoin === null) { + toast.error("余额获取失败,请稍后重试") return } - setBalance(res.data) + setBalance(hCoin) setGamePayOpen(true) } @@ -366,19 +421,19 @@ export default function HttpPage() { setGamePaying(false) } - // 短效无限量:打开 H币 支付弹窗(与 juipnet 一致) + // 短效无限量:打开 H币 支付弹窗 const openUnlimitedPay = async () => { if (!ensureLogin()) return if (unlimitedTotal <= 0) { toast.error("价格获取失败,请稍后重试") return } - const res = await fetchHttpBalance() - if (!res.success) { - toast.error(res.message) + const hCoin = await loadHCoinBalance() + if (hCoin === null) { + toast.error("余额获取失败,请稍后重试") return } - setBalance(res.data) + setBalance(hCoin) setUnlimitedPayOpen(true) } @@ -427,6 +482,85 @@ export default function HttpPage() { setUnlimitedPaying(false) } + // 打开短效包天/包量 H币支付弹窗 + const openDxPay = async (kind: "daily" | "volume") => { + if (!ensureLogin()) return + if (kind === "daily") { + if (effectiveDailyIpAmount < 500) { + toast.error("ip数量最低500个") + return + } + if (dxbtUnitPrice <= 0) { + toast.error("价格计算中,请稍后重试") + return + } + } else if (effectiveVolumeAmount <= 0) { + toast.error("请输入有效的充值金额") + return + } + setDxPayKind(kind) + setDxPayOpen(true) + const hCoin = await loadHCoinBalance() + if (hCoin !== null) setDxBalance(hCoin) + } + + // 短效包天/包量下单(H币支付,与 payUnlimitedOrder 一致) + const confirmDxPay = async () => { + setDxPaying(true) + const orderInfo = + dxPayKind === "daily" + ? { + order_type: 3, + money: dxbtTotal, + pay_type: 1, + data: { + durationType: Number(duration), + ipAmount: effectiveDailyIpAmount, + periodType: Number(period), + periodAmount: periodCount, + }, + } + : { + order_type: 4, + money: effectiveVolumeAmount, + pay_type: 1, + data: { + durationType: Number(duration), + ipAmount: 500, + }, + } + const res = await createHttpOrder(orderInfo) + if (!res.success) { + toast.error(res.message) + setDxPaying(false) + return + } + const result = res.data + if (result.code === 1) { + setDxPayOpen(false) + toast.success(result.msg || "支付成功") + } else if (result.code === 2) { + if (result.data && alipayBoxRef.current) { + alipayBoxRef.current.innerHTML = result.data + alipayBoxRef.current.querySelector("form")?.submit() + setDxPayOpen(false) + } else { + toast.error(result.msg || "支付宝支付数据异常") + } + } else if (result.code === 3) { + if (result.data) { + setQrContent(result.data) + setQrCodeOpen(true) + setDxPayOpen(false) + } else { + toast.error(result.msg || "微信支付数据异常") + } + } else { + toast.error(result.msg || "支付失败") + } + setDxPaying(false) + } + const handleTabClick = (id: string) => { setActiveTab(id) if (id === "game") { @@ -439,7 +573,7 @@ export default function HttpPage() { } } - // 短效无限量:向服务端查询单价并计算总价(与 juipnet 一致) + // 短效无限量:向服务端查询单价并计算总价 useEffect(() => { if (activeTab !== "unlimited") return const seq = ++unlimitedPriceSeqRef.current @@ -470,6 +604,66 @@ export default function HttpPage() { ).toFixed(2), ) + // 短效包天有效IP数 / 短效包量有效金额(非法输入回退默认值) + const parsedCustomIp = Number(customIpAmount) + const effectiveDailyIpAmount = + customIpAmount !== "" && + Number.isFinite(parsedCustomIp) && + parsedCustomIp > 0 + ? parsedCustomIp + : ipAmount + const parsedCustomAmount = Number(customAmount) + const effectiveVolumeAmount = + customAmount !== "" && + Number.isFinite(parsedCustomAmount) && + parsedCustomAmount > 0 + ? parsedCustomAmount + : volumeAmount + + // 短效包天:向服务端查询每IP每天单价 + useEffect(() => { + if (activeTab !== "daily") return + const seq = ++dxbtPriceSeqRef.current + setDxbtPriceLoading(true) + calcDxbtPrice({ + durationType: Number(duration), + ipAmount: effectiveDailyIpAmount, + periodType: Number(period), + periodAmount: periodCount, + }).then(res => { + if (seq !== dxbtPriceSeqRef.current) return + setDxbtPriceLoading(false) + if (!res.success) { + toast.error(res.message) + return + } + setDxbtUnitPrice(res.data.price) + }) + }, [activeTab, duration, effectiveDailyIpAmount, period, periodCount]) + + // 短效包量:向服务端查询每1元可购IP数量 + useEffect(() => { + if (activeTab !== "volume") return + const seq = ++dxblPriceSeqRef.current + setDxblPriceLoading(true) + calcDxblPrice({ durationType: Number(duration), ipAmount: 500 }).then( + res => { + if (seq !== dxblPriceSeqRef.current) return + setDxblPriceLoading(false) + if (!res.success) { + toast.error(res.message) + return + } + setDxblIpsPerYuan(res.data.ipAmount) + }, + ) + }, [activeTab, duration]) + + // 短效包天总价:periodAmount * price * ipAmount(文档公式) + const dxbtTotal = Number( + (periodCount * dxbtUnitPrice * effectiveDailyIpAmount).toFixed(2), + ) + // 计算价格 const calculatePrice = () => { // 根据不同的 tab 计算价格 @@ -482,10 +676,10 @@ export default function HttpPage() { price = unlimitedTotal break case "daily": - price = ipAmount * periodCount * 0.01 + price = dxbtTotal break case "volume": - price = volumeAmount || Number(customAmount) || 0 + price = effectiveVolumeAmount break default: price = 0 @@ -739,8 +933,11 @@ export default function HttpPage() { {IP_AMOUNT_OPTIONS.map(opt => ( setIpAmount(opt.value)} + active={customIpAmount === "" && ipAmount === opt.value} + onClick={() => { + setIpAmount(opt.value) + setCustomIpAmount("") + }} className="justify-center" > {opt.label} @@ -752,13 +949,21 @@ export default function HttpPage() { 每日最多使用 setCustomIpAmount(e.target.value)} placeholder="IP数" + min={500} className="w-28 h-7 inline-block text-center" /> 个IP + + 最低 500 个IP + @@ -793,12 +998,16 @@ export default function HttpPage() { - + openDxPay("daily")} + /> )} @@ -823,8 +1032,11 @@ export default function HttpPage() { {VOLUME_AMOUNTS.map(amount => ( setVolumeAmount(amount)} + active={customAmount === "" && volumeAmount === amount} + onClick={() => { + setVolumeAmount(amount) + setCustomAmount("") + }} className="justify-center" > {amount}元 @@ -836,12 +1048,16 @@ export default function HttpPage() { 自定义 setCustomAmount(e.target.value)} placeholder="金额" + min={1} className="w-28 h-7 inline-block text-center" /> - {/* 这个金额需要根据选项自动更新和也支持手动显示 */} 元 @@ -865,17 +1081,21 @@ export default function HttpPage() {
可使用{" "} - {Math.floor( - (volumeAmount || customAmount - ? Number(customAmount || volumeAmount) - : 0) * 100, - )} + {dxblPriceLoading + ? "计算中..." + : Math.floor( + effectiveVolumeAmount * dxblIpsPerYuan, + ).toLocaleString()} {" "} 个IP,有效期1年,提取免费,使用才扣费。
- + openDxPay("volume")} + /> )} @@ -1032,17 +1252,38 @@ export default function HttpPage() { -
- refreshGamePrice({ ...game, ipAmount: v })} - min={1} - max={lineCount} - /> - - 最大可选数量:{lineCount} - +
+
+ + refreshGamePrice({ ...game, ipAmount: v }) + } + min={1} + max={lineCount} + /> + + 最大可选数量:{lineCount} + +
+ {/* 进度条滑杆:拖动即时更新,停顿后算价 */} +
+ + handleIpSliderChange(Number(e.target.value)) + } + className="flex-1 accent-blue-500 cursor-pointer" + /> +
+

+ 当前选择:{game.ipAmount} 个IP +

@@ -1193,6 +1434,16 @@ export default function HttpPage() { onPay={() => payUnlimitedOrder()} /> + {/* 短效包天/包量支付弹窗(H币支付) */} + confirmDxPay()} + /> + {/* 支付宝表单渲染容器 */}
@@ -1494,7 +1745,7 @@ function PriceBar({ 价格: - {loading ? "计算中..." : `¥${price.toFixed(2)}`} + {loading ? "" : `¥${price.toFixed(2)}`}