HarmonyOS 模板市场实战:64 款内置卡片、分类补齐与搜索过滤
HarmonyOS 模板市场实战64 款内置卡片、分类补齐与搜索过滤一个卡片工具如果只展示用户已经创建的卡片首屏很容易空。这个项目采用了“真实用户数据 内置模板目录”的组合用户数据为空时首页和分类页仍然能展示完整内容用户创建卡片后管理页再承载真实卡片。本章围绕模板目录、分类补齐和搜索过滤展开实现拆解。内置模板不是写在页面里项目的模板目录放在AppDataService.etsinterfaceTemplateCatalogItem{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 - 倒计时否则后期改文案会很痛苦。分类概览真实数据不足时用模板补齐分类页的“分类概览”不是简单读取用户卡片。真实用户卡片可能只覆盖一两个分类如果直接展示就会造成页面严重空白。项目的策略是先按真实卡片分组。缺失分类时从内置模板目录生成分类 fallback。recommend视图覆盖 8 个分类。fallback 卡片由服务层创建privatecreateCategoryOverviewFallbackCard(categoryId:CardCategoryId,templates:TemplateCatalogItem[]):ShowcaseCardModel|undefined{constcategoryTemplatestemplates.filter((item)item.categoryIdcategoryId);if(!categoryTemplates.length){returnundefined;}constcategoryMetathis.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分类概览点击后页面会显示同类模板列表。列表项才应该携带templateIdconsttemplateId:stringitem.templateId?item.templateId:item.id;router.pushUrl({url:RoutePaths.cardDetail,params:{templateId:templateId}});这样用户路径是分类概览卡 - 同类模板列表 - 模板详情 - 添加到我的卡片这个路径比直接从分类概览进某张默认模板更清晰。搜索过滤覆盖多个字段模板搜索不只搜标题也包含副标题、详情、footer 和 tagsprivategetFilteredTemplates(tabId:string,query:string):TemplateCatalogItem[]{constnormalizedQuery:stringquery.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.etsexportfunctionimageKeyForTemplate(templateId:string,categoryId:CardCategoryId):string{switch(templateId){casebirthday:returntemplate-birthday;caseexam-countdown:returntemplate-exam-countdown;caseweather-brief:returntemplate-weather-brief;default:returnimageKeyForCategory(categoryId);}}如果新增模板但没补图片项目会回退到分类图。这个 fallback 可以保证不崩但长期不应该依赖。新增模板时最好同步补TEMPLATE_CATALOGcard_template_templateId.pngCardImages.ets映射页面侧保持简单分类页只关心数据和点击Grid(){ForEach(this.filteredCategoryCards(),(item:ShowcaseCardModel){GridItem(){ShowcaseCard({item:item,compactBadge:true,onCardClick:(){this.openShowcaseCard(item);}})}},(item:ShowcaseCardModel)item.id)}分类数据怎么补齐、图片怎么映射、搜索怎么过滤页面都不直接处理。验证清单模板目录调整后需要检查recommend分类概览是否覆盖 8 个分类。每个分类下是否有足够模板不出现空列表。搜索关键字能命中标题、标签和详情文案。分类概览点击后进入同类列表而不是直接详情。列表项进入详情时带templateId。新增模板图片是否在CardImages.ets中映射。小结模板市场的重点不是“塞更多假数据”而是把内置模板当作正式数据源管理。这个项目把模板目录、分类元信息、搜索过滤、图片映射都收在服务层和资源层让页面只负责展示和交互。对卡片类、工具类、模板类应用来说这种设计可以同时解决首屏空、分类不足、搜索不好用和详情参数丢失几个常见问题。模板市场不是静态列表而是“入口、筛选、兜底”的组合题如果只讲MarketPage.ets里的数组会显得像一个 UI 摆放示例必须把它拆成三条真实链路第一条是首页/底部导航进入市场第二条是标签、搜索和分类页之间的筛选协作第三条是图片、标题、统计值缺失时的兜底。Project028 的市场页价值就在这里它不是后端驱动的复杂商城但已经具备一个可审核、可扩展的本地模板市场雏形。在MarketPage.ets中页面同时依赖PageHeader、ChipTabs、SearchBarStub、ShowcaseCard和BottomNavBar。这说明它不是孤立页面而是复用项目基础组件来保持视觉一致。marketSummaryCard()从appDataService.getMarketSummaryCard()取摘要卡marketHeroImage()再通过cardImageResource()做图片资源解析。这里最容易被忽略的是兜底如果摘要卡没有imageKey页面会回退到CardImageKeys.marketLight避免市场头图空白。privatemarketSummaryCard():ShowcaseCardModel{returnappDataService.getMarketSummaryCard(this.selectedTab,this.normalizedQuery());}privatemarketHeroImage():Resource{constsummary:ShowcaseCardModelthis.marketSummaryCard();returncardImageResource(summary.imageKey?summary.imageKey:CardImageKeys.marketLight);}筛选逻辑也要写清楚。市场页的本地搜索并不直接修改模板源数据而是通过matchesQuery()过滤展示列表分类入口则跳转到RoutePaths.category把更细的分类浏览交给CategoryPage.ets。这种拆法适合轻量应用市场页承担“发现”分类页承担“检索”详情页承担“转化”。如果把三者塞在一个页面里后续要接远端模板、收藏、下载量排序时会很难维护。这里的实践判断很明确本地模板市场不是偷懒而是阶段性架构选择。Project028 当前没有服务器也不应该为了展示模板引入不必要的接口层。正确做法是先把数据结构、入口、筛选、兜底、跳转打稳等模板源变成远端时只替换数据服务不改页面交互。工程检查清单MarketPage - CategoryPage - CardDetailPage的入口关系要清楚。imageKey缺失时必须有兜底避免头图或模板图空白。ChipTabs、SearchBarStub、ShowcaseCard是复用组件不是普通装饰。轻量项目可先本地数据闭环不必过早接入后端。真实路径entry/src/main/ets/pages/MarketPage.ets、entry/src/main/ets/pages/CategoryPage.ets、entry/src/main/ets/common/CardImages.ets。