refactor: 将支付类型硬编码抽取到 pay-utils 统一管理

- PaymentTypeMeta 新增 iconSrc、chartBar、buttonClass 字段
- 新增工具函数: getPaymentMeta、getPaymentIconSrc、
  getPaymentChannelLabel、isStripeType、isRedirectPayment 等
- PaymentQRCode: 用 meta/工具函数替换散落的颜色和类型判断
- PaymentForm: 提交按钮颜色改用 meta.buttonClass
- PaymentMethodChart: 删除重复的 TYPE_CONFIG,改用 getPaymentMeta
- stripe-popup: 按钮颜色改用 meta.buttonClass

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erio
2026-03-06 16:46:36 +08:00
parent cee24c3afb
commit 3829d0e52e
5 changed files with 83 additions and 35 deletions

View File

@@ -1,7 +1,7 @@
'use client';
import { useState } from 'react';
import { PAYMENT_TYPE_META, getPaymentIconType } from '@/lib/pay-utils';
import { PAYMENT_TYPE_META, getPaymentIconType, getPaymentMeta } from '@/lib/pay-utils';
export interface MethodLimitInfo {
available: boolean;
@@ -327,9 +327,7 @@ export default function PaymentForm({
disabled={!isValid || loading}
className={`w-full rounded-lg py-3 text-center font-medium text-white transition-colors ${
isValid && !loading
? effectivePaymentType.startsWith('stripe')
? 'bg-[#635bff] hover:bg-[#5851db] active:bg-[#4b44c7]'
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800'
? getPaymentMeta(effectivePaymentType).buttonClass
: dark
? 'cursor-not-allowed bg-slate-700 text-slate-300'
: 'cursor-not-allowed bg-gray-300'

View File

@@ -2,6 +2,13 @@
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
import QRCode from 'qrcode';
import {
isStripeType,
isRedirectPayment,
getPaymentMeta,
getPaymentIconSrc,
getPaymentChannelLabel,
} from '@/lib/pay-utils';
interface PaymentQRCodeProps {
orderId: string;
@@ -71,14 +78,13 @@ export default function PaymentQRCode({
const paymentMethodListenerAdded = useRef(false);
// alipay_direct 使用电脑网站支付payUrl 是跳转链接不是二维码内容
const isAlipayDirect = paymentType === 'alipay_direct';
const isRedirect = isRedirectPayment(paymentType);
const qrPayload = useMemo(() => {
// alipay_direct 的 payUrl 是跳转链接,不应生成二维码
if (isAlipayDirect && !qrCode) return '';
if (isRedirect && !qrCode) return '';
const value = (qrCode || payUrl || '').trim();
return value;
}, [qrCode, payUrl, isAlipayDirect]);
}, [qrCode, payUrl, isRedirect]);
useEffect(() => {
let cancelled = false;
@@ -115,7 +121,7 @@ export default function PaymentQRCode({
}, [qrPayload]);
// Initialize Stripe Payment Element
const isStripe = paymentType?.startsWith('stripe');
const isStripe = isStripeType(paymentType);
useEffect(() => {
if (!isStripe || !clientSecret || !stripePublishableKey) return;
@@ -318,10 +324,10 @@ export default function PaymentQRCode({
}
};
const isWx = paymentType?.startsWith('wxpay');
const iconSrc = isStripe ? '' : isWx ? '/icons/wxpay.svg' : '/icons/alipay.svg';
const channelLabel = isStripe ? 'Stripe' : isWx ? '\u5FAE\u4FE1' : '\u652F\u4ED8\u5B9D';
const iconBgClass = isStripe ? 'bg-[#635bff]' : isWx ? 'bg-[#07C160]' : 'bg-[#1677FF]';
const meta = getPaymentMeta(paymentType || 'alipay');
const iconSrc = getPaymentIconSrc(paymentType || 'alipay');
const channelLabel = getPaymentChannelLabel(paymentType || 'alipay');
const iconBgClass = meta.iconBg;
if (cancelBlocked) {
return (
@@ -414,7 +420,7 @@ export default function PaymentQRCode({
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
stripeSubmitting
? 'bg-gray-400 cursor-not-allowed'
: 'bg-[#635bff] hover:bg-[#5249d9] active:bg-[#4840c4]',
: meta.buttonClass,
].join(' ')}
>
{stripeSubmitting ? (
@@ -457,16 +463,16 @@ export default function PaymentQRCode({
{TEXT_H5_HINT}
</p>
</>
) : isAlipayDirect && payUrl ? (
) : isRedirect && payUrl ? (
<>
<a
href={payUrl}
target="_blank"
rel="noopener noreferrer"
className="flex w-full items-center justify-center gap-2 rounded-lg bg-[#1677FF] py-3 font-medium text-white shadow-md hover:bg-[#0958d9] active:bg-[#003eb3]"
className={`flex w-full items-center justify-center gap-2 rounded-lg py-3 font-medium text-white shadow-md ${meta.buttonClass}`}
>
<img src={iconSrc} alt={channelLabel} className="h-5 w-5 brightness-0 invert" />
{iconSrc && <img src={iconSrc} alt={channelLabel} className="h-5 w-5 brightness-0 invert" />}
{`前往${channelLabel}收银台`}
</a>
<p className={['text-center text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
{TEXT_H5_HINT}

View File

@@ -1,6 +1,6 @@
'use client';
import { getPaymentTypeLabel } from '@/lib/pay-utils';
import { getPaymentTypeLabel, getPaymentMeta } from '@/lib/pay-utils';
interface PaymentMethod {
paymentType: string;
@@ -14,14 +14,6 @@ interface PaymentMethodChartProps {
dark?: boolean;
}
const TYPE_CONFIG: Record<string, { label: string; light: string; dark: string }> = {
alipay: { label: '支付宝(易支付)', light: 'bg-cyan-500', dark: 'bg-cyan-400' },
alipay_direct: { label: '支付宝(官方)', light: 'bg-blue-500', dark: 'bg-blue-400' },
wxpay: { label: '微信支付(易支付)', light: 'bg-green-500', dark: 'bg-green-400' },
wxpay_direct: { label: '微信支付(官方)', light: 'bg-emerald-500', dark: 'bg-emerald-400' },
stripe: { label: 'Stripe', light: 'bg-purple-500', dark: 'bg-purple-400' },
};
export default function PaymentMethodChart({ data, dark }: PaymentMethodChartProps) {
if (data.length === 0) {
return (
@@ -51,15 +43,12 @@ export default function PaymentMethodChart({ data, dark }: PaymentMethodChartPro
</h3>
<div className="space-y-4">
{data.map((method) => {
const config = TYPE_CONFIG[method.paymentType] || {
label: getPaymentTypeLabel(method.paymentType),
light: 'bg-gray-500',
dark: 'bg-gray-400',
};
const meta = getPaymentMeta(method.paymentType);
const label = getPaymentTypeLabel(method.paymentType);
return (
<div key={method.paymentType}>
<div className="mb-1.5 flex items-center justify-between text-sm">
<span className={dark ? 'text-slate-300' : 'text-slate-700'}>{config.label}</span>
<span className={dark ? 'text-slate-300' : 'text-slate-700'}>{label}</span>
<span className={dark ? 'text-slate-400' : 'text-slate-500'}>
¥{method.amount.toLocaleString()} · {method.percentage}%
</span>
@@ -70,7 +59,7 @@ export default function PaymentMethodChart({ data, dark }: PaymentMethodChartPro
)}
>
<div
className={['h-full rounded-full transition-all', dark ? config.dark : config.light].join(' ')}
className={['h-full rounded-full transition-all', dark ? meta.chartBar.dark : meta.chartBar.light].join(' ')}
style={{ width: `${method.percentage}%` }}
/>
</div>