style: 统一代码格式 (prettier)

This commit is contained in:
erio
2026-03-10 18:20:36 +08:00
parent abff49222b
commit 9f621713c3
31 changed files with 631 additions and 406 deletions

View File

@@ -190,9 +190,7 @@ describe('GET /pay/[orderId]', () => {
);
const response = await GET(
createRequest(
'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148',
),
createRequest('Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148'),
{
params: Promise.resolve({ orderId: 'order-001' }),
},

View File

@@ -1,4 +1,3 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@/lib/config', () => ({

View File

@@ -1,4 +1,3 @@
import { describe, expect, it, vi } from 'vitest';
vi.mock('@/lib/config', () => ({

View File

@@ -101,12 +101,7 @@ function shouldAutoRedirect(opts: {
qrCode?: string | null;
isMobile: boolean;
}): boolean {
return (
!opts.expired &&
!isStripeType(opts.paymentType) &&
!!opts.payUrl &&
(opts.isMobile || !opts.qrCode)
);
return !opts.expired && !isStripeType(opts.paymentType) && !!opts.payUrl && (opts.isMobile || !opts.qrCode);
}
// ============================================================
@@ -381,9 +376,7 @@ describe('Payment Flow - PC/Mobile, QR/Redirect', () => {
});
it('Mobile: uses H5 order, returns payUrl (no qrCode)', async () => {
mockWxpayCreateH5Order.mockResolvedValue(
'https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx123',
);
mockWxpayCreateH5Order.mockResolvedValue('https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx123');
const request: CreatePaymentRequest = {
orderId: 'order-wx-002',

View File

@@ -39,33 +39,34 @@ function DashboardContent() {
const isDark = theme === 'dark';
const isEmbedded = uiMode === 'embedded';
const text = locale === 'en'
? {
missingToken: 'Missing admin token',
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
invalidToken: 'Invalid admin token',
requestFailed: 'Request failed',
loadFailed: 'Failed to load data',
title: 'Dashboard',
subtitle: 'Recharge order analytics and insights',
daySuffix: 'd',
orders: 'Order Management',
refresh: 'Refresh',
loading: 'Loading...',
}
: {
missingToken: '缺少管理员凭证',
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
invalidToken: '管理员凭证无效',
requestFailed: '请求失败',
loadFailed: '加载数据失败',
title: '数据概览',
subtitle: '充值订单统计与分析',
daySuffix: '天',
orders: '订单管理',
refresh: '刷新',
loading: '加载中...',
};
const text =
locale === 'en'
? {
missingToken: 'Missing admin token',
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
invalidToken: 'Invalid admin token',
requestFailed: 'Request failed',
loadFailed: 'Failed to load data',
title: 'Dashboard',
subtitle: 'Recharge order analytics and insights',
daySuffix: 'd',
orders: 'Order Management',
refresh: 'Refresh',
loading: 'Loading...',
}
: {
missingToken: '缺少管理员凭证',
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
invalidToken: '管理员凭证无效',
requestFailed: '请求失败',
loadFailed: '加载数据失败',
title: '数据概览',
subtitle: '充值订单统计与分析',
daySuffix: '',
orders: '订单管理',
refresh: '刷新',
loading: '加载中...',
};
const [days, setDays] = useState<number>(30);
const [data, setData] = useState<DashboardData | null>(null);
@@ -138,7 +139,8 @@ function DashboardContent() {
<>
{DAYS_OPTIONS.map((d) => (
<button key={d} type="button" onClick={() => setDays(d)} className={days === d ? btnActive : btnBase}>
{d}{text.daySuffix}
{d}
{text.daySuffix}
</button>
))}
<a href={`/admin?${navParams}`} className={btnBase}>
@@ -162,7 +164,7 @@ function DashboardContent() {
)}
{loading ? (
<div className={`py-24 text-center ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.loading}</div>
<div className={`py-24 text-center ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.loading}</div>
) : data ? (
<div className="space-y-6">
<DashboardStats summary={data.summary} dark={isDark} locale={locale} />
@@ -190,9 +192,7 @@ function DashboardPageFallback() {
export default function DashboardPage() {
return (
<Suspense
fallback={<DashboardPageFallback />}
>
<Suspense fallback={<DashboardPageFallback />}>
<DashboardContent />
</Suspense>
);

View File

@@ -52,67 +52,68 @@ function AdminContent() {
const isDark = theme === 'dark';
const isEmbedded = uiMode === 'embedded';
const text = locale === 'en'
? {
missingToken: 'Missing admin token',
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
invalidToken: 'Invalid admin token',
requestFailed: 'Request failed',
loadOrdersFailed: 'Failed to load orders',
retryConfirm: 'Retry recharge for this order?',
retryFailed: 'Retry failed',
retryRequestFailed: 'Retry request failed',
cancelConfirm: 'Cancel this order?',
cancelFailed: 'Cancel failed',
cancelRequestFailed: 'Cancel request failed',
loadDetailFailed: 'Failed to load order details',
title: 'Order Management',
subtitle: 'View and manage all recharge orders',
dashboard: 'Dashboard',
refresh: 'Refresh',
loading: 'Loading...',
statuses: {
'': 'All',
PENDING: 'Pending',
PAID: 'Paid',
RECHARGING: 'Recharging',
COMPLETED: 'Completed',
EXPIRED: 'Expired',
CANCELLED: 'Cancelled',
FAILED: 'Recharge failed',
REFUNDED: 'Refunded',
},
}
: {
missingToken: '缺少管理员凭证',
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
invalidToken: '管理员凭证无效',
requestFailed: '请求失败',
loadOrdersFailed: '加载订单列表失败',
retryConfirm: '确认重试充值?',
retryFailed: '重试失败',
retryRequestFailed: '重试请求失败',
cancelConfirm: '确认取消该订单?',
cancelFailed: '取消失败',
cancelRequestFailed: '取消请求失败',
loadDetailFailed: '加载订单详情失败',
title: '订单管理',
subtitle: '查看和管理所有充值订单',
dashboard: '数据概览',
refresh: '刷新',
loading: '加载中...',
statuses: {
'': '全部',
PENDING: '待支付',
PAID: '支付',
RECHARGING: '充值中',
COMPLETED: '已完成',
EXPIRED: '已超时',
CANCELLED: '已取消',
FAILED: '充值失败',
REFUNDED: '已退款',
},
};
const text =
locale === 'en'
? {
missingToken: 'Missing admin token',
missingTokenHint: 'Please access the admin page from the Sub2API platform.',
invalidToken: 'Invalid admin token',
requestFailed: 'Request failed',
loadOrdersFailed: 'Failed to load orders',
retryConfirm: 'Retry recharge for this order?',
retryFailed: 'Retry failed',
retryRequestFailed: 'Retry request failed',
cancelConfirm: 'Cancel this order?',
cancelFailed: 'Cancel failed',
cancelRequestFailed: 'Cancel request failed',
loadDetailFailed: 'Failed to load order details',
title: 'Order Management',
subtitle: 'View and manage all recharge orders',
dashboard: 'Dashboard',
refresh: 'Refresh',
loading: 'Loading...',
statuses: {
'': 'All',
PENDING: 'Pending',
PAID: 'Paid',
RECHARGING: 'Recharging',
COMPLETED: 'Completed',
EXPIRED: 'Expired',
CANCELLED: 'Cancelled',
FAILED: 'Recharge failed',
REFUNDED: 'Refunded',
},
}
: {
missingToken: '缺少管理员凭证',
missingTokenHint: '请从 Sub2API 平台正确访问管理页面',
invalidToken: '管理员凭证无效',
requestFailed: '请求失败',
loadOrdersFailed: '加载订单列表失败',
retryConfirm: '确认重试充值?',
retryFailed: '重试失败',
retryRequestFailed: '重试请求失败',
cancelConfirm: '确认取消该订单?',
cancelFailed: '取消失败',
cancelRequestFailed: '取消请求失败',
loadDetailFailed: '加载订单详情失败',
title: '订单管理',
subtitle: '查看和管理所有充值订单',
dashboard: '数据概览',
refresh: '刷新',
loading: '加载中...',
statuses: {
'': '全部',
PENDING: '支付',
PAID: '已支付',
RECHARGING: '充值中',
COMPLETED: '已完成',
EXPIRED: '已超时',
CANCELLED: '已取消',
FAILED: '充值失败',
REFUNDED: '已退款',
},
};
const [orders, setOrders] = useState<AdminOrder[]>([]);
const [total, setTotal] = useState(0);
@@ -321,7 +322,9 @@ function AdminContent() {
/>
{/* Order Detail */}
{detailOrder && <OrderDetail order={detailOrder} onClose={() => setDetailOrder(null)} dark={isDark} locale={locale} />}
{detailOrder && (
<OrderDetail order={detailOrder} onClose={() => setDetailOrder(null)} dark={isDark} locale={locale} />
)}
</PayPageLayout>
);
}
@@ -339,9 +342,7 @@ function AdminPageFallback() {
export default function AdminPage() {
return (
<Suspense
fallback={<AdminPageFallback />}
>
<Suspense fallback={<AdminPageFallback />}>
<AdminContent />
</Suspense>
);

View File

@@ -15,7 +15,10 @@ export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get('token')?.trim();
if (!token) {
return NextResponse.json({ error: locale === 'en' ? 'Missing token parameter' : '缺少 token 参数' }, { status: 401 });
return NextResponse.json(
{ error: locale === 'en' ? 'Missing token parameter' : '缺少 token 参数' },
{ status: 401 },
);
}
try {
@@ -28,7 +31,10 @@ export async function GET(request: NextRequest) {
}
if (tokenUser.id !== userId) {
return NextResponse.json({ error: locale === 'en' ? 'Forbidden to access this user' : '无权访问该用户信息' }, { status: 403 });
return NextResponse.json(
{ error: locale === 'en' ? 'Forbidden to access this user' : '无权访问该用户信息' },
{ status: 403 },
);
}
const env = getEnv();
@@ -77,9 +83,7 @@ export async function GET(request: NextRequest) {
helpImageUrl: env.PAY_HELP_IMAGE_URL ?? null,
helpText: env.PAY_HELP_TEXT ?? null,
stripePublishableKey:
enabledTypes.includes('stripe') && env.STRIPE_PUBLISHABLE_KEY
? env.STRIPE_PUBLISHABLE_KEY
: null,
enabledTypes.includes('stripe') && env.STRIPE_PUBLISHABLE_KEY ? env.STRIPE_PUBLISHABLE_KEY : null,
sublabelOverrides: Object.keys(sublabelOverrides).length > 0 ? sublabelOverrides : null,
},
});
@@ -89,6 +93,9 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: locale === 'en' ? 'User not found' : '用户不存在' }, { status: 404 });
}
console.error('Get user error:', error);
return NextResponse.json({ error: locale === 'en' ? 'Failed to fetch user info' : '获取用户信息失败' }, { status: 500 });
return NextResponse.json(
{ error: locale === 'en' ? 'Failed to fetch user info' : '获取用户信息失败' },
{ status: 500 },
);
}
}

View File

@@ -22,15 +22,11 @@ export async function POST(request: NextRequest) {
return Response.json({ code: 'SUCCESS', message: '成功' });
}
const success = await handlePaymentNotify(notification, provider.name);
return Response.json(
success ? { code: 'SUCCESS', message: '成功' } : { code: 'FAIL', message: '处理失败' },
{ status: success ? 200 : 500 },
);
return Response.json(success ? { code: 'SUCCESS', message: '成功' } : { code: 'FAIL', message: '处理失败' }, {
status: success ? 200 : 500,
});
} catch (error) {
console.error('Wxpay notify error:', error);
return Response.json(
{ code: 'FAIL', message: '处理失败' },
{ status: 500 },
);
return Response.json({ code: 'FAIL', message: '处理失败' }, { status: 500 });
}
}

View File

@@ -8,5 +8,11 @@
body {
background: var(--background);
color: var(--foreground);
font-family: system-ui, -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
font-family:
system-ui,
-apple-system,
'PingFang SC',
'Hiragino Sans GB',
'Microsoft YaHei',
sans-serif;
}

View File

@@ -30,8 +30,16 @@ function OrdersContent() {
const text = {
missingAuth: pickLocaleText(locale, '缺少认证信息', 'Missing authentication information'),
visitOrders: pickLocaleText(locale, '请从 Sub2API 平台正确访问订单页面', 'Please open the orders page from Sub2API'),
sessionExpired: pickLocaleText(locale, '登录态已失效,请从 Sub2API 重新进入支付页。', 'Session expired. Please re-enter from Sub2API.'),
visitOrders: pickLocaleText(
locale,
'请从 Sub2API 平台正确访问订单页面',
'Please open the orders page from Sub2API',
),
sessionExpired: pickLocaleText(
locale,
'登录态已失效,请从 Sub2API 重新进入支付页。',
'Session expired. Please re-enter from Sub2API.',
),
loadFailed: pickLocaleText(locale, '订单加载失败,请稍后重试。', 'Failed to load orders. Please try again later.'),
networkError: pickLocaleText(locale, '网络错误,请稍后重试。', 'Network error. Please try again later.'),
switchingMobileTab: pickLocaleText(locale, '正在切换到移动端订单 Tab...', 'Switching to mobile orders tab...'),
@@ -40,7 +48,11 @@ function OrdersContent() {
backToPay: pickLocaleText(locale, '返回充值', 'Back to Top Up'),
loading: pickLocaleText(locale, '加载中...', 'Loading...'),
userPrefix: pickLocaleText(locale, '用户', 'User'),
authError: pickLocaleText(locale, '缺少认证信息,请从 Sub2API 平台正确访问订单页面', 'Missing authentication information. Please open the orders page from Sub2API.'),
authError: pickLocaleText(
locale,
'缺少认证信息,请从 Sub2API 平台正确访问订单页面',
'Missing authentication information. Please open the orders page from Sub2API.',
),
};
const [isIframeContext, setIsIframeContext] = useState(true);

View File

@@ -156,8 +156,7 @@ function PayContent() {
}
}
}
} catch {
}
} catch {}
};
const loadMoreOrders = async () => {
@@ -203,7 +202,11 @@ function PayContent() {
<div className="text-center text-red-500">
<p className="text-lg font-medium">{pickLocaleText(locale, '缺少认证信息', 'Missing authentication info')}</p>
<p className="mt-2 text-sm text-gray-500">
{pickLocaleText(locale, '请从 Sub2API 平台正确访问充值页面', 'Please open the recharge page from the Sub2API platform')}
{pickLocaleText(
locale,
'请从 Sub2API 平台正确访问充值页面',
'Please open the recharge page from the Sub2API platform',
)}
</p>
</div>
</div>
@@ -216,7 +219,11 @@ function PayContent() {
<div className="text-center text-red-500">
<p className="text-lg font-medium">{pickLocaleText(locale, '用户不存在', 'User not found')}</p>
<p className="mt-2 text-sm text-gray-500">
{pickLocaleText(locale, '请检查链接是否正确,或联系管理员', 'Please check whether the link is correct or contact the administrator')}
{pickLocaleText(
locale,
'请检查链接是否正确,或联系管理员',
'Please check whether the link is correct or contact the administrator',
)}
</p>
</div>
</div>
@@ -272,15 +279,33 @@ function PayContent() {
if (!res.ok) {
const codeMessages: Record<string, string> = {
INVALID_TOKEN: pickLocaleText(locale, '认证已失效,请重新从平台进入充值页面', 'Authentication expired. Please re-enter the recharge page from the platform'),
USER_INACTIVE: pickLocaleText(locale, '账户已被禁用,无法充值,请联系管理员', 'This account is disabled and cannot be recharged. Please contact the administrator'),
TOO_MANY_PENDING: pickLocaleText(locale, '您有过多待支付订单,请先完成或取消现有订单后再试', 'You have too many pending orders. Please complete or cancel existing orders first'),
USER_NOT_FOUND: pickLocaleText(locale, '用户不存在,请检查链接是否正确', 'User not found. Please check whether the link is correct'),
INVALID_TOKEN: pickLocaleText(
locale,
'认证已失效,请重新从平台进入充值页面',
'Authentication expired. Please re-enter the recharge page from the platform',
),
USER_INACTIVE: pickLocaleText(
locale,
'账户已被禁用,无法充值,请联系管理员',
'This account is disabled and cannot be recharged. Please contact the administrator',
),
TOO_MANY_PENDING: pickLocaleText(
locale,
'您有过多待支付订单,请先完成或取消现有订单后再试',
'You have too many pending orders. Please complete or cancel existing orders first',
),
USER_NOT_FOUND: pickLocaleText(
locale,
'用户不存在,请检查链接是否正确',
'User not found. Please check whether the link is correct',
),
DAILY_LIMIT_EXCEEDED: data.error,
METHOD_DAILY_LIMIT_EXCEEDED: data.error,
PAYMENT_GATEWAY_ERROR: data.error,
};
setError(codeMessages[data.code] || data.error || pickLocaleText(locale, '创建订单失败', 'Failed to create order'));
setError(
codeMessages[data.code] || data.error || pickLocaleText(locale, '创建订单失败', 'Failed to create order'),
);
return;
}
@@ -481,11 +506,24 @@ function PayContent() {
{pickLocaleText(locale, '支付说明', 'Payment Notes')}
</div>
<ul className={['mt-2 space-y-1 text-sm', isDark ? 'text-slate-300' : 'text-slate-600'].join(' ')}>
<li>{pickLocaleText(locale, '订单完成后会自动到账', 'Balance will be credited automatically after the order completes')}</li>
<li>{pickLocaleText(locale, '如需历史记录请查看「我的订单」', 'Check "My Orders" for payment history')}</li>
<li>
{pickLocaleText(
locale,
'订单完成后会自动到账',
'Balance will be credited automatically after the order completes',
)}
</li>
<li>
{pickLocaleText(
locale,
'如需历史记录请查看「我的订单」',
'Check "My Orders" for payment history',
)}
</li>
{config.maxDailyAmount > 0 && (
<li>
{pickLocaleText(locale, '每日最大充值', 'Maximum daily recharge')} ¥{config.maxDailyAmount.toFixed(2)}
{pickLocaleText(locale, '每日最大充值', 'Maximum daily recharge')} ¥
{config.maxDailyAmount.toFixed(2)}
</li>
)}
</ul>
@@ -551,18 +589,17 @@ function PayContent() {
/>
)}
{step === 'result' && orderResult && finalOrderState && (
<OrderStatus
orderId={orderResult.orderId}
order={finalOrderState}
statusAccessToken={orderResult.statusAccessToken}
onStateChange={setFinalOrderState}
onBack={handleBack}
dark={isDark}
locale={locale}
/>
)}
{step === 'result' && orderResult && finalOrderState && (
<OrderStatus
orderId={orderResult.orderId}
order={finalOrderState}
statusAccessToken={orderResult.statusAccessToken}
onStateChange={setFinalOrderState}
onBack={handleBack}
dark={isDark}
locale={locale}
/>
)}
{helpImageOpen && helpImageUrl && (
<div
@@ -594,9 +631,7 @@ function PayPageFallback() {
export default function PayPage() {
return (
<Suspense
fallback={<PayPageFallback />}
>
<Suspense fallback={<PayPageFallback />}>
<PayContent />
</Suspense>
);

View File

@@ -1,4 +1,3 @@
'use client';
import { useSearchParams } from 'next/navigation';
@@ -58,27 +57,60 @@ function closeCurrentWindow() {
function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale, hasAccessToken: boolean) {
if (!order) {
return locale === 'en'
? { label: 'Payment Error', color: 'text-red-600', icon: '✗', message: hasAccessToken ? 'Unable to load the order status. Please try again later.' : 'Missing order access token. Please go back to the recharge page.' }
: { label: '支付异常', color: 'text-red-600', icon: '✗', message: hasAccessToken ? '未查询到订单状态,请稍后重试。' : '订单访问凭证缺失,请返回原充值页查看订单结果。' };
? {
label: 'Payment Error',
color: 'text-red-600',
icon: '✗',
message: hasAccessToken
? 'Unable to load the order status. Please try again later.'
: 'Missing order access token. Please go back to the recharge page.',
}
: {
label: '支付异常',
color: 'text-red-600',
icon: '✗',
message: hasAccessToken ? '未查询到订单状态,请稍后重试。' : '订单访问凭证缺失,请返回原充值页查看订单结果。',
};
}
if (order.rechargeSuccess) {
return locale === 'en'
? { label: 'Recharge Successful', color: 'text-green-600', icon: '✓', message: 'Your balance has been credited successfully.' }
? {
label: 'Recharge Successful',
color: 'text-green-600',
icon: '✓',
message: 'Your balance has been credited successfully.',
}
: { label: '充值成功', color: 'text-green-600', icon: '✓', message: '余额已成功到账!' };
}
if (order.paymentSuccess) {
if (order.rechargeStatus === 'paid_pending' || order.rechargeStatus === 'recharging') {
return locale === 'en'
? { label: 'Top-up Processing', color: 'text-blue-600', icon: '⟳', message: 'Payment succeeded, and the balance top-up is being processed.' }
? {
label: 'Top-up Processing',
color: 'text-blue-600',
icon: '⟳',
message: 'Payment succeeded, and the balance top-up is being processed.',
}
: { label: '充值处理中', color: 'text-blue-600', icon: '⟳', message: '支付成功,余额正在充值中...' };
}
if (order.rechargeStatus === 'failed') {
return locale === 'en'
? { label: 'Payment Successful', color: 'text-amber-600', icon: '!', message: 'Payment succeeded, but the balance top-up has not completed yet. Please check again later or contact the administrator.' }
: { label: '支付成功', color: 'text-amber-600', icon: '!', message: '支付成功,但余额充值暂未完成,请稍后查看订单结果或联系管理员。' };
? {
label: 'Payment Successful',
color: 'text-amber-600',
icon: '!',
message:
'Payment succeeded, but the balance top-up has not completed yet. Please check again later or contact the administrator.',
}
: {
label: '支付成功',
color: 'text-amber-600',
icon: '!',
message: '支付成功,但余额充值暂未完成,请稍后查看订单结果或联系管理员。',
};
}
}
@@ -90,7 +122,12 @@ function getStatusConfig(order: PublicOrderStatusSnapshot | null, locale: Locale
if (order.status === 'EXPIRED') {
return locale === 'en'
? { label: 'Order Expired', color: 'text-gray-500', icon: '⏰', message: 'This order has expired. Please create a new order.' }
? {
label: 'Order Expired',
color: 'text-gray-500',
icon: '⏰',
message: 'This order has expired. Please create a new order.',
}
: { label: '订单已超时', color: 'text-gray-500', icon: '⏰', message: '订单已超时,请重新充值。' };
}
@@ -224,11 +261,7 @@ function ResultContent() {
</div>
)
) : (
<button
type="button"
onClick={goBack}
className="mt-4 text-sm text-blue-600 underline hover:text-blue-700"
>
<button type="button" onClick={goBack} className="mt-4 text-sm text-blue-600 underline hover:text-blue-700">
{text.back}
</button>
)}

View File

@@ -19,12 +19,20 @@ function StripePopupContent() {
const text = {
init: pickLocaleText(locale, '正在初始化...', 'Initializing...'),
orderId: pickLocaleText(locale, '订单号', 'Order ID'),
loadFailed: pickLocaleText(locale, '支付组件加载失败,请关闭窗口重试', 'Failed to load payment component. Please close the window and try again.'),
loadFailed: pickLocaleText(
locale,
'支付组件加载失败,请关闭窗口重试',
'Failed to load payment component. Please close the window and try again.',
),
payFailed: pickLocaleText(locale, '支付失败,请重试', 'Payment failed. Please try again.'),
closeWindow: pickLocaleText(locale, '关闭窗口', 'Close window'),
redirecting: pickLocaleText(locale, '正在跳转到支付页面...', 'Redirecting to payment page...'),
loadingForm: pickLocaleText(locale, '正在加载支付表单...', 'Loading payment form...'),
successClosing: pickLocaleText(locale, '支付成功,窗口即将自动关闭...', 'Payment successful. This window will close automatically...'),
successClosing: pickLocaleText(
locale,
'支付成功,窗口即将自动关闭...',
'Payment successful. This window will close automatically...',
),
closeWindowManually: pickLocaleText(locale, '手动关闭窗口', 'Close window manually'),
processing: pickLocaleText(locale, '处理中...', 'Processing...'),
payAmount: pickLocaleText(locale, `支付 ¥${amount.toFixed(2)}`, `Pay ¥${amount.toFixed(2)}`),
@@ -191,11 +199,17 @@ function StripePopupContent() {
{'¥'}
{amount.toFixed(2)}
</div>
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.orderId}: {orderId}</p>
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{text.orderId}: {orderId}
</p>
</div>
{stripeError ? (
<div className="space-y-3">
<div className={`rounded-lg border p-3 text-sm ${isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600'}`}>{stripeError}</div>
<div
className={`rounded-lg border p-3 text-sm ${isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600'}`}
>
{stripeError}
</div>
<button
type="button"
onClick={() => window.close()}
@@ -207,9 +221,7 @@ function StripePopupContent() {
) : (
<div className="flex items-center justify-center py-8">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-[#635bff] border-t-transparent" />
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{text.redirecting}
</span>
<span className={`ml-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.redirecting}</span>
</div>
)}
</div>
@@ -227,7 +239,9 @@ function StripePopupContent() {
{'¥'}
{amount.toFixed(2)}
</div>
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.orderId}: {orderId}</p>
<p className={`mt-1 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{text.orderId}: {orderId}
</p>
</div>
{!stripeLoaded ? (
@@ -238,9 +252,7 @@ function StripePopupContent() {
) : stripeSuccess ? (
<div className="py-6 text-center">
<div className="text-5xl text-green-600">{'✓'}</div>
<p className={`mt-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>
{text.successClosing}
</p>
<p className={`mt-3 text-sm ${isDark ? 'text-slate-400' : 'text-gray-500'}`}>{text.successClosing}</p>
<button
type="button"
onClick={() => window.close()}
@@ -252,7 +264,11 @@ function StripePopupContent() {
) : (
<>
{stripeError && (
<div className={`rounded-lg border p-3 text-sm ${isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600'}`}>{stripeError}</div>
<div
className={`rounded-lg border p-3 text-sm ${isDark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600'}`}
>
{stripeError}
</div>
)}
<div
ref={stripeContainerRef}
@@ -264,9 +280,7 @@ function StripePopupContent() {
onClick={handleSubmit}
className={[
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
stripeSubmitting
? 'bg-gray-400 cursor-not-allowed'
: getPaymentMeta('stripe').buttonClass,
stripeSubmitting ? 'bg-gray-400 cursor-not-allowed' : getPaymentMeta('stripe').buttonClass,
].join(' ')}
>
{stripeSubmitting ? (

View File

@@ -1,4 +1,3 @@
'use client';
import { useEffect, useRef, useState } from 'react';
@@ -19,28 +18,61 @@ interface OrderStatusProps {
function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
if (order.rechargeSuccess) {
return locale === 'en'
? { label: 'Recharge Successful', color: 'text-green-600', icon: '✓', message: 'Your balance has been credited. Thank you for your payment.' }
? {
label: 'Recharge Successful',
color: 'text-green-600',
icon: '✓',
message: 'Your balance has been credited. Thank you for your payment.',
}
: { label: '充值成功', color: 'text-green-600', icon: '✓', message: '余额已到账,感谢您的充值!' };
}
if (order.paymentSuccess) {
if (order.rechargeStatus === 'paid_pending' || order.rechargeStatus === 'recharging') {
return locale === 'en'
? { label: 'Recharging', color: 'text-blue-600', icon: '⟳', message: 'Payment received. Recharging your balance...' }
? {
label: 'Recharging',
color: 'text-blue-600',
icon: '⟳',
message: 'Payment received. Recharging your balance...',
}
: { label: '充值中', color: 'text-blue-600', icon: '⟳', message: '支付成功,正在充值余额中,请稍候...' };
}
if (order.rechargeStatus === 'failed') {
return locale === 'en'
? { label: 'Payment Successful', color: 'text-amber-600', icon: '!', message: 'Payment completed, but the balance top-up has not finished yet. The system may retry automatically. Please check the order list later or contact the administrator if it remains unresolved.' }
: { label: '支付成功', color: 'text-amber-600', icon: '!', message: '支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。' };
? {
label: 'Payment Successful',
color: 'text-amber-600',
icon: '!',
message:
'Payment completed, but the balance top-up has not finished yet. The system may retry automatically. Please check the order list later or contact the administrator if it remains unresolved.',
}
: {
label: '支付成功',
color: 'text-amber-600',
icon: '!',
message:
'支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。',
};
}
}
if (order.status === 'FAILED') {
return locale === 'en'
? { label: 'Payment Failed', color: 'text-red-600', icon: '✗', message: 'Payment was not completed. Please try again. If funds were deducted but not credited, contact the administrator.' }
: { label: '支付失败', color: 'text-red-600', icon: '✗', message: '支付未完成,请重新发起支付;如已扣款未到账,请联系管理员处理。' };
? {
label: 'Payment Failed',
color: 'text-red-600',
icon: '✗',
message:
'Payment was not completed. Please try again. If funds were deducted but not credited, contact the administrator.',
}
: {
label: '支付失败',
color: 'text-red-600',
icon: '✗',
message: '支付未完成,请重新发起支付;如已扣款未到账,请联系管理员处理。',
};
}
if (order.status === 'PENDING') {
@@ -51,7 +83,12 @@ function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
if (order.status === 'EXPIRED') {
return locale === 'en'
? { label: 'Order Expired', color: 'text-gray-500', icon: '⏰', message: 'This order has expired. Please create a new one.' }
? {
label: 'Order Expired',
color: 'text-gray-500',
icon: '⏰',
message: 'This order has expired. Please create a new one.',
}
: { label: '订单超时', color: 'text-gray-500', icon: '⏰', message: '订单已超时,请重新创建订单。' };
}
@@ -62,7 +99,12 @@ function getStatusConfig(order: PublicOrderStatusSnapshot, locale: Locale) {
}
return locale === 'en'
? { label: 'Payment Error', color: 'text-red-600', icon: '✗', message: 'Payment status is abnormal. Please contact the administrator.' }
? {
label: 'Payment Error',
color: 'text-red-600',
icon: '✗',
message: 'Payment status is abnormal. Please contact the administrator.',
}
: { label: '支付异常', color: 'text-red-600', icon: '✗', message: '支付状态异常,请联系管理员处理。' };
}
@@ -100,8 +142,7 @@ export default function OrderStatus({
if (cancelled) return;
setCurrentOrder(nextOrder);
onStateChangeRef.current?.(nextOrder);
} catch {
}
} catch {}
};
refreshOrder();

View File

@@ -1,5 +1,11 @@
import type { Locale } from '@/lib/locale';
import { formatStatus, formatCreatedAt, getStatusBadgeClass, getPaymentDisplayInfo, type MyOrder } from '@/lib/pay-utils';
import {
formatStatus,
formatCreatedAt,
getStatusBadgeClass,
getPaymentDisplayInfo,
type MyOrder,
} from '@/lib/pay-utils';
interface OrderTableProps {
isDark: boolean;
@@ -98,7 +104,9 @@ export default function OrderTable({ isDark, locale, loading, error, orders }: O
{formatStatus(order.status, locale)}
</span>
</div>
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>{formatCreatedAt(order.createdAt, locale)}</div>
<div className={isDark ? 'text-slate-300' : 'text-slate-600'}>
{formatCreatedAt(order.createdAt, locale)}
</div>
</div>
))}
</div>

View File

@@ -223,12 +223,15 @@ export default function PaymentForm({
!isValid &&
(() => {
const num = parseFloat(customAmount);
let msg = locale === 'en'
? 'Amount must be within range and support up to 2 decimal places'
: '金额需在范围内,且最多支持 2 位小数(精确到分)';
let msg =
locale === 'en'
? 'Amount must be within range and support up to 2 decimal places'
: '金额需在范围内,且最多支持 2 位小数(精确到分)';
if (!isNaN(num)) {
if (num < minAmount) msg = locale === 'en' ? `Minimum per transaction: ¥${minAmount}` : `单笔最低充值 ¥${minAmount}`;
else if (num > effectiveMax) msg = locale === 'en' ? `Maximum per transaction: ¥${effectiveMax}` : `单笔最充值 ¥${effectiveMax}`;
if (num < minAmount)
msg = locale === 'en' ? `Minimum per transaction: ¥${minAmount}` : `单笔最充值 ¥${minAmount}`;
else if (num > effectiveMax)
msg = locale === 'en' ? `Maximum per transaction: ¥${effectiveMax}` : `单笔最高充值 ¥${effectiveMax}`;
}
return <div className={['text-xs', dark ? 'text-amber-300' : 'text-amber-700'].join(' ')}>{msg}</div>;
})()}
@@ -252,7 +255,13 @@ export default function PaymentForm({
type="button"
disabled={isUnavailable}
onClick={() => !isUnavailable && setPaymentType(type)}
title={isUnavailable ? (locale === 'en' ? 'Daily limit reached, please use another payment method' : '今日充值额度已满,请使用其他支付方式') : undefined}
title={
isUnavailable
? locale === 'en'
? 'Daily limit reached, please use another payment method'
: '今日充值额度已满,请使用其他支付方式'
: undefined
}
className={[
'relative flex h-[58px] flex-col items-center justify-center rounded-lg border px-3 transition-all sm:flex-1',
isUnavailable
@@ -260,7 +269,7 @@ export default function PaymentForm({
? 'cursor-not-allowed border-slate-700 bg-slate-800/50 opacity-50'
: 'cursor-not-allowed border-gray-200 bg-gray-50 opacity-50'
: isSelected
? `${meta?.selectedBorder || 'border-blue-500'} ${dark ? (meta?.selectedBgDark || 'bg-blue-950') : (meta?.selectedBg || 'bg-blue-50')} ${dark ? 'text-slate-100' : 'text-slate-900'} shadow-sm`
? `${meta?.selectedBorder || 'border-blue-500'} ${dark ? meta?.selectedBgDark || 'bg-blue-950' : meta?.selectedBg || 'bg-blue-50'} ${dark ? 'text-slate-100' : 'text-slate-900'} shadow-sm`
: dark
? 'border-slate-700 bg-slate-900 text-slate-200 hover:border-slate-500'
: 'border-gray-300 bg-white text-slate-700 hover:border-gray-400',
@@ -271,7 +280,9 @@ export default function PaymentForm({
<span className="flex flex-col items-start leading-none">
<span className="text-xl font-semibold tracking-tight">{displayInfo.channel || type}</span>
{isUnavailable ? (
<span className="text-[10px] tracking-wide text-red-400">{locale === 'en' ? 'Daily limit reached' : '今日额度已满'}</span>
<span className="text-[10px] tracking-wide text-red-400">
{locale === 'en' ? 'Daily limit reached' : '今日额度已满'}
</span>
) : displayInfo.sublabel ? (
<span
className={`text-[10px] tracking-wide ${dark ? (isSelected ? 'text-slate-300' : 'text-slate-400') : 'text-slate-600'}`}
@@ -292,7 +303,7 @@ export default function PaymentForm({
return (
<p className={['mt-2 text-xs', dark ? 'text-amber-300' : 'text-amber-600'].join(' ')}>
{locale === 'en'
? 'The selected payment method has reached today\'s limit. Please switch to another method.'
? "The selected payment method has reached today's limit. Please switch to another method."
: '所选支付方式今日额度已满,请切换到其他支付方式'}
</p>
);
@@ -331,9 +342,7 @@ export default function PaymentForm({
<div
className={[
'rounded-lg border p-3 text-sm',
dark
? 'border-amber-700 bg-amber-900/30 text-amber-300'
: 'border-amber-200 bg-amber-50 text-amber-700',
dark ? 'border-amber-700 bg-amber-900/30 text-amber-300' : 'border-amber-200 bg-amber-50 text-amber-700',
].join(' ')}
>
{locale === 'en'

View File

@@ -4,12 +4,7 @@ import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
import QRCode from 'qrcode';
import type { Locale } from '@/lib/locale';
import type { PublicOrderStatusSnapshot } from '@/lib/order/status';
import {
isStripeType,
getPaymentMeta,
getPaymentIconSrc,
getPaymentChannelLabel,
} from '@/lib/pay-utils';
import { isStripeType, getPaymentMeta, getPaymentIconSrc, getPaymentChannelLabel } from '@/lib/pay-utils';
import { buildOrderStatusUrl } from '@/lib/order/status-url';
import { TERMINAL_STATUSES } from '@/lib/constants';
@@ -84,24 +79,38 @@ export default function PaymentQRCode({
scanPay: locale === 'en' ? 'Please scan with your payment app' : '请使用支付应用扫码支付',
back: locale === 'en' ? 'Back' : '返回',
cancelOrder: locale === 'en' ? 'Cancel Order' : '取消订单',
h5Hint: locale === 'en' ? 'After payment, please return to this page. The system will confirm automatically.' : '支付完成后请返回此页面,系统将自动确认',
h5Hint:
locale === 'en'
? 'After payment, please return to this page. The system will confirm automatically.'
: '支付完成后请返回此页面,系统将自动确认',
paid: locale === 'en' ? 'Order Paid' : '订单已支付',
paidCancelBlocked:
locale === 'en' ? 'This order has already been paid and cannot be cancelled. The recharge will be credited automatically.' : '该订单已支付完成,无法取消。充值将自动到账。',
locale === 'en'
? 'This order has already been paid and cannot be cancelled. The recharge will be credited automatically.'
: '该订单已支付完成,无法取消。充值将自动到账。',
backToRecharge: locale === 'en' ? 'Back to Recharge' : '返回充值',
credited: locale === 'en' ? 'Credited ¥' : '到账 ¥',
stripeLoadFailed: locale === 'en' ? 'Failed to load payment component. Please refresh and try again.' : '支付组件加载失败,请刷新页面重试',
initFailed: locale === 'en' ? 'Payment initialization failed. Please go back and try again.' : '支付初始化失败,请返回重试',
stripeLoadFailed:
locale === 'en'
? 'Failed to load payment component. Please refresh and try again.'
: '支付组件加载失败,请刷新页面重试',
initFailed:
locale === 'en' ? 'Payment initialization failed. Please go back and try again.' : '支付初始化失败,请返回重试',
loadingForm: locale === 'en' ? 'Loading payment form...' : '正在加载支付表单...',
payFailed: locale === 'en' ? 'Payment failed. Please try again.' : '支付失败,请重试',
successProcessing: locale === 'en' ? 'Payment successful, processing your order...' : '支付成功,正在处理订单...',
processing: locale === 'en' ? 'Processing...' : '处理中...',
payNow: locale === 'en' ? 'Pay' : '支付',
popupBlocked:
locale === 'en' ? 'Popup was blocked by your browser. Please allow popups for this site and try again.' : '弹出窗口被浏览器拦截,请允许本站弹出窗口后重试',
locale === 'en'
? 'Popup was blocked by your browser. Please allow popups for this site and try again.'
: '弹出窗口被浏览器拦截,请允许本站弹出窗口后重试',
redirectingPrefix: locale === 'en' ? 'Redirecting to ' : '正在跳转到',
redirectingSuffix: locale === 'en' ? '...' : '...',
redirectRetryHint: locale === 'en' ? 'If the payment app does not open automatically, go back and try again.' : '如未自动拉起支付应用,请返回上一页后重新发起支付。',
redirectRetryHint:
locale === 'en'
? 'If the payment app does not open automatically, go back and try again.'
: '如未自动拉起支付应用,请返回上一页后重新发起支付。',
notRedirectedPrefix: locale === 'en' ? 'Not redirected? Open ' : '未跳转?点击前往',
goPaySuffix: locale === 'en' ? '' : '',
gotoPrefix: locale === 'en' ? 'Open ' : '前往',
@@ -327,8 +336,7 @@ export default function PaymentQRCode({
onStatusChange(data);
}
}
} catch {
}
} catch {}
}, [orderId, onStatusChange, statusAccessToken]);
useEffect(() => {
@@ -372,8 +380,7 @@ export default function PaymentQRCode({
} else {
await pollStatus();
}
} catch {
}
} catch {}
};
const meta = getPaymentMeta(paymentType || 'alipay');
@@ -412,7 +419,9 @@ export default function PaymentQRCode({
{amount.toFixed(2)}
</div>
)}
<div className={`mt-1 text-sm ${expired ? 'text-red-500' : !expired && timeLeftSeconds <= 60 ? 'text-red-500 animate-pulse' : dark ? 'text-slate-400' : 'text-gray-500'}`}>
<div
className={`mt-1 text-sm ${expired ? 'text-red-500' : !expired && timeLeftSeconds <= 60 ? 'text-red-500 animate-pulse' : dark ? 'text-slate-400' : 'text-gray-500'}`}
>
{expired ? t.expired : `${t.remaining}: ${timeLeft}`}
</div>
</div>
@@ -428,9 +437,7 @@ export default function PaymentQRCode({
dark ? 'border-slate-700' : 'border-gray-300',
].join(' ')}
>
<p className={['text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
{t.initFailed}
</p>
<p className={['text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>{t.initFailed}</p>
</div>
) : !stripeLoaded ? (
<div className="flex items-center justify-center py-8">
@@ -440,10 +447,14 @@ export default function PaymentQRCode({
</span>
</div>
) : stripeError && !stripeLib ? (
<div className={[
'rounded-lg border p-3 text-sm',
dark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600',
].join(' ')}>{stripeError}</div>
<div
className={[
'rounded-lg border p-3 text-sm',
dark ? 'border-red-700 bg-red-900/30 text-red-400' : 'border-red-200 bg-red-50 text-red-600',
].join(' ')}
>
{stripeError}
</div>
) : (
<>
<div
@@ -472,9 +483,7 @@ export default function PaymentQRCode({
onClick={handleStripeSubmit}
className={[
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
stripeSubmitting
? 'cursor-not-allowed bg-gray-400'
: meta.buttonClass,
stripeSubmitting ? 'cursor-not-allowed bg-gray-400' : meta.buttonClass,
].join(' ')}
>
{stripeSubmitting ? (
@@ -505,7 +514,10 @@ export default function PaymentQRCode({
) : shouldAutoRedirect ? (
<>
<div className="flex items-center justify-center py-6">
<div className={`h-8 w-8 animate-spin rounded-full border-2 border-t-transparent`} style={{ borderColor: meta.color, borderTopColor: 'transparent' }} />
<div
className={`h-8 w-8 animate-spin rounded-full border-2 border-t-transparent`}
style={{ borderColor: meta.color, borderTopColor: 'transparent' }}
/>
<span className={['ml-3 text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
{`${t.redirectingPrefix}${channelLabel}${t.redirectingSuffix}`}
</span>
@@ -517,11 +529,11 @@ export default function PaymentQRCode({
className={`flex w-full items-center justify-center gap-2 rounded-lg py-3 font-medium text-white shadow-md ${meta.buttonClass}`}
>
{iconSrc && <img src={iconSrc} alt={channelLabel} className="h-5 w-5 brightness-0 invert" />}
{redirected ? `${t.notRedirectedPrefix}${channelLabel}` : `${t.gotoPrefix}${channelLabel}${t.gotoSuffix}`}
{redirected
? `${t.notRedirectedPrefix}${channelLabel}`
: `${t.gotoPrefix}${channelLabel}${t.gotoSuffix}`}
</a>
<p className={['text-center text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
{t.h5Hint}
</p>
<p className={['text-center text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>{t.h5Hint}</p>
</>
) : (
<>
@@ -584,9 +596,7 @@ export default function PaymentQRCode({
onClick={handleCancel}
className={[
'flex-1 rounded-lg border py-2 text-sm',
dark
? 'border-red-700 text-red-400 hover:bg-red-900/30'
: 'border-red-300 text-red-600 hover:bg-red-50',
dark ? 'border-red-700 text-red-400 hover:bg-red-900/30' : 'border-red-300 text-red-600 hover:bg-red-50',
].join(' ')}
>
{t.cancelOrder}

View File

@@ -85,7 +85,9 @@ export default function DailyChart({ data, dark, locale = 'zh' }: DailyChartProp
<h3 className={['mb-4 text-sm font-semibold', dark ? 'text-slate-200' : 'text-slate-800'].join(' ')}>
{chartTitle}
</h3>
<p className={['text-center text-sm py-16', dark ? 'text-slate-500' : 'text-gray-400'].join(' ')}>{emptyText}</p>
<p className={['text-center text-sm py-16', dark ? 'text-slate-500' : 'text-gray-400'].join(' ')}>
{emptyText}
</p>
</div>
);
}
@@ -121,7 +123,11 @@ export default function DailyChart({ data, dark, locale = 'zh' }: DailyChartProp
tickLine={false}
width={60}
/>
<Tooltip content={<CustomTooltip dark={dark} currency={currency} amountLabel={amountLabel} countLabel={countLabel} />} />
<Tooltip
content={
<CustomTooltip dark={dark} currency={currency} amountLabel={amountLabel} countLabel={countLabel} />
}
/>
<Line
type="monotone"
dataKey="amount"

View File

@@ -18,9 +18,20 @@ interface DashboardStatsProps {
export default function DashboardStats({ summary, dark, locale = 'zh' }: DashboardStatsProps) {
const currency = locale === 'en' ? '$' : '¥';
const cards = [
{ label: locale === 'en' ? 'Today Recharge' : '今日充值', value: `${currency}${summary.today.amount.toLocaleString()}`, accent: true },
{ label: locale === 'en' ? 'Today Orders' : '今日订单', value: `${summary.today.paidCount}/${summary.today.orderCount}` },
{ label: locale === 'en' ? 'Total Recharge' : '累计充值', value: `${currency}${summary.total.amount.toLocaleString()}`, accent: true },
{
label: locale === 'en' ? 'Today Recharge' : '今日充值',
value: `${currency}${summary.today.amount.toLocaleString()}`,
accent: true,
},
{
label: locale === 'en' ? 'Today Orders' : '今日订单',
value: `${summary.today.paidCount}/${summary.today.orderCount}`,
},
{
label: locale === 'en' ? 'Total Recharge' : '累计充值',
value: `${currency}${summary.total.amount.toLocaleString()}`,
accent: true,
},
{ label: locale === 'en' ? 'Paid Orders' : '累计订单', value: String(summary.total.paidCount) },
{ label: locale === 'en' ? 'Success Rate' : '成功率', value: `${summary.successRate}%` },
{ label: locale === 'en' ? 'Average Amount' : '平均充值', value: `${currency}${summary.avgAmount.toFixed(2)}` },

View File

@@ -97,7 +97,8 @@ export default function Leaderboard({ data, dark, locale = 'zh' }: LeaderboardPr
<td
className={`whitespace-nowrap px-4 py-3 text-sm font-medium ${dark ? 'text-slate-200' : 'text-slate-900'}`}
>
{currency}{entry.totalAmount.toLocaleString()}
{currency}
{entry.totalAmount.toLocaleString()}
</td>
<td className={tdMuted}>{entry.orderCount}</td>
</tr>

View File

@@ -49,77 +49,78 @@ interface OrderDetailProps {
export default function OrderDetail({ order, onClose, dark, locale = 'zh' }: OrderDetailProps) {
const currency = locale === 'en' ? '$' : '¥';
const text = locale === 'en'
? {
title: 'Order Details',
auditLogs: 'Audit Logs',
operator: 'Operator',
emptyLogs: 'No logs',
close: 'Close',
yes: 'Yes',
no: 'No',
orderId: 'Order ID',
userId: 'User ID',
userName: 'Username',
email: 'Email',
amount: 'Amount',
status: 'Status',
paymentSuccess: 'Payment Success',
rechargeSuccess: 'Recharge Success',
rechargeStatus: 'Recharge Status',
paymentChannel: 'Payment Channel',
provider: 'Provider',
rechargeCode: 'Recharge Code',
paymentTradeNo: 'Payment Trade No.',
clientIp: 'Client IP',
sourceHost: 'Source Host',
sourcePage: 'Source Page',
createdAt: 'Created At',
expiresAt: 'Expires At',
paidAt: 'Paid At',
completedAt: 'Completed At',
failedAt: 'Failed At',
failedReason: 'Failure Reason',
refundAmount: 'Refund Amount',
refundReason: 'Refund Reason',
refundAt: 'Refunded At',
forceRefund: 'Force Refund',
}
: {
title: '订单详情',
auditLogs: '审计日志',
operator: '操作者',
emptyLogs: '暂无日志',
close: '关闭',
yes: '',
no: '',
orderId: '订单号',
userId: '用户ID',
userName: '用户',
email: '邮箱',
amount: '金额',
status: '状态',
paymentSuccess: '支付成功',
rechargeSuccess: '充值成功',
rechargeStatus: '充值状态',
paymentChannel: '支付渠道',
provider: '提供商',
rechargeCode: '充值码',
paymentTradeNo: '支付单号',
clientIp: '客户端IP',
sourceHost: '来源域名',
sourcePage: '来源页面',
createdAt: '创建时间',
expiresAt: '过期时间',
paidAt: '支付时间',
completedAt: '完成时间',
failedAt: '失败时间',
failedReason: '失败原因',
refundAmount: '退款金额',
refundReason: '退款原因',
refundAt: '退款时间',
forceRefund: '强制退款',
};
const text =
locale === 'en'
? {
title: 'Order Details',
auditLogs: 'Audit Logs',
operator: 'Operator',
emptyLogs: 'No logs',
close: 'Close',
yes: 'Yes',
no: 'No',
orderId: 'Order ID',
userId: 'User ID',
userName: 'Username',
email: 'Email',
amount: 'Amount',
status: 'Status',
paymentSuccess: 'Payment Success',
rechargeSuccess: 'Recharge Success',
rechargeStatus: 'Recharge Status',
paymentChannel: 'Payment Channel',
provider: 'Provider',
rechargeCode: 'Recharge Code',
paymentTradeNo: 'Payment Trade No.',
clientIp: 'Client IP',
sourceHost: 'Source Host',
sourcePage: 'Source Page',
createdAt: 'Created At',
expiresAt: 'Expires At',
paidAt: 'Paid At',
completedAt: 'Completed At',
failedAt: 'Failed At',
failedReason: 'Failure Reason',
refundAmount: 'Refund Amount',
refundReason: 'Refund Reason',
refundAt: 'Refunded At',
forceRefund: 'Force Refund',
}
: {
title: '订单详情',
auditLogs: '审计日志',
operator: '操作者',
emptyLogs: '暂无日志',
close: '关闭',
yes: '',
no: '',
orderId: '订单号',
userId: '用户ID',
userName: '用户名',
email: '邮箱',
amount: '金额',
status: '状态',
paymentSuccess: '支付成功',
rechargeSuccess: '充值成功',
rechargeStatus: '充值状态',
paymentChannel: '支付渠道',
provider: '提供商',
rechargeCode: '充值码',
paymentTradeNo: '支付单号',
clientIp: '客户端IP',
sourceHost: '来源域名',
sourcePage: '来源页面',
createdAt: '创建时间',
expiresAt: '过期时间',
paidAt: '支付时间',
completedAt: '完成时间',
failedAt: '失败时间',
failedReason: '失败原因',
refundAmount: '退款金额',
refundReason: '退款原因',
refundAt: '退款时间',
forceRefund: '强制退款',
};
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {

View File

@@ -32,37 +32,38 @@ interface OrderTableProps {
export default function OrderTable({ orders, onRetry, onCancel, onViewDetail, dark, locale = 'zh' }: OrderTableProps) {
const currency = locale === 'en' ? '$' : '¥';
const text = locale === 'en'
? {
orderId: 'Order ID',
userName: 'Username',
email: 'Email',
notes: 'Notes',
amount: 'Amount',
status: 'Status',
paymentMethod: 'Payment',
source: 'Source',
createdAt: 'Created At',
actions: 'Actions',
retry: 'Retry',
cancel: 'Cancel',
empty: 'No orders',
}
: {
orderId: '订单号',
userName: '用户名',
email: '邮箱',
notes: '备注',
amount: '金额',
status: '状态',
paymentMethod: '支付方式',
source: '来源',
createdAt: '创建时间',
actions: '操作',
retry: '重试',
cancel: '取消',
empty: '暂无订单',
};
const text =
locale === 'en'
? {
orderId: 'Order ID',
userName: 'Username',
email: 'Email',
notes: 'Notes',
amount: 'Amount',
status: 'Status',
paymentMethod: 'Payment',
source: 'Source',
createdAt: 'Created At',
actions: 'Actions',
retry: 'Retry',
cancel: 'Cancel',
empty: 'No orders',
}
: {
orderId: '订单号',
userName: '用户名',
email: '邮箱',
notes: '备注',
amount: '金额',
status: '状态',
paymentMethod: '支付方式',
source: '来源',
createdAt: '创建时间',
actions: '操作',
retry: '重试',
cancel: '取消',
empty: '暂无订单',
};
const thCls = `px-4 py-3 text-left text-xs font-medium uppercase ${dark ? 'text-slate-400' : 'text-gray-500'}`;
const tdMuted = `whitespace-nowrap px-4 py-3 text-sm ${dark ? 'text-slate-400' : 'text-gray-500'}`;
@@ -133,7 +134,8 @@ export default function OrderTable({ orders, onRetry, onCancel, onViewDetail, da
<td className={tdMuted}>{order.userEmail || '-'}</td>
<td className={tdMuted}>{order.userNotes || '-'}</td>
<td className={`whitespace-nowrap px-4 py-3 text-sm font-medium ${dark ? 'text-slate-200' : ''}`}>
{currency}{order.amount.toFixed(2)}
{currency}
{order.amount.toFixed(2)}
</td>
<td className="whitespace-nowrap px-4 py-3 text-sm">
<span

View File

@@ -44,9 +44,7 @@ export default function PaymentMethodChart({ data, dark, locale = 'zh' }: Paymen
dark ? 'border-slate-700 bg-slate-800/60' : 'border-slate-200 bg-white shadow-sm',
].join(' ')}
>
<h3 className={['mb-4 text-sm font-semibold', dark ? 'text-slate-200' : 'text-slate-800'].join(' ')}>
{title}
</h3>
<h3 className={['mb-4 text-sm font-semibold', dark ? 'text-slate-200' : 'text-slate-800'].join(' ')}>{title}</h3>
<div className="space-y-4">
{data.map((method) => {
const meta = getPaymentMeta(method.paymentType);
@@ -56,7 +54,8 @@ export default function PaymentMethodChart({ data, dark, locale = 'zh' }: Paymen
<div className="mb-1.5 flex items-center justify-between text-sm">
<span className={dark ? 'text-slate-300' : 'text-slate-700'}>{label}</span>
<span className={dark ? 'text-slate-400' : 'text-slate-500'}>
{currency}{method.amount.toLocaleString()} · {method.percentage}%
{currency}
{method.amount.toLocaleString()} · {method.percentage}%
</span>
</div>
<div
@@ -65,7 +64,10 @@ export default function PaymentMethodChart({ data, dark, locale = 'zh' }: Paymen
)}
>
<div
className={['h-full rounded-full transition-all', dark ? meta.chartBar.dark : meta.chartBar.light].join(' ')}
className={[
'h-full rounded-full transition-all',
dark ? meta.chartBar.dark : meta.chartBar.light,
].join(' ')}
style={{ width: `${method.percentage}%` }}
/>
</div>

View File

@@ -74,10 +74,7 @@ export default function RefundDialog({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onCancel}>
<div
className={[
'w-full max-w-md rounded-xl p-6 shadow-xl',
dark ? 'bg-slate-900' : 'bg-white',
].join(' ')}
className={['w-full max-w-md rounded-xl p-6 shadow-xl', dark ? 'bg-slate-900' : 'bg-white'].join(' ')}
onClick={(e) => e.stopPropagation()}
>
<h3 className={['text-lg font-bold', dark ? 'text-slate-100' : 'text-gray-900'].join(' ')}>{text.title}</h3>
@@ -90,7 +87,10 @@ export default function RefundDialog({
<div className={['rounded-lg p-3', dark ? 'bg-slate-800' : 'bg-gray-50'].join(' ')}>
<div className={['text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>{text.amount}</div>
<div className="text-lg font-bold text-red-600">{currency}{amount.toFixed(2)}</div>
<div className="text-lg font-bold text-red-600">
{currency}
{amount.toFixed(2)}
</div>
</div>
{warning && (

View File

@@ -4,7 +4,10 @@ const BODY_CHARSET_RE = /(?:^|&)charset=([^&]+)/i;
function normalizeCharset(charset: string | null | undefined): string | null {
if (!charset) return null;
const normalized = charset.trim().replace(/^['"]|['"]$/g, '').toLowerCase();
const normalized = charset
.trim()
.replace(/^['"]|['"]$/g, '')
.toLowerCase();
if (!normalized) return null;
switch (normalized) {
@@ -67,9 +70,7 @@ export function decodeAlipayPayload(rawBody: string | Buffer, headers: Record<st
return fallbackDecoded;
}
throw new Error(
`Failed to decode Alipay payload${lastError instanceof Error ? `: ${lastError.message}` : ''}`,
);
throw new Error(`Failed to decode Alipay payload${lastError instanceof Error ? `: ${lastError.message}` : ''}`);
}
export function normalizeAlipaySignature(sign: string): string {

View File

@@ -11,11 +11,7 @@ import type {
import { pageExecute, execute } from './client';
import { verifySign } from './sign';
import { getEnv } from '@/lib/config';
import type {
AlipayTradeQueryResponse,
AlipayTradeRefundResponse,
AlipayTradeCloseResponse,
} from './types';
import type { AlipayTradeQueryResponse, AlipayTradeRefundResponse, AlipayTradeCloseResponse } from './types';
import { parseAlipayNotificationParams } from './codec';
export interface BuildAlipayPaymentUrlInput {
@@ -165,8 +161,7 @@ export class AlipayProvider implements PaymentProvider {
tradeNo,
orderId,
amount: Math.round(amount * 100) / 100,
status:
tradeStatus === 'TRADE_SUCCESS' || tradeStatus === 'TRADE_FINISHED' ? 'success' : 'failed',
status: tradeStatus === 'TRADE_SUCCESS' || tradeStatus === 'TRADE_FINISHED' ? 'success' : 'failed',
rawData: params,
};
}

View File

@@ -6,7 +6,11 @@ function wrapBase64(b64: string): string {
}
function normalizePemLikeValue(key: string): string {
return key.trim().replace(/\r\n/g, '\n').replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n');
return key
.trim()
.replace(/\r\n/g, '\n')
.replace(/\\r\\n/g, '\n')
.replace(/\\n/g, '\n');
}
function shouldLogVerifyDebug(): boolean {
@@ -42,7 +46,9 @@ export function generateSign(params: Record<string, string>, privateKey: string)
/** 用支付宝公钥验证签名(回调验签:排除 sign 和 sign_type */
export function verifySign(params: Record<string, string>, alipayPublicKey: string, sign: string): boolean {
const filtered = Object.entries(params)
.filter(([key, value]) => key !== 'sign' && key !== 'sign_type' && value !== '' && value !== undefined && value !== null)
.filter(
([key, value]) => key !== 'sign' && key !== 'sign_type' && value !== '' && value !== undefined && value !== null,
)
.sort(([a], [b]) => a.localeCompare(b));
const signStr = filtered.map(([key, value]) => `${key}=${value}`).join('&');

View File

@@ -62,7 +62,11 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
if (pendingCount >= MAX_PENDING_ORDERS) {
throw new OrderError(
'TOO_MANY_PENDING',
message(locale, `待支付订单过多(最多 ${MAX_PENDING_ORDERS} 笔)`, `Too many pending orders (${MAX_PENDING_ORDERS})`),
message(
locale,
`待支付订单过多(最多 ${MAX_PENDING_ORDERS} 笔)`,
`Too many pending orders (${MAX_PENDING_ORDERS})`,
),
429,
);
}
@@ -228,13 +232,21 @@ export async function createOrder(input: CreateOrderInput): Promise<CreateOrderR
if (msg.includes('environment variables') || msg.includes('not configured') || msg.includes('not found')) {
throw new OrderError(
'PAYMENT_GATEWAY_ERROR',
message(locale, `支付渠道(${input.paymentType})暂未配置,请联系管理员`, `Payment method (${input.paymentType}) is not configured. Please contact the administrator`),
message(
locale,
`支付渠道(${input.paymentType})暂未配置,请联系管理员`,
`Payment method (${input.paymentType}) is not configured. Please contact the administrator`,
),
503,
);
}
throw new OrderError(
'PAYMENT_GATEWAY_ERROR',
message(locale, '支付渠道暂时不可用,请稍后重试或更换支付方式', 'Payment method is temporarily unavailable. Please try again later or use another payment method'),
message(
locale,
'支付渠道暂时不可用,请稍后重试或更换支付方式',
'Payment method is temporarily unavailable. Please try again later or use another payment method',
),
502,
);
}
@@ -413,10 +425,7 @@ export async function confirmPayment(input: {
const result = await prisma.order.updateMany({
where: {
id: order.id,
OR: [
{ status: ORDER_STATUS.PENDING },
{ status: ORDER_STATUS.EXPIRED, updatedAt: { gte: graceDeadline } },
],
OR: [{ status: ORDER_STATUS.PENDING }, { status: ORDER_STATUS.EXPIRED, updatedAt: { gte: graceDeadline } }],
},
data: {
status: ORDER_STATUS.PAID,
@@ -574,11 +583,19 @@ export async function executeRecharge(orderId: string): Promise<void> {
function assertRetryAllowed(order: { status: string; paidAt: Date | null }, locale: Locale): void {
if (!order.paidAt) {
throw new OrderError('INVALID_STATUS', message(locale, '订单未支付,不允许重试', 'Order is not paid, retry denied'), 400);
throw new OrderError(
'INVALID_STATUS',
message(locale, '订单未支付,不允许重试', 'Order is not paid, retry denied'),
400,
);
}
if (isRefundStatus(order.status)) {
throw new OrderError('INVALID_STATUS', message(locale, '退款相关订单不允许重试', 'Refund-related order cannot retry'), 400);
throw new OrderError(
'INVALID_STATUS',
message(locale, '退款相关订单不允许重试', 'Refund-related order cannot retry'),
400,
);
}
if (order.status === ORDER_STATUS.FAILED || order.status === ORDER_STATUS.PAID) {
@@ -586,14 +603,22 @@ function assertRetryAllowed(order: { status: string; paidAt: Date | null }, loca
}
if (order.status === ORDER_STATUS.RECHARGING) {
throw new OrderError('CONFLICT', message(locale, '订单正在充值中,请稍后重试', 'Order is recharging, retry later'), 409);
throw new OrderError(
'CONFLICT',
message(locale, '订单正在充值中,请稍后重试', 'Order is recharging, retry later'),
409,
);
}
if (order.status === ORDER_STATUS.COMPLETED) {
throw new OrderError('INVALID_STATUS', message(locale, '订单已完成', 'Order already completed'), 400);
}
throw new OrderError('INVALID_STATUS', message(locale, '仅已支付和失败订单允许重试', 'Only paid and failed orders can retry'), 400);
throw new OrderError(
'INVALID_STATUS',
message(locale, '仅已支付和失败订单允许重试', 'Only paid and failed orders can retry'),
400,
);
}
export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Promise<void> {
@@ -638,7 +663,11 @@ export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Pro
const derived = deriveOrderState(latest);
if (derived.rechargeStatus === 'recharging' || latest.status === ORDER_STATUS.PAID) {
throw new OrderError('CONFLICT', message(locale, '订单正在充值中,请稍后重试', 'Order is recharging, retry later'), 409);
throw new OrderError(
'CONFLICT',
message(locale, '订单正在充值中,请稍后重试', 'Order is recharging, retry later'),
409,
);
}
if (derived.rechargeStatus === 'success') {
@@ -646,10 +675,18 @@ export async function retryRecharge(orderId: string, locale: Locale = 'zh'): Pro
}
if (isRefundStatus(latest.status)) {
throw new OrderError('INVALID_STATUS', message(locale, '退款相关订单不允许重试', 'Refund-related order cannot retry'), 400);
throw new OrderError(
'INVALID_STATUS',
message(locale, '退款相关订单不允许重试', 'Refund-related order cannot retry'),
400,
);
}
throw new OrderError('CONFLICT', message(locale, '订单状态已变更,请刷新后重试', 'Order status changed, refresh and retry'), 409);
throw new OrderError(
'CONFLICT',
message(locale, '订单状态已变更,请刷新后重试', 'Order status changed, refresh and retry'),
409,
);
}
await prisma.auditLog.create({
@@ -682,7 +719,11 @@ export async function processRefund(input: RefundInput): Promise<RefundResult> {
const order = await prisma.order.findUnique({ where: { id: input.orderId } });
if (!order) throw new OrderError('NOT_FOUND', message(locale, '订单不存在', 'Order not found'), 404);
if (order.status !== ORDER_STATUS.COMPLETED) {
throw new OrderError('INVALID_STATUS', message(locale, '仅已完成订单允许退款', 'Only completed orders can be refunded'), 400);
throw new OrderError(
'INVALID_STATUS',
message(locale, '仅已完成订单允许退款', 'Only completed orders can be refunded'),
400,
);
}
const rechargeAmount = Number(order.amount);
@@ -716,7 +757,11 @@ export async function processRefund(input: RefundInput): Promise<RefundResult> {
data: { status: ORDER_STATUS.REFUNDING },
});
if (lockResult.count === 0) {
throw new OrderError('CONFLICT', message(locale, '订单状态已变更,请刷新后重试', 'Order status changed, refresh and retry'), 409);
throw new OrderError(
'CONFLICT',
message(locale, '订单状态已变更,请刷新后重试', 'Order status changed, refresh and retry'),
409,
);
}
try {

View File

@@ -101,7 +101,8 @@ export function getOrderDisplayState(
label: '支付成功',
color: 'text-amber-600',
icon: '!',
message: '支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。',
message:
'支付已完成,但余额充值暂未完成。系统可能会自动重试,请稍后在订单列表查看;如长时间未到账请联系管理员。',
};
}
}

View File

@@ -1,9 +1,4 @@
import {
ORDER_STATUS,
PAYMENT_TYPE,
PAYMENT_PREFIX,
REDIRECT_PAYMENT_TYPES,
} from './constants';
import { ORDER_STATUS, PAYMENT_TYPE, PAYMENT_PREFIX, REDIRECT_PAYMENT_TYPES } from './constants';
import type { Locale } from './locale';
export interface UserInfo {
@@ -211,7 +206,10 @@ export function getPaymentTypeLabel(type: string, locale: Locale = 'zh'): string
return locale === 'en' ? `${meta.label} (${meta.provider})` : `${meta.label}${meta.provider}`;
}
export function getPaymentDisplayInfo(type: string, locale: Locale = 'zh'): { channel: string; provider: string; sublabel?: string } {
export function getPaymentDisplayInfo(
type: string,
locale: Locale = 'zh',
): { channel: string; provider: string; sublabel?: string } {
const meta = getPaymentText(type, locale);
return { channel: meta.label, provider: meta.provider, sublabel: meta.sublabel };
}

View File

@@ -125,12 +125,7 @@ export async function subtractBalance(
}
}
export async function addBalance(
userId: number,
amount: number,
notes: string,
idempotencyKey: string,
): Promise<void> {
export async function addBalance(userId: number, amount: number, notes: string, idempotencyKey: string): Promise<void> {
const env = getEnv();
const response = await fetch(`${env.SUB2API_BASE_URL}/api/v1/admin/users/${userId}/balance`, {
method: 'POST',