308 lines
12 KiB
TypeScript
308 lines
12 KiB
TypeScript
import { CheckCircleOutlined, CloseOutlined, CrownOutlined, RocketOutlined } from "@ant-design/icons";
|
||
import { useMemo, useState, type ReactNode } from "react";
|
||
import "../../styles/components/recharge-modal.css";
|
||
import { keyServerClient, type RechargeOrderResult } from "../../api/keyServerClient";
|
||
import { toast } from "../toast/toastStore";
|
||
|
||
type RechargeAudience = "personal" | "enterprise";
|
||
type PaymentMethod = "wechat" | "alipay" | "bank";
|
||
|
||
interface MembershipPlan {
|
||
id: string;
|
||
audience: RechargeAudience;
|
||
name: string;
|
||
subtitle: string;
|
||
period: string;
|
||
price: string;
|
||
grant: string;
|
||
comparisonLabel: string;
|
||
badge?: string;
|
||
icon: ReactNode;
|
||
benefits: string[];
|
||
}
|
||
|
||
const membershipPlans: MembershipPlan[] = [
|
||
{
|
||
id: "pro-month",
|
||
audience: "personal",
|
||
name: "专业版",
|
||
subtitle: "Pro",
|
||
period: "月付",
|
||
price: "299 元 / 月",
|
||
grant: "每月赠送 10000 积分,30 天有效",
|
||
comparisonLabel: "专业版基础权益",
|
||
icon: <CrownOutlined />,
|
||
benefits: ["通用大模型全解锁", "积分与 API 消耗 9 折", "并发提升到 3 个", "去水印、插队加速、专属客服"],
|
||
},
|
||
{
|
||
id: "pro-quarter",
|
||
audience: "personal",
|
||
name: "专业版",
|
||
subtitle: "Pro",
|
||
period: "季付",
|
||
price: "897 元 / 季",
|
||
grant: "连续 3 个月按月发放 Pro 积分",
|
||
comparisonLabel: "相比月付新增",
|
||
badge: "季度",
|
||
icon: <CrownOutlined />,
|
||
benefits: ["一次覆盖 3 个月使用周期", "每月延续 Pro 权益", "适合短期项目排期"],
|
||
},
|
||
{
|
||
id: "pro-year",
|
||
audience: "personal",
|
||
name: "专业版",
|
||
subtitle: "Pro",
|
||
period: "年付",
|
||
price: "1990 元 / 年",
|
||
grant: "全年合计 140000 积分,默认按月分摊",
|
||
comparisonLabel: "相比季付新增",
|
||
badge: "年费优惠",
|
||
icon: <CrownOutlined />,
|
||
benefits: ["折合 10 个月费用", "前 100 名额外赠 20000 积分", "适合全年持续高频使用"],
|
||
},
|
||
{
|
||
id: "enterprise-month",
|
||
audience: "enterprise",
|
||
name: "企业版",
|
||
subtitle: "Enterprise",
|
||
period: "月付",
|
||
price: "499 元 / 月",
|
||
grant: "每月赠送 2000 积分,30 天有效",
|
||
comparisonLabel: "企业版基础权益",
|
||
icon: <RocketOutlined />,
|
||
benefits: ["企业私有模型与高性能模型", "默认 10 并发,可申请提升", "积分与 API 消耗 8 折", "用量报表与正式 API 权限"],
|
||
},
|
||
{
|
||
id: "enterprise-quarter",
|
||
audience: "enterprise",
|
||
name: "企业版",
|
||
subtitle: "Enterprise",
|
||
period: "季付",
|
||
price: "1497 元 / 季",
|
||
grant: "连续 3 个月按月发放企业版积分",
|
||
comparisonLabel: "相比月付新增",
|
||
badge: "季度",
|
||
icon: <RocketOutlined />,
|
||
benefits: ["一次覆盖季度项目周期", "延续企业资源池与高并发", "适合阶段性团队投放"],
|
||
},
|
||
{
|
||
id: "enterprise-year",
|
||
audience: "enterprise",
|
||
name: "企业版",
|
||
subtitle: "Enterprise",
|
||
period: "年付",
|
||
price: "4990 元 / 年",
|
||
grant: "全年合计 340000 积分,默认按月分摊",
|
||
comparisonLabel: "相比季付新增",
|
||
badge: "企业年费",
|
||
icon: <RocketOutlined />,
|
||
benefits: ["折合 10 个月费用", "前 100 名额外赠 100000 积分", "支持对公充值与子账户额度分配"],
|
||
},
|
||
];
|
||
|
||
const defaultSelectedPlanIds: Record<RechargeAudience, string> = {
|
||
personal: "pro-month",
|
||
enterprise: "enterprise-month",
|
||
};
|
||
|
||
const rechargeRules = [
|
||
"充值比例:固定 1 元 = 100 积分,平台可限时活动额外赠送积分",
|
||
"有效期:充值积分到账起有效期 12 个月,系统按先进先出自动消耗",
|
||
"退费规则:充值积分到账后不支持退换、折现,仅限平台内消费",
|
||
];
|
||
|
||
const paymentMethods: Array<{ id: PaymentMethod; label: string; hint: string }> = [
|
||
{ id: "wechat", label: "微信支付", hint: "生成支付链接或二维码" },
|
||
{ id: "alipay", label: "支付宝", hint: "生成支付链接或二维码" },
|
||
{ id: "bank", label: "对公转账", hint: "企业客户可联系客服确认" },
|
||
];
|
||
|
||
export interface RechargeModalProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
currentBalance?: number;
|
||
}
|
||
|
||
export function RechargeModal({ open, onClose, currentBalance }: RechargeModalProps) {
|
||
const [activeAudience, setActiveAudience] = useState<RechargeAudience>("personal");
|
||
const [selectedPlanIds, setSelectedPlanIds] = useState<Record<RechargeAudience, string>>(defaultSelectedPlanIds);
|
||
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("wechat");
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [order, setOrder] = useState<RechargeOrderResult | null>(null);
|
||
const visiblePlans = useMemo(() => membershipPlans.filter((plan) => plan.audience === activeAudience), [activeAudience]);
|
||
const selectedPlanId = selectedPlanIds[activeAudience];
|
||
const selectedPlan = membershipPlans.find((plan) => plan.id === selectedPlanId) ?? visiblePlans[0];
|
||
|
||
const handlePlanSelect = (plan: MembershipPlan) => {
|
||
setSelectedPlanIds((current) => ({
|
||
...current,
|
||
[plan.audience]: plan.id,
|
||
}));
|
||
setOrder(null);
|
||
};
|
||
|
||
const handleCreateOrder = async () => {
|
||
if (!selectedPlan || submitting) return;
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
const nextOrder = await keyServerClient.createRechargeOrder({ planId: selectedPlan.id, paymentMethod });
|
||
setOrder(nextOrder);
|
||
if (nextOrder.payUrl) {
|
||
window.open(nextOrder.payUrl, "_blank", "noopener,noreferrer");
|
||
}
|
||
toast.success("充值订单已创建");
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : "订单创建失败,请联系客服处理。";
|
||
toast.error(message);
|
||
setOrder({
|
||
orderId: `support-${Date.now()}`,
|
||
status: "manual-review",
|
||
message: "支付接口暂不可用,请通过页面联系方式联系客服完成充值。",
|
||
});
|
||
} finally {
|
||
setSubmitting(false);
|
||
}
|
||
};
|
||
|
||
if (!open) return null;
|
||
|
||
return (
|
||
<div className="recharge-modal" role="dialog" aria-modal="true" aria-labelledby="recharge-modal-title">
|
||
<button type="button" className="recharge-modal__backdrop" onClick={onClose} aria-label="关闭充值弹窗" />
|
||
<section className="recharge-modal__panel" aria-describedby="recharge-modal-desc">
|
||
<div className="recharge-modal__promo" role="note">
|
||
<strong>限时活动</strong>
|
||
<span>年付套餐前 100 名额外赠送积分,季度套餐适合短期项目集中投放,充值积分有效期 12 个月。</span>
|
||
</div>
|
||
|
||
<header className="recharge-modal__header">
|
||
<div>
|
||
<span className="recharge-modal__eyebrow">会员与积分</span>
|
||
<h2 id="recharge-modal-title">积分充值</h2>
|
||
<p id="recharge-modal-desc">按个人或企业场景选择版本,重点展示相对左侧版本的新增权益。</p>
|
||
</div>
|
||
{currentBalance !== undefined ? (
|
||
<span className="recharge-modal__balance">当前余额:{(currentBalance / 100).toFixed(2)} 积分</span>
|
||
) : null}
|
||
<button type="button" className="recharge-modal__close" onClick={onClose} aria-label="关闭">
|
||
<CloseOutlined />
|
||
</button>
|
||
</header>
|
||
|
||
<div className="recharge-modal__audience-tabs" role="tablist" aria-label="充值对象">
|
||
<button
|
||
type="button"
|
||
role="tab"
|
||
aria-selected={activeAudience === "personal"}
|
||
className={activeAudience === "personal" ? "is-active" : ""}
|
||
onClick={() => setActiveAudience("personal")}
|
||
>
|
||
个人
|
||
</button>
|
||
<button
|
||
type="button"
|
||
role="tab"
|
||
aria-selected={activeAudience === "enterprise"}
|
||
className={activeAudience === "enterprise" ? "is-active" : ""}
|
||
onClick={() => setActiveAudience("enterprise")}
|
||
>
|
||
企业
|
||
</button>
|
||
</div>
|
||
|
||
<div className="recharge-modal__grid" data-audience={activeAudience}>
|
||
{visiblePlans.map((plan) => {
|
||
const isSelected = plan.id === selectedPlanId;
|
||
|
||
return (
|
||
<article
|
||
key={plan.id}
|
||
className={`recharge-modal__card recharge-modal__card--${plan.id}${isSelected ? " is-selected" : ""}`}
|
||
>
|
||
{plan.badge ? <span className="recharge-modal__badge">{plan.badge}</span> : null}
|
||
<div className="recharge-modal__card-head">
|
||
<span className="recharge-modal__card-icon">{plan.icon}</span>
|
||
<div>
|
||
<h3>{plan.name}</h3>
|
||
<span>{plan.subtitle}</span>
|
||
</div>
|
||
</div>
|
||
<span className="recharge-modal__period">{plan.period}</span>
|
||
<div className="recharge-modal__price">
|
||
<strong>{plan.price}</strong>
|
||
</div>
|
||
<p className="recharge-modal__grant">{plan.grant}</p>
|
||
<span className="recharge-modal__diff-label">{plan.comparisonLabel}</span>
|
||
<ul className="recharge-modal__features">
|
||
{plan.benefits.map((benefit) => (
|
||
<li key={benefit}>
|
||
<CheckCircleOutlined />
|
||
<span>{benefit}</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<button
|
||
type="button"
|
||
className={`recharge-modal__buy${isSelected ? " is-selected" : ""}`}
|
||
aria-pressed={isSelected}
|
||
onClick={() => handlePlanSelect(plan)}
|
||
>
|
||
{isSelected ? "当前方案" : "选择方案"}
|
||
</button>
|
||
</article>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<footer className="recharge-modal__rules">
|
||
<h3>积分充值规则</h3>
|
||
<ol>
|
||
{rechargeRules.map((rule) => (
|
||
<li key={rule}>{rule}</li>
|
||
))}
|
||
</ol>
|
||
</footer>
|
||
|
||
<section className="recharge-modal__checkout" aria-label="支付方式">
|
||
<div>
|
||
<span className="recharge-modal__checkout-eyebrow">支付确认</span>
|
||
<h3>{selectedPlan.name} · {selectedPlan.period}</h3>
|
||
<p>{selectedPlan.price},{selectedPlan.grant}</p>
|
||
</div>
|
||
<div className="recharge-modal__payment-methods" role="radiogroup" aria-label="选择支付方式">
|
||
{paymentMethods.map((method) => (
|
||
<button
|
||
key={method.id}
|
||
type="button"
|
||
role="radio"
|
||
aria-checked={paymentMethod === method.id}
|
||
className={paymentMethod === method.id ? "is-active" : ""}
|
||
onClick={() => {
|
||
setPaymentMethod(method.id);
|
||
setOrder(null);
|
||
}}
|
||
>
|
||
<strong>{method.label}</strong>
|
||
<span>{method.hint}</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
<button type="button" className="recharge-modal__pay" onClick={() => void handleCreateOrder()} disabled={submitting}>
|
||
{submitting ? "创建订单中..." : "立即充值"}
|
||
</button>
|
||
{order ? (
|
||
<div className="recharge-modal__order" role="status">
|
||
<strong>订单号:{order.orderId}</strong>
|
||
<span>状态:{order.status}</span>
|
||
{order.qrCodeUrl ? <img src={order.qrCodeUrl} alt="支付二维码" /> : null}
|
||
{order.payUrl ? <a href={order.payUrl} target="_blank" rel="noreferrer">打开支付链接</a> : null}
|
||
<p>{order.message || "支付完成后积分将自动入账,如长时间未到账请联系客服。"}</p>
|
||
</div>
|
||
) : null}
|
||
</section>
|
||
</section>
|
||
</div>
|
||
);
|
||
}
|