diff --git a/src/api/http.ts b/src/api/http.ts new file mode 100644 index 0000000..f45279f --- /dev/null +++ b/src/api/http.ts @@ -0,0 +1,176 @@ +import type { ApiResponse } from "@/lib/api" + +export const PHP_API_BASE_URL = + import.meta.env.VITE_PHP_API_BASE_URL ?? "https://php-api.juip.com" + +const DEFAULT_TIMEOUT = 30_000 + +export type GameOrderData = { + isAbroad: number + isRelayed: number + shareType: number + gameId: number + lineType: number + bandwidth: number + cityCode: number + isp: number + ipAmount: number + periodType: number + periodAmount: number +} + +export type GameCity = { + code: number + name: string +} + +export type GameItem = { + id: number + name: string +} + +export type HttpOrderInfo = { + order_type: number + money: number + pay_type: number + data: GameOrderData +} + +export type CreateOrderResult = { + code: number + msg?: string + data?: string +} + +type RawHttpResponse = Record + +async function postJson( + path: string, + body: unknown, +): Promise> { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT) + try { + const response = await fetch(`${PHP_API_BASE_URL}${path}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(body), + signal: controller.signal, + }) + if (!response.ok) { + return { + success: false, + status: response.status, + message: "请求失败", + } + } + const data = (await response.json()) as T + return { success: true, data } + } catch (e) { + if (controller.signal.aborted) { + return { + success: false, + status: 408, + message: "请求超时,请稍后重试", + } + } + return { + success: false, + status: 500, + message: (e as Error).message || "网络错误", + } + } finally { + clearTimeout(timer) + } +} + +function unwrapD(raw: RawHttpResponse | undefined): Record { + const d = raw?.d + if (d && typeof d === "object" && !Array.isArray(d)) { + return d as Record + } + return {} +} + +export async function fetchGameCities( + game: GameOrderData, +): Promise> { + const res = await postJson("/http/product/city", game) + if (!res.success) return res + const cities = unwrapD(res.data).cities + return { + success: true, + data: Array.isArray(cities) ? (cities as GameCity[]) : [], + } +} + +export async function fetchGameList( + game: GameOrderData, +): Promise> { + const res = await postJson("/http/product/game", game) + if (!res.success) return res + const games = unwrapD(res.data).games + return { + success: true, + data: Array.isArray(games) ? (games as GameItem[]) : [], + } +} + +export async function fetchGameLineCount( + game: GameOrderData, +): Promise> { + const res = await postJson("/http/product/linecount", game) + if (!res.success) return res + const count = unwrapD(res.data).count + return { success: true, data: Number(count) || 0 } +} + +export async function calcGamePrice( + orderInfo: HttpOrderInfo, +): Promise> { + const res = await postJson( + "/http/product/calc_price", + orderInfo, + ) + 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, + } +} + +export async function createHttpOrder( + orderInfo: HttpOrderInfo, +): Promise> { + const res = await postJson("/http/order/create_order", { + cookie: document.cookie, + order_info: orderInfo, + }) + if (!res.success) return res + return { + success: true, + data: { + code: Number(res.data?.code) || 0, + msg: typeof res.data?.msg === "string" ? res.data.msg : undefined, + data: typeof res.data?.data === "string" ? res.data.data : undefined, + }, + } +} + +export async function fetchHttpBalance(): Promise> { + const res = await postJson( + "/http/user/get_balance", + { + cookie: document.cookie, + }, + ) + if (!res.success) return res + if (typeof res.data === "number") return { success: true, data: res.data } + const raw = res.data ?? {} + const balance = raw.d ?? raw.data ?? raw.balance + return { success: true, data: Number(balance) || 0 } +} diff --git a/src/home/product/buy.tsx b/src/home/product/buy.tsx index 463da99..b05e120 100644 --- a/src/home/product/buy.tsx +++ b/src/home/product/buy.tsx @@ -1,7 +1,7 @@ -import { useState } from "react" +import { useEffect, useState } from "react" import { useLocation, useNavigate } from "react-router-dom" import { toast } from "sonner" -import { createOrder } from "@/api/order" +import { type CreateOrderRequest, createOrder } from "@/api/order" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" @@ -115,9 +115,17 @@ function ConnectCountSelector({ /> - - {value} - + { + const parsed = Number.parseInt(e.target.value, 10) + if (!Number.isNaN(parsed)) onChange(Math.max(1, parsed)) + }} + className="w-20 h-8 text-center text-lg font-semibold text-gray-700" + /> + + + + + {/* 支付宝表单渲染容器 */} +
) @@ -799,16 +1225,28 @@ function Stepper({ ) } -function PriceBar({ price, onPay }: { price: number; onPay: () => void }) { +function PriceBar({ + price, + onPay, + loading = false, +}: { + price: number + onPay: () => void + loading?: boolean +}) { return (
- 价格:¥{price.toFixed(2)} + 价格: + + {loading ? "计算中..." : `¥${price.toFixed(2)}`} +