值班交接最容易丢的往往不是标题,而是“接下来谁做什么”。一条消息里可能同时包含负责人、影响范围、截止时间和上下文,过几个小时再找,信息已经被新讨论顶到后面。原文作者用ArkTS和ArkUI做了一个离线“交接台”,面向小团队值班、研发支持和个人项目管理,记录事项状态、负责人、影响范围、下一步、截止时间和处理记录。应用不依赖网络,数据留在本机;交班时把未完成事项整理成纯文本,复制到群聊或工单。
需要先明确验证边界:开发和验证都在DevEco Studio的HarmonyOS 7手机模拟器中完成,证明的是模拟器上的功能流程,不能替代真机性能、通知能力或真实团队部署验证。示例事项是脱敏测试数据,用来覆盖页面和流程,不代表已经接入某个企业的真实工单系统。
一、先把交接对象定义成RelayItem
原文把一条交接事项收敛为RelayItem,字段没有追求多,重点是回答交接时最常问的几件事。history是后来补上的字段:最初只有当前状态,页面看起来能用,但状态一推进,之前发生过什么就没有地方可追。交接场景里这会产生反效果:工具把信息收拢了,却把过程抹掉了。因此每次状态变化都会在数组头部增加一条事件记录。
- export interface RelayItem {
- id: number;
- title: string;
- owner: string;
- impact: string;
- nextStep: string;
- due: string;
- status: string;
- note: string;
- updatedAt: string;
- history: string[];
- }
复制代码
二、首页计数从当前数组重新计算
首页先给出未完成、阻塞、待验证和已完成四个计数,再放搜索、状态筛选和新建入口。计数不是写死在页面上的文案,而是从当前数组重新计算。
- private refreshCounts(source: Array<RelayItem>): void {
- this.unfinishedCount = source.filter(
- (item: RelayItem) => item.status !== RelayStatus.DONE
- ).length;
- this.blockedCount = source.filter(
- (item: RelayItem) => item.status === RelayStatus.BLOCKED
- ).length;
- this.verifyCount = source.filter(
- (item: RelayItem) => item.status === RelayStatus.VERIFY
- ).length;
- this.doneCount = source.filter(
- (item: RelayItem) => item.status === RelayStatus.DONE
- ).length;
- }
复制代码
首屏样例数据故意保留四种状态:支付回调阻塞、版本灰度待验证、客户字段待处理,以及已经完成的周报校对。这样打开应用就能看到筛选和状态推进的实际入口,而不是一张空白页面。搜索和筛选是两条独立条件:筛选先判断状态,搜索再在标题、负责人和影响范围中匹配。当前模拟器的输入自动化工具只接受ASCII文本,所以中文关键词搜索没有作为自动化证据写进结论,仍需要在实际设备或可输入中文的测试环境中补一轮。
三、新建事项先挡住不完整输入
新建表单只把事项、负责人和下一步设为必填。影响范围、截止时间和补充记录允许暂时留空,因为交接现场经常是先把责任和下一步记下来,再补充细节。表单收起时,页面保持在列表视图;展开后可以一次填写六个字段。表单和列表使用同一套页面状态,打开表单时原来的事项不会被清空,收起后仍然停留在原来的筛选位置。
原文特意保留了一张不完整提交的结果:点击保存时,如果三个必填字段中有一个为空,页面显示错误提示,原表单不会被关闭,也不会把半条数据写进本地存储。校验提示没有弹出需要再确认的对话框,而是直接放在保存按钮上方。操作人能看到缺的是哪组信息,补完后继续填写,不会丢掉已经输入的字段。填写完整后,应用给新事项分配时间戳,初始状态设为待处理,并在history中写入新建事项。写入成功后才收起表单,这样页面上的状态和本地数据不会出现已经消失但实际没有保存的假象。保存后的事项回到列表数据源,计数卡片也随之重新计算;页面没有另做一份“刚刚新增”的临时列表,所以刷新或重新打开时看到的内容和保存到Preferences的内容使用同一个来源。
在手机模拟器中,输入框会触发系统键盘,键盘收起后再保存的路径也单独跑过一遍。保存按钮在键盘收起后仍然保持可见,列表卡片的间距也没有被压缩。这个小检查让填写完成和回到列表之间的操作更连贯,也能避免输入法把最后一个按钮推到屏幕之外。不同设备的输入法高度仍然需要在对应设备上重新确认。
四、状态推进必须留下痕迹
列表卡片默认只显示当前状态和下一步。点开一条事项后,再展示补充说明、处理历史和推进状态按钮。这个层级让首页保持可扫读,真正需要追过程时再展开。展开之后,补充说明和历史事件按同一张卡片排列,读者不必在列表、详情页和日志页之间来回跳。当前状态仍然放在卡片右上角,历史只在需要时展开。
状态推进使用一个很短的状态机:待处理进入待验证,待验证进入已完成;阻塞事项解除后回到待处理。每次推进都会复制当前对象的字段,只替换状态、更新时间和历史数组。
- const next = nextStatus(item.status);
- const nextItems = this.items.map((current: RelayItem) => {
- if (current.id !== item.id) {
- return current;
- }
- return {
- ...current,
- status: next,
- updatedAt: changedAt,
- history: [
- changedAt + ' 状态由「' + current.status + '」更新为「' + next + '」。',
- ...current.history
- ]
- } as RelayItem;
- });
复制代码
原文在模拟器里先把“支付回调偶发超时”从阻塞中推进到待处理,再展开记录确认新事件排在最前面。列表上的状态和历史文本同时变化,证明这里不是只改了一个临时的页面变量。状态事件文本包含旧状态和新状态,时间戳使用应用当前时间生成。它不是完整审计日志,但对于交接来说,至少能回答这条事项什么时候从阻塞恢复,也能避免下一位处理人把已经做过的动作再做一遍。状态变化后,筛选栏也会跟着重新计算。点击待处理只保留两条事项,刚刚解除阻塞的回调问题和原来的客户字段事项都能被找到;已完成的周报校对不会混进结果里。筛选结果中的两条事项来自同一个本地数组,计数卡片和列表条数保持一致,说明离线模式下页面的基本筛选链路可以独立运行。
五、Preferences解决交班后别丢
数据层使用Preferences保存序列化后的事项数组。应用启动时由EntryAbility初始化存储,页面出现后异步加载;每次新建和状态推进都调用save,写入后执行flush。
- static async save(items: Array<RelayItem>): Promise<void> {
- if (RelayStore.store === undefined) {
- return;
- }
- await RelayStore.store.put('items', JSON.stringify(items));
- await RelayStore.store.flush();
- }
复制代码
华为文档将Preferences定义为应用侧的持久化键值存储,并说明getPreferences返回的实例会被缓存。这里保存的是一个小型事项列表,使用JSON字符串比为每个字段拆很多key更容易迁移。旧数据没有history字段时,加载过程会补一条“已导入历史事项”的记录,避免升级后页面直接报错。
验证持久化时,原文作者先推进状态并关闭应用,再重新启动。重新进入首页后,回调事项仍是待处理,更新时间也保持为刚才的16:35;重新展开后还能看到原来的状态变化记录。这一步只验证了模拟器里的应用重启和本地数据保留,没有做清除应用数据后的恢复,也没有把数据同步到其他设备。离线交接台的边界很明确:它适合单设备上的小团队值班记录和个人项目跟进,跨设备协作需要再接入分布式数据或服务端同步。
六、交接摘要要能离开应用
如果事项只能留在页面里,交班时还是要重新描述一次。因此原文把未完成事项转换成一段纯文本,格式保持简单:状态、标题、负责人、下一步和截止时间。已完成事项不进入摘要,避免把已经结束的工作带进新的交班消息。
- export function formatHandoffSummary(items: Array<RelayItem>): string {
- const unfinished = items.filter(
- (item: RelayItem) => item.status !== RelayStatus.DONE
- );
- if (unfinished.length === 0) {
- return '当前没有未完成事项。';
- }
- return unfinished.map((item: RelayItem) => {
- return `${item.status}|${item.title}\n` +
- `负责人:${item.owner} 下一步:${item.nextStep} 截止:${item.due}`;
- }).join('\n\n');
- }
复制代码
摘要面板先让人确认内容,再点击复制。复制实现使用@kit.BasicServicesKit中的系统剪贴板,把文本写入text/plain。
- private async copySummary(): Promise<void> {
- const board = pasteboard.getSystemPasteboard();
- await board.setData(
- pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.summaryText)
- );
- this.copyState = '已复制,可粘贴到群聊或工单';
- }
复制代码
华为文档列出的纯文本类型正是text/plain,并提醒剪贴板读取在API 12之后受到权限控制。这里的需求只是写入,不读取其他应用的剪贴板内容,所以没有为了验证复制去申请读取权限。模拟器操作后页面收到setData的成功结果并显示提示,这证明应用侧写入调用完成;原文没有把它扩大成已经验证真实群聊粘贴成功。摘要面板保留了原始字段的顺序,读起来更像一条可以直接转发的工作消息,而不是把整个页面内容复制出去。先确认未完成事项,再执行复制,也给了操作人删改或补充的机会。复制完成后,摘要面板仍然留在当前页面,提示文字出现在按钮左侧。操作人可以看到调用已经返回,再决定去群聊或工单里粘贴,不需要凭感觉判断是否成功。提示只描述应用侧已经完成写入,不替用户承诺目标群聊或工单的接收结果。
七、实现边界与取舍
这版应用已经在HarmonyOS 7手机模拟器中完成了构建、安装、启动、建项校验、状态推进、历史记录、筛选、摘要生成、剪贴板写入提示和重启持久化。它还没有验证真实手机上的性能、横竖屏细节、通知和跨设备同步,也没有做企业账号、权限和多人并发编辑。这些边界会直接影响应用能不能进入真实工作流。离线存储解决了交班时找不到当前状态,却没有解决多人同时修改;纯文本摘要解决了复制到群里还要重新整理,却没有替代工单系统的审计和权限。下一步如果继续做,原文计划先补平板和2in1模拟器的布局验证,再决定是否值得引入分布式同步。
对个人项目和值班交接来说,这个规模已经足够把最容易丢的几项信息固定下来:谁负责、影响什么、现在到哪一步、接下来做什么,以及这一步是怎么走到的。剩下的能力应该等真实场景出现后再加,而不是先堆成一个看起来很完整、交接时却没人愿意打开的系统。 |