本文以“面试通”鸿蒙应用项目为基础,结合华为官方开发指南,系统阐述试题搜索页面及其核心组件“搜索历史工具”的设计思路与开发实现。

一、 系统架构与数据流设计

一个高效的搜索功能,其背后是清晰的数据流转和处理逻辑。下图展示了“面试通”搜索模块从用户输入到结果展示的完整流程:

数据层

业务逻辑层

用户界面层

“1. 输入关键词
触发搜索”

“2. 保存记录
触发搜索”

“3. 执行搜索”

“4. 返回结果”

“5. 点击历史记录
快捷搜索”

搜索首页
SearchPage.ets

搜索结果页
SearchResultPage.ets

搜索控制器
SearchController

历史记录管理器
SearchHistoryManager

本地持久化存储

试题数据源
本地/网络

搜索算法/过滤器

通过以上流程,我们将复杂的搜索交互分解为清晰的步骤,为后续的详细开发奠定基础。

二、 搜索页面(SearchPage)设计与实现

搜索首页是用户的第一触点,需要兼顾功能性和引导性。

1. 核心状态与页面结构
// SearchPage.ets
import { SearchHistoryManager, HistoryItem } from '../utils/SearchHistoryManager';
import router from '@ohos.router';

@Entry
@Component
struct SearchPage {
  // 搜索框输入值
  @State searchKeyword: string = '';
  // 控制清空按钮显示
  @State showClearIcon: boolean = false;
  // 搜索历史列表
  @State historyList: HistoryItem[] = [];
  // 热搜推荐列表
  @State hotSearchList: string[] = ['HarmonyOS', 'ArkTS', 'Ability', 'UI组件', '网络请求'];

  // 引入历史管理工具
  private historyManager: SearchHistoryManager = new SearchHistoryManager();

  // 页面显示时加载历史记录
  aboutToAppear(): void {
    this.loadSearchHistory();
  }

  // 加载历史记录
  private loadSearchHistory(): void {
    this.historyList = this.historyManager.getHistoryList();
  }

