From 1db3e3ae315e8399f6eb088994cdd7e7cee5f776 Mon Sep 17 00:00:00 2001 From: chenhang Date: Wed, 10 Jun 2026 11:59:53 +0800 Subject: [PATCH] refactor(chat): align chat header and input bar with Dart source --- src/app/chat/components/chat-header.tsx | 47 +++--- .../chat/components/chat-input-bar.module.css | 41 +++--- src/app/chat/components/chat-input-bar.tsx | 80 ++++------ .../chat-input-text-field.module.css | 20 ++- .../chat/components/chat-input-text-field.tsx | 82 +++++++---- .../chat/components/chat-menu-bar.module.css | 69 --------- src/app/chat/components/chat-menu-bar.tsx | 138 ------------------ .../components/chat-more-button.module.css | 22 --- src/app/chat/components/chat-more-button.tsx | 29 ---- src/app/chat/components/chat-screen.tsx | 74 +++++----- .../components/function-button-bar.module.css | 30 ---- .../chat/components/function-button-bar.tsx | 49 ------- .../components/image-upload-button.module.css | 62 -------- .../chat/components/image-upload-button.tsx | 119 --------------- src/app/chat/components/index.ts | 6 +- .../components/pwa-install-overlay.module.css | 2 +- src/utils/storage.ts | 1 + 17 files changed, 183 insertions(+), 688 deletions(-) delete mode 100644 src/app/chat/components/chat-menu-bar.module.css delete mode 100644 src/app/chat/components/chat-menu-bar.tsx delete mode 100644 src/app/chat/components/chat-more-button.module.css delete mode 100644 src/app/chat/components/chat-more-button.tsx delete mode 100644 src/app/chat/components/function-button-bar.module.css delete mode 100644 src/app/chat/components/function-button-bar.tsx delete mode 100644 src/app/chat/components/image-upload-button.module.css delete mode 100644 src/app/chat/components/image-upload-button.tsx diff --git a/src/app/chat/components/chat-header.tsx b/src/app/chat/components/chat-header.tsx index 6fba6eac..6bf693bf 100644 --- a/src/app/chat/components/chat-header.tsx +++ b/src/app/chat/components/chat-header.tsx @@ -6,14 +6,15 @@ * * 两种模式: * - 游客模式(isGuest=true):渲染 `GuestBanner` 提示注册(点击 → /auth) - * - 登录模式(isGuest=false):渲染 `MenuButton`(点击 → 打开 ChatMenuBar) + * - 登录模式(isGuest=false):渲染 `MenuButton`(点击 → push 到 /sidebar) + * + * 注意:菜单按钮直接 push 到侧边栏路由,**不**打开下拉菜单 + * (Dart 端 `MenuButton(onTap: _goSidebar)` 行为)。 */ import { useRouter } from "next/navigation"; -import { useState } from "react"; import { ROUTES } from "@/router/routes"; -import { ChatMenuBar } from "./chat-menu-bar"; import styles from "./chat-header.module.css"; export interface ChatHeaderProps { @@ -22,7 +23,6 @@ export interface ChatHeaderProps { export function ChatHeader({ isGuest }: ChatHeaderProps) { const router = useRouter(); - const [menuOpen, setMenuOpen] = useState(false); if (isGuest) { return ( @@ -43,29 +43,20 @@ export function ChatHeader({ isGuest }: ChatHeaderProps) { } return ( - <> -
-
- {/* 菜单按钮(点击 → 打开 ChatMenuBar 浮层) */} - -
-
- - setMenuOpen(false)} - environmentName="dev" - /> - +
+
+ {/* 菜单按钮(点击 → push 到 /sidebar,与 Dart MenuButton 一致) */} + +
+
); } diff --git a/src/app/chat/components/chat-input-bar.module.css b/src/app/chat/components/chat-input-bar.module.css index f8bd14d5..4dc0a361 100644 --- a/src/app/chat/components/chat-input-bar.module.css +++ b/src/app/chat/components/chat-input-bar.module.css @@ -1,35 +1,32 @@ -/* ChatInputBar 输入栏容器样式 */ +/* ChatInputBar 输入栏容器样式(与 Dart chat_input_bar.dart 对齐) */ +/* 外层:focused 时加 padding-top + 粉色背景 + 阴影 */ .bar { flex: 0 0 auto; - padding: var(--spacing-3, 12px) var(--spacing-5, 20px) var(--spacing-5, 20px); - background: var(--color-input-bar-background, rgba(0, 0, 0, 0.4)); - backdrop-filter: blur(8px); - border-top: 1px solid var(--color-chat-input-border, rgba(255, 255, 255, 0.12)); - display: flex; - flex-direction: column; - gap: var(--spacing-2, 8px); + padding: 0 var(--spacing-lg, 16px) var(--spacing-lg, 16px); + background: transparent; + transition: padding-top 0.2s ease, background-color 0.2s ease, + box-shadow 0.2s ease; } +.barFocused { + padding-top: var(--spacing-md, 12px); + background: #feeff2; + box-shadow: 0 4px 12px var(--color-input-box-shadow, rgba(0, 0, 0, 0.1)); +} + +/* 内层:白底 + 大圆角(Dart AppRadius.radius32)+ focused 时 accent 边框 */ .row { display: flex; align-items: center; - gap: var(--spacing-2, 8px); - padding: var(--spacing-1, 4px) var(--spacing-3, 12px); - background: var(--color-bubble-background, #fff); - border-radius: 9999px; - border: 1px solid var(--color-chat-input-border, rgba(255, 255, 255, 0.2)); - transition: border-color 0.2s, box-shadow 0.2s; + gap: var(--spacing-sm, 8px); + padding: var(--spacing-sm, 8px); + background: #fff; + border-radius: 32px; + border: 1px solid transparent; + transition: border-color 0.2s ease; } .rowFocused { border-color: var(--color-accent, #f84d96); - box-shadow: 0 4px 12px var(--color-input-shadow, rgba(0, 0, 0, 0.1)); -} - -.textFieldWrap { - flex: 1 1 auto; - min-width: 0; - display: flex; - align-items: center; } diff --git a/src/app/chat/components/chat-input-bar.tsx b/src/app/chat/components/chat-input-bar.tsx index f9a132d5..e444ffde 100644 --- a/src/app/chat/components/chat-input-bar.tsx +++ b/src/app/chat/components/chat-input-bar.tsx @@ -4,25 +4,28 @@ * * 原始 Dart: lib/ui/chat/widgets/chat_input_bar.dart(121 行) * - * 组成(从左到右): - * - ChatMoreButton(⊞):展开更多选项(图片/语音/功能) - * - ChatInputTextField:文本输入 - * - ChatSendButton(↑):发送 - * - * 状态管理(自包含): + * 状态: * - input:受控文本 - * - focused:是否聚焦(视觉状态) - * - hasContent:是否有内容(决定发送按钮启用) + * - isFocused:是否聚焦(控制外层 pink-bg / box-shadow / padding-top) + * - hasContent:是否有 trim 内容(决定发送按钮是否启用) + * + * 容器层次(与 Dart 一致): + * - 外层 `.bar`:focused 时 `padding-top: md` + 粉色背景 + 阴影 + * - 内层 `.row`:白底 + `border-radius: 32` + focused 时 accent 边框 + * - Row:[ChatInputTextField (Expanded), ChatSendButton] + * + * 行为: + * - send 后清空 input + 保持 textarea 焦点(与 Dart `_focusNode.requestFocus()` 一致) + * + * 注意:Dart 源里的 `ImageUploadButton` 已被注释(`// const ImageUploadButton()`), + * `ChatMoreButton` / `FunctionButtonBar` 不存在于 Dart 源;本组件仅保留与 Dart 一致的部分。 */ -import { useState } from "react"; +import { useRef, useState } from "react"; import { useChatDispatch } from "@/stores/chat/chat-context"; import { ChatInputTextField } from "./chat-input-text-field"; -import { ChatMoreButton } from "./chat-more-button"; import { ChatSendButton } from "./chat-send-button"; -import { FunctionButtonBar } from "./function-button-bar"; -import { ImageUploadButton } from "./image-upload-button"; import styles from "./chat-input-bar.module.css"; export interface ChatInputBarProps { @@ -32,8 +35,8 @@ export interface ChatInputBarProps { export function ChatInputBar({ disabled = false }: ChatInputBarProps) { const dispatch = useChatDispatch(); const [input, setInput] = useState(""); - const [focused, setFocused] = useState(false); - const [showFunctions, setShowFunctions] = useState(false); + const [isFocused, setIsFocused] = useState(false); + const textareaRef = useRef(null); const hasContent = input.trim().length > 0; @@ -41,45 +44,24 @@ export function ChatInputBar({ disabled = false }: ChatInputBarProps) { if (!hasContent) return; dispatch({ type: "ChatSendMessage", content: input }); setInput(""); + textareaRef.current?.focus(); }; return ( -
- {showFunctions && ( - { - /* 触发:ImageUploadButton 已在 ChatInputBar 中渲染 */ - }, - }, - { key: "restart", icon: "↻", label: "Restart" }, - { key: "history", icon: "⏱", label: "History" }, - { key: "call", icon: "📞", label: "Call" }, - ]} +
+
+ + - )} - -
- - setShowFunctions((v) => !v)} /> -
setFocused(true)} - onBlur={() => setFocused(false)} - > - -
-
); diff --git a/src/app/chat/components/chat-input-text-field.module.css b/src/app/chat/components/chat-input-text-field.module.css index fb31bc62..4c0bf8bc 100644 --- a/src/app/chat/components/chat-input-text-field.module.css +++ b/src/app/chat/components/chat-input-text-field.module.css @@ -1,14 +1,28 @@ -/* ChatInputTextField 输入框样式 */ +/* ChatInputTextField 输入框样式(外层白底圆角容器 + 内部 textarea) */ +/* 外层圆角容器(与 Dart `Container(borderRadius: AppRadius.xxxl ≈ 32)` 一致) */ +.wrap { + flex: 1 1 auto; + min-width: 0; + display: flex; + align-items: center; + background: transparent; /* 容器透明,背景由 .row 提供 */ +} + +/* textarea 本身(裸元素,背景透明让父容器透出) */ .textField { + flex: 1 1 auto; + min-width: 0; width: 100%; - height: 40px; - padding: 0 var(--spacing-4, 16px); + min-height: 40px; + max-height: 120px; /* 约 5 行 × 24px line-height */ + padding: 0; border: 0; outline: none; background: transparent; color: var(--color-text-foreground, #000); font-size: var(--font-size-base, 14px); + line-height: 24px; resize: none; font-family: inherit; } diff --git a/src/app/chat/components/chat-input-text-field.tsx b/src/app/chat/components/chat-input-text-field.tsx index b0f4c881..946fa518 100644 --- a/src/app/chat/components/chat-input-text-field.tsx +++ b/src/app/chat/components/chat-input-text-field.tsx @@ -5,12 +5,22 @@ * 原始 Dart: lib/ui/chat/widgets/chat_input_text_field.dart(75 行) * * 行为: - * - 多行输入(最多 5 行) - * - Enter 直接发送(无 Shift/Ctrl) - * - Shift+Enter / Ctrl+Enter 换行 - * - 受控 + 非受控:value + onChange + * - 多行输入(min 1 行 / max 5 行 ≈ max-height 120px) + * - Enter 直接发送(无 Shift/Ctrl/Meta) + * - Shift+Enter / Ctrl+Enter / Meta+Enter 换行 + * - 外层白底圆角容器(与 Dart `Container(borderRadius: AppRadius.xxxl ≈ 32)` 一致) + * + * 通过 `forwardRef` 暴露 textarea 节点,父组件可在 send 后调用 `.focus()` + * (对齐 Dart `_focusNode.requestFocus()`)。 + * 通过 `onFocusChange` 回调通知父组件焦点状态,用于外层容器的聚焦样式切换。 */ -import { type FormEvent, type KeyboardEvent } from "react"; +import { + forwardRef, + type FormEvent, + type KeyboardEvent, + useImperativeHandle, + useRef, +} from "react"; import styles from "./chat-input-text-field.module.css"; @@ -18,19 +28,32 @@ export interface ChatInputTextFieldProps { value: string; onChange: (value: string) => void; onSubmit: () => void; + /** 焦点变化回调(用于父组件 ChatInputBar 切换聚焦态视觉) */ + onFocusChange?: (focused: boolean) => void; placeholder?: string; disabled?: boolean; autoFocus?: boolean; } -export function ChatInputTextField({ - value, - onChange, - onSubmit, - placeholder = "Say something…", - disabled = false, - autoFocus = false, -}: ChatInputTextFieldProps) { +export const ChatInputTextField = forwardRef< + HTMLTextAreaElement, + ChatInputTextFieldProps +>(function ChatInputTextField( + { + value, + onChange, + onSubmit, + onFocusChange, + placeholder = "Say something…", + disabled = false, + autoFocus = false, + }, + ref, +) { + const innerRef = useRef(null); + // 对外暴露与 forwardRef 同名的 ref(指向内部 textarea 节点) + useImperativeHandle(ref, () => innerRef.current as HTMLTextAreaElement, []); + const handleKeyDown = (e: KeyboardEvent) => { if (e.key !== "Enter") return; if (e.shiftKey || e.ctrlKey || e.metaKey) return; // 修饰键 → 换行 @@ -39,18 +62,23 @@ export function ChatInputTextField({ }; return ( -