# 语音助手迷你模式实现方案

## 一、需求来源

在已有正常模式和回拨模式两种 UI 状态的基础上，新增第三套状态——迷你模式：

| 需求编号 | 描述 |
|---------|------|
| 1 | 点击标题栏 miniButton（tooltip"精简模式"）后进入迷你模式 |
| 2 | 进入迷你模式后，除 MiniPanel 外的所有控件（标题栏、中间区域）隐藏，窗口仅显示 MiniPanel 小条 |
| 3 | 窗口尺寸变为 181×40px，按钮缩放为 32×32px |
| 4 | 迷你模式下 MiniPanel 按钮从左到右：助手 logo（QLabel，仅显示用，24×24）→ 屏幕共享 → 静音 → 挂断 → 纵向分隔符 → 恢复按钮 |
| 5 | logo 控件和恢复按钮是仅在迷你模式下才显示的按钮 |
| 6 | 恢复按钮图标：view-fullscreen-symbolic.svg，tooltip"Restore" |
| 7 | logo 图标：logo.png |
| 8 | 按钮三态变化：默认状态背景色变为纯白色（#FFFFFF），hover/pressed/checked 不变 |
| 9 | 挂断按钮迷你模式特殊样式：默认白色背景 + 红色图标(call-stop-symbolic-red.svg)，hover 红色背景 + 原色图标，pressed 深红背景 + 原色图标 |
| 10 | 纵向分隔符：1×16px，`background: rgba(0,0,0,0.1)` |
| 11 | 点击恢复按钮回到正常模式，MiniPanel 的按钮、样式和状态全部恢复 |
| 12 | 迷你模式仅影响 UI 显示形态，不影响后台逻辑（语音识别、请求发送、AI 回答、朗读等） |

## 二、改动前现状分析

### 已实现

- `miniButton`（QToolButton, 30×30）：已在 `mainwindow.ui` 标题栏定义，tooltip"精简模式"，图标 `ukui-mini-symbolic.svg`，但**无任何点击信号连接**，属占位控件
- MiniPanel 包含 5 个按钮（48×48px）：屏幕共享、字幕、静音、挂断、接听（默认隐藏）
- `setCallbackMode(bool)`：回拨模式下切换 MiniPanel 按钮（隐藏 3 个功能按钮，显示接听按钮，调整 margins/spacing）
- 正常模式窗口尺寸 288×370px，通过 .ui 文件 min==max 锁定
- MiniPanel 高度 48px，主布局 bottom margin 24px

### 未实现/有问题

1. **miniButton 无功能**：标题栏迷你按钮已存在但无信号连接，点击无任何效果
2. **无迷你模式概念**：MainWindow 没有迷你模式的状态管理，无法切换到紧凑的 UI 显示
3. **无 logo 标签**：MiniPanel 中没有用于显示应用 logo 的 QLabel 控件
4. **无恢复按钮**：MiniPanel 中没有用于从迷你模式恢复的按钮
5. **无纵向分隔符**：MiniPanel 中没有视觉分隔控件
6. **按钮尺寸/样式无法动态切换**：所有按钮固定 48×48 + 固定样式表，无法缩放到 32×32

## 三、设计方案

### 3.1 设计原则：迷你模式仅影响 UI，不影响逻辑

**核心决策**：迷你模式和正常模式是不同的显示形态，不会影响后台逻辑。

- `m_miniMode` 标志位仅控制 UI 控件的显隐、尺寸、样式
- `canStartRecognition()`、`updateVoiceStateUi()` 等逻辑函数**不增加** `m_miniMode` 判断
- 语音识别、Gateway 请求、AI 回答、TTS 朗读等流程完全不受迷你模式影响
- 迷你模式下标题栏/中间区域被 hide()，但内部状态机正常运转

### 3.2 迷你模式与回拨模式的交互

**回拨模式视觉优先**：进入回拨模式时，如果当前处于迷你模式，先退出迷你模式并恢复 288×370 正常窗口，再显示原有回拨窗口。

```cpp
void MainWindow::setCallbackMode(bool enabled)
{
    if (enabled && m_miniMode) {
        setMiniMode(false);
    }
    // ... 原有逻辑
}
```

