Merge branch 'dev' into test

This commit is contained in:
2026-06-17 12:59:59 +08:00
13 changed files with 103 additions and 74 deletions
-1
View File
@@ -10,7 +10,6 @@ export * from "./chat-input-bar";
export * from "./chat-input-text-field";
export * from "./chat-quota-exhausted-banner";
export * from "./chat-screen";
export * from "./chat-send-button";
export * from "./date-header";
export * from "./fullscreen-image-viewer";
+7 -14
View File
@@ -1,6 +1,6 @@
"use client";
import { useEffect, useRef } from "react";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { MobileShell } from "@/app/_components/core/mobile-shell";
@@ -9,8 +9,6 @@ import { ROUTES } from "@/router/routes";
import { useAuthDispatch, useAuthState } from "@/stores/auth/auth-context";
import { useUserDispatch, useUserState } from "@/stores/user/user-context";
import type { UserView } from "@/models/user/user";
import { BackBar } from "./back-bar";
import { UserHeader, type SidebarUserState } from "./user-header";
import { VipBenefitsCard } from "./vip-benefits-card";
@@ -41,16 +39,11 @@ export function SidebarScreen() {
userDispatch({ type: "UserInit" });
}, [userDispatch]);
const prevUserRef = useRef<UserView | null>(user.currentUser);
useEffect(() => {
const wasLoggedIn = prevUserRef.current != null;
const isNowLoggedOut = user.currentUser == null;
prevUserRef.current = user.currentUser;
if (wasLoggedIn && isNowLoggedOut) {
authDispatch({ type: "AuthReset" });
router.replace(ROUTES.chat);
}
}, [user.currentUser, authDispatch, router]);
const handleLogoutClick = () => {
authDispatch({ type: "AuthReset" });
userDispatch({ type: "UserLogout" });
router.replace(ROUTES.chat);
};
// 状态派生:未登录 / 已登录未 VIP / 已登录 VIP
const sidebarState: SidebarUserState =
@@ -111,7 +104,7 @@ export function SidebarScreen() {
id: "logout",
title: "Log out",
destructive: true,
onClick: () => userDispatch({ type: "UserLogout" }),
onClick: handleLogoutClick,
},
]}
/>
+2
View File
@@ -5,7 +5,9 @@
export * from "./subscription-back-link";
export * from "./subscription-banner";
export * from "./subscription-benefits-card";
export * from "./subscription-checkout-button";
export * from "./subscription-cta-button";
export * from "./subscription-manage-button";
export * from "./subscription-plan-card";
export * from "./subscription-screen";
export * from "./subscription-user-row";
+18 -18
View File
@@ -13,25 +13,25 @@
* 原始 Dart: lib/data/repositories/auth_repository_impl.dart
*/
import { AuthApi, authApi } from "@/data/services/api";
import { AppleLoginRequest } from "@/data/dto/auth/apple_login_request";
import { FacebookLoginRequest } from "@/data/dto/auth/facebook_login_request";
import { FbIdLoginRequest } from "@/data/dto/auth/fb_id_login_request";
import { GoogleLoginRequest } from "@/data/dto/auth/google_login_request";
import { GuestLoginRequest } from "@/data/dto/auth/guest_login_request";
import { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
import { LoginRequest } from "@/data/dto/auth/login_request";
import { LoginResponse } from "@/data/dto/auth/login_response";
import { RefreshTokenRequest } from "@/data/dto/auth/refresh_token_request";
import { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
import { RegisterRequest } from "@/data/dto/auth/register_request";
import { SendCodeRequest } from "@/data/dto/auth/send_code_request";
import { User } from "@/data/dto/user/user";
import {
AppleLoginRequest,
FacebookLoginRequest,
FbIdLoginRequest,
GoogleLoginRequest,
GuestLoginRequest,
GuestLoginResponse,
LoginRequest,
LoginResponse,
RefreshTokenRequest,
RefreshTokenResponse,
RegisterRequest,
SendCodeRequest,
} from "@/data/dto/auth";
import { User } from "@/data/dto/user";
import { Result } from "@/utils/result";
import type { IAuthRepository } from "@/data/repositories/interfaces/iauth_repository";
import { AuthStorage } from "@/data/storage/auth/auth_storage";
import type { IAuthStorage } from "@/data/storage/auth/iauth_storage";
import { UserStorage } from "@/data/storage/user/user_storage";
import type { IUserStorage } from "@/data/storage/user/iuser_storage";
import type { IAuthRepository } from "@/data/repositories/interfaces";
import { AuthStorage, type IAuthStorage } from "@/data/storage/auth";
import { UserStorage, type IUserStorage } from "@/data/storage/user";
/** 硬编码平台名,对齐 Dart `PlatformUtil.platformName.toLowerCase()`Web 平台)。 */
const WEB_PLATFORM = "web";
+8 -7
View File
@@ -16,14 +16,15 @@
* 原始 Dart: lib/data/repositories/chat_repository_impl.dart
*/
import { ChatApi, chatApi } from "@/data/services/api";
import { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
import { ChatMessage } from "@/data/dto/chat/chat_message";
import { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
import { SendMessageRequest } from "@/data/dto/chat/send_message_request";
import {
ChatHistoryResponse,
ChatMessage,
ChatSendResponse,
SendMessageRequest,
} from "@/data/dto/chat";
import { Result } from "@/utils/result";
import type { IChatRepository } from "@/data/repositories/interfaces/ichat_repository";
import { LocalChatStorage } from "@/data/storage/chat/local_chat_storage";
import { LocalMessage } from "@/data/storage/chat/local_message";
import type { IChatRepository } from "@/data/repositories/interfaces";
import { LocalChatStorage, LocalMessage } from "@/data/storage/chat";
export class ChatRepository implements IChatRepository {
constructor(
@@ -14,10 +14,12 @@
*/
import type { Result } from "@/utils/result";
import type { GuestLoginResponse } from "@/data/dto/auth/guest_login_response";
import type { LoginResponse } from "@/data/dto/auth/login_response";
import type { RefreshTokenResponse } from "@/data/dto/auth/refresh_token_response";
import type { User } from "@/data/dto/user/user";
import type {
GuestLoginResponse,
LoginResponse,
RefreshTokenResponse,
} from "@/data/dto/auth";
import type { User } from "@/data/dto/user";
export interface IAuthRepository {
/** 用户注册。注册成功不会自动登录(与 Dart 行为一致)。 */
@@ -12,9 +12,11 @@
*/
import type { Result } from "@/utils/result";
import type { ChatHistoryResponse } from "@/data/dto/chat/chat_history_response";
import type { ChatMessage } from "@/data/dto/chat/chat_message";
import type { ChatSendResponse } from "@/data/dto/chat/chat_send_response";
import type {
ChatHistoryResponse,
ChatMessage,
ChatSendResponse,
} from "@/data/dto/chat";
export interface IChatRepository {
/** 发送一条消息。 */
@@ -10,11 +10,13 @@
*/
import type { Result } from "@/utils/result";
import type { CreditsData } from "@/data/dto/user/credits_data";
import type { CreditsHistoryData } from "@/data/dto/user/credits_history_data";
import type { UpdateProfileRequest } from "@/data/dto/user/update_profile_request";
import type { User } from "@/data/dto/user/user";
import type { UserStatsResponse } from "@/data/dto/user/user_stats_response";
import type {
CreditsData,
CreditsHistoryData,
UpdateProfileRequest,
User,
UserStatsResponse,
} from "@/data/dto/user";
export interface IUserRepository {
/** 获取用户统计信息。 */
+2 -3
View File
@@ -8,10 +8,9 @@
* 原始 Dart: lib/data/repositories/metrics_repository_impl.dart
*/
import { MetricsApi, metricsApi } from "@/data/services/api";
import { AppEvent } from "@/data/dto/metrics/app_event";
import { PwaEvent } from "@/data/dto/metrics/pwa_event";
import { AppEvent, PwaEvent } from "@/data/dto/metrics";
import { Result } from "@/utils/result";
import type { IMetricsRepository } from "@/data/repositories/interfaces/imetrics_repository";
import type { IMetricsRepository } from "@/data/repositories/interfaces";
export class MetricsRepository implements IMetricsRepository {
constructor(private readonly api: MetricsApi) {}
+8 -6
View File
@@ -9,13 +9,15 @@
* 原始 Dart: lib/data/repositories/user_repository_impl.dart
*/
import { UserApi, userApi } from "@/data/services/api";
import { CreditsData } from "@/data/dto/user/credits_data";
import { CreditsHistoryData } from "@/data/dto/user/credits_history_data";
import { UpdateProfileRequest } from "@/data/dto/user/update_profile_request";
import { User } from "@/data/dto/user/user";
import { UserStatsResponse } from "@/data/dto/user/user_stats_response";
import {
CreditsData,
CreditsHistoryData,
UpdateProfileRequest,
User,
UserStatsResponse,
} from "@/data/dto/user";
import { Result } from "@/utils/result";
import type { IUserRepository } from "@/data/repositories/interfaces/iuser_repository";
import type { IUserRepository } from "@/data/repositories/interfaces";
export class UserRepository implements IUserRepository {
constructor(private readonly api: UserApi) {}
+33
View File
@@ -0,0 +1,33 @@
/**
* Zod 默认值辅助器(处理 `null | undefined` → 标量默认)
*
* 防御性解析:后端对 Facebook / Apple 等 OAuth 用户可能返回 `null`(如 `email: null`),
* Zod 默认的 `.default("")` 只接受 `undefined`,遇到 `null` 会抛 ZodError → 登录静默失败。
* 用 `z.string().nullable().transform(v => v ?? "").default("")` 模式:
* - input 接受 `string | null | undefined`(保持 optional
* - output 永远是 `string`User 类的 `email: string` 声明不会破)
*
* 这是前端防御性解析(不要求后端修改)—— 和后端对齐更稳。
*/
import { z } from "zod";
/** `string | null | undefined` → `string`(默认空串) */
export const stringOrEmpty = z
.string()
.nullable()
.transform((v) => v ?? "")
.default("");
/** `number | null | undefined` → `number`(默认 0 */
export const numberOrZero = z
.number()
.nullable()
.transform((v) => v ?? 0)
.default(0);
/** `boolean | null | undefined` → `boolean`(默认 false */
export const booleanOrFalse = z
.boolean()
.nullable()
.transform((v) => v ?? false)
.default(false);
+5 -13
View File
@@ -4,19 +4,11 @@
*/
import { z } from "zod";
import { PersonalityTraitsSchema, PERSONALITY_TRAITS_DEFAULTS } from "./personality_traits";
/**
* 防御性解析:后端对 Facebook / Apple 等 OAuth 用户可能返回 `null`(如 `email: null`),
* Zod 默认的 `.default("")` 只接受 `undefined`,遇到 `null` 会抛 ZodError → 登录静默失败。
* 用 `z.string().nullable().transform(v => v ?? "").default("")` 模式:
* - input 接受 `string | null | undefined`(保持 optional
* - output 永远是 `string`User 类的 `email: string` 声明不会破)
*
* 这是前端防御性解析(不要求后端修改)—— 和后端对齐更稳。
*/
const stringOrEmpty = z.string().nullable().transform((v) => v ?? "").default("");
const numberOrZero = z.number().nullable().transform((v) => v ?? 0).default(0);
const booleanOrFalse = z.boolean().nullable().transform((v) => v ?? false).default(false);
import {
booleanOrFalse,
numberOrZero,
stringOrEmpty,
} from "../nullable-defaults";
export const UserSchema = z.object({
id: stringOrEmpty, // 兜底:理论 id 不会 null,但保持防御
+2
View File
@@ -4,5 +4,7 @@
export * from "./user-context";
export * from "./user-events";
export * from "./user-machine.actors";
export * from "./user-machine.helpers";
export * from "./user-machine";
export * from "./user-state";