2026-01-09 11:58:08 +08:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
|
|
import type React from "react"
|
2026-01-21 15:49:12 +08:00
|
|
|
|
import { useState, useEffect, useCallback, useRef } from "react"
|
|
|
|
|
|
import { X, CheckCircle, Bitcoin, Globe, Copy, Check, QrCode, Shield, Users, Loader2, AlertCircle } from "lucide-react"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
import { useStore } from "@/lib/store"
|
2026-01-21 15:49:12 +08:00
|
|
|
|
import QRCode from "qrcode"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
|
|
|
|
|
const WechatIcon = () => (
|
|
|
|
|
|
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
|
|
|
|
|
|
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.269-.03-.406-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type PaymentMethod = "wechat" | "alipay" | "usdt" | "paypal" | "stripe" | "bank"
|
|
|
|
|
|
|
|
|
|
|
|
interface PaymentModalProps {
|
|
|
|
|
|
isOpen: boolean
|
|
|
|
|
|
onClose: () => void
|
|
|
|
|
|
type: "section" | "fullbook"
|
|
|
|
|
|
sectionId?: string
|
|
|
|
|
|
sectionTitle?: string
|
|
|
|
|
|
amount: number
|
|
|
|
|
|
onSuccess: () => void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
// 支付状态类型
|
|
|
|
|
|
type PaymentState = "idle" | "creating" | "paying" | "polling" | "success" | "error"
|
|
|
|
|
|
|
2026-01-09 11:58:08 +08:00
|
|
|
|
export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, amount, onSuccess }: PaymentModalProps) {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("wechat")
|
|
|
|
|
|
const [paymentState, setPaymentState] = useState<PaymentState>("idle")
|
|
|
|
|
|
const [errorMessage, setErrorMessage] = useState("")
|
2026-01-09 11:58:08 +08:00
|
|
|
|
const [showQRCode, setShowQRCode] = useState(false)
|
2026-01-21 15:49:12 +08:00
|
|
|
|
const [qrCodeDataUrl, setQrCodeDataUrl] = useState("")
|
|
|
|
|
|
const [paymentUrl, setPaymentUrl] = useState("")
|
|
|
|
|
|
const [orderSn, setOrderSn] = useState("")
|
|
|
|
|
|
const [tradeSn, setTradeSn] = useState("")
|
|
|
|
|
|
const [currentGateway, setCurrentGateway] = useState("") // 当前支付网关
|
2026-01-09 11:58:08 +08:00
|
|
|
|
const [copied, setCopied] = useState(false)
|
2026-01-21 15:49:12 +08:00
|
|
|
|
|
|
|
|
|
|
const pollingRef = useRef<NodeJS.Timeout | null>(null)
|
|
|
|
|
|
const pollingCountRef = useRef(0)
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
|
|
|
|
|
const { purchaseSection, purchaseFullBook, user, settings } = useStore()
|
|
|
|
|
|
|
|
|
|
|
|
const paymentConfig = settings?.paymentMethods || {
|
|
|
|
|
|
wechat: { enabled: true, qrCode: "", account: "", groupQrCode: "" },
|
|
|
|
|
|
alipay: { enabled: true, qrCode: "", account: "" },
|
|
|
|
|
|
usdt: { enabled: true, network: "TRC20", address: "", exchangeRate: 7.2 },
|
|
|
|
|
|
paypal: { enabled: false, email: "", exchangeRate: 7.2 },
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const usdtAmount = (amount / (paymentConfig.usdt?.exchangeRate || 7.2)).toFixed(2)
|
|
|
|
|
|
const paypalAmount = (amount / (paymentConfig.paypal?.exchangeRate || 7.2)).toFixed(2)
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
// 清理轮询
|
|
|
|
|
|
const clearPolling = useCallback(() => {
|
|
|
|
|
|
if (pollingRef.current) {
|
|
|
|
|
|
clearInterval(pollingRef.current)
|
|
|
|
|
|
pollingRef.current = null
|
|
|
|
|
|
}
|
|
|
|
|
|
pollingCountRef.current = 0
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
// 重置状态
|
|
|
|
|
|
const resetState = useCallback(() => {
|
|
|
|
|
|
setPaymentState("idle")
|
|
|
|
|
|
setShowQRCode(false)
|
|
|
|
|
|
setQrCodeDataUrl("")
|
|
|
|
|
|
setPaymentUrl("")
|
|
|
|
|
|
setOrderSn("")
|
|
|
|
|
|
setTradeSn("")
|
|
|
|
|
|
setCurrentGateway("")
|
|
|
|
|
|
setErrorMessage("")
|
|
|
|
|
|
clearPolling()
|
|
|
|
|
|
}, [clearPolling])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
|
resetState()
|
|
|
|
|
|
}
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
clearPolling()
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isOpen, resetState, clearPolling])
|
|
|
|
|
|
|
|
|
|
|
|
// 创建订单并获取支付参数
|
|
|
|
|
|
const createPaymentOrder = async () => {
|
|
|
|
|
|
setPaymentState("creating")
|
|
|
|
|
|
setErrorMessage("")
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch("/api/payment/create-order", {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
userId: user?.id || "anonymous",
|
|
|
|
|
|
type,
|
|
|
|
|
|
sectionId,
|
|
|
|
|
|
sectionTitle,
|
|
|
|
|
|
amount,
|
|
|
|
|
|
paymentMethod,
|
|
|
|
|
|
referralCode: user?.referredBy,
|
|
|
|
|
|
}),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json()
|
|
|
|
|
|
|
|
|
|
|
|
if (result.code !== 200) {
|
|
|
|
|
|
throw new Error(result.message || "创建订单失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const { orderSn: newOrderSn, tradeSn: newTradeSn, paymentData, gateway } = result.data
|
|
|
|
|
|
|
|
|
|
|
|
setOrderSn(newOrderSn)
|
|
|
|
|
|
setTradeSn(newTradeSn)
|
|
|
|
|
|
|
|
|
|
|
|
// 保存网关信息用于后续查询
|
|
|
|
|
|
const gatewayId = gateway || (paymentMethod === "wechat" ? "wechat_native" : "alipay_wap")
|
|
|
|
|
|
setCurrentGateway(gatewayId)
|
|
|
|
|
|
|
|
|
|
|
|
// 根据支付方式处理不同的返回数据
|
|
|
|
|
|
if (paymentData.type === "url") {
|
|
|
|
|
|
// URL类型:跳转支付(支付宝WAP/WEB)
|
|
|
|
|
|
setPaymentUrl(paymentData.payload)
|
|
|
|
|
|
setShowQRCode(true)
|
|
|
|
|
|
setPaymentState("paying")
|
|
|
|
|
|
|
|
|
|
|
|
// 打开支付页面
|
|
|
|
|
|
window.open(paymentData.payload, "_blank")
|
|
|
|
|
|
|
|
|
|
|
|
// 开始轮询支付状态,传递网关信息
|
|
|
|
|
|
startPolling(newTradeSn, gatewayId)
|
|
|
|
|
|
} else if (paymentData.type === "qrcode") {
|
|
|
|
|
|
// 二维码类型:扫码支付(微信Native/支付宝QR)
|
|
|
|
|
|
const qrUrl = paymentData.payload
|
|
|
|
|
|
|
|
|
|
|
|
// 生成二维码图片
|
|
|
|
|
|
const dataUrl = await QRCode.toDataURL(qrUrl, {
|
|
|
|
|
|
width: 200,
|
|
|
|
|
|
margin: 2,
|
|
|
|
|
|
color: {
|
|
|
|
|
|
dark: "#000000",
|
|
|
|
|
|
light: "#ffffff",
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
setQrCodeDataUrl(dataUrl)
|
|
|
|
|
|
setPaymentUrl(qrUrl)
|
|
|
|
|
|
setShowQRCode(true)
|
|
|
|
|
|
setPaymentState("paying")
|
|
|
|
|
|
|
|
|
|
|
|
// 开始轮询支付状态,传递网关信息
|
|
|
|
|
|
startPolling(newTradeSn, gatewayId)
|
|
|
|
|
|
} else if (paymentData.type === "json") {
|
|
|
|
|
|
// JSON类型:JSAPI支付(需要调用JS SDK)
|
|
|
|
|
|
console.log("JSAPI支付参数:", paymentData.payload)
|
|
|
|
|
|
// 这里需要调用微信JS SDK
|
|
|
|
|
|
setPaymentState("error")
|
|
|
|
|
|
setErrorMessage("JSAPI支付需要在微信内打开")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("创建订单失败:", error)
|
|
|
|
|
|
setPaymentState("error")
|
|
|
|
|
|
setErrorMessage(error instanceof Error ? error.message : "创建订单失败,请重试")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 轮询支付状态
|
|
|
|
|
|
const startPolling = (tradeSnToQuery: string, gateway?: string) => {
|
|
|
|
|
|
setPaymentState("polling")
|
|
|
|
|
|
pollingCountRef.current = 0
|
|
|
|
|
|
|
|
|
|
|
|
pollingRef.current = setInterval(async () => {
|
|
|
|
|
|
pollingCountRef.current++
|
|
|
|
|
|
|
|
|
|
|
|
// 最多轮询60次(5分钟)
|
|
|
|
|
|
if (pollingCountRef.current > 60) {
|
|
|
|
|
|
clearPolling()
|
|
|
|
|
|
setPaymentState("error")
|
|
|
|
|
|
setErrorMessage("支付超时,请重新发起支付")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 构建查询URL,包含网关参数以提高查询准确性
|
|
|
|
|
|
let queryUrl = `/api/payment/query?tradeSn=${tradeSnToQuery}`
|
|
|
|
|
|
if (gateway) {
|
|
|
|
|
|
queryUrl += `&gateway=${gateway}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(queryUrl)
|
|
|
|
|
|
const result = await response.json()
|
|
|
|
|
|
|
|
|
|
|
|
if (result.code === 200 && result.data) {
|
|
|
|
|
|
const { status } = result.data
|
|
|
|
|
|
|
|
|
|
|
|
if (status === "paid") {
|
|
|
|
|
|
// 支付成功
|
|
|
|
|
|
clearPolling()
|
|
|
|
|
|
await handlePaymentSuccess()
|
|
|
|
|
|
} else if (status === "closed" || status === "refunded") {
|
|
|
|
|
|
// 订单已关闭
|
|
|
|
|
|
clearPolling()
|
|
|
|
|
|
setPaymentState("error")
|
|
|
|
|
|
setErrorMessage("订单已关闭")
|
|
|
|
|
|
}
|
|
|
|
|
|
// paying状态继续轮询
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("查询支付状态失败:", error)
|
|
|
|
|
|
// 查询失败继续轮询
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000) // 每5秒查询一次
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理支付成功
|
|
|
|
|
|
const handlePaymentSuccess = async () => {
|
|
|
|
|
|
setPaymentState("success")
|
|
|
|
|
|
|
|
|
|
|
|
// 调用store更新购买状态
|
|
|
|
|
|
let success = false
|
|
|
|
|
|
if (type === "section" && sectionId) {
|
|
|
|
|
|
success = await purchaseSection(sectionId, sectionTitle, paymentMethod)
|
|
|
|
|
|
} else if (type === "fullbook") {
|
|
|
|
|
|
success = await purchaseFullBook(paymentMethod)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开社群二维码
|
|
|
|
|
|
const groupUrl = paymentConfig.wechat?.groupQrCode
|
|
|
|
|
|
if (groupUrl) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
window.open(groupUrl, "_blank")
|
|
|
|
|
|
}, 800)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭弹窗
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
onSuccess()
|
|
|
|
|
|
onClose()
|
|
|
|
|
|
resetState()
|
|
|
|
|
|
}, 2500)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 手动确认支付(用于轮询失效的情况)
|
|
|
|
|
|
const handleManualConfirm = async () => {
|
|
|
|
|
|
if (!tradeSn) return
|
|
|
|
|
|
|
|
|
|
|
|
setPaymentState("polling")
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 构建查询URL,包含网关参数
|
|
|
|
|
|
let queryUrl = `/api/payment/query?tradeSn=${tradeSn}`
|
|
|
|
|
|
if (currentGateway) {
|
|
|
|
|
|
queryUrl += `&gateway=${currentGateway}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(queryUrl)
|
|
|
|
|
|
const result = await response.json()
|
|
|
|
|
|
|
|
|
|
|
|
if (result.code === 200 && result.data?.status === "paid") {
|
|
|
|
|
|
await handlePaymentSuccess()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setErrorMessage("未检测到支付,请确认是否已完成支付")
|
|
|
|
|
|
setPaymentState("paying")
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
setErrorMessage("查询支付状态失败,请稍后重试")
|
|
|
|
|
|
setPaymentState("paying")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 11:58:08 +08:00
|
|
|
|
const handleCopyAddress = (address: string) => {
|
|
|
|
|
|
navigator.clipboard.writeText(address)
|
|
|
|
|
|
setCopied(true)
|
|
|
|
|
|
setTimeout(() => setCopied(false), 2000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
// 处理开始支付
|
2026-01-09 11:58:08 +08:00
|
|
|
|
const handlePayment = async () => {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
// USDT和PayPal使用旧的手动确认流程
|
|
|
|
|
|
if (paymentMethod === "usdt" || paymentMethod === "paypal") {
|
|
|
|
|
|
setShowQRCode(true)
|
|
|
|
|
|
return
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
2026-01-21 15:49:12 +08:00
|
|
|
|
|
|
|
|
|
|
// 微信和支付宝使用新的API流程
|
|
|
|
|
|
await createPaymentOrder()
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
// USDT/PayPal手动确认
|
|
|
|
|
|
const handleCryptoConfirm = async () => {
|
|
|
|
|
|
setPaymentState("creating")
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟确认支付(实际需要人工审核)
|
2026-01-09 11:58:08 +08:00
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
|
|
|
|
|
|
|
|
|
|
let success = false
|
|
|
|
|
|
if (type === "section" && sectionId) {
|
|
|
|
|
|
success = await purchaseSection(sectionId, sectionTitle, paymentMethod)
|
|
|
|
|
|
} else if (type === "fullbook") {
|
|
|
|
|
|
success = await purchaseFullBook(paymentMethod)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (success) {
|
2026-01-21 15:49:12 +08:00
|
|
|
|
await handlePaymentSuccess()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setPaymentState("error")
|
|
|
|
|
|
setErrorMessage("确认支付失败,请联系客服")
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!isOpen) return null
|
|
|
|
|
|
|
|
|
|
|
|
const paymentMethods: {
|
|
|
|
|
|
id: PaymentMethod
|
|
|
|
|
|
name: string
|
|
|
|
|
|
icon: React.ReactNode
|
|
|
|
|
|
color: string
|
2026-01-09 12:24:15 +08:00
|
|
|
|
iconBg: string
|
2026-01-09 11:58:08 +08:00
|
|
|
|
enabled: boolean
|
|
|
|
|
|
extra?: string
|
|
|
|
|
|
}[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "wechat",
|
|
|
|
|
|
name: "微信支付",
|
|
|
|
|
|
icon: <WechatIcon />,
|
2026-01-09 12:24:15 +08:00
|
|
|
|
color: "#07C160",
|
|
|
|
|
|
iconBg: "rgba(7, 193, 96, 0.15)",
|
2026-01-09 11:58:08 +08:00
|
|
|
|
enabled: paymentConfig.wechat?.enabled ?? true,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "usdt",
|
|
|
|
|
|
name: `USDT (${paymentConfig.usdt?.network || "TRC20"})`,
|
|
|
|
|
|
icon: <Bitcoin className="w-5 h-5" />,
|
2026-01-09 12:24:15 +08:00
|
|
|
|
color: "#26A17B",
|
|
|
|
|
|
iconBg: "rgba(38, 161, 123, 0.15)",
|
2026-01-21 15:49:12 +08:00
|
|
|
|
enabled: paymentConfig.usdt?.enabled ?? false,
|
2026-01-09 11:58:08 +08:00
|
|
|
|
extra: `≈ $${usdtAmount}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
id: "paypal",
|
|
|
|
|
|
name: "PayPal",
|
|
|
|
|
|
icon: <Globe className="w-5 h-5" />,
|
2026-01-09 12:24:15 +08:00
|
|
|
|
color: "#003087",
|
|
|
|
|
|
iconBg: "rgba(0, 48, 135, 0.15)",
|
2026-01-09 11:58:08 +08:00
|
|
|
|
enabled: paymentConfig.paypal?.enabled ?? false,
|
|
|
|
|
|
extra: `≈ $${paypalAmount}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const availableMethods = paymentMethods.filter((m) => m.enabled)
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
// 二维码/详情页面
|
2026-01-09 11:58:08 +08:00
|
|
|
|
if (showQRCode) {
|
|
|
|
|
|
const isCrypto = paymentMethod === "usdt"
|
|
|
|
|
|
const isPaypal = paymentMethod === "paypal"
|
|
|
|
|
|
const isWechat = paymentMethod === "wechat"
|
|
|
|
|
|
const isAlipay = paymentMethod === "alipay"
|
|
|
|
|
|
|
|
|
|
|
|
let address = ""
|
|
|
|
|
|
let displayAmount = `¥${amount.toFixed(2)}`
|
|
|
|
|
|
let title = "扫码支付"
|
2026-01-21 15:49:12 +08:00
|
|
|
|
let hint = "支付完成后,系统将自动确认"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
|
|
|
|
|
|
if (isCrypto) {
|
|
|
|
|
|
address = paymentConfig.usdt?.address || ""
|
|
|
|
|
|
displayAmount = `$${usdtAmount} USDT`
|
|
|
|
|
|
title = "USDT支付"
|
|
|
|
|
|
hint = "请转账到以下地址,完成后点击确认"
|
|
|
|
|
|
} else if (isPaypal) {
|
|
|
|
|
|
address = paymentConfig.paypal?.email || ""
|
|
|
|
|
|
displayAmount = `$${paypalAmount} USD`
|
|
|
|
|
|
title = "PayPal支付"
|
|
|
|
|
|
hint = "请转账到以下PayPal账户"
|
|
|
|
|
|
} else if (isWechat) {
|
|
|
|
|
|
title = "微信支付"
|
2026-01-21 15:49:12 +08:00
|
|
|
|
hint = "请使用微信扫描二维码支付"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
} else if (isAlipay) {
|
|
|
|
|
|
title = "支付宝支付"
|
2026-01-21 15:49:12 +08:00
|
|
|
|
hint = paymentUrl?.startsWith("http") ? "已打开支付页面,请在新窗口完成支付" : "请使用支付宝扫描二维码支付"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
|
|
|
|
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-md modal-overlay" onClick={onClose} />
|
|
|
|
|
|
<div className="relative w-full sm:max-w-md glass-modal overflow-hidden safe-bottom modal-content sm:m-4">
|
|
|
|
|
|
{/* 顶部把手 - 仅移动端 */}
|
|
|
|
|
|
<div className="flex justify-center pt-3 pb-2 sm:hidden">
|
|
|
|
|
|
<div className="w-9 h-1 rounded-full bg-[var(--app-text-tertiary)]" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
className="absolute top-4 right-4 w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center text-[var(--app-text-secondary)] hover:text-white z-10 touch-feedback"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="w-4 h-4" />
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="p-6 pt-4 sm:pt-10">
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{/* 标题 */}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<h3 className="text-xl font-semibold text-white text-center mb-2">{title}</h3>
|
|
|
|
|
|
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{/* 金额显示 */}
|
|
|
|
|
|
<div className="text-center mb-6">
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<p className="text-4xl font-bold text-[var(--app-brand)] glow-text">{displayAmount}</p>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{/* 错误提示 */}
|
|
|
|
|
|
{errorMessage && (
|
|
|
|
|
|
<div className="glass-card p-4 mb-4 border-red-500/30">
|
|
|
|
|
|
<div className="flex items-center gap-3 text-red-400">
|
|
|
|
|
|
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
|
|
|
|
|
<p className="text-sm">{errorMessage}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* QR Code Display - 微信/支付宝 */}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{(isWechat || isAlipay) && (
|
|
|
|
|
|
<div className="flex flex-col items-center mb-6">
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="w-52 h-52 bg-white rounded-2xl p-4 mb-4 flex items-center justify-center shadow-lg">
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{paymentState === "creating" ? (
|
|
|
|
|
|
<div className="flex flex-col items-center text-gray-400">
|
|
|
|
|
|
<Loader2 className="w-12 h-12 animate-spin mb-2 text-[var(--app-brand)]" />
|
|
|
|
|
|
<span className="text-sm text-gray-600">正在生成二维码...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : qrCodeDataUrl ? (
|
2026-01-09 11:58:08 +08:00
|
|
|
|
<img
|
2026-01-21 15:49:12 +08:00
|
|
|
|
src={qrCodeDataUrl}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
alt="支付二维码"
|
2026-01-09 12:24:15 +08:00
|
|
|
|
className="w-full h-full object-contain rounded-lg"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
/>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
) : paymentUrl?.startsWith("http") ? (
|
|
|
|
|
|
<div className="flex flex-col items-center text-gray-600">
|
|
|
|
|
|
<CheckCircle className="w-12 h-12 text-green-500 mb-2" />
|
|
|
|
|
|
<span className="text-sm text-center">已在新窗口打开支付页面</span>
|
|
|
|
|
|
</div>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="flex flex-col items-center text-gray-400">
|
|
|
|
|
|
<QrCode className="w-16 h-16 mb-2" />
|
2026-01-21 15:49:12 +08:00
|
|
|
|
<span className="text-sm text-center">二维码加载中...</span>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<p className="text-[var(--app-text-tertiary)] text-sm">{hint}</p>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 轮询状态指示 */}
|
|
|
|
|
|
{paymentState === "polling" && (
|
|
|
|
|
|
<div className="flex items-center gap-2 mt-3 text-[var(--app-brand)]">
|
|
|
|
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
|
|
|
|
<span className="text-sm">正在等待支付结果...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Crypto/PayPal Address */}
|
|
|
|
|
|
{(isCrypto || isPaypal) && (
|
|
|
|
|
|
<div className="mb-6">
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="glass-card p-4">
|
|
|
|
|
|
<p className="text-[var(--app-text-tertiary)] text-xs mb-2">
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{isCrypto ? `收款地址 (${paymentConfig.usdt?.network})` : "PayPal账户"}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<p className="text-white text-sm break-all flex-1 font-mono bg-[var(--app-bg-secondary)] p-3 rounded-lg">
|
|
|
|
|
|
{address || "请在后台配置收款地址"}
|
|
|
|
|
|
</p>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{address && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => handleCopyAddress(address)}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
className="w-10 h-10 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center text-[var(--app-brand)] touch-feedback"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
>
|
|
|
|
|
|
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<p className="text-[var(--app-text-tertiary)] text-sm mt-3 text-center">{hint}</p>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 提示信息 */}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="glass-card p-4 mb-6 border-[var(--app-brand)]/20">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="w-10 h-10 rounded-xl bg-[var(--app-brand-light)] flex items-center justify-center">
|
|
|
|
|
|
<Shield className="w-5 h-5 text-[var(--app-brand)]" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-[var(--app-text-secondary)] text-sm flex-1">
|
|
|
|
|
|
支付完成后,系统将自动开通阅读权限
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Action Buttons */}
|
|
|
|
|
|
<div className="flex gap-3">
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<button
|
2026-01-21 15:49:12 +08:00
|
|
|
|
onClick={() => {
|
|
|
|
|
|
clearPolling()
|
|
|
|
|
|
setShowQRCode(false)
|
|
|
|
|
|
setPaymentState("idle")
|
|
|
|
|
|
setErrorMessage("")
|
|
|
|
|
|
}}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
className="btn-ios-secondary flex-1"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
>
|
|
|
|
|
|
返回
|
2026-01-09 12:24:15 +08:00
|
|
|
|
</button>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{(isCrypto || isPaypal) ? (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleCryptoConfirm}
|
|
|
|
|
|
disabled={paymentState === "creating"}
|
|
|
|
|
|
className="btn-ios flex-1 glow disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
{paymentState === "creating" ? (
|
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
|
|
|
|
处理中...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
"我已支付"
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleManualConfirm}
|
|
|
|
|
|
disabled={paymentState === "polling"}
|
|
|
|
|
|
className="btn-ios flex-1 glow disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
{paymentState === "polling" ? (
|
|
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
|
|
|
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
|
|
|
|
查询中...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
"已完成支付"
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 支付成功页面
|
2026-01-21 15:49:12 +08:00
|
|
|
|
if (paymentState === "success") {
|
2026-01-09 11:58:08 +08:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-md modal-overlay" />
|
|
|
|
|
|
<div className="relative w-full max-w-sm glass-modal overflow-hidden modal-content">
|
2026-01-09 11:58:08 +08:00
|
|
|
|
<div className="p-8 text-center">
|
2026-01-09 12:24:15 +08:00
|
|
|
|
{/* 成功动画 */}
|
|
|
|
|
|
<div className="w-24 h-24 mx-auto mb-6 rounded-full bg-[var(--app-brand-light)] flex items-center justify-center">
|
|
|
|
|
|
<CheckCircle className="w-12 h-12 text-[var(--app-brand)]" />
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<h3 className="text-2xl font-semibold text-white mb-2">支付成功</h3>
|
|
|
|
|
|
<p className="text-[var(--app-text-secondary)] mb-4">
|
|
|
|
|
|
{type === "fullbook" ? "您已解锁全部内容" : "您已解锁本节内容"}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{paymentConfig.wechat?.groupQrCode && (
|
|
|
|
|
|
<div className="glass-card p-4 mt-4">
|
|
|
|
|
|
<div className="flex items-center justify-center gap-2 text-[#07C160]">
|
|
|
|
|
|
<Users className="w-5 h-5" />
|
|
|
|
|
|
<span className="text-sm">正在跳转到读者社群...</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-21 15:49:12 +08:00
|
|
|
|
// 主支付选择页面
|
2026-01-09 11:58:08 +08:00
|
|
|
|
return (
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center">
|
|
|
|
|
|
<div className="absolute inset-0 bg-black/60 backdrop-blur-md modal-overlay" onClick={onClose} />
|
|
|
|
|
|
<div className="relative w-full sm:max-w-md glass-modal overflow-hidden safe-bottom modal-content sm:m-4 max-h-[90vh] overflow-y-auto scrollbar-hide">
|
|
|
|
|
|
{/* 顶部把手 - 仅移动端 */}
|
|
|
|
|
|
<div className="flex justify-center pt-3 pb-2 sm:hidden sticky top-0 bg-transparent">
|
|
|
|
|
|
<div className="w-9 h-1 rounded-full bg-[var(--app-text-tertiary)]" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-01-09 11:58:08 +08:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={onClose}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
className="absolute top-4 right-4 w-8 h-8 rounded-full bg-[var(--app-bg-secondary)] flex items-center justify-center text-[var(--app-text-secondary)] hover:text-white z-10 touch-feedback"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
>
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<X className="w-4 h-4" />
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Header */}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="px-6 pt-2 sm:pt-10 pb-4">
|
|
|
|
|
|
<h3 className="text-xl font-semibold text-white mb-1">确认支付</h3>
|
|
|
|
|
|
<p className="text-[var(--app-text-tertiary)] text-sm">
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{type === "fullbook" ? "购买整本书,解锁全部内容" : `购买: ${sectionTitle}`}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Amount */}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="px-6 py-6 text-center border-y border-[var(--app-separator)]">
|
|
|
|
|
|
<p className="text-[var(--app-text-tertiary)] text-sm mb-2">支付金额</p>
|
|
|
|
|
|
<p className="text-5xl font-bold text-white">¥{amount.toFixed(2)}</p>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
{(paymentMethod === "usdt" || paymentMethod === "paypal") && (
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<p className="text-[var(--app-brand)] text-sm mt-2">
|
|
|
|
|
|
≈ ${paymentMethod === "usdt" ? usdtAmount : paypalAmount} USD
|
|
|
|
|
|
</p>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
)}
|
|
|
|
|
|
{user?.referredBy && (
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="inline-flex items-center gap-2 mt-4 px-4 py-2 rounded-full bg-[var(--app-brand-light)]">
|
|
|
|
|
|
<Users className="w-4 h-4 text-[var(--app-brand)]" />
|
|
|
|
|
|
<span className="text-[var(--app-brand)] text-sm">
|
|
|
|
|
|
推荐人将获得 {settings?.distributorShare || 90}% 返佣
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Payment Methods */}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="p-6">
|
|
|
|
|
|
<p className="text-[var(--app-text-tertiary)] text-sm mb-4">选择支付方式</p>
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{availableMethods.map((method) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={method.id}
|
|
|
|
|
|
onClick={() => setPaymentMethod(method.id)}
|
|
|
|
|
|
className={`w-full p-4 rounded-xl flex items-center gap-4 transition-all touch-feedback ${
|
|
|
|
|
|
paymentMethod === method.id
|
|
|
|
|
|
? "glass-card-light border-[var(--app-brand)]/30"
|
|
|
|
|
|
: "glass-card hover:border-[var(--glass-border-light)]"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="w-12 h-12 rounded-xl flex items-center justify-center"
|
|
|
|
|
|
style={{ backgroundColor: method.iconBg, color: method.color }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{method.icon}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 text-left">
|
|
|
|
|
|
<span className="text-white font-medium">{method.name}</span>
|
|
|
|
|
|
{method.extra && (
|
|
|
|
|
|
<span className="text-[var(--app-text-tertiary)] text-sm ml-2">{method.extra}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{paymentMethod === method.id && (
|
|
|
|
|
|
<div className="w-6 h-6 rounded-full bg-[var(--app-brand)] flex items-center justify-center">
|
|
|
|
|
|
<Check className="w-4 h-4 text-white" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{availableMethods.length === 0 && (
|
|
|
|
|
|
<p className="text-[var(--app-text-tertiary)] text-center py-8">暂无可用支付方式</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Submit Button */}
|
|
|
|
|
|
<div className="p-6 pt-0">
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<button
|
2026-01-09 11:58:08 +08:00
|
|
|
|
onClick={handlePayment}
|
2026-01-21 15:49:12 +08:00
|
|
|
|
disabled={paymentState === "creating" || availableMethods.length === 0}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
className="btn-ios w-full glow text-lg disabled:opacity-50"
|
2026-01-09 11:58:08 +08:00
|
|
|
|
>
|
2026-01-21 15:49:12 +08:00
|
|
|
|
{paymentState === "creating" ? (
|
2026-01-09 12:24:15 +08:00
|
|
|
|
<div className="flex items-center justify-center gap-2">
|
2026-01-21 15:49:12 +08:00
|
|
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
|
|
|
|
正在创建订单...
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
`确认支付 ¥${amount.toFixed(2)}`
|
|
|
|
|
|
)}
|
2026-01-09 12:24:15 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
<p className="text-[var(--app-text-tertiary)] text-xs text-center mt-4">
|
|
|
|
|
|
支付即表示同意《用户协议》和《隐私政策》
|
|
|
|
|
|
</p>
|
2026-01-09 11:58:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|