- 新增布局管理器支持拖拽调整面板宽度 - 新增状态管理器实现布局持久化 - 重构前端页面结构优化四栏式布局 - 新增 Jest 测试框架和完整测试用例 - 完善项目文档(PRD/FD/TDD/TODO/LOG) - 清理 Python 缓存文件并更新 .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
33 KiB
FD - 可调整布局系统功能设计
文档信息
- 文档编号: FD-00001
- 创建日期: 2026-02-26
- 版本: v1.0
- 状态: 待评审
- 负责人: 开发团队
- 关联 PRD: PRD-00001 可调整布局系统 v1.1
一、功能概述
本文档详细描述可调整布局系统的功能设计,包括 6 个主要功能模块、23 个子功能的实现细节、验收标准和测试策略。
1.1 功能范围
本次实现的功能包括:
- 可拖动调整宽度 - 用户可以通过拖动面板边缘调整宽度
- 面板折叠/展开 - 用户可以折叠/展开侧边栏面板
- 恢复默认布局 - 用户可以一键恢复到默认布局
- 状态持久化 - 自动保存和恢复用户的布局偏好
- 窗口尺寸自适应 - 根据窗口大小自动调整布局
- 键盘可访问性 - 支持键盘操作和屏幕阅读器
1.2 参考文档
- PRD:
PRD/PRD-可调整布局系统.mdv1.1 - 设计参考: EnsoAI 编辑器 UI 设计
1.3 术语定义
| 术语 | 说明 |
|---|---|
| 面板 | 指分组面板、账号面板、邮件列表面板、邮件详情面板 |
| Resizer | 拖动分隔条,位于面板右侧边缘,用于调整宽度 |
| 折叠 | 隐藏面板内容,宽度变为 0 或显示 48px 细条 |
| 展开 | 显示面板内容,恢复到折叠前的宽度 |
| 布局状态 | 包括所有面板的宽度和折叠状态 |
| 默认布局 | 初始的面板宽度和折叠状态 |
二、功能详细设计
功能模块总览
| 模块编号 | 模块名称 | 子功能数量 | 优先级 |
|---|---|---|---|
| F1 | 可拖动调整宽度 | 4 | P0 |
| F2 | 面板折叠/展开 | 6 | P0 |
| F3 | 恢复默认布局 | 4 | P1 |
| F4 | 状态持久化 | 5 | P0 |
| F5 | 窗口尺寸自适应 | 3 | P1 |
| F6 | 键盘可访问性 | 4 | P2 |
总计: 6 个模块,26 个子功能
三、F1 - 可拖动调整宽度
F1.1 鼠标拖动调整宽度
功能编号: F1.1 功能名称: 鼠标拖动调整宽度 优先级: P0
功能描述
用户可以通过鼠标拖动面板右侧的 resizer(拖动条)来实时调整面板宽度。
前置条件
- 面板处于展开状态(非折叠)
- Resizer 元素已渲染且可见
主流程
- 用户将鼠标悬停在 resizer 上
- 鼠标光标变为
col-resize(双向箭头) - Resizer 显示视觉反馈(背景色变化、边框高亮)
- 用户按下鼠标左键(mousedown)
- 系统记录起始位置和起始宽度
- 用户移动鼠标(mousemove)
- 系统实时计算新宽度:
newWidth = startWidth + (currentX - startX) - 系统应用宽度限制(最小/最大值)
- 系统更新 CSS 变量,面板宽度实时变化
- 用户释放鼠标(mouseup)
- 系统触发防抖保存(500ms 后保存到 localStorage)
异常流程
- 异常 1: 用户拖动时鼠标移出窗口
- 处理:继续监听 mousemove 事件,直到 mouseup
- 异常 2: 拖动过程中窗口大小改变
- 处理:重新计算容器宽度,调整最大允许宽度
- 异常 3: 拖动速度过快导致性能问题
- 处理:使用 requestAnimationFrame 节流
界面元素
Resizer 元素:
<div class="resizer"
role="separator"
aria-orientation="vertical"
aria-label="调整分组面板宽度"
aria-valuenow="200"
aria-valuemin="150"
aria-valuemax="400"
0"
data-resizer="groups">
</div>
样式:
- 宽度:4px
- 高度:100%(填充面板高度)
- 位置:绝对定位,right: 0
- 光标:col-resize
- Hover 背景色:rgba(26, 26, 26, 0.1)
- Hover 边框:2px solid #1a1a1a
验收标准
- 可以拖动分组面板的 resizer
- 可以拖动账号面板的 resizer
- 可以拖动邮件列表面板的 resizer
- 拖动时面板宽度实时变化
- 拖动时光标为 col-resize
- 拖动时有视觉反馈(背景色、边框)
- 拖动流畅,帧率 ≥ 60fps
- 释放鼠标后 500ms 保存状态
F1.2 宽度限制(最小/最大)
功能编号: F1.2 功能名称: 宽度限制 优先级: P0
功能描述
系统对每个面板的宽度进行限制,防止用户拖动到不合理的尺寸。
宽度限制规则
| 面板 | 最小宽度 | 最大宽度 | 说明 |
|---|---|---|---|
| 分组面板 | 150px | 400px | 确保分组名称和图标可见 |
| 账号面板 | 180px | 500px | 确保邮箱地址可读 |
| 邮件列表 | 280px | 600px | 确保邮件预览可读 |
| 邮件详情 | 400px | 无限制 | 自动填充剩余空间 |
主流程
- 用户拖动 resizer
- 系统计算新宽度
- 系统获取该面板的最小/最大宽度
- 系统应用限制:
newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth)) - 系统更新面板宽度
验收标准
- 分组面板宽度不能小于 150px
- 分组面板宽度不能大于 400px
- 账号面板宽度不能小于 180px
- 账号面板宽度不能大于 500px
- 邮件列表宽度不能小于 280px
- 邮件列表宽度不能大于 600px
- 达到限制时,继续拖动无效果
F1.3 确保邮件详情面板可见
功能编号: F1.3 功能名称: 确保邮件详情面板可见 优先级: P0
功能描述
无论用户如何调整侧边栏宽度,邮件详情面板始终保持至少 400px 可见宽度。
主流程
- 用户拖动任意侧边栏的 resizer
- 系统计算容器总宽度
- 系统计算其他面板的总宽度
- 系统计算最大允许宽度:
maxAllowed = containerWidth - otherPanelsWidth - 400 - 系统应用限制:
newWidth = Math.min(newWidth, maxAllowed) - 系统更新面板宽度
异常流程
- 异常 1: 容器宽度不足(< 800px)
- 处理:触发窗口自适应逻辑部分面板
验收标准
- 邮件详情面板始终可见
- 邮件详情面板宽度 ≥ 400px
- 拖动侧边栏时,邮件详情面板不会被挤压消失
- 窗口宽度不足时,自动折叠侧边栏
F1.4 拖动视觉反馈
功能编号: F1.4 功能名称: 拖动视觉反馈 优先级: P1
功能描述
在拖动过程中提供清晰的视觉反馈,让用户知道当前正在拖动。
视觉反馈元素
-
Resizer Hover 状态:
- 背景色:rgba(26, 26, 26, 0.1)
- 中心线:2px solid #1a1a1a
-
拖动中状态:
- 全局光标:col-resize
- 禁用文本选择:user-select: none
- Resizer 保持高亮状态
-
拖动结束:
- 恢复默认光标
- 恢复文本选择
- Resizer 恢复普通状态
验收标准
- Hover 时 resizer 有视觉反馈
- 拖动时全局光标为 col-resize
- 拖动时无法选中文本
- 拖动结束后光标恢复正常
- 拖动结束后可以选中文本
四、F2 - 面板折叠/展开
F2.1 折叠按钮交互
功能编号: F2.1 功能名称: 折叠按钮交互 优先级: P0
功能描述
每个侧边栏面板的右上角有一个折叠按钮,点击可以折叠/展开面板。
前置条件
- 面板已渲染
- 至少有一个其他侧边栏面板处于展开状态
主流程
- 用户点击折叠按钮
- 系统检查是否至少有一个其他面板展开
- 如果是最后一个展开的面板,阻止折叠并提示用户
- 否则,切换面板的折叠状态
- 系统添加/移除
.collapsed类 - 系统触发 CSS 过渡动画(200ms)
- 系统立即保存状态到 localStorage
界面元素
折叠按钮:
<button class="collapse-btn"
aria-label="折叠分组面板"
aria-expanded="true"
title="折叠">
<svg><!-- 左箭头图标 --></svg>
</bn>
按钮样式:
- 尺寸:24x24px
- 位置:面板右上角
- 图标:展开时显示 ←,折叠时显示 →
- Hover 背景色:#f5f5f5
验收标准
- 点击折叠按钮可以折叠面板
- 折叠后按钮图标变为 →
- 点击折叠后的指示器可以展开
- 展开后按钮图标变为 ←
- 不能折叠最后一个展开的面板
- 折叠/展开有平滑动画
F2.2 折叠动画
功能编号: F2.2 功能名称: 折叠动画 优先级: P0
功能描述
面板折叠/展开时有平滑的过渡动画,提升用户体验。
动画参数
- 动画时长: 200ms
- 缓动函数: ease
- 动画属性: width, opacity
- 折叠方向: 宽度从当前值到 0,透明度从 1 到 0
- 展开方向: 宽度从 0 到保存的值,透明度从 0 到 1
CSS 实现
.resizable-panel {
transition: width 200ms ease, opacity 200ms ease;
}
.resizable-panel.collapsed {
width: 0 !important;
opacity: 0;
overflow: hidden;
}
验收标准
- 折叠动画流畅,无卡顿
- 展开动画流畅,无卡顿
- 动画时长为 200ms
- 动画期间不影响其他操作
F2.3 折叠指示器
功能编号: F2.3 功能名称: 折叠指示器 优先级: P1
功能描述
面板折叠后,在主容器边缘显示一个 48px 宽的细条,竖排显示面板名称。
界面元素
折叠指示器:
<div class="collapsed-indicator" data-panel="groups">
<span class="indicator-text">分组</span>
</div>
样式:
- 宽度:48px
- 高度:100%
- 背景色:#fafafa
- 边框:1px solid #e5e5e5
- 文字方向:竖排(writing-mode: vertical-rl)
- 文字大小:14px
- 文字颜色:#666
- Hover 背景色:#f0f0f0
- 光标:pointer
验收标准
- 折叠后显示 48px 宽的指示器
- 指示器显示面板名称(竖排)
- Hover 时有视觉反馈
- 光标为 pointer
F2.4 点击指示器展开
功能编号: F2.4 功能名称: 点击指示器展开 优先级: P1
功能描述
用户点击折叠指示器可以快速展开对应的面板。
主流程
- 用户点击折叠指示器
- 系统识别对应的面板
- 系统移除
.collapsed类 - 系统触发展开动画
- 系统恢复面板到折叠前的宽度
- 系统立即保存状态
验收标准
- 点击指示器可以展开面板
- 展开后恢复到折叠前的宽度
- 展开动画流畅
- 展开后指示器消失
F2.5 折叠时隐藏 Resizer
功能编号: F2.5 功能名称: 折叠时隐藏 Resizer 优先级: P0
功能描述
面板折叠后,该面板的 resizer 自动隐藏,避免用户误操作。
CSS 实现
.resizable-panel.collapsed .resizer {
display: none;
}
验收标准
- 折叠后 resizer 不可见
- 折叠后无法拖动调整宽度
- 展开后 resizer 恢复可见
- 展开后可以拖动调整宽度
F2.6 至少保持一个面板展开
功能编号: F2.6 功能名称: 至少保持一个面板展开 优先级: P0
功能描述
系统确保至少有一个侧边栏面板处于展开状态,防止用户折叠所有面板导致无法操作。
主流程
- 用户尝试折叠面板
- 系统检查其他侧边栏面板的状态
- 如果所有其他面板都已折叠,阻止操作
- 显示提示消息:"至少需要保持一个面板展开"
- 否则,允许折叠
验收标准
- 不能折叠最后一个展开的面板
- 尝试折叠时显示提示消息
- 至少有一个其他面板展开时可以折叠
五、F3 - 恢复默认布局
F3.1 重置按钮
功能编号: F3.1 功能名称: 重置按钮 优先级: P1
功能描述
在顶部导航栏添加"恢复默认布局"按钮,用户可以一键重置布局。
界面元素
重置按钮:
<button class="navbar-btn reset-layout-btn"
aria-label="恢复默认布局"
title="恢复默认布局">
<svg><!-- 重置图标 ↻ --></svg>
</button>
按钮位置:
- 导航栏右侧
- 在其他操作按钮之后
按钮样式:
- 尺寸:32x32px
- 图标:重置图标(↻)
- Hover 背景色:#f5f5f5
- Hover 边框:1px solid #ccc
验收标准
- 导航栏显示重置按钮
- 按钮位置合理
- Hover 时有视觉反馈
- Tooltip 显示"恢复默认布局"
F3.2 确认对话框
功能编号: F3.2 功能名称: 确认对话框 优先级: P1
功能描述
点击重置按钮后,弹出确认对话框,防止用户误操作。
界面元素
确认对话框:
<div class="modal" id="resetLayoutModal">
<div class="modal-content">
<div class="modal-header">
<h3>恢复默认布局</h3>
<button class="modal-close">×</button>
</div>
<div class="modal-body">
<p>确定要恢复到默认布局吗?</p>
<p class="text-muted">当前的面板宽度和折叠状态将被重置。</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary">取消</button>
<button class="btn btn-primary">确定</button>
</div>
</div>
</div>
主流程
- 用户点击重置按钮
- 系统显示确认对话框
- 用户点击"确定"或"取消"
- 如果点击"确定",执行重置逻辑
- 如果点击"取消",关闭对话框
验收标准
- 点击重置按钮显示对话框
- 对话框内容清晰
- 点击"取消"关闭对话框
- 点击"确定"执行重置
- 点击遮罩层关闭对话框
F3.3 重置逻辑
功能编号: F3.3 功能名称: 重置逻辑 优先级: P1
功能描述
执行重置操作,将所有面板恢复到默认宽度和折叠状态。
默认布局参数
| 面板 | 默认宽度 | 默认状态 |
|---|---|---|
| 分组面板 | 200px | 展开 |
| 账号面板 | 260px | 展开 |
| 邮件列表 | 380px | 展开 |
| 邮件详情 | 自适应 | 展开 |
主流程
- 用户确认重置
- 系统清除 localStorage 中的布局状态
- 系统重置 CSS 变量到默认值
- 系统移除所有
.collapsed类 - 系统触发重置动画(300ms)
- 系统关闭确认对话框
- 系统显示成功提示
验收标准
- 所有面板恢复到默认宽度
- 所有面板恢复到展开状态
- localStorage 被清除
- 重置动画流畅
- 显示成功提示
F3.4 重置动画
功能编号: F3.4 功能名称: 重置动画 优先级: P2
功能描述
重置时使用平滑的过渡动画,而不是瞬间跳变。
动画参数
- 动画时长: 300ms
- 缓动函数: ease
- 动画属性: width
CSS 实现
.main-container.resetting {
transition: grid-template-columns 300ms ease;
}
.resizable-panel.resetting {
transition: width 300ms ease;
}
验收标准
- 重置动画流畅
- 动画时长为 300ms
- 所有面板同时动画
六、F4 - 状态持久化
F4.1 保存布局状态
功能编号: F4.1 功能名称: 保存布局状态 优先级: P0
功能描述
自动保存用户的布局偏好到 localStorage,包括面板宽度和折叠状态。
数据结构
{
"version": "1.1",
"userId": "user123",
"timestamp": 1709020800000,
"panels": {
"groups": {
"width": "200px",
"collapsed": false
},
"accounts": {
"width": "260px",
"collapsed": false
},
"emails": {
"width": "380px",
"collapsed": false
}
}
}
存储键名
- 格式:
outlook_layout_state_${userId} - 未登录:
outlook_layout_state_guest
主流程
- 用户调整布局(拖动或折叠)
- 系统触发保存逻辑
- 系统读取当前所有面板的状态
- 系统构建数据对象
- 系统序列化为 JSON
- 系统写入 localStorage
验收标准
- 拖动后 500ms 保存状态
- 折叠后立即保存状态
- 数据格式正确
- 多用户隔离生效
- localStorage 数据 ≤ 1KB
F4.2 加载布局状态
功能编号: F4.2 功能名称: 加载布局状态 优先级: P0
功能描述
页面加载时从 localStorage 读取并恢复用户的布局偏好。
主流程
- 页面 DOMContentLoaded 事件触发
- 系统获取当前用户 ID
- 系统从 localStorage 读取数据
- 系统验证数据格式和版本
- 系统应用面板宽度(更新 CSS 变量)
- 系统应用折叠状态(添加
.collapsed类) - 系统跳过动画(避免页面加载时的闪烁)
异常流程
- 异常 1: localStorage 不可用
- 处理:使用默认布局,不报错
- 异常 2: 数据格式错误
- 处理:清除数据,使用默认布局
- 异常 3: 版本不兼容
- 处理:迁移数据或使用默认布局
验收标准
- 刷新页面后布局状态恢复
- 恢复时无动画闪烁
- localStorage 不可用时使用默认布局
- 数据错误时使用默认布局
F4.3 多用户隔离
功能编号: F4.3 功能名称: 多用户隔离 优先级: P1
功能描述
不同用户的布局偏好独立存储,互不影响。
实现方式
- 存储键名包含用户 ID:
outlook_layout_state_${userId} - 从全局变量或页面元素获取用户 ID
- 未登录用户使用
guest作为 ID
主流程
- 系统获取当前用户 ID
- 系统构建存储键名
- 系统使用该键名读写 localStorage
验收标准
- 不同用户的布局独立
- 切换用户后布局正确
- 未登录用户使用 guest 键名
F4.4 防抖保存
功能编号: F4.4 功能名称: 防抖保存 优先级: P1
功能描述
拖动调整宽度时,延迟 500ms 保存,避免频繁写入 localStorage。
实现逻辑
let pendingSave = null;
function saveStateDebounced() {
if (pendingSave) {
clearTimeout(pendingSave);
}
pendingSave = setTimeout(() => {
saveStateNow();
pendingSave = null;
}, 500);
}
验收标准
- 拖动时不立即保存
- 停止拖动后 500ms 保存
- 连续拖动只保存一次
- 折叠操作立即保存(不防抖)
F4.5 页面卸载时强制保存
功能编号: F4.5 功能名称: 页面卸载时强制保存 优先级: P1
功能描述
用户关闭页面或刷新时,强制执行防抖任务,确保最后一次调整被保存。
实现逻辑
window.addEventListener('beforeunload', () => {
if (pendingSave) {
clearTimeout(pendingSave);
saveStateNow();
}
});
验收标准
- 拖动后立即关闭页面,状态被保存
- 拖动后立即刷新页面,状态被保存
- 不影响页面卸载速度
七、F5 - 窗口尺寸自适应
F5.1 监听窗口大小变化
功能编号: F5.1 功能名称: 监听窗口大小变化 优先级: P1
功能描述
系统监听浏览器窗口的 resize 事件,根据窗口宽度自动调整布局。
实现逻辑
let resizeTimer = null;
window.addEventListener("resize", () => {
if (resizeTimer) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(() => {
handleWindowResize();
resizeTimer = null;
}, 200);
});
验收标准
- 窗口大小改变时触发处理
- 使用防抖避免频繁触发
- 防抖延迟为 200ms
F5.2 自动折叠面板
功能编号: F5.2 功能名称: 自动折叠面板 优先级: P1
功能描述
当窗口宽度小于特定断点时,自动折叠侧边栏面板,确保邮件详情面板可见。
断点规则
| 窗口宽度 | 自动折叠的面板 |
|---|---|
| < 1200px | 分组面板 |
| < 900px | 分组面板 + 账号面板 |
| < 700px | 分组面板 + 账号面板 + 邮件列表 |
主流程
- 窗口大小改变
- 系统获取当前窗口宽度
- 系统根据断点判断需要折叠的面板
- 系统自动折叠面板(不保存状态)
- 系统添加 .auto-collapsed 类(区分用户手动折叠)
特殊处理
- 自动折叠不保存到 localStorage
- 窗口变大时不自动展开(尊重用户选择)
- 用户手动展开后,窗口缩小时再次自动折叠
验收标准
- 窗口 < 1200px 时自动折叠分组面板
- 窗口 < 900px 时自动折叠账号面板
- 窗口 < 700px 时自动折叠邮件列表
- 自动折叠不保存状态
- 窗口变大时不自动展开
F5.3 媒体查询降级
功能编号: F5.3 功能名称: 媒体查询降级 优先级: P2
功能描述
使用 CSS 媒体查询作为降级方案,即使 JavaScript 失效也能保证基本的响应式布局。
CSS 实现
@media (max-width: 1200px) {
.group-panel {
display: none;
}
}
@media (max-width: 900px) {
.account-panel {
display: none;
}
}
@media (max-width: 700px) {
.email-list-panel {
display: none;
}
}
验收标准
- JavaScript 禁用时布局仍然响应式
- 媒体查询断点与 JavaScript 一致
- 小屏幕下邮件详情面板可见
八、F6 - 键盘可访问性
F6.1 Resizer 键盘支持
功能编号: F6.1 功能名称: Resizer 键盘支持 优先级: P2
功能描述
Resizer 支持键盘操作,用户可以使用方向键调整面板宽度。
键盘操作
| 按键 | 操作 | 调整幅度 |
|---|---|---|
| ← | 减小宽度 | 10px |
| → | 增大宽度 | 10px |
| Shift + ← | 快速减小 | 50px |
| Shift + → | 快速增大 | 50px |
| Tab | 聚焦到下一个 resizer | - |
| Shift + Tab | 聚焦到上一个 resizer | - |
主流程
- 用户按 Tab 键聚焦到 resizer
- Resizer 显示聚焦样式(outline)
- 用户按方向键
- 系统计算新宽度
- 系统应用宽度限制
- 系统更新面板宽度
- 系统更新 aria-valuenow 属性
- 系统触发防抖保存
验收标准
- Tab 键可以聚焦到 resizer
- 聚焦时有明显的视觉反馈
- 左箭头键减小宽度
- 右箭头键增大宽度
- Shift + 箭头键快速调整
- 键盘调整也应用宽度限制
- 键盘调整后保存状态
F6.2 折叠按钮键盘支持
功能编号: F6.2 功能名称: 折叠按钮键盘支持 优先级: P2
功能描述
折叠按钮支持键盘操作,用户可以使用 Enter 或 Space 键触发折叠/展开。
键盘操作
| 按键 | 操作 |
|---|---|
| Tab | 聚焦到按钮 |
| Enter | 触发折叠/展开 |
| Space | 触发折叠/展开 |
验收标准
- Tab 键可以聚焦到折叠按钮
- 聚焦时有视觉反馈
- Enter 键触发折叠/展开
- Space 键触发折叠/展开
F6.3 ARIA 属性支持
功能编号: F6.3 功能名称: ARIA 属性支持 优先级: P2
功能描述
为所有交互元素添加完整的 ARIA 属性,支持屏幕阅读器。
ARIA 属性清单
Resizer:
role="separator"aria-orientation="vertical"aria-label="调整[面板名称]宽度"aria-valuenow: 当前宽度值aria-valuemin: 最小宽度aria-valuemax: 最大宽度tabindex="0"
折叠按钮:
aria-label="折叠[面板名称]"或"展开[面板名称]"aria-expanded="true"或"false"title: 提示文本
折叠指示器:
role="button"aria-label="展开[面板名称]"tabindex="0"
验收标准
- 所有 resizer 有完整的 ARIA 属性
- 所有折叠按钮有完整的 ARIA 属性
- 所有折叠指示器有完整的 ARIA 属性
- aria-valuenow 实时更新
- aria-expanded 实时更新
F6.4 焦点管理
功能编号: F6.4 功能名称: 焦点管理 优先级: P2
功能描述
合理管理键盘焦点,确保用户使用键盘操作时体验流畅。
焦点规则
-
折叠面板后:
- 焦点移到折叠指示器
-
展开面板后:
- 焦点移到折叠按钮
-
Tab 顺序:
- 导航栏按钮 → 分组面板折叠按钮 → 分组面板 resizer → 账号面板折叠按钮 → ...
-
模态框打开:
- 焦点移到模态框的第一个可聚焦元素
-
模态框关闭:
- 焦点返回到触发按钮
验收标准
- 折叠后焦点移到指示器
- 展开后焦点移到折叠按钮
- Tab 顺序合理
- 模态框焦点管理正确
九、实现细节
9.1 文件修改清单
需要修改的文件
| 文件路径 | 修改内容 | 优先级 |
|---|---|---|
templates/index.html |
修改主容器结构,添加 resizer 和折叠按钮 | P0 |
static/css/main.css |
添加 Grid 布局、CSS 变量、resizer 样式 | P0 |
static/js/main.js |
初始化 LayoutManager | P0 |
需要新增的文件
| 文件路径 | 文件内容 | 优先级 |
|---|---|---|
static/js/layout-manager.js |
LayoutManager 类实现 | P0 |
static/js/state-manager.js |
StateManager 类实现 | P0 |
9.2 HTML 结构变更
当前结构
<div class="main-container">
<aside class="group-panel">...</aside>
<aside class="account-panel">...</aside>
<aside class="email-list-panel">...</aside>
<main class="email-detail-panel">...</main>
</div>
目标结构
<div class="main-container">
<!-- 分组面板 -->
<aside class="resizable-panel group-panel" data-panel="groups">
<div class="panel-header">
<span class="panel-title">分组</span>
<button class="collapse-btn" aria-label="折叠分组面板" aria-expanded="true">
<svg><!-- 左箭头 --></svg>
</button>
</div>
<div class="panel-content">
<!-- 原有内容 -->
</div>
<div class="resizer"
role="separator"
aria-orientation="vertical"
aria-label="调整分组面板宽度"
aria-valuenow="200"
aria-valuemin="150"
aria-valuemax="400"
tabindex="0"
data-resizer="groups">
</div>
</aside>
<!-- 折叠指示器(折叠时显示) -->
<div class="collapsed-indicator" data-panel="groups" style="display: none;">
<span class="indicator-text">分组</span>
</div>
<!-- 账号面板、邮件列表面板(结构同上) -->
<!-- ... -->
<!-- 邮件详情面板(不可折叠,无 resizer) -->
<main class="email-detail-panel">...</main>
</div>
9.3 CSS 变量系统
:root {
/* 面板初始宽度 */
--groups-panel-width: 200px;
--accounts-panel-width: 260px;
--emails-panel-width: 380px;
/* 最小/最大宽度 */
--groups-panel-min-width: 150px;
--groups-panel-max-width: 400px;
--accounts-panel-min-width: 180px;
--accounts-panel-max-width: 500px;
--emails-panel-min-width: 280px;
--emails-panel-max-width: 600px;
--email-detail-panel-min-width: 400px;
/* 折叠状态 */
--panel-collapsed-width: 0px;
--collapator-width: 48px;
/* Resizer */
--resizer-width: 4px;
/* 动画 */
--panel-transition-duration: 200ms;
--reset-transition-duration: 300ms;
}
9.4 JavaScript 模块设计
LayoutManager 类
class LayoutManager {
constructor() {
this.isResizing = false;
this.currentPanel = null;
this.startX = 0;
this.startWidth = 0;
this.pendingSave = null;
this.stateManager = new StateManager();
}
// 初始化
init() { }
// 拖动相关
startResize(e) { }
resize(e) { }
stopResize() { }
// 键盘相关
handleKeyboard(e) { }
// 折叠相关
togglePanel(panelName) { }
collapsePanel(panelName, auto = false) { }
expandPanel(panelName) { }
// 重置相关
resetLayout() { }
// 窗口自适应
handleWindowResize() { }
// 状态管理
saveStateDebounced() { }
saveStateNow() { }
loadState() { }
// 工具方法
updatePanelWidth(panel, width) { }
getMinWidth(panelType) { }
getMaxWidth(panelType) { }
calculateOtherPanelsWidth(excludePanel) { }
updateAriaValue(resizer, value) { }
getCurrentUserId() { }
}
StateManager 类
class StateManager {
constructor() {
this.storageKeyPrefix = 'outlook_layout_state_';
}
// 保存状态
save(userId, state) { }
// 加载状态
load(userId) { }
// 清除状态
clear(userId) { }
// 验证数据
validate(state) { }
// 版本迁移
migrate(state) { }
// 获取存储键名
getStorageKey(userId) { }
}
十、验收标准总览
10.1 功能验收标准
P0 - 必须通过(核心功能)
拖动调整宽度:
- F1.1 - 可以拖动调整分组面板宽度
- F1.1 - 可以拖动调整账号面板宽度
- F1.1 - 可以拖动调整邮件列表宽度
- F1.2 - 宽度限制生效(最小/最大)
- F1.3 - 邮件详情面板始终可见且 ≥ 400px
面板折叠/展开:
- F2.1 - 可以折叠/展开分组面板
- F2.1 - 可以折叠/展开账号面板
- F2.1 - 可以折叠/展开邮件列表
- F2.2 - 折叠/展开动画流畅(200ms)
- F2.5 - 折叠后 resizer 隐藏
- F2.6 - 不能折叠最后一个面板
状态持久化:
- F4.1 - 拖动后 500ms 保存状态
- F4.1 - 折叠后立即保存状态
- F4.2 - 刷新页面后状态恢复
P1 - 重要功能
视觉反馈:
- F1.4 - 拖动时有视觉反馈
- F2.3 - 折叠后显示 48px 指示器
- F2.4 - 点击指示器可以展开
恢复默认布局:
- F3.1 - 导航栏显示重置按钮
- F3.2 - 点击重置显示确认对话框
- F3.3 - 确认后恢复默认布局
状态管理:
- F4.3 - 多用户布局隔离
- F4.5 - 页面卸载时强制保存
窗口自适应:
- F5.1 - 监听窗口大小变化
- F5.2 - 窗口 < 1200px 自动折叠分组
- F5.2 - 窗口 < 900px 自动折叠账号
- F5.2 - 窗口 < 700px 自动折叠邮件列表
P2 - 优化功能
动画优化:
- F3.4 - 重置动画流畅(300ms)
- F5.3 - 媒体查询降级生效
键盘可访问性:
- F6.1 - Resizer 支持键盘操作
- F6.2 - 折叠按钮支持键盘操作
- F6.3 - ARIA 属性完整
- F6.4 - 焦点管理合理
10.2 非功能验收标准
性能
- 拖动时帧率 ≥ 60fps
- 折叠/展开动画流畅无卡顿
- localStorage 数据 ≤ 1KB
- 页面加载时间增加 ≤ 50ms
兼容性
- Chrome 90+ 正常工作
- Edge 90+ 正常工作
- Firefox 88+ 正常工作
- Safari 14+ 正常工作
可访问性
- 所有交互元素可键盘访问
- 所有交互元素有 ARIA 属性
- 屏幕阅读器可正常使用
10.3 回归验证清单
现有功能不受影响
- 邮件列表正常显示
- 邮件详情正常显示
- 点击邮件可以查看详情
- 账号切换正常
- 分组切换正常
- 邮件刷新正常
- 邮件删除正常
- Token 刷新正常
- 搜索功能正常
- 原有的响应式布局正常
十一、测试策略
11.1 单元测试
测试文件: tests/test_layout_manager.js
测试用例:
- LayoutManager 初始化
- 宽度计算逻辑
- 宽度限制逻辑
- 状态序列化/反序列化
- 用户 ID 获取逻辑
11.2 集成测试
测试场景:
- 拖动调整宽度 → 保存 → 刷新 → 恢复
- 折叠面板 → 保存 → 刷新 → 恢复
- 重置布局 → localStorage 清除
- 窗口缩小 → 自动折叠 → 窗口放大
- 多用户切换 → 布局独立
11.3 用户验收测试
测试场景:
- 新用户首次访问 → 默认布局
- 调整布局 → 刷新 → 布局恢复
- 折叠所有面板 → 阻止最后一个
- 拖动到极限 → 宽度限制生效
- 小屏幕访问 → 自动折叠
11.4 兼容性测试
测试矩阵:
| 浏览器 | 版本 | 操作系统 | 测试结果 |
|---|---|---|---|
| Chrome | 90+ | Windows | |
| Chrome | 90+ | macOS | |
| Edge | 90+ | Windows | |
| Firefox | 88+ | Windows | |
| Firefox | 88+ | macOS | |
| Safari | 14+ | macOS |
十二、风险和依赖
12.1 技术风险
| 风险 | 影响 | 概率 | 缓解措施 |
|---|---|---|---|
| CSS Grid 兼容性 | 高 | 低 | 提前测试目标浏览器 |
| 拖动性能问题 | 中 | 中 | 使用 requestAnimationFrame |
| localStorage 配额满 | 低 | 低 | 降级到默认布局 |
| 现有功能受影响 | 高 | 中 | 充分回归测试 |
12.2 依赖关系
- 无后端依赖:纯前端功能,不需要后端支持
- 无第三方库依赖:使用原生 JavaScript 实现
- CSS 特性依赖:CSS Grid、CSS 变量、CSS transitions
12.3 缓解措施
- 充分测试:在所有目标浏览器上测试
- 渐进增强:使用媒体查询作为降级方案
- 错误处理:localStorage 不可用时使用默认布局
- 性能优化:使用防抖、节流、requestAnimationFrame
十三、附录
13.1 参考文档
- PRD:
PRD/PRD-可调整布局系统.mdv1.1 - EnsoAI:
exeoAI/src/renderer/components/layout/ - CSS Grid: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout
- ARIA: https://www.w3.org/WAI/ARIA/apg/
13.2 功能清单汇总
| 模块 | 子功能数 | P0 | P1 | P2 |
|---|---|---|---|---|
| F1 - 可拖动调整宽度 | 4 | 3 | 1 | 0 |
| F2 - 面板折叠/展开 | 6 | 4 | 2 | 0 |
| F3 - 恢复默认布局 | 4 | 0 | 3 | 1 |
| F4 - 状态持久化 | 5 | 2 | 3 | 0 |
| F5 - 窗口尺寸自适应 | 3 | 0 | 2 | 1 |
| F6 - 键盘可访问性 | 4 | 0 | 0 | 4 |
| 总计 | 26 | 9 | 11 | 6 |
13.3 变更记录
| 版本 | 日期 | 变更内容 | 变更人 |
|---|---|---|---|
| v1.0 | 2026-02-26 | 初始版本,包含 6 个功能模块、26 个子功能 | 开发团队 |
文档结束