refactor: migrate state imports from contexts to stores directory

This commit is contained in:
2026-06-09 18:47:04 +08:00
parent f79755f6ec
commit a5d8214650
29 changed files with 72 additions and 50 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
*/ */
import { useState } from "react"; import { useState } from "react";
import { useAuthState } from "@/contexts/auth/auth-context"; import { useAuthState } from "@/stores/auth/auth-context";
import { EmailLoginForm } from "./email-login-form"; import { EmailLoginForm } from "./email-login-form";
import { EmailRegisterForm } from "./email-register-form"; import { EmailRegisterForm } from "./email-register-form";
@@ -10,7 +10,7 @@
*/ */
import { useState } from "react"; import { useState } from "react";
import { useAuthState, useAuthDispatch } from "@/contexts/auth/auth-context"; import { useAuthState, useAuthDispatch } from "@/stores/auth/auth-context";
import { AuthSocialButtons } from "./auth-social-buttons"; import { AuthSocialButtons } from "./auth-social-buttons";
import { AuthDivider } from "./auth-divider"; import { AuthDivider } from "./auth-divider";
+1 -1
View File
@@ -4,7 +4,7 @@
* *
* 原始 Dart: lib/ui/auth/widgets/auth_panel.dart * 原始 Dart: lib/ui/auth/widgets/auth_panel.dart
*/ */
import { useAuthState } from "@/contexts/auth/auth-context"; import { useAuthState } from "@/stores/auth/auth-context";
import { AuthFacebookPanel } from "./auth-facebook-panel"; import { AuthFacebookPanel } from "./auth-facebook-panel";
import { AuthEmailPanel } from "./auth-email-panel"; import { AuthEmailPanel } from "./auth-email-panel";
+3 -3
View File
@@ -13,9 +13,9 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useAuthState, useAuthDispatch } from "@/contexts/auth/auth-context"; import { useAuthState, useAuthDispatch } from "@/stores/auth/auth-context";
import { useChatDispatch } from "@/contexts/chat/chat-context"; import { useChatDispatch } from "@/stores/chat/chat-context";
import { useUserDispatch } from "@/contexts/user/user-context"; import { useUserDispatch } from "@/stores/user/user-context";
import { useAuthGate } from "@/lib/auth/use-auth-gate"; import { useAuthGate } from "@/lib/auth/use-auth-gate";
import { ROUTES } from "@/lib/routes"; import { ROUTES } from "@/lib/routes";
import { MobileShell } from "@/app/_components/core/mobile-shell"; import { MobileShell } from "@/app/_components/core/mobile-shell";
+1 -1
View File
@@ -6,7 +6,7 @@
*/ */
import { useState } from "react"; import { useState } from "react";
import { useAuthDispatch } from "@/contexts/auth/auth-context"; import { useAuthDispatch } from "@/stores/auth/auth-context";
import { import {
validateEmail, validateEmail,
validatePassword, validatePassword,
@@ -6,7 +6,7 @@
*/ */
import { useState } from "react"; import { useState } from "react";
import { useAuthDispatch } from "@/contexts/auth/auth-context"; import { useAuthDispatch } from "@/stores/auth/auth-context";
import { import {
validateConfirmPassword, validateConfirmPassword,
validateEmail, validateEmail,
+2 -2
View File
@@ -4,8 +4,8 @@ import { type FormEvent, useEffect, useRef, useState } from "react";
import { MobileShell } from "@/app/_components/core/mobile-shell"; import { MobileShell } from "@/app/_components/core/mobile-shell";
import { Dialog } from "@/app/_components/core/dialog"; import { Dialog } from "@/app/_components/core/dialog";
import { useChatDispatch, useChatState } from "@/contexts/chat/chat-context"; import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
import { GuestChatQuota } from "@/contexts/chat/chat-types"; import { GuestChatQuota } from "@/stores/chat/chat-types";
import type { UiMessage } from "@/models/chat/ui-message"; import type { UiMessage } from "@/models/chat/ui-message";
import styles from "./chat-screen.module.css"; import styles from "./chat-screen.module.css";
@@ -4,7 +4,7 @@ import { useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { MobileShell } from "@/app/_components/core/mobile-shell"; import { MobileShell } from "@/app/_components/core/mobile-shell";
import { useUserDispatch, useUserState } from "@/contexts/user/user-context"; import { useUserDispatch, useUserState } from "@/stores/user/user-context";
import styles from "./sidebar-screen.module.css"; import styles from "./sidebar-screen.module.css";
+17 -9
View File
@@ -10,18 +10,25 @@
* - 居中布局 (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 { useAuthState, useAuthDispatch } from "@/contexts/auth/auth-context"; import Link from "next/link";
import { useAuthState, useAuthDispatch } from "@/stores/auth/auth-context";
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 {
onSkip: () => void; /** Skip 链接的目标路由(href */
skipHref: string;
onFacebookLogin: () => void; onFacebookLogin: () => void;
} }
export function SplashButton({ export function SplashButton({
onSkip, skipHref,
onFacebookLogin, onFacebookLogin,
}: SplashButtonProps) { }: SplashButtonProps) {
const state = useAuthState(); const state = useAuthState();
@@ -30,15 +37,16 @@ export function SplashButton({
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
{/* Skip 文本按钮 */} {/* Skip 文本链接(next/link 渲染为 <a>,保留 SPA 体验) */}
<button <Link
type="button" href={skipHref}
onClick={onSkip}
className={styles.skip} className={styles.skip}
disabled={isLoading} aria-disabled={isLoading}
tabIndex={isLoading ? -1 : 0}
style={isLoading ? { pointerEvents: "none", opacity: 0.5 } : undefined}
> >
Skip Skip
</button> </Link>
<div className={styles.separator} aria-hidden="true" /> <div className={styles.separator} aria-hidden="true" />
+1 -1
View File
@@ -10,7 +10,7 @@
*/ */
import styles from "./splash-content.module.css"; import styles from "./splash-content.module.css";
const APOSTROPHE = "&#39;"; const APOSTROPHE = "'";
export function SplashContent() { export function SplashContent() {
return ( return (
+24 -22
View File
@@ -1,11 +1,8 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useAuthState } from "@/stores/auth/auth-context";
import { useEffect, useRef } from "react"; import { useChatDispatch } from "@/stores/chat/chat-context";
import { useUserDispatch } from "@/stores/user/user-context";
import { useAuthState } from "@/contexts/auth/auth-context";
import { useChatDispatch } from "@/contexts/chat/chat-context";
import { useUserDispatch } from "@/contexts/user/user-context";
import { useAuthGate } from "@/lib/auth/use-auth-gate"; import { useAuthGate } from "@/lib/auth/use-auth-gate";
import { ROUTES } from "@/lib/routes"; import { ROUTES } from "@/lib/routes";
import { MobileShell } from "@/app/_components/core/mobile-shell"; import { MobileShell } from "@/app/_components/core/mobile-shell";
@@ -17,26 +14,31 @@ 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() {
const router = useRouter(); // useAuthGate 仍消费(仅读取 isAuthed 用于子组件 gating,不触发跳转)
const { isAuthed } = useAuthGate(); useAuthGate();
// 以下 state/dispatch 当前未在本组件消费;保留以便未来按状态驱动的跳转
// 重新启用时再解注释 useEffect 即可。
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const state = useAuthState(); const state = useAuthState();
const wasSuccess = useRef(false); // eslint-disable-next-line @typescript-eslint/no-unused-vars
const chatDispatch = useChatDispatch(); const chatDispatch = useChatDispatch();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const userDispatch = useUserDispatch(); const userDispatch = useUserDispatch();
useEffect(() => { // 已登录时跳 /chat(已注释:改由 SplashButton 的 Link + 路由守卫统一处理)
if (isAuthed) router.replace(ROUTES.chat); // useEffect(() => {
}, [isAuthed, router]); // if (isAuthed) router.replace(ROUTES.chat);
// }, [isAuthed, router]);
// 登录成功跳 /chat // 登录成功跳 /chat(已注释:改由 SplashButton 的 Link + 路由守卫统一处理)
useEffect(() => { // useEffect(() => {
if (state.isSuccess && !wasSuccess.current) { // if (state.isSuccess && !wasSuccess.current) {
wasSuccess.current = true; // wasSuccess.current = true;
chatDispatch({ type: "ChatAuthStatusChanged" }); // chatDispatch({ type: "ChatAuthStatusChanged" });
userDispatch({ type: "UserInit" }); // userDispatch({ type: "UserInit" });
router.replace(ROUTES.chat); // router.replace(ROUTES.chat);
} // }
}, [state.isSuccess, chatDispatch, userDispatch, router]); // }, [state.isSuccess, chatDispatch, userDispatch, router]);
return ( return (
<MobileShell background="var(--color-sidebar-background)"> <MobileShell background="var(--color-sidebar-background)">
@@ -50,7 +52,7 @@ export function SplashScreen() {
<SplashContent /> <SplashContent />
<div className={styles.buttonArea}> <div className={styles.buttonArea}>
<SplashButton <SplashButton
onSkip={() => router.push(ROUTES.chat)} skipHref={ROUTES.chat}
onFacebookLogin={() => { onFacebookLogin={() => {
/* Facebook 登录触发由 SplashButton 内部 dispatch */ /* Facebook 登录触发由 SplashButton 内部 dispatch */
}} }}
+4 -4
View File
@@ -12,10 +12,10 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { AuthProvider } from "@/contexts/auth/auth-context"; import { AuthProvider } from "@/stores/auth/auth-context";
import { ChatProvider } from "@/contexts/chat/chat-context"; import { ChatProvider } from "@/stores/chat/chat-context";
import { SidebarProvider } from "@/contexts/sidebar/sidebar-context"; import { SidebarProvider } from "@/stores/sidebar/sidebar-context";
import { UserProvider } from "@/contexts/user/user-context"; import { UserProvider } from "@/stores/user/user-context";
export interface RootProvidersProps { export interface RootProvidersProps {
children: ReactNode; children: ReactNode;
+14 -2
View File
@@ -11,15 +11,27 @@
*/ */
import { createStorage, type Storage } from "unstorage"; import { createStorage, type Storage } from "unstorage";
import localStorageDriver from "unstorage/drivers/localstorage"; import localStorageDriver from "unstorage/drivers/localstorage";
import memoryDriver from "unstorage/drivers/memory";
import type { ZodType } from "zod"; import type { ZodType } from "zod";
import { Result, type Result as ResultT } from "@/utils/result"; import { Result, type Result as ResultT } from "@/utils/result";
export class SpAsyncUtil { export class SpAsyncUtil {
// ===== 私有静态状态 ===== // ===== 私有静态状态 =====
private static _storage: Storage = createStorage({ // 环境判断 + lazy 初始化:SSR 用 memory driver,浏览器用 localStorage driver。
driver: localStorageDriver({ base: "cozsweet:" }), // unstorage 1.10+ 要求显式传入 localStorage,故这里同步注入 window.localStorage。
private static _storage: Storage = (() => {
if (typeof window === "undefined") {
// SSR 环境:无 window,降级为内存驱动
return createStorage({ driver: memoryDriver() });
}
// 浏览器环境:使用 localStorage 驱动
return createStorage({
driver: localStorageDriver({
base: "cozsweet:",
}),
}); });
})();
// 私有构造函数:禁止实例化(对齐 Dart `SpAsyncUtil._()` // 私有构造函数:禁止实例化(对齐 Dart `SpAsyncUtil._()`
private constructor() {} private constructor() {}