fix(chat): 调整输入交互并加固 WS/告警处理

This commit is contained in:
2026-03-11 18:11:09 +08:00
parent dc403179f7
commit 558a0ff9bc
12 changed files with 315 additions and 44 deletions

View File

@@ -1,7 +1,7 @@
extends Control
# ============================================================================
# ChatUI.gd - 聊天界面控制器(Enter 显示/隐藏版本
# ChatUI.gd - 聊天界面控制器(T 唤起 / Enter 发送
# ============================================================================
# 聊天系统的用户界面控制器
#
@@ -9,8 +9,10 @@ extends Control
# - 显示聊天消息历史
# - 处理用户输入
# - 显示连接状态
# - Enter 显示/隐藏 + 点击外部取消输入状态 + 5秒自动隐藏
# - 只有按 Enter 才会取消倒计时
# - T 唤起聊天框Enter 发送消息
# - 点击聊天框外部隐藏聊天框
# - 失焦后 5 秒自动隐藏
# - 显示/隐藏带 0.5s 过渡动画
# - Call Down: 通过 EventSystem 订阅聊天事件
#
# 使用方式:
@@ -46,6 +48,8 @@ extends Control
# 聊天消息场景
@onready var chat_message_scene: PackedScene = preload("res://scenes/prefabs/ui/ChatMessage.tscn")
const CHAT_TRANSITION_DURATION: float = 0.5
# ============================================================================
# 成员变量
# ============================================================================
@@ -59,6 +63,9 @@ var _hide_timer: Timer = null
# 是否在输入中(输入时不隐藏)
var _is_typing: bool = false
# 显示/隐藏过渡动画
var _transition_tween: Tween = null
# 当前用户名
var _current_username: String = ""
@@ -69,7 +76,7 @@ var _current_username: String = ""
# 准备就绪
func _ready() -> void:
# 初始隐藏聊天框
hide_chat()
hide_chat(true)
# 创建隐藏计时器
_create_hide_timer()
@@ -80,6 +87,9 @@ func _ready() -> void:
# 连接 UI 信号
_connect_ui_signals()
# 尽可能保持回车提交后输入框继续编辑状态Godot 4.6+ 支持该属性)
_enable_keep_editing_on_submit()
# 清理
func _exit_tree() -> void:
# 取消事件订阅
@@ -93,30 +103,48 @@ func _exit_tree() -> void:
if _hide_timer:
_hide_timer.queue_free()
if is_instance_valid(_transition_tween):
_transition_tween.kill()
_transition_tween = null
# ============================================================================
# 输入处理
# ============================================================================
# 处理全局输入
func _input(event: InputEvent) -> void:
# 检查是否按下 Enter 键
if event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER):
if not (event is InputEventKey):
return
var key_event := event as InputEventKey
if not key_event.pressed or key_event.echo:
return
# T 键用于唤起聊天(输入框聚焦时不拦截)
if key_event.keycode == KEY_T and not chat_input.has_focus():
_handle_t_pressed()
return
# Enter 键用于发送聊天
if key_event.keycode == KEY_ENTER or key_event.keycode == KEY_KP_ENTER:
_handle_enter_pressed()
# 处理 T 键按下
func _handle_t_pressed() -> void:
if not _is_chat_visible:
show_chat()
# 延迟聚焦,避免同一输入事件周期冲突
call_deferred("_grab_input_focus")
# 处理 Enter 键按下
func _handle_enter_pressed() -> void:
# 如果聊天框未显示,显示它
# 聊天框未显示时不处理 Enter
if not _is_chat_visible:
show_chat()
# 使用 call_deferred 避免在同一个事件周期内触发 LineEdit 的 text_submitted 信号
call_deferred("_grab_input_focus")
return
# 如果聊天框已显示且输入框有焦点,检查输入框内容
# 输入框有焦点时,发送逻辑交给 LineEdit 的 text_submitted 统一处理
if chat_input.has_focus():
# 如果输入框有内容,发送消息
if not chat_input.text.is_empty():
_on_send_button_pressed()
return
# 如果聊天框已显示但输入框无焦点,重新聚焦(取消倒计时)
@@ -136,11 +164,12 @@ func _gui_input(event: InputEvent) -> void:
# 处理点击聊天框外部区域
func _handle_click_outside() -> void:
if not _is_chat_visible:
return
# 检查点击是否在聊天面板外部
if not chat_panel.get_global_rect().has_point(get_global_mouse_position()):
# 延迟释放输入框焦点,避免事件冲突
if chat_input.has_focus():
call_deferred("_release_input_focus")
hide_chat()
# 延迟释放输入框焦点(由 call_deferred 调用)
func _release_input_focus() -> void:
@@ -152,20 +181,32 @@ func _release_input_focus() -> void:
# ============================================================================
# 显示聊天框
func show_chat() -> void:
func show_chat(immediate: bool = false) -> void:
_is_chat_visible = true
if is_instance_valid(chat_panel):
chat_panel.show()
# 停止隐藏计时器
_stop_hide_timer()
if not is_instance_valid(chat_panel):
return
_stop_transition_tween()
chat_panel.show()
if immediate:
chat_panel.modulate.a = 1.0
return
chat_panel.modulate.a = 0.0
_transition_tween = create_tween()
_transition_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
_transition_tween.tween_property(chat_panel, "modulate:a", 1.0, CHAT_TRANSITION_DURATION)
_transition_tween.finished.connect(_on_show_transition_finished)
# 隐藏聊天框
func hide_chat() -> void:
func hide_chat(immediate: bool = false) -> void:
_is_chat_visible = false
_is_typing = false
if is_instance_valid(chat_panel):
chat_panel.hide()
if is_instance_valid(chat_input) and chat_input.has_focus():
chat_input.release_focus()
@@ -173,6 +214,25 @@ func hide_chat() -> void:
# 停止隐藏计时器
_stop_hide_timer()
if not is_instance_valid(chat_panel):
return
_stop_transition_tween()
if immediate:
chat_panel.hide()
chat_panel.modulate.a = 1.0
return
if not chat_panel.visible:
chat_panel.modulate.a = 1.0
return
_transition_tween = create_tween()
_transition_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
_transition_tween.tween_property(chat_panel, "modulate:a", 0.0, CHAT_TRANSITION_DURATION)
_transition_tween.finished.connect(_on_hide_transition_finished)
# 创建隐藏计时器
func _create_hide_timer() -> void:
_hide_timer = Timer.new()
@@ -202,6 +262,20 @@ func _stop_hide_timer() -> void:
func _on_hide_timeout() -> void:
hide_chat()
func _on_show_transition_finished() -> void:
_transition_tween = null
func _on_hide_transition_finished() -> void:
_transition_tween = null
if not _is_chat_visible and is_instance_valid(chat_panel):
chat_panel.hide()
chat_panel.modulate.a = 1.0
func _stop_transition_tween() -> void:
if is_instance_valid(_transition_tween):
_transition_tween.kill()
_transition_tween = null
# ============================================================================
# UI 事件处理
# ============================================================================
@@ -243,12 +317,30 @@ func _on_send_button_pressed() -> void:
# 清空输入框
chat_input.clear()
# 重新聚焦输入框
# 发送后延迟重新聚焦,避免被 LineEdit 的提交事件在同一帧内抢走焦点
call_deferred("_focus_input_after_send")
func _focus_input_after_send() -> void:
if not _is_chat_visible:
return
if not is_instance_valid(chat_input):
return
chat_input.grab_focus()
# 聊天输入提交(回车键)处理
func _on_chat_input_submitted(text: String) -> void:
func _on_chat_input_submitted(_text: String) -> void:
_on_send_button_pressed()
# 即便提交的是空串,也保持输入焦点,便于连续输入
call_deferred("_focus_input_after_send")
func _enable_keep_editing_on_submit() -> void:
if not is_instance_valid(chat_input):
return
for property in chat_input.get_property_list():
if property.get("name", "") == "keep_editing_on_text_submit":
chat_input.set("keep_editing_on_text_submit", true)
return
# ============================================================================
# 事件订阅Call Down
@@ -290,10 +382,11 @@ func _on_chat_error(data: Dictionary) -> void:
var error_code: String = data.get("error_code", "")
var message: String = data.get("message", "")
push_error("ChatUI: [%s] %s" % [error_code, message])
# 聊天发送失败通常是业务校验(频率/内容)导致,不应按引擎级错误处理。
push_warning("ChatUI: [%s] %s" % [error_code, message])
# 处理连接状态变化
func _on_connection_state_changed(data: Dictionary) -> void:
func _on_connection_state_changed(_data: Dictionary) -> void:
# 连接状态变化处理当前不更新UI
pass
@@ -319,7 +412,6 @@ func add_message_to_history(from_user: String, content: String, timestamp: float
# 每条消息用一行容器包起来,方便左右对齐且不挤在一起
var row := HBoxContainer.new()
row.layout_mode = 2 # 让 VBoxContainer 接管布局,否则会重叠在同一位置
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
row.alignment = BoxContainer.ALIGNMENT_END if is_self else BoxContainer.ALIGNMENT_BEGIN