feat: 支付闭环 + 运营助手 Tool Calling + 管理后台完善
- 支付系统:微信支付 mock 自动完成、NATIVE 扫码支付、JSAPI 集成 - 运营助手:Tool Calling 架构,19 个可执行工具,AI 驱动操作 - 角色管理:表格布局 + Dialog 表单 + 权限勾选 - 配置统一:config.ts 单一数据源 - API 审计:补齐 status toggle / comments 端点 - 暗黑模式硬件编码颜色全部替换为 CSS 变量
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import QRCode from 'qrcode';
|
||||
import { apiFetch } from '@/lib/auth';
|
||||
import { isWeChatBrowser } from '@/lib/wechat';
|
||||
|
||||
interface PayResult {
|
||||
prepay_id?: string;
|
||||
nonceStr?: string;
|
||||
timeStamp?: string;
|
||||
package?: string;
|
||||
paySign?: string;
|
||||
signType?: string;
|
||||
codeUrl?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
orderNo: string;
|
||||
payResult: PayResult;
|
||||
tradeType: 'JSAPI' | 'NATIVE';
|
||||
onPaid: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function PaymentModal({ open, orderNo, payResult, tradeType, onPaid, onClose }: Props) {
|
||||
const [status, setStatus] = useState<'pending' | 'paid' | 'failed'>('pending');
|
||||
const [qrDataUrl, setQrDataUrl] = useState('');
|
||||
const [message, setMessage] = useState('');
|
||||
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const wechatBridgeCalled = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setStatus('pending');
|
||||
setMessage('');
|
||||
wechatBridgeCalled.current = false;
|
||||
if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; }
|
||||
return;
|
||||
}
|
||||
|
||||
// NATIVE: render QR code
|
||||
if (tradeType === 'NATIVE' && payResult?.codeUrl) {
|
||||
QRCode.toDataURL(payResult.codeUrl, { margin: 1, width: 280 }, (err, url) => {
|
||||
if (!err) setQrDataUrl(url);
|
||||
});
|
||||
setMessage('请使用微信扫描二维码完成支付');
|
||||
startPolling();
|
||||
}
|
||||
|
||||
// JSAPI in WeChat: call WeixinJSBridge
|
||||
if (tradeType === 'JSAPI' && isWeChatBrowser() && !wechatBridgeCalled.current) {
|
||||
wechatBridgeCalled.current = true;
|
||||
setMessage('正在调起微信支付...');
|
||||
callWechatJsapi(payResult);
|
||||
startPolling();
|
||||
}
|
||||
}, [open, tradeType, payResult?.codeUrl]);
|
||||
|
||||
function startPolling() {
|
||||
if (pollingRef.current) clearInterval(pollingRef.current);
|
||||
pollingRef.current = setInterval(async () => {
|
||||
try {
|
||||
const res = await apiFetch(`/payment/wxpay/query?outTradeNo=${orderNo}`);
|
||||
const data = await res.json();
|
||||
if (data.trade_state === 'SUCCESS' || data.localStatus === 'PAID') {
|
||||
setStatus('paid');
|
||||
setMessage('支付成功!');
|
||||
if (pollingRef.current) clearInterval(pollingRef.current);
|
||||
setTimeout(onPaid, 1500);
|
||||
}
|
||||
} catch {}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function callWechatJsapi(params: PayResult) {
|
||||
if (typeof WeixinJSBridge === 'undefined') {
|
||||
document.addEventListener('WeixinJSBridgeReady', () => doInvoke(params), false);
|
||||
} else {
|
||||
doInvoke(params);
|
||||
}
|
||||
}
|
||||
|
||||
function doInvoke(params: PayResult) {
|
||||
WeixinJSBridge.invoke(
|
||||
'getBrandWCPayRequest',
|
||||
{
|
||||
appId: '', // filled by WeChat
|
||||
timeStamp: params.timeStamp || '',
|
||||
nonceStr: params.nonceStr || '',
|
||||
package: params.package || '',
|
||||
signType: params.signType || 'RSA',
|
||||
paySign: params.paySign || '',
|
||||
},
|
||||
(res: any) => {
|
||||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||
setStatus('paid');
|
||||
setMessage('支付成功!');
|
||||
if (pollingRef.current) clearInterval(pollingRef.current);
|
||||
setTimeout(onPaid, 1500);
|
||||
} else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
|
||||
setMessage('已取消支付');
|
||||
setStatus('pending');
|
||||
} else {
|
||||
setMessage('支付失败,请重试');
|
||||
setStatus('failed');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => { if (pollingRef.current) clearInterval(pollingRef.current); };
|
||||
}, []);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
||||
<div className="bg-card rounded-2xl p-8 w-full max-w-sm mx-4 shadow-xl border border-border" onClick={e => e.stopPropagation()}>
|
||||
<h3 className="text-lg font-semibold text-foreground text-center mb-4">支付</h3>
|
||||
|
||||
{tradeType === 'NATIVE' && (
|
||||
<div className="flex justify-center mb-4">
|
||||
{qrDataUrl ? (
|
||||
<img src={qrDataUrl} alt="支付二维码" className="w-56 h-56 rounded-xl border border-border" />
|
||||
) : (
|
||||
<div className="w-56 h-56 bg-muted rounded-xl animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === 'paid' ? (
|
||||
<div className="text-center">
|
||||
<div className="text-5xl mb-3">✅</div>
|
||||
<p className="text-green-600 font-medium">{message}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground text-center mb-4">{message}</p>
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
||||
<span className="w-2 h-2 bg-brand-600 rounded-full animate-pulse" />
|
||||
等待支付中...
|
||||
</div>
|
||||
<button onClick={onClose} className="mt-4 w-full py-2 text-sm text-muted-foreground border border-border rounded-xl hover:bg-accent">
|
||||
取消支付
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user