十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【中国方言题库|12】HarmonyOS ArkTS 题库列表组件实战:减少多地区页面重复并保证点击反馈

【中国方言题库|12】HarmonyOS ArkTS 题库列表组件实战:减少多地区页面重复并保证点击反馈 在方言学习应用中题库入口看起来只是“把若干地区卡片排成列表”真正落到工程里却会同时碰到数据排序、组件复用、学习进度回显、页面路由和多设备布局。若每个地区分别维护一套页面四川话、粤语、东北话等入口很快会出现重复结构如果只复用外观而没有统一数据与交互点击目标、进度口径和大屏布局仍然会分叉。中国方言题库当前版本没有为六个地区复制六份列表代码而是由BankListPage遍历统一的BANKS数据再把单个题库交给BankCard渲染。页面负责排序和容器布局卡片负责题库信息、学习进度与跳转目标UserDataManager提供真实本地进度读取。本文面向 HarmonyOS 5.0 及以上版本基于项目中的真实 ArkTS 源码复核这条链路并明确当前实现已经解决什么、还没有解决什么。本文唯一核验标记题库入口由数据驱动卡片点击只维护一条路由链路。一、先看真实交付边界本文涉及的核心源码不是抽象示例而是以下文件entry/src/main/ets/views/BankListPage.ets entry/src/main/ets/common/components/BankCard.ets entry/src/main/ets/common/components/ProgressBar.ets entry/src/main/ets/mock/MockBanks.ets librarya/src/main/ets/models/Dialect.ets librarya/src/main/ets/utils/UserDataManager.ets entry/src/main/ets/pages/Index.ets其中BankListPage是题库 Tab 的页面组件BankCard是可以在列表和网格中复用的题库卡片。BANKS当前包含四川话、粤语、东北话、上海话、闽南语和客家话六个题库。文章不会把未来规划写成现有能力当前排序只有“热度优先”和“题数优先”没有关键词筛选、地区筛选、分页加载或远端请求。二、为什么不能给每个地区复制一套入口如果六个地区分别写一套卡片短期代码直观长期却会形成四种同步成本新增字段时需要修改六处例如同时增加题数和正确率。调整触控区域时需要逐页确认容易出现某些卡片只能点文字、另一些整卡可点。大屏从单列切换三列时六套页面可能出现不同断点。学习进度的读取公式容易分叉有的显示累计答题数有的显示题库完成率。真实实现把这些变化收敛为“数据 一个卡片组件 一个容器页面”。题库之间不同的是Bank数据和少量详情页路由不同地区不再复制完整的列表视图。三、Bank 数据模型是复用的起点公共能力层定义的Bank接口包含题库卡片所需的完整字段export interface Bank { id: string regionId: string name: string cover: Resource totalCount: number accuracy: number hot: number chapters: Chapter[] }id是进度查询和页面跳转的稳定标识regionId表示所属地区cover是资源对象totalCount、accuracy、hot分别对应题数、默认正确率和热度。页面不需要判断“这是粤语还是四川话才显示什么字段”它只依赖同一个Bank契约。这里值得强调accuracy是模型里的初始值或兜底值不等于用户当前真实正确率。真实进度存在时BankCard会优先读取本地记录。四、统一目录数据如何驱动六个题库MockBanks.ets中的BANKS直接保存六个题库export const BANKS: Bank[] [ { id: b_sichuan, regionId: sichuan, name: 四川话题库, cover: $r(app.media.img_bank_cover_sichuan), totalCount: 0, accuracy: 0, hot: 98, chapters: SICHUAN_CHAPTERS }, // 其余五个题库使用相同结构 ]题库总量在目录同步流程中根据实际题目重新计算因此卡片读取的是统一目录对象而不是页面里手工写死的“某地区有多少题”。新增地区时列表结构本身不需要增加一个新的Builder只要数据契约、题目目录和目标页面满足现有规则遍历即可渲染新项。五、复制数组再排序避免污染全局目录BankListPage.filteredBanks()的第一行不是直接对BANKS调用sort()而是先展开复制private filteredBanks(): Bank[] { const result [...BANKS] if (this.sortAsc) { result.sort((a, b) b.hot - a.hot) } else { result.sort((a, b) b.totalCount - a.totalCount) } return result }JavaScript 和 ArkTS 数组的sort()会原地修改数组。如果直接写BANKS.sort(...)题库 Tab 的一次点击就会改变共享目录顺序首页、考试页等其他使用BANKS的页面也可能观察到变化。复制后排序让变化停留在当前渲染计算中保持共享目录的基准顺序。六、sortAsc 的名字与实际语义并不完全一致sortAsc从字面看像“是否升序”但源码中的真实语义是true - 按 hot 从大到小 false - 按 totalCount 从大到小两种模式其实都是降序只是排序字段不同。因此文章不把它描述成“升序/降序切换”。更准确的工程命名可以是sortMode并用联合类型或枚举表达hot与count。不过当前文章只复核现有实现不擅自宣称源码已经完成这个重构。七、排序按钮为什么能立即刷新页面页面把排序状态声明为StateState sortAsc: boolean true点击文本后切换布尔值.onClick(() { this.sortAsc !this.sortAsc })ArkUI 状态更新会触发相关 UI 重新构建filteredBanks()随之使用另一个排序字段。当前按钮文案也与状态绑定Text(this.sortAsc ? 按热度优先 : 按题数优先)这条链路不需要维护另一份“已排序数组”状态避免目录变化后还要手工同步缓存。八、ForEach 键为何包含排序状态列表和网格都使用同一套键生成逻辑(bank: Bank) ${bank.id}_${this.sortAsc}稳定的bank.id标识题库后缀把当前排序模式纳入键值。切换模式后键集合发生变化框架会按新的顺序重建对应项。当前实现以更明确的刷新换取简单可靠的排序反馈。这不是唯一方案。若卡片内部状态很多频繁重建可能增加代价当前卡片状态很轻只有测量宽度和共享进度链接因此这种写法在六个题库规模下是可控的。九、列表页只负责页面级职责BankListPage的build()很克制build() { Column() { this.Header() this.SortBar() this.BankList() } .width(100%) .height(100%) .backgroundColor(Colors.BACKGROUND) }它把标题、排序区和内容区拆成三个Builder。页面级状态只有排序模式、页面宽度和全局断点。题库封面、正确率、已答数量、进度条、卡片紧凑布局和点击跳转都下沉到BankCard页面没有重复这些细节。十、BankCard 如何接收统一数据卡片通过一个Bank属性接收数据Component export struct BankCard { bank: Bank {} as Bank StorageLink(bankProgress) progressList: BankProgress[] [] State cardWidth: number 360 }调用侧只需要BankCard({ bank: bank })同一个组件既能放进手机ListItem也能放进大屏Flex网格。它不依赖父容器是列表还是网格只根据自己的实际宽度决定横向卡片或紧凑卡片。十一、共享进度不是由父页面层层传递BankCard用StorageLink(bankProgress)关联应用级进度数组。卡片根据bank.id查找自己的记录private realFinished(): number { const p UserDataManager.getProgress(this.progressList, this.bank.id) return p ? p.finished : 0 }这样题库页不需要先遍历进度、组装额外 ViewModel再把多个数字逐层传给卡片。练习页更新同一个bankProgress后关联状态可反映到卡片显示。同时要保持边界意识AppStorage适合跨页面共享当前运行时状态持久化仍由UserDataManager和 Preferences 负责卡片并没有直接操作存储文件。十二、正确率采用“本地记录优先、目录值兜底”真实实现如下private realAccuracy(): number { const p UserDataManager.getProgress(this.progressList, this.bank.id) if (p p.finished 0) { return p.correct / p.finished } return this.bank.accuracy }只有存在记录且finished 0时才做除法避免零除。没有学习记录时回退到bank.accuracy。当前目录初始值为0所以新用户看到的是0%不是伪造的历史平均水平。十三、进度条为什么要限制最大值完成比例的计算是private progressRatio(): number { if (this.bank.totalCount 0) return 0 return Math.min(this.realFinished() / this.bank.totalCount, 1) }finished在当前数据模型中是累计答题次数重复练习后可能大于题库题量。卡片把比例限制在1ProgressBar内部又把输入限制到[0, 1]因此进度条宽度不会溢出容器。但界面文字仍显示真实累计值例如“已答 160 题”即使题库只有 120 道。这是当前数据口径不应把它误写成“去重完成题数”。十四、横向卡片适合手机单列宽度不小于 280vp 时卡片使用HorizontalCard()。左侧是 92vp 的封面右侧依次显示名称、入口提示、题数、正确率和进度条。标题设置了.layoutWeight(1) .constraintSize({ minWidth: 0 }) .maxLines(1) .textOverflow({ overflow: TextOverflow.Ellipsis })minWidth: 0允许文本在弹性布局里真正收缩配合单行省略长题库名不会把右侧“进入”挤出卡片。元信息用可换行的Flex窄宽度下两个标签可以自然换行。十五、紧凑卡片服务于大屏三列当卡片实际宽度小于 280vp 时private useCompactLayout(): boolean { return this.cardWidth 0 this.cardWidth 280 }组件切换到CompactCard()把封面放到顶部并占满卡片宽度文本信息放在下方。它不是简单缩小横向卡片而是改变信息排列方向以免三列网格中的封面和文字互相争夺水平空间。十六、页面断点与卡片宽度是两层判断页面是否使用网格由以下条件决定private useGridLayout(): boolean { return this.currentBp lg this.pageWidth 700 }第一层依据全局currentBreakpoint第二层依据页面真实宽度。进入网格后每个项目宽度是32%卡片再通过自身onAreaChange获得实际宽度并决定是否使用紧凑结构。这种“容器决定列数、子组件决定内部排版”的两层适配比只看设备类型更稳健。侧栏、分屏或窗口缩放都会改变内容区宽度卡片最终以自己拿到的空间为准。十七、为什么网格使用 Flex 而不是固定 Grid大屏分支使用Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.SpaceBetween }) { // 每项宽度 32% }六个题库会按三列排列SpaceBetween分配列间剩余空间项目底部保留 12vp。当前数据量固定且很小Flex足以表达换行网格。若未来题库达到数百个需要懒加载和复杂列策略再评估Grid或数据分页更合理当前源码没有这类能力。十八、手机分支为何使用 List手机和中等宽度分支采用ListList({ space: 12 }) { ForEach(this.filteredBanks(), (bank: Bank) { ListItem() { BankCard({ bank: bank }) } }) } .layoutWeight(1) .edgeEffect(EdgeEffect.Spring)ListItem明确表达纵向列表语义layoutWeight(1)让内容区占用标题和排序条之外的剩余高度。关闭滚动条保持界面简洁弹簧边缘效果提供滚动到边界时的反馈。十九、整卡点击比只点小文字更易操作横向和紧凑卡片都把.onClick()挂在卡片根容器上.onClick(() { router.pushUrl({ url: this.detailPageUrl(), params: { bankId: this.bank.id } }) })“进入”文字是视觉提示真正的可点击区域覆盖整个卡片。对手机触控而言这比要求用户命中一个小按钮更容易对三列布局而言也避免不同卡片中的按钮因文字换行而错位。二十、点击反馈的真实含义当前源码中可核验的点击反馈有两部分排序文本点击后文案和卡片顺序发生变化。题库卡片点击后执行router.pushUrl()进入目标详情页。源码没有给卡片配置显式stateStyles、按压缩放、触觉反馈、加载态或重复点击节流因此不能宣称已经实现这些效果。这里所说“保证点击反馈”是指点击区域和结果链路统一、可观察而不是虚构额外动画。二十一、详情页路由为何集中在卡片内部detailPageUrl()根据题库 ID 返回目标页面private detailPageUrl(): string { switch (this.bank.id) { case b_sichuan: return pages/SichuanBankPage case b_yue: return pages/YueBankPage // 其余地区 default: return pages/BankDetailPage } }页面只创建卡片不需要在两个布局分支中重复六次路由判断。无论卡片位于手机列表还是大屏网格点击都会走同一个函数并统一携带bankId。二十二、默认路由是扩展时的重要保护switch的default返回通用BankDetailPage。因此新增一个没有专属详情页的题库时仍可落到通用详情页而不是生成空 URL。不过这不代表新增数据一定能完整工作。新题库仍需保证题目目录、章节、资源和详情页对bankId的处理一致。默认路由只是减少遗漏导致的直接导航失败不是全自动注册机制。二十三、搜索入口同样保持单一跳转标题区右侧使用固定触控尺寸承载搜索图标Stack() { Image($r(app.media.ic_common_search)) .width(24) .height(24) } .width(Sizes.TOUCH_TARGET) .height(Sizes.TOUCH_TARGET) .onClick(() { router.pushUrl({ url: pages/SearchPage }) })图标视觉尺寸是 24vp触控容器使用统一TOUCH_TARGET避免“看得见但难点中”。搜索本身由独立页面负责题库列表不混入搜索输入状态。二十四、公共主题常量减少视觉重复页面和卡片从librarya引入Colors、Sizes统一使用背景色、表面色、主要文字色、提示文字色、字号、圆角和内边距。这样多个地区卡片保持相同视觉层级也便于后续做深浅色资源映射。这不等于当前文件单独完成了完整深色模式验证。是否真正适配仍取决于主题常量和资源在不同限定目录中的定义以及系统栏、封面图等整体表现。二十五、卡片职责的四层拆分可以把当前组件链路归纳为四层Catalog : BANKS 提供统一题库数据 Page : BankListPage 负责排序、列表/网格选择 Card : BankCard 负责信息组合、内部适配和点击路由 State : UserDataManager AppStorage 提供真实学习进度每层都有明确输入输出。页面不直接持久化数据卡片不修改目录目录不关心当前屏幕宽度进度服务不决定视觉布局。二十六、空数据时当前页面会怎样如果BANKS为空filteredBanks().length会显示0列表或网格没有项目。源码没有专门的空态图、说明文本或重试按钮。由于当前目录是本地静态数据正常构建中不会依赖网络加载空目录更可能是打包或代码配置错误。但从通用组件质量看未来若目录改为远端或可配置数据应该增加明确的empty与error状态而不是只呈现空白区域。二十七、排序计算的成本与当前规模filteredBanks()在文案计数和内容渲染处都可能被调用每次都会复制并排序。对六个题库而言成本可以忽略代码也很直接。若未来题库数量显著增大可以在状态变化时更新派生数组或由 ViewModel 缓存排序结果。但现在引入复杂缓存反而要处理目录更新、状态失效和一致性超出了当前真实需求。二十八、当前没有真正的“筛选”方法名叫filteredBanks()但实现没有过滤条件只做排序。页面标题虽写“按地区与题型挑选练习内容”实际入口目前提供的是地区题库列表和独立搜索页不包含页面内地区/题型筛选控件。技术文章必须保留这个事实边界。一个更准确的未来命名是sortedBanks()如果后续加入筛选再让“过滤”承担真实业务含义。二十九、参数传递如何保持目标一致卡片既通过detailPageUrl()选择页面又总是传递params: { bankId: this.bank.id }即使专属页面目前可以通过页面自身确定地区保留bankId仍让通用详情页和后续功能拥有统一上下文。路由目标和参数都从同一个bank对象推导避免标题显示四川话、参数却误传粤语 ID 这类手工绑定错误。三十、重复点击与路由失败仍需谨慎当前.onClick()直接调用router.pushUrl()没有等待 Promise也没有点击锁。快速连续点击理论上可能多次触发导航路由表缺失或页面加载失败时卡片也没有本地错误提示。如果真实运行测试发现重复入栈应在保持交互轻量的前提下增加短暂导航锁并在导航完成或失败后释放。是否需要修改必须以设备测试为依据不能仅凭推测给当前实现贴上“有 bug”的标签。三十一、多设备适配还要结合 Index 容器BankListPage不是孤立占满整个设备。Index.ets在当前断点为sm、md、lg时使用底部导航其余模式使用侧边导航。题库页拿到的pageWidth是内容区域宽度而不是物理屏幕宽度。这正是同时检查currentBp与pageWidth的价值同一台大屏设备在侧栏、分屏或窗口化状态下内容空间可能不足 700vp此时页面仍回到单列列表避免硬塞三列。三十二、建议的验证矩阵对这套组件做回归时至少验证以下场景1. 首次进入六个题库按热度从高到低排列 2. 切换排序文案变为“按题数优先”顺序按 totalCount 更新 3. 再次切换恢复热度排序 4. 点击封面、标题、空白区都进入同一题库详情 5. 点击搜索进入 SearchPage 6. 已有学习记录已答数量与正确率回显 7. 重复练习后累计已答可超过题量进度条仍不溢出 8. 小屏单列 List 可滚动 9. 大屏且内容宽度至少 700vp三列 Flex 10. 三列卡片宽度小于 280vp切换紧凑结构 11. 长题库名单行省略不遮挡“进入” 12. 返回题库页排序与当前运行时状态表现符合预期三十三、可进一步演进的方向在不改变当前职责边界的前提下可以逐步演进把布尔排序改成显式SortMode提高可读性。把filteredBanks()更名为sortedBanks()与真实行为一致。用配置映射替代switch让专属详情页注册更集中。根据真实设备测试决定是否增加按压态与导航节流。为目录异常增加空态但不为本地静态数据虚构网络重试。若题库规模扩大再评估惰性网格、分页和排序缓存。这些是从现有源码自然延伸的工程方向不代表当前版本已经实现。三十四、这套实现真正减少了什么重复它减少的不是几行ForEach而是把多个地区共同拥有的规则集中起来统一数据契约 统一排序入口 统一学习进度口径 统一卡片信息结构 统一整卡点击区域 统一路由参数 统一列表/网格切换 统一窄卡片内部排版当规则变化时开发者可以在对应职责层修改一次而不必逐地区寻找相似代码。三十五、结语中国方言题库的题库 Tab 展示了一个很实用的 HarmonyOS ArkTS 组件化思路页面用状态驱动排序和容器布局ForEach用统一数据生成地区入口BankCard读取共享学习进度并根据自身宽度调整结构整张卡片只维护一条可核验的路由链路。它也保留了清晰的事实边界当前没有页面内筛选、显式按压动画、导航节流、异步加载和专门空态finished是累计答题次数不是去重完成数。只有把这些边界说清楚组件复用、点击反馈和多设备适配的经验才真正可复核、可迁移。AI 辅助声明本文由 AI 辅助整理与润色技术结论、代码路径、数据口径和实现边界均依据项目真实源码复核。
返回列表