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:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { useEffect, useState, useCallback, Suspense } from 'react';
|
import { useEffect, useState, useCallback, Suspense } from 'react';
|
||||||
|
import { getPaymentMeta } from '@/lib/pay-utils';
|
||||||
|
|
||||||
function StripePopupContent() {
|
function StripePopupContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@@ -254,7 +255,7 @@ function StripePopupContent() {
|
|||||||
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
|
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
|
||||||
stripeSubmitting
|
stripeSubmitting
|
||||||
? 'bg-gray-400 cursor-not-allowed'
|
? 'bg-gray-400 cursor-not-allowed'
|
||||||
: 'bg-[#635bff] hover:bg-[#5249d9] active:bg-[#4840c4]',
|
: getPaymentMeta('stripe').buttonClass,
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{stripeSubmitting ? (
|
{stripeSubmitting ? (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
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 {
|
export interface MethodLimitInfo {
|
||||||
available: boolean;
|
available: boolean;
|
||||||
@@ -327,9 +327,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
|
||||||
? effectivePaymentType.startsWith('stripe')
|
? getPaymentMeta(effectivePaymentType).buttonClass
|
||||||
? 'bg-[#635bff] hover:bg-[#5851db] active:bg-[#4b44c7]'
|
|
||||||
: 'bg-blue-600 hover:bg-blue-700 active:bg-blue-800'
|
|
||||||
: dark
|
: dark
|
||||||
? 'cursor-not-allowed bg-slate-700 text-slate-300'
|
? 'cursor-not-allowed bg-slate-700 text-slate-300'
|
||||||
: 'cursor-not-allowed bg-gray-300'
|
: 'cursor-not-allowed bg-gray-300'
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
import { useEffect, useMemo, useState, useCallback, useRef } from 'react';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
|
import {
|
||||||
|
isStripeType,
|
||||||
|
isRedirectPayment,
|
||||||
|
getPaymentMeta,
|
||||||
|
getPaymentIconSrc,
|
||||||
|
getPaymentChannelLabel,
|
||||||
|
} from '@/lib/pay-utils';
|
||||||
|
|
||||||
interface PaymentQRCodeProps {
|
interface PaymentQRCodeProps {
|
||||||
orderId: string;
|
orderId: string;
|
||||||
@@ -71,14 +78,13 @@ export default function PaymentQRCode({
|
|||||||
const paymentMethodListenerAdded = useRef(false);
|
const paymentMethodListenerAdded = useRef(false);
|
||||||
|
|
||||||
// alipay_direct 使用电脑网站支付,payUrl 是跳转链接不是二维码内容
|
// alipay_direct 使用电脑网站支付,payUrl 是跳转链接不是二维码内容
|
||||||
const isAlipayDirect = paymentType === 'alipay_direct';
|
const isRedirect = isRedirectPayment(paymentType);
|
||||||
|
|
||||||
const qrPayload = useMemo(() => {
|
const qrPayload = useMemo(() => {
|
||||||
// alipay_direct 的 payUrl 是跳转链接,不应生成二维码
|
if (isRedirect && !qrCode) return '';
|
||||||
if (isAlipayDirect && !qrCode) return '';
|
|
||||||
const value = (qrCode || payUrl || '').trim();
|
const value = (qrCode || payUrl || '').trim();
|
||||||
return value;
|
return value;
|
||||||
}, [qrCode, payUrl, isAlipayDirect]);
|
}, [qrCode, payUrl, isRedirect]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -115,7 +121,7 @@ export default function PaymentQRCode({
|
|||||||
}, [qrPayload]);
|
}, [qrPayload]);
|
||||||
|
|
||||||
// Initialize Stripe Payment Element
|
// Initialize Stripe Payment Element
|
||||||
const isStripe = paymentType?.startsWith('stripe');
|
const isStripe = isStripeType(paymentType);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isStripe || !clientSecret || !stripePublishableKey) return;
|
if (!isStripe || !clientSecret || !stripePublishableKey) return;
|
||||||
@@ -318,10 +324,10 @@ export default function PaymentQRCode({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWx = paymentType?.startsWith('wxpay');
|
const meta = getPaymentMeta(paymentType || 'alipay');
|
||||||
const iconSrc = isStripe ? '' : isWx ? '/icons/wxpay.svg' : '/icons/alipay.svg';
|
const iconSrc = getPaymentIconSrc(paymentType || 'alipay');
|
||||||
const channelLabel = isStripe ? 'Stripe' : isWx ? '\u5FAE\u4FE1' : '\u652F\u4ED8\u5B9D';
|
const channelLabel = getPaymentChannelLabel(paymentType || 'alipay');
|
||||||
const iconBgClass = isStripe ? 'bg-[#635bff]' : isWx ? 'bg-[#07C160]' : 'bg-[#1677FF]';
|
const iconBgClass = meta.iconBg;
|
||||||
|
|
||||||
if (cancelBlocked) {
|
if (cancelBlocked) {
|
||||||
return (
|
return (
|
||||||
@@ -414,7 +420,7 @@ export default function PaymentQRCode({
|
|||||||
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
|
'w-full rounded-lg py-3 font-medium text-white shadow-md transition-colors',
|
||||||
stripeSubmitting
|
stripeSubmitting
|
||||||
? 'bg-gray-400 cursor-not-allowed'
|
? 'bg-gray-400 cursor-not-allowed'
|
||||||
: 'bg-[#635bff] hover:bg-[#5249d9] active:bg-[#4840c4]',
|
: meta.buttonClass,
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{stripeSubmitting ? (
|
{stripeSubmitting ? (
|
||||||
@@ -457,16 +463,16 @@ export default function PaymentQRCode({
|
|||||||
{TEXT_H5_HINT}
|
{TEXT_H5_HINT}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : isAlipayDirect && payUrl ? (
|
) : isRedirect && payUrl ? (
|
||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
href={payUrl}
|
href={payUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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>
|
</a>
|
||||||
<p className={['text-center text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
|
<p className={['text-center text-sm', dark ? 'text-slate-400' : 'text-gray-500'].join(' ')}>
|
||||||
{TEXT_H5_HINT}
|
{TEXT_H5_HINT}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { getPaymentTypeLabel } from '@/lib/pay-utils';
|
import { getPaymentTypeLabel, getPaymentMeta } from '@/lib/pay-utils';
|
||||||
|
|
||||||
interface PaymentMethod {
|
interface PaymentMethod {
|
||||||
paymentType: string;
|
paymentType: string;
|
||||||
@@ -14,14 +14,6 @@ interface PaymentMethodChartProps {
|
|||||||
dark?: boolean;
|
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) {
|
export default function PaymentMethodChart({ data, dark }: PaymentMethodChartProps) {
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -51,15 +43,12 @@ export default function PaymentMethodChart({ data, dark }: PaymentMethodChartPro
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{data.map((method) => {
|
{data.map((method) => {
|
||||||
const config = TYPE_CONFIG[method.paymentType] || {
|
const meta = getPaymentMeta(method.paymentType);
|
||||||
label: getPaymentTypeLabel(method.paymentType),
|
const label = getPaymentTypeLabel(method.paymentType);
|
||||||
light: 'bg-gray-500',
|
|
||||||
dark: 'bg-gray-400',
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div key={method.paymentType}>
|
<div key={method.paymentType}>
|
||||||
<div className="mb-1.5 flex items-center justify-between text-sm">
|
<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'}>
|
<span className={dark ? 'text-slate-400' : 'text-slate-500'}>
|
||||||
¥{method.amount.toLocaleString()} · {method.percentage}%
|
¥{method.amount.toLocaleString()} · {method.percentage}%
|
||||||
</span>
|
</span>
|
||||||
@@ -70,7 +59,7 @@ export default function PaymentMethodChart({ data, dark }: PaymentMethodChartPro
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<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}%` }}
|
style={{ width: `${method.percentage}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ export interface PaymentTypeMeta {
|
|||||||
selectedBorder: string;
|
selectedBorder: string;
|
||||||
selectedBg: string;
|
selectedBg: string;
|
||||||
iconBg: string;
|
iconBg: string;
|
||||||
|
/** 图标路径(Stripe 不使用外部图标) */
|
||||||
|
iconSrc?: string;
|
||||||
|
/** 图表条形颜色 class */
|
||||||
|
chartBar: { light: string; dark: string };
|
||||||
|
/** 按钮颜色 class(含 hover/active 状态) */
|
||||||
|
buttonClass: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
||||||
@@ -80,6 +86,9 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
|||||||
selectedBorder: 'border-cyan-400',
|
selectedBorder: 'border-cyan-400',
|
||||||
selectedBg: 'bg-cyan-50',
|
selectedBg: 'bg-cyan-50',
|
||||||
iconBg: 'bg-[#00AEEF]',
|
iconBg: 'bg-[#00AEEF]',
|
||||||
|
iconSrc: '/icons/alipay.svg',
|
||||||
|
chartBar: { light: 'bg-cyan-500', dark: 'bg-cyan-400' },
|
||||||
|
buttonClass: 'bg-[#00AEEF] hover:bg-[#009dd6] active:bg-[#008cbe]',
|
||||||
},
|
},
|
||||||
alipay_direct: {
|
alipay_direct: {
|
||||||
label: '支付宝',
|
label: '支付宝',
|
||||||
@@ -88,6 +97,9 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
|||||||
selectedBorder: 'border-blue-500',
|
selectedBorder: 'border-blue-500',
|
||||||
selectedBg: 'bg-blue-50',
|
selectedBg: 'bg-blue-50',
|
||||||
iconBg: 'bg-[#1677FF]',
|
iconBg: 'bg-[#1677FF]',
|
||||||
|
iconSrc: '/icons/alipay.svg',
|
||||||
|
chartBar: { light: 'bg-blue-500', dark: 'bg-blue-400' },
|
||||||
|
buttonClass: 'bg-[#1677FF] hover:bg-[#0958d9] active:bg-[#003eb3]',
|
||||||
},
|
},
|
||||||
wxpay: {
|
wxpay: {
|
||||||
label: '微信支付',
|
label: '微信支付',
|
||||||
@@ -96,6 +108,9 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
|||||||
selectedBorder: 'border-green-500',
|
selectedBorder: 'border-green-500',
|
||||||
selectedBg: 'bg-green-50',
|
selectedBg: 'bg-green-50',
|
||||||
iconBg: 'bg-[#2BB741]',
|
iconBg: 'bg-[#2BB741]',
|
||||||
|
iconSrc: '/icons/wxpay.svg',
|
||||||
|
chartBar: { light: 'bg-green-500', dark: 'bg-green-400' },
|
||||||
|
buttonClass: 'bg-[#2BB741] hover:bg-[#24a038] active:bg-[#1d8a2f]',
|
||||||
},
|
},
|
||||||
wxpay_direct: {
|
wxpay_direct: {
|
||||||
label: '微信支付',
|
label: '微信支付',
|
||||||
@@ -104,6 +119,9 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
|||||||
selectedBorder: 'border-green-600',
|
selectedBorder: 'border-green-600',
|
||||||
selectedBg: 'bg-green-50',
|
selectedBg: 'bg-green-50',
|
||||||
iconBg: 'bg-[#07C160]',
|
iconBg: 'bg-[#07C160]',
|
||||||
|
iconSrc: '/icons/wxpay.svg',
|
||||||
|
chartBar: { light: 'bg-emerald-500', dark: 'bg-emerald-400' },
|
||||||
|
buttonClass: 'bg-[#07C160] hover:bg-[#06ad56] active:bg-[#05994c]',
|
||||||
},
|
},
|
||||||
stripe: {
|
stripe: {
|
||||||
label: 'Stripe',
|
label: 'Stripe',
|
||||||
@@ -112,6 +130,8 @@ export const PAYMENT_TYPE_META: Record<string, PaymentTypeMeta> = {
|
|||||||
selectedBorder: 'border-[#635bff]',
|
selectedBorder: 'border-[#635bff]',
|
||||||
selectedBg: 'bg-[#635bff]/10',
|
selectedBg: 'bg-[#635bff]/10',
|
||||||
iconBg: 'bg-[#635bff]',
|
iconBg: 'bg-[#635bff]',
|
||||||
|
chartBar: { light: 'bg-purple-500', dark: 'bg-purple-400' },
|
||||||
|
buttonClass: 'bg-[#635bff] hover:bg-[#5249d9] active:bg-[#4840c4]',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,6 +150,40 @@ export function getPaymentIconType(type: string): string {
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取支付方式的元数据,带合理的 fallback */
|
||||||
|
export function getPaymentMeta(type: string): PaymentTypeMeta {
|
||||||
|
const base = getPaymentIconType(type);
|
||||||
|
return PAYMENT_TYPE_META[type] || PAYMENT_TYPE_META[base] || PAYMENT_TYPE_META.alipay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取支付方式图标路径 */
|
||||||
|
export function getPaymentIconSrc(type: string): string {
|
||||||
|
return getPaymentMeta(type).iconSrc || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取支付方式简短标签(如 '支付宝'、'微信'、'Stripe') */
|
||||||
|
export function getPaymentChannelLabel(type: string): string {
|
||||||
|
return getPaymentMeta(type).label;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 支付类型谓词函数 */
|
||||||
|
export function isStripeType(type: string | undefined | null): boolean {
|
||||||
|
return !!type?.startsWith('stripe');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWxpayType(type: string | undefined | null): boolean {
|
||||||
|
return !!type?.startsWith('wxpay');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAlipayType(type: string | undefined | null): boolean {
|
||||||
|
return !!type?.startsWith('alipay');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** alipay_direct 使用页面跳转而非二维码 */
|
||||||
|
export function isRedirectPayment(type: string | undefined | null): boolean {
|
||||||
|
return type === 'alipay_direct';
|
||||||
|
}
|
||||||
|
|
||||||
export function getStatusBadgeClass(status: string, isDark: boolean): string {
|
export function getStatusBadgeClass(status: string, isDark: boolean): string {
|
||||||
if (['COMPLETED', 'PAID'].includes(status)) {
|
if (['COMPLETED', 'PAID'].includes(status)) {
|
||||||
return isDark ? 'bg-emerald-500/20 text-emerald-200' : 'bg-emerald-100 text-emerald-700';
|
return isDark ? 'bg-emerald-500/20 text-emerald-200' : 'bg-emerald-100 text-emerald-700';
|
||||||
|
|||||||
Reference in New Issue
Block a user