**接听后进入正常窗口**：接听回拨后调用 `setCallbackMode(false)`，进入正常模式窗口，不恢复此前的迷你模式。

### 3.3 迷你模式 MiniPanel 布局

| 序号 | 控件 | 类型 | 尺寸 | 默认可见 | 说明 |
|------|------|------|------|---------|------|
| 1 | `label_logo` | QLabel | 24×24 | 否 | logo.png，scaledContents，不可点击 |
| 2 | `pushButton_screenshare` | QPushButton | 48→32 | 是 | 屏幕共享 |
| 3 | `pushButton_subtitle` | QPushButton | 48 | 是 | 字幕（迷你模式下隐藏） |
| 4 | `pushButton_mute` | QPushButton | 48→32 | 是 | 静音 |
| 5 | `pushButton_cutoff` | QPushButton | 48→32 | 是 | 挂断 |
| 6 | `pushButton_answer` | QPushButton | 48 | 否 | 接听（迷你模式下也隐藏） |
| 7 | `frame_divider` | QFrame | 1×16 | 否 | 纵向分隔线，`background: rgba(0,0,0,0.1)` |
| 8 | `pushButton_restore` | QPushButton | 32×32 | 否 | view-fullscreen-symbolic.svg，tooltip"Restore" |

迷你模式下 visible 的控件：label_logo、pushButton_screenshare、pushButton_mute、pushButton_cutoff、frame_divider、pushButton_restore

迷你模式横向尺寸按 181px 固定宽度计算：左 margin 8px + logo 24px + logo 右侧间距 8px + 3 个功能按钮 32px + 分隔符 1px + 恢复按钮 32px + 其他控件间距 2px + 右 margin 4px。

### 3.4 迷你模式按钮样式

**功能按钮（屏幕共享、静音）**：默认背景白色，其余与正常模式相同：

```css
QPushButton { border-radius: 16px; background-color: #FFFFFF; border: 1px solid rgba(0,0,0,0); }
QPushButton:hover { background-color: #F2F2F2; }
QPushButton:pressed { background-color: #EBEBEB; }
QPushButton:checked { background-color: #EBF4FF; }
QPushButton:checked:hover { background-color: #E1EFFE; }
QPushButton:checked:pressed { background-color: #D7E9FE; }
```

**挂断按钮（迷你模式特殊）**：默认白色背景 + 红色图标，hover/pressed 切换为红色背景 + 原色图标：

```css
QPushButton { border-radius: 16px; background-color: #FFFFFF; border: 1px solid rgba(0,0,0,0); }
QPushButton:hover { background-color: #F53F3F; }
QPushButton:pressed { background-color: #C43232; }
```

图标通过 `eventFilter` 在 Enter/Leave 事件中动态切换：
- Enter → `call-stop-symbolic.svg`（原色图标）
- Leave → `call-stop-symbolic-red.svg`（红色图标）

**恢复按钮**：

```css
QPushButton { border-radius: 16px; background-color: #FFFFFF; border: 1px solid rgba(0,0,0,0); }
QPushButton:hover { background-color: #F2F2F2; }
QPushButton:pressed { background-color: #EBEBEB; }
```

### 3.5 进入迷你模式

```
用户点击 miniButton
  → m_miniMode = true
  → titleWidget->hide()
  → stackedWidget_middle->hide()
  → 主布局 margins 改为 (0,0,0,0)，spacing 改为 0
  → bottomWidget 高度 48→40
  → bottomWidget->setMiniMode(true)
      → 隐藏 subtitle、answer
      → 显示 logo、screenshare、mute、cutoff、divider、restore
      → 按钮尺寸 48→32
      → 应用迷你模式样式表
      → 挂断图标切换为红色版
      → logo 尺寸 24×24
      → 布局 left margin 8、right margin 4、常规 spacing 2、logo 右侧间距 8
  → 窗口尺寸 288×370 → 181×40
  → 以切换前窗口右下角为基准移动，保持右下角坐标不变
```

### 3.6 退出迷你模式

