HarmonyOS6.1.1-Tabs:实施问题按阶段和专业分层时-怎样交接选中状态、阅读位置与未结事项
项目问题清单往往需要分层管理:按阶段(待办、修复中、待验证)和按类型(缺陷、风险、需求)。当多个团队成员同时使用时,核心问题不是"Tabs能否切换",而是"我在外层Tabs切换后,内层的选中位置能否被记住?下次打开应用,这些位置能否恢复?多人同时修改时,状态会否混乱?"本工程实现了双层Tabs的本地状态保留机制,通过状态机管理来追踪选中位置、阅读位置和列表滚动位置,但这套机制仅限于本地浏览器存储,不涉及实时同步、多人锁定或服务端持久化。因此本文的目标是建立实施口径:如何在现场验证Tabs状态保留的可靠性、多人协作时如何识别状态不同步的原因、分层规则与业务流程如何对齐。
一、把"分层问题管理"拆成四个独立验收结论
现场验收时不应笼统说"分层功能可用",而要拆分为四个彼此独立、分别对应不同责任方的结论:
| 验收结论 | 当前工程能否支持 | 现场可观察证据 | 下一责任方 |
|---|---|---|---|
| Tabs交互流畅 | 可以 | 外层(待办/修复中/待验证)与内层(缺陷/风险/需求)可无延迟切换,无闪烁/卡顿/崩溃 | 应用开发 |
| 选中状态被保留 | 可以 | 切换外层后再切回,内层的选中项和滚动位置相同;关闭应用再打开,所有位置恢复 | 应用对接 |
| 问题数据准确 | 部分 | 页面显示的问题数、优先级分布与后端业务系统一致;新增/修改问题后刷新是否能看到最新数据 | 问题管理系统 |
| 多人协作无冲突 | 不支持 | 需要后端状态同步、冲突检测、权限验证和操作审计 | 应用服务 |
这不是措辞游戏。四个结论分别对应:页面交互可靠性、本地状态持久化、业务数据一致性、多人协作协调。当现场出现"用户A看到问题001,用户B看不到"时,应快速判断是Tabs选中状态丢失、还是数据来源不一致、还是权限问题。
二、项目功能详解:双层Tabs与状态保留的完整实现
2.1 Tabs层级结构与状态初始化
Tabs组件采用双层设计:外层管理业务阶段(待办、修复中、待验证),内层管理问题类型(缺陷、风险、需求)。每一层的状态都需要独立维护和恢复。工程在应用启动时从本地存储中恢复上一次的选中位置:
class ProjectIssueTabsController {
private outerTabIndex: number = 0; // 外层Tabs的选中索引(默认0=待办)
private innerTabIndex: number = 0; // 内层Tabs的选中索引(默认0=缺陷)
private scrollPositions: Map<string, number> = new Map(); // 每个Tabs下的滚动位置
private selectedIssueIds: Map<string, string> = new Map(); // 每个Tabs下的选中问题ID
private stateChangeLog: TabsStateChangeRecord[] = []; // 用于审计的状态变更日志
constructor() {
// 应用启动时恢复上一次的Tabs状态
this.restoreTabsState();
}
// 从localStorage恢复上一次的Tabs状态
private restoreTabsState(): void {
try {
const savedState = localStorage.getItem('projectIssuesTabsState');
if (savedState) {
const state = JSON.parse(savedState);
this.outerTabIndex = state.outerTabIndex ?? 0;
this.innerTabIndex = state.innerTabIndex ?? 0;
this.scrollPositions = new Map(Object.entries(state.scrollPositions || {}));
this.selectedIssueIds = new Map(Object.entries(state.selectedIssueIds || {}));
this.recordStateChange('RESTORE', 'Tabs状态已从本地存储恢复');
}
} catch (error) {
// 如果解析失败,使用默认状态
this.recordStateChange('RESTORE_FAILED', `本地状态恢复失败: ${error.message}`);
this.outerTabIndex = 0;
this.innerTabIndex = 0;
}
}
// 记录每一次Tabs状态变更
private recordStateChange(action: string, reason: string): void {
this.stateChangeLog.push({
timestamp: new Date().toISOString(),
action,
outerTabIndex: this.outerTabIndex,
innerTabIndex: this.innerTabIndex,
reason
});
// 同时保存到localStorage,以便应用重启后恢复
this.persistTabsState();
}
// 将Tabs状态持久化到localStorage
private persistTabsState(): void {
const state = {
outerTabIndex: this.outerTabIndex,
innerTabIndex: this.innerTabIndex,
scrollPositions: Object.fromEntries(this.scrollPositions),
selectedIssueIds: Object.fromEntries(this.selectedIssueIds),
lastUpdated: new Date().toISOString()
};
localStorage.setItem('projectIssuesTabsState', JSON.stringify(state));
}
}
这段代码能支撑的验收结论:
- 应用启动时能从localStorage恢复上一次的外层/内层Tabs位置
- 每一次Tabs切换都有状态变更日志可查
- 状态持久化机制实现了"关闭再打开后恢复位置"的功能
它不能支撑的结论:
- 多个浏览器标签页中的Tabs状态会自动同步
- 其他设备或用户能看到这个用户的Tabs位置
- 服务端数据库中有持久化的用户Tabs偏好记录
2.2 外层Tabs切换与内层状态隔离
当用户从"待办"切换到"修复中"时,内层Tabs的选中位置需要被记住。工程通过为每个外层Tabs生成唯一的键(例如 outer_1_inner_2 表示外层索引1、内层索引2),来管理不同组合下的独立状态:
class ProjectIssueTabsController {
// 获取当前Tabs组合的唯一键
private getTabsCombinationKey(): string {
return `outer_${this.outerTabIndex}_inner_${this.innerTabIndex}`;
}
// 处理外层Tabs切换
public handleOuterTabChange(newOuterIndex: number): void {
if (newOuterIndex === this.outerTabIndex) {
// 点击了当前已选中的外层Tabs,忽略
return;
}
// 保存当前外层下的内层位置和滚动位置
const oldKey = this.getTabsCombinationKey();
const scrollPosition = this.getScrollPosition();
if (scrollPosition !== undefined) {
this.scrollPositions.set(oldKey, scrollPosition);
}
// 切换到新的外层Tabs
this.outerTabIndex = newOuterIndex;
// 检查新的外层下是否有之前保存的内层位置,如果有就恢复
const newKey = this.getTabsCombinationKey();
const savedScrollPosition = this.scrollPositions.get(newKey);
this.recordStateChange('OUTER_TAB_CHANGE',
`外层Tabs从${this.outerTabIndex === 0 ? '待办' : this.outerTabIndex === 1 ? '修复中' : '待验证'}切换到新选项,内层位置${savedScrollPosition ? '已恢复' : '重置'}`);
// 如果有保存的滚动位置,立即恢复
if (savedScrollPosition !== undefined) {
this.restoreScrollPosition(savedScrollPosition);
}
}
// 处理内层Tabs切换
public handleInnerTabChange(newInnerIndex: number): void {
if (newInnerIndex === this.innerTabIndex) {
return;
}
// 保存当前的滚动位置
const currentKey = this.getTabsCombinationKey();
const scrollPosition = this.getScrollPosition();
if (scrollPosition !== undefined) {
this.scrollPositions.set(currentKey, scrollPosition);
}
// 切换到新的内层Tabs
this.innerTabIndex = newInnerIndex;
// 尝试恢复该内层对应的滚动位置
const newKey = this.getTabsCombinationKey();
const savedScrollPosition = this.scrollPositions.get(newKey);
this.recordStateChange('INNER_TAB_CHANGE',
`内层Tabs切换到 ${['缺陷', '风险', '需求'][newInnerIndex]},滚动位置${savedScrollPosition ? '已恢复' : '重置'}`);
if (savedScrollPosition !== undefined) {
this.restoreScrollPosition(savedScrollPosition);
}
}
// 获取列表当前的滚动位置(像素)
private getScrollPosition(): number | undefined {
// 从DOM中读取当前列表的滚动偏移量
const issueList = document.getElementById('issue-list');
return issueList?.scrollTop;
}
// 恢复列表到指定的滚动位置
private restoreScrollPosition(position: number): void {
const issueList = document.getElementById('issue-list');
if (issueList) {
issueList.scrollTop = position;
}
}
}
这段代码能支撑的验收结论:
- 切换外层Tabs后,内层的选中项保持不变
- 每个外层/内层组合都有独立的滚动位置记录
- 用户的阅读位置被恢复,体验连贯
它不能支撑的结论:
- 在多个浏览器标签页中,Tabs状态会自动同步
- 其他用户的Tabs位置对当前用户可见
- 滚动位置在服务端被持久化用于数据分析
2.3 选中问题项的独立追踪
在某个Tabs下,用户可能会选中一个具体的问题项(例如问题ID=“ISSUE-0882”)。这个选中状态也需要按照不同的Tabs组合独立保存:
class ProjectIssueTabsController {
// 处理用户选中某个问题项
public selectIssue(issueId: string): void {
const key = this.getTabsCombinationKey();
// 如果同一项被连续选中两次,认为是打开详情
const currentSelected = this.selectedIssueIds.get(key);
if (currentSelected === issueId) {
this.recordStateChange('ISSUE_DETAIL_OPEN', `问题 ${issueId} 详情已打开`);
// 触发详情窗口打开(由UI层处理)
this.openIssueDetail(issueId);
return;
}
// 第一次选中,记录选中状态
this.selectedIssueIds.set(key, issueId);
this.recordStateChange('ISSUE_SELECT', `在 ${key} 下选中问题 ${issueId}`);
this.persistTabsState();
}
// 获取当前Tabs下应该展示为选中状态的问题ID
public getSelectedIssueId(): string | undefined {
const key = this.getTabsCombinationKey();
return this.selectedIssueIds.get(key);
}
// 打开问题详情(仅在外部能看到有效性)
private openIssueDetail(issueId: string): void {
// 这段逻辑在工程中仅作为桩函数
// 真实项目应由业务系统负责实现
console.log(`[业务系统接手] 打开问题详情: ${issueId}`);
}
}
这段代码能支撑的验收结论:
- 用户在某个Tabs下选中的问题项会被记录
- 切换Tabs后再切回,之前的选中项仍处于选中状态
- 选中状态在localStorage中被持久化
它不能支撑的结论:
- 问题详情窗口能打开(这是业务系统的职责)
- 选中状态的变更会通知其他用户
- 问题的业务状态(如"已完成")会自动反映在UI中
2.4 实施现场的Tabs状态验收核对表
实施顾问在现场应按以下步骤逐项验证Tabs状态保留机制:
| 验证步骤 | 操作 | 预期结果 | 失败处理 |
|---|---|---|---|
| 初始状态 | 打开应用,记录当前外层/内层Tabs位置 | 外层=待办(0),内层=缺陷(0) | 检查localStorage是否被清除 |
| 外层切换 | 点击"修复中",观察内层是否变化 | 内层保持在缺陷(0),列表刷新显示修复中的缺陷 | 检查是否误触发了内层Tabs |
| 内层切换 | 在"修复中"下点击"风险",再切回"缺陷" | 列表内容变化,滚动位置恢复 | 检查滚动位置是否被正确保存 |
| 问题选中 | 在"修复中-缺陷"下选中问题001,切到"待办"再切回 | 问题001仍处于选中状态 | 检查selectedIssueIds是否被清空 |
| 应用重启 | 关闭应用,重新打开 | 外层/内层Tabs位置、滚动位置、选中问题都恢复 | 检查localStorage是否可读写 |
| 多次快速切换 | 在短时间内连续切换Tabs | 无卡顿、无数据混乱、无状态丢失 | 检查是否存在并发竞态条件 |
三、企业实施风险预案与分阶段交付
3.1 Tabs分层管理的六大风险识别与应急方案
| 风险项 | 等级 | 预防措施 | 检测方法 | 应急方案 | 恢复步骤 | 责任人 |
|---|---|---|---|---|---|---|
| 选中状态丢失 | 高 | ① 每次切换时立即保存到localStorage ② 应用启动时恢复 ③ 定期备份状态(每30秒) | 关闭再打开应用,检查位置是否恢复;查看localStorage内容 | ① 用户手动重新选择Tabs ② 清除localStorage后重启 ③ 检查浏览器是否禁用localStorage | ① 确认localStorage可用 ② 状态恢复成功 ③ 提示用户已恢复 | 应用开发 |
| 多人协作状态混乱 | 严重 | ① 设置自动刷新周期(15秒)② 修改时触发全局刷新 ③ 添加操作时间戳 | 多用户同时打开,各自修改Tabs位置,观察是否不同步;检查时间戳差异 | ① 所有用户刷新应用 ② 切换Tabs后自动重载数据 ③ 通知用户等待同步完成 | ① 数据与服务端同步 ② 所有用户的Tabs视图一致 ③ 确认无数据遗漏 | 问题管理 |
| Tabs切换卡顿 | 中 | ① 异步加载问题列表数据 ② 虚拟滚动优化(仅渲染可见项)③ 数据分页(每页50条) | 加载1000条问题,测试切换响应时间;监控CPU和内存占用 | ① 自动降级到缓存数据 ② 显示加载进度条 ③ 允许用户取消加载 | ① 列表加载完成 ② 用户可继续操作 ③ 性能指标恢复正常 | 应用开发 |
| 滚动位置计算错误 | 中 | ① 在保存前验证滚动值(0 ≤ position ≤ maxScroll)② 使用相对位置而非绝对像素 ③ 屏幕旋转时重新计算 | 在不同屏幕尺寸和方向下切换Tabs,验证滚动位置是否正确恢复 | ① 清除保存的滚动位置 ② 用户手动滚动到需要的位置 ③ 提示状态已重置 | ① 滚动位置恢复 ② 列表显示正确内容 ③ 用户能看到需要的问题 | 应用对接 |
| 权限混乱导致数据泄露 | 严重 | ① 前端验证用户权限 ② 后端再次验证 ③ 敏感操作加审计 | 低权限用户试图选中不应看到的问题;查审计日志 | ① 立即清除localStorage中的敏感数据 ② 重新登录验证权限 ③ 通知安全部门 | ① 权限已验证 ② 只显示授权问题 ③ 审计日志完整 | 安全部门 |
| localStorage超额或被清除 | 中 | ① 定期清理过期状态数据 ② 监控存储使用量 ③ 使用SessionStorage作为备份 | localStorage大小超过5MB;浏览器清空缓存后再打开应用 | ① 检测存储失败时,回退到内存存储 ② 提示用户浏览器存储已满 ③ 建议清理浏览器缓存 | ① 手动清理无用数据 ② 恢复localStorage可用状态 ③ 再次尝试保存 | 应用开发 |
3.2 分阶段交付计划与交接标准
第一期:Tabs交互与本地状态保留(2周)
交付范围:双层Tabs结构、选中项和滚动位置的独立管理、localStorage恢复机制
| 交接检查项 | 验收标准 | 验证方法 |
|---|---|---|
| 外层Tabs可切换 | ≥3次切换无异常、响应 < 300ms | 快速连续点击外层各Tabs |
| 内层Tabs可切换 | ≥3次切换无异常、内层内容更新 < 500ms | 在各外层下分别切换内层 |
| 选中状态保留 | 切换外层后再切回,内层选中项相同 | 记录初始选中项 → 切换外层 → 切回 → 对比 |
| 滚动位置保留 | 滚动到列表中部 → 切换Tabs → 切回,位置相同 | 记录初始滚动偏移 → 切换 → 切回 → 对比数值 |
| 状态恢复 | 关闭应用 → 重新打开,所有状态恢复到关闭前 | 记录关闭前的完整状态 → 重启 → 逐项对比 |
| 多屏适配 | ≥2种屏幕尺寸和方向正常工作 | 在手机竖屏、横屏、平板上分别测试 |
| 无数据丢失 | 100次随机切换后,localStorage中的数据完整 | 写压测脚本,验证数据一致性 |
回滚条件:
- Tabs切换时页面崩溃
- 选中状态丢失超过1次
- 滚动位置计算错误导致用户看不到需要的内容
- 多屏测试中任一屏幕尺寸失败
交接方:应用开发 → 应用对接
第二期:问题数据集成与实时同步(2周)
交付范围:后端API集成、分页加载、数据刷新、进度计算
| 交接检查项 | 验收标准 | 验证方法 |
|---|---|---|
| 数据加载 | 首次加载 < 3秒、分页加载 < 2秒、成功率 > 99% | 测试网络各种速度下的加载时间;打开应用100次检查失败率 |
| 自动刷新 | 后端数据变更15秒内显示在页面 | 修改后端数据库 → 观察页面更新延迟 |
| 手动刷新 | 点击刷新按钮,立即重新加载数据 | 修改数据 → 手动刷新 → 验证显示最新值 |
| 数据一致 | 页面显示的数据与后端数据库比对,准确率 > 99.5% | 抽取100条数据逐项验证 |
| 进度计算 | 显示的完成度/优先级分布等与业务系统计算值偏差 < 2% | 对比Excel报表中的统计值 |
| 分页正确 | 下一页/上一页导航正确,无重复或遗漏 | 分页遍历全部数据,检查总数一致性 |
回滚条件:
- 数据加载失败率 > 1%
- 数据与后端差异 > 1%
- 自动刷新延迟 > 30秒
交接方:应用对接 → 问题管理
第三期:多人协作与冲突处理(3周)
交付范围:实时数据同步、冲突检测、权限验证、操作审计
| 交接检查项 | 验收标准 | 验证方法 |
|---|---|---|
| 权限验证 | 低权限用户无法修改Tabs下的问题;高权限用户可正常操作 | 分别用两种权限账户测试修改操作 |
| 冲突处理 | 两个用户同时修改同一问题,采用"最后修改者优先"策略 | 两个浏览器同时修改同一条数据,观察结果 |
| 实时通知 | 用户A修改数据后,用户B < 5秒收到通知 | 打开两个会话,一个修改 → 观察另一个的更新延迟 |
| 审计完整 | 每次操作都有记录(用户/时间/新旧值),日志无遗漏 | 执行50次修改,逐条检查审计日志 |
| 并发安全 | 10个用户同时修改,无数据破坏 | 压测工具模拟并发修改 |
回滚条件:
- 权限检查失效,低权用户能修改
- 冲突处理时数据被破坏
- 审计日志丢失 > 0.5%
- 并发测试中出现数据错误
交接方:问题管理 + 应用开发 → 安全部门验证 → 最终上线
3.3 交接点检查与风险评估
| 交接检查点 | 第一期→第二期 | 第二期→第三期 |
|---|---|---|
| 稳定性观察期 | +1周,在真实网络环境下持续运行,无新增异常 | +1周,多人并发使用,无冲突 |
| 数据准备 | 完整的测试问题数据集(≥1000条)已加载 | 100万条生产问题数据已迁移,权限配置完成 |
| 参与角色 | 现场工程师 ✓、应用对接 ✓、测试人员 ✓ | 问题管理 ✓、应用开发 ✓、安全部门 ✓ |
| 问题处理 | 第一期报告的所有问题已修复并回归测试 | 第二/三期新发现的问题,确定责任方并跟进 |
| 文档更新 | 用户操作手册已完成,包含Tabs使用说明 | 系统管理员手册已完成,包含权限配置说明 |
| 回滚准备 | 已准备第一期的回滚方案(如需要) | 已准备第二/三期的回滚方案和应急数据恢复方案 |
四、现场场景(3个真实场景)
场景1:工程师A和工程师B因Tabs位置不同步而产生的协作混乱
背景:
- 项目组有5名工程师,共享一个问题管理页面
- 工程师A在"待办-缺陷"标签页
- 工程师B在"修复中-风险"标签页
- A说"我把缺陷001从待办转到修复中了",但B在刷新后仍然看不到这个问题
时间线与现象:
T0: A切换到"修复中-缺陷"标签,看到缺陷001,开始修复
T1: A修改缺陷001的状态为"修复中",保存
T2: B在"修复中-风险"标签,看不到缺陷001(因为B选的是"风险"而非"缺陷")
T3: A告诉B"我已经转移了",但B看不到
T4: 问题根源:A和B的"内层Tabs选中位置"不同步,不是数据问题
诊断步骤:
class ScenarioDiagnostics {
// 步骤1:检查A和B的Tabs状态是否同步
public diagnoseTabsStateMismatch(userA_State: any, userB_State: any): void {
const keyA = `outer_${userA_State.outerTabIndex}_inner_${userA_State.innerTabIndex}`;
const keyB = `outer_${userB_State.outerTabIndex}_inner_${userB_State.innerTabIndex}`;
if (keyA !== keyB) {
console.log(`❌ Tabs位置不同步`);
console.log(` A的位置: ${keyA}`);
console.log(` B的位置: ${keyB}`);
console.log(`✅ 解决方案:告知B"你现在看的是'风险',请切到'缺陷'来查看001"`);
}
}
// 步骤2:验证数据是否真的被保存
public verifyDataSaved(issueId: string, expectedStatus: string): void {
// 调用后端API检查该问题的当前状态
const issue = fetchIssueFromServer(issueId);
if (issue.status === expectedStatus) {
console.log(`✅ 数据已正确保存到服务端`);
} else {
console.log(`❌ 数据未保存到服务端,可能是网络问题或权限问题`);
}
}
// 步骤3:检查B的浏览器是否有缓存问题
public checkBrowserCache(): void {
const cachedData = localStorage.getItem('projectIssuesTabsState');
if (cachedData) {
console.log(`localStorage中的Tabs状态:`, JSON.parse(cachedData));
console.log(`建议:清除缓存后刷新,或按F5强制刷新`);
}
}
}
现场处理:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 让工程师B查看当前选中的内层Tabs | 确认B在"修复中-风险"而非"修复中-缺陷" |
| 2 | 告知B切换到"缺陷"标签 | 缺陷001应该出现在B的列表中 |
| 3 | 如果仍看不到,让B按Ctrl+Shift+Delete清缓存 | 刷新后缺陷001应该出现 |
| 4 | 如果仍看不到,检查问题001的权限 | 确认B有查看权限 |
| 5 | 最后手段:让B关闭浏览器标签,重新打开应用 | 所有状态应该恢复,问题应该可见 |
防止措施:
- 在Tabs切换时,显示当前选中的外层+内层组合(如"修复中 > 缺陷")
- 在问题列表顶部显示当前Tabs过滤条件
- 添加"自动同步其他用户的Tabs视图"的选项(需要后端支持)
场景2:应用崩溃或网络中断后,Tabs位置和阅读位置的恢复
背景:
- 工程师正在"待办-风险"标签下查看风险列表
- 已滚动到列表的第三页(显示问题401-450)
- 突然应用崩溃或网络掉线
预期行为:
class CrashRecoveryScenario {
// 应用重启时的恢复流程
public appRestartRecovery(): void {
// 1. 从localStorage恢复Tabs状态
const savedState = JSON.parse(localStorage.getItem('projectIssuesTabsState'));
console.log(`恢复Tabs状态: outerTab=${savedState.outerTabIndex}, innerTab=${savedState.innerTabIndex}`);
// 2. 恢复滚动位置
const key = `outer_${savedState.outerTabIndex}_inner_${savedState.innerTabIndex}`;
const savedScroll = savedState.scrollPositions[key];
console.log(`恢复滚动位置: ${savedScroll}px`);
// 3. 重新加载问题数据
const issues = fetchIssuesFromServer(savedState.outerTabIndex, savedState.innerTabIndex);
// 4. 列表加载完成后,恢复滚动位置
setTimeout(() => {
document.getElementById('issue-list').scrollTop = savedScroll;
console.log(`✅ 应用已恢复到崩溃前的状态`);
}, 500);
}
// 网络中断时的本地缓存降级
public networkInterruptionFallback(): void {
console.log(`🔶 检测到网络中断,将使用本地缓存数据`);
// 从localStorage读取上一次的问题列表缓存
const cachedIssues = JSON.parse(localStorage.getItem('cachedIssues'));
if (cachedIssues && cachedIssues.length > 0) {
console.log(`✅ 本地缓存中有${cachedIssues.length}条问题,将显示这些数据`);
displayIssuesWithCacheNotice(cachedIssues, '离线模式:显示最后一次同步的数据');
} else {
console.log(`❌ 无本地缓存可用,请等待网络恢复后刷新`);
}
}
}
现场验证步骤:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 打开应用,导航到"待办-风险",滚动到第三页 | 记录当前状态 |
| 2 | 手动杀死应用进程(或网络断开) | 应用停止响应 |
| 3 | 重新打开应用 | 应看到"待办-风险"标签,列表滚动到第三页 |
| 4 | 如果网络仍中断,应显示缓存数据+离线提示 | 用户能看到上一次同步的问题列表 |
| 5 | 网络恢复后,点击"刷新"按钮 | 最新数据应该加载 |
失败现象与处理:
| 失败现象 | 原因 | 处理方式 |
|---|---|---|
| 重启后Tabs位置重置为"待办-缺陷" | localStorage被清除或损坏 | 检查浏览器存储配额;手动切回"待办-风险" |
| 列表滚动位置没有恢复 | scrollPositions Map未正确保存 | 手动向下滚动到需要的位置 |
| 离线模式下仍显示"加载中" | 缓存降级逻辑未执行 | 检查浏览器控制台中的错误;清缓存后重试 |
场景3:屏幕旋转或窗口拉伸时,Tabs状态与滚动位置的适应
背景:
- 用户在手机竖屏下使用应用,浏览"修复中-缺陷"列表
- 列表已滚动到第二页,第四个问题处于选中状态
- 用户将手机旋转到横屏
预期行为:
class ScreenOrientationScenario {
// 监听屏幕旋转事件
public onOrientationChange(): void {
console.log(`屏幕方向已改变`);
// 1. 获取当前的Tabs状态
const currentKey = `outer_${this.outerTabIndex}_inner_${this.innerTabIndex}`;
const currentScroll = this.getScrollPosition();
// 2. 保存当前状态到localStorage
this.recordStateChange('ORIENTATION_CHANGE', `屏幕已旋转,保存当前滚动位置 ${currentScroll}px`);
// 3. 等待DOM完成重新布局
window.addEventListener('orientationchange', () => {
// 4. 重新计算相对位置(因为横屏时每行显示的项数不同)
const newMaxScroll = this.calculateMaxScroll();
const relativePosition = currentScroll / this.previousMaxScroll; // 计算相对比例
const newScroll = relativePosition * newMaxScroll; // 应用到新的尺寸
// 5. 恢复到相对位置
this.restoreScrollPosition(newScroll);
console.log(`✅ 屏幕旋转后,列表已恢复到相应位置`);
});
}
// 计算当前可滚动的最大距离
private calculateMaxScroll(): number {
const issueList = document.getElementById('issue-list');
return issueList ? (issueList.scrollHeight - issueList.clientHeight) : 0;
}
}
现场验证步骤:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 用竖屏打开应用,导航到某个Tabs,滚动到中部 | 记录滚动位置和选中问题 |
| 2 | 将设备旋转到横屏 | 列表应以横屏布局显示 |
| 3 | 观察滚动位置 | 应恢复到相对相同的位置(虽然横屏时行数不同,但内容相同) |
| 4 | 观察选中状态 | 之前选中的问题应仍处于选中状态 |
| 5 | 再次旋转回竖屏 | 所有状态应恢复到最初的竖屏位置 |
失败现象:
- 旋转后滚动位置变为0(列表回到顶部)→ 绝对位置而非相对位置被保存
- 选中问题丢失 → selectedIssueIds未被正确保留
- 旋转时出现闪烁或抖动 → 重新布局时间过长,应使用CSS动画平滑过渡
异常处理与分流
当现场出现问题时,应按以下步骤快速分流给负责方:
class TabsIssueTriageFlow {
// 快速诊断问题类型
public triageIssue(symptom: string): void {
if (symptom === '外层Tabs切换时卡顿') {
console.log(`症状: 切换响应 > 1秒`);
console.log(`可能原因: ① 问题列表数据量大 ② 网络慢 ③ 渲染性能差`);
console.log(`检测步骤:`);
console.log(` 1. 查看浏览器DevTools中的Performance标签,录制切换操作`);
console.log(` 2. 检查是否是数据加载阻塞了UI线程`);
console.log(` 3. 如果是网络问题,检查Network标签中的请求时间`);
console.log(`转交: 应用开发 或 问题管理(取决于是应用问题还是数据问题)`);
}
else if (symptom === '内层Tabs状态丢失') {
console.log(`症状: 切换外层后再切回,内层选中位置/滚动位置不同`);
console.log(`可能原因: ① localStorage被清除 ② 浏览器禁用了localStorage ③ 状态保存代码bug`);
console.log(`检测步骤:`);
console.log(` 1. 打开浏览器DevTools的Application标签`);
console.log(` 2. 检查localStorage中的 projectIssuesTabsState 是否存在`);
console.log(` 3. 手动修改该值,看是否能被恢复`);
console.log(`转交: 应用开发`);
}
else if (symptom === '多人看到的问题列表不一样') {
console.log(`症状: 用户A和B在同一个Tabs下看到的问题数/优先级不同`);
console.log(`可能原因: ① 权限不同 ② 数据未同步 ③ 缓存版本不同`);
console.log(`检测步骤:`);
console.log(` 1. 确认两个用户的权限是否相同`);
console.log(` 2. 让两个用户都按Ctrl+Shift+Delete清缓存,再刷新`);
console.log(` 3. 如果仍不同,检查后端数据库中两个用户可见的问题集合`);
console.log(`转交: 问题管理 或 安全部门(权限问题)`);
}
else if (symptom === '应用重启后Tabs位置没有恢复') {
console.log(`症状: 关闭应用再打开,应该回到"修复中-风险",但现在是"待办-缺陷"`);
console.log(`可能原因: ① localStorage被禁用 ② 浏览器隐私模式不支持 ③ 存储配额已满`);
console.log(`检测步骤:`);
console.log(` 1. 在浏览器控制台执行: localStorage.setItem('test', 'value')`);
console.log(` 2. 检查是否抛出错误(如QuotaExceededError)`);
console.log(` 3. 如果是隐私模式,建议切换到普通模式`);
console.log(`转交: 应用开发 或 用户自行排查浏览器设置`);
}
}
}
FAQ
Q: Tabs状态保存在哪里?
A: 仅保存在浏览器的localStorage中,每次切换都会立即更新。如果用户清除浏览器缓存或使用隐私模式,状态会丢失。
Q: 多个浏览器标签页中的Tabs状态会同步吗?
A: 不会。每个标签页都有独立的localStorage副本。当前工程不支持跨标签页同步。如需实现,应在后端维护用户的Tabs偏好。
Q: 屏幕旋转时为什么滚动位置有时不对?
A: 如果保存的是绝对像素值,旋转后屏幕宽度改变,列表布局也改变,绝对像素值可能会"越界"。应该保存相对位置(百分比)而非绝对位置。
Q: 如何清除所有保存的Tabs状态?
A: 在浏览器控制台执行:localStorage.removeItem('projectIssuesTabsState'),然后刷新页面。
Q: 如果localStorage已满怎么办?
A: 浏览器会抛出QuotaExceededError。应应用应该捕获该错误并回退到内存存储(应用重启后状态会丢失)。同时提示用户清除浏览器缓存。
Q: 可以导出Tabs配置给其他用户吗?
A: 当前工程不支持。如需实现,应该在后端维护用户偏好配置,支持导入/导出。
必要条件|模拟器与真机准备对照
| 条件 | API 24 模拟器 | HarmonyOS 6.1.1 真机 |
|---|---|---|
| SDK/API与构建工具 | 使用 API 24 镜像验证构建和基础页面 | 使用兼容 API 24 的签名包安装 |
| Kit引入 | 先确认编译期 Kit 类型可用 | 再确认设备运行时模块实际可用 |
| 模块/页面配置 | 页面路由和 Stage 启动可验证 | 页面路由、签名和设备安装状态均需验证 |
| 权限 | 可演练授权弹窗和拒绝分支 | 需重新授权并确认系统设置中的真实状态 |
| 系统能力/硬件 | 只能代表模拟器提供的能力 | Camera、麦克风、地图、视觉识别等以真机能力为准 |
SDK/API 对照完成后插入 DevEco Studio API 24 与构建配置截图:

授权对照完成后插入真实设备权限截图:
版本和能力对照完成后插入设备/模拟器信息截图:

更多推荐


所有评论(0)