Ver código fonte

fix: console real user session + password visibility + standalone console layout

- ConsoleShell 显示真实登录邮箱/余额(localStorage + /v1/wallet),未登录跳转登录弹窗,退出按钮
- AuthModal 密码输入增加可见切换(👁/🙈,5 处)
- console 页移除官网 SiteHeader,改为自包含应用外壳
DoTouch Dev 2 semanas atrás
pai
commit
1e2b19ac36

+ 2 - 2
.omo/run-continuation/ses_0229f653bffedHZYFR3ILxjlpg.json

@@ -1,10 +1,10 @@
 {
   "sessionID": "ses_0229f653bffedHZYFR3ILxjlpg",
-  "updatedAt": "2026-08-08T17:35:27.903Z",
+  "updatedAt": "2026-08-08T18:03:23.168Z",
   "sources": {
     "background-task": {
       "state": "idle",
-      "updatedAt": "2026-08-08T17:35:27.903Z"
+      "updatedAt": "2026-08-08T18:03:23.168Z"
     }
   }
 }

+ 2 - 4
apps/web/src/app/[locale]/console/page.tsx

@@ -1,9 +1,8 @@
 /**
- * 用户控制台页 - 站点导航 + 控制台外壳(侧边栏/横幅/面板)+ 客服 + 认证弹窗
- * 复用: SiteHeader / SupportFab / AuthModal
+ * 用户控制台页 - 自包含应用外壳(无官网头部导航)+ 客服 + 认证弹窗
+ * 登录会话由 ConsoleShell 管理;未登录自动跳转官网登录弹窗
  */
 import { Suspense } from 'react';
-import { SiteHeader } from '@/components/SiteHeader';
 import { SupportFab } from '@/components/SupportFab';
 import { AuthModal } from '@/components/AuthModal';
 import { ConsoleShell } from '@/components/console/ConsoleShell';
@@ -11,7 +10,6 @@ import { ConsoleShell } from '@/components/console/ConsoleShell';
 export default function ConsolePage() {
   return (
     <div className="flex min-h-screen flex-col">
-      <SiteHeader />
       <ConsoleShell />
       <SupportFab />
       <Suspense fallback={null}>

+ 81 - 5
apps/web/src/components/AuthModal.tsx

@@ -99,6 +99,7 @@ export function AuthModal() {
   const [submitting, setSubmitting] = useState(false);
   const [sentEmail, setSentEmail] = useState<string | null>(null);
   const [countdown, setCountdown] = useState(0);
+  const [showPwd, setShowPwd] = useState(false);
 
   // 查询参数变化时同步视图
   useEffect(() => {
@@ -287,7 +288,22 @@ export function AuthModal() {
             </div>
             <div className="mb-[13px]">
               <label className={labelClass}>{t('password')}</label>
-              <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className={fieldClass} />
+              <div className="relative">
+              <input
+                type={showPwd ? 'text' : 'password'}
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                className={fieldClass + ' pr-10'}
+              />
+              <button
+                type="button"
+                aria-label="Toggle password visibility"
+                onClick={() => setShowPwd((v) => !v)}
+                className="absolute right-3 top-1/2 -translate-y-1/2 text-[15px] text-brand-muted transition-colors hover:text-brand-text"
+              >
+                {showPwd ? '🙈' : '👁'}
+              </button>
+            </div>
             </div>
             <button
               type="button"
@@ -313,11 +329,41 @@ export function AuthModal() {
             </div>
             <div className="mb-[13px]">
               <label className={labelClass}>{t('password')}</label>
-              <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className={fieldClass} />
+              <div className="relative">
+              <input
+                type={showPwd ? 'text' : 'password'}
+                value={password}
+                onChange={(e) => setPassword(e.target.value)}
+                className={fieldClass + ' pr-10'}
+              />
+              <button
+                type="button"
+                aria-label="Toggle password visibility"
+                onClick={() => setShowPwd((v) => !v)}
+                className="absolute right-3 top-1/2 -translate-y-1/2 text-[15px] text-brand-muted transition-colors hover:text-brand-text"
+              >
+                {showPwd ? '🙈' : '👁'}
+              </button>
+            </div>
             </div>
             <div className="mb-[13px]">
               <label className={labelClass}>{t('confirm')}</label>
-              <input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} className={fieldClass} />
+              <div className="relative">
+              <input
+                type={showPwd ? 'text' : 'password'}
+                value={confirm}
+                onChange={(e) => setConfirm(e.target.value)}
+                className={fieldClass + ' pr-10'}
+              />
+              <button
+                type="button"
+                aria-label="Toggle password visibility"
+                onClick={() => setShowPwd((v) => !v)}
+                className="absolute right-3 top-1/2 -translate-y-1/2 text-[15px] text-brand-muted transition-colors hover:text-brand-text"
+              >
+                {showPwd ? '🙈' : '👁'}
+              </button>
+            </div>
             </div>
             {codeBlock}
             <button
@@ -344,11 +390,41 @@ export function AuthModal() {
             {codeBlock}
             <div className="mb-[13px]">
               <label className={labelClass}>{t('new_pwd')}</label>
-              <input type="password" value={newPwd} onChange={(e) => setNewPwd(e.target.value)} className={fieldClass} />
+              <div className="relative">
+              <input
+                type={showPwd ? 'text' : 'password'}
+                value={newPwd}
+                onChange={(e) => setNewPwd(e.target.value)}
+                className={fieldClass + ' pr-10'}
+              />
+              <button
+                type="button"
+                aria-label="Toggle password visibility"
+                onClick={() => setShowPwd((v) => !v)}
+                className="absolute right-3 top-1/2 -translate-y-1/2 text-[15px] text-brand-muted transition-colors hover:text-brand-text"
+              >
+                {showPwd ? '🙈' : '👁'}
+              </button>
+            </div>
             </div>
             <div className="mb-[13px]">
               <label className={labelClass}>{t('confirm_new')}</label>
-              <input type="password" value={confirmNew} onChange={(e) => setConfirmNew(e.target.value)} className={fieldClass} />
+              <div className="relative">
+              <input
+                type={showPwd ? 'text' : 'password'}
+                value={confirmNew}
+                onChange={(e) => setConfirmNew(e.target.value)}
+                className={fieldClass + ' pr-10'}
+              />
+              <button
+                type="button"
+                aria-label="Toggle password visibility"
+                onClick={() => setShowPwd((v) => !v)}
+                className="absolute right-3 top-1/2 -translate-y-1/2 text-[15px] text-brand-muted transition-colors hover:text-brand-text"
+              >
+                {showPwd ? '🙈' : '👁'}
+              </button>
+            </div>
             </div>
             <button
               type="button"

+ 59 - 7
apps/web/src/components/console/ConsoleShell.tsx

@@ -2,9 +2,11 @@
 /**
  * 控制台外壳 - 侧边栏导航(4 标签)+ 配额提醒横幅 + 主区头部 + 面板切换
  * 客户端组件:管理 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 { OverviewPanel } from './OverviewPanel';
 import { KeysPanel } from './KeysPanel';
@@ -12,6 +14,9 @@ import { WalletPanel } from './WalletPanel';
 import { UsagePanel } from './UsagePanel';
 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' },
@@ -24,16 +29,54 @@ 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-[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(桌面端) */}
         <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" />
@@ -58,10 +101,19 @@ export function ConsoleShell() {
           ))}
         </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>
       </aside>
 

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
apps/web/tsconfig.tsbuildinfo


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff