07|HarmonyOS资料板键鼠操作与无障碍替代【共创稿事节】
现场资料板在平板上使用时,拖动“配电柜现场.jpg”放进“设备照片”栏确实直观;到了带键盘和鼠标的大屏工位,这套交互就显出缺口。有人用Tab找到了卡片,却无法用键盘完成移动;有人借助屏幕朗读听见“图片”,却不知道卡片现在属于哪一栏;还有人从详情弹窗返回,焦点已经跑到页面顶部。拖拽功能本身仍然可用,但资料板不能把“能拖起来”当成唯一的整理入口。
前六篇已经统一了boardId、itemId、attachmentId、columnId、sortKey以及BoardStore的状态提交接口,拖拽取消、跨栏重排和附件重新关联都沿用同一份卡片事实。本篇不再增加一种平行的移动逻辑,而是给已有BoardStore增加键盘、鼠标菜单和辅助访问能够共同触发的移动命令。从卡片右上角菜单选择“移动到设备照片”,与按住卡片拖到目标栏,最后必须落到同一条业务校验路径。

把操作语义从手势中拆出来
资料板的三列仍为“待整理”“设备照片”“检修记录”。一张JPEG照片可以进入照片栏,一份PDF巡查单可以进入记录栏,来源失效的附件即便还保留卡片,也应在操作前给出清晰的状态提示。过去拖拽时,源组件生成卡片身份,目标组件形成dropIntent,最终交给BoardStore提交。键盘操作不需要伪造一个拖拽事件;它只需直接产生相同语义的moveIntent。
我把命令归一为cardId、targetColumnId、targetIndex、boardRevision和source。source仅记录交互来源,比如keyboard、pointerMenu或drag,不参与业务授权。真正的拒绝条件仍是卡片缺失、文件类型不符、附件不可访问、目标索引越界或版本过期。这样的设计能减少两套入口逐渐分叉的风险,也利于日后增加触控设备的长按菜单。
大屏宽度越宽,鼠标悬停层越丰富,但悬停提示不应承载唯一的指令。卡片本身需要一个可获焦的操作按钮;按钮展开的菜单明确列出可以选择的目标栏,对不允许的目标提供“文件类型不匹配”之类原因。对无法稳定拖动的用户,“选择卡片→选择目标栏→确认位置”就是完整的替代路径,而不是功能不全的降级页面。
命令菜单由领域规则生成
业务层可先根据卡片类型与栏目接收集合输出操作菜单,界面只负责渲染。下面的Node.js代码不绑定系统键码,也不依赖指针坐标;它可以由ArkUI页面调用同一组领域判断。
const assert = require('node:assert/strict');
function moveChoices(card, columns) {
return columns.map(column => {
const accepted = column.kinds.includes(card.kind);
const enabled = accepted && card.sourceState === 'READY';
return { columnId: column.id,
label: `移动到${column.label}`,
enabled,
reason: !accepted ? '文件类型不符合目标栏' :
card.sourceState !== 'READY' ? '附件来源需要重新关联' : '' };
});
}
const cols = [
{id:'photos',label:'设备照片',kinds:['image']},
{id:'records',label:'检修记录',kinds:['pdf','doc']}
];
assert.equal(moveChoices({kind:'image',sourceState:'READY'},cols)[0].enabled,true);
assert.equal(moveChoices({kind:'image',sourceState:'MISSING'},cols)[0].enabled,false);
这个菜单有意保留拒绝原因,不把所有禁用项悄悄隐藏。隐藏会让依赖键盘的操作者以为功能突然消失,也让屏幕朗读用户无法得知下一步应该重新关联文件还是选择别的栏位。按钮文案写具体动作,避免整页都是含义重复的“更多”和“确定”。如果菜单项会改变位置,辅助描述应告诉用户目标栏目以及移动后会发生的变化。
焦点要有身份,不依赖列表行号
卡片在前一篇已经支持来源重新关联,卡片内容可能因此刷新;这一篇又允许通过菜单调整位置,索引随时变化。把焦点恢复目标记成“第3行第2项”,移动后就可能跳到别的卡片。更可靠的方式是使用稳定itemId,必要时拼接栏目身份形成组件键值。BoardStore提交后,用移动结果返回的卡片ID确定焦点应该回到哪里。
在键鼠场景中,我更偏向让Tab切换卡片操作入口,方向键用于当前区域内的移动候选选择,Enter打开操作菜单,Esc关闭菜单并回到原卡片。快捷键不能随便覆盖系统或文本输入框已有的组合键;当焦点落在搜索框时,方向键应先服务于文本编辑,不能把输入法操作误解成卡片移动。HarmonyOS的组件焦点属性提供focusable和tabIndex等入口,但它们负责组件走焦,具体业务动作仍由页面命令处理。
下面先用独立函数解决焦点恢复的“卡片被移走”问题。它优先寻找原卡片,若卡片已被删除则回退到目标栏第一张,再回退到资料板工具栏,让焦点不会落到已经卸载的组件上。
const assert = require('node:assert/strict');
function focusAfterCommit(next, cardId, targetColumn) {
if (next.cards[cardId]) return `card:${cardId}:action`;
const first = (next.columns[targetColumn] || [])[0];
return first ? `card:${first}:action` : 'board:toolbar';
}
const state = {
cards:{'photo-01':{id:'photo-01'}},
columns:{photos:['photo-01','photo-02'],records:[]}
};
assert.equal(focusAfterCommit(state,'photo-01','photos'),
'card:photo-01:action');
assert.equal(focusAfterCommit({cards:{},columns:{photos:[]}},'gone','photos'),
'board:toolbar');
焦点恢复还要与页面卸载衔接。移动命令异步返回之前,使用者可能已经关闭了资料板或切换了项目;这时旧页面的回调不该重新抢焦点。需要给当前页面会话一个代际标记,命令完成时先核对页面是否仍在、代际是否匹配,再在界面层执行焦点请求。它是视图生命周期保护,不应该混进持久化的卡片数据。

