个人中心的余额充值部分
This commit is contained in:
@@ -407,7 +407,11 @@ export default function DashboardPage() {
|
||||
onOpenChange={setRealNameOpen}
|
||||
onVerified={refresh}
|
||||
/>
|
||||
<RechargeDialog open={rechargeOpen} onOpenChange={setRechargeOpen} />
|
||||
<RechargeDialog
|
||||
open={rechargeOpen}
|
||||
onOpenChange={setRechargeOpen}
|
||||
onPaid={refresh}
|
||||
/>
|
||||
<CashOutDialog
|
||||
open={cashOutOpen}
|
||||
onOpenChange={setCashOutOpen}
|
||||
|
||||
@@ -93,6 +93,7 @@ async function request<R = undefined>(
|
||||
return {
|
||||
success: false,
|
||||
status: response.status,
|
||||
code: json.Code,
|
||||
message: json.Message || "请求失败",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +167,8 @@ export async function createRechargeOrder(data: {
|
||||
}
|
||||
|
||||
// 轮询充值支付状态:返回 true 表示支付成功
|
||||
// 新后端 /user/IsPay 响应体:{ Code: 0|1, Message: "" },Code 本身即支付状态
|
||||
// 兼容旧格式 { Code: 10000, Data: 0|1 }
|
||||
export async function checkRechargeStatus(
|
||||
orderNo: string,
|
||||
): Promise<ApiResponse<boolean>> {
|
||||
@@ -175,6 +177,8 @@ export async function checkRechargeStatus(
|
||||
undefined,
|
||||
"GET",
|
||||
)
|
||||
if (!res.success) return res
|
||||
return { success: true, data: res.data === 1 }
|
||||
if (res.success) return { success: true, data: res.data === 1 }
|
||||
if (res.code === 1) return { success: true, data: true }
|
||||
if (res.code === 0) return { success: true, data: false }
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { CheckCircle2, Loader2, XCircle } from "lucide-react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
checkRechargeStatus,
|
||||
createRechargeOrder,
|
||||
type RechargeOrderResult,
|
||||
} from "@/api/order"
|
||||
import { QrCodeImage } from "@/components/qrCodeImage"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
@@ -15,46 +22,205 @@ import wechat from "@/home/_assets/wechat.svg"
|
||||
|
||||
type PayChannel = "50" | "30"
|
||||
|
||||
const POLL_INTERVAL = 3000
|
||||
|
||||
interface RechargeDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
// 支付成功后回调(刷新余额等)
|
||||
onPaid?: () => void
|
||||
}
|
||||
|
||||
export function RechargeDialog({ open, onOpenChange }: RechargeDialogProps) {
|
||||
// 余额充值(与官网 juipnet User/Index.cshtml 一致):
|
||||
// - 下单 POST /user/CreateOrder
|
||||
// - 支付宝(50):PayData 为自动提交表单,渲染后自动跳转支付宝网关
|
||||
// - 微信(30):PayData 为扫码链接,直接渲染二维码,轮询 /user/IsPay
|
||||
export function RechargeDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onPaid,
|
||||
}: RechargeDialogProps) {
|
||||
const [amount, setAmount] = useState("100")
|
||||
const [payChannel, setPayChannel] = useState<PayChannel>("50")
|
||||
|
||||
const [order, setOrder] = useState<RechargeOrderResult | null>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [orderError, setOrderError] = useState("")
|
||||
const [paid, setPaid] = useState(false)
|
||||
const [failCount, setFailCount] = useState(0)
|
||||
|
||||
const alipayBoxRef = useRef<HTMLDivElement>(null)
|
||||
const alipaySubmittedRef = useRef(false)
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setAmount("100")
|
||||
setPayChannel("50")
|
||||
setOrder(null)
|
||||
setCreating(false)
|
||||
setOrderError("")
|
||||
setPaid(false)
|
||||
setFailCount(0)
|
||||
alipaySubmittedRef.current = false
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) reset()
|
||||
}, [open, reset])
|
||||
|
||||
const handleGoPay = () => {
|
||||
const handleGoPay = async () => {
|
||||
const chargeAmount = Number(amount)
|
||||
if (!Number.isFinite(chargeAmount) || chargeAmount <= 0) {
|
||||
toast.error("请输入正确的充值金额")
|
||||
return
|
||||
}
|
||||
|
||||
window.open(
|
||||
`/pay?amount=${chargeAmount}&channel=${payChannel}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
setCreating(true)
|
||||
setOrderError("")
|
||||
const res = await createRechargeOrder({
|
||||
ChargeAmount: chargeAmount,
|
||||
PayChannel: Number(payChannel),
|
||||
})
|
||||
setCreating(false)
|
||||
if (!res.success) {
|
||||
setOrderError(res.message)
|
||||
return
|
||||
}
|
||||
if (!res.data?.PayData) {
|
||||
setOrderError("下单失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
setOrder(res.data)
|
||||
}
|
||||
|
||||
// 支付宝:渲染 PayData 表单并自动提交,跳转支付宝网关
|
||||
useEffect(() => {
|
||||
if (!open || payChannel !== "50" || !order || orderError) return
|
||||
if (alipaySubmittedRef.current) return
|
||||
alipaySubmittedRef.current = true
|
||||
const box = alipayBoxRef.current
|
||||
if (!box) return
|
||||
box.innerHTML = order.PayData
|
||||
box.querySelector("form")?.submit()
|
||||
}, [open, payChannel, order, orderError])
|
||||
|
||||
// 微信:轮询 /user/IsPay 直到支付成功
|
||||
useEffect(() => {
|
||||
if (!open || payChannel !== "30" || !order || paid) return
|
||||
let stopped = false
|
||||
|
||||
const poll = async () => {
|
||||
const res = await checkRechargeStatus(order.OrderInfo.OrderNo)
|
||||
if (stopped) return
|
||||
if (res.success && res.data) {
|
||||
setPaid(true)
|
||||
toast.success("充值成功,余额已更新")
|
||||
onPaid?.()
|
||||
} else if (!res.success) {
|
||||
setFailCount(count => count + 1)
|
||||
}
|
||||
}
|
||||
|
||||
poll()
|
||||
const timer = window.setInterval(poll, POLL_INTERVAL)
|
||||
return () => {
|
||||
stopped = true
|
||||
window.clearInterval(timer)
|
||||
}
|
||||
}, [open, payChannel, order, paid, onPaid])
|
||||
|
||||
const isWechat = payChannel === "30"
|
||||
|
||||
const renderBody = () => {
|
||||
if (creating) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2 py-10">
|
||||
<Loader2 className="size-8 animate-spin text-blue-500" />
|
||||
<p className="text-sm text-gray-500">正在生成订单...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (orderError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 py-6">
|
||||
<XCircle className="size-10 text-red-400" />
|
||||
<p className="text-sm text-red-500">{orderError}</p>
|
||||
<Button variant="outline" className="w-full" onClick={reset}>
|
||||
重新发起
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (order && !isWechat) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-2 py-10">
|
||||
<Loader2 className="size-8 animate-spin text-blue-500" />
|
||||
<p className="text-sm text-gray-500">正在跳转支付宝支付...</p>
|
||||
<p className="text-xs text-gray-400">
|
||||
如未自动跳转,请检查浏览器是否拦截
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (order && isWechat) {
|
||||
if (paid) {
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-3 py-6">
|
||||
<CheckCircle2 className="size-12 text-emerald-500" />
|
||||
<p className="font-medium text-emerald-600">支付成功</p>
|
||||
<p className="text-xs text-gray-500">余额已更新</p>
|
||||
<Button className="w-full" onClick={() => onOpenChange(false)}>
|
||||
完成
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="space-y-4 py-1">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<img src={wechat} alt="微信支付" className="size-6" />
|
||||
<span className="font-semibold">微信支付 | 收银台</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 rounded-lg bg-gray-50 p-4 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">应付金额</span>
|
||||
<span className="font-bold text-orange-500">
|
||||
¥{Number(order.OrderInfo.PaymentAmount ?? amount).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">收款方</span>
|
||||
<span>聚IP</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">订单号</span>
|
||||
<span className="font-mono text-xs leading-5">
|
||||
{order.OrderInfo.OrderNo}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="rounded-lg border border-gray-200 p-2">
|
||||
<QrCodeImage text={order.PayData} size={200} />
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
请使用微信扫码支付,支付完成后自动确认
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-400">
|
||||
<Loader2 className="size-3 animate-spin" />
|
||||
正在等待支付结果
|
||||
{failCount > 2 && "(网络波动,请稍候)"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">账户充值</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-1">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">充值金额</Label>
|
||||
@@ -101,11 +267,25 @@ export function RechargeDialog({ open, onOpenChange }: RechargeDialogProps) {
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleGoPay}
|
||||
disabled={creating}
|
||||
className="w-full h-11 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||
>
|
||||
充值
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">账户充值</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{renderBody()}
|
||||
|
||||
<div ref={alipayBoxRef} className="hidden" />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
|
||||
@@ -92,9 +92,17 @@ export default function LoginPage() {
|
||||
|
||||
function LoginForm() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// 登录成功后回跳来源页(仅接受站内路径)
|
||||
const rawRedirect = new URLSearchParams(location.search).get("redirect")
|
||||
const redirect =
|
||||
rawRedirect?.startsWith("/") && !rawRedirect.startsWith("//")
|
||||
? rawRedirect
|
||||
: "/"
|
||||
|
||||
const form = useForm<LoginValues>({
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: {
|
||||
@@ -116,7 +124,7 @@ function LoginForm() {
|
||||
toast.success("登录成功", {
|
||||
description: "欢迎回来!",
|
||||
})
|
||||
navigate("/")
|
||||
navigate(redirect, { replace: true })
|
||||
} else {
|
||||
toast.error(resp.message || "登录失败")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { CheckCircle2, Loader2, XCircle } from "lucide-react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
import {
|
||||
checkPayStatus,
|
||||
checkRechargeStatus,
|
||||
createRechargeOrder,
|
||||
type RechargeOrderResult,
|
||||
} from "@/api/order"
|
||||
import { checkPayStatus } from "@/api/order"
|
||||
import { QrCodeImage } from "@/components/qrCodeImage"
|
||||
import RealNameCheck from "@/components/realNameCheck"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -16,10 +10,10 @@ import wechat from "../_assets/wechat.svg"
|
||||
|
||||
const POLL_INTERVAL = 3000
|
||||
|
||||
// 收银台(独立窗口):
|
||||
// - 充值模式 /pay?amount=100&channel=30:页面内下单 /user/CreateOrder,轮询 /user/IsPay
|
||||
// - 购买模式 /pay?mode=order:主页面已下单,读取 localStorage 支付数据,轮询 /product/IsPay
|
||||
// 参考 juipnet Host/Views/User/Index.cshtml(charge)与 Product/buy.cshtml(payCallback/isPay)
|
||||
// 购买收银台(独立窗口):
|
||||
// 主页面下单成功后把支付数据写入 localStorage,本页读取并轮询 /product/IsPay
|
||||
// 参考 juipnet Host/Views/Product/buy.cshtml(payCallback/isPay)
|
||||
// (余额充值已改为个人中心页面内弹层,见 rechargeDialog.tsx)
|
||||
|
||||
type PollState = "paid" | "pending" | "error"
|
||||
|
||||
@@ -57,131 +51,13 @@ function usePayPolling(
|
||||
}
|
||||
|
||||
export default function PayPage() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const mode = searchParams.get("mode")
|
||||
|
||||
return (
|
||||
<>
|
||||
<RealNameCheck />
|
||||
{mode === "order" ? <OrderPay /> : <RechargePay />}
|
||||
<OrderPay />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 充值模式 ====================
|
||||
|
||||
function RechargePay() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const amountParam = Number(searchParams.get("amount"))
|
||||
const channelParam = searchParams.get("channel")
|
||||
|
||||
const valid =
|
||||
Number.isFinite(amountParam) &&
|
||||
amountParam > 0 &&
|
||||
(channelParam === "50" || channelParam === "30")
|
||||
|
||||
const [order, setOrder] = useState<RechargeOrderResult | null>(null)
|
||||
const [orderError, setOrderError] = useState("")
|
||||
const [autoOpened, setAutoOpened] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!valid) return
|
||||
let cancelled = false
|
||||
createRechargeOrder({
|
||||
ChargeAmount: amountParam,
|
||||
PayChannel: Number(channelParam),
|
||||
}).then(res => {
|
||||
if (cancelled) return
|
||||
if (res.success) {
|
||||
setOrder(res.data)
|
||||
} else {
|
||||
setOrderError(res.message)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [valid, amountParam, channelParam])
|
||||
|
||||
useEffect(() => {
|
||||
if (!order?.PayData || autoOpened) return
|
||||
|
||||
// 支付宝:PayData 是支付链接或自动提交表单,直接跳转;
|
||||
// 微信:交给 CashierLayout 判断(链接/表单跳转,weixin:// 扫码内容显示二维码)
|
||||
if (channelParam !== "50") return
|
||||
|
||||
setAutoOpened(true)
|
||||
const payData = order.PayData
|
||||
if (/^https?:\/\//i.test(payData)) {
|
||||
window.location.href = payData
|
||||
} else {
|
||||
document.open()
|
||||
document.write(payData)
|
||||
document.close()
|
||||
}
|
||||
}, [order, channelParam, autoOpened])
|
||||
|
||||
const checkPaid = useCallback(async (orderNo: string): Promise<PollState> => {
|
||||
const res = await checkRechargeStatus(orderNo)
|
||||
if (!res.success) return "error"
|
||||
return res.data ? "paid" : "pending"
|
||||
}, [])
|
||||
|
||||
const { paid, failCount } = usePayPolling(order?.OrderInfo.OrderNo, checkPaid)
|
||||
|
||||
const isAlipay = channelParam === "50"
|
||||
|
||||
if (isAlipay) {
|
||||
return (
|
||||
<div className="min-h-dvh flex items-center justify-center bg-gray-50 p-4">
|
||||
<div className="w-full max-w-sm rounded-xl bg-white p-6 shadow-sm text-center">
|
||||
{orderError ? (
|
||||
<>
|
||||
<XCircle className="mx-auto size-10 text-red-400" />
|
||||
<p className="mt-2 text-sm text-red-500">{orderError}</p>
|
||||
<p className="text-xs text-gray-500">请回到原页面重新发起</p>
|
||||
</>
|
||||
) : !order ? (
|
||||
<>
|
||||
<Loader2 className="mx-auto size-8 animate-spin text-blue-500" />
|
||||
<p className="mt-2 text-sm text-gray-500">正在生成订单...</p>
|
||||
</>
|
||||
) : paid ? (
|
||||
<>
|
||||
<CheckCircle2 className="mx-auto size-12 text-emerald-500" />
|
||||
<p className="mt-2 font-medium text-emerald-600">支付成功</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
订单已生效,可以关闭本页面
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Loader2 className="mx-auto size-8 animate-spin text-blue-500" />
|
||||
<p className="mt-2 text-sm text-gray-500">正在跳转支付宝...</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
如未自动跳转,请检查浏览器是否拦截
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CashierLayout
|
||||
isWechat={true}
|
||||
amount={amountParam}
|
||||
orderNo={order?.OrderInfo.OrderNo}
|
||||
payData={order?.PayData ?? ""}
|
||||
paid={paid}
|
||||
loading={valid && !order && !orderError}
|
||||
error={orderError}
|
||||
failCount={failCount}
|
||||
invalid={!valid}
|
||||
/>
|
||||
)
|
||||
}
|
||||
// ==================== 购买模式 ====================
|
||||
|
||||
function OrderPay() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useRef, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Toaster, toast } from "sonner"
|
||||
import {
|
||||
calcGamePrice,
|
||||
@@ -8,7 +9,9 @@ import {
|
||||
fetchGameList,
|
||||
fetchHttpBalance,
|
||||
} from "@/api/http"
|
||||
import { QrCodeImage } from "@/components/qrCodeImage"
|
||||
import RealNameCheck from "@/components/realNameCheck"
|
||||
import { useProfileStore } from "@/components/stores/profile"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
@@ -101,10 +104,20 @@ const DEFAULT_GAME: GameOrderData = {
|
||||
}
|
||||
|
||||
export default function HttpPage() {
|
||||
const navigate = useNavigate()
|
||||
const token = useProfileStore(state => state.token)
|
||||
const [activeTab, setActiveTab] = useState("prepaid")
|
||||
const [customAmount, setCustomAmount] = useState("")
|
||||
const [customIpAmount, setCustomIpAmount] = useState("")
|
||||
|
||||
// 未登录时跳转登录页,登录后回跳本页(参考 juipnet 产品页做法)
|
||||
const ensureLogin = () => {
|
||||
if (token !== null) return true
|
||||
toast.error("请先登录")
|
||||
navigate(`/login?redirect=${encodeURIComponent("/product/http")}`)
|
||||
return false
|
||||
}
|
||||
|
||||
const [duration, setDuration] = useState(DURATION_OPTIONS[0].value)
|
||||
const [period, setPeriod] = useState(PERIOD_OPTIONS[0].value)
|
||||
const [periodCount, setPeriodCount] = useState(1)
|
||||
@@ -130,6 +143,10 @@ export default function HttpPage() {
|
||||
const isRecharge = true
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// 微信扫码支付弹窗
|
||||
const [qrCodeOpen, setQrCodeOpen] = useState(false)
|
||||
const [qrContent, setQrContent] = useState("")
|
||||
|
||||
// 长效游戏支付(H币)
|
||||
const [gamePayOpen, setGamePayOpen] = useState(false)
|
||||
const [balance, setBalance] = useState(0)
|
||||
@@ -265,6 +282,7 @@ export default function HttpPage() {
|
||||
}
|
||||
|
||||
const openGamePay = async () => {
|
||||
if (!ensureLogin()) return
|
||||
if (lineCountRef.current <= 0) {
|
||||
toast.error("可用IP为0!")
|
||||
return
|
||||
@@ -283,6 +301,7 @@ export default function HttpPage() {
|
||||
}
|
||||
|
||||
const payGameOrder = async () => {
|
||||
if (!ensureLogin()) return
|
||||
setGamePaying(true)
|
||||
const res = await createHttpOrder({
|
||||
order_type: 5,
|
||||
@@ -307,6 +326,14 @@ export default function HttpPage() {
|
||||
} else {
|
||||
toast.error(result.msg || "支付宝支付数据异常")
|
||||
}
|
||||
} else if (result.code === 3) {
|
||||
if (result.data) {
|
||||
setQrContent(result.data)
|
||||
setQrCodeOpen(true)
|
||||
setGamePayOpen(false)
|
||||
} else {
|
||||
toast.error(result.msg || "微信支付数据异常")
|
||||
}
|
||||
} else {
|
||||
toast.error(result.msg || "支付失败")
|
||||
}
|
||||
@@ -351,18 +378,52 @@ export default function HttpPage() {
|
||||
const totalPrice = calculatePrice()
|
||||
|
||||
// 处理支付
|
||||
const handlePay = () => {
|
||||
const handlePay = async () => {
|
||||
if (!ensureLogin()) return
|
||||
setIsLoading(true)
|
||||
// 模拟支付请求
|
||||
setTimeout(() => {
|
||||
const res = await createHttpOrder({
|
||||
order_type: 1,
|
||||
money: payAmount,
|
||||
pay_type: Number(payMethod),
|
||||
data: {},
|
||||
})
|
||||
setIsLoading(false)
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
return
|
||||
}
|
||||
const result = res.data
|
||||
if (result.code === 1) {
|
||||
setPayDialogOpen(false)
|
||||
toast.success(`支付成功!支付金额:¥${payAmount.toFixed(2)}`)
|
||||
}, 1500)
|
||||
toast.success(result.msg || "充值成功")
|
||||
} else if (result.code === 2) {
|
||||
if (result.data && alipayBoxRef.current) {
|
||||
alipayBoxRef.current.innerHTML = result.data
|
||||
alipayBoxRef.current.querySelector("form")?.submit()
|
||||
setPayDialogOpen(false)
|
||||
} else {
|
||||
toast.error(result.msg || "支付宝支付数据异常")
|
||||
}
|
||||
} else if (result.code === 3) {
|
||||
if (result.data) {
|
||||
setQrContent(result.data)
|
||||
setQrCodeOpen(true)
|
||||
setPayDialogOpen(false)
|
||||
} else {
|
||||
toast.error(result.msg || "微信支付数据异常")
|
||||
}
|
||||
} else {
|
||||
toast.error(result.msg || "支付失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 打开支付弹窗
|
||||
// 打开支付弹窗(仅预储值 tab 支持支付宝/微信在线充值)
|
||||
const openPayDialog = (amount?: number) => {
|
||||
if (!ensureLogin()) return
|
||||
if (activeTab !== "prepaid") {
|
||||
toast.error("该套餐暂不支持在线支付,请联系客服购买")
|
||||
return
|
||||
}
|
||||
const effectivePrice = Number(amount ?? totalPrice)
|
||||
if (effectivePrice <= 0) {
|
||||
toast.error("请输入有效的充值金额")
|
||||
@@ -1030,6 +1091,30 @@ export default function HttpPage() {
|
||||
|
||||
{/* 支付宝表单渲染容器 */}
|
||||
<div ref={alipayBoxRef} className="hidden" />
|
||||
|
||||
{/* 微信扫码支付弹窗 */}
|
||||
<Dialog open={qrCodeOpen} onOpenChange={setQrCodeOpen}>
|
||||
<DialogContent className="sm:max-w-xs">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">微信扫码支付</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center gap-3 pb-2">
|
||||
<div className="rounded-lg border border-gray-200 p-2">
|
||||
<QrCodeImage text={qrContent} size={220} />
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
请使用微信扫码支付
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => setQrCodeOpen(false)}
|
||||
>
|
||||
已支付完成
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Wrap>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,8 @@ type ApiResponse<T = undefined> =
|
||||
success: false
|
||||
status?: number
|
||||
message: string
|
||||
// 后端业务码(非 10000 时透传),如 /user/IsPay 的 Code 0/1 即支付状态
|
||||
code?: number
|
||||
}
|
||||
| {
|
||||
success: true
|
||||
|
||||
@@ -26,7 +26,8 @@ export type HttpOrderInfo = {
|
||||
order_type: number
|
||||
money: number
|
||||
pay_type: number
|
||||
data: GameOrderData
|
||||
// 预储值(order_type=1)下单时 data 为空对象
|
||||
data: GameOrderData | Record<string, never>
|
||||
}
|
||||
|
||||
export type CreateOrderResult = {
|
||||
|
||||
Reference in New Issue
Block a user