Skip to content

Commit 2432e2d

Browse files
authored
[0203] 增加侧边栏收缩功能 (#3390)
1 parent dadfa23 commit 2432e2d

3 files changed

Lines changed: 241 additions & 43 deletions

File tree

devel/0203.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# [0203] Chat Tab 侧边栏收起/展开功能
2+
3+
## 相关文档
4+
- [docs/demand.md](../docs/demand.md) - Chat Tab UI 需求文档与实现方案
5+
6+
## 任务相关的代码文件
7+
- `src/Plugins/Qt/qt_chat_tab_widget.hpp`
8+
- `src/Plugins/Qt/qt_chat_tab_widget.cpp`
9+
10+
## 如何测试
11+
12+
### 确定性测试(单元测试)
13+
暂无单元测试。
14+
15+
### 非确定性测试(文档验证)
16+
1. 启动 Mogan STEM,切换到 Chat Tab
17+
2. 观察左侧边栏默认处于展开状态,底部"收缩"按钮可见
18+
3. 点击底部"收缩"按钮,确认:
19+
- 侧边栏立即缩为窄条(48px 宽)
20+
- 窄条底部显示一个 "<" 展开按钮,原有内容(标题、会话列表等)不可见
21+
4. 点击窄条底部的 "<" 按钮,确认:
22+
- 侧边栏立即恢复原有宽度
23+
- 原有内容(标题、会话列表等)重新显示,窄条隐藏
24+
5. 在展开/收起状态下切换会话、发送消息,确认右侧内容区布局正常
25+
26+
## 如何提交
27+
28+
提交前执行以下最少步骤:
29+
30+
```bash
31+
xmake b stem
32+
```
33+
34+
确认编译通过后提交并推送:
35+
36+
```bash
37+
git add -A
38+
git commit -m "[0203] Chat Tab 侧边栏收起/展开功能"
39+
git push origin hongwei/0202/chat-tab
40+
```
41+
42+
## What
43+
44+
为 Chat Tab 的左侧边栏实现收起/展开功能。收起时 sidebar 缩为一小条窄栏(底部仅含展开按钮),展开时恢复完整内容,切换无动画(避免卡顿)。
45+
46+
修改内容:
47+
1. `qt_chat_tab_widget.hpp`
48+
- 新增 `collapseButton_` 成员,引用展开态底部的收缩按钮
49+
- 新增 `sidebarNormalContent_` 成员,展开时的内容容器
50+
- 新增 `sidebarCollapsedBar_` 成员,收起时的窄条容器
51+
- 新增 `sidebarCollapsed_` 标志,记录当前收起状态
52+
- 新增 `sidebarExpandedWidth_` 成员,保存展开时的原始宽度
53+
- 新增 `toggle_sidebar()` 私有方法
54+
55+
2. `qt_chat_tab_widget.cpp`
56+
- 新增常量 `kSidebarCollapsedWidth = 48`,定义窄条宽度
57+
- 构造函数中初始化新成员,并在创建侧边栏后记录 `sidebarExpandedWidth_`
58+
- 重构 `setup_left_sidebar()`:将原有内容包进 `sidebarNormalContent_` 容器,收缩按钮移至底部;新增 `sidebarCollapsedBar_` 窄条容器(底部含 "<" 展开按钮),默认隐藏
59+
- 新增 `toggle_sidebar()` 实现:直接切换两个容器的可见性并 `setFixedWidth`,无动画
60+
61+
## Why
62+
63+
需求文档中明确要求侧边栏支持收起/展开(P1 优先级),以提升 Chat Tab 在窄屏或小窗口下的可用性,让用户可以把更多可视区域留给聊天内容。
64+
65+
关键设计决策:
66+
1. 收起后 sidebar 不完全消失,而是缩为一小条窄栏,窄栏底部保留展开按钮——用户始终知道侧边栏的位置,可以随时点回来
67+
2. 收缩/展开按钮放在 sidebar 底部而非顶部——符合"设置/系统操作放底部"的常见 UI 惯例
68+
3. 不使用过渡动画——动画在当前实现中会造成卡顿感,直接切换更干脆
69+
70+
## How
71+
72+
### 双容器结构
73+
74+
sidebar 内部维护两个互斥的 QWidget 容器:
75+
76+
```
77+
sidebarWidget_ (sidebarLayout)
78+
├── sidebarNormalContent_ ← 展开时显示
79+
│ ├── "Chat" 标题
80+
│ ├── "New chat" 按钮
81+
│ ├── "Conversations (N)" 标签
82+
│ ├── 会话列表
83+
│ ├── stretch
84+
│ └── "收缩" 按钮(底部)
85+
└── sidebarCollapsedBar_ ← 收起时显示
86+
├── stretch
87+
└── "<" 按钮(底部)
88+
```
89+
90+
### 切换逻辑
91+
92+
```cpp
93+
void
94+
QTChatTabWidget::toggle_sidebar () {
95+
if (!sidebarWidget_) return;
96+
97+
if (sidebarCollapsed_) {
98+
// 展开:显示正常内容,隐藏窄条,恢复宽度
99+
if (sidebarNormalContent_) sidebarNormalContent_->show ();
100+
if (sidebarCollapsedBar_) sidebarCollapsedBar_->hide ();
101+
sidebarWidget_->setFixedWidth (sidebarExpandedWidth_);
102+
sidebarCollapsed_= false;
103+
}
104+
else {
105+
// 收起:隐藏正常内容,显示窄条,缩为窄条
106+
if (sidebarNormalContent_) sidebarNormalContent_->hide ();
107+
if (sidebarCollapsedBar_) sidebarCollapsedBar_->show ();
108+
sidebarWidget_->setFixedWidth (DpiUtils::scaled (kSidebarCollapsedWidth));
109+
sidebarCollapsed_= true;
110+
}
111+
}
112+
```
113+
114+
## 影响范围确认
115+
116+
本次修改仅涉及 `QTChatTabWidget` 类的内部实现,不影响:
117+
- 会话创建、切换、发送消息等核心逻辑
118+
- Scheme 层 API
119+
- 其他 Qt 插件或窗口组件
120+
121+
唯一的外部可见变化是侧边栏底部"收缩"按钮从无效状态变为可交互状态,点击后 sidebar 缩为窄条并在底部显示展开按钮。

src/Plugins/Qt/qt_chat_tab_widget.cpp

Lines changed: 102 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ is_empty_document_body (tree body) {
114114

115115
/// 左侧边栏最小宽度(像素)。
116116
constexpr int kSidebarMinWidth= 200;
117+
/// 左侧边栏收起后的窄条宽度(像素)。
118+
constexpr int kSidebarCollapsedWidth= 48;
117119
/// 边栏布局水平边距。
118120
constexpr int kSidebarMarginX= 12;
119121
/// 边栏布局垂直边距。
@@ -201,9 +203,12 @@ QTChatTabWidget::QTChatTabWidget (QWidget* parent)
201203
: QWidget (parent), sidebarWidget_ (nullptr), contentWidget_ (nullptr),
202204
conversationCountLabel_ (nullptr), conversationListWidget_ (nullptr),
203205
conversationListLayout_ (nullptr), newChatButton_ (nullptr),
204-
conversationStack_ (nullptr), activeConversation_ (nullptr),
205-
nextConversationTitleId_ (1), chatMenuToolBar_ (nullptr),
206-
chatModeToolBar_ (nullptr), chatFocusToolBar_ (nullptr) {
206+
collapseButton_ (nullptr), sidebarNormalContent_ (nullptr),
207+
sidebarCollapsedBar_ (nullptr), conversationStack_ (nullptr),
208+
activeConversation_ (nullptr), nextConversationTitleId_ (1),
209+
sidebarCollapsed_ (false), sidebarExpandedWidth_ (0),
210+
chatMenuToolBar_ (nullptr), chatModeToolBar_ (nullptr),
211+
chatFocusToolBar_ (nullptr) {
207212
setFocusPolicy (Qt::StrongFocus);
208213

209214
QHBoxLayout* mainLayout= new QHBoxLayout (this);
@@ -227,8 +232,9 @@ QTChatTabWidget::QTChatTabWidget (QWidget* parent)
227232
setup_left_sidebar (sidebarLayout);
228233
sidebar->adjustSize ();
229234
const int contentWidth= sidebar->sizeHint ().width ();
230-
sidebar->setFixedWidth (
231-
qMax (DpiUtils::scaled (kSidebarMinWidth), contentWidth));
235+
sidebarExpandedWidth_=
236+
qMax (DpiUtils::scaled (kSidebarMinWidth), contentWidth);
237+
sidebar->setFixedWidth (sidebarExpandedWidth_);
232238
mainLayout->addWidget (sidebar);
233239

234240
// 右侧内容区
@@ -251,34 +257,21 @@ QTChatTabWidget::~QTChatTabWidget () {
251257
*/
252258
void
253259
QTChatTabWidget::setup_left_sidebar (QVBoxLayout* sidebarLayout) {
254-
// 顶部行:标题 + 收缩按钮
255-
QHBoxLayout* headerLayout= new QHBoxLayout ();
256-
headerLayout->setSpacing (0);
257-
258-
QLabel* navTitle= new QLabel ("Chat", this);
260+
// ---- 展开态:正常内容容器 ----
261+
QWidget* normalContent= new QWidget (sidebarWidget_);
262+
normalContent->setObjectName ("chat-tab-sidebar-normal");
263+
QVBoxLayout* normalLayout= new QVBoxLayout (normalContent);
264+
normalLayout->setContentsMargins (0, 0, 0, 0);
265+
normalLayout->setSpacing (DpiUtils::scaled (kSidebarSpacing));
266+
267+
// 顶部标题
268+
QLabel* navTitle= new QLabel ("Chat", normalContent);
259269
navTitle->setObjectName ("chat-tab-nav-title");
260270
DpiUtils::applyScaledFont (navTitle, kNavTitleFontPx);
261-
headerLayout->addWidget (navTitle);
262-
263-
headerLayout->addStretch ();
264-
265-
QPushButton* collapseBtn= new QPushButton ("收缩", this);
266-
collapseBtn->setObjectName ("chat-tab-collapse-btn");
267-
collapseBtn->setFocusPolicy (Qt::NoFocus);
268-
collapseBtn->setCursor (Qt::PointingHandCursor);
269-
DpiUtils::applyScaledFont (collapseBtn, kCollapseFontPx);
270-
collapseBtn->setStyleSheet (
271-
QString ("QPushButton { border: 1px solid #cccccc; border-radius: %1px; "
272-
"padding: %2px %3px; background-color: #ffffff; }")
273-
.arg (DpiUtils::scaled (kCollapseBorderRadius))
274-
.arg (DpiUtils::scaled (kCollapsePadY))
275-
.arg (DpiUtils::scaled (kCollapsePadX)));
276-
headerLayout->addWidget (collapseBtn);
277-
278-
sidebarLayout->addLayout (headerLayout);
271+
normalLayout->addWidget (navTitle);
279272

280273
// New chat 按钮
281-
newChatButton_= new QPushButton ("New chat", this);
274+
newChatButton_= new QPushButton ("New chat", normalContent);
282275
newChatButton_->setObjectName ("chat-tab-new-btn");
283276
newChatButton_->setFocusPolicy (Qt::NoFocus);
284277
newChatButton_->setCursor (Qt::PointingHandCursor);
@@ -288,22 +281,71 @@ QTChatTabWidget::setup_left_sidebar (QVBoxLayout* sidebarLayout) {
288281
.arg (DpiUtils::scaled (kNavButtonPadX)));
289282
connect (newChatButton_, &QPushButton::clicked, this,
290283
[this] () { create_new_conversation (); });
291-
sidebarLayout->addWidget (newChatButton_);
284+
normalLayout->addWidget (newChatButton_);
292285

293-
conversationCountLabel_= new QLabel ("Conversations (0)", this);
286+
conversationCountLabel_= new QLabel ("Conversations (0)", normalContent);
294287
conversationCountLabel_->setObjectName ("chat-tab-conversation-count");
295288
DpiUtils::applyScaledFont (conversationCountLabel_, kNavTitleFontPx);
296289
conversationCountLabel_->setStyleSheet ("color: #666666;");
297-
sidebarLayout->addWidget (conversationCountLabel_);
290+
normalLayout->addWidget (conversationCountLabel_);
298291

299-
conversationListWidget_= new QWidget (this);
292+
conversationListWidget_= new QWidget (normalContent);
300293
conversationListWidget_->setObjectName ("chat-tab-conversation-list");
301294
conversationListLayout_= new QVBoxLayout (conversationListWidget_);
302295
conversationListLayout_->setContentsMargins (0, 0, 0, 0);
303296
conversationListLayout_->setSpacing (DpiUtils::scaled (kSidebarSpacing));
304-
sidebarLayout->addWidget (conversationListWidget_);
297+
normalLayout->addWidget (conversationListWidget_);
298+
299+
normalLayout->addStretch ();
305300

306-
sidebarLayout->addStretch ();
301+
// 底部收缩按钮
302+
QPushButton* collapseBtn= new QPushButton ("收缩", normalContent);
303+
collapseBtn->setObjectName ("chat-tab-collapse-btn");
304+
collapseBtn->setFocusPolicy (Qt::NoFocus);
305+
collapseBtn->setCursor (Qt::PointingHandCursor);
306+
DpiUtils::applyScaledFont (collapseBtn, kCollapseFontPx);
307+
collapseBtn->setStyleSheet (
308+
QString ("QPushButton { border: 1px solid #cccccc; border-radius: %1px; "
309+
"padding: %2px %3px; background-color: #ffffff; }")
310+
.arg (DpiUtils::scaled (kCollapseBorderRadius))
311+
.arg (DpiUtils::scaled (kCollapsePadY))
312+
.arg (DpiUtils::scaled (kCollapsePadX)));
313+
connect (collapseBtn, &QPushButton::clicked, this,
314+
[this] () { toggle_sidebar (); });
315+
collapseButton_= collapseBtn;
316+
normalLayout->addWidget (collapseBtn);
317+
318+
sidebarNormalContent_= normalContent;
319+
sidebarLayout->addWidget (normalContent);
320+
321+
// ---- 收起态:窄条容器,底部展开按钮 ----
322+
QWidget* collapsedBar= new QWidget (sidebarWidget_);
323+
collapsedBar->setObjectName ("chat-tab-sidebar-collapsed");
324+
QVBoxLayout* collapsedLayout= new QVBoxLayout (collapsedBar);
325+
collapsedLayout->setContentsMargins (0, 0, 0,
326+
DpiUtils::scaled (kSidebarMarginY));
327+
collapsedLayout->setSpacing (0);
328+
329+
collapsedLayout->addStretch ();
330+
331+
QPushButton* expandBtn= new QPushButton ("<", collapsedBar);
332+
expandBtn->setObjectName ("chat-tab-collapsed-expand-btn");
333+
expandBtn->setFocusPolicy (Qt::NoFocus);
334+
expandBtn->setCursor (Qt::PointingHandCursor);
335+
DpiUtils::applyScaledFont (expandBtn, kCollapseFontPx);
336+
expandBtn->setStyleSheet (
337+
QString ("QPushButton { border: none; border-radius: %1px; "
338+
"padding: %2px 0px; background-color: transparent; } "
339+
"QPushButton:hover { background-color: #e0e0e0; }")
340+
.arg (DpiUtils::scaled (kCollapseBorderRadius))
341+
.arg (DpiUtils::scaled (kCollapsePadY)));
342+
connect (expandBtn, &QPushButton::clicked, this,
343+
[this] () { toggle_sidebar (); });
344+
collapsedLayout->addWidget (expandBtn);
345+
346+
collapsedBar->hide ();
347+
sidebarCollapsedBar_= collapsedBar;
348+
sidebarLayout->addWidget (collapsedBar);
307349
}
308350

309351
/**
@@ -515,6 +557,31 @@ QTChatTabWidget::refresh_sidebar () {
515557
}
516558
}
517559

560+
/**
561+
* @brief 切换侧边栏的收起/展开状态。
562+
*
563+
* 收起时 sidebar 缩为窄条(仅含展开按钮),展开时恢复完整内容。
564+
*/
565+
void
566+
QTChatTabWidget::toggle_sidebar () {
567+
if (!sidebarWidget_) return;
568+
569+
if (sidebarCollapsed_) {
570+
// 展开:显示正常内容,隐藏窄条,恢复宽度
571+
if (sidebarNormalContent_) sidebarNormalContent_->show ();
572+
if (sidebarCollapsedBar_) sidebarCollapsedBar_->hide ();
573+
sidebarWidget_->setFixedWidth (sidebarExpandedWidth_);
574+
sidebarCollapsed_= false;
575+
}
576+
else {
577+
// 收起:隐藏正常内容,显示窄条,缩为窄条
578+
if (sidebarNormalContent_) sidebarNormalContent_->hide ();
579+
if (sidebarCollapsedBar_) sidebarCollapsedBar_->show ();
580+
sidebarWidget_->setFixedWidth (DpiUtils::scaled (kSidebarCollapsedWidth));
581+
sidebarCollapsed_= true;
582+
}
583+
}
584+
518585
/**
519586
* @brief 将指定面板从欢迎态切换到会话态。
520587
*

src/Plugins/Qt/qt_chat_tab_widget.hpp

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ class QTChatTabWidget : public QWidget {
134134
*/
135135
void focus_input_editor (ChatConversationPanel* panel);
136136

137+
/**
138+
* @brief 切换侧边栏的收起/展开状态。
139+
*/
140+
void toggle_sidebar ();
141+
137142
public:
138143
/**
139144
* @brief 为 Chat Tab 安装主菜单栏内容。
@@ -154,16 +159,21 @@ class QTChatTabWidget : public QWidget {
154159
void set_chat_focus_icons (widget focusWidget);
155160

156161
private:
157-
QWidget* sidebarWidget_; ///< 左侧边栏容器。
158-
QWidget* contentWidget_; ///< 右侧内容区容器。
159-
QLabel* conversationCountLabel_; ///< 显示会话数量的标签。
160-
QWidget* conversationListWidget_; ///< 承载会话列表的控件。
161-
QVBoxLayout* conversationListLayout_; ///< 会话按钮的布局。
162-
QPushButton* newChatButton_; ///< 新建会话按钮。
163-
QStackedWidget* conversationStack_; ///< 会话页面的堆叠控件。
164-
QList<ChatConversationPanel*> conversations_; ///< 所有会话面板的列表。
162+
QWidget* sidebarWidget_; ///< 左侧边栏容器。
163+
QWidget* contentWidget_; ///< 右侧内容区容器。
164+
QLabel* conversationCountLabel_; ///< 显示会话数量的标签。
165+
QWidget* conversationListWidget_; ///< 承载会话列表的控件。
166+
QVBoxLayout* conversationListLayout_; ///< 会话按钮的布局。
167+
QPushButton* collapseButton_; ///< 侧边栏内的收缩按钮。
168+
QPushButton* newChatButton_; ///< 新建会话按钮。
169+
QWidget* sidebarNormalContent_; ///< 侧边栏展开时的内容容器。
170+
QWidget* sidebarCollapsedBar_; ///< 侧边栏收起时的窄条容器。
171+
QStackedWidget* conversationStack_; ///< 会话页面的堆叠控件。
172+
QList<ChatConversationPanel*> conversations_; ///< 所有会话面板的列表。
165173
ChatConversationPanel* activeConversation_; ///< 当前激活的会话。
166174
int nextConversationTitleId_; ///< 自动生成会话标题的 ID 计数器。
175+
bool sidebarCollapsed_; ///< 侧边栏当前是否处于收起状态。
176+
int sidebarExpandedWidth_; ///< 侧边栏展开时的宽度(像素)。
167177
QToolBar* chatMenuToolBar_; ///< Chat Tab 的菜单工具栏。
168178
QToolBar* chatModeToolBar_; ///< Chat Tab 的模式工具栏。
169179
QToolBar* chatFocusToolBar_; ///< Chat Tab 的焦点工具栏。

0 commit comments

Comments
 (0)