|
@@ -2,9 +2,11 @@
|
|
|
/**
|
|
/**
|
|
|
* 控制台外壳 - 侧边栏导航(4 标签)+ 配额提醒横幅 + 主区头部 + 面板切换
|
|
* 控制台外壳 - 侧边栏导航(4 标签)+ 配额提醒横幅 + 主区头部 + 面板切换
|
|
|
* 客户端组件:管理 activeTab 与 bannerOpen 状态,承载各面板
|
|
* 客户端组件:管理 activeTab 与 bannerOpen 状态,承载各面板
|
|
|
|
|
+ * 会话:读取 localStorage(dotouch_user / dotouch_token),未登录跳转登录弹窗
|
|
|
*/
|
|
*/
|
|
|
-import { useState } from 'react';
|
|
|
|
|
-import { useTranslations } from 'next-intl';
|
|
|
|
|
|
|
+import { useEffect, useState } from 'react';
|
|
|
|
|
+import { useRouter } from 'next/navigation';
|
|
|
|
|
+import { useLocale, useTranslations } from 'next-intl';
|
|
|
import { BrandLogo } from '@/components/BrandLogo';
|
|
import { BrandLogo } from '@/components/BrandLogo';
|
|
|
import { OverviewPanel } from './OverviewPanel';
|
|
import { OverviewPanel } from './OverviewPanel';
|
|
|
import { KeysPanel } from './KeysPanel';
|
|
import { KeysPanel } from './KeysPanel';
|
|
@@ -12,6 +14,9 @@ import { WalletPanel } from './WalletPanel';
|
|
|
import { UsagePanel } from './UsagePanel';
|
|
import { UsagePanel } from './UsagePanel';
|
|
|
import { cls } from './ui';
|
|
import { cls } from './ui';
|
|
|
|
|
|
|
|
|
|
+/** 演示环境 API 地址(生产替换为真实网关域名) */
|
|
|
|
|
+const API_BASE = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:8081/v1';
|
|
|
|
|
+
|
|
|
/** 侧边导航项(图标 + i18n 标签键) */
|
|
/** 侧边导航项(图标 + i18n 标签键) */
|
|
|
const tabs = [
|
|
const tabs = [
|
|
|
{ id: 'overview', icon: '📊', labelKey: 'c_overview' },
|
|
{ id: 'overview', icon: '📊', labelKey: 'c_overview' },
|
|
@@ -24,16 +29,54 @@ type Tab = (typeof tabs)[number]['id'];
|
|
|
|
|
|
|
|
export function ConsoleShell() {
|
|
export function ConsoleShell() {
|
|
|
const t = useTranslations();
|
|
const t = useTranslations();
|
|
|
|
|
+ const locale = useLocale();
|
|
|
|
|
+ const router = useRouter();
|
|
|
const [activeTab, setActiveTab] = useState<Tab>('overview');
|
|
const [activeTab, setActiveTab] = useState<Tab>('overview');
|
|
|
// 配额提醒横幅默认隐藏,由总览面板的演示开关触发
|
|
// 配额提醒横幅默认隐藏,由总览面板的演示开关触发
|
|
|
const [bannerOpen, setBannerOpen] = useState(false);
|
|
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);
|
|
const activeLabel = t(tabs.find((tab) => tab.id === activeTab)!.labelKey);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className="flex w-full flex-1">
|
|
<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-[84px] min-[860px]:h-[calc(100vh-84px)] min-[860px]:w-[236px] min-[860px]:border-b-0 min-[860px]:border-r min-[860px]:border-white/[0.08]">
|
|
|
|
|
|
|
+ <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(桌面端) */}
|
|
{/* 品牌 Logo(桌面端) */}
|
|
|
<div className="hidden border-b border-white/[0.08] px-[10px] pb-[18px] pt-5 min-[860px]:block">
|
|
<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" />
|
|
<BrandLogo idPrefix="console-side" className="h-[34px] w-auto" />
|
|
@@ -58,10 +101,19 @@ export function ConsoleShell() {
|
|
|
))}
|
|
))}
|
|
|
</nav>
|
|
</nav>
|
|
|
|
|
|
|
|
- {/* 用户区:邮箱 + 余额(桌面端) */}
|
|
|
|
|
- <div className="hidden border-t border-white/[0.08] px-3 pb-1.5 pt-3.5 min-[860px]:block">
|
|
|
|
|
- <div className="break-all text-[12.5px] text-brand-muted">nguyen.anh@example.com</div>
|
|
|
|
|
- <div className="mt-1 text-base font-extrabold">$23.50</div>
|
|
|
|
|
|
|
+ {/* 用户区:真实登录邮箱 + 余额 + 退出(桌面端) */}
|
|
|
|
|
+ <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>
|
|
</div>
|
|
|
</aside>
|
|
</aside>
|
|
|
|
|
|