|
|
|
@ -1,12 +1,10 @@ |
|
|
|
import { Button } from "@arco-design/web-react"; |
|
|
|
import { useCallback, useEffect, useMemo, useState } from "react"; |
|
|
|
import { createPortal } from "react-dom"; |
|
|
|
import { Fragment, useCallback, useEffect, useMemo, useState } from "react"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import { getPlanList } from "@/api/api"; |
|
|
|
import type { ProductPlan } from "@/api/api"; |
|
|
|
import { getMemberLevelLabel } from "@/constants"; |
|
|
|
// import { memberLevelMap } from "@/constants";
|
|
|
|
import { openPaySubscribeDialog } from "@/dialog"; |
|
|
|
import { useMemberLevelStore } from "@/store/useMemberLevelStore"; |
|
|
|
import { useParamConfigStore } from "@/store/useParamConfigStore"; |
|
|
|
import { useUserStore } from "@/store/useUserStore"; |
|
|
|
|
|
|
|
@ -39,10 +37,13 @@ type PlanGroup = { |
|
|
|
const PLAN_INLINE_TAG_RE = /<\/?(?:mark|title|section)>/gi; |
|
|
|
const HIGHLIGHT_CLASS_NAME = "desc_highlight"; |
|
|
|
const SUBSCRIBE_PLAN_ASSETS = { |
|
|
|
crown: new URL("@/assets/images/subscription/crown.png", import.meta.url).href, |
|
|
|
check: new URL("@/assets/images/home/feature-check-icon.png", import.meta.url).href, |
|
|
|
pointsIcon: new URL("@/assets/images/points/points-active.png", import.meta.url).href, |
|
|
|
}; |
|
|
|
const SUBSCRIBE_PLAN_LOCAL_ASSETS = { |
|
|
|
goal: new URL("@/assets/images/subscription/subscribe-plan-goal-icon.png", import.meta.url).href, |
|
|
|
featureTitle: new URL("@/assets/images/subscription/subscribe-plan-feature-title-icon.png", import.meta.url).href, |
|
|
|
}; |
|
|
|
|
|
|
|
function getPlanKey(plan: ProductPlan, index: number): string { |
|
|
|
if (plan.id != null && plan.id !== "") { |
|
|
|
@ -66,6 +67,39 @@ function formatPlanTierLabel(plan: ProductPlan): string { |
|
|
|
return String(coins); |
|
|
|
} |
|
|
|
|
|
|
|
function formatCurrencyAmount(value: number): string { |
|
|
|
if (!Number.isFinite(value)) { |
|
|
|
return "0"; |
|
|
|
} |
|
|
|
|
|
|
|
if (Number.isInteger(value)) { |
|
|
|
return String(value); |
|
|
|
} |
|
|
|
|
|
|
|
return value.toFixed(2).replace(/\.?0+$/, ""); |
|
|
|
} |
|
|
|
|
|
|
|
function formatPointPriceValue(plan: ProductPlan, price: number): string | null { |
|
|
|
const coins = Number(plan.coins); |
|
|
|
if (!Number.isFinite(price) || !Number.isFinite(coins) || price <= 0 || coins <= 0) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
return formatCurrencyAmount((price / coins) * 100); |
|
|
|
} |
|
|
|
|
|
|
|
function getNewUserLabel(value: ProductPlanCustomInfo["new_user"], fallback: string): string { |
|
|
|
if (typeof value === "string") { |
|
|
|
return value.trim(); |
|
|
|
} |
|
|
|
|
|
|
|
if (value === true) { |
|
|
|
return fallback; |
|
|
|
} |
|
|
|
|
|
|
|
return ""; |
|
|
|
} |
|
|
|
|
|
|
|
/** 去掉 mark/title/section 标签,保留纯文本 */ |
|
|
|
function stripPlanInlineTags(text: string): string { |
|
|
|
return text.replace(PLAN_INLINE_TAG_RE, "").trim(); |
|
|
|
@ -135,7 +169,7 @@ function parseMarkSegments(text: string): PlanDescriptionSegment[] { |
|
|
|
|
|
|
|
/** 将描述拆成权益列表行 */ |
|
|
|
function splitPlanDescriptionLines(description: string): string[] { |
|
|
|
return description.split("\n").filter((l) => l.trim() !== ""); |
|
|
|
return description.split(/\n|\\n/).filter((l) => l.trim() !== ""); |
|
|
|
} |
|
|
|
|
|
|
|
/** 套餐描述行:解析 mark 片段;分组标题行解析外壳内的 mark */ |
|
|
|
@ -158,12 +192,10 @@ export default function SubscribePlanList({ embedded = false, onBillingCycleChan |
|
|
|
const { t } = useTranslation(); |
|
|
|
const user = useUserStore((s) => s.user); |
|
|
|
const serviceQrCode = useParamConfigStore((s) => s.config?.systemConfig?.serviceQrCode ?? ""); |
|
|
|
const memberLevelMap = useMemberLevelStore((s) => s.memberLevelMap); |
|
|
|
|
|
|
|
const [billingCycle, setBillingCycleState] = useState<SubscribeBillingCycle>("monthly"); |
|
|
|
const [planList, setPlanList] = useState<ProductPlan[]>([]); |
|
|
|
const [selectedPlanKeyByGroup, setSelectedPlanKeyByGroup] = useState<Record<string, string>>({}); |
|
|
|
const [futureCreatorModalOpen, setFutureCreatorModalOpen] = useState(false); |
|
|
|
const userMemberLevel = Number(user?.memberLevel ?? 0); |
|
|
|
|
|
|
|
// 拉取套餐列表
|
|
|
|
@ -223,13 +255,7 @@ export default function SubscribePlanList({ embedded = false, onBillingCycleChan |
|
|
|
void openPaySubscribeDialog({ type: "subscribe", plan }); |
|
|
|
}, []); |
|
|
|
|
|
|
|
const openFutureCreatorModal = useCallback(() => { |
|
|
|
setFutureCreatorModalOpen(true); |
|
|
|
}, []); |
|
|
|
|
|
|
|
const joinCls = (...parts: Array<string | false | undefined | null>) => parts.filter(Boolean).join(" "); |
|
|
|
const closeIconSrc = new URL("@/assets/images/common/icons/close.png", import.meta.url).href; |
|
|
|
const wechatIconSrc = new URL("@/assets/images/common/payments/wxpay-logo.png", import.meta.url).href; |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className={joinCls("plans", embedded && "plans_embedded")}> |
|
|
|
@ -284,34 +310,37 @@ export default function SubscribePlanList({ embedded = false, onBillingCycleChan |
|
|
|
const price = Number(plan.price ?? 0) / 100; // 价格单位为分,需要除以100转换为元
|
|
|
|
const original = Number(plan.original_price_amount) > 0 ? Number(plan.original_price_amount) : null; // 原价
|
|
|
|
const level = Number(plan.level ?? -1); // 会员等级
|
|
|
|
const titleBadges: Array<{ type: string; text: string }> = []; //
|
|
|
|
if (plan.recommended) titleBadges.push({ type: "recommended", text: t("subscribe.recommended_plan") }); |
|
|
|
if (plan.custom_info?.rightTags) titleBadges.push({ type: "top", text: plan.custom_info.rightTags }); |
|
|
|
const isLevel4Plan = level === 4; |
|
|
|
const isFreePlan = price <= 0; |
|
|
|
const isPurchasedLevel4Plan = isLevel4Plan && userMemberLevel === 4; // 已购买4级会员 Future Creator 教学项目
|
|
|
|
const isPurchasedLevel4Plan = isLevel4Plan && userMemberLevel === 4; // 已购买4级会员 Future Creator 课程
|
|
|
|
const ctaDisabled = isPurchasedLevel4Plan ? false : userMemberLevel === level || userMemberLevel > level; // 按钮是否禁用
|
|
|
|
const userMemberLevelLabel = getMemberLevelLabel(memberLevelMap, userMemberLevel, t); |
|
|
|
const ctaLabel = isPurchasedLevel4Plan |
|
|
|
? t("subscribe.contact_service") |
|
|
|
: userMemberLevel === level // 当前会员等级
|
|
|
|
? t("subscribe.current_plan") |
|
|
|
: userMemberLevel > level // 当前会员等级大于计划等级
|
|
|
|
? t("subscribe.already_member_level", { level: userMemberLevelLabel }) |
|
|
|
? t("subscribe.already_member_level", { level: t(memberLevelMap[userMemberLevel] ?? memberLevelMap[0]) }) |
|
|
|
: t("subscribe.subscribe_now"); // 订阅按钮文本
|
|
|
|
|
|
|
|
// 点击CTA按钮
|
|
|
|
const handleCtaClick = () => { |
|
|
|
if (isPurchasedLevel4Plan) { |
|
|
|
openFutureCreatorModal(); |
|
|
|
return; |
|
|
|
} |
|
|
|
showPaySubscribe(plan); |
|
|
|
}; |
|
|
|
|
|
|
|
// 渲染描述段落
|
|
|
|
const priceUnit = isLevel4Plan ? t("subscribe.per_100_days") : plan.planCategory === "yearly" ? t("subscribe.per_year") : t("subscribe.per_month"); |
|
|
|
const pointsPeriod = isLevel4Plan ? t("subscribe.points_granted_once") : plan.planCategory === "yearly" ? (plan.pointsGrantMode === "monthly" ? t("subscribe.points_period_monthly") : t("subscribe.points_period_yearly")) : t("subscribe.points_period_monthly"); |
|
|
|
const priceUnit = plan.planCategory === "yearly" ? t("subscribe.per_year") : t("subscribe.per_month"); |
|
|
|
const pointsPeriod = plan.planCategory === "yearly" ? (plan.pointsGrantMode === "monthly" ? t("subscribe.points_period_monthly") : t("subscribe.points_period_yearly")) : t("subscribe.points_period_monthly"); |
|
|
|
const pointPriceValue = formatPointPriceValue(plan, price); |
|
|
|
const pointPriceText = plan.custom_info?.point_amount ?? (pointPriceValue ? t("subscribe.plan_point_price", { value: pointPriceValue }) : ""); |
|
|
|
const discountText = price > 0 ? (plan.custom_info?.discount_info ?? t("subscribe.plan_default_discount")) : ""; |
|
|
|
const descriptionLines = splitPlanDescriptionLines(plan.description || ""); |
|
|
|
const goalTitle = plan.custom_info?.goal_title?.trim() ?? ""; |
|
|
|
const goalDescriptionLines = splitPlanDescriptionLines(plan.custom_info?.goal_description ?? ""); |
|
|
|
const showPlanGoal = goalTitle !== "" || goalDescriptionLines.length > 0; |
|
|
|
const featureTitle = plan.custom_info?.feature_title?.trim() ?? ""; |
|
|
|
const newUserLabel = getNewUserLabel(plan.custom_info?.new_user, t("subscribe.plan_new_user")); |
|
|
|
const isNewUserPlan = newUserLabel !== ""; |
|
|
|
const recommendedLabel = plan.recommended === true && !isNewUserPlan ? t("subscribe.plan_recommended") : ""; |
|
|
|
const isRecommendedPlan = recommendedLabel !== ""; |
|
|
|
const renderDescSegments = (line: string) => |
|
|
|
getPlanDescriptionSegments(line).map((seg, segIdx) => ( |
|
|
|
<span key={segIdx} className={seg.className}> |
|
|
|
@ -319,188 +348,173 @@ export default function SubscribePlanList({ embedded = false, onBillingCycleChan |
|
|
|
</span> |
|
|
|
)); |
|
|
|
|
|
|
|
return ( |
|
|
|
<div className="plan_grid_item" key={groupKey}> |
|
|
|
<div className="plan_card"> |
|
|
|
<div className={joinCls("plan_item", "plan_item__header")}> |
|
|
|
<div className="plan_item__title-row"> |
|
|
|
<div className="plan_item__title"> |
|
|
|
{price > 0 ? <img className="plan_item__crown" alt="" src={SUBSCRIBE_PLAN_ASSETS.crown} /> : null} |
|
|
|
<span>{plan.title}</span> |
|
|
|
</div> |
|
|
|
{titleBadges.length > 0 ? ( |
|
|
|
<div className="plan_item__title-badges"> |
|
|
|
{titleBadges.map((badge) => ( |
|
|
|
<span key={`${badge.type}-${badge.text}`} className={joinCls("plan_item__title-badge", `plan_item__title-badge_${badge.type}`)}> |
|
|
|
{badge.text} |
|
|
|
</span> |
|
|
|
))} |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
const planContent = ( |
|
|
|
<> |
|
|
|
<div className={joinCls("plan_item", "plan_item__header")}> |
|
|
|
<div className="plan_item__title-row"> |
|
|
|
<div className="plan_item__title"> |
|
|
|
<span>{plan.title}</span> |
|
|
|
</div> |
|
|
|
{discountText ? <span className="plan_item__discount-badge">{discountText}</span> : null} |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="plan_item__price-block"> |
|
|
|
<div className="plan_item__price"> |
|
|
|
<span className="plan_item__price-symbol">¥</span> |
|
|
|
<span className={joinCls("plan_item__price-amount", !price && "plan_item__price-amount_free")}>{price}</span> |
|
|
|
<span className="plan_item__price-unit">{priceUnit}</span> |
|
|
|
{original != null && original > price ? <span className="plan_item__price-original">¥{original}</span> : null} |
|
|
|
{plan.custom_info?.discount_info && price > 0 ? <span className="plan_item__price-discount">{plan.custom_info.discount_info}</span> : null} |
|
|
|
</div> |
|
|
|
{plan.custom_info?.point_amount ? <p className="plan_item__price-subtitle">{plan.custom_info.point_amount}</p> : null} |
|
|
|
<div className="plan_item__price-block"> |
|
|
|
<div className="plan_item__price"> |
|
|
|
<span className="plan_item__price-symbol">¥</span> |
|
|
|
<span className={joinCls("plan_item__price-amount", !price && "plan_item__price-amount_free")}>{formatCurrencyAmount(price)}</span> |
|
|
|
<span className="plan_item__price-unit">{priceUnit}</span> |
|
|
|
{original != null && original > price ? <span className="plan_item__price-original">¥{original}</span> : null} |
|
|
|
</div> |
|
|
|
{pointPriceText ? <p className="plan_item__price-subtitle">{pointPriceText}</p> : null} |
|
|
|
</div> |
|
|
|
|
|
|
|
{price > 0 && plan.coins != null ? ( |
|
|
|
<div className="plan_points"> |
|
|
|
<span className="plan_points__content"> |
|
|
|
<span className="plan_points__icon-wrap" aria-hidden="true"> |
|
|
|
<img className="plan_points__icon" alt="" src={SUBSCRIBE_PLAN_ASSETS.pointsIcon} /> |
|
|
|
</span> |
|
|
|
<span className="plan_points__value">{plan.coins}</span> |
|
|
|
<span className="plan_points__unit"> |
|
|
|
{t("subscribe.points_unit")} |
|
|
|
{pointsPeriod} |
|
|
|
</span> |
|
|
|
{price > 0 && plan.coins != null ? ( |
|
|
|
<div className="plan_points"> |
|
|
|
<span className="plan_points__content"> |
|
|
|
<span className="plan_points__icon-wrap" aria-hidden="true"> |
|
|
|
<img className="plan_points__icon" alt="" src={SUBSCRIBE_PLAN_ASSETS.pointsIcon} /> |
|
|
|
</span> |
|
|
|
<span className="plan_points__value">{plan.coins}</span> |
|
|
|
<span className="plan_points__unit"> |
|
|
|
{t("subscribe.points_unit")} |
|
|
|
{pointsPeriod} |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
|
|
|
|
<Button type="primary" className={joinCls("plan_item__cta", isFreePlan && "plan_item__cta_free")} disabled={ctaDisabled} onClick={handleCtaClick}> |
|
|
|
{ctaLabel} |
|
|
|
</Button> |
|
|
|
|
|
|
|
{planGroup.plans.length > 1 ? ( |
|
|
|
<div className="plan_tier_selector" role="radiogroup" aria-label={plan.title ?? ""}> |
|
|
|
<div className="plan_tier_selector__labels"> |
|
|
|
{planGroup.plans.map((item) => { |
|
|
|
const active = item.key === selectedPlanItem.key; |
|
|
|
|
|
|
|
return ( |
|
|
|
<button |
|
|
|
key={`${item.key}-label`} |
|
|
|
type="button" |
|
|
|
className={joinCls("plan_tier_selector__label", active && "plan_tier_selector__label_active")} |
|
|
|
role="radio" |
|
|
|
aria-checked={active} |
|
|
|
onClick={() => { |
|
|
|
setSelectedPlanKeyByGroup((prev) => ({ ...prev, [groupKey]: item.key })); |
|
|
|
}} |
|
|
|
> |
|
|
|
{formatPlanTierLabel(item.plan)} |
|
|
|
</button> |
|
|
|
); |
|
|
|
})} |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
|
|
|
|
<Button |
|
|
|
type="primary" |
|
|
|
className={joinCls("plan_item__cta", isFreePlan && "plan_item__cta_free", isPurchasedLevel4Plan && "plan_item__cta_contact", isLevel4Plan && !isPurchasedLevel4Plan && "plan_item__cta_pro")} |
|
|
|
disabled={ctaDisabled} |
|
|
|
onClick={handleCtaClick} |
|
|
|
> |
|
|
|
{ctaLabel} |
|
|
|
</Button> |
|
|
|
|
|
|
|
{planGroup.plans.length > 1 ? ( |
|
|
|
<div className="plan_tier_selector" role="radiogroup" aria-label={plan.title ?? ""}> |
|
|
|
<div className="plan_tier_selector__labels"> |
|
|
|
{planGroup.plans.map((item) => { |
|
|
|
const active = item.key === selectedPlanItem.key; |
|
|
|
|
|
|
|
return ( |
|
|
|
<button |
|
|
|
key={`${item.key}-label`} |
|
|
|
type="button" |
|
|
|
className={joinCls("plan_tier_selector__label", active && "plan_tier_selector__label_active")} |
|
|
|
role="radio" |
|
|
|
aria-checked={active} |
|
|
|
onClick={() => { |
|
|
|
setSelectedPlanKeyByGroup((prev) => ({ ...prev, [groupKey]: item.key })); |
|
|
|
}} |
|
|
|
> |
|
|
|
{formatPlanTierLabel(item.plan)} |
|
|
|
</button> |
|
|
|
); |
|
|
|
})} |
|
|
|
</div> |
|
|
|
<div className="plan_tier_selector__track"> |
|
|
|
<span className="plan_tier_selector__track-line" aria-hidden="true" /> |
|
|
|
<span className="plan_tier_selector__track-line plan_tier_selector__track-line_active" style={{ width: `calc((100% - 15px) * ${String(selectedTierProgress)})` }} aria-hidden="true" /> |
|
|
|
{planGroup.plans.map((item, itemIndex) => { |
|
|
|
const active = item.key === selectedPlanItem.key; |
|
|
|
const highlighted = itemIndex <= selectedPlanIndex; |
|
|
|
|
|
|
|
return ( |
|
|
|
<button |
|
|
|
key={`${item.key}-dot`} |
|
|
|
type="button" |
|
|
|
className={joinCls("plan_tier_selector__dot", highlighted && "plan_tier_selector__dot_active")} |
|
|
|
aria-label={formatPlanTierLabel(item.plan)} |
|
|
|
aria-pressed={active} |
|
|
|
onClick={() => { |
|
|
|
setSelectedPlanKeyByGroup((prev) => ({ ...prev, [groupKey]: item.key })); |
|
|
|
}} |
|
|
|
/> |
|
|
|
); |
|
|
|
})} |
|
|
|
</div> |
|
|
|
<div className="plan_tier_selector__track"> |
|
|
|
<span className="plan_tier_selector__track-line" aria-hidden="true" /> |
|
|
|
<span className="plan_tier_selector__track-line plan_tier_selector__track-line_active" style={{ width: `calc((100% - 15px) * ${String(selectedTierProgress)})` }} aria-hidden="true" /> |
|
|
|
{planGroup.plans.map((item, itemIndex) => { |
|
|
|
const active = item.key === selectedPlanItem.key; |
|
|
|
const highlighted = itemIndex <= selectedPlanIndex; |
|
|
|
|
|
|
|
return ( |
|
|
|
<button |
|
|
|
key={`${item.key}-dot`} |
|
|
|
type="button" |
|
|
|
className={joinCls("plan_tier_selector__dot", highlighted && "plan_tier_selector__dot_active")} |
|
|
|
aria-label={formatPlanTierLabel(item.plan)} |
|
|
|
aria-pressed={active} |
|
|
|
onClick={() => { |
|
|
|
setSelectedPlanKeyByGroup((prev) => ({ ...prev, [groupKey]: item.key })); |
|
|
|
}} |
|
|
|
/> |
|
|
|
); |
|
|
|
})} |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
|
|
|
|
{plan.custom_info?.bonus_gift ? ( |
|
|
|
<div className="plan_commission"> |
|
|
|
<span className="plan_commission__text"> |
|
|
|
<span className="desc_highlight">{t("subscribe.bonus_gift_prefix")}</span> |
|
|
|
{plan.custom_info.bonus_gift} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
</div> |
|
|
|
|
|
|
|
{plan.custom_info?.illustrator ? ( |
|
|
|
<div className="plan_commission"> |
|
|
|
<span className="plan_commission__text"> |
|
|
|
<span className="desc_highlight">{t("subscribe.commission_highlight")}</span> |
|
|
|
{plan.custom_info.illustrator} |
|
|
|
</span> |
|
|
|
<span className="plan_commission__badge">{t("subscribe.limited_time_badge")}</span> |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
</div> |
|
|
|
<div className={joinCls("plan_item", "plan_item__features")}> |
|
|
|
{showPlanGoal ? ( |
|
|
|
<div className="plan_goal"> |
|
|
|
{goalTitle ? ( |
|
|
|
<div className="plan_goal__title"> |
|
|
|
<img className="plan_goal__icon" src={SUBSCRIBE_PLAN_LOCAL_ASSETS.goal} alt="" /> |
|
|
|
<span>{goalTitle}</span> |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
{goalDescriptionLines.length > 0 ? ( |
|
|
|
<p className="plan_goal__desc"> |
|
|
|
{goalDescriptionLines.map((line, lineIdx) => ( |
|
|
|
<span key={lineIdx}>{line}</span> |
|
|
|
))} |
|
|
|
</p> |
|
|
|
) : null} |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
|
|
|
|
{descriptionLines.length > 0 ? ( |
|
|
|
<div className="plan_description"> |
|
|
|
{featureTitle ? ( |
|
|
|
<> |
|
|
|
<div className="plan_feature_divider" aria-hidden="true" /> |
|
|
|
<div className="plan_feature_header"> |
|
|
|
<img className="plan_feature_header__icon" src={SUBSCRIBE_PLAN_LOCAL_ASSETS.featureTitle} alt="" /> |
|
|
|
<span className="plan_feature_header__text">{featureTitle}</span> |
|
|
|
</div> |
|
|
|
</> |
|
|
|
) : null} |
|
|
|
<ul className="plan_description__list"> |
|
|
|
{descriptionLines.map((line, lineIdx) => |
|
|
|
isSectionLine(line) ? ( |
|
|
|
<Fragment key={lineIdx}> |
|
|
|
{isTitleLine(line) ? <li className="plan_feature_divider" aria-hidden="true" /> : null} |
|
|
|
<li key={lineIdx} className={joinCls("plan_feature", isTitleLine(line) ? "plan_feature_title" : "plan_feature_section")}> |
|
|
|
{renderDescSegments(line)} |
|
|
|
</li> |
|
|
|
</Fragment> |
|
|
|
) : ( |
|
|
|
<li key={lineIdx} className="plan_feature"> |
|
|
|
<img className="plan_feature__check" src={SUBSCRIBE_PLAN_ASSETS.check} alt="" /> |
|
|
|
<span className="plan_feature__text">{renderDescSegments(line)}</span> |
|
|
|
</li> |
|
|
|
), |
|
|
|
)} |
|
|
|
</ul> |
|
|
|
</div> |
|
|
|
) : null} |
|
|
|
</div> |
|
|
|
</> |
|
|
|
); |
|
|
|
const highlightLabel = isNewUserPlan ? newUserLabel : recommendedLabel; |
|
|
|
const highlightClassName = isNewUserPlan ? "plan_item__new-user-label" : isRecommendedPlan ? "plan_item__recommended-label" : ""; |
|
|
|
|
|
|
|
<div className={joinCls("plan_item", "plan_item__features")}> |
|
|
|
<ul className="plan_item__feature-list"> |
|
|
|
{splitPlanDescriptionLines(plan.description || "").map((line, lineIdx) => |
|
|
|
isSectionLine(line) ? ( |
|
|
|
<li key={lineIdx} className={joinCls("plan_feature", isTitleLine(line) ? "plan_feature_title" : "plan_feature_section")}> |
|
|
|
{renderDescSegments(line)} |
|
|
|
</li> |
|
|
|
) : ( |
|
|
|
<li key={lineIdx} className="plan_feature"> |
|
|
|
<img className="plan_feature__check" src={SUBSCRIBE_PLAN_ASSETS.check} alt="" /> |
|
|
|
<span className="plan_feature__text">{renderDescSegments(line)}</span> |
|
|
|
</li> |
|
|
|
), |
|
|
|
)} |
|
|
|
</ul> |
|
|
|
</div> |
|
|
|
return ( |
|
|
|
<div className="plan_grid_item" key={groupKey}> |
|
|
|
<div className={joinCls("plan_card", isNewUserPlan && "plan_card_new-user", isRecommendedPlan && "plan_card_recommended")}> |
|
|
|
{highlightLabel ? ( |
|
|
|
<div className={joinCls("plan_item__highlight-frame", highlightClassName)}> |
|
|
|
<div className="plan_item__highlight-label">{highlightLabel}</div> |
|
|
|
<div className="plan_item__highlight-content">{planContent}</div> |
|
|
|
</div> |
|
|
|
) : ( |
|
|
|
planContent |
|
|
|
)} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
); |
|
|
|
})} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{futureCreatorModalOpen |
|
|
|
? createPortal( |
|
|
|
<div |
|
|
|
className="subscribe-fc-modal-overlay" |
|
|
|
role="presentation" |
|
|
|
onClick={() => { |
|
|
|
setFutureCreatorModalOpen(false); |
|
|
|
}} |
|
|
|
> |
|
|
|
<div |
|
|
|
id="subscribe-future-creator-dialog" |
|
|
|
className="subscribe-fc-modal-dialog" |
|
|
|
role="dialog" |
|
|
|
aria-modal="true" |
|
|
|
aria-labelledby="subscribe-future-creator-title" |
|
|
|
onClick={(e) => { |
|
|
|
e.stopPropagation(); |
|
|
|
}} |
|
|
|
> |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
className="subscribe-fc-modal-dialog__close" |
|
|
|
onClick={() => { |
|
|
|
setFutureCreatorModalOpen(false); |
|
|
|
}} |
|
|
|
aria-label={t("dialog.common.closeAria")} |
|
|
|
> |
|
|
|
<img src={closeIconSrc} alt="" /> |
|
|
|
</button> |
|
|
|
<div className="subscribe-fc-modal-dialog__body"> |
|
|
|
<h3 id="subscribe-future-creator-title" className="subscribe-fc-modal-dialog__title"> |
|
|
|
{t("subscribe.future_creator_title")} |
|
|
|
</h3> |
|
|
|
<p className="subscribe-fc-modal-dialog__success">{t("subscribe.future_creator_success")}</p> |
|
|
|
<div className="subscribe-fc-modal-dialog__qrWrap"> |
|
|
|
{serviceQrCode ? <img className="subscribe-fc-modal-dialog__qr" src={resolveOssImageThumbUrl(serviceQrCode)} alt={t("contactService.qrAlt")} /> : <div className="subscribe-fc-modal-dialog__qrPlaceholder" aria-hidden />} |
|
|
|
</div> |
|
|
|
<p className="subscribe-fc-modal-dialog__tip"> |
|
|
|
<img className="subscribe-fc-modal-dialog__tipIcon" src={wechatIconSrc} alt="" width={14} height={14} /> |
|
|
|
<span>{t("subscribe.future_creator_wechat_tip")}</span> |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div>, |
|
|
|
document.body, |
|
|
|
) |
|
|
|
: null} |
|
|
|
</div> |
|
|
|
); |
|
|
|
} |
|
|
|
|