refactor(splash): encapsulate auth logic in SplashButton for SSR support
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
|
||||||
/**
|
/**
|
||||||
* 500px 移动端 Shell
|
* 500px 移动端 Shell(Server Component)
|
||||||
*
|
*
|
||||||
* 原始 Dart: 每个屏幕的 `Scaffold → SafeArea → Center → ConstrainedBox(maxWidth: 500)` 模式
|
* 原始 Dart: 每个屏幕的 `Scaffold → SafeArea → Center → ConstrainedBox(maxWidth: 500)` 模式
|
||||||
* (`Breakpoints.mobileMaxWidth = 500.0`)。
|
* (`Breakpoints.mobileMaxWidth = 500.0`)。
|
||||||
*
|
*
|
||||||
* 集中封装,调用方仅需 `<MobileShell>{...}</MobileShell>` 即可获得统一的移动端宽度约束。
|
* 集中封装,调用方仅需 `<MobileShell>{...}</MobileShell>` 即可获得统一的移动端宽度约束。
|
||||||
|
*
|
||||||
|
* 本组件无 hooks,可作为 Server Component 直接 SSR。
|
||||||
*/
|
*/
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -5,36 +5,63 @@
|
|||||||
* 原始 Dart: lib/ui/splash/widgets/splash_button.dart
|
* 原始 Dart: lib/ui/splash/widgets/splash_button.dart
|
||||||
* `Row(children: [Skip 文本, SizedBox 26, Facebook 登录按钮])`
|
* `Row(children: [Skip 文本, SizedBox 26, Facebook 登录按钮])`
|
||||||
*
|
*
|
||||||
* 关键对齐点:
|
* 本组件是 splash 鉴权流程的**自治单元**:
|
||||||
|
* - 直接消费 `useAuthGate` / `useAuthState` / `useAuthDispatch`
|
||||||
|
* - 直接消费 `useChatDispatch` / `useUserDispatch`(登录成功后初始化)
|
||||||
|
* - 自管路由跳转(已登录 / 登录成功 → /chat)
|
||||||
|
* - Skip 用 `<Link>` 保留 SPA 体验
|
||||||
|
*
|
||||||
|
* 设计要点:
|
||||||
* - 仅两个元素:Skip 文本 + Facebook 登录按钮
|
* - 仅两个元素:Skip 文本 + Facebook 登录按钮
|
||||||
* - 居中布局 (MainAxisAlignment.center)
|
* - 居中布局 (MainAxisAlignment.center)
|
||||||
* - Facebook 按钮:带渐变(primaryGradient)+ 圆角 24 + 高度 48
|
* - Facebook 按钮:带渐变(primaryGradient)+ 圆角 24 + 高度 48
|
||||||
* - 加载中:按钮内显示 spinner
|
* - 加载中:按钮内显示 spinner
|
||||||
*
|
|
||||||
* Skip 按钮使用 `<Link>` 而非 `<button onClick>`:
|
|
||||||
* - 保留 Next.js SPA 体验 + prefetch
|
|
||||||
* - 避免编程式 `router.push`(消除水合后副作用)
|
|
||||||
* - 右键"在新标签页打开"等浏览器行为自然支持
|
|
||||||
*/
|
*/
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { useAuthState, useAuthDispatch } from "@/stores/auth/auth-context";
|
import { useAuthState, useAuthDispatch } from "@/stores/auth/auth-context";
|
||||||
|
import { useChatDispatch } from "@/stores/chat/chat-context";
|
||||||
|
import { useUserDispatch } from "@/stores/user/user-context";
|
||||||
|
import { useAuthGate } from "@/lib/auth/use-auth-gate";
|
||||||
|
import { ROUTES } from "@/lib/routes";
|
||||||
import { LoadingIndicator } from "@/app/_components/core/loading-indicator";
|
import { LoadingIndicator } from "@/app/_components/core/loading-indicator";
|
||||||
import styles from "./splash-button.module.css";
|
import styles from "./splash-button.module.css";
|
||||||
|
|
||||||
export interface SplashButtonProps {
|
export interface SplashButtonProps {
|
||||||
/** Skip 链接的目标路由(href) */
|
/** Skip 链接的目标路由(默认 /chat) */
|
||||||
skipHref: string;
|
skipHref?: string;
|
||||||
onFacebookLogin: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SplashButton({
|
export function SplashButton({ skipHref = ROUTES.chat }: SplashButtonProps) {
|
||||||
skipHref,
|
// ===== 鉴权状态 =====
|
||||||
onFacebookLogin,
|
const { isAuthed } = useAuthGate();
|
||||||
}: SplashButtonProps) {
|
|
||||||
const state = useAuthState();
|
const state = useAuthState();
|
||||||
const dispatch = useAuthDispatch();
|
const authDispatch = useAuthDispatch();
|
||||||
const isLoading = state.isLoading;
|
const isLoading = state.isLoading;
|
||||||
|
|
||||||
|
// ===== 跨 store 初始化(登录成功后触发) =====
|
||||||
|
const chatDispatch = useChatDispatch();
|
||||||
|
const userDispatch = useUserDispatch();
|
||||||
|
const router = useRouter();
|
||||||
|
const wasSuccess = useRef(false);
|
||||||
|
|
||||||
|
// 已登录时跳 /chat
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthed) router.replace(ROUTES.chat);
|
||||||
|
}, [isAuthed, router]);
|
||||||
|
|
||||||
|
// 登录成功跳 /chat + 通知 chat/user 初始化
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.isSuccess && !wasSuccess.current) {
|
||||||
|
wasSuccess.current = true;
|
||||||
|
chatDispatch({ type: "ChatAuthStatusChanged" });
|
||||||
|
userDispatch({ type: "UserInit" });
|
||||||
|
router.replace(ROUTES.chat);
|
||||||
|
}
|
||||||
|
}, [state.isSuccess, chatDispatch, userDispatch, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{/* Skip 文本链接(next/link 渲染为 <a>,保留 SPA 体验) */}
|
{/* Skip 文本链接(next/link 渲染为 <a>,保留 SPA 体验) */}
|
||||||
@@ -54,8 +81,7 @@ export function SplashButton({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({ type: "AuthFacebookLoginSubmitted" });
|
authDispatch({ type: "AuthFacebookLoginSubmitted" });
|
||||||
onFacebookLogin();
|
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className={styles.facebookButton}
|
className={styles.facebookButton}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"use client";
|
/**
|
||||||
|
* Splash 屏幕(纯布局 / Server Component)
|
||||||
|
*
|
||||||
|
* 鉴权流程逻辑已封装到 `<SplashButton>` 内部(client 组件):
|
||||||
|
* - 已登录检测 / 登录成功跳转 / Facebook 登录 dispatch
|
||||||
|
* - 跨 store 初始化(ChatAuthStatusChanged / UserInit)
|
||||||
|
*
|
||||||
|
* 本组件职责:纯 JSX 布局,无 hooks → 可作为 Server Component 直接 SSR。
|
||||||
|
*/
|
||||||
|
|
||||||
import { useAuthState } from "@/stores/auth/auth-context";
|
|
||||||
import { useChatDispatch } from "@/stores/chat/chat-context";
|
|
||||||
import { useUserDispatch } from "@/stores/user/user-context";
|
|
||||||
import { useAuthGate } from "@/lib/auth/use-auth-gate";
|
|
||||||
import { ROUTES } from "@/lib/routes";
|
|
||||||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
||||||
|
|
||||||
import { SplashBackground } from "./splash-background";
|
import { SplashBackground } from "./splash-background";
|
||||||
@@ -14,32 +17,6 @@ import { SplashButton } from "./splash-button";
|
|||||||
import styles from "./splash-screen.module.css";
|
import styles from "./splash-screen.module.css";
|
||||||
|
|
||||||
export function SplashScreen() {
|
export function SplashScreen() {
|
||||||
// useAuthGate 仍消费(仅读取 isAuthed 用于子组件 gating,不触发跳转)
|
|
||||||
useAuthGate();
|
|
||||||
// 以下 state/dispatch 当前未在本组件消费;保留以便未来按状态驱动的跳转
|
|
||||||
// 重新启用时再解注释 useEffect 即可。
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const state = useAuthState();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const chatDispatch = useChatDispatch();
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const userDispatch = useUserDispatch();
|
|
||||||
|
|
||||||
// 已登录时跳 /chat(已注释:改由 SplashButton 的 Link + 路由守卫统一处理)
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (isAuthed) router.replace(ROUTES.chat);
|
|
||||||
// }, [isAuthed, router]);
|
|
||||||
|
|
||||||
// 登录成功跳 /chat(已注释:改由 SplashButton 的 Link + 路由守卫统一处理)
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (state.isSuccess && !wasSuccess.current) {
|
|
||||||
// wasSuccess.current = true;
|
|
||||||
// chatDispatch({ type: "ChatAuthStatusChanged" });
|
|
||||||
// userDispatch({ type: "UserInit" });
|
|
||||||
// router.replace(ROUTES.chat);
|
|
||||||
// }
|
|
||||||
// }, [state.isSuccess, chatDispatch, userDispatch, router]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell background="var(--color-sidebar-background)">
|
<MobileShell background="var(--color-sidebar-background)">
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
@@ -51,12 +28,7 @@ export function SplashScreen() {
|
|||||||
<div className={styles.spacer} />
|
<div className={styles.spacer} />
|
||||||
<SplashContent />
|
<SplashContent />
|
||||||
<div className={styles.buttonArea}>
|
<div className={styles.buttonArea}>
|
||||||
<SplashButton
|
<SplashButton />
|
||||||
skipHref={ROUTES.chat}
|
|
||||||
onFacebookLogin={() => {
|
|
||||||
/* Facebook 登录触发由 SplashButton 内部 dispatch */
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<p className={styles.bottom}>
|
<p className={styles.bottom}>
|
||||||
Elio Silvestri, Your exclusive AI boyfriend
|
Elio Silvestri, Your exclusive AI boyfriend
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { SplashScreen } from "@/app/splash/components/splash-screen";
|
import { SplashScreen } from "@/app/splash/components/splash-screen";
|
||||||
|
|
||||||
export default function SplashPage() {
|
export default function SplashPage() {
|
||||||
|
|||||||
Reference in New Issue
Block a user