HarmonyOS 模板市场实战:64 款内置卡片、分类补齐与搜索过滤

HarmonyOS 模板市场实战:64 款内置卡片、分类补齐与搜索过滤

HarmonyOS 模板市场实战:64 款内置卡片、分类补齐与搜索过滤

一个卡片工具如果只展示用户已经创建的卡片,首屏很容易空。这个项目采用了“真实用户数据 + 内置模板目录”的组合:用户数据为空时,首页和分类页仍然能展示完整内容;用户创建卡片后,管理页再承载真实卡片。

本章围绕模板目录、分类补齐和搜索过滤展开实现拆解。

内置模板不是写在页面里

项目的模板目录放在AppDataService.ets

interfaceTemplateCatalogItem{id:string;title:string;subtitle:string;detail:string;value:string;footer:string;badge:string;tone:ToneName;categoryId:CardCategoryId;tags:string[];tabs:CatalogTabKey[];popularity:number;}

一条模板包含展示文案、分类、标签、所属 Tab 和热度。页面不直接维护模板,只调用服务层方法:

appDataService.getCategoryCards('recommend',this.searchText)appDataService.getCategoryRows(this.selectedCategoryId,this.searchText)appDataService.getCategoryHotRows()

这样模板市场、分类页、详情页都能使用同一份目录。

分类元信息单独维护

分类 ID 是内部 key,用户看到的是中文名、色调和标记:

constCATEGORY_META_COUNTDOWN:CategoryMeta={label:'倒计时',tone:'rose',mark:'倒'};

这个元信息用于:

  • 分类概览卡标题。
  • 列表行左侧标记。
  • 详情页类别中文展示。
  • fallback 卡片的色调。

不要在页面里到处写countdown -> 倒计时,否则后期改文案会很痛苦。

分类概览:真实数据不足时用模板补齐

分类页的“分类概览”不是简单读取用户卡片。真实用户卡片可能只覆盖一两个分类,如果直接展示,就会造成页面严重空白。

项目的策略是:

  1. 先按真实卡片分组。
  2. 缺失分类时,从内置模板目录生成分类 fallback。
  3. recommend视图覆盖 8 个分类。

fallback 卡片由服务层创建:

privatecreateCategoryOverviewFallbackCard(categoryId:CardCategoryId,templates:TemplateCatalogItem[]):ShowcaseCardModel|undefined{constcategoryTemplates=templates.filter((item)=>item.categoryId===categoryId);if(!categoryTemplates.length){returnundefined;}constcategoryMeta=this.getCategoryMeta(categoryId);return{id:`category-overview-${categoryId}`,title:categoryMeta.label,subtitle:`${categoryTemplates.length}款可用卡片`,value:`${categoryTemplates.length}`,footer:this.getCategoryOverviewFooter(categoryId),tone:categoryMeta.tone,categoryId:categoryId,imageKey:imageKeyForCategory(categoryId)};}

注意这里带的是categoryId,不是templateId。因为概览卡的语义是“进入分类”,不是“进入某一张模板详情”。

热门列表:列表项再携带 templateId

分类概览点击后,页面会显示同类模板列表。列表项才应该携带templateId

consttemplateId:string=item.templateId?item.templateId:item.id;router.pushUrl({url:RoutePaths.cardDetail,params:{templateId:templateId}});

这样用户路径是:

分类概览卡 -> 同类模板列表 -> 模板详情 -> 添加到我的卡片

这个路径比直接从分类概览进某张默认模板更清晰。

搜索过滤覆盖多个字段

模板搜索不只搜标题,也包含副标题、详情、footer 和 tags:

privategetFilteredTemplates(tabId:string,query:string):TemplateCatalogItem[]{constnormalizedQuery:string=query.trim();returnTEMPLATE_CATALOG.filter((item)=>item.tabs.indexOf(tabIdasCatalogTabKey)>=0).filter((item)=>{if(!normalizedQuery.length){returntrue;}constsourceText=[item.title,item.subtitle,item.detail,item.footer,item.tags.join(' ')].join(' ');returnincludesText(sourceText,normalizedQuery);}).sort((left,right)=>right.popularity-left.popularity);}

这种搜索对模板市场更友好。用户搜“考试”“备份”“喝水”,都能命中相关模板。

图片资源也按模板 ID 映射

每个模板都有对应图片,资源映射放在CardImages.ets

