fix: 修复 lint errors(hooks 条件调用、未转义引号、effect 内 setState)

This commit is contained in:
erio
2026-03-05 23:08:48 +08:00
parent 93a417b312
commit ab961e669a
2 changed files with 122 additions and 104 deletions

View File

@@ -187,6 +187,20 @@ function PayContent() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [userId, token]); }, [userId, token]);
useEffect(() => {
if (step !== 'result' || finalStatus !== 'COMPLETED') return;
// 立即在后台刷新余额2.2s 显示结果页后再切回表单(届时余额已更新)
loadUserAndOrders();
const timer = setTimeout(() => {
setStep('form');
setOrderResult(null);
setFinalStatus('');
setError('');
}, 2200);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step, finalStatus]);
if (!effectiveUserId || Number.isNaN(effectiveUserId) || effectiveUserId <= 0) { if (!effectiveUserId || Number.isNaN(effectiveUserId) || effectiveUserId <= 0) {
return ( return (
<div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}> <div className={`flex min-h-screen items-center justify-center p-4 ${isDark ? 'bg-slate-950' : 'bg-slate-50'}`}>
@@ -290,20 +304,6 @@ function PayContent() {
setError(''); setError('');
}; };
useEffect(() => {
if (step !== 'result' || finalStatus !== 'COMPLETED') return;
// 立即在后台刷新余额2.2s 显示结果页后再切回表单(届时余额已更新)
loadUserAndOrders();
const timer = setTimeout(() => {
setStep('form');
setOrderResult(null);
setFinalStatus('');
setError('');
}, 2200);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [step, finalStatus]);
return ( return (
<PayPageLayout <PayPageLayout
isDark={isDark} isDark={isDark}
@@ -311,35 +311,37 @@ function PayContent() {
maxWidth={isMobile ? 'sm' : 'lg'} maxWidth={isMobile ? 'sm' : 'lg'}
title="Sub2API 余额充值" title="Sub2API 余额充值"
subtitle="安全支付,自动到账" subtitle="安全支付,自动到账"
actions={!isMobile ? ( actions={
<> !isMobile ? (
<button <>
type="button" <button
onClick={loadUserAndOrders} type="button"
className={[ onClick={loadUserAndOrders}
'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors', className={[
isDark ? 'border-slate-600 text-slate-200 hover:bg-slate-800' : 'border-slate-300 text-slate-700 hover:bg-slate-100', 'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors',
].join(' ')} isDark
> ? 'border-slate-600 text-slate-200 hover:bg-slate-800'
: 'border-slate-300 text-slate-700 hover:bg-slate-100',
</button> ].join(' ')}
<a >
href={ordersUrl}
className={[ </button>
'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors', <a
isDark ? 'border-slate-600 text-slate-200 hover:bg-slate-800' : 'border-slate-300 text-slate-700 hover:bg-slate-100', href={ordersUrl}
].join(' ')} className={[
> 'inline-flex items-center rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors',
isDark
</a> ? 'border-slate-600 text-slate-200 hover:bg-slate-800'
</> : 'border-slate-300 text-slate-700 hover:bg-slate-100',
) : undefined} ].join(' ')}
>
</a>
</>
) : undefined
}
> >
{error && ( {error && <div className="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-600">{error}</div>}
<div className="mb-4 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-600">
{error}
</div>
)}
{step === 'form' && isMobile && ( {step === 'form' && isMobile && (
<div <div
@@ -354,10 +356,12 @@ function PayContent() {
className={[ className={[
'rounded-lg px-3 py-2 text-sm font-semibold transition-all duration-200', 'rounded-lg px-3 py-2 text-sm font-semibold transition-all duration-200',
activeMobileTab === 'pay' activeMobileTab === 'pay'
? (isDark ? isDark
? 'bg-indigo-500/30 text-indigo-100 ring-1 ring-indigo-300/35 shadow-sm' ? 'bg-indigo-500/30 text-indigo-100 ring-1 ring-indigo-300/35 shadow-sm'
: 'bg-white text-slate-900 ring-1 ring-slate-300 shadow-md shadow-slate-300/50') : 'bg-white text-slate-900 ring-1 ring-slate-300 shadow-md shadow-slate-300/50'
: (isDark ? 'text-slate-400 hover:text-slate-200' : 'text-slate-500 hover:text-slate-700'), : isDark
? 'text-slate-400 hover:text-slate-200'
: 'text-slate-500 hover:text-slate-700',
].join(' ')} ].join(' ')}
> >
@@ -368,10 +372,12 @@ function PayContent() {
className={[ className={[
'rounded-lg px-3 py-2 text-sm font-semibold transition-all duration-200', 'rounded-lg px-3 py-2 text-sm font-semibold transition-all duration-200',
activeMobileTab === 'orders' activeMobileTab === 'orders'
? (isDark ? isDark
? 'bg-indigo-500/30 text-indigo-100 ring-1 ring-indigo-300/35 shadow-sm' ? 'bg-indigo-500/30 text-indigo-100 ring-1 ring-indigo-300/35 shadow-sm'
: 'bg-white text-slate-900 ring-1 ring-slate-300 shadow-md shadow-slate-300/50') : 'bg-white text-slate-900 ring-1 ring-slate-300 shadow-md shadow-slate-300/50'
: (isDark ? 'text-slate-400 hover:text-slate-200' : 'text-slate-500 hover:text-slate-700'), : isDark
? 'text-slate-400 hover:text-slate-200'
: 'text-slate-500 hover:text-slate-700',
].join(' ')} ].join(' ')}
> >
@@ -382,9 +388,7 @@ function PayContent() {
{step === 'form' && config.enabledPaymentTypes.length === 0 && ( {step === 'form' && config.enabledPaymentTypes.length === 0 && (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" /> <div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
<span className={['ml-3 text-sm', isDark ? 'text-slate-400' : 'text-gray-500'].join(' ')}> <span className={['ml-3 text-sm', isDark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>...</span>
...
</span>
</div> </div>
)} )}
@@ -432,31 +436,46 @@ function PayContent() {
/> />
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div className={['rounded-2xl border p-4', isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50'].join(' ')}> <div
className={[
'rounded-2xl border p-4',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50',
].join(' ')}
>
<div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}></div> <div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}></div>
<ul className={['mt-2 space-y-1 text-sm', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}> <ul className={['mt-2 space-y-1 text-sm', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
<li></li> <li></li>
<li>"我的订单"</li> <li></li>
{config.maxDailyAmount > 0 && ( {config.maxDailyAmount > 0 && <li> ¥{config.maxDailyAmount.toFixed(2)}</li>}
<li> ¥{config.maxDailyAmount.toFixed(2)}</li> {!hasToken && (
<li className={isDark ? 'text-amber-200' : 'text-amber-700'}> token</li>
)} )}
{!hasToken && <li className={isDark ? 'text-amber-200' : 'text-amber-700'}> token</li>}
</ul> </ul>
</div> </div>
{hasHelpContent && ( {hasHelpContent && (
<div className={['rounded-2xl border p-4', isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50'].join(' ')}> <div
className={[
'rounded-2xl border p-4',
isDark ? 'border-slate-700 bg-slate-800/70' : 'border-slate-200 bg-slate-50',
].join(' ')}
>
<div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>Support</div> <div className={['text-xs', isDark ? 'text-slate-400' : 'text-slate-500'].join(' ')}>Support</div>
{helpImageUrl && ( {helpImageUrl && (
<img <img
src={helpImageUrl} src={helpImageUrl}
alt='help' alt="help"
onClick={() => setHelpImageOpen(true)} onClick={() => setHelpImageOpen(true)}
className='mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain bg-white/70 p-2' className="mt-3 max-h-40 w-full cursor-zoom-in rounded-lg object-contain bg-white/70 p-2"
/> />
)} )}
{helpText && ( {helpText && (
<div className={['mt-3 space-y-1 text-sm leading-6', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}> <div
className={[
'mt-3 space-y-1 text-sm leading-6',
isDark ? 'text-slate-300' : 'text-slate-600',
].join(' ')}
>
{helpText.split('\\n').map((line, i) => ( {helpText.split('\\n').map((line, i) => (
<p key={i}>{line}</p> <p key={i}>{line}</p>
))} ))}
@@ -490,9 +509,7 @@ function PayContent() {
/> />
)} )}
{step === 'result' && ( {step === 'result' && <OrderStatus status={finalStatus} onBack={handleBack} dark={isDark} />}
<OrderStatus status={finalStatus} onBack={handleBack} dark={isDark} />
)}
{helpImageOpen && helpImageUrl && ( {helpImageOpen && helpImageUrl && (
<div <div
@@ -501,8 +518,8 @@ function PayContent() {
> >
<img <img
src={helpImageUrl} src={helpImageUrl}
alt='help' alt="help"
className='max-h-[90vh] max-w-full rounded-xl object-contain shadow-2xl' className="max-h-[90vh] max-w-full rounded-xl object-contain shadow-2xl"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
</div> </div>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { PAYMENT_TYPE_META } from '@/lib/pay-utils'; import { PAYMENT_TYPE_META } from '@/lib/pay-utils';
export interface MethodLimitInfo { export interface MethodLimitInfo {
@@ -49,11 +49,9 @@ export default function PaymentForm({
const [customAmount, setCustomAmount] = useState(''); const [customAmount, setCustomAmount] = useState('');
// Reset paymentType when enabledPaymentTypes changes (e.g. after config loads) // Reset paymentType when enabledPaymentTypes changes (e.g. after config loads)
useEffect(() => { const effectivePaymentType = enabledPaymentTypes.includes(paymentType)
if (!enabledPaymentTypes.includes(paymentType)) { ? paymentType
setPaymentType(enabledPaymentTypes[0] || 'stripe'); : enabledPaymentTypes[0] || 'stripe';
}
}, [enabledPaymentTypes, paymentType]);
const handleQuickAmount = (val: number) => { const handleQuickAmount = (val: number) => {
setAmount(val); setAmount(val);
@@ -81,22 +79,23 @@ export default function PaymentForm({
}; };
const selectedAmount = amount || 0; const selectedAmount = amount || 0;
const isMethodAvailable = !methodLimits || (methodLimits[paymentType]?.available !== false); const isMethodAvailable = !methodLimits || methodLimits[effectivePaymentType]?.available !== false;
const methodSingleMax = methodLimits?.[paymentType]?.singleMax; const methodSingleMax = methodLimits?.[effectivePaymentType]?.singleMax;
const effectiveMax = (methodSingleMax !== undefined && methodSingleMax > 0) ? methodSingleMax : maxAmount; const effectiveMax = methodSingleMax !== undefined && methodSingleMax > 0 ? methodSingleMax : maxAmount;
const feeRate = methodLimits?.[paymentType]?.feeRate ?? 0; const feeRate = methodLimits?.[effectivePaymentType]?.feeRate ?? 0;
const feeAmount = feeRate > 0 && selectedAmount > 0 const feeAmount = feeRate > 0 && selectedAmount > 0 ? Math.ceil(((selectedAmount * feeRate) / 100) * 100) / 100 : 0;
? Math.ceil(selectedAmount * feeRate / 100 * 100) / 100 const payAmount =
: 0; feeRate > 0 && selectedAmount > 0 ? Math.round((selectedAmount + feeAmount) * 100) / 100 : selectedAmount;
const payAmount = feeRate > 0 && selectedAmount > 0 const isValid =
? Math.round((selectedAmount + feeAmount) * 100) / 100 selectedAmount >= minAmount &&
: selectedAmount; selectedAmount <= effectiveMax &&
const isValid = selectedAmount >= minAmount && selectedAmount <= effectiveMax && hasValidCentPrecision(selectedAmount) && isMethodAvailable; hasValidCentPrecision(selectedAmount) &&
isMethodAvailable;
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!isValid || loading) return; if (!isValid || loading) return;
await onSubmit(selectedAmount, paymentType); await onSubmit(selectedAmount, effectivePaymentType);
}; };
const renderPaymentIcon = (type: string) => { const renderPaymentIcon = (type: string) => {
@@ -215,19 +214,17 @@ export default function PaymentForm({
</div> </div>
</div> </div>
{customAmount !== '' && !isValid && (() => { {customAmount !== '' &&
const num = parseFloat(customAmount); !isValid &&
let msg = '金额需在范围内,且最多支持 2 位小数(精确到分)'; (() => {
if (!isNaN(num)) { const num = parseFloat(customAmount);
if (num < minAmount) msg = `单笔最低充值 ¥${minAmount}`; let msg = '金额需在范围内,且最多支持 2 位小数(精确到分)';
else if (num > effectiveMax) msg = `单笔最高充值 ¥${effectiveMax}`; if (!isNaN(num)) {
} if (num < minAmount) msg = `单笔最低充值 ¥${minAmount}`;
return ( else if (num > effectiveMax) msg = `单笔最高充值 ¥${effectiveMax}`;
<div className={['text-xs', dark ? 'text-amber-300' : 'text-amber-700'].join(' ')}> }
{msg} return <div className={['text-xs', dark ? 'text-amber-300' : 'text-amber-700'].join(' ')}>{msg}</div>;
</div> })()}
);
})()}
{/* Payment Type — only show when multiple types available */} {/* Payment Type — only show when multiple types available */}
{enabledPaymentTypes.length > 1 && ( {enabledPaymentTypes.length > 1 && (
@@ -238,7 +235,7 @@ export default function PaymentForm({
<div className="flex gap-3"> <div className="flex gap-3">
{enabledPaymentTypes.map((type) => { {enabledPaymentTypes.map((type) => {
const meta = PAYMENT_TYPE_META[type]; const meta = PAYMENT_TYPE_META[type];
const isSelected = paymentType === type; const isSelected = effectivePaymentType === type;
const limitInfo = methodLimits?.[type]; const limitInfo = methodLimits?.[type];
const isUnavailable = limitInfo !== undefined && !limitInfo.available; const isUnavailable = limitInfo !== undefined && !limitInfo.available;
@@ -284,7 +281,7 @@ export default function PaymentForm({
{/* 当前选中渠道额度不足时的提示 */} {/* 当前选中渠道额度不足时的提示 */}
{(() => { {(() => {
const limitInfo = methodLimits?.[paymentType]; const limitInfo = methodLimits?.[effectivePaymentType];
if (!limitInfo || limitInfo.available) return null; if (!limitInfo || limitInfo.available) return null;
return ( return (
<p className={['mt-2 text-xs', dark ? 'text-amber-300' : 'text-amber-600'].join(' ')}> <p className={['mt-2 text-xs', dark ? 'text-amber-300' : 'text-amber-600'].join(' ')}>
@@ -311,10 +308,12 @@ export default function PaymentForm({
<span>{feeRate}%</span> <span>{feeRate}%</span>
<span>¥{feeAmount.toFixed(2)}</span> <span>¥{feeAmount.toFixed(2)}</span>
</div> </div>
<div className={[ <div
'flex items-center justify-between mt-1.5 pt-1.5 border-t font-medium', className={[
dark ? 'border-slate-700 text-slate-100' : 'border-slate-200 text-slate-900', 'flex items-center justify-between mt-1.5 pt-1.5 border-t font-medium',
].join(' ')}> dark ? 'border-slate-700 text-slate-100' : 'border-slate-200 text-slate-900',
].join(' ')}
>
<span></span> <span></span>
<span>¥{payAmount.toFixed(2)}</span> <span>¥{payAmount.toFixed(2)}</span>
</div> </div>
@@ -327,7 +326,7 @@ export default function PaymentForm({
disabled={!isValid || loading} disabled={!isValid || loading}
className={`w-full rounded-lg py-3 text-center font-medium text-white transition-colors ${ className={`w-full rounded-lg py-3 text-center font-medium text-white transition-colors ${
isValid && !loading isValid && !loading
? paymentType === 'stripe' ? effectivePaymentType === 'stripe'
? 'bg-[#635bff] hover:bg-[#5851db] active:bg-[#4b44c7]' ? 'bg-[#635bff] hover:bg-[#5851db] active:bg-[#4b44c7]'
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800' : 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800'
: dark : dark
@@ -335,7 +334,9 @@ export default function PaymentForm({
: 'cursor-not-allowed bg-gray-300' : 'cursor-not-allowed bg-gray-300'
}`} }`}
> >
{loading ? '处理中...' : `立即充值 ¥${(feeRate > 0 && selectedAmount > 0 ? payAmount : selectedAmount || 0).toFixed(2)}`} {loading
? '处理中...'
: `立即充值 ¥${(feeRate > 0 && selectedAmount > 0 ? payAmount : selectedAmount || 0).toFixed(2)}`}
</button> </button>
</form> </form>
); );