【共创稿事节】鸿蒙文搜图 · 回忆胶囊用一句话搜出照片封存成带倒计时的时间胶囊到期才能开启文搜图第一篇把「找到」做完了一句话进、相似度排序的图片网格出。但「找到」之后呢翻出来就看完关掉和没翻过差别不大。这一篇在检索的终点接一个新动作——封存把搜出来的那张照片连同当时的检索词、给未来的一句留言一起锁进一颗「回忆胶囊」设一个倒计时30 秒到 30 天时间不到看不了到期开启时模糊变清晰的揭示。检索解决「找回来」胶囊解决「再遇见」。技术底色不变端侧 NPU 语义检索、零联网、零权限、数据不出设备。新增的是一层纯本地的时间状态机——封存、倒计时、到期、开启、重温全部收在一个服务类里和检索内核完全解耦。工程地址LI_harmonyOS/memory-capsule运行环境HarmonyOS 7.0API 26 ArkTS 严格模式已在模拟器安装运行、全流程验证。包名com.example.memorycapsulemodule.json5没有申请任何权限。一、效果展示一条「搜到 → 封存 → 等待 → 开启」的完整叙事线完整流程五步载入 → 搜索 → 封存 → 等倒计时 → 开启。以下截图均为演示模式内置 12 张真实照片素材模拟器运行真机模式走同一套 UI、只把检索内核换成 NPU。第 1 步就绪。应用启动后初始化完成深色主题首屏——顶部标题区、搜索框 快捷回忆词、空态引导。和第一篇的浅色形成刻意区分回忆是夜晚的事深色才对味第 2 步载入 搜索。点「载入回忆」把 12 张照片登记进端侧索引点快捷词「海边日落」直接检索350ms 返回命中卡片——相似度角标 主题标签和第一篇同款双口径第 3 步封存。点任意一张命中卡片底部弹出封存表单照片预览 检索词回显 给未来的留言框 五档封存时长30 秒/1 分钟/1 天/7 天/30 天。选时长、写留言、点「 封存」第 4 步等待。封存成功后胶囊墙出现这颗胶囊图片模糊预览 锁图标右侧倒计时「还有 29 秒 开启」下方进度环每秒推进。看不到内容本身就是产品语义——悬念是胶囊的核心体验第 5 步开启。倒计时归零胶囊自动切换为「时间到了可以开启了」按钮从灰变绿。点开启弹层里照片从模糊到清晰渐显留言在动画结束后浮现——这一秒就是整颗胶囊等待的意义二、这个 App 解决什么问题「时间胶囊」类产品慢递信、给未来的自己写信有个共同痛点写的时候不知道配什么图配图的时候又要翻半天相册。回忆胶囊把这两步合成一步传统做法死角挑一张照片存相册收藏收藏夹几百张存了等于没存没有「开启时刻」写慢递信纯文字没有画面寄丢、忘看发「朋友圈仅自己可见」社交产品没有时间锁随时能翻出来悬念为零回忆胶囊的答案是检索即选图、封存即寄出、时间即悬念。一句话搜出那张照片语义检索保证「就是那张」封存时带上留言和时长到期前谁也看不了——包括封存的自己。三件事分别落在三个技术点上选图靠语义延续第一篇的文搜图内核标签匹配引擎演示、NPU 端侧推理真机接口不变悬念靠状态机SEALED → OPENABLE → OPENED三态流转到期前 UI 层拿不到清晰图仪式感靠动画开启时模糊→清晰渐显 留言延迟浮现把「等了 30 秒/30 天」兑现成可感知的一瞬。三、交互设计一颗胶囊的三态流转页面没有路由跳转状态收在State集合里。除了第一篇同款的检索状态ready/indexedCount/loading/queryText/resultList/statusText新增胶囊专属的六个State ready: boolean false; State indexedCount: number 0; State loading: boolean false; State queryText: string ; State resultList: SearchHit[] []; State capsules: Capsule[] []; State statusText: string 初始化中…; /** 当前选中要封存的图片进入封存流程 */ State selectedHit: SearchHit | null null; /** 封存表单可见bindSheet $$ 绑定需独立布尔状态 */ State sealSheetVisible: boolean false; /** 开启弹层可见bindSheet $$ 绑定需独立布尔状态 */ State openSheetVisible: boolean false; /** 封存表单留言 */ State messageText: string ; /** 封存表单时长索引 */ State durationIndex: number 0; /** 开启动画中的胶囊弹层展示 */ State openedCapsule: Capsule | null null; /** 开启动画触发false→true 时播放 */ State revealAnim: boolean false;检索流程防重入、状态文案四结局找到 N 张/没找到/检索中/初始化中全部沿用第一篇的纪律不赘述。这一篇的核心是胶囊状态机3.1 三态流转SEALED → OPENABLE → OPENED一颗胶囊的生命周期由CapsuleState枚举驱动状态是数据不是 UI——UI 只按状态渲染永远不反向改状态export enum CapsuleState { /** 封存中不可开启模糊预览 */ SEALED sealed, /** 可开启倒计时结束 */ OPENABLE openable, /** 已开启进回忆时间线 */ OPENED opened }三个状态对应三张脸SEALED模糊预览 锁图标 倒计时 紫色进度环「查看」按钮点了只 toast「还没到开启时间」OPENABLE倒计时文案变「时间到了可以开启了」、按钮变绿「开启」自动切换不需要任何用户操作OPENED图片清晰、按钮变「重温」点开重放揭示动画。3.2 每秒 tick状态机的心跳倒计时靠setInterval每秒驱动这里有个容易踩的坑值得展开。最初版本只在tick()返回「有状态变化」时才刷新数组// 倒计时刷新每秒 tick到期自动变可开启 无条件重赋数组驱动进度环/剩余时间重绘 this.timerId setInterval(() { this.service.tick(); if (this.service.getCapsules().length 0) { this.capsules this.service.getCapsules().slice(); } }, 1000);tick()只在 SEALED→OPENABLE 跃迁时返回 true——但倒计时文本和进度环每秒都在变如果只在跃迁时刷新进度环会卡死在封存那一刻剩余时间永远不走。所以改成有胶囊时每秒无条件重赋数组slice()生成新引用触发State重新渲染tick()的返回值反而不再重要。时间类 UI 的刷新频率由 UI 决定不由状态机决定——状态机管跃迁UI 管心跳。3.3 封存表单三字段一次收口点命中卡片进封存流程表单三个字段照片、留言、时长一次收齐确认时一次性落库/** 确认封存 */ private confirmSeal(): void { if (!this.selectedHit) { return; } const duration: number DURATIONS[this.durationIndex].ms; this.service.seal(this.queryText, this.messageText, this.selectedHit, duration); this.capsules this.service.getCapsules().slice(); this.selectedHit null; this.sealSheetVisible false; this.toast(胶囊已封存倒计时开始); }注意封存时把queryText当时的检索词一起存进胶囊——检索词是回忆的一部分三个月后开启时看到「海边日落」四个字比看到一张照片本身多一层「我当时为什么搜它」的语境。留言允许为空message.trim()为空时开启弹层显示「封存时没有留下留言」空值是一等公民和第一篇空结果同款处理。3.4 开启模糊到清晰的仪式开启胶囊的动画分两拍先弹层展示模糊态revealAnim false100ms 后置 true 触发渐显——用状态切换驱动动画而不是动画库ArkUI 的.animation()属性会自动补间blur和opacity的变化/** 开启胶囊带揭示动画 */ private tryOpen(id: string): void { const opened: Capsule | null this.service.openCapsule(id); if (!opened) { this.toast(还没到开启时间再等等); return; } this.capsules this.service.getCapsules().slice(); this.openedCapsule opened; this.openSheetVisible true; this.revealAnim false; setTimeout(() { this.revealAnim true; // 触发模糊→清晰揭示动画 }, 100); }openCapsule在服务层校验状态——不是 OPENABLE 一律返回 nullUI 层 toast「还没到开启时间」。时间锁是产品语义必须在数据层强制UI 只负责把拒绝翻译成人话。四、工程结构memory-capsule/ ├── entry/src/main/ │ ├── ets/ │ │ ├── pages/Index.ets # 检索 胶囊墙 两个自实现弹层 │ │ └── common/ │ │ ├── MemoryCapsuleService.ets # 单例服务检索 胶囊管理二合一 │ │ └── DemoData.ets # 12 张示例图标签 主题 rawfile 路径 │ └── resources/rawfile/demo/ # 12 张真实照片素材旅程/拼搏/陪伴相比第一篇少了一个scopeConfig.ets作用域让位给主题标签多的是MemoryCapsuleService里的胶囊管理段。三层职责不变层文件职责不知道的事页面层Index.ets状态机、布局、倒计时心跳、弹层不知道检索是 NPU 还是标签匹配服务层MemoryCapsuleService.ets检索 封存/倒计时/开启状态机不知道 UI 有哪些状态数据层DemoData.ets素材定义标签 主题不知道自己被谁消费接口即防火墙在胶囊管理上体现得更彻底seal / tick / openCapsule三个方法和检索内核init / importFromAlbum / search住在同一个类里但互不感知——真机启用 NPU 检索时只换search的内部实现胶囊管理一行不改。五、核心实现5.1 数据层标签矩阵 回忆主题12 张演示图沿用第一篇的同义词矩阵海边/海/日落/夕阳/沙滩…保证「大海」「夕阳」都能命中同一张图每张多了一个theme字段——回忆主题旅程/拼搏/陪伴封存表单和胶囊墙用它做轻量分类标签export const DEMO_IMAGES: DemoImage[] [ // —— 旅程 journey —— { id: sea, rawfile: demo/sea_sunset.png, scope: travel, title: 海边日落, theme: journey, tags: [海边, 海, 日落, 夕阳, 沙滩, 风景, 旅行, 大海] }, { id: snow, rawfile: demo/snow_mountain.png, scope: travel, title: 雪山, theme: journey, tags: [雪山, 雪, 山, 冬天, 风景, 旅行, 滑雪] }, …主题不是装饰快捷词六个海边日落/雪山/深夜加班/撸猫/头脑风暴/城市夜景两两覆盖三个主题演示时任意封存两颗胶囊胶囊墙就能展示不同主题标签并存的样态。5.2 服务层seal / tick / open 三件套封存是纯同步操作——生成 id、算openAt、unshift排最前新胶囊永远在胶囊墙第一位/** 封存一颗胶囊 */ public seal(query: string, message: string, hit: SearchHit, durationMs: number): Capsule { const now: number Date.now(); const capsule: Capsule { id: cap_${now}_${this.seq}, query: query, message: message, imagePath: hit.imagePath, title: hit.title ?? 一张照片, theme: hit.theme ?? journey, sealedAt: now, openAt: now durationMs, state: CapsuleState.SEALED }; this.seq; this.capsules.unshift(capsule); // 新胶囊排最前 hilog.info(DOMAIN, TAG, capsule sealed id%{public}s openAt%{public}d, capsule.id, capsule.openAt); return capsule; }tick()扫描所有 SEALED 胶囊、把到期的置为 OPENABLEopenCapsule()校验状态后置 OPENED。两个方法都只改数据不改 UI——UI 的刷新由页面的每秒心跳统一驱动。5.3 胶囊墙模糊预览的三档透明度胶囊卡片的预览图按状态分三档渲染——SEALED 模糊 18 透明度 0.7 锁图标OPENABLE/OPENED 清晰.animation()自动补间状态切换时的过渡Stack() { Image($rawfile(c.imagePath)) .width(72) .height(72) .objectFit(ImageFit.Cover) .borderRadius(14) .blur(c.state CapsuleState.SEALED ? 18 : 0) .opacity(c.state CapsuleState.SEALED ? 0.7 : 1) .animation({ duration: 600, curve: Curve.EaseOut })到期那一秒模糊值 18→0 被.animation()补间成 600ms 的渐显——状态跃迁的瞬间被动画拉长成可感知的过程用户不用盯进度环也能注意到「这颗胶囊到时间了」。5.4 倒计时文本四级单位自适应剩余时间展示做了单位降级天/时 → 时/分 → 分/秒 → 秒30 天和 30 秒共用一个函数胶囊墙永远显示当前量级最有信息量的两级单位/** 剩余时间展示天/时/分/秒 */ private remainText(c: Capsule): string { const remain: number Math.max(0, c.openAt - Date.now()); if (remain 0) { return 可开启; } const sec: number Math.floor(remain / 1000); const days: number Math.floor(sec / 86400); if (days 1) { return ${days} 天 ${Math.floor((sec % 86400) / 3600)} 时; } const hours: number Math.floor(sec / 3600); if (hours 1) { return ${hours} 时 ${Math.floor((sec % 3600) / 60)} 分; } const minutes: number Math.floor(sec / 60); if (minutes 1) { return ${minutes} 分 ${sec % 60} 秒; } return ${sec} 秒; }进度环同理sealProgress()算(now - sealedAt) / (openAt - sealedAt)30 秒和 30 天的进度环走的是同一条归一化曲线——封存第一秒和最后一秒环的推进速度感知一致。5.5 弹层自实现替代 bindSheet两个弹层封存表单、开启揭示没有用系统bindSheet而是Stack 条件渲染自实现主界面build()根容器是Stack弹层作为兄弟节点按sealSheetVisible/openSheetVisible条件挂载遮罩层Blank()把内容压到底部、半透明黑遮罩 圆角面板点遮罩关闭、点面板内部被空onClick拦截// —— 封存表单弹层自实现条件渲染 —— if (this.sealSheetVisible) { this.sealOverlay() }自实现换来三个好处条件渲染的节点在 dumpLayout 里可见bindSheet 的内容在无障碍树里时有时无自动化验证和可达性测试都更稳出场动画完全可控translate animation 自定义不受系统 Sheet 手势冲突影响不依赖$$双向绑定——实测当前版本bindSheet的$$绑定存在状态设 true 但 Sheet 不渲染的静默失败自实现彻底绕开。代价是要自己处理遮罩点击、内层拦截这些细节但对「弹层即产品体验」的胶囊开启场景控制权在自己手里更值。六、真机运行指南演示模式默认点「载入回忆」载入 12 张内置素材标签匹配引擎出结果任何设备完整跑通载入→搜索→封存→倒计时→开启全流程封存时长选「30 秒 · 演示」档30 秒后即可体验开启动画真机模式把MemoryCapsuleService.search内部替换为textSearchImage调用与第一篇RealTextImageSearchService同款init调visionBase建分析器、导入走PhotoViewPickerinsertImage——胶囊管理部分与检索内核无关零改动全程端侧、不联网、不上传、不申请权限胶囊数据在内存态退出页面release收口重进重新初始化——回忆胶囊是体验形态不是持久化相册下一步做落盘。七、踩坑清单速查表现象原因修法bindSheet 状态设 true 但 Sheet 不弹$$双向绑定静默失败Stack 条件渲染自实现弹层进度环/倒计时文本不走只在状态跃迁时刷新数组有胶囊时每秒无条件slice()重赋点弹层内部误关弹层遮罩 onClick 穿透内层面板加空onClick拦截到期后胶囊还显示锁tick()没被定时驱动setInterval每秒 tick aboutToDisappear清理30 秒和 30 天进度感知不一致进度环用绝对时间(now-sealedAt)/(openAt-sealedAt)归一化ForEach 回调无类型标注报错ArkTS 严格模式(d: DurationOption, idx: number)显式标注退出页面定时器残留setInterval未清理aboutToDisappear里clearInterval开启动画不播弹层挂载即清晰先 false 挂模糊态100ms 后置 true 触发补间八、总结至此文搜图方向走出了「找到之后」这一步一句话进、相似度排序的图片出、选中封存成时间胶囊、到期开启。检索内核与第一篇完全同源接口即防火墙换 NPU 内核 UI 零改动胶囊状态机三态流转时间锁在数据层强制UI 只做翻译模糊预览 倒计时 进度环看不到本身就是产品语义开启时模糊→清晰渐显 留言浮现把等待兑现成可感知的一瞬弹层自实现替代 bindSheet控制权换稳定性自动化验证友好。下一步两个方向一是胶囊落盘——用关系型数据库或偏好设置把胶囊持久化重进应用倒计时继续走让「封存 30 天」真的跨会话生效二是和图像超分系列打通——开启老照片胶囊时先走端侧 4× 超分再揭示「等了 30 天还你一张更清晰的回忆」。两个内核都已经在各自系列里备齐了。
企业数字化 ERP 产品动态
相关推荐
脉冲同步法:跨时钟域单周期脉冲传输的可靠实现与工程实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 3:30:05
Cursor Windows 环境与账号追踪机制分析:TaoToken 统一 Key 接入配置与验证 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 3:30:05
Codex 破局:前端组件秒级生成技术指南(TaoToken 配置实战) /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 6:58:54
模型训练流程自动化:新实验模型的分层设计与实操避坑指南 1. 从一条内部消息说起:模型训练流程的自动化到底在做什么前阵子圈子里在传一个消息,说 OpenAI 内部已经基本把新实验模型的训练流程自动化了。消息本身没有太多细节,但做训练系统的人一看就明白,这句话的分量不在“自动化”三个字… · 2026/9/26 6:58:48
华旭金卡身份证阅读器JS调用实战指南 简介:本资源是一套面向Web开发者与前端工程师的华旭金卡身份证阅读器JS集成实战方案,解决在网页端快速接入国产二代证读卡设备的核心难题,适用于政务系统、银行开户、实名认证等需现场身份核验的业务场景。压缩包共31个文件,含6个… · 2026/9/26 6:58:48
AI Agent开发实战:从ReAct循环到记忆与评测的完整指南 最近和几个做AI应用的朋友聊项目,几乎每个人都在提Agent。但聊深一步就发现,大家说的Agent根本不是同一回事。有人把Agent当成“会调用工具的大模型”,有人把它当成“能自主跑几十步的复杂系统”,还有人直接把带Agent字样的开源项… · 2026/9/26 6:58:48
轮胎字符识别实战:图像预处理与分类器调参全解析 简介:面向机器学习课程设计与期末大作业的轮胎字符识别完整项目,提供可直接运行的Python源码、配套文档说明与训练数据,覆盖从轮胎图像预处理、字符定位到识别的全流程。项目包含模型推理与参数文件、大量测试图片及多种识别结果样例… · 2026/9/26 6:58:48
AIGC创意猎人第65期:测试用例自动生成与降AI率实战指南 1. AIGC 创意猎人的定位与核心价值1.1 这个系列到底在做什么“AIGC 创意猎人”这个系列,我从第一季追到现在,最大的感受是它不像市面上那些泛泛而谈的AI工具盘点,而是真正站在一个内容创作者、产品经理或者技术爱好者的角度,去“狩… · 2026/9/26 6:58:48
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46