exportfunctionimageKeyForTemplate(templateId:string,categoryId:CardCategoryId):string{switch(templateId){case'birthday':return'template-birthday';case'exam-countdown':return'template-exam-countdown';case'weather-brief':return'template-weather-brief';default:returnimageKeyForCategory(categoryId);}}

如果新增模板但没补图片,项目会回退到分类图。这个 fallback 可以保证不崩,但长期不应该依赖。新增模板时最好同步补:

  • TEMPLATE_CATALOG
  • card_template_<templateId>.png
  • CardImages.ets映射

页面侧保持简单

分类页只关心数据和点击:

Grid(){ForEach(this.filteredCategoryCards(),(item:ShowcaseCardModel)=>{GridItem(){ShowcaseCard({item:item,compactBadge:true,onCardClick:()=>{this.openShowcaseCard(item);}})}},(item:ShowcaseCardModel)=>item.id)}

分类数据怎么补齐、图片怎么映射、搜索怎么过滤,页面都不直接处理。

验证清单

模板目录调整后,需要检查:

  1. recommend分类概览是否覆盖 8 个分类。
  2. 每个分类下是否有足够模板,不出现空列表。
  3. 搜索关键字能命中标题、标签和详情文案。
  4. 分类概览点击后进入同类列表,而不是直接详情。
  5. 列表项进入详情时带templateId
  6. 新增模板图片是否在CardImages.ets中映射。

小结

模板市场的重点不是“塞更多假数据”,而是把内置模板当作正式数据源管理。这个项目把模板目录、分类元信息、搜索过滤、图片映射都收在服务层和资源层,让页面只负责展示和交互。

对卡片类、工具类、模板类应用来说,这种设计可以同时解决首屏空、分类不足、搜索不好用和详情参数丢失几个常见问题。

模板市场不是静态列表,而是“入口、筛选、兜底”的组合题

如果只讲MarketPage.ets里的数组,会显得像一个 UI 摆放示例;必须把它拆成三条真实链路:第一条是首页/底部导航进入市场,第二条是标签、搜索和分类页之间的筛选协作,第三条是图片、标题、统计值缺失时的兜底。Project028 的市场页价值就在这里:它不是后端驱动的复杂商城,但已经具备一个可审核、可扩展的本地模板市场雏形。

MarketPage.ets中,页面同时依赖PageHeaderChipTabsSearchBarStubShowcaseCardBottomNavBar。这说明它不是孤立页面,而是复用项目基础组件来保持视觉一致。marketSummaryCard()appDataService.getMarketSummaryCard()取摘要卡,marketHeroImage()再通过cardImageResource()做图片资源解析。这里最容易被忽略的是兜底:如果摘要卡没有imageKey,页面会回退到CardImageKeys.marketLight,避免市场头图空白。

privatemarketSummaryCard():ShowcaseCardModel{returnappDataService.getMarketSummaryCard(this.selectedTab,this.normalizedQuery());}privatemarketHeroImage():Resource{constsummary:ShowcaseCardModel=this.marketSummaryCard();returncardImageResource(summary.imageKey?summary.imageKey:CardImageKeys.marketLight);}

筛选逻辑也要写清楚。市场页的本地搜索并不直接修改模板源数据,而是通过matchesQuery()过滤展示列表;分类入口则跳转到RoutePaths.category,把更细的分类浏览交给CategoryPage.ets。这种拆法适合轻量应用:市场页承担“发现”,分类页承担“检索”,详情页承担“转化”。如果把三者塞在一个页面里,后续要接远端模板、收藏、下载量排序时会很难维护。

这里的实践判断很明确:本地模板市场不是偷懒,而是阶段性架构选择。Project028 当前没有服务器,也不应该为了展示模板引入不必要的接口层。正确做法是先把数据结构、入口、筛选、兜底、跳转打稳,等模板源变成远端时,只替换数据服务,不改页面交互。

工程检查清单

  • MarketPage -> CategoryPage -> CardDetailPage的入口关系要清楚。
  • imageKey缺失时必须有兜底,避免头图或模板图空白。
  • ChipTabsSearchBarStubShowcaseCard是复用组件,不是普通装饰。
  • 轻量项目可先本地数据闭环,不必过早接入后端。
  • 真实路径:entry/src/main/ets/pages/MarketPage.etsentry/src/main/ets/pages/CategoryPage.etsentry/src/main/ets/common/CardImages.ets