diff --git a/plans.md b/plans.md
new file mode 100644
index 00000000..ceff63b3
--- /dev/null
+++ b/plans.md
@@ -0,0 +1,34 @@
+GET https://proapi.banlv-ai.com/api/payment/plans
+结果:
+status=200
+success=true
+plans 数量 8
+VIP 套餐 3 个
+credits 套餐 5 个
+snake_keys=[],已经没有 plan_id / amount_cents 这种字段
+schema 校验通过:proapi_plans_camel_schema_ok true
+前端按这个结构用:
+type PaymentPlan = {
+ planId: string;
+ planName: string;
+ orderType: 'vip_monthly' | 'vip_quarterly' | 'vip_annual' | 'dol';
+ vipDays: number | null;
+ dolAmount: number | null;
+ creditBalance: number;
+ amountCents: number;
+ originalAmountCents: number | null;
+ dailyPriceCents: number | null;
+ currency: string;
+};
+分组:
+const plans = res.data.plans;
+
+const vipPlans = plans.filter(p => p.vipDays !== null);
+const creditPlans = plans.filter(p => p.dolAmount !== null);
+下单仍然传 planId:
+{
+ "planId": "vip_monthly",
+ "payChannel": "stripe",
+ "autoRenew": true
+}
+价格、币种全部以前端拿到的 amountCents / currency 展示,不要写死。当前我这边访问 pro 返回的是 JPY,这是 Cloudflare 根据访问 IP 判的国家币种。
\ No newline at end of file
diff --git a/src/app/coins-rules/coins-rules-screen.module.css b/src/app/coins-rules/coins-rules-screen.module.css
new file mode 100644
index 00000000..eb8dc166
--- /dev/null
+++ b/src/app/coins-rules/coins-rules-screen.module.css
@@ -0,0 +1,259 @@
+.screen {
+ min-height: 100dvh;
+ box-sizing: border-box;
+ padding: 18px 20px 28px;
+ background:
+ radial-gradient(circle at 16% 5%, rgba(255, 196, 143, 0.2), transparent 28%),
+ radial-gradient(circle at 86% 8%, rgba(246, 87, 160, 0.2), transparent 26%),
+ linear-gradient(180deg, #fcf3f4 0%, #fffafa 48%, #fff 100%);
+ color: #191316;
+}
+
+.header {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+
+.backLink {
+ display: inline-flex;
+ width: 42px;
+ height: 42px;
+ align-items: center;
+ justify-content: center;
+ color: #21191d;
+ text-decoration: none;
+ border: 1px solid rgba(25, 19, 22, 0.08);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.82);
+ box-shadow: 0 10px 22px rgba(50, 30, 40, 0.08);
+ transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.16s ease;
+}
+
+.backLink:hover {
+ background: #ffffff;
+ box-shadow: 0 14px 26px rgba(50, 30, 40, 0.12);
+ transform: translateY(-1px);
+}
+
+.backLink:active {
+ transform: translateY(1px);
+}
+
+.backLink:focus-visible {
+ outline: 2px solid #f657a0;
+ outline-offset: 3px;
+}
+
+.hero {
+ position: relative;
+ overflow: hidden;
+ padding: 22px;
+ border: 1px solid rgba(246, 87, 160, 0.14);
+ border-radius: 28px;
+ background:
+ linear-gradient(135deg, rgba(255, 255, 255, 0.88) 0%, rgba(255, 237, 246, 0.94) 100%),
+ #ffffff;
+ box-shadow: 0 18px 46px rgba(86, 45, 62, 0.1);
+}
+
+.hero::after {
+ position: absolute;
+ right: -34px;
+ bottom: -42px;
+ width: 150px;
+ height: 150px;
+ border-radius: 999px;
+ background: radial-gradient(circle, rgba(246, 87, 160, 0.2), transparent 68%);
+ content: "";
+}
+
+.heroIcon {
+ display: inline-flex;
+ width: 46px;
+ height: 46px;
+ align-items: center;
+ justify-content: center;
+ border-radius: 16px;
+ background: linear-gradient(180deg, #ffe89c 0%, #f8b83e 100%);
+ color: #8f5400;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.72),
+ 0 12px 24px rgba(216, 132, 22, 0.2);
+}
+
+.eyebrow {
+ margin: 16px 0 0;
+ color: #f657a0;
+ font-size: 12px;
+ font-weight: 850;
+ letter-spacing: 0.12em;
+ line-height: 1;
+ text-transform: uppercase;
+}
+
+.title {
+ margin: 8px 0 0;
+ color: #171114;
+ font-size: 30px;
+ font-weight: 900;
+ letter-spacing: -0.8px;
+ line-height: 1.08;
+}
+
+.subtitle {
+ max-width: 300px;
+ margin: 10px 0 0;
+ color: #6d5d64;
+ font-size: 15px;
+ font-weight: 650;
+ line-height: 1.55;
+}
+
+.freeCard {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 12px;
+ align-items: center;
+ margin-top: 16px;
+ padding: 16px;
+ border: 1px solid rgba(248, 184, 62, 0.24);
+ border-radius: 22px;
+ background: rgba(255, 250, 235, 0.84);
+ box-shadow: 0 12px 28px rgba(142, 93, 18, 0.08);
+}
+
+.freeIcon {
+ display: inline-flex;
+ width: 40px;
+ height: 40px;
+ align-items: center;
+ justify-content: center;
+ border-radius: 14px;
+ background: #ffffff;
+ color: #d98100;
+ box-shadow: 0 8px 18px rgba(142, 93, 18, 0.1);
+}
+
+.freeTitle {
+ margin: 0;
+ color: #2a1f14;
+ font-size: 16px;
+ font-weight: 850;
+ line-height: 1.1;
+}
+
+.freeText {
+ margin: 5px 0 0;
+ color: #7a6040;
+ font-size: 13px;
+ font-weight: 650;
+ line-height: 1.45;
+}
+
+.ruleList {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-top: 16px;
+}
+
+.ruleCard {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ gap: 12px;
+ align-items: center;
+ padding: 14px;
+ border: 1px solid rgba(25, 19, 22, 0.06);
+ border-radius: 20px;
+ background: rgba(255, 255, 255, 0.9);
+ box-shadow: 0 10px 26px rgba(55, 36, 44, 0.06);
+ animation: cardIn 0.42s ease both;
+ transition: box-shadow 0.18s ease, transform 0.18s ease;
+}
+
+.ruleCard:hover {
+ box-shadow: 0 15px 32px rgba(55, 36, 44, 0.1);
+ transform: translateY(-2px);
+}
+
+.ruleIcon {
+ display: inline-flex;
+ width: 36px;
+ height: 36px;
+ align-items: center;
+ justify-content: center;
+ border-radius: 14px;
+ background: #fff0f6;
+ color: #f657a0;
+}
+
+.ruleContent {
+ min-width: 0;
+}
+
+.ruleTitle {
+ margin: 0;
+ color: #21191d;
+ font-size: 16px;
+ font-weight: 850;
+ line-height: 1.15;
+}
+
+.freeBadge {
+ display: inline-flex;
+ margin: 7px 0 0;
+ padding: 4px 9px;
+ border-radius: 999px;
+ background: #edf4ff;
+ color: #2e6eea;
+ font-size: 12px;
+ font-weight: 780;
+ line-height: 1;
+}
+
+.cost {
+ margin: 0;
+ color: #171114;
+ font-size: 15px;
+ font-weight: 900;
+ line-height: 1.15;
+ text-align: right;
+ white-space: nowrap;
+}
+
+@keyframes cardIn {
+ from {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@media (max-width: 360px) {
+ .screen {
+ padding-right: 16px;
+ padding-left: 16px;
+ }
+
+ .hero {
+ padding: 20px;
+ }
+
+ .title {
+ font-size: 27px;
+ }
+
+ .ruleCard {
+ grid-template-columns: auto minmax(0, 1fr);
+ }
+
+ .cost {
+ grid-column: 2;
+ text-align: left;
+ }
+}
diff --git a/src/app/coins-rules/coins-rules-screen.tsx b/src/app/coins-rules/coins-rules-screen.tsx
new file mode 100644
index 00000000..53e75ab8
--- /dev/null
+++ b/src/app/coins-rules/coins-rules-screen.tsx
@@ -0,0 +1,130 @@
+"use client";
+
+import {
+ ArrowLeft,
+ Coins,
+ Gift,
+ ImageIcon,
+ MessageCircle,
+ Mic2,
+ Phone,
+ Sparkles,
+} from "lucide-react";
+import Link from "next/link";
+
+import { MobileShell } from "@/app/_components/core";
+import { ROUTES } from "@/router/routes";
+
+import styles from "./coins-rules-screen.module.css";
+
+interface CoinRuleItem {
+ title: string;
+ cost: string;
+ free?: string;
+ icon: typeof MessageCircle;
+}
+
+const COIN_RULES: readonly CoinRuleItem[] = [
+ {
+ title: "普通聊天",
+ cost: "2 coins / 条",
+ free: "每周免费 50 条",
+ icon: MessageCircle,
+ },
+ {
+ title: "私密聊天",
+ cost: "10 coins / 条",
+ free: "每天免费 1 条",
+ icon: Sparkles,
+ },
+ {
+ title: "语音消息",
+ cost: "20 coins / 条",
+ icon: Mic2,
+ },
+ {
+ title: "语音电话",
+ cost: "20 coins / 张",
+ icon: Phone,
+ },
+ {
+ title: "照片",
+ cost: "40 coins / 张",
+ icon: ImageIcon,
+ },
+ {
+ title: "私密相册",
+ cost: "10 张 300 coins",
+ icon: ImageIcon,
+ },
+ {
+ title: "私密相册",
+ cost: "20 张 600 coins",
+ icon: ImageIcon,
+ },
+] as const;
+
+export function CoinsRulesScreen() {
+ return (
+
+
+
+
+
+
+
+
+
+
免费额度
+
+ 普通聊天每周免费 50 条,私密聊天每天免费 1 条。
+
+
+
+
+
+ {COIN_RULES.map((rule, index) => {
+ const Icon = rule.icon;
+ return (
+
+
+
+
+
+
{rule.title}
+ {rule.free ? (
+
{rule.free}
+ ) : null}
+
+ {rule.cost}
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/src/app/coins-rules/page.tsx b/src/app/coins-rules/page.tsx
new file mode 100644
index 00000000..dc6e6cd3
--- /dev/null
+++ b/src/app/coins-rules/page.tsx
@@ -0,0 +1,5 @@
+import { CoinsRulesScreen } from "./coins-rules-screen";
+
+export default function CoinsRulesPage() {
+ return ;
+}
diff --git a/src/app/sidebar/components/sidebar-screen.module.css b/src/app/sidebar/components/sidebar-screen.module.css
index f5d7d1b7..e989c7a5 100644
--- a/src/app/sidebar/components/sidebar-screen.module.css
+++ b/src/app/sidebar/components/sidebar-screen.module.css
@@ -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;
+ }
}
diff --git a/src/app/sidebar/components/sidebar-wallet-card.tsx b/src/app/sidebar/components/sidebar-wallet-card.tsx
index 1a42be0a..a9395d41 100644
--- a/src/app/sidebar/components/sidebar-wallet-card.tsx
+++ b/src/app/sidebar/components/sidebar-wallet-card.tsx
@@ -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 (
@@ -31,7 +33,11 @@ export function SidebarWalletCard({
coins
-