Merge branch 'dev' into test
This commit is contained in:
@@ -2,11 +2,6 @@
|
|||||||
/**
|
/**
|
||||||
* ChatScreen 编排层
|
* ChatScreen 编排层
|
||||||
*
|
*
|
||||||
* 职责(本文件):
|
|
||||||
* - 订阅 auth 状态机(loginStatus)—— 派生 isGuest / 派鉴权生命周期事件给 chat 机器
|
|
||||||
* - 屏幕生命周期事件(init / visible / invisible)
|
|
||||||
* - 不直接管理 WebSocket / 不直接读 AuthStorage(除 token 取值)
|
|
||||||
*
|
|
||||||
* 子组件职责:
|
* 子组件职责:
|
||||||
* - ChatHeader:顶部栏(游客 banner / 菜单按钮)
|
* - ChatHeader:顶部栏(游客 banner / 菜单按钮)
|
||||||
* - ChatArea:消息列表
|
* - ChatArea:消息列表
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
.button {
|
.button {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: 48px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 9999px;
|
border-radius: 50%;
|
||||||
background: var(--color-button-gradient-end, #fc69df);
|
background: var(--color-button-gradient-end, #fc69df);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
* - 默认:粉色背景 + 渐变边缘
|
* - 默认:粉色背景 + 渐变边缘
|
||||||
* - 聚焦 / 激活:完整 primaryGradient(accent → 透明)
|
* - 聚焦 / 激活:完整 primaryGradient(accent → 透明)
|
||||||
*
|
*
|
||||||
* 图标:lucide-react <Send />(tree-shakable,currentColor 继承父 color)
|
* 图标:lucide-react <ArrowUp />(tree-shakable,currentColor 继承父 color)
|
||||||
*/
|
*/
|
||||||
import { ArrowUp, Send } from "lucide-react";
|
import { ArrowUp } from "lucide-react";
|
||||||
|
|
||||||
import styles from "./chat-send-button.module.css";
|
import styles from "./chat-send-button.module.css";
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
*/
|
*/
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import { BrowserDetector } from "@/utils/browser-detect";
|
||||||
import { SpAsyncUtil } from "@/utils/storage";
|
import { SpAsyncUtil } from "@/utils/storage";
|
||||||
import { pwaUtil } from "@/utils/pwa";
|
import { pwaUtil } from "@/utils/pwa";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ export function PwaInstallOverlay() {
|
|||||||
// isInstalled() 检查 standalone display-mode(避免给已安装用户再弹)
|
// isInstalled() 检查 standalone display-mode(避免给已安装用户再弹)
|
||||||
if (!pwaUtil.isSupported()) return;
|
if (!pwaUtil.isSupported()) return;
|
||||||
if (pwaUtil.isInstalled()) return;
|
if (pwaUtil.isInstalled()) return;
|
||||||
|
if (BrowserDetector.isInAppBrowser()) return;
|
||||||
// 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。
|
// 提前挂 beforeinstallprompt 监听,避免用户点 OK 时事件已在更早时机丢失。
|
||||||
pwaUtil.prepareInstallPrompt();
|
pwaUtil.prepareInstallPrompt();
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.skip {
|
.skip {
|
||||||
font-family: var(--font-athelas);
|
|
||||||
font-size: var(--font-size-xxl);
|
font-size: var(--font-size-xxl);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -58,7 +57,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.facebookLabel {
|
.facebookLabel {
|
||||||
font-family: var(--font-athelas);
|
|
||||||
font-size: var(--font-size-xxl);
|
font-size: var(--font-size-xxl);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* Splash 主内容文案
|
* Splash 主内容文案
|
||||||
*
|
|
||||||
* 原始 Dart: lib/ui/splash/widgets/splash_content.dart
|
|
||||||
*
|
|
||||||
* 关键差异:
|
|
||||||
* - 使用 Athelas 字体(衬线体)
|
|
||||||
* - 标题加粗 + 斜体(与原 Dart 一致)
|
|
||||||
* - 颜色:白色(背景图上需高对比度)
|
|
||||||
*/
|
*/
|
||||||
import styles from "./splash-content.module.css";
|
import styles from "./splash-content.module.css";
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,4 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
/* Apple 平台 Athelas 渲染时仍可能需要 fallback */
|
|
||||||
font-family: var(--font-athelas, serif);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
margin: var(--spacing-xxxl) 0 0 0;
|
margin: var(--spacing-xxxl) 0 0 0;
|
||||||
font-family: var(--font-athelas);
|
|
||||||
font-size: var(--font-size-md);
|
font-size: var(--font-size-md);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import localFont from "next/font/local";
|
|||||||
* - 产出的 CSS 变量由 `globals.css` 桥接到 Tailwind(如 `font-sans`、`font-athelas`)。
|
* - 产出的 CSS 变量由 `globals.css` 桥接到 Tailwind(如 `font-sans`、`font-athelas`)。
|
||||||
*
|
*
|
||||||
* 注意:本文件不通过 `src/lib/index.ts` 桶导出,避免被误打进客户端 bundle。
|
* 注意:本文件不通过 `src/lib/index.ts` 桶导出,避免被误打进客户端 bundle。
|
||||||
* 引用方式:`import { geistSans, geistMono, athelas } from "@/lib/fonts";`
|
* 引用方式:`import { geistSans, geistMono, athelas } from "@/core/fonts";`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@@ -29,7 +29,7 @@ const geistMono = Geist_Mono({
|
|||||||
* 放置位置:/public/fonts/Athelas-*.ttf
|
* 放置位置:/public/fonts/Athelas-*.ttf
|
||||||
*
|
*
|
||||||
* 路径计算(`next/font/local` 要求相对路径从调用文件出发):
|
* 路径计算(`next/font/local` 要求相对路径从调用文件出发):
|
||||||
* src/lib/fonts.ts → ../../public/fonts/...
|
* src/core/fonts.ts → ../../public/fonts/...
|
||||||
*/
|
*/
|
||||||
const athelas = localFont({
|
const athelas = localFont({
|
||||||
src: [
|
src: [
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
* 通过环境变量配置后端服务地址与超时时间。
|
* 通过环境变量配置后端服务地址与超时时间。
|
||||||
* 原始 Dart: lib/core/net/service_manager.dart
|
* 原始 Dart: lib/core/net/service_manager.dart
|
||||||
*/
|
*/
|
||||||
|
import { AppEnvUtil, type AppEnv } from "@/utils/app-env";
|
||||||
|
|
||||||
export type AppEnv = "development" | "test" | "production";
|
export type { AppEnv };
|
||||||
|
|
||||||
export interface ApiConfig {
|
export interface ApiConfig {
|
||||||
/** 后端 API 基础 URL(含协议) */
|
/** 后端 API 基础 URL(含协议) */
|
||||||
@@ -33,9 +34,7 @@ export class TimeoutConstants {
|
|||||||
* 解析当前环境
|
* 解析当前环境
|
||||||
*/
|
*/
|
||||||
export function getAppEnv(): AppEnv {
|
export function getAppEnv(): AppEnv {
|
||||||
const env = process.env.NEXT_PUBLIC_APP_ENV;
|
return AppEnvUtil.current;
|
||||||
if (env === "production" || env === "test") return env;
|
|
||||||
return "development";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type GuestChatQuotaInput,
|
type GuestChatQuotaInput,
|
||||||
type GuestChatQuotaData,
|
type GuestChatQuotaData,
|
||||||
} from "@/data/schemas/chat/guest_chat_quota";
|
} from "@/data/schemas/chat/guest_chat_quota";
|
||||||
|
import { AppEnvUtil } from "@/utils/app-env";
|
||||||
|
|
||||||
export class GuestChatQuota {
|
export class GuestChatQuota {
|
||||||
// 静态常量
|
// 静态常量
|
||||||
@@ -13,7 +14,7 @@ export class GuestChatQuota {
|
|||||||
static readonly maxQuotaPerDayTest = 4;
|
static readonly maxQuotaPerDayTest = 4;
|
||||||
|
|
||||||
static readonly defaultTotalQuota = 50;
|
static readonly defaultTotalQuota = 50;
|
||||||
static readonly defaultTotalQuotaDev = 5;
|
static readonly defaultTotalQuotaTest = 5;
|
||||||
static readonly quotaExhausted = 0;
|
static readonly quotaExhausted = 0;
|
||||||
|
|
||||||
declare readonly remaining: number;
|
declare readonly remaining: number;
|
||||||
@@ -40,29 +41,22 @@ export class GuestChatQuota {
|
|||||||
return GuestChatQuota.quotaExhausted;
|
return GuestChatQuota.quotaExhausted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否处于开发环境
|
|
||||||
*/
|
|
||||||
static get isDevelopment(): boolean {
|
|
||||||
return process.env.NODE_ENV !== "production";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前环境的每日最大配额
|
* 获取当前环境的每日最大配额
|
||||||
*/
|
*/
|
||||||
static get threshold(): number {
|
static get threshold(): number {
|
||||||
return GuestChatQuota.isDevelopment
|
return AppEnvUtil.isProduction()
|
||||||
? GuestChatQuota.maxQuotaPerDayTest
|
? GuestChatQuota.maxQuotaPerDay
|
||||||
: GuestChatQuota.maxQuotaPerDay;
|
: GuestChatQuota.maxQuotaPerDayTest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前环境的总配额默认值
|
* 获取当前环境的总配额默认值
|
||||||
*/
|
*/
|
||||||
static get totalQuotaDefault(): number {
|
static get totalQuotaDefault(): number {
|
||||||
return GuestChatQuota.isDevelopment
|
return AppEnvUtil.isProduction()
|
||||||
? GuestChatQuota.defaultTotalQuotaDev
|
? GuestChatQuota.defaultTotalQuota
|
||||||
: GuestChatQuota.defaultTotalQuota;
|
: GuestChatQuota.defaultTotalQuotaTest;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,5 +2,4 @@
|
|||||||
* @file Automatically generated by barrelsby.
|
* @file Automatically generated by barrelsby.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./browser-detect";
|
|
||||||
export * from "./media-recorder";
|
export * from "./media-recorder";
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export type AuthEvent =
|
|||||||
| { type: "AuthPanelModeChanged"; mode: AuthPanelMode }
|
| { type: "AuthPanelModeChanged"; mode: AuthPanelMode }
|
||||||
| { type: "AuthModeChanged"; mode: AuthMode }
|
| { type: "AuthModeChanged"; mode: AuthMode }
|
||||||
| { type: "AuthFormCleared" }
|
| { type: "AuthFormCleared" }
|
||||||
| { type: "AuthReset" }
|
|
||||||
| { type: "AuthLogoutSubmitted" }
|
| { type: "AuthLogoutSubmitted" }
|
||||||
/** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */
|
/** App 启动时一次性派发 —— 读 storage 把 loginStatus 同步到状态机 */
|
||||||
| { type: "AuthInit" }
|
| { type: "AuthInit" }
|
||||||
|
|||||||
@@ -78,9 +78,6 @@ export const authMachine = setup({
|
|||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
AuthReset: {
|
|
||||||
actions: assign(() => initialState),
|
|
||||||
},
|
|
||||||
AuthLogoutSubmitted: "loggingOut",
|
AuthLogoutSubmitted: "loggingOut",
|
||||||
// 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 <AuthStatusChecker /> 派发
|
// 启动一次性 init:从 storage 同步 loginStatus 到状态机 —— 由 <AuthStatusChecker /> 派发
|
||||||
AuthInit: "initializing",
|
AuthInit: "initializing",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function UserAuthSync() {
|
|||||||
const userDispatch = useUserDispatch();
|
const userDispatch = useUserDispatch();
|
||||||
const lastInitializedStatusRef = useRef<string | null>(null);
|
const lastInitializedStatusRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
//TODO 在测位栏界面监听变化,监听登录状态的变化若发生变化,则需要分发 user init 事件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loginStatus === "notLoggedIn" || loginStatus === "guest") {
|
if (loginStatus === "notLoggedIn" || loginStatus === "guest") {
|
||||||
lastInitializedStatusRef.current = null;
|
lastInitializedStatusRef.current = null;
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
*/
|
*/
|
||||||
--font-system: var(--font-athelas), system-ui, -apple-system,
|
--font-system: var(--font-athelas), system-ui, -apple-system,
|
||||||
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
--font-athelas: "Athelas", serif;
|
|
||||||
|
|
||||||
/* Tailwind 默认字体别名:font-sans → font-system */
|
/* Tailwind 默认字体别名:font-sans → font-system */
|
||||||
--font-sans: var(--font-system);
|
--font-sans: var(--font-system);
|
||||||
|
|||||||
+28
-7
@@ -3,13 +3,34 @@
|
|||||||
*
|
*
|
||||||
* 原始 Dart: lib/utils/app_env_util.dart
|
* 原始 Dart: lib/utils/app_env_util.dart
|
||||||
*
|
*
|
||||||
* 替代 `AppEnvUtil.isDevelopment`:基于 Next.js 注入的 `process.env.NODE_ENV`。
|
* 基于 `.env*` 中的 `NEXT_PUBLIC_APP_ENV` 判断当前业务环境。
|
||||||
|
*
|
||||||
|
* 注意:该变量带 `NEXT_PUBLIC_` 前缀,可同时用于服务端和客户端代码。
|
||||||
*/
|
*/
|
||||||
export const isDevelopment = (): boolean =>
|
export type AppEnv = "development" | "test" | "production";
|
||||||
process.env.NODE_ENV !== "production";
|
|
||||||
|
|
||||||
export const isProduction = (): boolean =>
|
export class AppEnvUtil {
|
||||||
process.env.NODE_ENV === "production";
|
private constructor() {}
|
||||||
|
|
||||||
export const isTest = (): boolean =>
|
static get current(): AppEnv {
|
||||||
process.env.NODE_ENV === "test";
|
const env = process.env.NEXT_PUBLIC_APP_ENV;
|
||||||
|
if (env === "production" || env === "test") return env;
|
||||||
|
return "development";
|
||||||
|
}
|
||||||
|
|
||||||
|
static isDevelopment(): boolean {
|
||||||
|
return AppEnvUtil.current === "development";
|
||||||
|
}
|
||||||
|
|
||||||
|
static isProduction(): boolean {
|
||||||
|
return AppEnvUtil.current === "production";
|
||||||
|
}
|
||||||
|
|
||||||
|
static isTest(): boolean {
|
||||||
|
return AppEnvUtil.current === "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
static canOutputLogs(): boolean {
|
||||||
|
return !AppEnvUtil.isProduction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,13 +29,4 @@ export const BrowserDetector = {
|
|||||||
return /WebView|; wv\)|FBAN|FBAV|MicroMessenger|Instagram|Line\//i.test(userAgent);
|
return /WebView|; wv\)|FBAN|FBAV|MicroMessenger|Instagram|Line\//i.test(userAgent);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 当前是否以 PWA standalone 模式运行 */
|
|
||||||
isPWAStandalone(): boolean {
|
|
||||||
if (typeof window === "undefined") return false;
|
|
||||||
return (
|
|
||||||
window.matchMedia?.("(display-mode: standalone)").matches === true ||
|
|
||||||
// iOS Safari 旧版
|
|
||||||
(navigator as Navigator & { standalone?: boolean }).standalone === true
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
+6
-6
@@ -1,22 +1,22 @@
|
|||||||
import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino";
|
import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino";
|
||||||
|
|
||||||
import { isProduction, isTest } from "./app-env";
|
import { AppEnvUtil } from "./app-env";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局 pino root logger(单例,进程级复用)。
|
* 全局 pino root logger(单例,进程级复用)。
|
||||||
*
|
*
|
||||||
* 配置驱动(基于 `app-env.ts` 的 `isProduction()` / `isTest()`):
|
* 配置驱动(基于 `app-env.ts` 的 `AppEnvUtil`):
|
||||||
* - production: info 级别 + JSON(机器可解析,接入日志平台)
|
* - production: silent(不输出日志)
|
||||||
* - test: warn 级别(减少 vitest 噪音)
|
* - test: warn 级别(减少 vitest 噪音)
|
||||||
* - development: debug 级别 + ISO 时间戳(人眼友好)
|
* - development: debug 级别 + ISO 时间戳(人眼友好)
|
||||||
*/
|
*/
|
||||||
const rootLogger: PinoLogger = pino(createLoggerOptions());
|
const rootLogger: PinoLogger = pino(createLoggerOptions());
|
||||||
|
|
||||||
function createLoggerOptions(): LoggerOptions {
|
function createLoggerOptions(): LoggerOptions {
|
||||||
if (isProduction()) {
|
if (!AppEnvUtil.canOutputLogs()) {
|
||||||
return { level: "info" };
|
return { level: "silent" };
|
||||||
}
|
}
|
||||||
if (isTest()) {
|
if (AppEnvUtil.isTest()) {
|
||||||
return { level: "warn" };
|
return { level: "warn" };
|
||||||
}
|
}
|
||||||
// development
|
// development
|
||||||
|
|||||||
Reference in New Issue
Block a user