refactor(chat): remove QuotaDialog and related state/effects
Drop the QuotaDialog component invocation in chat-screen along with its useState/useEffect wiring, and delete the component files. Update JSDoc in chat-screen and routes to reflect the removed consumer. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,14 +7,12 @@
|
|||||||
* 职责(本文件):
|
* 职责(本文件):
|
||||||
* - **订阅 auth 状态机**(loginStatus)—— 派生 isGuest / **派鉴权生命周期事件**给 chat 机器
|
* - **订阅 auth 状态机**(loginStatus)—— 派生 isGuest / **派鉴权生命周期事件**给 chat 机器
|
||||||
* - 屏幕生命周期事件(init / visible / invisible)
|
* - 屏幕生命周期事件(init / visible / invisible)
|
||||||
* - 配额告警监听(**仅**游客 —— "非游客无消息限制"业务事实)
|
|
||||||
* - **不**直接管理 WebSocket / **不**直接读 AuthStorage(除 token 取值)
|
* - **不**直接管理 WebSocket / **不**直接读 AuthStorage(除 token 取值)
|
||||||
*
|
*
|
||||||
* 子组件职责:
|
* 子组件职责:
|
||||||
* - ChatHeader:顶部栏(游客 banner / 菜单按钮)
|
* - ChatHeader:顶部栏(游客 banner / 菜单按钮)
|
||||||
* - ChatArea:消息列表
|
* - ChatArea:消息列表
|
||||||
* - ChatInputBar:输入栏(文字 + 发送 + More)
|
* - ChatInputBar:输入栏(文字 + 发送 + More)
|
||||||
* - QuotaDialog:配额告警弹窗(**仅**游客)
|
|
||||||
* - PwaInstallOverlay:PWA 安装提示触发器
|
* - PwaInstallOverlay:PWA 安装提示触发器
|
||||||
* - BrowserHintOverlay:浏览器提示
|
* - BrowserHintOverlay:浏览器提示
|
||||||
*
|
*
|
||||||
@@ -26,14 +24,13 @@
|
|||||||
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
* - `ChatLogout` → 登出(机器自动 cleanup WS actor)
|
||||||
* - chat 机器**内部**用 fromCallback actor 完整管理 WS 生命周期
|
* - chat 机器**内部**用 fromCallback actor 完整管理 WS 生命周期
|
||||||
*/
|
*/
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import { useAuthState } from "@/stores/auth/auth-context";
|
import { useAuthState } from "@/stores/auth/auth-context";
|
||||||
import type { LoginStatus } from "@/models/auth/login-status";
|
import type { LoginStatus } from "@/models/auth/login-status";
|
||||||
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
import { AuthStorage } from "@/data/storage/auth/auth_storage";
|
||||||
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
import { useChatDispatch, useChatState } from "@/stores/chat/chat-context";
|
||||||
import { GuestChatQuota } from "@/stores/chat";
|
|
||||||
|
|
||||||
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
import { MobileShell } from "@/app/_components/core/mobile-shell";
|
||||||
|
|
||||||
@@ -42,7 +39,6 @@ import { ChatArea } from "./chat-area";
|
|||||||
import { ChatHeader } from "./chat-header";
|
import { ChatHeader } from "./chat-header";
|
||||||
import { ChatInputBar } from "./chat-input-bar";
|
import { ChatInputBar } from "./chat-input-bar";
|
||||||
import { PwaInstallOverlay } from "./pwa-install-overlay";
|
import { PwaInstallOverlay } from "./pwa-install-overlay";
|
||||||
import { QuotaDialog } from "./quota-dialog";
|
|
||||||
import styles from "./chat-screen.module.css";
|
import styles from "./chat-screen.module.css";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,9 +84,6 @@ export function ChatScreen() {
|
|||||||
// isGuest 派生(chat-screen 是**唯一**知道这个值的地方 —— chat 机器无 isGuest 字段)
|
// isGuest 派生(chat-screen 是**唯一**知道这个值的地方 —— chat 机器无 isGuest 字段)
|
||||||
const isGuest = deriveIsGuest(authState.loginStatus);
|
const isGuest = deriveIsGuest(authState.loginStatus);
|
||||||
|
|
||||||
const [quotaDialogOpen, setQuotaDialogOpen] = useState(false);
|
|
||||||
const [quotaExhausted, setQuotaExhausted] = useState(false);
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
// 屏幕生命周期 + 派初次鉴权生命周期事件
|
// 屏幕生命周期 + 派初次鉴权生命周期事件
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
@@ -121,36 +114,6 @@ export function ChatScreen() {
|
|||||||
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
dispatchAuthLifecycle(authState.loginStatus, chatDispatch);
|
||||||
}, [authState.loginStatus, chatDispatch]);
|
}, [authState.loginStatus, chatDispatch]);
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
// 配额耗尽触发器(**仅**游客 —— "非游客无消息限制")
|
|
||||||
// ─────────────────────────────────────────────────────────────
|
|
||||||
const prevTriggerRef = useRef(state.quotaExceededTrigger);
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isGuest) return; // ← 防御:非游客 backend 不会发 ChatQuotaExceeded
|
|
||||||
if (
|
|
||||||
state.quotaExceededTrigger !== prevTriggerRef.current &&
|
|
||||||
state.quotaExceededTrigger > 0
|
|
||||||
) {
|
|
||||||
setQuotaExhausted(true);
|
|
||||||
setQuotaDialogOpen(true);
|
|
||||||
}
|
|
||||||
prevTriggerRef.current = state.quotaExceededTrigger;
|
|
||||||
}, [state.quotaExceededTrigger, isGuest]);
|
|
||||||
|
|
||||||
// 配额警告(剩余 = 5)—— **仅**游客
|
|
||||||
const prevRemainingRef = useRef(state.guestRemainingQuota);
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
isGuest && // ← 派生自 auth.loginStatus
|
|
||||||
state.guestRemainingQuota === GuestChatQuota.warningThreshold &&
|
|
||||||
prevRemainingRef.current !== state.guestRemainingQuota
|
|
||||||
) {
|
|
||||||
setQuotaExhausted(false);
|
|
||||||
setQuotaDialogOpen(true);
|
|
||||||
}
|
|
||||||
prevRemainingRef.current = state.guestRemainingQuota;
|
|
||||||
}, [state.guestRemainingQuota, isGuest]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MobileShell>
|
<MobileShell>
|
||||||
<div className={styles.shell}>
|
<div className={styles.shell}>
|
||||||
@@ -183,13 +146,6 @@ export function ChatScreen() {
|
|||||||
|
|
||||||
{/* PWA 安装提示触发器(无可见 UI,3.5s 后弹 dialog) */}
|
{/* PWA 安装提示触发器(无可见 UI,3.5s 后弹 dialog) */}
|
||||||
<PwaInstallOverlay />
|
<PwaInstallOverlay />
|
||||||
|
|
||||||
{/* 配额告警弹窗(**仅**游客,gate 在 isGuest) */}
|
|
||||||
<QuotaDialog
|
|
||||||
open={quotaDialogOpen}
|
|
||||||
onClose={() => setQuotaDialogOpen(false)}
|
|
||||||
isExhausted={quotaExhausted}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
/* QuotaDialog 配额提示弹窗样式 */
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
width: calc(100% - 40px);
|
|
||||||
max-width: 360px;
|
|
||||||
background: var(--color-surface, #1f1a2e);
|
|
||||||
border-radius: var(--radius-lg, 12px);
|
|
||||||
padding: var(--spacing-5, 20px);
|
|
||||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 32px;
|
|
||||||
margin-bottom: var(--spacing-4, 16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconExhausted {
|
|
||||||
background: rgba(255, 80, 80, 0.2);
|
|
||||||
color: #ff5050;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconWarning {
|
|
||||||
background: rgba(248, 77, 150, 0.2);
|
|
||||||
color: var(--color-accent, #f84d96);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: var(--font-size-lg, 18px);
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0 0 var(--spacing-2, 8px);
|
|
||||||
color: var(--color-text-primary, #fff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
font-size: var(--font-size-sm, 14px);
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--color-text-secondary, #9e9e9e);
|
|
||||||
margin: 0 0 var(--spacing-5, 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-3, 12px);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
height: 40px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: var(--radius-md, 8px);
|
|
||||||
font-size: var(--font-size-base, 14px);
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnSecondary {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--color-text-primary, #fff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnPrimary {
|
|
||||||
background: var(--color-accent, #f84d96);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"use client";
|
|
||||||
/**
|
|
||||||
* QuotaDialog 配额提示弹窗
|
|
||||||
*
|
|
||||||
* 原始 Dart: lib/ui/chat/widgets/quota_dialog.dart(130 行)
|
|
||||||
*
|
|
||||||
* 两种模式:
|
|
||||||
* - 警告模式(isExhausted=false):剩余配额 < 5 时,提示用户登录
|
|
||||||
* - 耗尽模式(isExhausted=true):配额已用完,强制登录
|
|
||||||
*/
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
|
|
||||||
import { ROUTES } from "@/router/routes";
|
|
||||||
|
|
||||||
import styles from "./quota-dialog.module.css";
|
|
||||||
|
|
||||||
export interface QuotaDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
isExhausted: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function QuotaDialog({ open, onClose, isExhausted }: QuotaDialogProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
if (!open) return null;
|
|
||||||
|
|
||||||
const title = isExhausted
|
|
||||||
? "Daily quota exhausted"
|
|
||||||
: "Login for unlimited access!";
|
|
||||||
|
|
||||||
const content = isExhausted
|
|
||||||
? "Login to continue chatting~"
|
|
||||||
: "Dear, sign up now and all your chats with Elio will be saved forever. In guest mode, your chat history will be deleted when you leave, and Elio will forget you.";
|
|
||||||
|
|
||||||
const handleLogin = () => {
|
|
||||||
onClose();
|
|
||||||
router.push(ROUTES.auth);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.overlay}
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="quota-dialog-title"
|
|
||||||
>
|
|
||||||
<div className={styles.dialog}>
|
|
||||||
<div
|
|
||||||
className={`${styles.icon} ${isExhausted ? styles.iconExhausted : styles.iconWarning}`}
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{isExhausted ? "🚫" : "⚠️"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 id="quota-dialog-title" className={styles.title}>
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
<p className={styles.content}>{content}</p>
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${styles.btn} ${styles.btnSecondary}`}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${styles.btn} ${styles.btnPrimary}`}
|
|
||||||
onClick={handleLogin}
|
|
||||||
>
|
|
||||||
Go to Login
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
* - 动态深链通过 `ROUTE_BUILDERS` 函数生成,避免手拼字符串。
|
* - 动态深链通过 `ROUTE_BUILDERS` 函数生成,避免手拼字符串。
|
||||||
*
|
*
|
||||||
* 注意:proxy 路由跳转逻辑(2026-06-15 迁出),但本文件的**类型化常量**仍在
|
* 注意:proxy 路由跳转逻辑(2026-06-15 迁出),但本文件的**类型化常量**仍在
|
||||||
* 被 9 个组件(auth-screen / chat-header / quota-dialog / DeepLinkPersist /
|
* 被 8 个组件(auth-screen / chat-header / DeepLinkPersist /
|
||||||
* error / not-found / page / sidebar-screen / splash-screen)使用 —— **不能**整体注释。
|
* error / not-found / page / sidebar-screen / splash-screen)使用 —— **不能**整体注释。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user