From 744e23fc29b4ed3d1a776db2f8f9030c1d1054cb Mon Sep 17 00:00:00 2001 From: kanban Date: Wed, 10 Jun 2026 18:21:28 +0800 Subject: [PATCH] refactor(stores): split state and event unions into separate files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract `Context` interface + `initialContext` const into `-context.ts` and the event union into `-events.ts` for all four state machines (auth, chat, sidebar, user) under src/stores/. - `*-machine.ts` keeps helpers, actors, actions, and the `setup().createMachine()` body; re-exports the types/initial value to preserve its public API. - `*-types.ts` re-exports from the new files for backward compatibility (sidebar keeps enum re-exports, chat keeps GuestChatQuota). - `index.ts` barrels updated to re-export the new files. - Removed unused model imports (AuthMode, AuthPanelMode) from auth-machine.ts; kept LoginType, UiMessage, UserView where still used by actors/helpers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/stores/auth/auth-context.ts | 30 ++++++++++++++ src/stores/auth/auth-events.ts | 27 +++++++++++++ src/stores/auth/auth-machine.ts | 56 +++------------------------ src/stores/auth/auth-types.ts | 5 ++- src/stores/auth/index.ts | 9 ++++- src/stores/chat/chat-context.ts | 30 ++++++++++++++ src/stores/chat/chat-events.ts | 23 +++++++++++ src/stores/chat/chat-machine.ts | 53 +++---------------------- src/stores/chat/chat-types.ts | 7 ++-- src/stores/chat/index.ts | 3 ++ src/stores/sidebar/index.ts | 3 ++ src/stores/sidebar/sidebar-context.ts | 20 ++++++++++ src/stores/sidebar/sidebar-events.ts | 13 +++++++ src/stores/sidebar/sidebar-machine.ts | 35 ++++------------- src/stores/sidebar/sidebar-types.ts | 12 +++--- src/stores/user/index.ts | 3 ++ src/stores/user/user-context.ts | 20 ++++++++++ src/stores/user/user-events.ts | 14 +++++++ src/stores/user/user-machine.ts | 36 +++-------------- src/stores/user/user-types.ts | 5 ++- 20 files changed, 233 insertions(+), 171 deletions(-) create mode 100644 src/stores/auth/auth-context.ts create mode 100644 src/stores/auth/auth-events.ts create mode 100644 src/stores/chat/chat-context.ts create mode 100644 src/stores/chat/chat-events.ts create mode 100644 src/stores/sidebar/sidebar-context.ts create mode 100644 src/stores/sidebar/sidebar-events.ts create mode 100644 src/stores/user/user-context.ts create mode 100644 src/stores/user/user-events.ts diff --git a/src/stores/auth/auth-context.ts b/src/stores/auth/auth-context.ts new file mode 100644 index 00000000..4f25bbef --- /dev/null +++ b/src/stores/auth/auth-context.ts @@ -0,0 +1,30 @@ +/** + * Auth 状态机:Context 形状 + 初始值 + */ +import type { AuthMode } from "@/models/auth/auth-mode"; +import type { AuthPanelMode } from "@/models/auth/auth-panel-mode"; +import type { LoginType } from "@/models/auth/login-type"; + +export interface AuthContext { + /** 当前面板模式(Facebook / Email) */ + authPanelMode: AuthPanelMode; + /** 内部登录模式(login / register) */ + authMode: AuthMode; + email: string; + password: string; + username: string; + confirmPassword: string; + errorMessage: string | null; + loginType: LoginType; +} + +export const initialContext: AuthContext = { + authPanelMode: "facebook", + authMode: "login", + email: "", + password: "", + username: "", + confirmPassword: "", + errorMessage: null, + loginType: "none", +}; diff --git a/src/stores/auth/auth-events.ts b/src/stores/auth/auth-events.ts new file mode 100644 index 00000000..676869ef --- /dev/null +++ b/src/stores/auth/auth-events.ts @@ -0,0 +1,27 @@ +/** + * Auth 状态机:事件联合 + * + * 事件名与原 Dart AuthEvent 对齐。 + */ +import type { AuthMode } from "@/models/auth/auth-mode"; +import type { AuthPanelMode } from "@/models/auth/auth-panel-mode"; + +export type AuthEvent = + // 内部 UI 事件 + | { type: "AuthPanelModeChanged"; mode: AuthPanelMode } + | { type: "AuthModeChanged"; mode: AuthMode } + | { type: "AuthFormCleared" } + | { type: "AuthReset" } + // 业务事件(提交) + | { type: "AuthEmailLoginSubmitted"; email: string; password: string } + | { + type: "AuthEmailRegisterSubmitted"; + email: string; + password: string; + username: string; + confirmPassword: string; + } + // 社交登录(已迁移到 NextAuth)— 业务层直接 await nextauth.googleLogin() 即可 + | { type: "AuthGoogleLoginSubmitted" } + | { type: "AuthFacebookLoginSubmitted" } + | { type: "AuthAppleLoginSubmitted" }; diff --git a/src/stores/auth/auth-machine.ts b/src/stores/auth/auth-machine.ts index cfc5b55a..f692a0b6 100644 --- a/src/stores/auth/auth-machine.ts +++ b/src/stores/auth/auth-machine.ts @@ -9,62 +9,18 @@ */ import { setup, fromPromise, assign } from "xstate"; -import type { AuthMode } from "@/models/auth/auth-mode"; -import type { AuthPanelMode } from "@/models/auth/auth-panel-mode"; import type { LoginType } from "@/models/auth/login-type"; import { authRepository } from "@/data/repositories/auth_repository"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { Result } from "@/utils/result"; -// ============================================================ -// Context -// ============================================================ -export interface AuthContext { - /** 当前面板模式(Facebook / Email) */ - authPanelMode: AuthPanelMode; - /** 内部登录模式(login / register) */ - authMode: AuthMode; - email: string; - password: string; - username: string; - confirmPassword: string; - errorMessage: string | null; - loginType: LoginType; -} +import { AuthContext, initialContext } from "./auth-context"; +import type { AuthEvent } from "./auth-events"; -const initialContext: AuthContext = { - authPanelMode: "facebook", - authMode: "login", - email: "", - password: "", - username: "", - confirmPassword: "", - errorMessage: null, - loginType: "none", -}; - -// ============================================================ -// Events(事件类型名与原 Dart AuthEvent 对齐) -// ============================================================ -export type AuthEvent = - // 内部 UI 事件 - | { type: "AuthPanelModeChanged"; mode: AuthPanelMode } - | { type: "AuthModeChanged"; mode: AuthMode } - | { type: "AuthFormCleared" } - | { type: "AuthReset" } - // 业务事件(提交) - | { type: "AuthEmailLoginSubmitted"; email: string; password: string } - | { - type: "AuthEmailRegisterSubmitted"; - email: string; - password: string; - username: string; - confirmPassword: string; - } - // 社交登录(已迁移到 NextAuth)— 业务层走 GoogleLogin / FacebookLogin 类 - | { type: "AuthGoogleLoginSubmitted" } - | { type: "AuthFacebookLoginSubmitted" } - | { type: "AuthAppleLoginSubmitted" }; +// 重新导出 Context / Event 类型,保持 machine 文件的公共 API 不变 +export type { AuthContext } from "./auth-context"; +export { initialContext } from "./auth-context"; +export type { AuthEvent } from "./auth-events"; // ============================================================ // Helpers diff --git a/src/stores/auth/auth-types.ts b/src/stores/auth/auth-types.ts index 245f5515..998f3b13 100644 --- a/src/stores/auth/auth-types.ts +++ b/src/stores/auth/auth-types.ts @@ -3,7 +3,8 @@ * * 原始文件定义了 AuthState 接口、AuthEvent 联合、initialAuthState。 * 状态机重构后: - * - `AuthEvent` 联合由 `auth-machine.ts` 重新导出(保留类型) + * - `AuthEvent` 联合由 `auth-events.ts` 导出 + * - Context 形状与初始值由 `auth-context.ts` 导出 * - `AuthState` 由 `auth-context.tsx` 导出(保持原 API 兼容) */ -export type { AuthEvent } from "./auth-machine"; +export type { AuthEvent } from "./auth-events"; diff --git a/src/stores/auth/index.ts b/src/stores/auth/index.ts index e8035840..5dbd5147 100644 --- a/src/stores/auth/index.ts +++ b/src/stores/auth/index.ts @@ -1,7 +1,12 @@ /** - * @file Automatically generated by barrelsby. + * @file Auth 公共导出 + * + * 状态机重构后: + * - Context 形状与初始值位于 auth-context.ts + * - 事件联合位于 auth-events.ts + * - machine / 类型 / React 入口分别由对应文件导出 */ - export * from "./auth-context"; +export * from "./auth-events"; export * from "./auth-machine"; export * from "./auth-types"; diff --git a/src/stores/chat/chat-context.ts b/src/stores/chat/chat-context.ts new file mode 100644 index 00000000..ba882afd --- /dev/null +++ b/src/stores/chat/chat-context.ts @@ -0,0 +1,30 @@ +/** + * Chat 状态机:Context 形状 + 初始值 + * + * 注:GuestChatQuota 仍位于 chat-machine.ts(属于业务常量,与上下文配对紧密)。 + */ +import type { UiMessage } from "@/models/chat/ui-message"; + +export interface ChatContext { + messages: UiMessage[]; + isReplyingAI: boolean; + isGuest: boolean; + guestRemainingQuota: number; + guestTotalQuota: number; + quotaExceededTrigger: number; + isLoadingMore: boolean; + hasMore: boolean; + historyOffset: number; +} + +export const initialContext: ChatContext = { + messages: [], + isReplyingAI: false, + isGuest: true, + guestRemainingQuota: 40, + guestTotalQuota: 50, + quotaExceededTrigger: 0, + isLoadingMore: false, + hasMore: true, + historyOffset: 0, +}; diff --git a/src/stores/chat/chat-events.ts b/src/stores/chat/chat-events.ts new file mode 100644 index 00000000..3cc507d2 --- /dev/null +++ b/src/stores/chat/chat-events.ts @@ -0,0 +1,23 @@ +/** + * Chat 状态机:事件联合 + * + * 事件名与原 Dart ChatEvent 对齐。 + */ +export type ChatEvent = + | { type: "ChatInit" } + | { type: "ChatScreenVisible" } + | { type: "ChatScreenInvisible" } + | { type: "ChatSendMessage"; content: string } + | { type: "ChatSendImage"; imageBase64: string } + | { type: "ChatLoadMoreHistory" } + | { + type: "ChatAISentenceReceived"; + index: number; + text: string; + total: number; + done: boolean; + } + | { type: "ChatWebSocketError"; errorMessage: string } + | { type: "ChatWebSocketConnected"; userId: string } + | { type: "ChatQuotaExceeded" } + | { type: "ChatAuthStatusChanged" }; diff --git a/src/stores/chat/chat-machine.ts b/src/stores/chat/chat-machine.ts index b41a4ac5..4aef4435 100644 --- a/src/stores/chat/chat-machine.ts +++ b/src/stores/chat/chat-machine.ts @@ -23,32 +23,8 @@ import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { formatDate } from "@/utils/date"; import { Result } from "@/utils/result"; -// ============================================================ -// Context -// ============================================================ -export interface ChatContext { - messages: UiMessage[]; - isReplyingAI: boolean; - isGuest: boolean; - guestRemainingQuota: number; - guestTotalQuota: number; - quotaExceededTrigger: number; - isLoadingMore: boolean; - hasMore: boolean; - historyOffset: number; -} - -const initialContext: ChatContext = { - messages: [], - isReplyingAI: false, - isGuest: true, - guestRemainingQuota: 40, - guestTotalQuota: 50, - quotaExceededTrigger: 0, - isLoadingMore: false, - hasMore: true, - historyOffset: 0, -}; +import { ChatContext, initialContext } from "./chat-context"; +import type { ChatEvent } from "./chat-events"; /** 游客配额阈值(与 Dart `GuestChatQuota` 保持一致) */ export const GuestChatQuota = { @@ -56,27 +32,10 @@ export const GuestChatQuota = { quotaExhausted: 0, } as const; -// ============================================================ -// Events -// ============================================================ -export type ChatEvent = - | { type: "ChatInit" } - | { type: "ChatScreenVisible" } - | { type: "ChatScreenInvisible" } - | { type: "ChatSendMessage"; content: string } - | { type: "ChatSendImage"; imageBase64: string } - | { type: "ChatLoadMoreHistory" } - | { - type: "ChatAISentenceReceived"; - index: number; - text: string; - total: number; - done: boolean; - } - | { type: "ChatWebSocketError"; errorMessage: string } - | { type: "ChatWebSocketConnected"; userId: string } - | { type: "ChatQuotaExceeded" } - | { type: "ChatAuthStatusChanged" }; +// 重新导出 Context / Event 类型,保持 machine 文件的公共 API 不变 +export type { ChatContext } from "./chat-context"; +export { initialContext } from "./chat-context"; +export type { ChatEvent } from "./chat-events"; // ============================================================ // Helpers diff --git a/src/stores/chat/chat-types.ts b/src/stores/chat/chat-types.ts index 7313eb41..7d67d910 100644 --- a/src/stores/chat/chat-types.ts +++ b/src/stores/chat/chat-types.ts @@ -3,9 +3,10 @@ * * 原始文件定义了 ChatState 接口、ChatEvent 联合、initialChatState、GuestChatQuota。 * 状态机重构后: - * - `ChatEvent` 联合由 `chat-machine.ts` 重新导出(保留类型) + * - `ChatEvent` 联合由 `chat-events.ts` 导出 + * - Context 形状与初始值由 `chat-context.ts` 导出 * - `ChatState` 由 `chat-context.tsx` 导出(保持原 API 兼容) - * - `GuestChatQuota` 重新导出 + * - `GuestChatQuota` 仍由 `chat-machine.ts` 导出(业务常量,与上下文配对紧密) */ -export type { ChatEvent } from "./chat-machine"; +export type { ChatEvent } from "./chat-events"; export { GuestChatQuota } from "./chat-machine"; diff --git a/src/stores/chat/index.ts b/src/stores/chat/index.ts index bc50eb06..a1b064a2 100644 --- a/src/stores/chat/index.ts +++ b/src/stores/chat/index.ts @@ -5,10 +5,13 @@ * - 业务类(Dart 风格 context/reducer/side-effects)已合并到 chat-machine.ts * - 类型由 chat-types.ts 重新导出 * - React 入口为 chat-context.tsx(useMachine 包装) + * - Context 形状与初始值位于 chat-context.ts + * - 事件联合位于 chat-events.ts * * 注:chat-side-effects.ts 暂未删除(其中 WebSocket 长生命周期逻辑 * 待下一轮迁移到 fromCallback actor)。 */ export * from "./chat-context"; +export * from "./chat-events"; export * from "./chat-types"; export { chatMachine } from "./chat-machine"; diff --git a/src/stores/sidebar/index.ts b/src/stores/sidebar/index.ts index 405fffd9..6ef25855 100644 --- a/src/stores/sidebar/index.ts +++ b/src/stores/sidebar/index.ts @@ -5,7 +5,10 @@ * - reducer 逻辑已合并到 sidebar-machine.ts * - 类型由 sidebar-types.ts 重新导出 * - React 入口为 sidebar-context.tsx(useMachine 包装) + * - Context 形状与初始值位于 sidebar-context.ts + * - 事件联合位于 sidebar-events.ts */ export * from "./sidebar-context"; +export * from "./sidebar-events"; export * from "./sidebar-types"; export { sidebarMachine } from "./sidebar-machine"; diff --git a/src/stores/sidebar/sidebar-context.ts b/src/stores/sidebar/sidebar-context.ts new file mode 100644 index 00000000..e70da168 --- /dev/null +++ b/src/stores/sidebar/sidebar-context.ts @@ -0,0 +1,20 @@ +/** + * Sidebar 状态机:Context 形状 + 初始值 + * + * SidebarPage / BottomNavItem 仍由 sidebar-machine.ts 导出(事件/上下文均需引用)。 + */ +import { SidebarPage, BottomNavItem } from "./sidebar-machine"; + +export interface SidebarContext { + currentPage: SidebarPage; + selectedBottomNavItem: BottomNavItem; + characterName: string; + characterProgress: number; +} + +export const initialContext: SidebarContext = { + currentPage: SidebarPage.DefaultView, + selectedBottomNavItem: BottomNavItem.None, + characterName: "Luna", + characterProgress: 0, +}; diff --git a/src/stores/sidebar/sidebar-events.ts b/src/stores/sidebar/sidebar-events.ts new file mode 100644 index 00000000..6b5ed72a --- /dev/null +++ b/src/stores/sidebar/sidebar-events.ts @@ -0,0 +1,13 @@ +/** + * Sidebar 状态机:事件联合 + */ +import type { SidebarPage, BottomNavItem } from "./sidebar-machine"; + +export type SidebarEvent = + | { + type: "SidebarShowPage"; + page: SidebarPage; + bottomNavItem?: BottomNavItem; + } + | { type: "SidebarClearBottomNav" } + | { type: "SidebarUpdateCharacter"; name: string; progress: number }; diff --git a/src/stores/sidebar/sidebar-machine.ts b/src/stores/sidebar/sidebar-machine.ts index d7d7b0f1..21fffd43 100644 --- a/src/stores/sidebar/sidebar-machine.ts +++ b/src/stores/sidebar/sidebar-machine.ts @@ -11,6 +11,9 @@ */ import { setup, assign } from "xstate"; +import { SidebarContext, initialContext } from "./sidebar-context"; +import type { SidebarEvent } from "./sidebar-events"; + /** * Sidebar 页面/底部导航枚举(与原 sidebar-types.ts 保持一致) */ @@ -33,34 +36,10 @@ export const BottomNavItem = { export type BottomNavItem = (typeof BottomNavItem)[keyof typeof BottomNavItem]; -// ============================================================ -// Context -// ============================================================ -export interface SidebarContext { - currentPage: SidebarPage; - selectedBottomNavItem: BottomNavItem; - characterName: string; - characterProgress: number; -} - -const initialContext: SidebarContext = { - currentPage: SidebarPage.DefaultView, - selectedBottomNavItem: BottomNavItem.None, - characterName: "Luna", - characterProgress: 0, -}; - -// ============================================================ -// Events -// ============================================================ -export type SidebarEvent = - | { - type: "SidebarShowPage"; - page: SidebarPage; - bottomNavItem?: BottomNavItem; - } - | { type: "SidebarClearBottomNav" } - | { type: "SidebarUpdateCharacter"; name: string; progress: number }; +// 重新导出 Context / Event 类型,保持 machine 文件的公共 API 不变 +export type { SidebarContext } from "./sidebar-context"; +export { initialContext } from "./sidebar-context"; +export type { SidebarEvent } from "./sidebar-events"; // ============================================================ // Machine diff --git a/src/stores/sidebar/sidebar-types.ts b/src/stores/sidebar/sidebar-types.ts index cd631aa9..a81b4c77 100644 --- a/src/stores/sidebar/sidebar-types.ts +++ b/src/stores/sidebar/sidebar-types.ts @@ -3,12 +3,10 @@ * * 原始文件定义了 SidebarPage、BottomNavItem、SidebarState、SidebarEvent。 * 状态机重构后: - * - 枚举(SidebarPage、BottomNavItem)由 `sidebar-machine.ts` 重新导出 - * - 事件联合(SidebarEvent)由 `sidebar-machine.ts` 重新导出 + * - 枚举(SidebarPage、BottomNavItem)由 `sidebar-machine.ts` 导出 + * - 事件联合(SidebarEvent)由 `sidebar-events.ts` 导出 + * - Context 形状由 `sidebar-context.ts` 导出 * - SidebarState 由 `sidebar-context.tsx` 导出(保持原 API 兼容) */ -export { - SidebarPage, - BottomNavItem, - type SidebarEvent, -} from "./sidebar-machine"; +export { SidebarPage, BottomNavItem } from "./sidebar-machine"; +export type { SidebarEvent } from "./sidebar-events"; diff --git a/src/stores/user/index.ts b/src/stores/user/index.ts index f48a429e..47edfcf8 100644 --- a/src/stores/user/index.ts +++ b/src/stores/user/index.ts @@ -5,7 +5,10 @@ * - 业务类(Dart 风格 context/reducer/side-effects)已合并到 user-machine.ts * - 类型由 user-types.ts 重新导出 * - React 入口为 user-context.tsx(useMachine 包装) + * - Context 形状与初始值位于 user-context.ts + * - 事件联合位于 user-events.ts */ export * from "./user-context"; +export * from "./user-events"; export * from "./user-types"; export { userMachine } from "./user-machine"; diff --git a/src/stores/user/user-context.ts b/src/stores/user/user-context.ts new file mode 100644 index 00000000..aeecf8d9 --- /dev/null +++ b/src/stores/user/user-context.ts @@ -0,0 +1,20 @@ +/** + * User 状态机:Context 形状 + 初始值 + */ +import type { UserView } from "@/models/user/user"; + +export interface UserContext { + currentUser: UserView | null; + pronouns: string; + coinBalance: number; + isLoading: boolean; + avatarUrl: string | null; +} + +export const initialContext: UserContext = { + currentUser: null, + pronouns: "He", + coinBalance: 45, + isLoading: false, + avatarUrl: null, +}; diff --git a/src/stores/user/user-events.ts b/src/stores/user/user-events.ts new file mode 100644 index 00000000..af51026b --- /dev/null +++ b/src/stores/user/user-events.ts @@ -0,0 +1,14 @@ +/** + * User 状态机:事件联合 + */ +import type { UserView } from "@/models/user/user"; + +export type UserEvent = + | { type: "UserInit" } + | { type: "UserFetch" } + | { type: "UserUpdate"; user: UserView } + | { type: "UserUpdateUsername"; username: string } + | { type: "UserUpdatePronouns"; pronouns: string } + | { type: "UserLogout" } + | { type: "UserDeleteChatHistory" } + | { type: "UserDeleteAccount" }; diff --git a/src/stores/user/user-machine.ts b/src/stores/user/user-machine.ts index 72f0a5ad..c5006026 100644 --- a/src/stores/user/user-machine.ts +++ b/src/stores/user/user-machine.ts @@ -17,37 +17,13 @@ import { authRepository } from "@/data/repositories/auth_repository"; import { UserStorage } from "@/data/storage/user/user_storage"; import { Result } from "@/utils/result"; -// ============================================================ -// Context -// ============================================================ -export interface UserContext { - currentUser: UserView | null; - pronouns: string; - coinBalance: number; - isLoading: boolean; - avatarUrl: string | null; -} +import { UserContext, initialContext } from "./user-context"; +import type { UserEvent } from "./user-events"; -const initialContext: UserContext = { - currentUser: null, - pronouns: "He", - coinBalance: 45, - isLoading: false, - avatarUrl: null, -}; - -// ============================================================ -// Events -// ============================================================ -export type UserEvent = - | { type: "UserInit" } - | { type: "UserFetch" } - | { type: "UserUpdate"; user: UserView } - | { type: "UserUpdateUsername"; username: string } - | { type: "UserUpdatePronouns"; pronouns: string } - | { type: "UserLogout" } - | { type: "UserDeleteChatHistory" } - | { type: "UserDeleteAccount" }; +// 重新导出 Context / Event 类型,保持 machine 文件的公共 API 不变 +export type { UserContext } from "./user-context"; +export { initialContext } from "./user-context"; +export type { UserEvent } from "./user-events"; // ============================================================ // Helpers diff --git a/src/stores/user/user-types.ts b/src/stores/user/user-types.ts index e9240e1b..738d2605 100644 --- a/src/stores/user/user-types.ts +++ b/src/stores/user/user-types.ts @@ -3,7 +3,8 @@ * * 原始文件定义了 UserState 接口、UserEvent 联合、initialUserState。 * 状态机重构后: - * - `UserEvent` 联合由 `user-machine.ts` 重新导出(保留类型) + * - `UserEvent` 联合由 `user-events.ts` 导出 + * - Context 形状与初始值由 `user-context.ts` 导出 * - `UserState` 由 `user-context.tsx` 导出(保持原 API 兼容) */ -export type { UserEvent } from "./user-machine"; +export type { UserEvent } from "./user-events";