feat(subscription): add VIP subscription page

Add a new /subscription route showing three VIP membership plans (Monthly,
Quarterly, Annual) with a pink→peach banner, per-plan gradient price bars,
a benefits card, a mock activate CTA, and an agreement checkbox.

- New reusable <Checkbox /> in src/app/_components/core for the legal
  agreement toggle
- Hardcoded SUBSCRIPTION_PLANS constant in src/data/constants
- New SubscriptionScreen composed of 7 colocated components (back link,
  user row, banner, plan card, benefits card, CTA button) with CSS
  Modules matching the existing design-token system
- Route registered as protected and reached from /sidebar "Membership"
- Mock CTA: window.alert + router.push(/chat) — no payment calls

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-15 17:48:41 +08:00
committed by chenhang
parent b9315c9f8b
commit e88a99626d
23 changed files with 1001 additions and 0 deletions
@@ -0,0 +1,65 @@
"use client";
/**
* 订阅套餐卡片
*
* 视觉规格(与设计稿对齐):
* - 卡片顶部:套餐名(12px / secondary
* - 中部:大字价格(28px / 300+ 划线原价(12px / line-through / 999
* - 底部 28px 高渐变条:白字 12px/600 每日折算价
* - 选中/高亮:2px 粉色边框 + 浅粉阴影
* - 三个卡片底部渐变不同:粉 → 紫红 → 紫
*/
import type { SubscriptionPlan, SubscriptionPlanId } from "@/data/constants/subscription-plans";
import styles from "./subscription-plan-card.module.css";
const PER_DAY_GRADIENTS: Record<SubscriptionPlanId, string> = {
monthly: "linear-gradient(90deg, #ff7ab8 0%, #ff5d9c 100%)",
quarterly: "linear-gradient(90deg, #d16bff 0%, #ff5da3 100%)",
annual: "linear-gradient(90deg, #9a4dff 0%, #c44dff 100%)",
};
export interface SubscriptionPlanCardProps {
plan: SubscriptionPlan;
selected: boolean;
onClick: () => void;
className?: string;
}
export function SubscriptionPlanCard({
plan,
selected,
onClick,
className,
}: SubscriptionPlanCardProps) {
const classes = [
styles.card,
selected ? styles.selected : "",
!selected && plan.highlight ? styles.highlight : "",
className,
]
.filter(Boolean)
.join(" ");
const perDayStyle = { background: PER_DAY_GRADIENTS[plan.id] };
return (
<button
type="button"
onClick={onClick}
aria-pressed={selected}
aria-label={`${plan.name} plan, $${plan.price}`}
className={classes}
>
<p className={styles.name}>{plan.name}</p>
<div className={styles.priceBlock}>
<span className={styles.currency}>$</span>
<span className={styles.price}>{plan.price}</span>
</div>
<p className={styles.originalPrice}>${plan.originalPrice}</p>
<div className={styles.perDayBar} style={perDayStyle}>
{plan.perDay}
</div>
</button>
);
}