feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API

主要更新:
1. 按H5网页端完全重构匹配功能(match页面)
   - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募
   - 资源对接等类型弹出手机号/微信号输入框
   - 去掉重新匹配按钮,改为返回按钮

2. 修复所有卡片对齐和宽度问题
   - 目录页附录卡片居中
   - 首页阅读进度卡片满宽度
   - 我的页面菜单卡片对齐
   - 推广中心分享卡片统一宽度

3. 修复目录页图标和文字对齐
   - section-icon固定40rpx宽高
   - section-title与图标垂直居中

4. 更新真实完整文章标题(62篇)
   - 从book目录读取真实markdown文件名
   - 替换之前的简化标题

5. 新增文章数据API
   - /api/db/chapters - 获取完整书籍结构
   - 支持按ID获取单篇文章内容
This commit is contained in:
卡若
2026-01-21 15:49:12 +08:00
parent 1ee25e3dab
commit b60edb3d47
197 changed files with 34430 additions and 7345 deletions

View File

@@ -1,9 +1,10 @@
"use client"
import type React from "react"
import { useState, useEffect } from "react"
import { X, CheckCircle, Bitcoin, Globe, Copy, Check, QrCode, Shield, Users } from "lucide-react"
import { useState, useEffect, useCallback, useRef } from "react"
import { X, CheckCircle, Bitcoin, Globe, Copy, Check, QrCode, Shield, Users, Loader2, AlertCircle } from "lucide-react"
import { useStore } from "@/lib/store"
import QRCode from "qrcode"
const WechatIcon = () => (
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
@@ -11,12 +12,6 @@ const WechatIcon = () => (
</svg>
)
const AlipayIcon = () => (
<svg viewBox="0 0 24 24" className="w-5 h-5" fill="currentColor">
<path d="M20.422 13.066c-.198-.07-.405-.137-.62-.202.107-.263.204-.534.29-.814h-3.32v-.93h3.927v-.627h-3.927v-1.18h-1.637c.07-.138.131-.28.184-.425l-1.483-.326a4.091 4.091 0 0 1-.405.75h-2.78v1.181H7.72v.627h2.932v.93H7.205v.652h5.784a9.296 9.296 0 0 1-.608.814 13.847 13.847 0 0 0-2.76-.93l-.483.652c1.038.273 1.96.608 2.766 1.008a8.483 8.483 0 0 1-3.603 1.484l.43.652c1.71-.378 3.103-1.03 4.18-1.957.665.395 1.223.835 1.67 1.32l.608-.652c-.44-.43-.984-.836-1.637-1.215a9.6 9.6 0 0 0 .72-.93c.182-.264.345-.53.488-.798.587.168 1.12.35 1.598.544 1.956.8 2.82 1.614 2.82 2.665 0 .727-.587 1.277-2.21 1.277-1.193 0-2.524-.203-3.996-.609l-.103.75c1.445.378 2.843.567 4.196.567 2.158 0 3.204-.748 3.204-2.013 0-1.382-1.183-2.437-3.58-3.413z" />
<path d="M21.714 4H2.286A2.286 2.286 0 0 0 0 6.286v11.428A2.286 2.286 0 0 0 2.286 20h19.428A2.286 2.286 0 0 0 24 17.714V6.286A2.286 2.286 0 0 0 21.714 4zM2.286 5.143h19.428c.631 0 1.143.512 1.143 1.143v8.08c-.957-.454-2.222-.903-3.75-1.346a9.8 9.8 0 0 0 .607-2.02h-4.571V9.286h5.143V8.143h-5.143V6.286H13.43v1.857H8.286v1.143h5.143V11H8.286v1.143h6.356a11.54 11.54 0 0 1-.916 1.512 16.648 16.648 0 0 0-3.3-1.12l-.576.78c1.242.328 2.348.73 3.31 1.21a10.175 10.175 0 0 1-4.317 1.78l.514.78c2.048-.454 3.718-1.237 5.008-2.344.796.472 1.464 1 2.004 1.583l.726-.78c-.527-.516-1.179-.996-1.96-1.455.327-.407.627-.839.9-1.295.264-.447.495-.907.694-1.38.7.2 1.341.412 1.916.637 2.343.96 3.378 1.935 3.378 3.195 0 .872-.703 1.532-2.647 1.532-1.43 0-3.023-.244-4.786-.732l-.123.9c1.73.454 3.407.68 5.03.68 2.585 0 3.84-.899 3.84-2.416 0-1.166-.69-2.152-2.066-2.96v-.001c-.24-.14-.495-.276-.77-.408V6.286a1.143 1.143 0 0 0-1.143-1.143z" />
</svg>
)
type PaymentMethod = "wechat" | "alipay" | "usdt" | "paypal" | "stripe" | "bank"
@@ -30,23 +25,26 @@ interface PaymentModalProps {
onSuccess: () => void
}
// 支付状态类型
type PaymentState = "idle" | "creating" | "paying" | "polling" | "success" | "error"
export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, amount, onSuccess }: PaymentModalProps) {
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("alipay")
const [isProcessing, setIsProcessing] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("wechat")
const [paymentState, setPaymentState] = useState<PaymentState>("idle")
const [errorMessage, setErrorMessage] = useState("")
const [showQRCode, setShowQRCode] = useState(false)
const [qrCodeDataUrl, setQrCodeDataUrl] = useState("")
const [paymentUrl, setPaymentUrl] = useState("")
const [orderSn, setOrderSn] = useState("")
const [tradeSn, setTradeSn] = useState("")
const [currentGateway, setCurrentGateway] = useState("") // 当前支付网关
const [copied, setCopied] = useState(false)
const pollingRef = useRef<NodeJS.Timeout | null>(null)
const pollingCountRef = useRef(0)
const { purchaseSection, purchaseFullBook, user, settings } = useStore()
useEffect(() => {
if (isOpen) {
setShowQRCode(false)
setIsSuccess(false)
setIsProcessing(false)
}
}, [isOpen])
const paymentConfig = settings?.paymentMethods || {
wechat: { enabled: true, qrCode: "", account: "", groupQrCode: "" },
alipay: { enabled: true, qrCode: "", account: "" },
@@ -57,30 +55,248 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
const usdtAmount = (amount / (paymentConfig.usdt?.exchangeRate || 7.2)).toFixed(2)
const paypalAmount = (amount / (paymentConfig.paypal?.exchangeRate || 7.2)).toFixed(2)
// 清理轮询
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")
}
}
const handleCopyAddress = (address: string) => {
navigator.clipboard.writeText(address)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
// 处理开始支付
const handlePayment = async () => {
setShowQRCode(true)
if (paymentMethod === "wechat" && paymentConfig.wechat?.qrCode) {
const link = paymentConfig.wechat.qrCode
if (link.startsWith("http") || link.startsWith("weixin://")) {
window.open(link, "_blank")
}
} else if (paymentMethod === "alipay" && paymentConfig.alipay?.qrCode) {
const link = paymentConfig.alipay.qrCode
if (link.startsWith("http") || link.startsWith("alipays://")) {
window.open(link, "_blank")
}
// USDT和PayPal使用旧的手动确认流程
if (paymentMethod === "usdt" || paymentMethod === "paypal") {
setShowQRCode(true)
return
}
// 微信和支付宝使用新的API流程
await createPaymentOrder()
}
const confirmPayment = async () => {
setIsProcessing(true)
// USDT/PayPal手动确认
const handleCryptoConfirm = async () => {
setPaymentState("creating")
// 模拟确认支付(实际需要人工审核)
await new Promise((resolve) => setTimeout(resolve, 1000))
let success = false
@@ -90,24 +306,11 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
success = await purchaseFullBook(paymentMethod)
}
setIsProcessing(false)
if (success) {
setIsSuccess(true)
const groupUrl = paymentConfig.wechat?.groupQrCode
if (groupUrl) {
setTimeout(() => {
window.open(groupUrl, "_blank")
}, 800)
}
setTimeout(() => {
onSuccess()
onClose()
setIsSuccess(false)
setShowQRCode(false)
}, 2500)
await handlePaymentSuccess()
} else {
setPaymentState("error")
setErrorMessage("确认支付失败,请联系客服")
}
}
@@ -130,21 +333,13 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
iconBg: "rgba(7, 193, 96, 0.15)",
enabled: paymentConfig.wechat?.enabled ?? true,
},
{
id: "alipay",
name: "支付宝",
icon: <AlipayIcon />,
color: "#1677FF",
iconBg: "rgba(22, 119, 255, 0.15)",
enabled: paymentConfig.alipay?.enabled ?? true,
},
{
id: "usdt",
name: `USDT (${paymentConfig.usdt?.network || "TRC20"})`,
icon: <Bitcoin className="w-5 h-5" />,
color: "#26A17B",
iconBg: "rgba(38, 161, 123, 0.15)",
enabled: paymentConfig.usdt?.enabled ?? true,
enabled: paymentConfig.usdt?.enabled ?? false,
extra: `$${usdtAmount}`,
},
{
@@ -160,7 +355,7 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
const availableMethods = paymentMethods.filter((m) => m.enabled)
// 二维码/详情页面 - iOS毛玻璃风格
// 二维码/详情页面
if (showQRCode) {
const isCrypto = paymentMethod === "usdt"
const isPaypal = paymentMethod === "paypal"
@@ -170,8 +365,7 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
let address = ""
let displayAmount = `¥${amount.toFixed(2)}`
let title = "扫码支付"
let hint = "支付完成后,请点击下方按钮确认"
let qrCodeUrl = ""
let hint = "支付完成后,系统将自动确认"
if (isCrypto) {
address = paymentConfig.usdt?.address || ""
@@ -185,12 +379,10 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
hint = "请转账到以下PayPal账户"
} else if (isWechat) {
title = "微信支付"
qrCodeUrl = paymentConfig.wechat?.qrCode || ""
hint = "请使用微信扫码支付"
hint = "请使用微信扫描二维码支付"
} else if (isAlipay) {
title = "支付宝支付"
qrCodeUrl = paymentConfig.alipay?.qrCode || ""
hint = "请使用支付宝扫码支付"
hint = paymentUrl?.startsWith("http") ? "已打开支付页面,请在新窗口完成支付" : "请使用支付宝扫描二维码支付"
}
return (
@@ -218,24 +410,52 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
<p className="text-4xl font-bold text-[var(--app-brand)] glow-text">{displayAmount}</p>
</div>
{/* QR Code Display */}
{/* 错误提示 */}
{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 - 微信/支付宝 */}
{(isWechat || isAlipay) && (
<div className="flex flex-col items-center mb-6">
<div className="w-52 h-52 bg-white rounded-2xl p-4 mb-4 flex items-center justify-center shadow-lg">
{qrCodeUrl ? (
{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 ? (
<img
src={qrCodeUrl || "/placeholder.svg"}
src={qrCodeDataUrl}
alt="支付二维码"
className="w-full h-full object-contain rounded-lg"
/>
) : 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>
) : (
<div className="flex flex-col items-center text-gray-400">
<QrCode className="w-16 h-16 mb-2" />
<span className="text-sm text-center"></span>
<span className="text-sm text-center">...</span>
</div>
)}
</div>
<p className="text-[var(--app-text-tertiary)] text-sm">{hint}</p>
{/* 轮询状态指示 */}
{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>
)}
</div>
)}
@@ -279,25 +499,47 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
{/* Action Buttons */}
<div className="flex gap-3">
<button
onClick={() => setShowQRCode(false)}
onClick={() => {
clearPolling()
setShowQRCode(false)
setPaymentState("idle")
setErrorMessage("")
}}
className="btn-ios-secondary flex-1"
>
</button>
<button
onClick={confirmPayment}
disabled={isProcessing}
className="btn-ios flex-1 glow disabled:opacity-50"
>
{isProcessing ? (
<div className="flex items-center justify-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
...
</div>
) : (
"已完成支付"
)}
</button>
{(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>
)}
</div>
</div>
</div>
@@ -306,7 +548,7 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
}
// 支付成功页面
if (isSuccess) {
if (paymentState === "success") {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/60 backdrop-blur-md modal-overlay" />
@@ -334,7 +576,7 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
)
}
// 主支付选择页面 - iOS风格
// 主支付选择页面
return (
<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} />
@@ -421,13 +663,13 @@ export function PaymentModal({ isOpen, onClose, type, sectionId, sectionTitle, a
<div className="p-6 pt-0">
<button
onClick={handlePayment}
disabled={isProcessing || availableMethods.length === 0}
disabled={paymentState === "creating" || availableMethods.length === 0}
className="btn-ios w-full glow text-lg disabled:opacity-50"
>
{isProcessing ? (
{paymentState === "creating" ? (
<div className="flex items-center justify-center gap-2">
<div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
...
<Loader2 className="w-5 h-5 animate-spin" />
...
</div>
) : (
`确认支付 ¥${amount.toFixed(2)}`