```
用户点击 restore 按钮
  → emit restoreClicked()
  → MainWindow::setMiniMode(false)
  → bottomWidget->setMiniMode(false)
      → 隐藏 logo、divider、restore
      → 按钮尺寸 32→48
      → 应用正常模式样式表
      → 挂断图标切换回原色版
      → 恢复正常布局 margins/spacing
      → 显示 screenshare、subtitle、mute
  → bottomWidget 高度 40→48
  → titleWidget->show()
  → stackedWidget_middle->show()
  → 主布局 margins 恢复 (0,0,0,24)
  → 窗口尺寸 181×40 → 288×370
  → 以切换前窗口右下角为基准移动，保持右下角坐标不变
```

### 3.7 样式管理策略

将按钮样式表抽取为命名常量（匿名 namespace），避免在代码中硬编码长字符串：

| 常量名 | 用途 |
|--------|------|
| `kMiniBtnStyleWhite` | 迷你模式功能按钮（32px，白底，含 checked 状态） |
| `kMiniCutoffStyle` | 迷你模式挂断按钮（32px，白底→红底 hover） |
| `kMiniRestoreStyle` | 迷你模式恢复按钮（32px，白底） |
| `kNormalBtnStyle` | 正常模式功能按钮（48px，#F7F7F7 底，含 checked 状态） |
| `kNormalCutoffStyle` | 正常模式挂断按钮（48px，#F53F3F 红底） |

`applyMiniModeStyles()` 和 `applyNormalModeStyles()` 负责批量应用。

## 四、文件改动详情

### 4.1 voice-assistant/src.qrc

新增 3 个图标资源：

```xml
<file>image/icon/logo.png</file>
<file>image/icon/view-fullscreen-symbolic.svg</file>
<file>image/icon/call-stop-symbolic-red.svg</file>
```

### 4.2 voice-assistant/minipanel.ui

在 HBoxLayout 最前面新增 `label_logo`（QLabel）：

```xml
<widget class="QLabel" name="label_logo">
  <property name="visible"><bool>false</bool></property>
  <property name="minimumSize"><size><width>24</width><height>24</height></size></property>
  <property name="maximumSize"><size><width>24</width><height>24</height></size></property>
  <property name="pixmap">
    <pixmap resource="src.qrc">:/image/icon/logo.png</pixmap>
  </property>
  <property name="scaledContents"><bool>true</bool></property>
  <property name="alignment"><set>Qt::AlignCenter</set></property>
</widget>
```

在 `pushButton_answer` 之后新增 `frame_divider`（QFrame）：

```xml
<widget class="QFrame" name="frame_divider">
  <property name="visible"><bool>false</bool></property>
  <property name="minimumSize"><size><width>1</width><height>16</height></size></property>
  <property name="maximumSize"><size><width>1</width><height>16</height></size></property>
  <property name="frameShape"><enum>QFrame::VLine</enum></property>
  <property name="lineWidth"><number>1</number></property>
  <property name="styleSheet">
    <string notr="true">QFrame { background: rgba(0, 0, 0, 0.1); border: none; }</string>
  </property>
</widget>
```

在 `frame_divider` 之后新增 `pushButton_restore`（QPushButton）：

```xml
<widget class="QPushButton" name="pushButton_restore">
  <property name="visible"><bool>false</bool></property>
  <property name="minimumSize"><size><width>32</width><height>32</height></size></property>
  <property name="maximumSize"><size><width>32</width><height>32</height></size></property>
  <property name="toolTip"><string>Restore</string></property>
  <property name="styleSheet">
    QPushButton { border-radius: 16px; background-color: #FFFFFF; border: 1px solid rgba(0,0,0,0); }
    QPushButton:hover { background-color: #F2F2F2; }
    QPushButton:pressed { background-color: #EBEBEB; }
  </property>
  <property name="icon">
    <iconset resource="src.qrc">:/image/icon/view-fullscreen-symbolic.svg</iconset>
  </property>
</widget>
```

### 4.3 voice-assistant/minipanel.h

新增内容：

