修复天卡的连接数 &完善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 { 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({
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
<span className="w-8 text-center text-lg font-semibold text-gray-700">
|
||||
{value}
|
||||
</span>
|
||||
<Input
|
||||
type="number"
|
||||
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
|
||||
variant="outline"
|
||||
size="icon"
|
||||
@@ -214,7 +222,7 @@ export default function BuyPage() {
|
||||
const [payType, setPayType] = useState("100")
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
useState(() => {
|
||||
useEffect(() => {
|
||||
if (!product) {
|
||||
navigate("/product", { replace: true })
|
||||
return
|
||||
@@ -225,19 +233,39 @@ export default function BuyPage() {
|
||||
setPwd(pw)
|
||||
setBatchAccount(randomChars(3))
|
||||
setBatchPwd(pw)
|
||||
})
|
||||
}, [navigate, product])
|
||||
|
||||
if (!product) return null
|
||||
|
||||
const packageId = Number(product.id)
|
||||
const isTest = product.isTest ?? false
|
||||
const isDayCard = product.card === "天"
|
||||
const isNormalPackage = !isTest && !isDayCard
|
||||
// 天卡首单(0.1元)连接数固定为1;大于0.1元的天卡连接数可增减、手输入
|
||||
const isFirstOrderDayCard = isDayCard && product.price <= 0.1
|
||||
|
||||
// 天卡固定连接数为1
|
||||
const displayConnectCount = isDayCard ? 1 : connectCount
|
||||
const singleTotal = product.price * displayConnectCount
|
||||
const batchTotal = product.price * batchConnectCount * batchCount
|
||||
const displayConnectCount = isFirstOrderDayCard ? 1 : connectCount
|
||||
|
||||
const clampMinPrice = (total: number, count: number): number => {
|
||||
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
|
||||
|
||||
// 批量预览
|
||||
@@ -277,7 +305,7 @@ export default function BuyPage() {
|
||||
const useBalance = oPayType === 1 ? 1 : 0
|
||||
const payChannel = oPayType === 70 ? 30 : 50
|
||||
|
||||
const params = {
|
||||
const params: CreateOrderRequest = {
|
||||
PackageId: packageId,
|
||||
OrderType: mode === "single" ? 1 : 2,
|
||||
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) {
|
||||
toast.success("购买成功!")
|
||||
@@ -311,8 +339,8 @@ export default function BuyPage() {
|
||||
|
||||
const handleBack = () => navigate(-1)
|
||||
|
||||
// 判断是否显示Tab(普通产品才显示)
|
||||
const showTabs = isNormalPackage
|
||||
// 天卡首单(0.1元)不显示批量注册;天卡>0.1元时显示(juipnet:应付款!=0.1时显示批量tab)
|
||||
const showTabs = !isTest && !isFirstOrderDayCard
|
||||
|
||||
return (
|
||||
<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>
|
||||
{isDayCard && (
|
||||
<p className="text-sm text-amber-600">天卡不支持退款,请谨慎购买</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab 切换 */}
|
||||
@@ -371,14 +402,14 @@ export default function BuyPage() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 连接设备数:测试卡不显示,天卡显示但禁用 */}
|
||||
{/* 连接设备数:测试卡不显示;天卡首单(0.1元)显示但禁用 */}
|
||||
{!isTest && (
|
||||
<ConnectCountSelector
|
||||
value={displayConnectCount}
|
||||
onChange={setConnectCount}
|
||||
label="连接设备数"
|
||||
hint="一个账号可同时在线设备数"
|
||||
disabled={isDayCard}
|
||||
disabled={isFirstOrderDayCard}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { useState } from "react"
|
||||
import { useRef, useState } from "react"
|
||||
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 { Button } from "@/components/ui/button"
|
||||
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 BANDWIDTH_OPTIONS = [
|
||||
{ value: "1", label: "1M" },
|
||||
{ value: "2", label: "2M" },
|
||||
{ value: "5", label: "5M" },
|
||||
{ value: "10", label: "10M" },
|
||||
{ value: 1, label: "1M" },
|
||||
{ value: 2, label: "2M" },
|
||||
{ value: 5, label: "5M" },
|
||||
{ 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() {
|
||||
const [activeTab, setActiveTab] = useState("prepaid")
|
||||
const [customAmount, setCustomAmount] = useState("")
|
||||
@@ -80,15 +112,220 @@ export default function HttpPage() {
|
||||
const [whitelistCount, setWhitelistCount] = useState(1)
|
||||
const [ipAmount, setIpAmount] = useState(5000)
|
||||
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 [payAmount, setPayAmount] = useState(0)
|
||||
const [payMethod, setPayMethod] = useState("2")
|
||||
const [isRecharge] = useState(true)
|
||||
const isRecharge = true
|
||||
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 = () => {
|
||||
// 根据不同的 tab 计算价格
|
||||
@@ -106,9 +343,6 @@ export default function HttpPage() {
|
||||
case "volume":
|
||||
price = volumeAmount || Number(customAmount) || 0
|
||||
break
|
||||
case "game":
|
||||
price = periodCount * 5
|
||||
break
|
||||
default:
|
||||
price = 0
|
||||
}
|
||||
@@ -160,7 +394,7 @@ export default function HttpPage() {
|
||||
{TABS.map(tab => (
|
||||
<li key={tab.id}>
|
||||
<button
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
onClick={() => handleTabClick(tab.id)}
|
||||
className={cn(
|
||||
"px-6 py-2.5 rounded-md text-sm font-bold transition-all cursor-pointer",
|
||||
activeTab === tab.id
|
||||
@@ -467,24 +701,162 @@ export default function HttpPage() {
|
||||
|
||||
<OptionBox label="类型">
|
||||
<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>
|
||||
</OptionBox>
|
||||
|
||||
<OptionBox label="模式">
|
||||
<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>
|
||||
</OptionBox>
|
||||
|
||||
<OptionBox label="运营商选择">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<OptionBtn active>普通线路</OptionBtn>
|
||||
<OptionBtn>电信</OptionBtn>
|
||||
<OptionBtn>移动</OptionBtn>
|
||||
<OptionBtn>联通</OptionBtn>
|
||||
{game.isAbroad === 1 && (
|
||||
<OptionBox label="中继选择">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<OptionBtn
|
||||
active={game.isRelayed === 0}
|
||||
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>
|
||||
</OptionBox>
|
||||
|
||||
@@ -493,8 +865,10 @@ export default function HttpPage() {
|
||||
{BANDWIDTH_OPTIONS.map(opt => (
|
||||
<OptionBtn
|
||||
key={opt.value}
|
||||
active={bandwidth === opt.value}
|
||||
onClick={() => setBandwidth(opt.value)}
|
||||
active={game.bandwidth === opt.value}
|
||||
onClick={() =>
|
||||
refreshGamePrice({ ...game, bandwidth: opt.value })
|
||||
}
|
||||
>
|
||||
{opt.label}
|
||||
</OptionBtn>
|
||||
@@ -504,11 +878,13 @@ export default function HttpPage() {
|
||||
|
||||
<OptionBox label="套餐周期">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{PERIOD_OPTIONS.map(opt => (
|
||||
{GAME_PERIOD_OPTIONS.map(opt => (
|
||||
<OptionBtn
|
||||
key={opt.value}
|
||||
active={period === opt.value}
|
||||
onClick={() => setPeriod(opt.value)}
|
||||
active={game.periodType === opt.value}
|
||||
onClick={() =>
|
||||
refreshGamePrice({ ...game, periodType: opt.value })
|
||||
}
|
||||
>
|
||||
{opt.label}
|
||||
</OptionBtn>
|
||||
@@ -516,23 +892,26 @@ export default function HttpPage() {
|
||||
</div>
|
||||
</OptionBox>
|
||||
|
||||
<OptionBox label="IP数量">
|
||||
<Stepper
|
||||
value={periodCount}
|
||||
unit="个"
|
||||
onChange={setPeriodCount}
|
||||
/>
|
||||
</OptionBox>
|
||||
|
||||
<OptionBox label="购买时长">
|
||||
<Stepper
|
||||
value={whitelistCount}
|
||||
unit="天"
|
||||
onChange={setWhitelistCount}
|
||||
value={game.periodAmount}
|
||||
unit={
|
||||
GAME_PERIOD_OPTIONS.find(
|
||||
opt => opt.value === game.periodType,
|
||||
)?.unit ?? "天"
|
||||
}
|
||||
onChange={v =>
|
||||
refreshGamePrice({ ...game, periodAmount: v })
|
||||
}
|
||||
min={1}
|
||||
/>
|
||||
</OptionBox>
|
||||
|
||||
<PriceBar price={totalPrice} onPay={openPayDialog} />
|
||||
<PriceBar
|
||||
price={gamePrice}
|
||||
loading={gamePriceLoading}
|
||||
onPay={() => void openGamePay()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -581,8 +960,11 @@ export default function HttpPage() {
|
||||
) : (
|
||||
<div className="text-center py-6">
|
||||
<p className="text-gray-600">
|
||||
个人剩余H币:{}
|
||||
<a href="/" className="text-blue-500 hover:underline ml-2">
|
||||
个人剩余H币:{payAmount.toFixed(2)}
|
||||
<a
|
||||
href="/product/http"
|
||||
className="text-blue-500 hover:underline ml-2"
|
||||
>
|
||||
去充值
|
||||
</a>
|
||||
</p>
|
||||
@@ -604,6 +986,50 @@ export default function HttpPage() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
@@ -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 (
|
||||
<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">
|
||||
<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>
|
||||
<Button
|
||||
onClick={onPay}
|
||||
size="lg"
|
||||
disabled={loading}
|
||||
className="rounded-md bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90"
|
||||
>
|
||||
实付 ¥{price.toFixed(2)}
|
||||
|
||||
@@ -8,6 +8,7 @@ export type Product = {
|
||||
productId: number
|
||||
title: string
|
||||
price: number
|
||||
minPrice?: number
|
||||
desc?: string
|
||||
tag?: string
|
||||
card?: string
|
||||
@@ -108,6 +109,7 @@ function transformToBrands(data: ProductItem[]): Brand[] {
|
||||
productId: item.Product.Id,
|
||||
title: pkg.Name,
|
||||
price: pkg.Price,
|
||||
minPrice: pkg.MinPrice,
|
||||
originalPrice: pkg.LinePrice,
|
||||
duration: pkg.Profile,
|
||||
card: pkg.OriginName,
|
||||
|
||||
Reference in New Issue
Block a user