搜索页看起来只是一个输入框、一组结果和一次路由跳转真正决定可信度的却是数据边界用户搜到的究竟是法条、题目还是题库高亮文字是否真的参与了匹配点击结果后能否打开同一条内容空结果究竟表示没有数据还是索引尚未准备好这些问题如果没有明确答案页面即使能“搜到东西”也不能称为可复核的法条搜索。本文基于知律项目D:\huawei\one19-11、包名com.jiaweikang.one19的真实源码复核SearchPage.ets、MockBanks.ets、公共Question模型和PracticePage.ets。当前页面真实实现了本地题库名匹配、题干匹配、题型分类筛选、20 条结果上限、搜索前首页态和空结果态但它没有独立法条数据模型没有关键词高亮也没有法条详情页。题目卡片点击后传入bankId和mode: random进入的是整套题库随机练习而不是命中的具体题目。本文先还原这些事实再设计一条可落地的 HarmonyOS 5.0 本地搜索链路。一、先给“法条搜索”划清能力边界当前SearchPage导入的数据是import { BANKS, REGIONS, getQuestions } from ../mock/MockBanksBANKS是六个法律主题题库getQuestions返回题目。搜索范围只有题库名称与题干this.bankResults this.categoryType.length 0 ? [] : BANKS.filter(bank bank.name.toLowerCase().includes(keyword)) if (question.stem.toLowerCase().includes(keyword)) { questionResults.push(question) }所以当前能力更准确的名称是“法律题库与题目搜索”。题目解析中虽然包含《民法典》第 143 条等引用但doSearch()并不检索analysis也不存在法条正文、效力层级、发布机关、生效日期等字段。文章中的升级方案不会把题目解析伪装成完整法规数据库。二、当前页面有三种真实入口第一种是普通关键词用户输入文字点击“搜索”或触发输入法提交。第二种是热门搜索点击REGIONS中的“民法”“劳动法”“网络安全”等分区名写入keyword后调用doSearch()。第三种是分类入口路由参数带入categoryType与categoryName页面在aboutToAppear()自动执行检索interface SearchParams { categoryType?: string categoryName?: string }分类模式不是关键词包含匹配而是按Question.type精确过滤。用户只要编辑输入框使值不再等于分类名页面就会清空分类状态重新回到关键词检索。这是当前代码里值得保留的模式切换规则。三、四态 UI 已经具备基础骨架页面通过searched、bankResults与questionResults组合出三类可见状态searched false展示热门搜索和提示已搜索且两类结果都为空展示NoResult()任一结果非空展示ResultList()。如果升级为真实法条索引还应增加loading与error形成完整状态机type SearchStatus idle | loading | content | empty | error interface SearchViewState { status: SearchStatus query: string items: SearchResultItem[] message: string }本地小数据检索通常很快但索引首次构建、文件读取或数据库迁移仍可能失败。不能把“还没加载完”和“确实没有结果”渲染成同一张空图。四、空输入处理是正确的但语义还可更明确当前逻辑会对关键词执行trim()。当关键词为空且没有分类条件时它清空结果并把searched设为falseif (this.keyword.trim().length 0 this.categoryType.length 0) { this.bankResults [] this.questionResults [] this.searched false return }这避免了空字符串与所有题库名、题干都匹配的问题。升级时仍应保留这一规则并把规范化后的查询作为唯一检索输入避免显示值、匹配值和高亮值各自处理后产生偏差。五、中文搜索不能只依赖 toLowerCasetoLowerCase()对英文大小写有用对“民法典”“劳动合同”这类中文词没有规范化效果。更稳妥的本地查询函数至少应处理首尾空白、连续空白与全角空格function normalizeQuery(value: string): string { return value .trim() .replace(/\u3000/g, ) .replace(/\s/g, ) .toLowerCase() }如果产品要支持“第143条”和“第 143 条”互相命中可以再建立专用法条编号规范化函数但不要直接删除正文中的全部标点。过度规范化可能让原本不同的条款编号或金额表达式碰撞。六、当前检索复杂度可以接受MockBanks固定生成 6 个题库每个题库扩展到 250 道题总量约 1500。每次搜索会遍历全部题库与题目并在累计 20 条后停止。这个数量级在本地内存中通常足够轻量不需要为了“性能”立即引入复杂数据库。需要注意的是题目扩展使用多个情景前缀循环生成变体。同一基础题可能出现“真实案例”“实务考点”“强化训练”等版本。直接按题干包含匹配会得到语义高度相似的多个结果。结果数上限保护了 UI却没有解决重复内容挤占前 20 条的问题。七、先去重再截断当前代码在发现第 20 条时立即break因此结果取决于题库与题目数组顺序。建议为搜索结果建立稳定去重键function baseStem(stem: string): string { return stem.replace(/^【[^】]】\s*/, ).trim() } function dedupeQuestions(items: Question[]): Question[] { const seen new Setstring() const result: Question[] [] for (const item of items) { const key ${item.bankId}:${baseStem(item.stem)} if (seen.has(key)) { continue } seen.add(key) result.push(item) } return result }这里的去重只针对当前模拟题扩展规则。真实法条不能只按正文去重而应使用法规 ID、条款 ID 与版本 ID防止把修订前后的不同文本误合并。八、搜索结果需要统一模型目前页面分别维护Bank[]和Question[]。当未来加入法规、法条、案例与办事指南时继续增加数组会让排序和状态管理越来越分散。可以在服务层统一为type SearchResultKind bank | question | law interface SearchResultItem { id: string kind: SearchResultKind title: string summary: string sourceName: string score: number route: SearchRoute } interface SearchRoute { url: string params: Recordstring, string }id必须对应可稳定回读的数据实体score只用于本地排序不能显示为平台热度route必须能打开当前命中项而不是只打开它所属的集合。九、真正的法条模型不能复用 Question当前公共Question包含题干、选项、正确答案和解析interface Question { id: string bankId: string chapterId: string type: string stem: string options: Option[] answer: string analysis: string }法条至少需要法规名称、条号、正文、版本与效力信息interface LawArticle { id: string lawId: string lawName: string articleNo: string content: string keywords: string[] effectiveFrom: string version: string }如果数据没有官方来源与版本信息页面应明确标注为学习材料或题目解析不能对外宣称“现行有效法条全文”。十、关键词高亮必须来自匹配范围当前QuestionResultCard直接渲染Text(question.stem)没有任何Span或分段文本因此目前不存在关键词高亮。改造时不要在 UI 层临时重复查找而应由搜索服务返回高亮范围interface HighlightRange { start: number end: number } interface HighlightedText { text: string ranges: HighlightRange[] }同一份规范化规则必须同时参与匹配和范围计算。否则用户可能看到结果被命中却找不到高亮或者高亮偏移到错误字符。十一、ArkUI 中安全渲染高亮片段对于单个关键词可以先把文本切成普通片段和命中片段interface TextSegment { text: string highlighted: boolean } function splitByKeyword(text: string, keyword: string): TextSegment[] { const query keyword.trim() if (!query) { return [{ text, highlighted: false }] } const source text.toLowerCase() const target query.toLowerCase() const segments: TextSegment[] [] let cursor 0 let index source.indexOf(target) while (index 0) { if (index cursor) { segments.push({ text: text.slice(cursor, index), highlighted: false }) } segments.push({ text: text.slice(index, index query.length), highlighted: true }) cursor index query.length index source.indexOf(target, cursor) } if (cursor text.length) { segments.push({ text: text.slice(cursor), highlighted: false }) } return segments }这段方案适合当前中英文混合题干。若加入多关键词、同义词或拼音匹配应在索引层计算原文偏移避免 UI 猜测命中位置。十二、用 Text 与 Span 保持可访问文本ArkUI 可以在一个Text容器中渲染多个SpanBuilder HighlightedTitle(text: string, keyword: string) { Text() { ForEach(splitByKeyword(text, keyword), (segment: TextSegment) { Span(segment.text) .fontColor(segment.highlighted ? Colors.PRIMARY : Colors.TEXT_PRIMARY) .fontWeight(segment.highlighted ? FontWeight.Bold : FontWeight.Medium) }) } .fontSize(Sizes.BODY_FONT) .lineHeight(22) .maxLines(3) .textOverflow({ overflow: TextOverflow.Ellipsis }) }高亮不应只靠低对比度背景色。正文与背景仍要满足可读性命中项可以同时使用字重和主题色深浅色模式下都从资源或主题令牌取值。十三、不要把用户输入当成正则表达式有些实现会直接写new RegExp(keyword, gi)。当用户输入(、[、等字符时表达式可能报错或改变匹配含义。当前需求只需要字面量包含匹配indexOf更安全。如果确实要使用正则必须先转义元字符并对超长输入设置上限。法律文本中括号、点号和条款编号很常见这个边界不能忽略。十四、相关性排序要可解释当前结果顺序就是数据顺序。一个简单、可复核的本地评分可以是function scoreQuestion(question: Question, query: string): number { const stem normalizeQuery(question.stem) const analysis normalizeQuery(question.analysis) if (stem query) return 100 if (stem.startsWith(query)) return 80 if (stem.includes(query)) return 60 if (analysis.includes(query)) return 30 return 0 }题干完全匹配优先其次是前缀、包含和解析命中。分值是排序权重不是正确率、热度或权威等级UI 没有必要把它展示给用户。十五、空结果要告诉用户“搜了什么”当前空结果文案是“未找到相关内容”“换个关键词试试”功能上成立但缺少查询上下文。可以改为Text(未找到“${this.viewState.query}”相关内容) Text(可缩短关键词或尝试法规名称、条款编号和主题词)同时保留清空操作和热门主题入口。不要在空结果时展示虚构推荐数量也不要为了显得“有内容”而返回不相关题目。十六、初始态与空结果态不能混用用户刚进入页面时没有搜索行为此时展示热门主题合理用户执行搜索后没有结果才应展示空结果。当前searched已经区分了二者。升级为状态机后idle对应初始态empty对应有效查询后的零结果。清空输入时回到idle而不是保留上一轮“未找到”的提示。十七、当前题目点击不是详情跳转源码中的点击事件是router.pushUrl({ url: pages/PracticePage, params: { bankId: question.bankId, mode: random } })它没有传questionId。PracticePage在随机模式中加载整个题库并洗牌所以用户点击搜索命中的某道题后不保证首先看到它。这是集合入口不是详情入口。十八、精确题目路由的最小改造如果暂时不增加法条页可以先让练习页支持单题预览interface PracticeParams { bankId: string chapterId?: string questionId?: string mode: string }搜索结果点击时传入稳定题目 IDrouter.pushUrl({ url: pages/PracticePage, params: { bankId: question.bankId, questionId: question.id, mode: preview } })练习页通过bankId获取题库后再按questionId查找找不到时进入明确错误态不能静默回退到随机题库否则用户仍会看到不一致内容。十九、独立法条页要使用 lawId 与 articleId当产品加入真实法规数据后路由契约应避免传完整正文interface LawDetailParams { lawId: string articleId: string }详情页根据 ID 从同一数据仓库回读内容。这样既减少路由载荷也能保证收藏、历史和分享都引用同一条记录。法条版本变化时可以额外传versionId或由仓库解析当前版本。二十、路由参数需要运行时校验ArkTS 接口只提供编译期约束外部路由参数仍可能缺失。详情页应先验证function parseLawDetailParams( params: LawDetailParams | undefined ): LawDetailParams | undefined { if (!params || !params.lawId || !params.articleId) { return undefined } return params }无效参数要展示“内容不存在或已更新”并提供返回操作。不要用空字符串查询仓库后展示第一条数据。二十一、把检索从页面移到服务层当前doSearch()同时负责输入判定、遍历数据、分类逻辑、数量上限与状态写入。数据继续扩展后页面会越来越难测试。建议拆成interface SearchOptions { query: string categoryType?: string limit: number } class LegalSearchService { search(options: SearchOptions): SearchResultItem[] { const query normalizeQuery(options.query) // 读取仓库、匹配、去重、评分、排序、截断 return [] } }页面只负责收集输入、调用服务、更新SearchViewState和渲染。索引与数据读取再通过 Repository 隔离符合Page - Service - Repository的职责方向。二十二、小数据内存索引与大数据 RDB 的选择当前约 1500 道本地题目应用启动后已经在内存中生成直接遍历最简单。若法规正文扩展到数万条、需要多字段排序和版本管理才值得评估关系型数据库。RDB 表可以包含article_id、law_id、article_no、content、normalized_content、version与更新时间。是否使用全文索引要以 HarmonyOS 当前版本官方 API 能力为准没有确认 API 时不应在文章里承诺不存在的全文检索接口。二十三、索引构建不能阻塞首帧如果法条数据来自本地 JSON 或数据库迁移首次构建索引应放在服务层异步执行并让 UI 进入loading。完成后只提交最终结果给状态。还要处理查询竞态用户快速输入“劳动”后又输入“劳动合同”较慢的第一轮结果不应覆盖第二轮。可以使用递增请求号private searchRequestId: number 0 private async runSearch(query: string): Promisevoid { const requestId this.searchRequestId this.viewState { status: loading, query, items: [], message: } const items await this.searchService.searchAsync(query) if (requestId ! this.searchRequestId) { return } this.viewState { status: items.length 0 ? content : empty, query, items, message: } }即使当前检索是同步的保留清晰的请求所有权也能为后续数据规模增长留出空间。二十四、输入提交与按钮点击要走同一方法当前输入法提交和“搜索”按钮都调用doSearch()这是正确做法。升级后也应只保留一个入口例如submitSearch()集中完成规范化、空值判断与服务调用。热门主题点击、分类页自动进入和历史搜索回填也要复用这个入口避免四种入口产生四种状态转换。二十五、分类筛选与关键词筛选可以组合当前分类模式下只比较question.type不再使用关键词。当用户编辑分类名时页面直接退出分类模式。这个交互简单但无法表达“只在案例分析中搜索合同”。如果产品需要组合筛选可以把查询条件建模为interface SearchFilter { query: string categoryTypes: string[] bankIds: string[] }筛选条件作为结构化数据参与服务调用UI 使用筛选标签显示当前范围。清除某个标签只修改对应字段不应通过比较显示字符串来推断用户意图。二十六、测试必须覆盖高亮边界至少准备以下本地单元测试空字符串、全角空格与连续空格回到初始态关键词位于开头、中间、结尾时范围正确多次出现的关键词全部高亮(、[、等字符不会造成正则异常无结果返回空数组不返回推荐伪结果相似题去重后再执行 20 条截断结果 ID 能回读同一条题目或法条过期路由 ID进入错误态。这些测试可以在纯 ArkTS 服务层完成不依赖 ArkUI 页面实例。二十七、UI 验收要覆盖长文本与安全区当前页面已经读取顶部避让区和底部导航指示区并在结果列表底部增加安全间距。继续改造时要验证手机竖屏下长法条标题最多显示几行横屏、小窗和平板宽度下结果卡片是否过宽深色模式中高亮色与正文色是否都可读输入法弹出后最后一条结果能否滚动到可见区域空结果图、文案和返回操作是否被系统导航区遮挡连续点击同一结果是否重复压入多个详情页。高亮样式不能改变文本容器稳定高度否则结果滚动时会发生明显跳动。二十八、法律内容还需要版本与来源提示技术上“搜得到”不等于内容可以被当作法律依据。真实法条详情至少要展示法规名称、条号、内容来源、版本或更新时间并在数据过期时有更新策略。当前模拟题解析中出现法律名称和条号适合作为普法练习材料但模型里没有来源 URL、公布机关和版本字段。因此本阶段只能如实定位为本地学习题库搜索不能写成联网法规查询也不能声称覆盖全部现行法律。二十九、性能指标要从真实测量产生当前源码没有搜索耗时埋点也没有平台 PV、点击率或转化率数据。文章不虚构“毫秒级”“命中率 99%”等数字。可以在开发阶段记录本地指标const startedAt Date.now() const items service.search(options) const elapsedMs Date.now() - startedAt这些数据只用于调试或性能测试不应未经统计设计就展示给用户。发布文章时也不把单次设备测量包装成平台结论。三十、渐进式落地顺序第一步保留现有题库与题干搜索增加统一SearchResultItem和可测试的LegalSearchService。第二步先实现安全高亮、去重、排序和完整四态 UI不改变数据来源。第三步为题目结果增加questionId精确路由修复“点击命中题目却进入随机整库”的语义偏差。第四步在来源与版本可核验后新增LawArticle、仓库和详情页再把能力名称升级为真正的法条搜索。第五步根据真实数据规模决定继续内存遍历还是迁移到 RDB不提前引入不必要的复杂度。三十一、闭环验收清单交付前逐项确认输入框、按钮、热门主题和分类入口复用同一搜索方法空输入回到初始态有效查询零结果进入空结果态匹配、高亮与排序使用同一份规范化查询题目变体去重发生在 20 条截断之前高亮不会被特殊字符破坏每条结果携带稳定 ID点击结果能打开同一条内容而不是随机集合无效详情参数有明确错误态与返回动作法条数据具备来源和版本字段后才对外称为法条深浅色、长文本、小窗、平板和系统安全区均完成检查不展示虚构热度、排名、搜索耗时或覆盖率。三十二、结语知律现有SearchPage已经有一个可用的本地检索骨架数据全部来自本地MockBanks关键词匹配和分类筛选逻辑清楚初始态、空结果态、列表态与安全区适配也能从源码复核。它当前的真实边界同样明确搜的是题库和题干没有关键词高亮没有独立法条模型点击题目进入的是随机练习而非精确详情。可靠的升级路线不是在卡片上加一块彩色背景而是让数据实体、匹配范围、展示高亮和详情路由共享同一份契约。先用稳定 ID 保证“搜到什么就打开什么”再补来源、版本与法条正文搜索能力才会从一个能用的题库入口成长为可解释、可测试、可复现的 HarmonyOS 法律内容检索链路。---本文部分内容由 AI 辅助整理。所有现状判断均基于D:\huawei\one19-11中com.jiaweikang.one19的本地源码复核示例改造代码用于说明工程方案不代表当前版本已经实现独立法条库、关键词高亮或法条详情页。