修复对公转账

This commit is contained in:
sjy
2026-08-14 15:54:21 +08:00
9 changed files with 402 additions and 206 deletions
File diff suppressed because one or more lines are too long
+35 -35
View File
@@ -1,36 +1,36 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" id="favicon" />
<script>
// 立即从 localStorage 设置 favicon,避免闪烁
(function() {
try {
var cached = localStorage.getItem('siteInfo');
if (cached) {
var info = JSON.parse(cached);
if (info.siteLogo) {
var link = document.getElementById('favicon');
link.href = info.siteLogo;
link.type = 'image/png';
}
if (info.siteName) {
document.title = info.siteName;
}
}
} catch (e) {}
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>民众智创</title>
<script type="module" crossorigin src="/assets/index-BMPNcnJK.js"></script>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" id="favicon" />
<script>
// 立即从 localStorage 设置 favicon,避免闪烁
(function() {
try {
var cached = localStorage.getItem('siteInfo');
if (cached) {
var info = JSON.parse(cached);
if (info.siteLogo) {
var link = document.getElementById('favicon');
link.href = info.siteLogo;
link.type = 'image/png';
}
if (info.siteName) {
document.title = info.siteName;
}
}
} catch (e) {}
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>民众智创</title>
<script type="module" crossorigin src="/assets/index-JDcJqWtB.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DSYnuUvx.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
</head>
<body>
<div id="root"></div>
</body>
</html>
+14
View File
@@ -574,6 +574,20 @@ export async function setDefaultInvoiceHeader(id: string): Promise<any> {
return api.put(`/invoice-headers/${id}/set-default`);
}
// ── Bank Accounts ──────────────────────────────────────────
export interface BankAccountInfo {
id: string;
accountName: string;
bankName: string;
accountNo: string;
description: string | null;
}
export async function getDefaultBankAccount(): Promise<{ hasAccount: boolean; account: BankAccountInfo | null }> {
return api.get('/bank/default-account');
}
export async function getCreditRatios(): Promise<any[]> {
return api.get('/credits/ratios');
}
+209 -23
View File
@@ -48,6 +48,7 @@ import {
CrownFilled,
FireOutlined,
BankFilled,
BankOutlined,
CheckCircleFilled,
WechatOutlined,
AlipayCircleOutlined,
@@ -60,7 +61,7 @@ import {
} from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore';
import { getMenuConfigs, getCreditProductCatalog, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername } from '../../api';
import { getMenuConfigs, getCreditProductCatalog, getPaymentMethods, createRechargeOrder, getPaymentOrder, cancelPaymentOrder, getSiteInfo, getUnreadCount, createContactRequest, getUser, changePassword, changeUsername, getDefaultBankAccount } from '../../api';
import type { CreditProduct, CreditProductCatalog } from '../../types';
import NotificationPopup from '../NotificationPopup';
import ActivityBanner from './ActivityBanner';
@@ -495,6 +496,24 @@ const AppLayout: React.FC = () => {
const [pendingProduct, setPendingProduct] = useState<{ id: string; product: CreditProduct; } | null>(null);
const [purchaseQuantity, setPurchaseQuantity] = useState(1);
const [qrRevealed, setQrRevealed] = useState(false);
const [paymentTab, setPaymentTab] = useState<'alipay' | 'corporate'>('alipay');
const [corporateTransferForm] = Form.useForm();
const [corporateSubmitting, setCorporateSubmitting] = useState(false);
const [bankAccountInfo, setBankAccountInfo] = useState<{ hasAccount: boolean; account: any } | null>(null);
const [bankAccountLoading, setBankAccountLoading] = useState(false);
const fetchBankAccount = useCallback(async () => {
if (bankAccountInfo || bankAccountLoading) return;
setBankAccountLoading(true);
try {
const data = await getDefaultBankAccount();
setBankAccountInfo(data);
} catch {
setBankAccountInfo({ hasAccount: false, account: null });
} finally {
setBankAccountLoading(false);
}
}, [bankAccountInfo, bankAccountLoading]);
const PENDING_ORDER_KEY = 'pending_payment_order';
const [unreadCount, setUnreadCount] = useState(0);
@@ -886,6 +905,8 @@ const AppLayout: React.FC = () => {
setPendingProduct({ id: productId, product });
setPurchaseQuantity(product.productType === 'team_subscription' ? 2 : 1);
setQrRevealed(false);
setPaymentTab('alipay');
corporateTransferForm.resetFields();
setCurrentPaymentInfo(null);
setCreditsModalOpen(false);
setQrCodeModalOpen(true);
@@ -944,6 +965,50 @@ const AppLayout: React.FC = () => {
}
}, [pendingProduct, paymentMethod, purchaseQuantity]);
const submitCorporateTransfer = useCallback(async () => {
if (!pendingProduct) return;
try {
const values: any = await corporateTransferForm.validateFields();
setCorporateSubmitting(true);
const { id: productId, product } = pendingProduct;
const order = await createRechargeOrder(productId, 'corporate');
const finalPrice = Number(order.amount ?? product.currentPrice ?? product.price ?? 0);
const credits = Number(product.grantCredits || product.monthlyGrantCredits || 0);
const originalPrice = product.regularPrice;
const cycleMap: Record<string, string> = { monthly: '月套餐', quarterly: '季套餐', yearly: '年套餐' };
const isSubscription = !!product.billingCycle;
const periodLabel = product.billingCycle ? cycleMap[product.billingCycle] : '';
const tierName = product.name || '积分充值';
const displayTitle = isSubscription ? `${tierName} ${periodLabel}` : tierName;
const paymentInfo = {
price: finalPrice,
credits,
qrCode: '',
method: 'corporate',
title: displayTitle,
originalPrice: originalPrice ? Number(originalPrice) : undefined,
tierName,
periodLabel,
isSubscription,
};
void values;
setCurrentPaymentInfo(paymentInfo);
message.success('已提交对公转账信息,客服将在1-3个工作日内审核到账');
setQrCodeModalOpen(false);
setPendingProduct(null);
corporateTransferForm.resetFields();
await useAuthStore.getState().refreshUser();
await loadCreditCatalog();
} catch (err: any) {
if (err?.errorFields) {
return;
}
message.error(err?.message || '提交失败,请重试');
} finally {
setCorporateSubmitting(false);
}
}, [pendingProduct, corporateTransferForm]);
const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => {
stopPolling();
setCountdown(timeoutSeconds);
@@ -1921,9 +1986,11 @@ const AppLayout: React.FC = () => {
setCurrentPaymentInfo(null);
setPendingProduct(null);
setQrRevealed(false);
setPaymentTab('alipay');
corporateTransferForm.resetFields();
}}
footer={null}
width={760}
width={'50%'}
closable
title={
<Typography.Title level={5} style={{ margin: 0 }}>
@@ -1998,31 +2065,150 @@ const AppLayout: React.FC = () => {
</div>
</div>
{/* 右侧:在线支付方式。后台线下成交不在客户端创建。 */}
<div style={{ width: 300, padding: 20, background: '#fff', display: 'flex', flexDirection: 'column' }}>
<Typography.Text strong style={{ marginBottom: 12 }}></Typography.Text>
<div style={{ display: 'flex', gap: 8, marginBottom: 18 }}>
{enabledMethods.alipay && <Button type={paymentMethod === 'alipay' ? 'primary' : 'default'} onClick={() => { if (!qrRevealed) setPaymentMethod('alipay'); }} icon={<AlipayCircleOutlined />}></Button>}
{enabledMethods.wechat && <Button type={paymentMethod === 'wechat' ? 'primary' : 'default'} onClick={() => { if (!qrRevealed) setPaymentMethod('wechat'); }} icon={<WechatOutlined />}></Button>}
{/* 右侧:支付方式 */}
<div style={{ width: '50%', padding: 20, background: '#fff', display: 'flex', flexDirection: 'column' }}>
{/* Tab 切换 */}
<div style={{ display: 'flex', background: '#f3f4f6', borderRadius: 10, padding: 4, marginBottom: 20 }}>
<div
onClick={() => setPaymentTab('alipay')}
style={{
flex: 1, textAlign: 'center', padding: '8px 0', borderRadius: 8,
cursor: 'pointer', fontSize: 14, fontWeight: 600,
background: paymentTab === 'alipay' ? '#fff' : 'transparent',
color: paymentTab === 'alipay' ? '#1677ff' : '#64748b',
boxShadow: paymentTab === 'alipay' ? '0 2px 8px rgba(0,0,0,0.08)' : 'none',
transition: 'all 0.2s',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
}}
>
<AlipayCircleOutlined style={{ color: paymentTab === 'alipay' ? '#1677ff' : '#94a3b8' }} />
</div>
<div
onClick={() => { setPaymentTab('corporate'); fetchBankAccount(); }}
style={{
flex: 1, textAlign: 'center', padding: '8px 0', borderRadius: 8,
cursor: 'pointer', fontSize: 14, fontWeight: 600,
background: paymentTab === 'corporate' ? '#fff' : 'transparent',
color: paymentTab === 'corporate' ? '#6366f1' : '#64748b',
boxShadow: paymentTab === 'corporate' ? '0 2px 8px rgba(0,0,0,0.08)' : 'none',
transition: 'all 0.2s',
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
}}
>
<BankOutlined style={{ color: paymentTab === 'corporate' ? '#6366f1' : '#94a3b8' }} />
</div>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ marginBottom: 12, color: '#64748b' }}>{paymentMethod === 'wechat' ? '请用微信扫码支付' : '请用支付宝扫码支付'}</div>
<div style={{ position: 'relative', width: 180, height: 180, borderRadius: 12, border: '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff' }}>
{qrRevealed && currentPaymentInfo?.qrCode ? <QRCodeSVG value={currentPaymentInfo.qrCode} size={150} level="M" /> : (
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 13, color: '#64748b', marginBottom: 12 }}></div>
<Button type="primary" loading={paying} onClick={confirmPayment}></Button>
{/* 支付宝 Tab */}
{paymentTab === 'alipay' && (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 12 }}>
<AlipayCircleOutlined style={{ fontSize: 18, color: '#1677ff' }} />
<span style={{ fontSize: 14, color: '#64748b' }}></span>
</div>
<div style={{ position: 'relative', width: 180, height: 180, borderRadius: 12, border: '1px solid #f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff' }}>
{qrRevealed && currentPaymentInfo?.qrCode ? (
<QRCodeSVG value={currentPaymentInfo.qrCode} size={150} level="M" />
) : (
<div style={{ textAlign: 'center' }}>
<div style={{ fontSize: 13, color: '#64748b', marginBottom: 12 }}><br/></div>
<Button type="primary" loading={paying} onClick={confirmPayment} style={{ borderRadius: 8, height: 36, fontWeight: 600 }}>
使
</Button>
</div>
)}
</div>
{qrRevealed && (
<div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}>
{countdown}
</div>
)}
{qrRevealed && (
<Button
danger
style={{ marginTop: 14, borderRadius: 8 }}
onClick={async () => {
stopPolling();
if (currentOrderNoRef.current) { try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { } currentOrderNoRef.current = null; }
localStorage.removeItem(PENDING_ORDER_KEY); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); setPendingProduct(null); setQrRevealed(false); message.info('已取消支付');
}}
>
</Button>
)}
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>
<br/>
</div>
</div>
{qrRevealed && <div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}>{countdown} </div>}
{qrRevealed && <Button danger style={{ marginTop: 14 }} onClick={async () => {
stopPolling();
if (currentOrderNoRef.current) { try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { } currentOrderNoRef.current = null; }
localStorage.removeItem(PENDING_ORDER_KEY); setQrCodeModalOpen(false); setCurrentPaymentInfo(null); setPendingProduct(null); setQrRevealed(false); message.info('已取消支付');
}}></Button>}
</div>
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>线/线</div>
)}
{/* 对公转账 Tab */}
{paymentTab === 'corporate' && (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflowY: 'auto' }}>
{/* 收款账户信息 */}
<div style={{ marginBottom: 16 }}>
<Typography.Text style={{ fontSize: 12, color: '#64748b', marginBottom: 8, display: 'block', fontWeight: 600 }}></Typography.Text>
<div style={{ padding: 12, background: '#f8fafc', borderRadius: 8, border: '1px solid #e2e8f0', minHeight: 80, display: 'flex', alignItems: 'center' }}>
{bankAccountLoading ? (
<div style={{ width: '100%', textAlign: 'center', color: '#94a3b8', fontSize: 12 }}>...</div>
) : bankAccountInfo?.hasAccount && bankAccountInfo?.account ? (
<div style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12, marginBottom: 4 }}>
<span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e', fontWeight: 500 }}>{bankAccountInfo.account.accountName}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12, marginBottom: 4 }}>
<span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 4 }}>
{/* <BankFilled style={{ color: '#c8161d', fontSize: 14 }} /> */}
{bankAccountInfo.account.bankName}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '4px 0', fontSize: 12 }}>
<span style={{ color: '#64748b' }}></span>
<span style={{ color: '#1a1a2e', fontWeight: 500, fontFamily: 'monospace' }}>{bankAccountInfo.account.accountNo}</span>
</div>
</div>
) : (
<div style={{ width: '100%', textAlign: 'center', color: '#94a3b8', fontSize: 12 }}></div>
)}
</div>
</div>
{/* 打款信息填写 */}
<div style={{ marginBottom: 12 }}>
<Typography.Text style={{ fontSize: 12, color: '#64748b', marginBottom: 4, display: 'block', fontWeight: 600 }}></Typography.Text>
<Typography.Text style={{ fontSize: 11, color: '#ef4444', marginBottom: 8, display: 'block' }}></Typography.Text>
<Form form={corporateTransferForm} layout="vertical" size="small" requiredMark={false}>
<Form.Item name="accountName" label="账户名称" rules={[{ required: true, message: '请输入账户名称' }]} style={{ marginBottom: 10 }}>
<Input placeholder="请输入付款方账户名称" />
</Form.Item>
<Form.Item name="bankName" label="开户银行" rules={[{ required: true, message: '请输入开户银行' }]} style={{ marginBottom: 10 }}>
<Input placeholder="请输入开户银行" prefix={<BankOutlined style={{ color: '#94a3b8' }} />} />
</Form.Item>
<Form.Item name="bankAccount" label="账号" rules={[{ required: true, message: '请输入账号' }]} style={{ marginBottom: 8 }}>
<Input placeholder="请输入付款方账号" />
</Form.Item>
</Form>
</div>
<Button
type="primary"
block
size="large"
loading={corporateSubmitting}
onClick={submitCorporateTransfer}
style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', fontWeight: 600, height: 38, fontSize: 14 }}
>
</Button>
<div style={{ marginTop: 8, fontSize: 11, color: '#94a3b8', textAlign: 'center' }}>
12
</div>
</div>
)}
</div>
</div>
</Modal>
@@ -964,13 +964,15 @@ const GenerateConver: React.FC = () => {
{/* 上传视频 */}
<div style={{ marginBottom: 16 }}>
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
<span style={{ color: '#f94444' }}></span>
<p style={{ fontSize: 12, color: '#64748b', fontWeight: 400, }}>
<div style={{ marginBottom: 10 }}>
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b' }}>
<span style={{ color: '#f94444' }}></span>
</p>
<p style={{ fontSize: 12, color: '#64748b', fontWeight: 400, margin: '4px 0 0' }}>
{currentMaxDuration}
</p>
</p>
</div>
{videoUrl ? (
<div style={{ position: 'relative' }}>
<video