```cpp
// 公有方法
void setMiniMode(bool enabled);

// 信号
void restoreClicked();

// 私有槽
void on_pushButton_restore_clicked();

// protected override
bool eventFilter(QObject *watched, QEvent *event) override;

// 私有方法
void applyMiniModeStyles();
void applyNormalModeStyles();

// 私有成员
bool m_miniMode = false;
```

### 4.4 voice-assistant/minipanel.cpp

**匿名 namespace 新增 5 个样式常量**（`kMiniBtnStyleWhite`、`kMiniCutoffStyle`、`kMiniRestoreStyle`、`kNormalBtnStyle`、`kNormalCutoffStyle`），用于模式切换时批量应用样式。

**构造函数改动**：

```cpp
ui->pushButton_cutoff->installEventFilter(this);  // 为挂断按钮注册事件过滤器
```

**析构函数改动**：

```cpp
ui->pushButton_cutoff->removeEventFilter(this);
```

**新增 `eventFilter`**：处理挂断按钮在迷你模式下的 hover 图标切换：

```cpp
bool MiniPanel::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == ui->pushButton_cutoff && m_miniMode) {
        const QEvent::Type type = event->type();
        if (type == QEvent::Enter) {
            ui->pushButton_cutoff->setIcon(QIcon(":/image/icon/call-stop-symbolic.svg"));
        } else if (type == QEvent::Leave) {
            ui->pushButton_cutoff->setIcon(QIcon(":/image/icon/call-stop-symbolic-red.svg"));
        }
    }
    return QWidget::eventFilter(watched, event);
}
```

**新增 `on_pushButton_restore_clicked`**：

```cpp
void MiniPanel::on_pushButton_restore_clicked()
{
    emit restoreClicked();
}
```

**修改 `setCallbackMode`**：增加迷你模式守卫：

```cpp
void MiniPanel::setCallbackMode(bool enabled)
{
    if (m_miniMode) {
        return;  // 迷你模式下不切换回拨按钮布局
    }
    // ... 原有逻辑不变
}
```

**新增 `setMiniMode(bool enabled)`**：核心方法，切换迷你/正常模式的按钮布局、尺寸、样式：

```cpp
void MiniPanel::setMiniMode(bool enabled)
{
    if (m_miniMode == enabled) return;
    m_miniMode = enabled;

    if (enabled) {
        // 隐藏：subtitle、answer
        // 显示：logo、screenshare、mute、cutoff、divider、restore
        // logo 尺寸 24×24
        // 按钮尺寸 48→32
        // 应用迷你样式 + 红色挂断图标
        // 布局 left margin 8、right margin 4、常规 spacing 2、logo 右侧间距 8
    } else {
        // 隐藏：logo、divider、restore
        // 按钮尺寸 32→48
        // 应用正常样式 + 原色挂断图标
        // 恢复正常 margins/spacing
        // 显示：screenshare、subtitle、mute
    }
}
```

**新增 `applyMiniModeStyles` / `applyNormalModeStyles`**：批量应用样式表。

### 4.5 voice-assistant/mainwindow.h

新增：

```cpp
// 私有方法
void setMiniMode(bool enabled);

// 私有成员
bool m_miniMode = false;
```

### 4.6 voice-assistant/mainwindow.cpp

**构造函数新增 2 个信号连接**：

```cpp
connect(ui->bottomWidget, &MiniPanel::restoreClicked, this, [this]() {
    setMiniMode(false);
});
connect(ui->miniButton, &QToolButton::clicked, this, [this]() {
    setMiniMode(true);
});
```

**新增 `setMiniMode(bool enabled)`**：

```cpp
void MainWindow::setMiniMode(bool enabled)
{
    if (m_miniMode == enabled) return;
    m_miniMode = enabled;

    if (enabled) {
        ui->titleWidget->hide();
        ui->stackedWidget_middle->hide();
        ui->verticalLayout->setContentsMargins(0, 0, 0, 0);
        ui->verticalLayout->setSpacing(0);
        ui->bottomWidget->setMinimumHeight(40);
        ui->bottomWidget->setMaximumHeight(40);
        ui->bottomWidget->setMiniMode(true);
        resizeKeepingBottomRight(QSize(181, 40));
    } else {
        ui->bottomWidget->setMiniMode(false);
        ui->bottomWidget->setMinimumHeight(48);
        ui->bottomWidget->setMaximumHeight(48);
        ui->titleWidget->show();
        ui->stackedWidget_middle->show();
        ui->verticalLayout->setContentsMargins(0, 0, 0, 24);
        ui->verticalLayout->setSpacing(0);
        resizeKeepingBottomRight(QSize(288, 370));
    }
}
```

