"use client"; /** * ChatScreen 编排层 * * 原始 Dart: lib/ui/chat/chat_screen.dart(172 行) * * 职责(本文件): * - **订阅 auth 状态机**(loginStatus)—— 派生 isGuest / **派鉴权生命周期事件**给 chat 机器 * - 屏幕生命周期事件(init / visible / invisible) * - 配额告警监听(**仅**游客 —— "非游客无消息限制"业务事实) * - **不**直接管理 WebSocket / **不**直接读 AuthStorage(除 token 取值) * * 子组件职责: * - ChatHeader:顶部栏(游客 banner / 菜单按钮) * - ChatArea:消息列表 * - ChatInputBar:输入栏(文字 + 发送 + More) * - QuotaDialog:配额告警弹窗(**仅**游客) * - PwaInstallOverlay:PWA 安装提示触发器 * - BrowserHintOverlay:浏览器提示 * - CharacterIntro:角色介绍卡片(首屏) * * **鉴权解耦**(**事件驱动**): * - chat 机器**不**感知鉴权 / **不**管 WebSocket —— 由 chat-screen 派生 loginStatus * - chat-screen **只**派 3 个事件: * - `ChatGuestLogin` → 游客会话(**断** WS = 不连) * - `ChatNonGuestLogin { token }` → 非游客会话(**连** WS) * - `ChatLogout` → 登出(机器自动 cleanup WS actor) * - chat 机器**内部**用 fromCallback actor 完整管理 WS 生命周期 */ import { useEffect, useRef, useState } from "react"; import Image from "next/image"; import { useAuthState } from "@/stores/auth/auth-context"; import type { LoginStatus } from "@/models/auth/login-status"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { useChatDispatch, useChatState } from "@/stores/chat/chat-context"; import { GuestChatQuota } from "@/stores/chat"; import { MobileShell } from "@/app/_components/core/mobile-shell"; import { BrowserHintOverlay } from "./browser-hint-overlay"; import { ChatArea } from "./chat-area"; import { ChatHeader } from "./chat-header"; import { ChatInputBar } from "./chat-input-bar"; import { PwaInstallOverlay } from "./pwa-install-overlay"; import { QuotaDialog } from "./quota-dialog"; import styles from "./chat-screen.module.css"; /** * 派生 isGuest —— 单一来源:`auth.loginStatus` */ function deriveIsGuest(loginStatus: LoginStatus): boolean { return loginStatus === "guest"; } /** * 按 loginStatus 派鉴权生命周期事件(chat 机器**不**感知 loginStatus —— 这层转换在此) * * 设计:**不**直接管理 WS / **不**读 AuthStorage(除取 token) */ function dispatchAuthLifecycle( loginStatus: LoginStatus, chatDispatch: ReturnType, ): void { if (loginStatus === "guest") { // 游客:派 ChatGuestLogin(chat 机器**不**连 WS —— 业务事实"断 WS") chatDispatch({ type: "ChatGuestLogin" }); return; } if (loginStatus === "notLoggedIn") { // 登出:派 ChatLogout(chat 机器自动 cleanup WS actor + 清消息) chatDispatch({ type: "ChatLogout" }); return; } // 非游客:拿 loginToken → 派 ChatNonGuestLogin { token } void (async () => { const tokenR = await AuthStorage.getInstance().getLoginToken(); if (tokenR.success && tokenR.data) { chatDispatch({ type: "ChatNonGuestLogin", token: tokenR.data }); } })(); } export function ChatScreen() { const state = useChatState(); const authState = useAuthState(); const chatDispatch = useChatDispatch(); // isGuest 派生(chat-screen 是**唯一**知道这个值的地方 —— chat 机器无 isGuest 字段) const isGuest = deriveIsGuest(authState.loginStatus); const [quotaDialogOpen, setQuotaDialogOpen] = useState(false); const [quotaExhausted, setQuotaExhausted] = useState(false); // ───────────────────────────────────────────────────────────── // 屏幕生命周期 + 派初次鉴权生命周期事件 // ───────────────────────────────────────────────────────────── useEffect(() => { chatDispatch({ type: "ChatScreenVisible" }); dispatchAuthLifecycle(authState.loginStatus, chatDispatch); return () => { chatDispatch({ type: "ChatScreenInvisible" }); }; // 注:mount useEffect 故意**不**依赖 loginStatus —— 首次 mount 时机是固定的 // ("用户进 /chat 那一刻"),loginStatus 由 splash/auth-screen 提前 settle。 // loginStatus 后续变化由下方"副作用 ②" 统一处理。 // eslint-disable-next-line react-hooks/exhaustive-deps }, [chatDispatch]); // ───────────────────────────────────────────────────────────── // 副作用 ②:loginStatus 变化 → 派对应鉴权生命周期事件 // // 规则: // - 首次 mount(prev === null)→ 由 mount useEffect 统一处理 // - "notLoggedIn" → 派 ChatLogout // - 登录态变化(guest→email 等)→ 派 ChatGuestLogin 或 ChatNonGuestLogin // ───────────────────────────────────────────────────────────── const prevLoginStatusRef = useRef(null); useEffect(() => { const prev = prevLoginStatusRef.current; prevLoginStatusRef.current = authState.loginStatus; if (prev === null) return; // 首次 mount 由 mount useEffect 处理 if (prev === authState.loginStatus) return; // 登录态**没**变 dispatchAuthLifecycle(authState.loginStatus, chatDispatch); }, [authState.loginStatus, chatDispatch]); // ───────────────────────────────────────────────────────────── // 配额耗尽触发器(**仅**游客 —— "非游客无消息限制") // ───────────────────────────────────────────────────────────── const prevTriggerRef = useRef(state.quotaExceededTrigger); useEffect(() => { if (!isGuest) return; // ← 防御:非游客 backend 不会发 ChatQuotaExceeded if ( state.quotaExceededTrigger !== prevTriggerRef.current && state.quotaExceededTrigger > 0 ) { setQuotaExhausted(true); setQuotaDialogOpen(true); } prevTriggerRef.current = state.quotaExceededTrigger; }, [state.quotaExceededTrigger, isGuest]); // 配额警告(剩余 = 5)—— **仅**游客 const prevRemainingRef = useRef(state.guestRemainingQuota); useEffect(() => { if ( isGuest && // ← 派生自 auth.loginStatus state.guestRemainingQuota === GuestChatQuota.warningThreshold && prevRemainingRef.current !== state.guestRemainingQuota ) { setQuotaExhausted(false); setQuotaDialogOpen(true); } prevRemainingRef.current = state.guestRemainingQuota; }, [state.guestRemainingQuota, isGuest]); return (
{/* 背景图(与原 Dart MobileLayout 一致) */}
{/* isGuest 派生自 auth.loginStatus */}
{/* 浏览器提示(应用内浏览器检测) */} {/* PWA 安装提示触发器(无可见 UI,3.5s 后弹 dialog) */} {/* 配额告警弹窗(**仅**游客,gate 在 isGuest) */} setQuotaDialogOpen(false)} isExhausted={quotaExhausted} />
); }