Compare commits
2 Commits
6151add53f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fc625ede8 | ||
|
|
160a9c9b97 |
@@ -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",
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>> {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user