注意退出迷你模式时**先**调用 `bottomWidget->setMiniMode(false)` 再显示 titleWidget/stackedWidget_middle，确保 MiniPanel 按钮恢复正常后再显示上层控件。

### 4.7 voice-assistant/translations/voice_assistant_zh_CN.ts

新增翻译：

| 英文原文 | 中文翻译 |
|---------|---------|
| `Restore` | `恢复` |

### 4.8 voice-assistant/translations/voice_assistant_zh_TW.ts

新增翻译：

| 英文原文 | 繁體翻譯 |
|---------|---------|
| `Restore` | `復原` |

## 五、关键流程图

### 5.1 进入迷你模式

```
[正常模式 288×370]
       |
   点击 miniButton
       |
   m_miniMode = true
       |
   titleWidget->hide()
   stackedWidget_middle->hide()
   主布局 margins → (0,0,0,0)
   bottomWidget 高度 → 40
   bottomWidget->setMiniMode(true)
       ├─ 隐藏 subtitle、answer
       ├─ 显示 logo、screenshare、mute、cutoff、divider、restore
       ├─ logo 尺寸 24×24
       ├─ 按钮尺寸 48→32
       ├─ 应用迷你样式表
       ├─ 挂断图标 → call-stop-symbolic-red.svg
       └─ 布局 left margin 8、right margin 4、常规 spacing 2、logo 右侧间距 8
   窗口尺寸 → 181×40
   保持切换前窗口右下角坐标不变
       |
[迷你模式 181×40: logo | 屏幕共享 | 静音 | 挂断 | │ | 恢复]
```

### 5.2 退出迷你模式

```
[迷你模式 181×40]
       |
   点击 restore 按钮
       |
   emit restoreClicked()
       |
   MainWindow::setMiniMode(false)
       |
   bottomWidget->setMiniMode(false)
       ├─ 隐藏 logo、divider、restore
       ├─ 按钮尺寸 32→48
       ├─ 应用正常样式表
       ├─ 挂断图标 → call-stop-symbolic.svg
       ├─ 恢复正常 margins/spacing
       └─ 显示 screenshare、subtitle、mute
   bottomWidget 高度 → 48
   titleWidget->show()
   stackedWidget_middle->show()
   主布局 margins → (0,0,0,24)
   窗口尺寸 → 288×370
   保持切换前窗口右下角坐标不变
       |
[正常模式 288×370]
```

### 5.3 迷你模式下挂断按钮 hover 切换

```
[迷你模式挂断按钮]
       |
   鼠标进入 (Enter event)
       ├─ eventFilter 检测到 m_miniMode == true
       └─ 图标 → call-stop-symbolic.svg + 红色背景（样式表 :hover）
       |
   鼠标离开 (Leave event)
       ├─ eventFilter 检测到 m_miniMode == true
       └─ 图标 → call-stop-symbolic-red.svg + 白色背景（样式表默认）
```

### 5.4 迷你模式与回拨模式的交互

```
[迷你模式中，AI 回答完成后触发回拨]
       |
   handleAiReplyFinished → setCallbackMode(true)
       |
   MainWindow::setCallbackMode(true)
       |
   若 m_miniMode == true，先 setMiniMode(false)
       |
   窗口恢复 288×370
       |
   显示原有回拨窗口（挂断 + 接听）
       |
   用户点击接听 → setCallbackMode(false)
       |
   进入正常模式窗口
```

### 5.5 三种模式对比

