Browse Source

no message

subscribe-plan-list
leiyuee 1 week ago
parent
commit
21569b181f
  1. 2
      src/api/api.ts
  2. 225
      src/components/subscribe-plan-list/index.scss
  3. 95
      src/components/subscribe-plan-list/index.tsx
  4. 7
      src/locales/en-US.json
  5. 7
      src/locales/zh-CN.json
  6. 7
      src/locales/zh-TW.json

2
src/api/api.ts

@ -25,7 +25,6 @@ type ListApiResponse<T> = {
data?: ListApiData<T>;
};
export type ProductPlanCustomInfo = {
rightTags?: string;
discount_info?: string;
point_amount?: string;
illustrator?: string;
@ -40,7 +39,6 @@ export type ProductPlan = ApiParams & {
price_amount?: number | string;
original_price_amount?: number | string;
level?: number | string;
recommended?: boolean;
coins?: number | string;
pointsGrantMode?: string;
custom_info?: ProductPlanCustomInfo;

225
src/components/subscribe-plan-list/index.scss

@ -114,15 +114,15 @@
.plan-grid {
display: grid;
grid-template-columns: repeat(4, 357px);
grid-template-columns: repeat(4, 313px);
align-items: start;
justify-content: center;
gap: 10px;
gap: 12px;
width: 100%;
}
.plan_grid_item {
width: 357px;
width: 313px;
min-width: 0;
}
@ -131,6 +131,7 @@
flex-direction: column;
width: 100%;
min-width: 0;
gap: 0;
}
.plan_item__header {
@ -138,9 +139,9 @@
z-index: 2;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 287px;
padding: 24px 20px;
gap: 16px;
min-height: 266px;
padding: 20px;
border-radius: 26px;
box-sizing: border-box;
background: #ffffff;
@ -149,10 +150,10 @@
.plan_item__title-row {
display: flex;
align-items: flex-start;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 28px;
min-height: 20px;
}
.plan_item__title {
@ -160,17 +161,9 @@
align-items: center;
min-width: 0;
color: #333333;
font-size: 20px;
font-weight: 500;
line-height: 28px;
}
.plan_item__crown {
flex: 0 0 auto;
width: 24px;
height: 24px;
margin-right: 8px;
display: block;
font-size: 16px;
font-weight: 700;
line-height: 20px;
}
.plan_item__title span {
@ -180,39 +173,27 @@
white-space: nowrap;
}
.plan_item__title-badges {
display: flex;
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: flex-end;
gap: 6px;
max-width: 124px;
}
.plan_item__title-badge {
.plan_item__discount-badge {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
height: 28px;
padding: 0 15px;
border-radius: 26px;
min-width: 66px;
max-width: 96px;
height: 20px;
padding: 0 10px;
overflow: hidden;
border-radius: 100px;
box-sizing: border-box;
background: #f3efff;
color: #6f47f5;
font-size: 12px;
font-weight: 500;
line-height: 26px;
font-weight: 400;
line-height: 20px;
text-overflow: ellipsis;
white-space: nowrap;
}
.plan_item__title-badge_recommended {
color: #e55936;
background: #ffe3c3;
}
.plan_item__title-badge_top {
color: #6f47f5;
background: #ebe5ff;
}
.plan_item__price-block {
display: flex;
flex-direction: column;
@ -224,7 +205,6 @@
display: flex;
align-items: flex-start;
min-height: 44px;
padding-right: 72px;
box-sizing: border-box;
}
@ -240,7 +220,7 @@
margin-left: 2px;
color: #333333;
font-size: 36px;
font-weight: 500;
font-weight: 700;
line-height: 44px;
}
@ -252,7 +232,7 @@
margin-top: 18px;
margin-left: 4px;
color: #666666;
font-size: 16px;
font-size: 12px;
font-weight: 400;
line-height: 24px;
white-space: nowrap;
@ -263,35 +243,16 @@
margin-top: 18px;
margin-left: 4px;
color: #999999;
font-size: 16px;
font-weight: 500;
font-size: 12px;
font-weight: 400;
line-height: 24px;
text-decoration: line-through;
white-space: nowrap;
}
.plan_item__price-discount {
position: absolute;
top: 20px;
right: 0;
display: inline-flex;
align-items: center;
justify-content: center;
height: 20px;
padding: 0 10px;
border-radius: 9px;
box-sizing: border-box;
background: #ebe5ff;
color: #333333;
font-size: 12px;
font-weight: 500;
line-height: 20px;
white-space: nowrap;
}
.plan_item__price-subtitle {
margin: 0;
color: #666666;
color: #999999;
font-size: 12px;
font-weight: 400;
line-height: 24px;
@ -301,7 +262,7 @@
position: relative;
display: flex;
align-items: center;
width: 314px;
width: 273px;
max-width: 100%;
height: 49px;
padding: 0 12px;
@ -378,7 +339,7 @@
}
.plan_item__cta.arco-btn:not(.arco-btn-disabled) {
background: linear-gradient(90deg, #333333 0%, #6f47f5 100%) !important;
background: #333333 !important;
}
.plan_item__cta.arco-btn-disabled,
@ -392,7 +353,7 @@
.plan_tier_selector {
width: 100%;
max-width: 314px;
max-width: 273px;
height: 44px;
margin: 0 auto;
box-sizing: border-box;
@ -489,7 +450,7 @@
.plan_commission {
position: relative;
width: 314px;
width: 273px;
max-width: 100%;
height: 35px;
padding: 0;
@ -548,16 +509,75 @@
.plan_item__features {
position: relative;
z-index: 1;
min-height: 510px;
margin-top: -10px;
padding: 46px 20px 24px;
min-height: 1028px;
margin-top: -58px;
padding: 70px 20px 20px;
border-radius: 26px;
box-sizing: border-box;
background: linear-gradient(0deg, #ffffff 70%, #f4f4f5 100%);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
background: #ffffff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
.plan_item__feature-list {
.plan_goal {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
min-height: 102px;
padding: 20px;
border-radius: 26px;
box-sizing: border-box;
background: #f9f6ff;
}
.plan_goal__title {
display: flex;
align-items: center;
gap: 5px;
max-width: 100%;
color: #6f47f5;
font-size: 14px;
font-weight: 700;
line-height: 28px;
}
.plan_goal__icon {
display: block;
flex: 0 0 auto;
width: 15px;
height: 15px;
object-fit: contain;
}
.plan_goal__title span {
overflow: hidden;
min-width: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
.plan_goal__desc {
display: flex;
flex-direction: column;
margin: 6px 0 0;
color: #333333;
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
.plan_description {
width: 100%;
}
.plan_description__divider {
width: 100%;
height: 1px;
margin: 20px 0;
background: #eeeeee;
}
.plan_description__list {
margin: 0;
padding: 0;
list-style: none;
@ -567,8 +587,8 @@
display: flex;
align-items: flex-start;
min-width: 0;
margin: 0 0 10px;
color: #666666;
margin: 0;
color: #333333;
font-size: 12px;
font-weight: 400;
line-height: 24px;
@ -582,10 +602,10 @@
.plan_feature.plan_feature_title {
display: block;
margin: 0 0 10px;
color: #666666;
font-size: 12px;
font-weight: 500;
line-height: 24px;
color: #333333;
font-size: 14px;
font-weight: 700;
line-height: 28px;
}
.plan_feature__check {
@ -600,15 +620,15 @@
.plan_feature__text {
flex: 1 1 auto;
min-width: 0;
color: #666666;
color: #333333;
font-size: 12px;
font-weight: 500;
font-weight: 400;
line-height: 24px;
}
.desc_highlight {
color: #6f47f5;
font-weight: 500;
font-weight: 700;
}
}
@ -629,36 +649,13 @@
}
.plan-grid {
grid-template-columns: repeat(4, 320px);
grid-template-columns: repeat(4, 313px);
width: max-content;
justify-content: flex-start;
}
.plan_grid_item {
width: 320px;
}
.plan_item__header {
min-height: 287px;
}
.plan_item__title-row {
flex-direction: column;
}
.plan_item__title-badges {
max-width: 100%;
justify-content: flex-start;
}
.plan_item__price {
padding-right: 0;
}
.plan_item__price-discount {
position: static;
margin-top: 20px;
margin-left: 8px;
width: 313px;
}
}
}
@ -786,4 +783,4 @@
width: 14px;
height: 14px;
object-fit: cover;
}
}

95
src/components/subscribe-plan-list/index.tsx

@ -38,10 +38,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 FIGMA_TEMP_ASSETS = {
// Figma temporary asset. Replace with a local exported icon if the design asset is added to the repo.
goal: "https://www.figma.com/api/mcp/asset/b627a414-493d-430e-bbba-2d862a46e127",
};
function getPlanKey(plan: ProductPlan, index: number): string {
if (plan.id != null && plan.id !== "") {
@ -65,6 +68,27 @@ 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);
}
/** 去掉 mark/title/section 标签,保留纯文本 */
function stripPlanInlineTags(text: string): string {
return text.replace(PLAN_INLINE_TAG_RE, "").trim();
@ -282,9 +306,6 @@ 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 课程
@ -295,7 +316,7 @@ export default function SubscribePlanList({ embedded = false, onBillingCycleChan
? t("subscribe.current_plan")
: userMemberLevel > level // 当前会员等级大于计划等级
? t("subscribe.already_member_level", { level: t(memberLevelMap[userMemberLevel] ?? memberLevelMap[0]) })
: t("subscribe.subscribe_now"); // 订阅按钮文本
: t("subscribe.plan_cta_goal"); // 订阅按钮文本
// 点击CTA按钮
const handleCtaClick = () => {
@ -309,6 +330,10 @@ export default function SubscribePlanList({ embedded = false, onBillingCycleChan
// 渲染描述段落
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 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 renderDescSegments = (line: string) =>
getPlanDescriptionSegments(line).map((seg, segIdx) => (
<span key={segIdx} className={seg.className}>
@ -322,29 +347,19 @@ export default function SubscribePlanList({ embedded = false, onBillingCycleChan
<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}
{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={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}
{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}
{pointPriceText ? <p className="plan_item__price-subtitle">{pointPriceText}</p> : null}
</div>
{price > 0 && plan.coins != null ? (
@ -429,20 +444,36 @@ export default function SubscribePlanList({ embedded = false, onBillingCycleChan
</div>
<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 className="plan_goal">
<div className="plan_goal__title">
<img className="plan_goal__icon" src={FIGMA_TEMP_ASSETS.goal} alt="" />
<span>{t("subscribe.plan_goal_title")}</span>
</div>
<p className="plan_goal__desc">
<span>{t("subscribe.plan_goal_line1")}</span>
<span>{t("subscribe.plan_goal_line2")}</span>
</p>
</div>
{descriptionLines.length > 0 ? (
<div className="plan_description">
<div className="plan_description__divider" aria-hidden="true" />
<ul className="plan_description__list">
{descriptionLines.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>
) : null}
</div>
</div>
</div>

7
src/locales/en-US.json

@ -564,7 +564,6 @@
"subscribe.promo_carousel_label": "Membership benefit campaigns",
"subscribe.billing_badge_hot": "HOT",
"subscribe.billing_badge_value": "Best value",
"subscribe.recommended_plan": "Recommended",
"subscribe.discount_off_8": "OFF 8%",
"subscribe.discount_off_15": "OFF 15%",
"subscribe.other_cycle_unavailable": "Other billing cycles are not available yet. Please choose single purchase",
@ -589,6 +588,12 @@
"subscribe.buy_now": "Buy now",
"subscribe.points_package": "Points package",
"subscribe.already_member_level": "You are already \"{{level}}\"",
"subscribe.plan_cta_goal": "Finish the first work",
"subscribe.plan_default_discount": "30% off limited offer",
"subscribe.plan_point_price": "100 points ≈ ¥{{value}}",
"subscribe.plan_goal_title": "More trials / steadier output",
"subscribe.plan_goal_line1": "Combine images, characters, videos,",
"subscribe.plan_goal_line2": "and shots into the first publishable work",
"subscribe.qa_title": "FAQ",
"subscribe.qa_subtitle": "Popi points and content rights",
"subscribe.qa_footnote_line1": "Note: The rules above are for FAQ display on the official site. Actual terms follow",

7
src/locales/zh-CN.json

@ -564,7 +564,6 @@
"subscribe.promo_carousel_label": "会员权益活动",
"subscribe.billing_badge_hot": "HOT",
"subscribe.billing_badge_value": "性价比",
"subscribe.recommended_plan": "主推计划",
"subscribe.discount_off_8": "OFF 8%",
"subscribe.discount_off_15": "OFF 15%",
"subscribe.other_cycle_unavailable": "其他周期暂未开放,请选择单次购买",
@ -589,6 +588,12 @@
"subscribe.buy_now": "立即购买",
"subscribe.points_package": "积分包",
"subscribe.already_member_level": "你已是{{level}}",
"subscribe.plan_cta_goal": "完成第一条作品",
"subscribe.plan_default_discount": "限时7折",
"subscribe.plan_point_price": "每100积分≈¥{{value}}元",
"subscribe.plan_goal_title": "多次试错 / 稳定出作品",
"subscribe.plan_goal_line1": "把图片、角色、视频和镜头",
"subscribe.plan_goal_line2": "组合成第一条可发布作品",
"subscribe.qa_title": "常见问题",
"subscribe.qa_subtitle": "Popi 积分与内容权益规则",
"subscribe.qa_footnote_line1": "注:以上规则用于官网常见问题展示,具体执行以",

7
src/locales/zh-TW.json

@ -565,7 +565,6 @@
"subscribe.promo_carousel_label": "會員權益活動",
"subscribe.billing_badge_hot": "HOT",
"subscribe.billing_badge_value": "性價比",
"subscribe.recommended_plan": "主推計劃",
"subscribe.discount_off_8": "OFF 8%",
"subscribe.discount_off_15": "OFF 15%",
"subscribe.other_cycle_unavailable": "其他週期暫未開放,請選擇單次購買",
@ -590,6 +589,12 @@
"subscribe.buy_now": "立即購買",
"subscribe.points_package": "積分包",
"subscribe.already_member_level": "你已是{{level}}",
"subscribe.plan_cta_goal": "完成第一條作品",
"subscribe.plan_default_discount": "限時7折",
"subscribe.plan_point_price": "每100積分≈¥{{value}}元",
"subscribe.plan_goal_title": "多次試錯 / 穩定出作品",
"subscribe.plan_goal_line1": "把圖片、角色、影片和鏡頭",
"subscribe.plan_goal_line2": "組合成第一條可發布作品",
"subscribe.qa_title": "常見問題",
"subscribe.qa_subtitle": "Popi 積分與內容權益規則",
"subscribe.qa_footnote_line1": "註:以上規則用於官網常見問題展示,具體執行以",

Loading…
Cancel
Save