2026-06-02 12:38:01 +08:00
|
|
|
|
import { CheckCircleOutlined, CloseOutlined, CrownOutlined, RocketOutlined } from "@ant-design/icons";
|
|
|
|
|
|
import { useMemo, useState, type ReactNode } from "react";
|
2026-06-03 20:19:07 +08:00
|
|
|
|
import { keyServerClient, type RechargeOrderResult } from "../../api/keyServerClient";
|
|
|
|
|
|
import { toast } from "../toast/toastStore";
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
type RechargeAudience = "personal" | "enterprise";
|
2026-06-03 20:19:07 +08:00
|
|
|
|
type PaymentMethod = "wechat" | "alipay" | "bank";
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
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 个月,系统按先进先出自动消耗",
|
|
|
|
|
|
"退费规则:充值积分到账后不支持退换、折现,仅限平台内消费",
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-06-03 20:19:07 +08:00
|
|
|
|
const paymentMethods: Array<{ id: PaymentMethod; label: string; hint: string }> = [
|
|
|
|
|
|
{ id: "wechat", label: "微信支付", hint: "生成支付链接或二维码" },
|
|
|
|
|
|
{ id: "alipay", label: "支付宝", hint: "生成支付链接或二维码" },
|
|
|
|
|
|
{ id: "bank", label: "对公转账", hint: "企业客户可联系客服确认" },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-06-02 12:38:01 +08:00
|
|
|
|
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);
|
2026-06-03 20:19:07 +08:00
|
|
|
|
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("wechat");
|
|
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
|
|
const [order, setOrder] = useState<RechargeOrderResult | null>(null);
|
2026-06-02 12:38:01 +08:00
|
|
|
|
const visiblePlans = useMemo(() => membershipPlans.filter((plan) => plan.audience === activeAudience), [activeAudience]);
|
|
|
|
|
|
const selectedPlanId = selectedPlanIds[activeAudience];
|
2026-06-03 20:19:07 +08:00
|
|
|
|
const selectedPlan = membershipPlans.find((plan) => plan.id === selectedPlanId) ?? visiblePlans[0];
|
2026-06-02 12:38:01 +08:00
|
|
|
|
|
|
|
|
|
|
const handlePlanSelect = (plan: MembershipPlan) => {
|
|
|
|
|
|
setSelectedPlanIds((current) => ({
|
|
|
|
|
|
...current,
|
|
|
|
|
|
[plan.audience]: plan.id,
|
|
|
|
|
|
}));
|
2026-06-03 20:19:07 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-06-02 12:38:01 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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>
|
2026-06-03 20:19:07 +08:00
|
|
|
|
|
|
|
|
|
|
<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>
|
2026-06-02 12:38:01 +08:00
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|