时间轨迹第 6 篇:模板页源代码拆解,收藏、自定义、最近使用与模板应用
这一篇讲的是 TemplatePage.ets。它不是简单的模板列表,而是整套拍照水印系统的“样式中枢”。 用户在这里选的不是一张卡片,而是后续拍照时会被复用的表达方式。

系列导航
- 第 1 篇:首页、路由、登录与启动闭环
- 第 2 篇:时间地点页源代码拆解
- 第 3 篇:相机页源代码拆解
- 第 4 篇:工作记录页源码导览
- 第 5 篇:工程底座源码导览

摘要
在 HarmonyOS 应用开发中,模板选择功能往往被简化为拍照前的一个简单点击操作。本文深入剖析的 TemplatePage 组件,正是要将这一体验彻底升级——通过 ArkTS 语言构建的现代化 UI组件 体系,将模板选择从"拍照前随便点一下"的临时操作,转变为"可复用、可收藏、可最近使用、可自定义"的完整产品级流程。
TemplatePage 的核心价值在于为用户提供了一套完整的模板管理解决方案:
- 智能筛选与搜索:基于模板分类的高效筛选机制,配合实时搜索功能,让用户在数百个模板中快速定位所需
- 个性化收藏系统:支持用户收藏常用模板,打造专属的个性化水印库
- 智能最近使用:自动记录用户使用历史,提供智能化的最近使用推荐
- 灵活自定义能力:允许用户创建、编辑和删除自定义模板,真正实现系统的可扩展性
- 无缝数据同步:选中模板后即时同步到相机页面,确保用户体验的连贯性
通过这一系列精心设计的 UI组件 交互流程,TemplatePage 不仅提升了功能实用性,更让整个水印系统在 HarmonyOS 生态中展现出专业的产品质感。当用户能够轻松管理、快速调用心仪模板时,他们会真切感受到:这不再是一个临时功能,而是一个真正为他们服务的智能工具。
本文将从 ArkTS 实现角度,详细拆解 TemplatePage 的架构设计、数据组织、交互逻辑与性能优化,为 HarmonyOS 开发者提供一套可复用的高质量模板管理组件解决方案。
目录
一、模板页在整套链路中的位置
#mermaid-svg-lPLEol6FYVyXJp2Q{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lPLEol6FYVyXJp2Q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lPLEol6FYVyXJp2Q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lPLEol6FYVyXJp2Q .error-icon{fill:#552222;}#mermaid-svg-lPLEol6FYVyXJp2Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lPLEol6FYVyXJp2Q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lPLEol6FYVyXJp2Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lPLEol6FYVyXJp2Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lPLEol6FYVyXJp2Q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lPLEol6FYVyXJp2Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lPLEol6FYVyXJp2Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lPLEol6FYVyXJp2Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lPLEol6FYVyXJp2Q .marker.cross{stroke:#333333;}#mermaid-svg-lPLEol6FYVyXJp2Q svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lPLEol6FYVyXJp2Q p{margin:0;}#mermaid-svg-lPLEol6FYVyXJp2Q .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-lPLEol6FYVyXJp2Q .cluster-label text{fill:#333;}#mermaid-svg-lPLEol6FYVyXJp2Q .cluster-label span{color:#333;}#mermaid-svg-lPLEol6FYVyXJp2Q .cluster-label span p{background-color:transparent;}#mermaid-svg-lPLEol6FYVyXJp2Q .label text,#mermaid-svg-lPLEol6FYVyXJp2Q span{fill:#333;color:#333;}#mermaid-svg-lPLEol6FYVyXJp2Q .node rect,#mermaid-svg-lPLEol6FYVyXJp2Q .node circle,#mermaid-svg-lPLEol6FYVyXJp2Q .node ellipse,#mermaid-svg-lPLEol6FYVyXJp2Q .node polygon,#mermaid-svg-lPLEol6FYVyXJp2Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lPLEol6FYVyXJp2Q .rough-node .label text,#mermaid-svg-lPLEol6FYVyXJp2Q .node .label text,#mermaid-svg-lPLEol6FYVyXJp2Q .image-shape .label,#mermaid-svg-lPLEol6FYVyXJp2Q .icon-shape .label{text-anchor:middle;}#mermaid-svg-lPLEol6FYVyXJp2Q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lPLEol6FYVyXJp2Q .rough-node .label,#mermaid-svg-lPLEol6FYVyXJp2Q .node .label,#mermaid-svg-lPLEol6FYVyXJp2Q .image-shape .label,#mermaid-svg-lPLEol6FYVyXJp2Q .icon-shape .label{text-align:center;}#mermaid-svg-lPLEol6FYVyXJp2Q .node.clickable{cursor:pointer;}#mermaid-svg-lPLEol6FYVyXJp2Q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lPLEol6FYVyXJp2Q .arrowheadPath{fill:#333333;}#mermaid-svg-lPLEol6FYVyXJp2Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lPLEol6FYVyXJp2Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lPLEol6FYVyXJp2Q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lPLEol6FYVyXJp2Q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lPLEol6FYVyXJp2Q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lPLEol6FYVyXJp2Q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lPLEol6FYVyXJp2Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lPLEol6FYVyXJp2Q .cluster text{fill:#333;}#mermaid-svg-lPLEol6FYVyXJp2Q .cluster span{color:#333;}#mermaid-svg-lPLEol6FYVyXJp2Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lPLEol6FYVyXJp2Q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lPLEol6FYVyXJp2Q rect.text{fill:none;stroke-width:0;}#mermaid-svg-lPLEol6FYVyXJp2Q .icon-shape,#mermaid-svg-lPLEol6FYVyXJp2Q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lPLEol6FYVyXJp2Q .icon-shape p,#mermaid-svg-lPLEol6FYVyXJp2Q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lPLEol6FYVyXJp2Q .icon-shape .label rect,#mermaid-svg-lPLEol6FYVyXJp2Q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lPLEol6FYVyXJp2Q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lPLEol6FYVyXJp2Q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lPLEol6FYVyXJp2Q :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
HomePage / CameraPage 进入模板页
TemplatePage
筛选 / 搜索 / 收藏
创建自定义模板
写入 recentTemplateIds
同步 cameraWatermarkTemplate
CameraPage 读取并应用
这张图说明的是:模板页不是孤立页面,它和相机页是一对前后链路。
下面是更详细的「模板应用数据流」序列图,展示从用户点击模板到CameraPage渲染水印的完整时序过程:
CameraPage
持久化存储
AppStorage
模板页
用户
CameraPage
持久化存储
AppStorage
模板页
用户
#mermaid-svg-oLK9CmqNiL4np8rs{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-oLK9CmqNiL4np8rs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oLK9CmqNiL4np8rs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oLK9CmqNiL4np8rs .error-icon{fill:#552222;}#mermaid-svg-oLK9CmqNiL4np8rs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oLK9CmqNiL4np8rs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oLK9CmqNiL4np8rs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oLK9CmqNiL4np8rs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oLK9CmqNiL4np8rs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oLK9CmqNiL4np8rs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oLK9CmqNiL4np8rs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oLK9CmqNiL4np8rs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oLK9CmqNiL4np8rs .marker.cross{stroke:#333333;}#mermaid-svg-oLK9CmqNiL4np8rs svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oLK9CmqNiL4np8rs p{margin:0;}#mermaid-svg-oLK9CmqNiL4np8rs .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oLK9CmqNiL4np8rs text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-oLK9CmqNiL4np8rs .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-oLK9CmqNiL4np8rs .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-oLK9CmqNiL4np8rs .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-oLK9CmqNiL4np8rs .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-oLK9CmqNiL4np8rs #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-oLK9CmqNiL4np8rs .sequenceNumber{fill:white;}#mermaid-svg-oLK9CmqNiL4np8rs #sequencenumber{fill:#333;}#mermaid-svg-oLK9CmqNiL4np8rs #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-oLK9CmqNiL4np8rs .messageText{fill:#333;stroke:none;}#mermaid-svg-oLK9CmqNiL4np8rs .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oLK9CmqNiL4np8rs .labelText,#mermaid-svg-oLK9CmqNiL4np8rs .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-oLK9CmqNiL4np8rs .loopText,#mermaid-svg-oLK9CmqNiL4np8rs .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-oLK9CmqNiL4np8rs .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-oLK9CmqNiL4np8rs .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-oLK9CmqNiL4np8rs .noteText,#mermaid-svg-oLK9CmqNiL4np8rs .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-oLK9CmqNiL4np8rs .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oLK9CmqNiL4np8rs .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oLK9CmqNiL4np8rs .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-oLK9CmqNiL4np8rs .actorPopupMenu{position:absolute;}#mermaid-svg-oLK9CmqNiL4np8rs .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-oLK9CmqNiL4np8rs .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-oLK9CmqNiL4np8rs .actor-man circle,#mermaid-svg-oLK9CmqNiL4np8rs line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-oLK9CmqNiL4np8rs :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
用户返回CameraPage
点击选择模板
写入 cameraWatermarkTemplate
保存模板数据
更新 recentTemplateIds
保存最近使用记录
返回模板选择结果
读取 cameraWatermarkTemplate
读取模板配置
解析模板数据
渲染水印图层
显示带水印的相机预览
这个序列图清晰地展示了:
二、模板数据的组织方式很成熟
const TEMPLATE_THEMES: TemplateTheme[] = [
{ bg: '#EEF4FF', badgeBg: '#0D47A1', badgeLabel: '今日现场', decorIcon: '🏗', infoLine1: '📍 地 点:深圳市 · 南山区', infoLine2: '备 注:点击输入今日备注' },
{ bg: '#E8F5ED', badgeBg: '#1B5E20', badgeLabel: '工程巡检', decorIcon: '✅', infoLine1: '📍 地 点:施工区域 · A栋3层', infoLine2: '备 注:安全巡检内容' },
{ bg: '#FFF3E8', badgeBg: '#E65100', badgeLabel: '考勤打卡', decorIcon: '📍', infoLine1: '📍 地 点:深圳市 · 南山区', infoLine2: '备 注:负责人签到打卡' }
];
模板页不是依赖随机样式,而是有一组预设主题。
这种设计的好处是:
- 视觉稳定
- 每类模板都能快速识别
- 后面新增自定义模板时不会破坏整体风格
三、筛选和搜索不是装饰,是降低选择成本

