ConsoleShell.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. 'use client';
  2. /**
  3. * 控制台外壳 - 侧边栏导航(4 标签)+ 配额提醒横幅 + 主区头部 + 面板切换
  4. * 客户端组件:管理 activeTab 与 bannerOpen 状态,承载各面板
  5. * 会话:读取 localStorage(dotouch_user / dotouch_token),未登录跳转登录弹窗
  6. */
  7. import { useEffect, useState } from 'react';
  8. import { useRouter } from 'next/navigation';
  9. import { useLocale, useTranslations } from 'next-intl';
  10. import { BrandLogo } from '@/components/BrandLogo';
  11. import { OverviewPanel } from './OverviewPanel';
  12. import { KeysPanel } from './KeysPanel';
  13. import { WalletPanel } from './WalletPanel';
  14. import { UsagePanel } from './UsagePanel';
  15. import { ReferralPanel } from './ReferralPanel';
  16. import { cls } from './ui';
  17. /** 演示环境 API 地址(生产替换为真实网关域名) */
  18. const API_BASE = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:8081/v1';
  19. /** 侧边导航项(图标 + i18n 标签键) */
  20. const tabs = [
  21. { id: 'overview', icon: '📊', labelKey: 'c_overview' },
  22. { id: 'keys', icon: '🔑', labelKey: 'c_keys' },
  23. { id: 'wallet', icon: '💳', labelKey: 'c_wallet' },
  24. { id: 'usage', icon: '📈', labelKey: 'c_usage' },
  25. { id: 'referral', icon: '🤝', labelKey: 'c_referral' },
  26. ] as const;
  27. type Tab = (typeof tabs)[number]['id'];
  28. export function ConsoleShell() {
  29. const t = useTranslations();
  30. const locale = useLocale();
  31. const router = useRouter();
  32. const [activeTab, setActiveTab] = useState<Tab>('overview');
  33. // 配额提醒横幅默认隐藏,由总览面板的演示开关触发
  34. const [bannerOpen, setBannerOpen] = useState(false);
  35. const [userEmail, setUserEmail] = useState<string | null>(null);
  36. const [balanceCents, setBalanceCents] = useState<number | null>(null);
  37. // 会话检查:读取本地登录态,未登录跳转官网登录弹窗
  38. useEffect(() => {
  39. let email: string | null = null;
  40. let token: string | null = null;
  41. try {
  42. email = localStorage.getItem('dotouch_user');
  43. token = localStorage.getItem('dotouch_token');
  44. } catch { /* 忽略 */ }
  45. if (!email || !token) {
  46. router.replace(`/${locale}?auth=login`);
  47. return;
  48. }
  49. setUserEmail(email);
  50. // 拉取真实余额
  51. fetch(`${API_BASE}/wallet`, {
  52. headers: { Authorization: `Bearer ${token}` },
  53. })
  54. .then((r) => r.json().catch(() => null))
  55. .then((data) => {
  56. if (data && typeof data.balanceCents === 'number') {
  57. setBalanceCents(data.balanceCents);
  58. }
  59. })
  60. .catch(() => { /* 余额获取失败不阻塞页面 */ });
  61. }, [locale, router]);
  62. const logout = () => {
  63. try {
  64. localStorage.removeItem('dotouch_token');
  65. localStorage.removeItem('dotouch_user');
  66. } catch { /* 忽略 */ }
  67. router.push(`/${locale}`);
  68. };
  69. const activeLabel = t(tabs.find((tab) => tab.id === activeTab)!.labelKey);
  70. return (
  71. <div className="flex w-full flex-1">
  72. {/* ══ 侧边栏 ══ */}
  73. <aside className="flex shrink-0 flex-col border-b border-white/[0.08] bg-brand-card min-[860px]:sticky min-[860px]:top-0 min-[860px]:h-screen min-[860px]:w-[236px] min-[860px]:border-b-0 min-[860px]:border-r min-[860px]:border-white/[0.08]">
  74. {/* 品牌 Logo(桌面端) */}
  75. <div className="hidden border-b border-white/[0.08] px-[10px] pb-[18px] pt-5 min-[860px]:block">
  76. <BrandLogo idPrefix="console-side" className="h-[34px] w-auto" />
  77. </div>
  78. {/* 导航:移动端横向 / 桌面端纵向 */}
  79. <nav className="flex flex-1 flex-wrap gap-1 p-3.5 min-[860px]:flex-col min-[860px]:flex-nowrap">
  80. {tabs.map((tab) => (
  81. <button
  82. key={tab.id}
  83. type="button"
  84. onClick={() => setActiveTab(tab.id)}
  85. className={`flex items-center gap-2.5 rounded-[10px] px-3 py-[11px] text-sm transition-colors ${
  86. activeTab === tab.id
  87. ? 'bg-[rgba(124,92,255,.15)] font-semibold text-brand-text'
  88. : 'text-brand-muted hover:bg-[rgba(124,92,255,.08)] hover:text-brand-text'
  89. }`}
  90. >
  91. <span className="w-5 text-center">{tab.icon}</span>
  92. <span>{t(tab.labelKey)}</span>
  93. </button>
  94. ))}
  95. </nav>
  96. {/* 用户区:真实登录邮箱 + 余额 + 退出(桌面端) */}
  97. <div className="hidden border-t border-white/[0.08] px-3 pb-3 pt-3.5 min-[860px]:block">
  98. <div className="break-all text-[12.5px] text-brand-muted">{userEmail ?? '…'}</div>
  99. <div className="mt-1 text-base font-extrabold">
  100. {balanceCents !== null ? `$${(balanceCents / 100).toFixed(2)}` : '$—'}
  101. </div>
  102. <button
  103. type="button"
  104. onClick={logout}
  105. className="mt-2 w-full rounded-lg border border-white/[0.08] bg-brand-card2 px-3 py-1.5 text-xs text-brand-muted transition-colors hover:border-[rgba(255,107,107,.4)] hover:text-[#FF8A8A]"
  106. >
  107. {t('c_logout') ?? '退出登录'}
  108. </button>
  109. </div>
  110. </aside>
  111. {/* ══ 主区 ══ */}
  112. <main className="min-w-0 flex-1 px-5 pb-[60px] pt-[26px] min-[860px]:px-8">
  113. {/* 配额提醒横幅(默认隐藏,演示开关触发) */}
  114. {bannerOpen && (
  115. <div className="mb-[18px] flex items-center gap-2.5 rounded-xl border border-[rgba(255,194,77,.4)] bg-[rgba(255,194,77,.1)] px-[18px] py-3 text-[13px] text-brand-gold">
  116. <span>⚠️</span>
  117. <span className="flex-1">{t('q_banner_80')}</span>
  118. <button
  119. type="button"
  120. aria-label="Close"
  121. onClick={() => setBannerOpen(false)}
  122. className="text-brand-gold/70 transition-colors hover:text-brand-gold"
  123. >
  124. </button>
  125. </div>
  126. )}
  127. {/* 主区头部:当前标签标题 + 欢迎语 + 创建 Key 按钮 */}
  128. <div className="mb-6 flex flex-wrap items-center justify-between gap-3">
  129. <div>
  130. <h1 className="text-[22px] font-extrabold">{activeLabel}</h1>
  131. <div className="mt-[3px] text-[13px] text-brand-muted">{t('c_welcome')}</div>
  132. </div>
  133. <button
  134. type="button"
  135. onClick={() => setActiveTab('keys')}
  136. className={cls.btnPrimary}
  137. >
  138. {t('c_create_key')}
  139. </button>
  140. </div>
  141. {/* 面板切换 */}
  142. {activeTab === 'overview' && (
  143. <OverviewPanel onToggleBanner={() => setBannerOpen((v) => !v)} />
  144. )}
  145. {activeTab === 'keys' && <KeysPanel />}
  146. {activeTab === 'wallet' && <WalletPanel />}
  147. {activeTab === 'usage' && <UsagePanel />}
  148. {activeTab === 'referral' && <ReferralPanel />}
  149. </main>
  150. </div>
  151. );
  152. }