forked from datawhale/whale-town-front
- 更新 WebSocket URL 以支持 Socket.IO 握手参数 (EIO=4) - 重构聊天面板布局,使用绝对定位和百分比锚点 - 优化输入框样式,添加装饰元素 - 修复输入框焦点释放的事件冲突问题 - 将 ChatUI 集成到主场景中 - 改进主场景容器布局设置
337 lines
10 KiB
GDScript
337 lines
10 KiB
GDScript
extends Control
|
||
|
||
# ============================================================================
|
||
# ChatUI.gd - 聊天界面控制器(Enter 显示/隐藏版本)
|
||
# ============================================================================
|
||
# 聊天系统的用户界面控制器
|
||
#
|
||
# 核心职责:
|
||
# - 显示聊天消息历史
|
||
# - 处理用户输入
|
||
# - 显示连接状态
|
||
# - Enter 显示/隐藏 + 点击外部取消输入状态 + 5秒自动隐藏
|
||
# - 只有按 Enter 才会取消倒计时
|
||
# - Call Down: 通过 EventSystem 订阅聊天事件
|
||
#
|
||
# 使用方式:
|
||
# var chat_ui := preload("res://scenes/ui/ChatUI.tscn").instantiate()
|
||
# add_child(chat_ui)
|
||
#
|
||
# 注意事项:
|
||
# - 遵循 "Signal Up, Call Down" 架构
|
||
# - 使用 @onready 缓存节点引用
|
||
# - 所有 UI 操作通过 ChatManager
|
||
# ============================================================================
|
||
|
||
# ============================================================================
|
||
# 节点引用
|
||
# ============================================================================
|
||
|
||
# 聊天面板
|
||
@onready var chat_panel: Control = %ChatPanel
|
||
|
||
# 聊天历史容器
|
||
@onready var chat_history: ScrollContainer = %ChatHistory
|
||
|
||
# 消息列表
|
||
@onready var message_list: VBoxContainer = %MessageList
|
||
|
||
# 聊天输入框
|
||
@onready var chat_input: LineEdit = %ChatInput
|
||
|
||
# ============================================================================
|
||
# 预加载资源
|
||
# ============================================================================
|
||
|
||
# 聊天消息场景
|
||
@onready var chat_message_scene: PackedScene = preload("res://scenes/prefabs/ui/ChatMessage.tscn")
|
||
|
||
# ============================================================================
|
||
# 成员变量
|
||
# ============================================================================
|
||
|
||
# 是否显示聊天框
|
||
var _is_chat_visible: bool = false
|
||
|
||
# 隐藏计时器
|
||
var _hide_timer: Timer = null
|
||
|
||
# 是否在输入中(输入时不隐藏)
|
||
var _is_typing: bool = false
|
||
|
||
# 当前用户名
|
||
var _current_username: String = ""
|
||
|
||
# ============================================================================
|
||
# 生命周期方法
|
||
# ============================================================================
|
||
|
||
# 准备就绪
|
||
func _ready() -> void:
|
||
print("ChatUI 初始化完成")
|
||
|
||
# 初始隐藏聊天框
|
||
hide_chat()
|
||
|
||
# 创建隐藏计时器
|
||
_create_hide_timer()
|
||
|
||
# 订阅事件(Call Down via EventSystem)
|
||
_subscribe_to_events()
|
||
|
||
# 连接 UI 信号
|
||
_connect_ui_signals()
|
||
|
||
# 清理
|
||
func _exit_tree() -> void:
|
||
# 取消事件订阅
|
||
if EventSystem:
|
||
EventSystem.disconnect_event(EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self)
|
||
EventSystem.disconnect_event(EventNames.CHAT_ERROR_OCCURRED, _on_chat_error, self)
|
||
EventSystem.disconnect_event(EventNames.CHAT_CONNECTION_STATE_CHANGED, _on_connection_state_changed, self)
|
||
EventSystem.disconnect_event(EventNames.CHAT_LOGIN_SUCCESS, _on_login_success, self)
|
||
|
||
# 清理计时器
|
||
if _hide_timer:
|
||
_hide_timer.queue_free()
|
||
|
||
# ============================================================================
|
||
# 输入处理
|
||
# ============================================================================
|
||
|
||
# 处理全局输入
|
||
func _input(event: InputEvent) -> void:
|
||
# 检查是否按下 Enter 键
|
||
if event is InputEventKey and event.keycode == KEY_ENTER:
|
||
_handle_enter_pressed()
|
||
|
||
# 处理 Enter 键按下
|
||
func _handle_enter_pressed() -> void:
|
||
# 如果聊天框未显示,显示它
|
||
if not _is_chat_visible:
|
||
show_chat()
|
||
chat_input.grab_focus()
|
||
return
|
||
|
||
# 如果聊天框已显示且输入框有焦点,发送消息
|
||
if chat_input.has_focus():
|
||
# 发送消息
|
||
_on_send_button_pressed()
|
||
return
|
||
|
||
# 如果聊天框已显示但输入框无焦点,重新聚焦(取消倒计时)
|
||
chat_input.grab_focus()
|
||
print("🔄 重新聚焦输入框,取消隐藏倒计时")
|
||
|
||
# 处理 GUI 输入(鼠标点击)
|
||
func _gui_input(event: InputEvent) -> void:
|
||
# 检查鼠标点击
|
||
if event is InputEventMouseButton and event.pressed:
|
||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||
_handle_click_outside()
|
||
|
||
# 处理点击聊天框外部区域
|
||
func _handle_click_outside() -> void:
|
||
# 检查点击是否在聊天面板外部
|
||
if not chat_panel.get_global_rect().has_point(get_global_mouse_position()):
|
||
# 延迟释放输入框焦点,避免事件冲突
|
||
if chat_input.has_focus():
|
||
call_deferred("_release_input_focus")
|
||
print("🖱️ 点击外部,取消输入状态")
|
||
|
||
# 延迟释放输入框焦点(由 call_deferred 调用)
|
||
func _release_input_focus() -> void:
|
||
if chat_input and chat_input.has_focus():
|
||
chat_input.release_focus()
|
||
|
||
# ============================================================================
|
||
# 显示/隐藏逻辑
|
||
# ============================================================================
|
||
|
||
# 显示聊天框
|
||
func show_chat() -> void:
|
||
if _is_chat_visible:
|
||
return
|
||
|
||
_is_chat_visible = true
|
||
chat_panel.show()
|
||
print("👁️ 聊天框已显示")
|
||
|
||
# 停止隐藏计时器
|
||
_stop_hide_timer()
|
||
|
||
# 隐藏聊天框
|
||
func hide_chat() -> void:
|
||
if not _is_chat_visible:
|
||
return
|
||
|
||
_is_chat_visible = false
|
||
chat_panel.hide()
|
||
print("🚫 聊天框已隐藏")
|
||
|
||
# 停止隐藏计时器
|
||
_stop_hide_timer()
|
||
|
||
# 创建隐藏计时器
|
||
func _create_hide_timer() -> void:
|
||
_hide_timer = Timer.new()
|
||
_hide_timer.wait_time = 5.0 # 5 秒
|
||
_hide_timer.one_shot = true
|
||
_hide_timer.timeout.connect(_on_hide_timeout)
|
||
add_child(_hide_timer)
|
||
|
||
# 开始隐藏倒计时
|
||
func _start_hide_timer() -> void:
|
||
if _is_typing:
|
||
return # 输入时不隐藏
|
||
|
||
_stop_hide_timer() # 先停止之前的计时器
|
||
_hide_timer.start()
|
||
print("⏱️ 开始 5 秒倒计时...")
|
||
|
||
# 停止隐藏倒计时
|
||
func _stop_hide_timer() -> void:
|
||
if _hide_timer:
|
||
_hide_timer.stop()
|
||
|
||
# 隐藏计时器超时
|
||
func _on_hide_timeout() -> void:
|
||
hide_chat()
|
||
|
||
# ============================================================================
|
||
# UI 事件处理
|
||
# ============================================================================
|
||
|
||
# 连接 UI 信号
|
||
func _connect_ui_signals() -> void:
|
||
# 输入框回车
|
||
chat_input.text_submitted.connect(_on_chat_input_submitted)
|
||
|
||
# 输入框焦点变化
|
||
chat_input.focus_entered.connect(_on_input_focus_entered)
|
||
chat_input.focus_exited.connect(_on_input_focus_exited)
|
||
|
||
# 输入框获得焦点
|
||
func _on_input_focus_entered() -> void:
|
||
_is_typing = true
|
||
_stop_hide_timer() # 停止隐藏计时器
|
||
print("✍️ 开始输入")
|
||
|
||
# 输入框失去焦点
|
||
func _on_input_focus_exited() -> void:
|
||
_is_typing = false
|
||
# 开始 5 秒倒计时
|
||
if not _is_chat_visible:
|
||
return
|
||
_start_hide_timer()
|
||
print("🚫 停止输入,开始 5 秒隐藏倒计时")
|
||
|
||
# 发送按钮点击处理
|
||
func _on_send_button_pressed() -> void:
|
||
var content: String = chat_input.text.strip_edges()
|
||
|
||
if content.is_empty():
|
||
return
|
||
|
||
# 发送消息
|
||
ChatManager.send_chat_message(content, "local")
|
||
|
||
# 清空输入框
|
||
chat_input.clear()
|
||
|
||
# 重新聚焦输入框
|
||
chat_input.grab_focus()
|
||
|
||
# 聊天输入提交(回车键)处理
|
||
func _on_chat_input_submitted(text: String) -> void:
|
||
_on_send_button_pressed()
|
||
|
||
# ============================================================================
|
||
# 事件订阅(Call Down)
|
||
# ============================================================================
|
||
|
||
# 订阅事件
|
||
func _subscribe_to_events() -> void:
|
||
# 订阅聊天消息接收事件
|
||
EventSystem.connect_event(EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self)
|
||
|
||
# 订阅聊天错误事件
|
||
EventSystem.connect_event(EventNames.CHAT_ERROR_OCCURRED, _on_chat_error, self)
|
||
|
||
# 订阅连接状态变化事件
|
||
EventSystem.connect_event(EventNames.CHAT_CONNECTION_STATE_CHANGED, _on_connection_state_changed, self)
|
||
|
||
# 订阅登录成功事件
|
||
EventSystem.connect_event(EventNames.CHAT_LOGIN_SUCCESS, _on_login_success, self)
|
||
|
||
# ============================================================================
|
||
# 事件处理器
|
||
# ============================================================================
|
||
|
||
# 处理接收到的聊天消息
|
||
func _on_chat_message_received(data: Dictionary) -> void:
|
||
var from_user: String = data.get("from_user", "")
|
||
var content: String = data.get("content", "")
|
||
var timestamp: float = data.get("timestamp", 0.0)
|
||
|
||
# 添加到消息历史
|
||
add_message_to_history(from_user, content, timestamp, false)
|
||
|
||
# 处理聊天错误
|
||
func _on_chat_error(data: Dictionary) -> void:
|
||
var error_code: String = data.get("error_code", "")
|
||
var message: String = data.get("message", "")
|
||
|
||
print("❌ ChatUI 错误: [", error_code, "] ", message)
|
||
|
||
# 处理连接状态变化
|
||
func _on_connection_state_changed(data: Dictionary) -> void:
|
||
# 连接状态变化处理(当前不更新UI)
|
||
pass
|
||
|
||
# 处理登录成功
|
||
func _on_login_success(data: Dictionary) -> void:
|
||
_current_username = data.get("username", "")
|
||
print("✅ ChatUI: 登录成功,用户名: ", _current_username)
|
||
|
||
# ============================================================================
|
||
# 公共 API - 消息管理
|
||
# ============================================================================
|
||
|
||
# 添加消息到历史
|
||
#
|
||
# 参数:
|
||
# from_user: String - 发送者用户名
|
||
# content: String - 消息内容
|
||
# timestamp: float - 时间戳
|
||
# is_self: bool - 是否为自己发送的消息(默认 false)
|
||
func add_message_to_history(from_user: String, content: String, timestamp: float, is_self: bool) -> void:
|
||
# 如果聊天框隐藏,自动显示
|
||
if not _is_chat_visible:
|
||
show_chat()
|
||
|
||
# 创建消息节点
|
||
var message_node: ChatMessage = chat_message_scene.instantiate()
|
||
|
||
# 设置消息内容
|
||
message_node.set_message(from_user, content, timestamp, is_self)
|
||
|
||
# 添加到列表
|
||
message_list.add_child(message_node)
|
||
|
||
# 自动滚动到底部
|
||
call_deferred("_scroll_to_bottom")
|
||
|
||
# ============================================================================
|
||
# 内部方法 - UI 更新
|
||
# ============================================================================
|
||
|
||
# 滚动到底部
|
||
func _scroll_to_bottom() -> void:
|
||
# 等待一帧,确保 UI 更新完成
|
||
await get_tree().process_frame
|
||
|
||
# 滚动到底部
|
||
if is_instance_valid(chat_history):
|
||
chat_history.scroll_vertical = chat_history.get_v_scroll_bar().max_value
|