private getFilteredTemplates(): WatermarkTemplate[] {
let list = this.selectedCategory === '全部'
? this.templates
: this.templates.filter((t: WatermarkTemplate) => t.category === this.getCategoryKey(this.selectedCategory));
const q = this.searchText.trim();
if (q.length > 0) {
list = list.filter((t: WatermarkTemplate) =>
t.name.indexOf(q) >= 0 || t.description.indexOf(q) >= 0);
}
return list;
}
这段代码说明模板页已经不是“堆模板”,而是“帮用户找到合适模板”。
特别是在模板数量变多以后:
- 分类可以缩小范围
- 搜索可以快速定位
- 用户不会被一屏模板淹没
四、收藏和最近使用,决定了模板页是否真能高频使用
1. 收藏模板
private toggleFavorite(item: WatermarkTemplate): void {
…
this.favIds = JSON.stringify(newIds);
}
收藏不是装饰,它是在告诉系统:
这个模板以后会被频繁用到。
2. 最近使用
this.recentIds = JSON.stringify([item.id, …ids.filter((id: string) => id !== item.id)].slice(0, 10));
这段逻辑很小,但很关键:
- 最近使用会自动排前
- 旧项会被挤到后面
- 用户再次进入时可以快速回到惯用模板
五、自定义模板让系统真正可扩展
private addCustomTemplate(): void {
const name = this.newTplName.trim();
if (name.length === 0) {
promptAction.showToast({ message: '请输入模板名称', duration: 1200 });
return;
}
const newId = Date.now().toString() + '_c';
const newTpl: WatermarkTemplate = {
id: newId, name: name,
category: catKey === 'all' ? 'custom' : catKey,
description: this.newTplDesc.trim() || '自定义模板',
isFavorite: false
};
}
这说明模板页已经不是一个封闭列表,而是允许用户把自己的场景塞进来。
最典型的场景有:
- 工程现场
- 考勤打卡
- 旅行记录
- 生活记录

