Compare commits

...

2 Commits

Author SHA1 Message Date
Eamon
1fc625ede8 产品购买 & 测试账号-免费领取 2026-10-13 13:32:53 +08:00
Eamon
160a9c9b97 取消单个窗口单IP选项 & 登录过期跳转到登录页面 2026-10-09 11:25:58 +08:00
13 changed files with 568 additions and 453 deletions

View File

@@ -1,11 +1,11 @@
import type { ApiResponse } from "@/lib/api" import type { ApiResponse } from "@/lib/api"
import type { MyAccountItem, MyAccountQueryParams } from "@/lib/models/account" import type { MyAccountItem, MyAccountQueryParams } from "@/lib/models/account"
import { callPublic } from "./base" import { callByUserCookie } from "./base"
export async function fetchMyAccounts( export async function fetchMyAccounts(
params: MyAccountQueryParams, params: MyAccountQueryParams,
): Promise<ApiResponse<MyAccountItem[]>> { ): Promise<ApiResponse<MyAccountItem[]>> {
const res = await callPublic<MyAccountItem[] | unknown>( const res = await callByUserCookie<MyAccountItem[] | unknown>(
"/user/ApiMyAccounts", "/user/ApiMyAccounts",
params, params,
"GET", "GET",

View File

@@ -4,9 +4,27 @@ import {
CLIENT_ID, CLIENT_ID,
CLIENT_SECRET, CLIENT_SECRET,
} from "@/lib/api" } from "@/lib/api"
import { getUserToken } from "./token" import { clearCookie, clearSession, getUserToken } from "./token"
type CallMode = "public" | "device" | "user" type CallMode = "public" | "device" | "user" | "cookie"
// ==================== 登录态失效全局处理 ====================
// user/cookie 模式请求未登录、收到 401(token 失效)或 302(后端跳登录页)时:
// 清除本地会话并跳转登录页,带 redirect 参数便于登录后回跳。
// 与 logout() 及 juipnet 老项目行为保持一致。
let redirectingToLogin = false
function handleUnauthorized() {
if (redirectingToLogin) return
clearSession()
clearCookie("token")
clearCookie("userInfo")
const current = window.location.pathname
if (current !== "/login") {
redirectingToLogin = true
window.location.href = `/login?redirect=${encodeURIComponent(current)}`
}
}
const DEFAULT_TIMEOUT = 15_000 const DEFAULT_TIMEOUT = 15_000
@@ -45,8 +63,20 @@ async function request<R = undefined>(
headers, headers,
body: init.body, body: init.body,
signal: controller.signal, signal: controller.signal,
redirect: "manual",
}) })
// 登录态失效时后端会把受保护接口 302 到登录页(如 /User/WebLogin)。
// manual 模式不自动跟随,此处识别重定向并返回 302,
// 由 call() 在 user/cookie 模式下统一触发 handleUnauthorized 跳登录页。
if (response.type === "opaqueredirect") {
return {
success: false,
status: 302,
message: "登录状态已失效,请重新登录",
}
}
const responseHeaders = captureHeaders(response) const responseHeaders = captureHeaders(response)
const contentType = response.headers.get("Content-Type") ?? "text/plain" const contentType = response.headers.get("Content-Type") ?? "text/plain"
@@ -108,7 +138,13 @@ async function request<R = undefined>(
return { success: true, data: json, headers: responseHeaders } return { success: true, data: json, headers: responseHeaders }
} }
throw new Error(`无法解析响应数据: ${contentType}`) // 其他内容类型(如 text/html 错误页)无法按 API 格式解析,
// 保留真实 HTTP 状态码返回,便于上层识别 401/3xx 等登录态问题
return {
success: false,
status: response.status,
message: `无法解析响应数据: ${contentType}`,
}
} catch (e) { } catch (e) {
if (controller.signal.aborted) { if (controller.signal.aborted) {
return { return {
@@ -210,6 +246,7 @@ async function call<R = undefined>(
if (mode === "user") { if (mode === "user") {
const token = getUserToken() const token = getUserToken()
if (!token) { if (!token) {
handleUnauthorized()
return { return {
success: false, success: false,
status: 401, status: 401,
@@ -217,6 +254,18 @@ async function call<R = undefined>(
} }
} }
auth = `Bearer ${token}` auth = `Bearer ${token}`
} else if (mode === "cookie") {
// cookie 模式:凭证走浏览器 cookie(老 /user/* 接口),
// 但登录态判断仍以本地 token 为准,缺失时同样触发跳登录
const token = getUserToken()
if (!token) {
handleUnauthorized()
return {
success: false,
status: 401,
message: "未登录或会话已过期",
}
}
} else if (mode === "device") { } else if (mode === "device") {
if (attempt > 1) { if (attempt > 1) {
deviceToken = null deviceToken = null
@@ -243,6 +292,16 @@ async function call<R = undefined>(
) { ) {
continue continue
} }
// 会话失效的三种表现:本地无 token(前面已处理)、后端 401、
// 或后端 302 重定向到登录页(如 /user/* 过期跳 /User/WebLogin)
if (
!resp.success &&
(resp.status === 401 || resp.status === 302) &&
(mode === "user" || mode === "cookie")
) {
handleUnauthorized()
}
return resp return resp
} }
@@ -283,4 +342,12 @@ async function callByUser<R = undefined>(
return call<R>("user", endpoint, data, method) return call<R>("user", endpoint, data, method)
} }
export { callByDevice, callByUser, callPublic } async function callByUserCookie<R = undefined>(
endpoint: string,
data?: unknown,
method: "GET" | "POST" = "POST",
): Promise<ApiResponse<R>> {
return call<R>("cookie", endpoint, data, method)
}
export { callByDevice, callByUser, callByUserCookie, callPublic }

View File

@@ -1,11 +1,11 @@
import type { ApiResponse } from "@/lib/api" import type { ApiResponse } from "@/lib/api"
import type { ApiIndexItem } from "@/lib/models/dashboard" import type { ApiIndexItem } from "@/lib/models/dashboard"
import { callPublic } from "./base" import { callByUserCookie } from "./base"
export async function fetchApiIndex(): Promise< export async function fetchApiIndex(): Promise<
ApiResponse<ApiIndexItem | null> ApiResponse<ApiIndexItem | null>
> { > {
const res = await callPublic<ApiIndexItem | ApiIndexItem[]>( const res = await callByUserCookie<ApiIndexItem | ApiIndexItem[]>(
"/user/ApiIndex", "/user/ApiIndex",
undefined, undefined,
"GET", "GET",

View File

@@ -1,6 +1,7 @@
import type { ApiResponse } from "@/lib/api" import type { ApiResponse } from "@/lib/api"
import type { import type {
CreateOrderResult, CreateOrderResult,
DxwxlData,
GameCity, GameCity,
GameItem, GameItem,
GameOrderData, GameOrderData,
@@ -212,6 +213,25 @@ export async function calcGamePrice(
return calcOrderPrice(orderInfo as unknown as PackageOrderInfo) return calcOrderPrice(orderInfo as unknown as PackageOrderInfo)
} }
export async function calcUnlimitedPrice(
data: DxwxlData,
): Promise<ApiResponse<number>> {
const res = await phpPost<RawHttpResponse>("/http/product/calc_price", {
order_type: 2,
money: 1,
pay_type: 1,
data,
})
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 calcOrderPrice( export async function calcOrderPrice(
orderInfo: PackageOrderInfo, orderInfo: PackageOrderInfo,
): Promise<ApiResponse<number>> { ): Promise<ApiResponse<number>> {

View File

@@ -6,7 +6,7 @@ import type {
UserItem, UserItem,
UserQueryParams, UserQueryParams,
} from "@/lib/models/user" } from "@/lib/models/user"
import { callByUser, callPublic } from "./base" import { callByUser, callByUserCookie } from "./base"
// ==================== 用户管理 ==================== // ==================== 用户管理 ====================
@@ -54,11 +54,11 @@ export async function updateUserProfile(
export async function updateUserInfo( export async function updateUserInfo(
data: UpdateUserInfoParams, data: UpdateUserInfoParams,
): Promise<ApiResponse<undefined>> { ): Promise<ApiResponse<undefined>> {
return await callPublic<undefined>("/user/ApiUpdateInfo", data) return await callByUserCookie<undefined>("/user/ApiUpdateInfo", data)
} }
export async function updateUserPwd( export async function updateUserPwd(
data: UpdateUserPwdParams, data: UpdateUserPwdParams,
): Promise<ApiResponse<PhpUserModel>> { ): Promise<ApiResponse<PhpUserModel>> {
return await callPublic<PhpUserModel>("/user/UpdatePwd", data) return await callByUserCookie<PhpUserModel>("/user/UpdatePwd", data)
} }

View File

@@ -3,7 +3,10 @@ import { useProfileStore } from "@/components/stores/profile"
export default function AuthGuard({ children }: { children: React.ReactNode }) { export default function AuthGuard({ children }: { children: React.ReactNode }) {
const location = useLocation() const location = useLocation()
const isValid = useProfileStore(state => state.token !== null) const token = useProfileStore(state => state.token)
const userInfo = useProfileStore(state => state.userInfo)
const isValid = token !== null && userInfo !== null
if (!isValid) { if (!isValid) {
return ( return (

View File

@@ -1,7 +1,7 @@
import { useEffect, 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 { createOrder, createTestAccount } from "@/api/order"
import RealNameCheck from "@/components/realNameCheck" import RealNameCheck from "@/components/realNameCheck"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
@@ -18,7 +18,6 @@ import Wrap from "@/components/wrap"
import type { CreateOrderRequest } from "@/lib/models/order" import type { CreateOrderRequest } from "@/lib/models/order"
import type { Product } from "@/lib/models/product" import type { Product } from "@/lib/models/product"
import { clearPayPaid, PAY_PAID_KEY, saveOrderPay } from "@/lib/recharge" import { clearPayPaid, PAY_PAID_KEY, saveOrderPay } from "@/lib/recharge"
import { cn } from "@/lib/utils"
import alipay from "../_assets/alipay.svg" import alipay from "../_assets/alipay.svg"
import balance from "../_assets/balance.svg" import balance from "../_assets/balance.svg"
import wechat from "../_assets/wechat.svg" import wechat from "../_assets/wechat.svg"
@@ -41,42 +40,6 @@ function randomDigits(length: number): string {
return result return result
} }
// ============ 模式切换 Tab ============
function ModeTabs({
mode,
onModeChange,
}: {
mode: "single" | "batch"
onModeChange: (mode: "single" | "batch") => void
}) {
return (
<div className="flex bg-gray-50 rounded-xl p-1 w-48 max-md:w-full">
<button
className={cn(
"flex-1 py-2 text-sm font-medium rounded-lg transition-all cursor-pointer",
mode === "single"
? "bg-white text-blue-600 shadow-sm"
: "text-gray-500 hover:text-gray-700",
)}
onClick={() => onModeChange("single")}
>
单个注册
</button>
<button
className={cn(
"flex-1 py-2 text-sm font-medium rounded-lg transition-all cursor-pointer",
mode === "batch"
? "bg-white text-blue-600 shadow-sm"
: "text-gray-500 hover:text-gray-700",
)}
onClick={() => onModeChange("batch")}
>
批量注册
</button>
</div>
)
}
// ============ 连接设备数选择器 ============ // ============ 连接设备数选择器 ============
function ConnectCountSelector({ function ConnectCountSelector({
value, value,
@@ -176,50 +139,15 @@ function CouponSelector() {
) )
} }
// ============ 批量注册说明 ============
function BatchInfo() {
return (
<div className="flex flex-col gap-2 md:flex-row md:items-start md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28 md:pt-1">
说明:
</Label>
<div className="flex-1 text-sm text-gray-600 leading-relaxed p-2 bg-blue-50/70 rounded-xl border border-blue-100">
<p>
批量注册的账号会使用【账号前缀】+【开始数】+【个数】顺序进行注册,
</p>
<p className="mt-1">
如:注册账号前缀为【user】开始数为【2】个数为【10】,则注册的账号为:
<span className="text-blue-600 font-medium">
{" "}
user2,user3,user4,....user11
</span>
</p>
</div>
</div>
)
}
// ============ 主页面 ============ // ============ 主页面 ============
export default function BuyPage() { export default function BuyPage() {
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const product = location.state?.product as Product | undefined const product = location.state?.product as Product | undefined
// 模式
const [mode, setMode] = useState<"single" | "batch">("single")
// 单个注册
const [account, setAccount] = useState("") const [account, setAccount] = useState("")
const [pwd, setPwd] = useState("") const [pwd, setPwd] = useState("")
const [connectCount, setConnectCount] = useState(1) const [connectCount, setConnectCount] = useState(1)
// 批量注册
const [batchAccount, setBatchAccount] = useState("")
const [batchPwd, setBatchPwd] = useState("")
const [batchStart, setBatchStart] = useState(1)
const [batchCount, setBatchCount] = useState(1)
const [batchConnectCount, setBatchConnectCount] = useState(1)
const [payType, setPayType] = useState("100") const [payType, setPayType] = useState("100")
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
@@ -240,12 +168,8 @@ export default function BuyPage() {
navigate("/product", { replace: true }) navigate("/product", { replace: true })
return return
} }
const acct = randomChars(2) + randomDigits(4) setAccount(randomChars(2) + randomDigits(4))
const pw = randomDigits(3) setPwd(randomDigits(3))
setAccount(acct)
setPwd(pw)
setBatchAccount(randomChars(3))
setBatchPwd(pw)
}, [navigate, product]) }, [navigate, product])
if (!product) return null if (!product) return null
@@ -271,69 +195,63 @@ export default function BuyPage() {
return total return total
} }
const singleTotal = clampMinPrice( const total = clampMinPrice(
product.price * displayConnectCount, product.price * displayConnectCount,
displayConnectCount, displayConnectCount,
) )
const batchTotal = clampMinPrice(
product.price * batchConnectCount * batchCount,
batchConnectCount * batchCount,
)
const total = mode === "single" ? singleTotal : batchTotal
// 批量预览 // 测试套餐:免费领取
const batchPreview = (() => { const handleClaim = async () => {
if (!batchAccount) return "" if (!account.trim() || !pwd.trim()) {
const end = batchStart + batchCount - 1 toast.error("账号和密码不能为空")
if (batchCount <= 4) { return
const items: string[] = []
for (let i = batchStart; i <= end; i++) {
items.push(`${batchAccount}${i}`)
}
return items.join(", ")
}
return `${batchAccount}${batchStart}, ${batchAccount}${batchStart + 1}, ${batchAccount}${batchStart + 2}...${batchAccount}${end}`
})()
const handlePay = async () => {
if (mode === "single") {
if (!account.trim() || !pwd.trim()) {
toast.error("账号和密码不能为空")
return
}
} else {
if (!batchAccount.trim() || !batchPwd.trim()) {
toast.error("账号前缀和密码不能为空")
return
}
if (batchCount > 500) {
toast.error("一次最多注册500个")
return
}
} }
setSubmitting(true) setSubmitting(true)
try { try {
const result = await createTestAccount({
ProductId: product.productId,
PackageId: packageId,
Account: account.trim(),
Pwd: pwd.trim(),
})
if (result.success) {
toast.success("领取成功")
navigate("/admin/channels")
} else {
toast.error(result.message || "领取失败")
}
} catch {
toast.error("网络错误,请重试")
} finally {
setSubmitting(false)
}
}
const handlePay = async () => {
if (!account.trim() || !pwd.trim()) {
toast.error("账号和密码不能为空")
return
}
setSubmitting(true)
try {
// 支付类型/通道映射:1余额 100支付宝 70微信;30微信 50支付宝 70余额
const oPayType = Number(payType) const oPayType = Number(payType)
const useBalance = oPayType === 1 ? 1 : 0 const useBalance = oPayType === 1
const payChannel = oPayType === 70 ? 30 : 50 const payChannel = oPayType === 1 ? 70 : oPayType === 70 ? 30 : 50
const params: CreateOrderRequest = { const params: CreateOrderRequest = {
Account: account.trim(),
PackageId: packageId, PackageId: packageId,
OrderType: mode === "single" ? 1 : 2, Pwd: pwd.trim(),
Account: mode === "single" ? account.trim() : batchAccount.trim(), ConnectCount: displayConnectCount,
Pwd: mode === "single" ? pwd.trim() : batchPwd.trim(), CouponAmount: 0,
ConnectCount:
mode === "single" ? displayConnectCount : batchConnectCount,
CouponId: 0, CouponId: 0,
UseAccountAmount: useBalance,
OPayType: oPayType, OPayType: oPayType,
PayChannel: payChannel, PayChannel: payChannel,
Price: product.price, UseAccountAmount: useBalance,
...(mode === "batch" && {
MinPostfix: batchStart,
MaxPostfix: batchCount,
}),
} }
const result = await createOrder(params) const result = await createOrder(params)
@@ -343,7 +261,6 @@ export default function BuyPage() {
return return
} }
// 参考 juipnet Product/buy.cshtml:
// Data == "00" 为余额支付直接成功;否则需要微信/支付宝支付 // Data == "00" 为余额支付直接成功;否则需要微信/支付宝支付
const data = result.data const data = result.data
if (typeof data === "string") { if (typeof data === "string") {
@@ -381,9 +298,6 @@ export default function BuyPage() {
const handleBack = () => navigate(-1) const handleBack = () => navigate(-1)
// 天卡首单(0.1元)不显示批量注册;天卡>0.1元时显示(juipnet:应付款!=0.1时显示批量tab)
const showTabs = !isTest && !isFirstOrderDayCard
return ( return (
<Wrap className="bg-white min-h-screen flex items-start justify-center pt-24"> <Wrap className="bg-white min-h-screen flex items-start justify-center pt-24">
<RealNameCheck /> <RealNameCheck />
@@ -392,249 +306,162 @@ export default function BuyPage() {
<div className="text-center mb-6 space-y-2"> <div className="text-center mb-6 space-y-2">
<h4 className="text-2xl font-bold"> <h4 className="text-2xl font-bold">
<span className="text-gray-800">{product.title}</span> <span className="text-gray-800">{product.title}</span>
<span className="text-orange-500 ml-2">¥{total.toFixed(2)}</span> <span className="text-orange-500 ml-2">
{isTest ? "免费领取" : `¥${total.toFixed(2)}`}
</span>
</h4> </h4>
<p className="text-sm text-amber-600"> {isTest ? (
请务必选好所需物品,换货会产生费用 <p className="text-sm text-amber-600">
</p> 测试套餐免费领取,领取成功后可在 IP账号管理 中查看
{isDayCard && ( </p>
<p className="text-sm text-amber-600">天卡不支持退款,请谨慎购买</p> ) : (
<>
<p className="text-sm text-amber-600">
请务必选好所需物品,换货会产生费用
</p>
{isDayCard && (
<p className="text-sm text-amber-600">
天卡不支持退款,请谨慎购买
</p>
)}
</>
)} )}
</div> </div>
{/* Tab 切换 */}
{showTabs && (
<div className="flex justify-center mb-3">
<ModeTabs mode={mode} onModeChange={setMode} />
</div>
)}
<div className="space-y-5 mb-20"> <div className="space-y-5 mb-20">
{mode === "single" ? ( {/* IP产品账号 */}
// ====== 单个注册 ======
<>
{/* IP产品账号 */}
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
IP产品账号:
</Label>
<Input
value={account}
onChange={e => setAccount(e.target.value)}
placeholder="4-10位字母或数字"
className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1"
/>
<span className="text-xs text-gray-400">
4至10位字母或数字或组合
</span>
</div>
{/* IP产品密码 */}
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
IP产品密码:
</Label>
<Input
value={pwd}
onChange={e => setPwd(e.target.value)}
placeholder="1-10位字母或数字"
className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1"
/>
<span className="text-xs text-gray-400">
1至10位字母或数字或组合
</span>
</div>
{!isTest && (
<ConnectCountSelector
value={displayConnectCount}
onChange={setConnectCount}
label="连接设备数"
hint="一个账号可同时在线设备数"
disabled={isFirstOrderDayCard}
/>
)}
</>
) : (
// ====== 批量注册 ======
<>
{/* 批量说明 */}
<BatchInfo />
{/* IP账号前缀 */}
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
IP账号前缀:
</Label>
<Input
value={batchAccount}
onChange={e => setBatchAccount(e.target.value)}
placeholder="3-8位字母或数字"
className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1"
/>
<span className="text-xs text-gray-400">
3至8位字母或数字或组合
</span>
</div>
{/* 开始号 */}
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
开始号:
</Label>
<Input
type="number"
value={batchStart}
onChange={e =>
setBatchStart(Number.parseInt(e.target.value, 10) || 1)
}
className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1 md:max-w-32"
/>
<span className="text-xs text-gray-400">
本批次账号的起始账号
</span>
</div>
{/* 注册个数 */}
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
注册个数:
</Label>
<Input
type="number"
value={batchCount}
onChange={e =>
setBatchCount(
Math.min(500, Number.parseInt(e.target.value, 10) || 1),
)
}
className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1 md:max-w-32"
/>
</div>
{/* 账号预览 */}
{batchPreview && (
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
即将生成的账号:
</Label>
<span className="text-sm text-gray-700 font-medium">
{batchPreview}
</span>
</div>
)}
{/* IP产品密码 */}
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
IP产品密码:
</Label>
<Input
value={batchPwd}
onChange={e => setBatchPwd(e.target.value)}
placeholder="1-10位字母或数字"
className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1"
/>
<span className="text-xs text-gray-400">
1至10位字母或数字或组合
</span>
</div>
{/* 单账号连接设备数 */}
<ConnectCountSelector
value={batchConnectCount}
onChange={setBatchConnectCount}
label="单账号连接设备数"
hint="每个账号可同时在线设备的数量"
/>
</>
)}
{/* 优惠券 */}
{!isTest && <CouponSelector />}
{/* 余额 */}
<div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6"> <div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28"> <Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
余额: IP产品账号:
</Label> </Label>
<div className="flex items-center gap-2 w-full md:w-auto md:flex-1"> <Input
<span className="text-sm text-gray-600"> value={account}
当前账户余额 onChange={e => setAccount(e.target.value)}
<span className="font-bold text-orange-500 ml-1">¥10.00</span> placeholder="4-10位字母或数字"
</span> className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1"
<Button />
variant="link" <span className="text-xs text-gray-400">
className="text-sm text-blue-500 p-0 h-auto font-medium" 4至10位字母或数字或组合
onClick={() => navigate("/admin")} </span>
>
去充值
</Button>
</div>
</div> </div>
{/* 支付方式 */} {/* IP产品密码 */}
<div className="flex flex-col gap-3 md:flex-row md:items-center md:gap-6"> <div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28"> <Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
支付方式: IP产品密码:
</Label> </Label>
<RadioGroup <Input
value={payType} value={pwd}
onValueChange={setPayType} onChange={e => setPwd(e.target.value)}
className="flex flex-wrap gap-x-8 gap-y-3 md:gap-8" placeholder="1-10位字母或数字"
> className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1"
<div className="flex items-center gap-2"> />
<RadioGroupItem value="100" id="alipay" /> <span className="text-xs text-gray-400">
<Label 1至10位字母或数字或组合
htmlFor="alipay" </span>
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
>
<img src={alipay} alt="支付宝" className="w-5 h-5" />
支付宝
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="70" id="wechat" />
<Label
htmlFor="wechat"
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
>
<img src={wechat} alt="微信" className="w-5 h-5" />
微信
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="1" id="balance" />
<Label
htmlFor="balance"
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
>
<img src={balance} alt="余额" className="w-5 h-5" />
余额
</Label>
</div>
</RadioGroup>
</div> </div>
{/* 总金额 / 应付款 */} {!isTest && (
<div className="space-y-2"> <ConnectCountSelector
<div className="flex items-center justify-between md:justify-start md:gap-6"> value={displayConnectCount}
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28"> onChange={setConnectCount}
总金额: label="连接设备数"
</Label> hint="一个账号可同时在线设备数"
<span className="font-medium text-gray-800"> disabled={isFirstOrderDayCard}
¥{total.toFixed(2)} />
</span> )}
</div>
<div className="flex items-center justify-between md:justify-start md:gap-6"> {!isTest && (
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28"> <>
应付款: {/* 优惠券 */}
</Label> <CouponSelector />
<span className="text-xl font-bold text-orange-500">
¥{total.toFixed(2)} {/* 余额 */}
</span> <div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-6">
</div> <Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
</div> 余额:
</Label>
<div className="flex items-center gap-2 w-full md:w-auto md:flex-1">
<span className="text-sm text-gray-600">
当前账户余额
<span className="font-bold text-orange-500 ml-1">
¥10.00
</span>
</span>
<Button
variant="link"
className="text-sm text-blue-500 p-0 h-auto font-medium"
onClick={() => navigate("/admin")}
>
去充值
</Button>
</div>
</div>
{/* 支付方式 */}
<div className="flex flex-col gap-3 md:flex-row md:items-center md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
支付方式:
</Label>
<RadioGroup
value={payType}
onValueChange={setPayType}
className="flex flex-wrap gap-x-8 gap-y-3 md:gap-8"
>
<div className="flex items-center gap-2">
<RadioGroupItem value="100" id="alipay" />
<Label
htmlFor="alipay"
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
>
<img src={alipay} alt="支付宝" className="w-5 h-5" />
支付宝
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="70" id="wechat" />
<Label
htmlFor="wechat"
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
>
<img src={wechat} alt="微信" className="w-5 h-5" />
微信
</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="1" id="balance" />
<Label
htmlFor="balance"
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
>
<img src={balance} alt="余额" className="w-5 h-5" />
余额
</Label>
</div>
</RadioGroup>
</div>
{/* 总金额 / 应付款 */}
<div className="space-y-2">
<div className="flex items-center justify-between md:justify-start md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
总金额:
</Label>
<span className="font-medium text-gray-800">
¥{total.toFixed(2)}
</span>
</div>
<div className="flex items-center justify-between md:justify-start md:gap-6">
<Label className="text-sm font-medium text-gray-700 md:whitespace-nowrap md:w-28">
应付款:
</Label>
<span className="text-xl font-bold text-orange-500">
¥{total.toFixed(2)}
</span>
</div>
</div>
</>
)}
<div className="flex gap-4"> <div className="flex gap-4">
<Button <Button
@@ -644,13 +471,23 @@ export default function BuyPage() {
> >
上一步 上一步
</Button> </Button>
<Button {isTest ? (
className="flex-1 h-12 rounded-xl text-base bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 transition-all" <Button
onClick={handlePay} className="flex-1 h-12 rounded-xl text-base bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 transition-all"
disabled={submitting} onClick={handleClaim}
> disabled={submitting}
{submitting ? "提交中..." : "确认支付"} >
</Button> {submitting ? "领取中..." : "免费领取"}
</Button>
) : (
<Button
className="flex-1 h-12 rounded-xl text-base bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 transition-all"
onClick={handlePay}
disabled={submitting}
>
{submitting ? "提交中..." : "确认支付"}
</Button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -324,16 +324,15 @@ export default function BuyStaticPage() {
try { try {
const oPayType = Number(payType) const oPayType = Number(payType)
const params: CreateOrderRequest = { const params: CreateOrderRequest = {
PackageId: packageId,
OrderType: 1,
Account: account.trim(), Account: account.trim(),
PackageId: packageId,
Pwd: pwd.trim(), Pwd: pwd.trim(),
ConnectCount: 1, ConnectCount: 1,
CouponAmount: 0,
CouponId: 0, CouponId: 0,
UseAccountAmount: oPayType === 1 ? 1 : 0, UseAccountAmount: oPayType === 1,
OPayType: oPayType, OPayType: oPayType,
PayChannel: oPayType === 70 ? 30 : 50, PayChannel: oPayType === 1 ? 70 : oPayType === 70 ? 30 : 50,
Price: Number(getPrice()),
} }
const result = await createOrder(params) const result = await createOrder(params)
@@ -353,7 +352,7 @@ export default function BuyStaticPage() {
saveOrderPay({ saveOrderPay({
orderNo: data.OrderInfo.OrderNo, orderNo: data.OrderInfo.OrderNo,
amount: Number(data.OrderInfo.OtherPayAmount) || Number(getPrice()), amount: Number(data.OrderInfo.OtherPayAmount) || Number(getPrice()),
payChannel: oPayType === 70 ? 30 : 50, payChannel: oPayType === 1 ? 70 : oPayType === 70 ? 30 : 50,
payData: data.PayData, payData: data.PayData,
}) })
const win = window.open( const win = window.open(

View File

@@ -1,8 +1,9 @@
import { useRef, useState } from "react" import { useEffect, useRef, useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { Toaster, toast } from "sonner" import { Toaster, toast } from "sonner"
import { import {
calcGamePrice, calcGamePrice,
calcUnlimitedPrice,
createHttpOrder, createHttpOrder,
fetchGameCities, fetchGameCities,
fetchGameLineCount, fetchGameLineCount,
@@ -58,6 +59,9 @@ const DURATION_OPTIONS = [
{ value: "360", label: "3小时~6小时" }, { value: "360", label: "3小时~6小时" },
] ]
// 短效无限量:有效时长固定 1~5 分钟
const UNLIMITED_DURATION_OPTIONS = [{ value: "5", label: "1~5分钟" }]
const PERIOD_OPTIONS = [ const PERIOD_OPTIONS = [
{ value: "1", label: "按天" }, { value: "1", label: "按天" },
{ value: "7", label: "按周" }, { value: "7", label: "按周" },
@@ -65,6 +69,15 @@ const PERIOD_OPTIONS = [
{ value: "90", label: "按季" }, { value: "90", label: "按季" },
] ]
// 短效无限量:周期含按小时(1060),默认按小时
const UNLIMITED_PERIOD_OPTIONS = [
{ value: "1060", label: "按小时", unit: "小时" },
{ value: "1", label: "按天", unit: "天" },
{ value: "7", label: "按周", unit: "周" },
{ value: "30", label: "按月", unit: "月" },
{ value: "90", label: "按季", unit: "季" },
]
const IP_AMOUNT_OPTIONS = [ const IP_AMOUNT_OPTIONS = [
{ value: 5000, label: "每日最多使用 5,000 个IP" }, { value: 5000, label: "每日最多使用 5,000 个IP" },
{ value: 10000, label: "每日最多使用 10,000 个IP" }, { value: 10000, label: "每日最多使用 10,000 个IP" },
@@ -125,6 +138,15 @@ export default function HttpPage() {
const [ipAmount, setIpAmount] = useState(5000) const [ipAmount, setIpAmount] = useState(5000)
const [volumeAmount, setVolumeAmount] = useState(1) const [volumeAmount, setVolumeAmount] = useState(1)
// 短效无限量:周期类型/购买时长/白名单数量 + 服务端单价
const [unlimitedPeriod, setUnlimitedPeriod] = useState(
UNLIMITED_PERIOD_OPTIONS[0].value,
)
const [unlimitedPeriodCount, setUnlimitedPeriodCount] = useState(1)
const [unlimitedUnitPrice, setUnlimitedUnitPrice] = useState(0)
const [unlimitedPriceLoading, setUnlimitedPriceLoading] = useState(false)
const unlimitedPriceSeqRef = useRef(0)
// 长效游戏 // 长效游戏
const [game, _setGame] = useState<GameOrderData>(DEFAULT_GAME) const [game, _setGame] = useState<GameOrderData>(DEFAULT_GAME)
const gameRef = useRef(game) const gameRef = useRef(game)
@@ -153,6 +175,10 @@ export default function HttpPage() {
const [gamePaying, setGamePaying] = useState(false) const [gamePaying, setGamePaying] = useState(false)
const alipayBoxRef = useRef<HTMLDivElement>(null) const alipayBoxRef = useRef<HTMLDivElement>(null)
// 短效无限量支付(H币)
const [unlimitedPayOpen, setUnlimitedPayOpen] = useState(false)
const [unlimitedPaying, setUnlimitedPaying] = useState(false)
const setGame = (next: GameOrderData) => { const setGame = (next: GameOrderData) => {
gameRef.current = next gameRef.current = next
_setGame(next) _setGame(next)
@@ -340,6 +366,67 @@ export default function HttpPage() {
setGamePaying(false) setGamePaying(false)
} }
// 短效无限量:打开 H币 支付弹窗(与 juipnet 一致)
const openUnlimitedPay = async () => {
if (!ensureLogin()) return
if (unlimitedTotal <= 0) {
toast.error("价格获取失败,请稍后重试")
return
}
const res = await fetchHttpBalance()
if (!res.success) {
toast.error(res.message)
return
}
setBalance(res.data)
setUnlimitedPayOpen(true)
}
const payUnlimitedOrder = async () => {
if (!ensureLogin()) return
setUnlimitedPaying(true)
const res = await createHttpOrder({
order_type: 2,
money: unlimitedTotal,
pay_type: 1,
data: {
durationType: 5,
periodType: Number(unlimitedPeriod),
periodAmount: unlimitedPeriodCount,
maxWhitelist: whitelistCount,
},
})
if (!res.success) {
toast.error(res.message)
setUnlimitedPaying(false)
return
}
const result = res.data
if (result.code === 1) {
setUnlimitedPayOpen(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()
setUnlimitedPayOpen(false)
} else {
toast.error(result.msg || "支付宝支付数据异常")
}
} else if (result.code === 3) {
if (result.data) {
setQrContent(result.data)
setQrCodeOpen(true)
setUnlimitedPayOpen(false)
} else {
toast.error(result.msg || "微信支付数据异常")
}
} else {
toast.error(result.msg || "支付失败")
}
setUnlimitedPaying(false)
}
const handleTabClick = (id: string) => { const handleTabClick = (id: string) => {
setActiveTab(id) setActiveTab(id)
if (id === "game") { if (id === "game") {
@@ -352,6 +439,37 @@ export default function HttpPage() {
} }
} }
// 短效无限量:向服务端查询单价并计算总价(与 juipnet 一致)
useEffect(() => {
if (activeTab !== "unlimited") return
const seq = ++unlimitedPriceSeqRef.current
setUnlimitedPriceLoading(true)
calcUnlimitedPrice({
durationType: 5,
periodType: Number(unlimitedPeriod),
periodAmount: unlimitedPeriodCount,
maxWhitelist: whitelistCount,
}).then(res => {
if (seq !== unlimitedPriceSeqRef.current) return
setUnlimitedPriceLoading(false)
if (!res.success) {
toast.error(res.message)
return
}
setUnlimitedUnitPrice(res.data)
})
}, [activeTab, unlimitedPeriod, unlimitedPeriodCount, whitelistCount])
// 短效无限量总价:periodAmount * whitelist * max(0.6, 1-0.1*(whitelist-1)) * 单价
const unlimitedTotal = Number(
(
unlimitedPeriodCount *
whitelistCount *
Math.max(0.6, 1 - 0.1 * (whitelistCount - 1)) *
unlimitedUnitPrice
).toFixed(2),
)
// 计算价格 // 计算价格
const calculatePrice = () => { const calculatePrice = () => {
// 根据不同的 tab 计算价格 // 根据不同的 tab 计算价格
@@ -361,7 +479,7 @@ export default function HttpPage() {
price = Number(customAmount) || 1 price = Number(customAmount) || 1
break break
case "unlimited": case "unlimited":
price = whitelistCount * periodCount * 0.5 price = unlimitedTotal
break break
case "daily": case "daily":
price = ipAmount * periodCount * 0.01 price = ipAmount * periodCount * 0.01
@@ -530,12 +648,8 @@ export default function HttpPage() {
<OptionBox label="有效时长"> <OptionBox label="有效时长">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{DURATION_OPTIONS.map(opt => ( {UNLIMITED_DURATION_OPTIONS.map(opt => (
<OptionBtn <OptionBtn key={opt.value} active>
key={opt.value}
active={duration === opt.value}
onClick={() => setDuration(opt.value)}
>
{opt.label} {opt.label}
</OptionBtn> </OptionBtn>
))} ))}
@@ -544,11 +658,11 @@ 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 => ( {UNLIMITED_PERIOD_OPTIONS.map(opt => (
<OptionBtn <OptionBtn
key={opt.value} key={opt.value}
active={period === opt.value} active={unlimitedPeriod === opt.value}
onClick={() => setPeriod(opt.value)} onClick={() => setUnlimitedPeriod(opt.value)}
> >
{opt.label} {opt.label}
</OptionBtn> </OptionBtn>
@@ -558,9 +672,13 @@ export default function HttpPage() {
<OptionBox label="购买时长"> <OptionBox label="购买时长">
<Stepper <Stepper
value={periodCount} value={unlimitedPeriodCount}
unit="小时" unit={
onChange={setPeriodCount} UNLIMITED_PERIOD_OPTIONS.find(
opt => opt.value === unlimitedPeriod,
)?.unit ?? "小时"
}
onChange={setUnlimitedPeriodCount}
/> />
</OptionBox> </OptionBox>
@@ -572,6 +690,8 @@ export default function HttpPage() {
onChange={v => setWhitelistCount(v)} onChange={v => setWhitelistCount(v)}
min={1} min={1}
max={1000} max={1000}
step={10}
stepDown={1}
/> />
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
您的白名单上限{whitelistCount}个, 您的白名单上限{whitelistCount}个,
@@ -590,7 +710,11 @@ export default function HttpPage() {
</div> </div>
</OptionBox> </OptionBox>
<PriceBar price={totalPrice} onPay={openPayDialog} /> <PriceBar
price={unlimitedTotal}
loading={unlimitedPriceLoading}
onPay={openUnlimitedPay}
/>
</div> </div>
)} )}
@@ -717,6 +841,7 @@ export default function HttpPage() {
placeholder="金额" placeholder="金额"
className="w-28 h-7 inline-block text-center" className="w-28 h-7 inline-block text-center"
/> />
{/* 这个金额需要根据选项自动更新和也支持手动显示 */}
元 元
</span> </span>
</div> </div>
@@ -1049,45 +1174,24 @@ export default function HttpPage() {
</Dialog> </Dialog>
{/* 长效游戏支付弹窗(H币支付) */} {/* 长效游戏支付弹窗(H币支付) */}
<Dialog open={gamePayOpen} onOpenChange={setGamePayOpen}> <HCoinPayDialog
<DialogContent className="sm:max-w-md"> open={gamePayOpen}
<DialogHeader> onOpenChange={setGamePayOpen}
<DialogTitle className="text-center"> amount={gamePrice}
待支付:¥{gamePrice.toFixed(2)}元 balance={balance}
</DialogTitle> paying={gamePaying}
</DialogHeader> onPay={() => payGameOrder()}
/>
<div className="py-4"> {/* 短效无限量支付弹窗(H币支付) */}
<div className="text-center py-6"> <HCoinPayDialog
<p className="text-gray-600"> open={unlimitedPayOpen}
个人剩余H币: onOpenChange={setUnlimitedPayOpen}
<span className="text-orange-500 font-medium ml-1"> amount={unlimitedTotal}
{balance.toFixed(2)} balance={balance}
</span> paying={unlimitedPaying}
<a onPay={() => payUnlimitedOrder()}
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={() => 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" /> <div ref={alipayBoxRef} className="hidden" />
@@ -1120,6 +1224,64 @@ export default function HttpPage() {
) )
} }
function HCoinPayDialog({
open,
onOpenChange,
amount,
balance,
paying,
onPay,
}: {
open: boolean
onOpenChange: (open: boolean) => void
amount: number
balance: number
paying: boolean
onPay: () => void
}) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-center">
待支付:¥{amount.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币:{amount.toFixed(2)}
</p>
</div>
</div>
<DialogFooter>
<Button
onClick={onPay}
disabled={paying}
className="w-full bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 py-3"
>
{paying ? "支付中..." : "立即支付"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
function PrepaidCardCustom({ function PrepaidCardCustom({
amount, amount,
onAmountChange, onAmountChange,
@@ -1279,17 +1441,22 @@ function Stepper({
onChange, onChange,
min = 1, min = 1,
max, max,
step = 1,
stepDown,
}: { }: {
value: number value: number
unit: string unit: string
onChange: (v: number) => void onChange: (v: number) => void
min?: number min?: number
max?: number max?: number
step?: number
stepDown?: number
}) { }) {
const downStep = stepDown ?? step
return ( return (
<span className="inline-flex items-center rounded-md border overflow-hidden"> <span className="inline-flex items-center rounded-md border overflow-hidden">
<button <button
onClick={() => onChange(Math.max(min, value - 1))} onClick={() => onChange(Math.max(min, value - downStep))}
disabled={value <= min} disabled={value <= min}
className="px-3 py-2 bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 disabled:opacity-40 transition-opacity cursor-pointer" className="px-3 py-2 bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 disabled:opacity-40 transition-opacity cursor-pointer"
> >
@@ -1300,7 +1467,9 @@ function Stepper({
{unit} {unit}
</span> </span>
<button <button
onClick={() => onChange(max ? Math.min(max, value + 1) : value + 1)} onClick={() =>
onChange(max ? Math.min(max, value + step) : value + step)
}
disabled={max !== undefined && value >= max} disabled={max !== undefined && value >= max}
className="px-3 py-2 bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 disabled:opacity-40 transition-opacity cursor-pointer" className="px-3 py-2 bg-linear-to-r from-blue-500 to-cyan-400 text-white hover:opacity-90 disabled:opacity-40 transition-opacity cursor-pointer"
> >

View File

@@ -88,7 +88,7 @@ export default function ProductGrid({ products }: ProductGridProps) {
className="w-full text-blue-500 border-blue-400 hover:bg-blue-50" className="w-full text-blue-500 border-blue-400 hover:bg-blue-50"
onClick={() => handleBuy(product)} onClick={() => handleBuy(product)}
> >
立即购买 {product.isTest ? "免费领取" : "立即购买"}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -13,10 +13,18 @@ export default function ProductTabs({
activeId, activeId,
onTabChange, onTabChange,
}: ProductTabsProps) { }: ProductTabsProps) {
const visibleTabs = tabs.filter(tab => tab.id !== "window")
return ( return (
<Tabs value={activeId} onValueChange={onTabChange} className="w-full"> <Tabs value={activeId} onValueChange={onTabChange} className="w-full">
<TabsList className="grid w-full h-auto bg-transparent p-0 grid-cols-3"> <TabsList
{tabs.map(tab => ( className={cn(
"grid w-full h-auto bg-transparent p-0",
visibleTabs.length === 2 ? "grid-cols-2" : "grid-cols-3",
)}
>
{visibleTabs.map(tab => (
<TabsTrigger <TabsTrigger
key={tab.id} key={tab.id}
value={tab.id} value={tab.id}
@@ -33,4 +41,4 @@ export default function ProductTabs({
</TabsList> </TabsList>
</Tabs> </Tabs>
) )
} }

View File

@@ -12,6 +12,13 @@ export type GameOrderData = {
periodAmount: number periodAmount: number
} }
export type DxwxlData = {
durationType: number
periodType: number
periodAmount: number
maxWhitelist: number
}
export type GameCity = { export type GameCity = {
code: number code: number
name: string name: string
@@ -27,7 +34,7 @@ export type HttpOrderInfo = {
money: number money: number
pay_type: number pay_type: number
// 预储值(order_type=1)下单时 data 为空对象 // 预储值(order_type=1)下单时 data 为空对象
data: GameOrderData | Record<string, never> data: GameOrderData | DxwxlData | Record<string, never>
} }
export type CreateOrderResult = { export type CreateOrderResult = {

View File

@@ -39,19 +39,24 @@ export type OrderItem = {
DeleteTag: number DeleteTag: number
} }
// 产品购买(不含水滴)
export type CreateOrderRequest = { export type CreateOrderRequest = {
PackageId: number
OrderType: number
Account: string Account: string
PackageId: number
Pwd: string Pwd: string
ConnectCount: number ConnectCount: number
MinPostfix?: number // 优惠券减免金额
MaxPostfix?: number CouponAmount: number
CouponId?: number // 优惠券ID
UseAccountAmount?: number CouponId: number
// 支付类型:1余额 100支付宝 70微信
OPayType: number OPayType: number
PayChannel?: number // 支付通道:30微信 50支付宝 70余额
Price?: number PayChannel: number
// 是否使用余额
UseAccountAmount: boolean
// 代理下用户会员号(可选)
phone?: string
} }
export type CreateTestAccountRequest = { export type CreateTestAccountRequest = {