| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- 'use client';
- /**
- * 控制台外壳 - 侧边栏导航(4 标签)+ 配额提醒横幅 + 主区头部 + 面板切换
- * 客户端组件:管理 activeTab 与 bannerOpen 状态,承载各面板
- * 会话:读取 localStorage(dotouch_user / dotouch_token),未登录跳转登录弹窗
- */
- import { useEffect, useState } from 'react';
- import { useRouter } from 'next/navigation';
- import { useLocale, useTranslations } from 'next-intl';
- import { BrandLogo } from '@/components/BrandLogo';
- import { OverviewPanel } from './OverviewPanel';
- import { KeysPanel } from './KeysPanel';
- import { WalletPanel } from './WalletPanel';
- import { UsagePanel } from './UsagePanel';
- import { ReferralPanel } from './ReferralPanel';
- import { cls } from './ui';
- /** 演示环境 API 地址(生产替换为真实网关域名) */
- const API_BASE = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:8081/v1';
- /** 侧边导航项(图标 + i18n 标签键) */
- const tabs = [
- { id: 'overview', icon: '📊', labelKey: 'c_overview' },
- { id: 'keys', icon: '🔑', labelKey: 'c_keys' },
- { id: 'wallet', icon: '💳', labelKey: 'c_wallet' },
- { id: 'usage', icon: '📈', labelKey: 'c_usage' },
- { id: 'referral', icon: '🤝', labelKey: 'c_referral' },
- ] as const;
- type Tab = (typeof tabs)[number]['id'];
- export function ConsoleShell() {
- const t = useTranslations();
- const locale = useLocale();
- const router = useRouter();
- const [activeTab, setActiveTab] = useState<Tab>('overview');
- // 配额提醒横幅默认隐藏,由总览面板的演示开关触发
- const [bannerOpen, setBannerOpen] = useState(false);
- const [userEmail, setUserEmail] = useState<string | null>(null);
- const [balanceCents, setBalanceCents] = useState<number | null>(null);
- // 会话检查:读取本地登录态,未登录跳转官网登录弹窗
- useEffect(() => {
- let email: string | null = null;
- let token: string | null = null;
- try {
- email = localStorage.getItem('dotouch_user');
- token = localStorage.getItem('dotouch_token');
- } catch { /* 忽略 */ }
- if (!email || !token) {
- router.replace(`/${locale}?auth=login`);
- return;
- }
- setUserEmail(email);
- // 拉取真实余额
- fetch(`${API_BASE}/wallet`, {
- headers: { Authorization: `Bearer ${token}` },
- })
- .then((r) => r.json().catch(() => null))
- .then((data) => {
- if (data && typeof data.balanceCents === 'number') {
- setBalanceCents(data.balanceCents);
- }
- })
- .catch(() => { /* 余额获取失败不阻塞页面 */ });
- }, [locale, router]);
- const logout = () => {
- try {
- localStorage.removeItem('dotouch_token');
- localStorage.removeItem('dotouch_user');
- } catch { /* 忽略 */ }
- router.push(`/${locale}`);
- };
- const activeLabel = t(tabs.find((tab) => tab.id === activeTab)!.labelKey);
- return (
- <div className="flex w-full flex-1">
- {/* ══ 侧边栏 ══ */}
- <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]">
- {/* 品牌 Logo(桌面端) */}
- <div className="hidden border-b border-white/[0.08] px-[10px] pb-[18px] pt-5 min-[860px]:block">
- <BrandLogo idPrefix="console-side" className="h-[34px] w-auto" />
- </div>
- {/* 导航:移动端横向 / 桌面端纵向 */}
- <nav className="flex flex-1 flex-wrap gap-1 p-3.5 min-[860px]:flex-col min-[860px]:flex-nowrap">
- {tabs.map((tab) => (
- <button
- key={tab.id}
- type="button"
- onClick={() => setActiveTab(tab.id)}
- className={`flex items-center gap-2.5 rounded-[10px] px-3 py-[11px] text-sm transition-colors ${
- activeTab === tab.id
- ? 'bg-[rgba(124,92,255,.15)] font-semibold text-brand-text'
- : 'text-brand-muted hover:bg-[rgba(124,92,255,.08)] hover:text-brand-text'
- }`}
- >
- <span className="w-5 text-center">{tab.icon}</span>
- <span>{t(tab.labelKey)}</span>
- </button>
- ))}
- </nav>
- {/* 用户区:真实登录邮箱 + 余额 + 退出(桌面端) */}
- <div className="hidden border-t border-white/[0.08] px-3 pb-3 pt-3.5 min-[860px]:block">
- <div className="break-all text-[12.5px] text-brand-muted">{userEmail ?? '…'}</div>
- <div className="mt-1 text-base font-extrabold">
- {balanceCents !== null ? `$${(balanceCents / 100).toFixed(2)}` : '$—'}
- </div>
- <button
- type="button"
- onClick={logout}
- 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]"
- >
- {t('c_logout') ?? '退出登录'}
- </button>
- </div>
- </aside>
- {/* ══ 主区 ══ */}
- <main className="min-w-0 flex-1 px-5 pb-[60px] pt-[26px] min-[860px]:px-8">
- {/* 配额提醒横幅(默认隐藏,演示开关触发) */}
- {bannerOpen && (
- <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">
- <span>⚠️</span>
- <span className="flex-1">{t('q_banner_80')}</span>
- <button
- type="button"
- aria-label="Close"
- onClick={() => setBannerOpen(false)}
- className="text-brand-gold/70 transition-colors hover:text-brand-gold"
- >
- ✕
- </button>
- </div>
- )}
- {/* 主区头部:当前标签标题 + 欢迎语 + 创建 Key 按钮 */}
- <div className="mb-6 flex flex-wrap items-center justify-between gap-3">
- <div>
- <h1 className="text-[22px] font-extrabold">{activeLabel}</h1>
- <div className="mt-[3px] text-[13px] text-brand-muted">{t('c_welcome')}</div>
- </div>
- <button
- type="button"
- onClick={() => setActiveTab('keys')}
- className={cls.btnPrimary}
- >
- {t('c_create_key')}
- </button>
- </div>
- {/* 面板切换 */}
- {activeTab === 'overview' && (
- <OverviewPanel onToggleBanner={() => setBannerOpen((v) => !v)} />
- )}
- {activeTab === 'keys' && <KeysPanel />}
- {activeTab === 'wallet' && <WalletPanel />}
- {activeTab === 'usage' && <UsagePanel />}
- {activeTab === 'referral' && <ReferralPanel />}
- </main>
- </div>
- );
- }
|