feat(site): 官网直接登录 + 购买(不再单独部署 Web 面)
用户要求「直接在官网登录,别多搞一个 web 工程」。官网本就是 admin 前端的一部分 (embed 进 gateway),在它上面加登录 + 购买,仍是「一个前端」,零新增部署。 sundynix-web(薄 Web 面)不部署;账单/组织/团队等留桌面端。 定价页「购买」流程: 未登录 → 弹微信扫码登录(带参二维码 + 关注/扫码事件)→ 登录后自动继续那笔购买 已登录 → 直接弹微信支付二维码 → 轮询单态 → 到账 支付走后端现有链路(下单/回调/查单/掉单补偿全复用),与桌面端同一套。 站点用户 token 存独立 key(sdx_site_token),与运维后台的 sdx_admin_token 隔开——普通用户登录拿到的用户 JWT 不该与管理员令牌混用。 本地验证:未登录点购买 → 弹登录弹窗;未配微信时如实显示「微信登录未配置」 (部署配好即真二维码)。登录门 + 支付弹窗 UI 骨架与错误态均正确;真二维码/ 真支付留待部署后。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import QRCode from 'qrcode'
|
||||
import { createOrder, orderStatus } from '../lib/site-api'
|
||||
|
||||
// 官网购买支付弹窗:下单 → 微信 code_url 画二维码 → 轮询单态。
|
||||
// 复用后端支付链路(下单/回调/查单/掉单补偿全共用),与桌面端/联调台同一套。
|
||||
const POLL_MS = 2500
|
||||
|
||||
type Phase = 'creating' | 'waiting' | 'paid' | 'expired'
|
||||
|
||||
export function PayDialog({
|
||||
item,
|
||||
onClose,
|
||||
}: {
|
||||
item: { kind: 'sub' | 'pack'; id: string; name: string; priceFen: number }
|
||||
onClose: (paid: boolean) => void
|
||||
}) {
|
||||
const [qr, setQr] = useState('')
|
||||
const [phase, setPhase] = useState<Phase>('creating')
|
||||
const [err, setErr] = useState('')
|
||||
const [warn, setWarn] = useState('')
|
||||
const orderRef = useRef('')
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
let timer: number | null = null
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const o = await createOrder(item.kind === 'sub' ? { planId: item.id } : { packId: item.id })
|
||||
if (!alive) return
|
||||
orderRef.current = o.order_id
|
||||
setQr(await QRCode.toDataURL(o.code_url, { width: 220, margin: 1 }))
|
||||
setPhase('waiting')
|
||||
const tick = async () => {
|
||||
if (!alive) return
|
||||
try {
|
||||
const s = await orderStatus(orderRef.current)
|
||||
if (!alive) return
|
||||
setWarn(s.warn ?? '')
|
||||
if (s.status === 'paid') {
|
||||
setPhase('paid')
|
||||
window.setTimeout(() => onClose(true), 900)
|
||||
return
|
||||
}
|
||||
if (s.status === 'expired' || s.status === 'failed') {
|
||||
setPhase('expired')
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
/* 单次失败忽略 */
|
||||
}
|
||||
timer = window.setTimeout(() => void tick(), POLL_MS)
|
||||
}
|
||||
timer = window.setTimeout(() => void tick(), POLL_MS)
|
||||
} catch (e) {
|
||||
if (alive) setErr((e as Error).message)
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
alive = false
|
||||
if (timer) window.clearTimeout(timer)
|
||||
}
|
||||
}, [item, onClose])
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-ink/40 p-4" onClick={() => onClose(phase === 'paid')}>
|
||||
<div className="w-full max-w-sm rounded-2xl border border-hairline bg-surface p-6" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="text-base font-semibold text-ink">微信支付 · {item.name}</h3>
|
||||
<button onClick={() => onClose(phase === 'paid')} className="text-sm text-ink-3 hover:text-ink">
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-col items-center gap-3">
|
||||
{err ? (
|
||||
<p className="py-10 text-center text-sm text-red-500">{err}</p>
|
||||
) : phase === 'creating' ? (
|
||||
<p className="py-16 text-sm text-ink-3">正在生成支付二维码…</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="relative rounded-xl bg-white p-3">
|
||||
<img src={qr} alt="微信支付二维码" width={220} height={220} />
|
||||
{(phase === 'paid' || phase === 'expired') && (
|
||||
<div className="absolute inset-0 flex items-center justify-center rounded-xl bg-white/85 text-sm font-medium text-ink">
|
||||
{phase === 'paid' ? '✅ 支付成功' : '二维码已失效'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-semibold tabular-nums text-ink">¥{(item.priceFen / 100).toFixed(2)}</div>
|
||||
{phase === 'waiting' && <div className="mt-0.5 text-xs text-ink-3">微信扫一扫完成支付</div>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{warn && <p className="w-full rounded-lg bg-amber-50 px-3 py-2 text-center text-xs text-amber-700">{warn}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user