| 特性 | 正常模式 | 回拨模式 | 迷你模式 |
|------|---------|---------|---------|
| 窗口尺寸 | 288×370 | 288×370 | 181×40 |
| 标题栏 | 显示 | 显示 | 隐藏 |
| 中间区域 | 显示 | 显示 | 隐藏 |
| MiniPanel 高度 | 48 | 48 | 40 |
| 按钮尺寸 | 48×48 | 48×48 | 功能按钮/恢复按钮 32×32，logo 24×24 |
| 按钮布局 | 屏幕+字幕+静音+挂断 | 挂断+接听 | logo+屏幕+静音+挂断+分隔+恢复 |
| 按钮默认背景 | #F7F7F7 | #F7F7F7 | #FFFFFF |
| 挂断按钮 | 红色背景 | 红色背景 | 白底+红图标 / hover红底+原色图标 |
| 影响后台逻辑 | 否 | 是（阻止 ASR） | 否 |

## 六、测试用例覆盖

| 测试场景 | 覆盖情况 |
|----------|----------|
| 点击 miniButton 进入迷你模式 | miniButton clicked → setMiniMode(true) |
| 迷你模式窗口尺寸 181×40 | setMinimumSize/setMaximumSize/resize |
| 迷小模式隐藏标题栏和中间区域 | titleWidget->hide(), stackedWidget_middle->hide() |
| 迷你模式按钮布局正确 | setMiniMode 中按钮显隐逻辑 |
| 迷你模式按钮 32×32、logo 24×24 | setMinimumSize/setMaximumSize |
| 迷你模式按钮白色默认背景 | kMiniBtnStyleWhite |
| 挂断按钮默认红图标 + hover 红底 | eventFilter + kMiniCutoffStyle |
| 挂断按钮 hover 时切换原色图标 | eventFilter Enter→call-stop-symbolic.svg |
| 挂断按钮 leave 时切换回红图标 | eventFilter Leave→call-stop-symbolic-red.svg |
| 恢复按钮点击回到正常模式 | restoreClicked → setMiniMode(false) |
| 退出迷你模式后按钮恢复 48×48 | setMiniMode(false) 中恢复尺寸 |
| 退出迷你模式后样式恢复 | applyNormalModeStyles() |
| 退出迷你模式后窗口恢复 288×370 | setMinimumSize/setMaximumSize/resize |
| 退出迷你模式后标题栏/中间区域恢复 | titleWidget->show(), stackedWidget_middle->show() |
| 迷你模式下后台逻辑不受影响 | 不修改 canStartRecognition/updateVoiceStateUi |
| 进入回拨模式时退出迷你模式 | MainWindow::setCallbackMode(true) 先调用 setMiniMode(false) |
| 迷你模式/正常模式下 subtitle 按钮状态恢复 | 退出迷你模式后 subtitle->show() |
| logo QLabel 不可点击 | QLabel 无点击事件 |
| 分隔符样式 rgba(0,0,0,0.1) | frame_divider stylesheet |
| Restore tooltip 翻译 | zh_CN: "恢复"，zh_TW: "復原" |

## 七、风险与缓解

| 风险 | 缓解措施 |
|------|----------|
| 回拨模式与迷你模式布局冲突 | 进入回拨模式时先退出迷你模式，回拨窗口始终使用原有 288×370 布局 |
| eventFilter 在非迷你模式下误触发 | eventFilter 内检查 `m_miniMode == true` 才处理 Enter/Leave |
| 模式切换后窗口跳回屏幕右下角 | 切换尺寸时记录旧 `frameGeometry().bottomRight()`，resize 后按新 frame size 反推左上角，保持右下角坐标不变 |
| 按钮样式表切换导致图标丢失 | 样式表切换不覆盖 icon 属性，图标由代码显式设置 |
| 迷你模式下 MiniPanel 高度 40px 与按钮 32px 对齐 | 按钮 32px + 上下 (40-32)/2=4px 内边距，布局 margins top/bottom=0 |
| 主布局 bottom margin 从 24→0 再恢复 | setMiniMode 中显式设置和恢复 `setContentsMargins(0,0,0,0)` / `(0,0,0,24)` |
| 样式表硬编码维护困难 | 抽取为匿名 namespace 常量，集中管理 |
