修复天卡的连接数 &完善http长效游戏页面
This commit is contained in:
176
src/api/http.ts
Normal file
176
src/api/http.ts
Normal file
@@ -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<string, unknown>
|
||||||
|
|
||||||
|
async function postJson<T = RawHttpResponse>(
|
||||||
|
path: string,
|
||||||
|
body: unknown,
|
||||||
|
): Promise<ApiResponse<T>> {
|
||||||
|
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<string, unknown> {
|
||||||
|
const d = raw?.d
|
||||||
|
if (d && typeof d === "object" && !Array.isArray(d)) {
|
||||||
|
return d as Record<string, unknown>
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchGameCities(
|
||||||
|
game: GameOrderData,
|
||||||
|
): Promise<ApiResponse<GameCity[]>> {
|
||||||
|
const res = await postJson<RawHttpResponse>("/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<ApiResponse<GameItem[]>> {
|
||||||
|
const res = await postJson<RawHttpResponse>("/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<ApiResponse<number>> {
|
||||||
|
const res = await postJson<RawHttpResponse>("/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<ApiResponse<number>> {
|
||||||
|
const res = await postJson<RawHttpResponse>(
|
||||||
|
"/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<ApiResponse<CreateOrderResult>> {
|
||||||
|
const res = await postJson<RawHttpResponse>("/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<ApiResponse<number>> {
|
||||||
|
const res = await postJson<RawHttpResponse | number>(
|
||||||
|
"/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 }
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useLocation, useNavigate } from "react-router-dom"
|
import { useLocation, useNavigate } from "react-router-dom"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { createOrder } from "@/api/order"
|
import { type CreateOrderRequest, createOrder } from "@/api/order"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@@ -115,9 +115,17 @@ function ConnectCountSelector({
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
<span className="w-8 text-center text-lg font-semibold text-gray-700">
|
<Input
|
||||||
{value}
|
type="number"
|
||||||
</span>
|
min={1}
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={e => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -214,7 +222,7 @@ export default function BuyPage() {
|
|||||||
const [payType, setPayType] = useState("100")
|
const [payType, setPayType] = useState("100")
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
|
||||||
useState(() => {
|
useEffect(() => {
|
||||||
if (!product) {
|
if (!product) {
|
||||||
navigate("/product", { replace: true })
|
navigate("/product", { replace: true })
|
||||||
return
|
return
|
||||||
@@ -225,19 +233,39 @@ export default function BuyPage() {
|
|||||||
setPwd(pw)
|
setPwd(pw)
|
||||||
setBatchAccount(randomChars(3))
|
setBatchAccount(randomChars(3))
|
||||||
setBatchPwd(pw)
|
setBatchPwd(pw)
|
||||||
})
|
}, [navigate, product])
|
||||||
|
|
||||||
if (!product) return null
|
if (!product) return null
|
||||||
|
|
||||||
const packageId = Number(product.id)
|
const packageId = Number(product.id)
|
||||||
const isTest = product.isTest ?? false
|
const isTest = product.isTest ?? false
|
||||||
const isDayCard = product.card === "天"
|
const isDayCard = product.card === "天"
|
||||||
const isNormalPackage = !isTest && !isDayCard
|
// 天卡首单(0.1元)连接数固定为1;大于0.1元的天卡连接数可增减、手输入
|
||||||
|
const isFirstOrderDayCard = isDayCard && product.price <= 0.1
|
||||||
|
|
||||||
// 天卡固定连接数为1
|
const displayConnectCount = isFirstOrderDayCard ? 1 : connectCount
|
||||||
const displayConnectCount = isDayCard ? 1 : connectCount
|
|
||||||
const singleTotal = product.price * displayConnectCount
|
const clampMinPrice = (total: number, count: number): number => {
|
||||||
const batchTotal = product.price * batchConnectCount * batchCount
|
if (
|
||||||
|
!isFirstOrderDayCard &&
|
||||||
|
!isTest &&
|
||||||
|
product.minPrice !== undefined &&
|
||||||
|
product.minPrice > 0
|
||||||
|
) {
|
||||||
|
const minCost = product.minPrice * count
|
||||||
|
if (total < minCost) return minCost
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
const singleTotal = clampMinPrice(
|
||||||
|
product.price * displayConnectCount,
|
||||||
|
displayConnectCount,
|
||||||
|
)
|
||||||
|
const batchTotal = clampMinPrice(
|
||||||
|
product.price * batchConnectCount * batchCount,
|
||||||
|
batchConnectCount * batchCount,
|
||||||
|
)
|
||||||
const total = mode === "single" ? singleTotal : batchTotal
|
const total = mode === "single" ? singleTotal : batchTotal
|
||||||
|
|
||||||
// 批量预览
|
// 批量预览
|
||||||
@@ -277,7 +305,7 @@ export default function BuyPage() {
|
|||||||
const useBalance = oPayType === 1 ? 1 : 0
|
const useBalance = oPayType === 1 ? 1 : 0
|
||||||
const payChannel = oPayType === 70 ? 30 : 50
|
const payChannel = oPayType === 70 ? 30 : 50
|
||||||
|
|
||||||
const params = {
|
const params: CreateOrderRequest = {
|
||||||
PackageId: packageId,
|
PackageId: packageId,
|
||||||
OrderType: mode === "single" ? 1 : 2,
|
OrderType: mode === "single" ? 1 : 2,
|
||||||
Account: mode === "single" ? account.trim() : batchAccount.trim(),
|
Account: mode === "single" ? account.trim() : batchAccount.trim(),
|
||||||
@@ -295,7 +323,7 @@ export default function BuyPage() {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await createOrder(params as any)
|
const result = await createOrder(params)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success("购买成功!")
|
toast.success("购买成功!")
|
||||||
@@ -311,8 +339,8 @@ export default function BuyPage() {
|
|||||||
|
|
||||||
const handleBack = () => navigate(-1)
|
const handleBack = () => navigate(-1)
|
||||||
|
|
||||||
// 判断是否显示Tab(普通产品才显示)
|
// 天卡首单(0.1元)不显示批量注册;天卡>0.1元时显示(juipnet:应付款!=0.1时显示批量tab)
|
||||||
const showTabs = isNormalPackage
|
const showTabs = !isTest && !isFirstOrderDayCard
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-16">
|
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-16">
|
||||||
@@ -326,6 +354,9 @@ export default function BuyPage() {
|
|||||||
<p className="text-sm text-amber-600">
|
<p className="text-sm text-amber-600">
|
||||||
请务必选好所需物品,换货会产生费用
|
请务必选好所需物品,换货会产生费用
|
||||||
</p>
|
</p>
|
||||||
|
{isDayCard && (
|
||||||
|
<p className="text-sm text-amber-600">天卡不支持退款,请谨慎购买</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tab 切换 */}
|
{/* Tab 切换 */}
|
||||||
@@ -371,14 +402,14 @@ export default function BuyPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 连接设备数:测试卡不显示,天卡显示但禁用 */}
|
{/* 连接设备数:测试卡不显示;天卡首单(0.1元)显示但禁用 */}
|
||||||
{!isTest && (
|
{!isTest && (
|
||||||
<ConnectCountSelector
|
<ConnectCountSelector
|
||||||
value={displayConnectCount}
|
value={displayConnectCount}
|
||||||
onChange={setConnectCount}
|
onChange={setConnectCount}
|
||||||
label="连接设备数"
|
label="连接设备数"
|
||||||
hint="一个账号可同时在线设备数"
|
hint="一个账号可同时在线设备数"
|
||||||
disabled={isDayCard}
|
disabled={isFirstOrderDayCard}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import { useState } from "react"
|
import { useRef, useState } from "react"
|
||||||
import { Toaster, toast } from "sonner"
|
import { Toaster, toast } from "sonner"
|
||||||
|
import {
|
||||||
|
calcGamePrice,
|
||||||
|
createHttpOrder,
|
||||||
|
fetchGameCities,
|
||||||
|
fetchGameLineCount,
|
||||||
|
fetchGameList,
|
||||||
|
fetchHttpBalance,
|
||||||
|
type GameCity,
|
||||||
|
type GameItem,
|
||||||
|
type GameOrderData,
|
||||||
|
} from "@/api/http"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
@@ -63,12 +74,33 @@ const IP_AMOUNT_OPTIONS = [
|
|||||||
const VOLUME_AMOUNTS = [1, 10, 20, 50, 80, 100, 200, 500, 1000]
|
const VOLUME_AMOUNTS = [1, 10, 20, 50, 80, 100, 200, 500, 1000]
|
||||||
|
|
||||||
const BANDWIDTH_OPTIONS = [
|
const BANDWIDTH_OPTIONS = [
|
||||||
{ value: "1", label: "1M" },
|
{ value: 1, label: "1M" },
|
||||||
{ value: "2", label: "2M" },
|
{ value: 2, label: "2M" },
|
||||||
{ value: "5", label: "5M" },
|
{ value: 5, label: "5M" },
|
||||||
{ value: "10", label: "10M" },
|
{ value: 10, label: "10M" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const GAME_PERIOD_OPTIONS = [
|
||||||
|
{ value: 1, label: "按天", unit: "天" },
|
||||||
|
{ value: 7, label: "按周", unit: "周" },
|
||||||
|
{ value: 30, label: "按月", unit: "月" },
|
||||||
|
{ value: 90, label: "按季", unit: "季" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const DEFAULT_GAME: GameOrderData = {
|
||||||
|
isAbroad: 0,
|
||||||
|
isRelayed: 0,
|
||||||
|
shareType: 1,
|
||||||
|
gameId: 0,
|
||||||
|
lineType: 1,
|
||||||
|
bandwidth: 1,
|
||||||
|
cityCode: 0,
|
||||||
|
isp: 0,
|
||||||
|
ipAmount: 1,
|
||||||
|
periodType: 1,
|
||||||
|
periodAmount: 1,
|
||||||
|
}
|
||||||
|
|
||||||
export default function HttpPage() {
|
export default function HttpPage() {
|
||||||
const [activeTab, setActiveTab] = useState("prepaid")
|
const [activeTab, setActiveTab] = useState("prepaid")
|
||||||
const [customAmount, setCustomAmount] = useState("")
|
const [customAmount, setCustomAmount] = useState("")
|
||||||
@@ -80,15 +112,220 @@ export default function HttpPage() {
|
|||||||
const [whitelistCount, setWhitelistCount] = useState(1)
|
const [whitelistCount, setWhitelistCount] = useState(1)
|
||||||
const [ipAmount, setIpAmount] = useState(5000)
|
const [ipAmount, setIpAmount] = useState(5000)
|
||||||
const [volumeAmount, setVolumeAmount] = useState(1)
|
const [volumeAmount, setVolumeAmount] = useState(1)
|
||||||
const [bandwidth, setBandwidth] = useState(BANDWIDTH_OPTIONS[0].value)
|
|
||||||
|
// 长效游戏
|
||||||
|
const [game, _setGame] = useState<GameOrderData>(DEFAULT_GAME)
|
||||||
|
const gameRef = useRef(game)
|
||||||
|
const [cities, setCities] = useState<GameCity[]>([])
|
||||||
|
const [games, setGames] = useState<GameItem[]>([])
|
||||||
|
const [lineCount, _setLineCount] = useState(100)
|
||||||
|
const lineCountRef = useRef(100)
|
||||||
|
const [gamePrice, setGamePrice] = useState(0)
|
||||||
|
const [gamePriceLoading, setGamePriceLoading] = useState(false)
|
||||||
|
const gameSeqRef = useRef(0)
|
||||||
|
|
||||||
// 支付弹窗状态
|
// 支付弹窗状态
|
||||||
const [payDialogOpen, setPayDialogOpen] = useState(false)
|
const [payDialogOpen, setPayDialogOpen] = useState(false)
|
||||||
const [payAmount, setPayAmount] = useState(0)
|
const [payAmount, setPayAmount] = useState(0)
|
||||||
const [payMethod, setPayMethod] = useState("2")
|
const [payMethod, setPayMethod] = useState("2")
|
||||||
const [isRecharge] = useState(true)
|
const isRecharge = true
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
// 长效游戏支付(H币)
|
||||||
|
const [gamePayOpen, setGamePayOpen] = useState(false)
|
||||||
|
const [balance, setBalance] = useState(0)
|
||||||
|
const [gamePaying, setGamePaying] = useState(false)
|
||||||
|
const alipayBoxRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const setGame = (next: GameOrderData) => {
|
||||||
|
gameRef.current = next
|
||||||
|
_setGame(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setLineCountValue = (next: number) => {
|
||||||
|
lineCountRef.current = next
|
||||||
|
_setLineCount(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrent = (seq: number) => seq === gameSeqRef.current
|
||||||
|
|
||||||
|
const loadGamePrice = async (g: GameOrderData, seq: number) => {
|
||||||
|
if (lineCountRef.current <= 0) {
|
||||||
|
toast.error("可用IP为0!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setGamePriceLoading(true)
|
||||||
|
const res = await calcGamePrice({
|
||||||
|
order_type: 5,
|
||||||
|
money: 1,
|
||||||
|
pay_type: 1,
|
||||||
|
data: g,
|
||||||
|
})
|
||||||
|
if (!isCurrent(seq)) return
|
||||||
|
setGamePriceLoading(false)
|
||||||
|
if (!res.success) {
|
||||||
|
toast.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setGamePrice(Number((g.periodAmount * g.ipAmount * res.data).toFixed(2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新游戏参数:拉取线路数量并重置IP数量为1,随后重新计算价格
|
||||||
|
const applyGame = async (
|
||||||
|
next: GameOrderData,
|
||||||
|
opts: { fetchCities?: boolean; fetchGames?: boolean } = {},
|
||||||
|
) => {
|
||||||
|
const seq = gameSeqRef.current + 1
|
||||||
|
gameSeqRef.current = seq
|
||||||
|
setGame(next)
|
||||||
|
|
||||||
|
if (opts.fetchCities) {
|
||||||
|
const cityRes = await fetchGameCities(next)
|
||||||
|
if (!isCurrent(seq)) return
|
||||||
|
if (cityRes.success) {
|
||||||
|
setCities(cityRes.data)
|
||||||
|
} else {
|
||||||
|
toast.error(cityRes.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalGame = next
|
||||||
|
if (opts.fetchGames) {
|
||||||
|
const gameRes = await fetchGameList(next)
|
||||||
|
if (!isCurrent(seq)) return
|
||||||
|
if (!gameRes.success) {
|
||||||
|
toast.error(gameRes.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setGames(gameRes.data)
|
||||||
|
finalGame = { ...next, gameId: gameRes.data[0]?.id ?? 0 }
|
||||||
|
setGame(finalGame)
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineRes = await fetchGameLineCount(finalGame)
|
||||||
|
if (!isCurrent(seq)) return
|
||||||
|
if (!lineRes.success) {
|
||||||
|
toast.error(lineRes.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setLineCountValue(lineRes.data)
|
||||||
|
finalGame = { ...finalGame, ipAmount: 1 }
|
||||||
|
setGame(finalGame)
|
||||||
|
|
||||||
|
await loadGamePrice(finalGame, seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅重新计算价格(不影响线路数量)
|
||||||
|
const refreshGamePrice = (next: GameOrderData) => {
|
||||||
|
const seq = gameSeqRef.current + 1
|
||||||
|
gameSeqRef.current = seq
|
||||||
|
setGame(next)
|
||||||
|
void loadGamePrice(next, seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAbroad = (isAbroad: number) => {
|
||||||
|
void applyGame(
|
||||||
|
{
|
||||||
|
...gameRef.current,
|
||||||
|
isAbroad,
|
||||||
|
shareType: 1,
|
||||||
|
lineType: 1,
|
||||||
|
gameId: 0,
|
||||||
|
},
|
||||||
|
{ fetchCities: true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectShareType = (shareType: number) => {
|
||||||
|
if (shareType === 1) {
|
||||||
|
void applyGame({
|
||||||
|
...gameRef.current,
|
||||||
|
shareType,
|
||||||
|
lineType: 1,
|
||||||
|
gameId: 0,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
void applyGame({ ...gameRef.current, shareType }, { fetchGames: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeRelay = (isRelayed: number) => {
|
||||||
|
void applyGame({ ...gameRef.current, isRelayed })
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeIsp = (isp: number) => {
|
||||||
|
void applyGame({ ...gameRef.current, isp })
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeCity = (cityCode: number) => {
|
||||||
|
void applyGame({ ...gameRef.current, cityCode })
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeGameId = (gameId: number) => {
|
||||||
|
void applyGame({ ...gameRef.current, gameId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const openGamePay = async () => {
|
||||||
|
if (lineCountRef.current <= 0) {
|
||||||
|
toast.error("可用IP为0!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (gamePrice <= 0) {
|
||||||
|
toast.error("价格获取失败,请稍后重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await fetchHttpBalance()
|
||||||
|
if (!res.success) {
|
||||||
|
toast.error(res.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBalance(res.data)
|
||||||
|
setGamePayOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payGameOrder = async () => {
|
||||||
|
setGamePaying(true)
|
||||||
|
const res = await createHttpOrder({
|
||||||
|
order_type: 5,
|
||||||
|
money: gamePrice,
|
||||||
|
pay_type: 1,
|
||||||
|
data: gameRef.current,
|
||||||
|
})
|
||||||
|
if (!res.success) {
|
||||||
|
toast.error(res.message)
|
||||||
|
setGamePaying(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const result = res.data
|
||||||
|
if (result.code === 1) {
|
||||||
|
setGamePayOpen(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()
|
||||||
|
setGamePayOpen(false)
|
||||||
|
} else {
|
||||||
|
toast.error(result.msg || "支付宝支付数据异常")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(result.msg || "支付失败")
|
||||||
|
}
|
||||||
|
setGamePaying(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTabClick = (id: string) => {
|
||||||
|
setActiveTab(id)
|
||||||
|
if (id === "game") {
|
||||||
|
void applyGame(
|
||||||
|
{ ...gameRef.current, periodType: 1 },
|
||||||
|
{
|
||||||
|
fetchCities: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 计算价格
|
// 计算价格
|
||||||
const calculatePrice = () => {
|
const calculatePrice = () => {
|
||||||
// 根据不同的 tab 计算价格
|
// 根据不同的 tab 计算价格
|
||||||
@@ -106,9 +343,6 @@ export default function HttpPage() {
|
|||||||
case "volume":
|
case "volume":
|
||||||
price = volumeAmount || Number(customAmount) || 0
|
price = volumeAmount || Number(customAmount) || 0
|
||||||
break
|
break
|
||||||
case "game":
|
|
||||||
price = periodCount * 5
|
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
price = 0
|
price = 0
|
||||||
}
|
}
|
||||||
@@ -160,7 +394,7 @@ export default function HttpPage() {
|
|||||||
{TABS.map(tab => (
|
{TABS.map(tab => (
|
||||||
<li key={tab.id}>
|
<li key={tab.id}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => handleTabClick(tab.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-6 py-2.5 rounded-md text-sm font-bold transition-all cursor-pointer",
|
"px-6 py-2.5 rounded-md text-sm font-bold transition-all cursor-pointer",
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
@@ -467,24 +701,162 @@ export default function HttpPage() {
|
|||||||
|
|
||||||
<OptionBox label="类型">
|
<OptionBox label="类型">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<OptionBtn active>国内游戏</OptionBtn>
|
<OptionBtn
|
||||||
<OptionBtn>国际游戏</OptionBtn>
|
active={game.isAbroad === 0}
|
||||||
|
onClick={() => selectAbroad(0)}
|
||||||
|
>
|
||||||
|
国内游戏
|
||||||
|
</OptionBtn>
|
||||||
|
<OptionBtn
|
||||||
|
active={game.isAbroad === 1}
|
||||||
|
onClick={() => selectAbroad(1)}
|
||||||
|
>
|
||||||
|
国际游戏
|
||||||
|
</OptionBtn>
|
||||||
</div>
|
</div>
|
||||||
</OptionBox>
|
</OptionBox>
|
||||||
|
|
||||||
<OptionBox label="模式">
|
<OptionBox label="模式">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<OptionBtn active>共享线路</OptionBtn>
|
<OptionBtn
|
||||||
<OptionBtn>独享游戏</OptionBtn>
|
active={game.shareType === 1}
|
||||||
|
onClick={() => selectShareType(1)}
|
||||||
|
>
|
||||||
|
共享线路
|
||||||
|
</OptionBtn>
|
||||||
|
<OptionBtn
|
||||||
|
active={game.shareType === 2}
|
||||||
|
onClick={() => selectShareType(2)}
|
||||||
|
>
|
||||||
|
独享游戏
|
||||||
|
</OptionBtn>
|
||||||
</div>
|
</div>
|
||||||
</OptionBox>
|
</OptionBox>
|
||||||
|
|
||||||
<OptionBox label="运营商选择">
|
{game.isAbroad === 1 && (
|
||||||
<div className="flex flex-wrap gap-3">
|
<OptionBox label="中继选择">
|
||||||
<OptionBtn active>普通线路</OptionBtn>
|
<div className="flex flex-wrap gap-3">
|
||||||
<OptionBtn>电信</OptionBtn>
|
<OptionBtn
|
||||||
<OptionBtn>移动</OptionBtn>
|
active={game.isRelayed === 0}
|
||||||
<OptionBtn>联通</OptionBtn>
|
onClick={() => changeRelay(0)}
|
||||||
|
>
|
||||||
|
直连
|
||||||
|
</OptionBtn>
|
||||||
|
<OptionBtn
|
||||||
|
active={game.isRelayed === 1}
|
||||||
|
onClick={() => changeRelay(1)}
|
||||||
|
>
|
||||||
|
中继
|
||||||
|
</OptionBtn>
|
||||||
|
</div>
|
||||||
|
</OptionBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{game.shareType === 2 && (
|
||||||
|
<OptionBox label="线路质量">
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<OptionBtn
|
||||||
|
active={game.lineType === 1}
|
||||||
|
onClick={() =>
|
||||||
|
refreshGamePrice({ ...game, lineType: 1 })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
普通线路
|
||||||
|
</OptionBtn>
|
||||||
|
<OptionBtn
|
||||||
|
active={game.lineType === 2}
|
||||||
|
onClick={() =>
|
||||||
|
refreshGamePrice({ ...game, lineType: 2 })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
优质线路
|
||||||
|
</OptionBtn>
|
||||||
|
</div>
|
||||||
|
</OptionBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{game.shareType === 2 && games.length > 0 && (
|
||||||
|
<OptionBox label="独享游戏">
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
|
{games.map(item => (
|
||||||
|
<OptionBtn
|
||||||
|
key={item.id}
|
||||||
|
active={game.gameId === item.id}
|
||||||
|
onClick={() => changeGameId(item.id)}
|
||||||
|
className="justify-center"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</OptionBtn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</OptionBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{game.isAbroad === 0 && (
|
||||||
|
<OptionBox label="运营商选择">
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<OptionBtn
|
||||||
|
active={game.isp === 0}
|
||||||
|
onClick={() => changeIsp(0)}
|
||||||
|
>
|
||||||
|
普通线路
|
||||||
|
</OptionBtn>
|
||||||
|
<OptionBtn
|
||||||
|
active={game.isp === 2}
|
||||||
|
onClick={() => changeIsp(2)}
|
||||||
|
>
|
||||||
|
电信
|
||||||
|
</OptionBtn>
|
||||||
|
<OptionBtn
|
||||||
|
active={game.isp === 3}
|
||||||
|
onClick={() => changeIsp(3)}
|
||||||
|
>
|
||||||
|
移动
|
||||||
|
</OptionBtn>
|
||||||
|
<OptionBtn
|
||||||
|
active={game.isp === 4}
|
||||||
|
onClick={() => changeIsp(4)}
|
||||||
|
>
|
||||||
|
联通
|
||||||
|
</OptionBtn>
|
||||||
|
</div>
|
||||||
|
</OptionBox>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<OptionBox label="地区选择">
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
|
<OptionBtn
|
||||||
|
active={game.cityCode === 0}
|
||||||
|
onClick={() => changeCity(0)}
|
||||||
|
className="justify-center"
|
||||||
|
>
|
||||||
|
随机地区
|
||||||
|
</OptionBtn>
|
||||||
|
{cities.map(city => (
|
||||||
|
<OptionBtn
|
||||||
|
key={city.code}
|
||||||
|
active={game.cityCode === city.code}
|
||||||
|
onClick={() => changeCity(city.code)}
|
||||||
|
className="justify-center"
|
||||||
|
>
|
||||||
|
{city.name}
|
||||||
|
</OptionBtn>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</OptionBox>
|
||||||
|
|
||||||
|
<OptionBox label="IP数量">
|
||||||
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
|
<Stepper
|
||||||
|
value={game.ipAmount}
|
||||||
|
unit="个"
|
||||||
|
onChange={v => refreshGamePrice({ ...game, ipAmount: v })}
|
||||||
|
min={1}
|
||||||
|
max={lineCount}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
最大可选数量:{lineCount}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</OptionBox>
|
</OptionBox>
|
||||||
|
|
||||||
@@ -493,8 +865,10 @@ export default function HttpPage() {
|
|||||||
{BANDWIDTH_OPTIONS.map(opt => (
|
{BANDWIDTH_OPTIONS.map(opt => (
|
||||||
<OptionBtn
|
<OptionBtn
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
active={bandwidth === opt.value}
|
active={game.bandwidth === opt.value}
|
||||||
onClick={() => setBandwidth(opt.value)}
|
onClick={() =>
|
||||||
|
refreshGamePrice({ ...game, bandwidth: opt.value })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</OptionBtn>
|
</OptionBtn>
|
||||||
@@ -504,11 +878,13 @@ export default function HttpPage() {
|
|||||||
|
|
||||||
<OptionBox label="套餐周期">
|
<OptionBox label="套餐周期">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{PERIOD_OPTIONS.map(opt => (
|
{GAME_PERIOD_OPTIONS.map(opt => (
|
||||||
<OptionBtn
|
<OptionBtn
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
active={period === opt.value}
|
active={game.periodType === opt.value}
|
||||||
onClick={() => setPeriod(opt.value)}
|
onClick={() =>
|
||||||
|
refreshGamePrice({ ...game, periodType: opt.value })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</OptionBtn>
|
</OptionBtn>
|
||||||
@@ -516,23 +892,26 @@ export default function HttpPage() {
|
|||||||
</div>
|
</div>
|
||||||
</OptionBox>
|
</OptionBox>
|
||||||
|
|
||||||
<OptionBox label="IP数量">
|
|
||||||
<Stepper
|
|
||||||
value={periodCount}
|
|
||||||
unit="个"
|
|
||||||
onChange={setPeriodCount}
|
|
||||||
/>
|
|
||||||
</OptionBox>
|
|
||||||
|
|
||||||
<OptionBox label="购买时长">
|
<OptionBox label="购买时长">
|
||||||
<Stepper
|
<Stepper
|
||||||
value={whitelistCount}
|
value={game.periodAmount}
|
||||||
unit="天"
|
unit={
|
||||||
onChange={setWhitelistCount}
|
GAME_PERIOD_OPTIONS.find(
|
||||||
|
opt => opt.value === game.periodType,
|
||||||
|
)?.unit ?? "天"
|
||||||
|
}
|
||||||
|
onChange={v =>
|
||||||
|
refreshGamePrice({ ...game, periodAmount: v })
|
||||||
|
}
|
||||||
|
min={1}
|
||||||
/>
|
/>
|
||||||
</OptionBox>
|
</OptionBox>
|
||||||
|
|
||||||
<PriceBar price={totalPrice} onPay={openPayDialog} />
|
<PriceBar
|
||||||
|
price={gamePrice}
|
||||||
|
loading={gamePriceLoading}
|
||||||
|
onPay={() => void openGamePay()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -581,8 +960,11 @@ export default function HttpPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="text-center py-6">
|
<div className="text-center py-6">
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
个人剩余H币:{}
|
个人剩余H币:{payAmount.toFixed(2)}
|
||||||
<a href="/" className="text-blue-500 hover:underline ml-2">
|
<a
|
||||||
|
href="/product/http"
|
||||||
|
className="text-blue-500 hover:underline ml-2"
|
||||||
|
>
|
||||||
去充值
|
去充值
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -604,6 +986,50 @@ export default function HttpPage() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 长效游戏支付弹窗(H币支付) */}
|
||||||
|
<Dialog open={gamePayOpen} onOpenChange={setGamePayOpen}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-center">
|
||||||
|
待支付:¥{gamePrice.toFixed(2)}元
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="py-4">
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
个人剩余H币:
|
||||||
|
<span className="text-orange-500 font-medium ml-1">
|
||||||
|
{balance.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="/product/http"
|
||||||
|
className="text-blue-500 hover:underline ml-2"
|
||||||
|
>
|
||||||
|
去充值
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600 mt-2">
|
||||||
|
本次支付H币:{gamePrice.toFixed(2)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => void payGameOrder()}
|
||||||
|
disabled={gamePaying}
|
||||||
|
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 py-3"
|
||||||
|
>
|
||||||
|
{gamePaying ? "支付中..." : "立即支付"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 支付宝表单渲染容器 */}
|
||||||
|
<div ref={alipayBoxRef} className="hidden" />
|
||||||
</Wrap>
|
</Wrap>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@@ -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 (
|
return (
|
||||||
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||||
<div className="p-5 flex items-center justify-between gap-4 flex-wrap">
|
<div className="p-5 flex items-center justify-between gap-4 flex-wrap">
|
||||||
<span className="text-sm font-medium text-gray-700">
|
<span className="text-sm font-medium text-gray-700">
|
||||||
价格:<span className="text-lg font-bold">¥{price.toFixed(2)}</span>
|
价格:
|
||||||
|
<span className="text-lg font-bold">
|
||||||
|
{loading ? "计算中..." : `¥${price.toFixed(2)}`}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
onClick={onPay}
|
onClick={onPay}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
disabled={loading}
|
||||||
className="rounded-md bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
className="rounded-md bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
||||||
>
|
>
|
||||||
实付 ¥{price.toFixed(2)}
|
实付 ¥{price.toFixed(2)}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type Product = {
|
|||||||
productId: number
|
productId: number
|
||||||
title: string
|
title: string
|
||||||
price: number
|
price: number
|
||||||
|
minPrice?: number
|
||||||
desc?: string
|
desc?: string
|
||||||
tag?: string
|
tag?: string
|
||||||
card?: string
|
card?: string
|
||||||
@@ -108,6 +109,7 @@ function transformToBrands(data: ProductItem[]): Brand[] {
|
|||||||
productId: item.Product.Id,
|
productId: item.Product.Id,
|
||||||
title: pkg.Name,
|
title: pkg.Name,
|
||||||
price: pkg.Price,
|
price: pkg.Price,
|
||||||
|
minPrice: pkg.MinPrice,
|
||||||
originalPrice: pkg.LinePrice,
|
originalPrice: pkg.LinePrice,
|
||||||
duration: pkg.Profile,
|
duration: pkg.Profile,
|
||||||
card: pkg.OriginName,
|
card: pkg.OriginName,
|
||||||
|
|||||||
Reference in New Issue
Block a user