这类自定义能力,是文章能不能显得“像产品”而不是“像 demo”的关键。
六、应用模板时要同时处理页面返回和数据写入
private applyTemplate(item: WatermarkTemplate): void {
this.selectedCameraTemplate = this.getWatermarkIndex(item);
this.wmSelectedTemplateName = item.id.endsWith('_c') ? item.name : '';
this.wmSelectedTemplateDesc = item.id.endsWith('_c') ? item.description : '';
…
if (this.fromCamera) {
this.onBack?.();
} else {
this.onGoCamera?.();
}
}
这里的逻辑其实分成两层:
这很重要,因为模板页既可以从首页进入,也可以从相机页反向进入。
七、页面结构上,它在追求“快速决策”
Text('水印模板')
Text('选择合适的模板,记录你的现场')
Text(this.showSearch ? '✕' : '🔍')
模板页顶部只需要解决三个问题:
- 现在在哪个模板分类里
- 是否在搜索
- 是否准备返回相机页
所以页面设计上要让“挑模板”这件事尽量快。
八、这页最值得写进文章的 3 个点
九、实操检查清单
- 分类筛选是否能正确过滤模板
- 搜索是否能命中模板名称和描述
- 收藏后是否会写回收藏列表
- 自定义模板是否能持久化
- 从相机页返回后是否会立刻应用模板
- 最近使用是否会自动排到前面

结语
模板页的真正价值,不是展示了多少种卡片,而是它让用户在拍照前就完成了“风格决策”。
在这个应用里,模板选择不是一个点,而是整条水印链路的起点。
参考资料
- 华为 HarmonyOS 多设备开发最佳实践
- 博客质量分计算——发布 version 5.0


