diff --git a/barrelsby.json b/barrelsby.json index 24e4334a..5dc810c9 100644 --- a/barrelsby.json +++ b/barrelsby.json @@ -20,6 +20,7 @@ "./src/app/chat/components", "./src/app/sidebar/components", "./src/app/splash/components", + "./src/app/subscription/components", "./src/hooks", "./src/integrations", "./src/models/auth", diff --git a/src/app/_components/core/checkbox.module.css b/src/app/_components/core/checkbox.module.css new file mode 100644 index 00000000..91b98c01 --- /dev/null +++ b/src/app/_components/core/checkbox.module.css @@ -0,0 +1,56 @@ +/* 通用圆形复选框 + * + * 视觉规格: + * - 16px 白色圆形复选框 + 1px 浅阴影 + * - 选中时 10px 粉色实心圆 + * - 行内标签可点击 + */ +.row { + display: flex; + align-items: flex-start; + gap: var(--spacing-sm); + width: 100%; + cursor: pointer; +} + +.checkbox { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--auth-legal-checkbox-size); + height: var(--auth-legal-checkbox-size); + border-radius: 50%; + background: var(--color-auth-surface); + border: 1px solid var(--color-auth-surface); + padding: 0; + cursor: pointer; + margin-top: 2px; + box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06); +} + +.checkbox:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +.checkboxInner { + display: block; + width: var(--auth-legal-checkbox-inner-size); + height: var(--auth-legal-checkbox-inner-size); + border-radius: 50%; + background: var(--color-auth-legal-check); +} + +.text { + font-size: var(--font-size-sm); + line-height: 1.4; + color: var(--color-auth-text-primary); + text-align: left; + flex: 1 1 auto; +} + +.disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/src/app/_components/core/checkbox.tsx b/src/app/_components/core/checkbox.tsx new file mode 100644 index 00000000..652caaa8 --- /dev/null +++ b/src/app/_components/core/checkbox.tsx @@ -0,0 +1,65 @@ +"use client"; +/** + * 通用圆形复选框 + * + * 视觉规格(与 `auth-legal-text` 一致): + * - 16px 白色圆形 + * - 选中时 10px 粉色实心圆 + * - 提供可选 `label`,与复选框一起点击可切换 + */ +import type { ReactNode } from "react"; + +import styles from "./checkbox.module.css"; + +export interface CheckboxProps { + checked: boolean; + onChange: (next: boolean) => void; + /** 可选标签;提供时点击文字也能切换 */ + label?: ReactNode; + disabled?: boolean; + className?: string; + /** 当未提供 label 时使用 */ + ariaLabel?: string; +} + +export function Checkbox({ + checked, + onChange, + label, + disabled = false, + className, + ariaLabel, +}: CheckboxProps) { + const button = ( + onChange(!checked)} + className={[styles.checkbox, disabled ? styles.disabled : ""] + .filter(Boolean) + .join(" ")} + > + {checked ? : null} + + ); + + if (label == null) { + return ( + + {button} + + ); + } + + return ( + + {button} + {label} + + ); +} diff --git a/src/app/_components/core/index.ts b/src/app/_components/core/index.ts index c6bb6d94..ed7c62e1 100644 --- a/src/app/_components/core/index.ts +++ b/src/app/_components/core/index.ts @@ -4,6 +4,7 @@ export * from "./auth-back-button"; export * from "./bottom-sheet"; +export * from "./checkbox"; export * from "./dialog"; export * from "./loading-indicator"; export * from "./mobile-shell"; diff --git a/src/app/sidebar/components/sidebar-screen.tsx b/src/app/sidebar/components/sidebar-screen.tsx index 577a7ade..e689c446 100644 --- a/src/app/sidebar/components/sidebar-screen.tsx +++ b/src/app/sidebar/components/sidebar-screen.tsx @@ -76,6 +76,11 @@ export function SidebarScreen() { router.push(ROUTES.subscription), + }, { id: "logout", title: "Log out", diff --git a/src/app/subscription/components/index.ts b/src/app/subscription/components/index.ts new file mode 100644 index 00000000..ac8be8c5 --- /dev/null +++ b/src/app/subscription/components/index.ts @@ -0,0 +1,11 @@ +/** + * @file Automatically generated by barrelsby. + */ + +export * from "./subscription-back-link"; +export * from "./subscription-banner"; +export * from "./subscription-benefits-card"; +export * from "./subscription-cta-button"; +export * from "./subscription-plan-card"; +export * from "./subscription-screen"; +export * from "./subscription-user-row"; diff --git a/src/app/subscription/components/subscription-back-link.module.css b/src/app/subscription/components/subscription-back-link.module.css new file mode 100644 index 00000000..14119282 --- /dev/null +++ b/src/app/subscription/components/subscription-back-link.module.css @@ -0,0 +1,20 @@ +.backLink { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 8px; + margin-left: -8px; + color: #000; + text-decoration: none; + border-radius: var(--radius-md); +} + +.backLink:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +.icon { + display: block; + color: inherit; +} diff --git a/src/app/subscription/components/subscription-back-link.tsx b/src/app/subscription/components/subscription-back-link.tsx new file mode 100644 index 00000000..42bf3f6c --- /dev/null +++ b/src/app/subscription/components/subscription-back-link.tsx @@ -0,0 +1,45 @@ +"use client"; +/** + * 订阅页返回箭头 + * + * 视觉规格(与设计稿对齐): + * - 24×24 SVG chevron-left + * - 黑色,2px 描边 + * - 无 "Back" 文本 + */ +import Link from "next/link"; + +import { ROUTES } from "@/router/routes"; + +import styles from "./subscription-back-link.module.css"; + +export interface SubscriptionBackLinkProps { + className?: string; +} + +export function SubscriptionBackLink({ className }: SubscriptionBackLinkProps) { + return ( + + + + + + ); +} diff --git a/src/app/subscription/components/subscription-banner.module.css b/src/app/subscription/components/subscription-banner.module.css new file mode 100644 index 00000000..d118d929 --- /dev/null +++ b/src/app/subscription/components/subscription-banner.module.css @@ -0,0 +1,28 @@ +.banner { + position: relative; + background: linear-gradient(90deg, #f96ade 0%, #ffb88a 100%); + border-radius: var(--radius-xl); + padding: var(--spacing-lg) var(--spacing-md); + text-align: center; +} + +.text { + margin: 0; + font-size: 17px; + font-weight: 600; + color: #ffffff; + line-height: 1.3; +} + +/* 向下指的 CSS 三角 */ +.pointer { + position: absolute; + bottom: -8px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 10px solid #ffb88a; +} diff --git a/src/app/subscription/components/subscription-banner.tsx b/src/app/subscription/components/subscription-banner.tsx new file mode 100644 index 00000000..d114141e --- /dev/null +++ b/src/app/subscription/components/subscription-banner.tsx @@ -0,0 +1,26 @@ +"use client"; +/** + * 订阅页渐变横幅 + * + * 视觉规格(与设计稿对齐): + * - 粉→桃渐变背景(90deg) + * - 居中白色 17px/600 文字 + * - 底部中央有一个向下指的 CSS 三角 + */ +import styles from "./subscription-banner.module.css"; + +export interface SubscriptionBannerProps { + className?: string; +} + +export function SubscriptionBanner({ className }: SubscriptionBannerProps) { + return ( + + Subscribe to become the VIP Member + + + ); +} diff --git a/src/app/subscription/components/subscription-benefits-card.module.css b/src/app/subscription/components/subscription-benefits-card.module.css new file mode 100644 index 00000000..082bb2a0 --- /dev/null +++ b/src/app/subscription/components/subscription-benefits-card.module.css @@ -0,0 +1,56 @@ +.card { + background: #ffffff; + border-radius: var(--radius-xl); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + overflow: hidden; + border: 1px solid rgba(0, 0, 0, 0.04); +} + +.header { + background: linear-gradient( + 90deg, + var(--color-auth-primary-gradient-start), + var(--color-auth-primary-gradient-end) + ); + padding: var(--spacing-md); + text-align: center; +} + +.title { + margin: 0; + font-size: var(--font-size-lg); + font-weight: 600; + color: #ffffff; + line-height: 1.3; +} + +.list { + list-style: none; + margin: 0; + padding: 0; +} + +.item { + display: flex; + align-items: flex-start; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-lg); + font-size: var(--font-size-md); + color: var(--color-auth-text-primary); + line-height: 1.4; +} + +.item + .item { + border-top: 1px solid rgba(0, 0, 0, 0.05); +} + +.numeral { + flex: 0 0 auto; + font-weight: 500; + min-width: 20px; + color: var(--color-auth-text-primary); +} + +.text { + flex: 1 1 auto; +} diff --git a/src/app/subscription/components/subscription-benefits-card.tsx b/src/app/subscription/components/subscription-benefits-card.tsx new file mode 100644 index 00000000..4a191433 --- /dev/null +++ b/src/app/subscription/components/subscription-benefits-card.tsx @@ -0,0 +1,41 @@ +"use client"; +/** + * 订阅权益卡片 + * + * 视觉规格(与设计稿对齐): + * - 白色卡片,--radius-xl 圆角 + * - 顶部粉色渐变条:居中白色标题 + * - 主体:1./2./3./4. 编号列表 + * - 行间细分隔线 + */ +import styles from "./subscription-benefits-card.module.css"; + +export interface SubscriptionBenefitsCardProps { + title: string; + items: readonly string[]; + className?: string; +} + +export function SubscriptionBenefitsCard({ + title, + items, + className, +}: SubscriptionBenefitsCardProps) { + return ( + + + {title} + + + {items.map((item, idx) => ( + + {idx + 1}. + {item} + + ))} + + + ); +} diff --git a/src/app/subscription/components/subscription-cta-button.module.css b/src/app/subscription/components/subscription-cta-button.module.css new file mode 100644 index 00000000..9af87f5a --- /dev/null +++ b/src/app/subscription/components/subscription-cta-button.module.css @@ -0,0 +1,67 @@ +/* 视觉规格: + * - 全宽,52px 高 + * - 粉渐变(#f96ADE → #f657A0) + * - 半径 24,白字 16px/600 + * - 阴影 0 3px 5px rgba(208,12,65,0.20) + */ +.button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 100%; + height: 52px; + padding: 0 var(--spacing-lg); + border: none; + border-radius: var(--radius-xxxl); + background: linear-gradient( + 90deg, + var(--color-auth-primary-gradient-start), + var(--color-auth-primary-gradient-end) + ); + box-shadow: 0 3px 5px var(--color-auth-primary-button-shadow); + color: #ffffff; + font-size: var(--font-size-lg); + font-weight: 600; + cursor: pointer; + transition: filter 0.15s ease, transform 0.05s ease, opacity 0.15s ease; +} + +.button:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +.button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.button:not(:disabled):active { + transform: scale(0.98); +} + +.label { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + flex: 1 1 auto; + justify-content: center; + text-align: center; +} + +.spinner { + display: inline-block; + width: 18px; + height: 18px; + border-width: 2px; + border-style: solid; + border-color: #ffffff transparent transparent transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/app/subscription/components/subscription-cta-button.tsx b/src/app/subscription/components/subscription-cta-button.tsx new file mode 100644 index 00000000..f59a4038 --- /dev/null +++ b/src/app/subscription/components/subscription-cta-button.tsx @@ -0,0 +1,42 @@ +"use client"; +/** + * 订阅页主操作按钮 + * + * 视觉规格(与设计稿对齐): + * - 全宽,52px 高,--radius-xxxl 圆角 + * - 粉色渐变(#f96ADE → #f657A0) + * - 白字 16px/600 + 粉色阴影 + * - :disabled 灰化 + */ +import type { ButtonHTMLAttributes, ReactNode } from "react"; + +import styles from "./subscription-cta-button.module.css"; + +export interface SubscriptionCtaButtonProps + extends ButtonHTMLAttributes { + children: ReactNode; + isLoading?: boolean; +} + +export function SubscriptionCtaButton({ + children, + isLoading = false, + className, + disabled, + ...rest +}: SubscriptionCtaButtonProps) { + const classes = [styles.button, className].filter(Boolean).join(" "); + return ( + + + {isLoading ? : null} + {children} + + + ); +} diff --git a/src/app/subscription/components/subscription-plan-card.module.css b/src/app/subscription/components/subscription-plan-card.module.css new file mode 100644 index 00000000..0c9dd96a --- /dev/null +++ b/src/app/subscription/components/subscription-plan-card.module.css @@ -0,0 +1,87 @@ +.card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + padding: var(--spacing-sm) var(--spacing-xs) 0; + background: #ffffff; + border: 1px solid transparent; + border-radius: var(--radius-lg); + cursor: pointer; + text-align: center; + overflow: hidden; + transition: border-color 0.15s ease, box-shadow 0.15s ease, + transform 0.05s ease; + width: 100%; + font: inherit; + color: inherit; +} + +.card:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +.card:not(:disabled):active { + transform: scale(0.98); +} + +.highlight { + border-color: var(--color-accent); + box-shadow: 0 4px 12px rgba(248, 77, 150, 0.18); +} + +.selected { + border-color: var(--color-accent); + border-width: 2px; + box-shadow: 0 6px 16px rgba(248, 77, 150, 0.28); +} + +.name { + margin: 0 0 var(--spacing-xs); + font-size: var(--font-size-sm); + font-weight: 500; + color: var(--color-auth-text-primary); + line-height: 1.2; +} + +.priceBlock { + display: flex; + align-items: baseline; + justify-content: center; + gap: 2px; + color: #000; + line-height: 1; +} + +.currency { + font-size: var(--font-size-md); + font-weight: 400; + color: #000; +} + +.price { + font-size: 28px; + font-weight: 300; + color: #000; + line-height: 1; +} + +.originalPrice { + margin: 4px 0 var(--spacing-sm); + font-size: var(--font-size-xs); + color: #999999; + text-decoration: line-through; + line-height: 1; +} + +.perDayBar { + align-self: stretch; + margin: 0 calc(-1 * var(--spacing-xs)) 0; + padding: 6px 0; + color: #ffffff; + font-size: var(--font-size-xs); + font-weight: 600; + text-align: center; + line-height: 1.2; +} diff --git a/src/app/subscription/components/subscription-plan-card.tsx b/src/app/subscription/components/subscription-plan-card.tsx new file mode 100644 index 00000000..136bba70 --- /dev/null +++ b/src/app/subscription/components/subscription-plan-card.tsx @@ -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 = { + 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 ( + + {plan.name} + + $ + {plan.price} + + ${plan.originalPrice} + + {plan.perDay} + + + ); +} diff --git a/src/app/subscription/components/subscription-screen.module.css b/src/app/subscription/components/subscription-screen.module.css new file mode 100644 index 00000000..a73dbb8b --- /dev/null +++ b/src/app/subscription/components/subscription-screen.module.css @@ -0,0 +1,65 @@ +.shell { + display: flex; + flex-direction: column; + min-height: 100dvh; + background: var(--color-page-background); + padding: 0 var(--spacing-lg) var(--spacing-xxl); +} + +.header { + display: flex; + align-items: center; + height: 40px; + padding-top: var(--spacing-sm); +} + +.backSlot { + display: inline-flex; +} + +.userSlot { + margin-top: var(--spacing-md); + padding: 0 var(--spacing-xs); +} + +.bannerSlot { + margin-top: var(--spacing-lg); +} + +.plansRow { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--spacing-sm); + margin-top: var(--spacing-xl); +} + +.autoRenewCaption { + margin: var(--spacing-md) 0 0; + padding: 0 var(--spacing-xs); + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + line-height: 1.5; +} + +.benefitsSlot { + margin-top: var(--spacing-lg); +} + +.ctaSlot { + margin-top: var(--spacing-xl); +} + +.agreementSlot { + margin-top: var(--spacing-md); + padding: 0 var(--spacing-xs); +} + +.agreementLink { + color: var(--color-auth-text-primary); + font-weight: 700; + text-decoration: none; +} + +.agreementLink:hover { + text-decoration: underline; +} diff --git a/src/app/subscription/components/subscription-screen.tsx b/src/app/subscription/components/subscription-screen.tsx new file mode 100644 index 00000000..41f5a2d5 --- /dev/null +++ b/src/app/subscription/components/subscription-screen.tsx @@ -0,0 +1,149 @@ +"use client"; +/** + * 订阅页主屏幕 + * + * 原始 Dart: lib/ui/subscription/... + * + * 布局(顶到底): + * 1. 顶部返回箭头 + * 2. 用户信息行 + * 3. 渐变横幅 + * 4. 三个套餐卡片 + * 5. 自动续费说明 + * 6. 权益列表卡片 + * 7. 主操作按钮 + * 8. 协议复选框 + */ +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +import { Checkbox } from "@/app/_components/core/checkbox"; +import { MobileShell } from "@/app/_components/core/mobile-shell"; +import { AppConstants } from "@/core/app_constants"; +import { SUBSCRIPTION_PLANS } from "@/data/constants/subscription-plans"; +import { ROUTES } from "@/router/routes"; +import { useUserState } from "@/stores/user/user-context"; + +import { SubscriptionBackLink } from "./subscription-back-link"; +import { SubscriptionBanner } from "./subscription-banner"; +import { SubscriptionBenefitsCard } from "./subscription-benefits-card"; +import { SubscriptionCtaButton } from "./subscription-cta-button"; +import { SubscriptionPlanCard } from "./subscription-plan-card"; +import { SubscriptionUserRow } from "./subscription-user-row"; +import styles from "./subscription-screen.module.css"; + +const VIP_BENEFITS = [ + "Unlimited chat sessions per day", + "Unlimited free access to private messages", + "Unlimited access to AI boyfriend photos", + "Free voice chat for X minutes", +] as const; + +const FALLBACK_USERNAME = "User name"; + +export function SubscriptionScreen() { + const router = useRouter(); + const user = useUserState(); + + const initialPlanId = + SUBSCRIPTION_PLANS.find((p) => p.highlight)?.id ?? null; + + const [selectedPlanId, setSelectedPlanId] = useState( + initialPlanId, + ); + const [agreed, setAgreed] = useState(false); + + const canActivate = selectedPlanId !== null && agreed; + + const handleActivate = () => { + if (!canActivate) return; + // Mock phase: 仅提示并跳走,不调用任何支付接口 + window.alert("Subscription activated (mock). Welcome to VIP!"); + router.push(ROUTES.chat); + }; + + const name = user.currentUser?.username ?? FALLBACK_USERNAME; + const avatarUrl = user.currentUser?.avatarUrl ?? null; + + return ( + + + + + + + + + + + + + + + + {SUBSCRIPTION_PLANS.map((plan) => ( + setSelectedPlanId(plan.id)} + /> + ))} + + + + It will automatically renew upon expiration, and can be canceled at + any time + + + + + + + + + Confirm the agreement and Activate + + + + + + I agree to the{" "} + + VIP Membership Benefits Agreement + {" "} + and{" "} + + Automatic renewal Agreement + + > + } + /> + + + + ); +} diff --git a/src/app/subscription/components/subscription-user-row.module.css b/src/app/subscription/components/subscription-user-row.module.css new file mode 100644 index 00000000..bf138f26 --- /dev/null +++ b/src/app/subscription/components/subscription-user-row.module.css @@ -0,0 +1,52 @@ +.row { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.avatar { + flex: 0 0 auto; + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--color-accent); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + color: #ffffff; +} + +.avatarImg { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.avatarIcon { + color: inherit; +} + +.text { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1 1 auto; + min-width: 0; +} + +.name { + margin: 0; + font-size: var(--font-size-lg); + font-weight: 600; + color: #000; + line-height: 1.2; +} + +.subtitle { + margin: 0; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + line-height: 1.2; +} diff --git a/src/app/subscription/components/subscription-user-row.tsx b/src/app/subscription/components/subscription-user-row.tsx new file mode 100644 index 00000000..97d3c5b6 --- /dev/null +++ b/src/app/subscription/components/subscription-user-row.tsx @@ -0,0 +1,61 @@ +"use client"; +/** + * 订阅页用户信息行 + * + * 视觉规格(与设计稿对齐): + * - 56×56 粉色圆形头像 + * - 头像右侧两行文本:用户名(16px/600)、副标题(12px/secondary) + * - 无头像时显示默认用户图标 + */ +import Image from "next/image"; + +import styles from "./subscription-user-row.module.css"; + +export interface SubscriptionUserRowProps { + name: string; + /** null → 显示默认用户图标 */ + avatarUrl: string | null; + subtitle?: string; + className?: string; +} + +export function SubscriptionUserRow({ + name, + avatarUrl, + subtitle = "VIP membership not activated", + className, +}: SubscriptionUserRowProps) { + return ( + + + {avatarUrl ? ( + + ) : ( + + + + + )} + + + {name} + {subtitle} + + + ); +} diff --git a/src/app/subscription/page.tsx b/src/app/subscription/page.tsx new file mode 100644 index 00000000..e600f7d5 --- /dev/null +++ b/src/app/subscription/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { SubscriptionScreen } from "@/app/subscription/components/subscription-screen"; + +export default function SubscriptionPage() { + return ; +} diff --git a/src/data/constants/subscription-plans.ts b/src/data/constants/subscription-plans.ts new file mode 100644 index 00000000..a8f1cd83 --- /dev/null +++ b/src/data/constants/subscription-plans.ts @@ -0,0 +1,49 @@ +/** + * VIP 订阅套餐常量 + * + * Mock 阶段硬编码,后续可改为从 `ApiPath.paymentProducts` 拉取。 + * + * 原始 Dart: lib/ui/subscription/... + */ +export type SubscriptionPlanId = "monthly" | "quarterly" | "annual"; + +export interface SubscriptionPlan { + id: SubscriptionPlanId; + /** 卡片顶部名称,例如 "Monthly" */ + name: string; + /** 当前价格 */ + price: number; + /** 划线原价 */ + originalPrice: number; + /** 每日折算价字符串,e.g. "$0.66/day" */ + perDay: string; + /** 是否高亮(默认推荐:月付) */ + highlight: boolean; +} + +export const SUBSCRIPTION_PLANS: readonly SubscriptionPlan[] = [ + { + id: "monthly", + name: "Monthly", + price: 19.9, + originalPrice: 24.9, + perDay: "$0.66/day", + highlight: true, + }, + { + id: "quarterly", + name: "Quarterly", + price: 49.9, + originalPrice: 59.9, + perDay: "$0.55/day", + highlight: false, + }, + { + id: "annual", + name: "Annual", + price: 169.9, + originalPrice: 199.9, + perDay: "$0.47/day", + highlight: false, + }, +] as const; diff --git a/src/router/routes.ts b/src/router/routes.ts index 668ef8fb..111d1614 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -21,6 +21,7 @@ export const ROUTES = { chat: "/chat", auth: "/auth", sidebar: "/sidebar", + subscription: "/subscription", } as const; export type StaticRoute = (typeof ROUTES)[keyof typeof ROUTES]; @@ -41,6 +42,7 @@ export const AUTH_ONLY_ROUTES: readonly StaticRoute[] = [ export const PROTECTED_ROUTES: readonly StaticRoute[] = [ ROUTES.chat, ROUTES.sidebar, + ROUTES.subscription, ] as const; /** 公开路由(游客态也允许访问) */
Subscribe to become the VIP Member
{plan.name}
${plan.originalPrice}
+ It will automatically renew upon expiration, and can be canceled at + any time +
{name}
{subtitle}