  build() {
    Column({ space: 0 }) {
      // 顶部导航栏
      this.buildTitleBar()

      // 搜索输入区域
      this.buildSearchInput()

      // 内容区域:根据有无输入切换视图
      if (this.searchKeyword.trim().length > 0) {
        // 显示搜索联想建议(可根据需要实现)
        this.buildSuggestions()
      } else {
        // 显示历史记录和热搜
        Scroll() {
          Column() {
            this.buildSearchHistory()
            this.buildHotSearch()
          }
        }
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor($r('app.color.background'))
  }
}
2. 搜索输入栏组件实现

搜索输入栏是核心交互组件,需提供良好的即时反馈。

// SearchPage.ets (续)
@Builder
buildSearchInput() {
  Row({ space: 12 }) {
    // 搜索图标
    Image($r('app.media.ic_search'))
      .width(24)
      .height(24)

    // 文本输入框
    TextInput({
      placeholder: '请输入试题关键词,如“生命周期”',
      text: this.searchKeyword
    })
    .placeholderColor($r('app.color.placeholder'))
    .placeholderFont({ size: 16 })
    .height(40)
    .flexGrow(1)
    .fontSize(18)
    .caretColor($r('app.color.primary'))
    .onChange((value: string) => {
      this.searchKeyword = value;
      this.showClearIcon = value.length > 0;
      // 此处可添加防抖函数,用于实时搜索建议
    })
    .onSubmit(() => {
      // 回车键提交搜索
      if (this.searchKeyword.trim()) {
        this.doSearch();
      }
    })

    // 动态清空按钮
    if (this.showClearIcon) {
      Image($r('app.media.ic_clear'))
        .width(20)
        .height(20)
        .onClick(() => {
          this.searchKeyword = '';
          this.showClearIcon = false;
        })
    }

    // 搜索按钮(移动端适配)
    Button('搜索', { type: ButtonType.Normal })
      .fontSize(16)
      .height(40)
      .padding({ left: 16, right: 16 })
      .backgroundColor(this.searchKeyword.trim() ? $r('app.color.primary') : $r('app.color.button_disabled'))
      .enabled(this.searchKeyword.trim().length > 0)
      .onClick(() => {
        if (this.searchKeyword.trim()) {
          this.doSearch();
        }
      })
  }
  .width('100%')
  .padding({ left: 24, right: 24, top: 16, bottom: 16 })
  .backgroundColor(Color.White)
}
3. 搜索历史与热搜区域
// SearchPage.ets (续)
@Builder
buildSearchHistory() {
  if (this.historyList.length > 0) {
    Column({ space: 12 }) {
      // 标题栏
      Row({ space: 0 }) {
        Text('搜索历史')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .flexGrow(1)

        Image($r('app.media.ic_delete'))
          .width(20)
          .height(20)
          .onClick(() => {
            // 清空历史确认弹窗
            this.showClearHistoryDialog();
          })
      }
      .width('100%')

      // 历史标签流式布局
      Flow({ spacing: 12, direction: FlowDirection.LeftToRight }) {
        ForEach(this.historyList, (item: HistoryItem) => {
          // 历史记录标签组件
          this.buildHistoryTag(item)
        })
      }
    }
    .width('100%')
    .padding(24)
  }
}

@Builder
buildHistoryTag(item: HistoryItem) {
  Text(item.keyword)
    .fontSize(14)
    .padding({ left: 16, right: 16, top: 8, bottom: 8 })
    .backgroundColor($r('app.color.tag_background'))
    .fontColor($r('app.color.text_secondary'))
    .borderRadius(20)
    .onClick(() => {
      // 点击历史记录,填充关键词并执行搜索
      this.searchKeyword = item.keyword;
      this.doSearch();
    })
}

三、 搜索历史工具(SearchHistoryManager)核心开发

搜索历史工具是保障良好用户体验的关键,需要实现高效、持久化的本地存储。

1. 数据模型与存储方案设计

“面试通”采用鸿蒙首选项(Preferences)存储搜索历史,这是轻量键值存储的官方推荐方案。

// utils/SearchHistoryManager.ts
import dataPreferences from '@ohos.data.preferences';
import util from '@ohos.util';

// 历史记录项数据模型
export interface HistoryItem {
  id: string;           // 唯一ID(时间戳+随机数)
  keyword: string;      // 搜索关键词
  timestamp: number;    // 搜索时间戳
  searchCount: number;  // 搜索次数(用于智能排序)
}

// 搜索历史管理工具类
export class SearchHistoryManager {
  private static readonly PREFERENCE_NAME: string = 'search_history_store';
  private static readonly MAX_HISTORY_COUNT: number = 20; // 最大保存条数
  private preferences: dataPreferences.Preferences | null = null;

  // 初始化首选项数据库
  async initialize(): Promise<void> {
    try {
      const context = getContext(this) as common.UIAbilityContext;
      this.preferences = await dataPreferences.getPreferences(context, SearchHistoryManager.PREFERENCE_NAME);
    } catch (error) {
      console.error(`搜索历史首选项初始化失败: ${error.message}`);
    }
  }

  // 添加搜索记录(核心方法)
  async addSearchHistory(keyword: string): Promise<void> {
    if (!keyword.trim() || !this.preferences) {
      return;
    }

    const trimmedKeyword = keyword.trim();
    let historyList = await this.getHistoryList();

    // 1. 检查是否已存在相同关键词
    const existingIndex = historyList.findIndex(item => item.keyword === trimmedKeyword);
    const currentTime = new Date().getTime();

    if (existingIndex >= 0) {
      // 已存在:更新次数和时间,移至顶部
      const existingItem = historyList[existingIndex];
      existingItem.searchCount += 1;
      existingItem.timestamp = currentTime;
      // 从原位置移除
      historyList.splice(existingIndex, 1);
    } else {
      // 新记录:创建新条目
      const newItem: HistoryItem = {
        id: this.generateId(trimmedKeyword, currentTime),
        keyword: trimmedKeyword,
        timestamp: currentTime,
        searchCount: 1
      };
      // 如果达到上限,移除最旧的一条
      if (historyList.length >= SearchHistoryManager.MAX_HISTORY_COUNT) {
        historyList.pop(); // 按时间排序后,最后一条是最旧的
      }
    }

    // 将更新的项目插入到数组开头
    if (existingIndex >= 0) {
      historyList.unshift(historyList.splice(existingIndex, 1)[0]);
    } else {
      historyList.unshift({
        id: this.generateId(trimmedKeyword, currentTime),
        keyword: trimmedKeyword,
        timestamp: currentTime,
        searchCount: 1
      });
    }

    // 3. 保存更新后的列表
    await this.saveHistoryList(historyList);
  }

  // 生成唯一ID
  private generateId(keyword: string, timestamp: number): string {
    const input = `${keyword}_${timestamp}_${Math.random()}`;
    const md5 = util.hashString.hash(util.hashString.HashMd5, input);
    return md5.substring(0, 12); // 取前12位作为ID
  }

  // 获取历史记录列表(按时间倒序)
  async getHistoryList(): Promise<HistoryItem[]> {
    if (!this.preferences) {
      await this.initialize();
    }

    try {
      const historyJson = await this.preferences.get('history_list', '[]');
      const list = JSON.parse(historyJson) as HistoryItem[];
      // 按时间戳降序排序(最新的在前)
      return list.sort((a, b) => b.timestamp - a.timestamp);
    } catch (error) {
      console.error(`读取搜索历史失败: ${error.message}`);
      return [];
    }
  }

  // 保存历史记录列表到首选项
  private async saveHistoryList(list: HistoryItem[]): Promise<void> {
    if (!this.preferences) return;

    try {
      const jsonStr = JSON.stringify(list);
      await this.preferences.put('history_list', jsonStr);
      await this.preferences.flush(); // 提交更改
    } catch (error) {
      console.error(`保存搜索历史失败: ${error.message}`);
    }
  }

  // 清空所有搜索历史
  async clearAllHistory(): Promise<boolean> {
    try {
      await this.preferences.delete('history_list');
      await this.preferences.flush();
      return true;
    } catch (error) {
      console.error(`清空搜索历史失败: ${error.message}`);
      return false;
    }
  }

  // 删除单条历史记录
  async deleteHistoryItem(id: string): Promise<boolean> {
    const historyList = await this.getHistoryList();
    const newList = historyList.filter(item => item.id !== id);

    if (newList.length !== historyList.length) {
      await this.saveHistoryList(newList);
      return true;
    }
    return false;
  }
}
2. 在页面中集成历史管理工具
// SearchPage.ets (续)
// 执行搜索并保存历史
private async doSearch(): Promise<void> {
  const keyword = this.searchKeyword.trim();
  if (!keyword) return;

  // 1. 保存到搜索历史
  await this.historyManager.addSearchHistory(keyword);

  // 2. 刷新本地历史列表显示
  this.loadSearchHistory();

  // 3. 跳转到搜索结果页,传递搜索关键词
  router.pushUrl({
    url: 'pages/SearchResultPage',
    params: { keyword: keyword }
  }).catch(err => {
    console.error(`跳转到搜索结果页失败: ${err.message}`);
  });
}

// 显示清空历史确认对话框
private showClearHistoryDialog(): void {
  AlertDialog.show({
    title: '清空搜索历史',
    message: '确定要清空所有搜索历史记录吗?此操作不可撤销。',
    primaryButton: {
      value: '取消',
      action: () => {
        // 取消操作
      }
    },
    secondaryButton: {
      value: '清空',
      backgroundColor: $r('app.color.danger'),
      fontColor: Color.White,
      action: async () => {
        const success = await this.historyManager.clearAllHistory();
        if (success) {
          this.historyList = [];
          // 可在此处添加成功提示
        }
      }
    }
  });
}

四、 搜索结果页(SearchResultPage)实现

搜索结果页需要清晰地展示搜索条件和匹配的试题列表。

// SearchResultPage.ets
@Component
struct SearchResultPage {
  // 通过路由参数接收的搜索关键词
  private searchKeyword: string = router.getParams()?.['keyword'] || '';
  // 试题列表
  @State questionList: QuestionItem[] = [];
  // 加载状态
  @State isLoading: boolean = true;
  // 是否无结果
  @State noResults: boolean = false;

  aboutToAppear(): void {
    if (this.searchKeyword) {
      this.performSearch();
    }
  }

  // 执行搜索逻辑
  private async performSearch(): Promise<void> {
    this.isLoading = true;
    // 模拟网络请求延迟
    setTimeout(async () => {
      try {
        // 实际项目中,这里调用服务接口
        const results = await this.mockSearchApi(this.searchKeyword);
        this.questionList = results;
        this.noResults = results.length === 0;
      } catch (error) {
        console.error(`搜索失败: ${error.message}`);
        this.noResults = true;
      } finally {
        this.isLoading = false;
      }
    }, 300);
  }

  build() {
    Column() {
      // 固定顶部搜索栏
      this.buildStickySearchBar()

      // 内容区域
      if (this.isLoading) {
        this.buildLoadingView()
      } else if (this.noResults) {
        this.buildEmptyView()
      } else {
        this.buildResultList()
      }
    }
  }

  @Builder
  buildStickySearchBar() {
    Column() {
      // 显示当前搜索关键词
      Text(`"${this.searchKeyword}" 的搜索结果`)
        .fontSize(18)
        .fontWeight(FontWeight.Medium)
        .margin({ bottom: 8 })

      // 结果显示统计
      Text(`共找到 ${this.questionList.length} 道相关试题`)
        .fontSize(14)
        .fontColor($r('app.color.text_secondary'))
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .border({ width: { bottom: 1 }, color: $r('app.color.border') })
  }
}

五、 高级功能与优化建议

1. 搜索联想建议(Type-ahead Suggestions)

在用户输入时提供实时建议,可基于本地历史或调用服务端接口。

// 防抖函数优化搜索建议
private debounceTimer: number | undefined;

private onSearchInputChange(value: string): void {
  this.searchKeyword = value;
  this.showClearIcon = value.length > 0;

  // 清除之前的定时器
  if (this.debounceTimer) {
    clearTimeout(this.debounceTimer);
  }

  // 设置新的防抖定时器(300毫秒后触发)
  if (value.trim().length > 0) {
    this.debounceTimer = setTimeout(() => {
      this.fetchSuggestions(value);
    }, 300);
  }
}

private async fetchSuggestions(keyword: string): Promise<void> {
  // 1. 首先从历史记录中匹配
  const history = await this.historyManager.getHistoryList();
  const historySuggestions = history
    .filter(item => item.keyword.includes(keyword))
    .slice(0, 5); // 最多显示5条

  // 2. 如果有服务端接口,可以在此处调用
  // const serverSuggestions = await searchApi.getSuggestions(keyword);

  // 3. 更新UI显示建议
  // this.suggestionList = [...historySuggestions, ...serverSuggestions];
}
2. 历史记录智能排序算法

结合搜索频率和时间因素进行智能排序。

// 在SearchHistoryManager中添加智能排序方法
getSmartHistoryList(): HistoryItem[] {
  // 计算每个项目的权重分数
  const scoredList = this.historyList.map(item => {
    // 时间衰减因子(24小时衰减一半)
    const timeFactor = Math.exp(-(Date.now() - item.timestamp) / (24 * 60 * 60 * 1000) * Math.LN2);
    // 搜索频率因子
    const frequencyFactor = Math.log(1 + item.searchCount);
    // 综合权重
    const score = timeFactor * frequencyFactor;

    return { ...item, score };
  });

  // 按权重分数降序排列
  return scoredList.sort((a, b) => b.score - a.score);
}

六、 效果对比与总结

特性 基础实现 优化实现 用户体验提升
历史存储 仅内存存储,应用关闭丢失 首选项持久化存储,长期保存 用户数据不丢失,体验连贯
历史排序 仅按时间倒序 智能排序(频率+时间衰减) 高频搜索更易访问,更智能
输入交互 仅手动提交搜索 防抖联想建议 + 历史快捷填充 减少输入,搜索更便捷
性能优化 每次操作直接读写存储 批量操作 + 内存缓存 响应更快,更省电
数据安全 明文存储 关键词脱敏 + 数据加密(可扩展) 用户隐私更安全

总结
本文基于HarmonyOS ArkTS开发规范,完整实现了“面试通”应用的试题搜索功能。核心亮点在于:

  1. 架构清晰:通过分层设计,分离UI展示、业务逻辑与数据持久化。
  2. 体验流畅:结合搜索历史、智能联想、防抖优化,减少用户操作步骤。
  3. 存储可靠:使用鸿蒙首选项(Preferences)实现历史记录的可靠本地存储。
  4. 扩展性强:工具类设计便于复用,可轻松集成到应用其他模块。

此实现严格遵循了华为官方开发指南,充分利用了ArkUI声明式语法和状态管理,开发者可在此基础上,进一步集成网络搜索、试题高亮、搜索过滤等高级功能。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