feat(payment): sync plans and add coins rules

This commit is contained in:
2026-06-29 14:11:50 +08:00
parent 274fe87ced
commit e372d58b4c
24 changed files with 1099 additions and 224 deletions
@@ -1,24 +1,230 @@
/* ============================================================
* 侧边栏主屏幕(浅色主题)
* ============================================================ */
.shell {
position: relative;
display: flex;
flex-direction: column;
background: var(--color-page-background, #ffffff);
color: var(--color-auth-text-primary, #333333);
gap: 14px;
overflow: hidden;
min-height: 100dvh;
padding: 0 var(--page-padding-x, 28px) var(--spacing-xxl, 24px);
box-sizing: border-box;
padding: 0 var(--page-padding-x, 28px) 28px;
background:
radial-gradient(circle at 18% 6%, rgba(255, 206, 160, 0.22), transparent 28%),
radial-gradient(circle at 92% 0%, rgba(246, 87, 160, 0.22), transparent 30%),
linear-gradient(180deg, #fcf3f4 0%, #fffafa 44%, #ffffff 100%);
color: #191316;
}
.bgOrbOne,
.bgOrbTwo {
position: absolute;
z-index: 0;
pointer-events: none;
border-radius: 999px;
filter: blur(4px);
}
.bgOrbOne {
top: 86px;
right: -72px;
width: 168px;
height: 168px;
background: rgba(246, 87, 160, 0.11);
}
.bgOrbTwo {
bottom: 56px;
left: -88px;
width: 180px;
height: 180px;
background: rgba(248, 184, 62, 0.1);
}
.topBar,
.hero,
.userSlot,
.cardSlot,
.settingsSlot {
position: relative;
z-index: 1;
}
.topBar {
margin: 4px 0 -2px;
}
.hero {
padding: 12px 2px 6px;
}
.kicker {
margin: 0;
color: #f657a0;
font-size: 12px;
font-weight: 850;
letter-spacing: 0.14em;
line-height: 1;
text-transform: uppercase;
}
.title {
margin: 8px 0 0;
color: #171114;
font-size: 34px;
font-weight: 920;
letter-spacing: -1px;
line-height: 1;
}
.subtitle {
max-width: 320px;
margin: 10px 0 0;
color: #75636a;
font-size: 14px;
font-weight: 650;
line-height: 1.5;
}
.userSlot {
padding: var(--sidebar-user-padding-y, 20px) 0;
padding: 18px;
border: 1px solid rgba(25, 19, 22, 0.06);
border-radius: 26px;
background: rgba(255, 255, 255, 0.84);
box-shadow: 0 16px 40px rgba(55, 36, 44, 0.08);
backdrop-filter: blur(18px);
}
.cardSlot {
padding: var(--sidebar-card-gap-y, 8px) 0;
padding: 0;
}
.settingsSlot {
padding: var(--spacing-lg, 16px) 0 0;
margin-top: 2px;
padding: 2px 0 0;
}
.settingsLabel {
margin: 0 0 9px;
padding: 0 4px;
color: #817076;
font-size: 13px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.logoutCard {
display: flex;
width: 100%;
align-items: center;
gap: 12px;
box-sizing: border-box;
padding: 16px;
border: 1px solid rgba(25, 19, 22, 0.06);
border-radius: 22px;
background: rgba(255, 255, 255, 0.86);
color: #171114;
cursor: pointer;
font: inherit;
text-align: left;
box-shadow: 0 12px 30px rgba(55, 36, 44, 0.06);
transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.16s ease;
}
.logoutCard:hover {
background: #ffffff;
box-shadow: 0 16px 34px rgba(55, 36, 44, 0.1);
transform: translateY(-1px);
}
.logoutCard:active {
transform: translateY(1px);
}
.logoutCard:focus-visible {
outline: 2px solid #f657a0;
outline-offset: 3px;
}
.logoutIcon {
display: inline-flex;
width: 40px;
height: 40px;
flex: 0 0 auto;
align-items: center;
justify-content: center;
border-radius: 15px;
background: rgba(255, 107, 107, 0.1);
color: #ef4d64;
}
.logoutText {
display: flex;
min-width: 0;
flex-direction: column;
gap: 3px;
}
.logoutTitle {
color: #ef4d64;
font-size: 16px;
font-weight: 760;
line-height: 1.15;
}
.logoutSubtitle {
color: #817076;
font-size: 13px;
font-weight: 620;
line-height: 1.25;
}
.revealOne,
.revealTwo,
.revealThree {
animation: revealUp 0.42s ease both;
}
.revealOne {
animation-delay: 40ms;
}
.revealTwo {
animation-delay: 95ms;
}
.revealThree {
animation-delay: 150ms;
}
@keyframes revealUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 360px) {
.shell {
gap: 12px;
padding-right: 20px;
padding-left: 20px;
}
.title {
font-size: 30px;
}
.subtitle {
font-size: 13px;
}
.userSlot {
padding: 16px;
border-radius: 22px;
}
}
@@ -7,6 +7,7 @@ import styles from "./sidebar-wallet-card.module.css";
export interface SidebarWalletCardProps {
creditBalance: number;
onTopUp: () => void;
onRulesClick: () => void;
}
const formatCoins = (value: number): string =>
@@ -15,6 +16,7 @@ const formatCoins = (value: number): string =>
export function SidebarWalletCard({
creditBalance,
onTopUp,
onRulesClick,
}: SidebarWalletCardProps) {
return (
<section className={styles.card} aria-label="My wallet">
@@ -31,7 +33,11 @@ export function SidebarWalletCard({
<span className={styles.balanceUnit}> coins</span>
</p>
<button type="button" className={styles.rulesButton}>
<button
type="button"
className={styles.rulesButton}
onClick={onRulesClick}
>
Coins
</button>
</div>
+41 -18
View File
@@ -1,9 +1,10 @@
"use client";
import { LogOut } from "lucide-react";
import { signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import { MobileShell, SettingsSection } from "@/app/_components/core";
import { MobileShell } from "@/app/_components/core";
import { ROUTES } from "@/router/routes";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
@@ -20,6 +21,12 @@ import styles from "./components/sidebar-screen.module.css";
const FALLBACK_USERNAME = "User name";
const VIP_SUBSCRIPTION_URL = `${ROUTES.subscription}?type=vip`;
const STATUS_LABELS: Record<SidebarUserState, string> = {
guest: "Sign in to keep your coins and unlock more features.",
member: "Manage your wallet, subscription and account settings.",
vip: "Your VIP benefits are active. Enjoy the full experience.",
};
/**
* 侧边栏主屏幕
*
@@ -58,12 +65,22 @@ export function SidebarScreen() {
const avatarUrl = user.avatarUrl ?? user.currentUser?.avatarUrl ?? null;
return (
<MobileShell>
<MobileShell background="#fcf3f4">
<div className={styles.shell}>
<BackBar />
<div className={styles.bgOrbOne} aria-hidden="true" />
<div className={styles.bgOrbTwo} aria-hidden="true" />
{/* 用户信息区:三种状态 */}
<section className={styles.userSlot}>
<div className={styles.topBar}>
<BackBar />
</div>
<section className={styles.hero}>
<p className={styles.kicker}>Cozsweet</p>
<h1 className={styles.title}>Account</h1>
<p className={styles.subtitle}>{STATUS_LABELS[sidebarState]}</p>
</section>
<section className={`${styles.userSlot} ${styles.revealOne}`}>
<UserHeader
state={sidebarState}
name={name}
@@ -73,10 +90,11 @@ export function SidebarScreen() {
/>
</section>
<section className={styles.cardSlot}>
<section className={`${styles.cardSlot} ${styles.revealTwo}`}>
<SidebarWalletCard
creditBalance={user.creditBalance}
onTopUp={() => router.push(VIP_SUBSCRIPTION_URL)}
onRulesClick={() => router.push(ROUTES.coinsRules)}
/>
</section>
@@ -91,18 +109,23 @@ export function SidebarScreen() {
*/}
{sidebarState !== "guest" ? (
<section className={styles.settingsSlot}>
<SettingsSection
title="Other"
items={[
{
id: "logout",
title: "Log out",
destructive: true,
onClick: handleLogoutClick,
},
]}
/>
<section className={`${styles.settingsSlot} ${styles.revealThree}`}>
<p className={styles.settingsLabel}>Other</p>
<button
type="button"
className={styles.logoutCard}
onClick={handleLogoutClick}
>
<span className={styles.logoutIcon} aria-hidden="true">
<LogOut size={19} strokeWidth={2.2} />
</span>
<span className={styles.logoutText}>
<span className={styles.logoutTitle}>Log out</span>
<span className={styles.logoutSubtitle}>
End the current session safely
</span>
</span>
</button>
</section>
) : null}
</div>