键盘菜单与拖拽不能各存一份卡片
上一章已经把BoardStore确定为正式状态来源,键盘菜单只能提交移动意图。这里要特别防止一个看似方便的写法:菜单点击后先在当前栏目数组删除卡片,再调用已有拖拽接口。那样会让同一次移动发生两次数组操作,而且当源栏与目标栏属于不同组件时,详情抽屉仍可能收到不一致的顺序。
更合适的流程是读取当前boardRevision,构造移动命令,把类型检查、目标位置计算及版本比较交给统一领域入口,成功后一次性替换状态。拒绝时返回明确原因,原来的卡片和焦点都保留。下面的代码强调同一个状态变更既可从菜单进入,也可从拖拽进入;source仅用于诊断和交互回放,不改变结果。
const assert = require('node:assert/strict');
function submitMove(board, command) {
if (command.revision !== board.revision) return {ok:false,reason:'STALE'};
const card = board.cards[command.cardId];
if (!card) return {ok:false,reason:'MISSING'};
if (!board.allowed[command.targetColumn].includes(card.kind))
return {ok:false,reason:'TYPE'};
const next = structuredClone(board);
for (const lane of Object.values(next.columns)) {
const at = lane.indexOf(card.id);
if (at >= 0) lane.splice(at,1);
}
const dest = next.columns[command.targetColumn];
dest.splice(Math.min(Math.max(0,command.index),dest.length),0,card.id);
next.revision++;
return {ok:true,next,focusId:card.id};
}
const board = {revision:8,cards:{'photo-01':{id:'photo-01',kind:'image'}},
columns:{todo:['photo-01'],photos:[]},allowed:{todo:['image'],photos:['image']}};
const moved = submitMove(board,{cardId:'photo-01',targetColumn:'photos',
index:0,revision:8,source:'keyboard'});
assert.equal(moved.next.columns.photos[0],'photo-01');
assert.equal(board.columns.todo[0],'photo-01');
与第04篇直接处理数组重排相比,这一层更关心交互来源一致性,实际投放位置仍应调用同一套位置计算服务。对于同栏重排,必须沿用“移除后修正目标索引”的规则,不能因为键盘菜单选中了“上移一格”就写第二份简化算法。若连续按两次Enter导致两个命令同时到达,BoardStore应依据版本拒绝过期提交,界面随后根据最新状态重新生成候选菜单。
朗读内容要告诉用户会发生什么
一张无文字的照片卡若只朗读“图片”,用户并不知道它是“配电柜现场.jpg”、目前在“待整理”栏、有哪些操作。可访问标签应包含可辨识的卡片名称及类型,辅助描述再说明“操作菜单可移动到设备照片”等意图。菜单中的目标栏既要读出名称,也要读出禁用原因;状态改变后应更新清晰的结果提示,例如“配电柜现场已移动到设备照片,位置第二项”。
屏幕朗读提示不能靠颜色区别成功与失败,也不要将整页统计数字连续读成没有上下文的一串数。卡片标题、栏目标题和状态提示各自承担不同语义,相关按钮具备稳定的朗读顺序。在较窄手机布局中,三列可以折叠成分区切换,但每张卡片的移动菜单仍应完整保留。需要关注横竖屏切换后的焦点归属,特别是浮层关闭时恢复的组件是否仍然存在。

调试要检查实际可完成的操作链
排查顺序可以从一份固定的boardId=site-east开始:先让鼠标点击“配电柜现场.jpg”的操作菜单,选择“设备照片”,核对来源栏、目标栏、计数与详情是否同步;再用键盘执行同一动作,对比生成的moveIntent字段,确认除了source外没有业务差别。接着把附件来源改为MISSING,检查禁用菜单项是否能说清原因,并确认不发生状态提交。最后在命令发出后切换资料板,检查旧页面不会夺回新页面焦点。
如果Tab顺序突然变化,先检查组件是否意外变成不可获焦以及列表渲染键是否使用了数组序号;如果Enter和鼠标点击重复触发,应确认业务命令有版本保护;如果屏幕朗读内容不完整,则分别检查卡片标签、菜单描述与移动结果提示,而不是通过反复添加装饰文字修补。
HarmonyOS官方焦点属性、键盘快捷键以及无障碍组件属性文档可用于核对界面层能力。快捷键绑定的生效范围和系统保留组合键需要逐一评估,不把某个快捷键写成跨设备必然一致的行为。业务层的submitMove与上述键盘、鼠标和可访问入口分离,才方便独立复核状态变更规则。
至此资料板不依赖拖拽才能完成整理:触控拖动保留直观性,键盘与操作菜单保留精确性,辅助访问保留明确的状态解释。更重要的是三种路径都不制造新的卡片数据副本。下一篇将沿用此处的命令入口,处理拖拽会话结束、监听释放和撤销历史,避免使用方式增多后清理机制失控。
更多推荐

所有评论(0)