OpenHarmony 英语学习 App 实战:自定义生词本、持久化存储与学习数据管理

摘要

英语学习 App 不能只提供固定词库。真实学习中,用户会从课本、阅读、听力、影视台词里遇到自己的生词,因此“自定义生词本”是非常重要的功能。本文以「英语视界 YingYu」项目为例,分享如何在 OpenHarmony/HarmonyOS 中实现自定义生词本、详情弹窗、添加表单、搜索过滤和本地持久化。📘

相关文件包括:

entry/src/main/ets/pages/CustomWordBook.ets
entry/src/main/ets/utils/StorageService.ts
entry/src/main/ets/model/DataModels.ts
entry/src/main/ets/utils/YingYuPreferences.ts

一、自定义生词本要解决什么问题?

固定词库适合系统学习,但用户自己的生词更有价值:

  • 课外阅读遇到的词;
  • 老师补充的重点词;
  • 听力材料里的陌生词;
  • 错题中反复出现的词;
  • 考试作文常用表达。

所以自定义生词本需要具备:

  • 添加单词;
  • 保存音标、释义、例句、标签;
  • 查看详情;
  • 搜索过滤;
  • 参与复习系统;
  • 本地持久化保存。

二、数据模型:CustomWord

项目中使用 CustomWord 表示一个自定义单词:

export interface CustomWord {
  id: string
  word: string
  phonetic?: string
  translation: string
  example?: string
  tags: string[]
  createdAt: string
  reviewCount: number
  mastered: boolean
}

字段设计比较完整:

  • id:唯一标识;
  • word:英文单词;
  • phonetic:可选音标;
  • translation:中文释义;
  • example:可选例句;
  • tags:标签;
  • createdAt:添加时间;
  • reviewCount:复习次数;
  • mastered:是否掌握。

这些字段不仅能展示单词详情,也能和复习系统、统计系统结合。

三、存储 Key 设计

StorageService.ts 中集中管理存储 Key:

const STORAGE_KEY_CUSTOM_WORDS = 'yingyu_custom_words'
const STORAGE_KEY_REVIEW_RECORDS = 'yingyu_review_records'
const STORAGE_KEY_DAILY_TASKS = 'yingyu_daily_tasks'
const STORAGE_KEY_LEARNING_GOALS = 'yingyu_learning_goals'

把 Key 放在统一服务里有两个好处:

  • 避免页面里到处写字符串;
  • 后续迁移数据结构更方便。

四、生成唯一 ID

添加单词时需要生成唯一 ID:

function generateId(): string {
  return Date.now().toString(36) + Math.random().toString(36).substring(2, 9)
}

这是一个轻量方案,适合本地数据。它结合时间戳和随机字符串,在单机使用场景下足够可靠。

五、读取自定义单词

读取逻辑封装在 getCustomWords() 中:

export function getCustomWords(): CustomWord[] {
  try {
    const stored = AppStorage.get<string>(STORAGE_KEY_CUSTOM_WORDS)
    if (stored) {
      return JSON.parse(stored) as CustomWord[]
    }
  } catch (e) {
    console.error('Failed to load custom words:', e)
  }
  return []
}

这里有一个值得保留的习惯:读取失败时返回空数组,而不是让页面崩掉。学习类应用要优先保证可用性。

六、保存自定义单词

保存时把数组序列化成 JSON:

export function saveCustomWords(words: CustomWord[]): void {
  try {
    AppStorage.setOrCreate(STORAGE_KEY_CUSTOM_WORDS, JSON.stringify(words))
  } catch (e) {
    console.error('Failed to save custom words:', e)
  }
}

对于小规模本地生词本,JSON 数组足够简单。后续如果数据量上升,可以迁移到数据库或云端同步。

七、添加单词:补齐系统字段

页面提交的只是用户输入字段,真正保存前由服务层补齐 ID、创建时间、复习次数和掌握状态:

export function addCustomWord(
  word: Omit<CustomWord, 'id' | 'createdAt' | 'reviewCount' | 'mastered'>
): CustomWord {
  const newWord: CustomWord = {
    ...word,
    id: generateId(),
    createdAt: new Date().toISOString(),
    reviewCount: 0,
    mastered: false
  }

  const words = getCustomWords()
  words.push(newWord)
  saveCustomWords(words)
  return newWord
}

这样页面只关心用户输入,服务层负责数据完整性。

八、更新和删除

更新单词:

export function updateCustomWord(id: string, updates: Partial<CustomWord>): boolean {
  const words = getCustomWords()
  const index = words.findIndex(w => w.id === id)
  if (index !== -1) {
    words[index] = { ...words[index], ...updates }
    saveCustomWords(words)
    return true
  }
  return false
}

删除单词:

export function deleteCustomWord(id: string): boolean {
  const words = getCustomWords()
  const filtered = words.filter(w => w.id !== id)
  if (filtered.length !== words.length) {
    saveCustomWords(filtered)
    return true
  }
  return false
}

返回 boolean 可以让页面判断操作是否成功,并显示 Toast 或刷新列表。

九、搜索过滤

搜索逻辑同时支持英文、中文释义和标签:

export function searchCustomWords(keyword: string): CustomWord[] {
  const words = getCustomWords()
  const lower = keyword.toLowerCase()
  return words.filter(w =>
    w.word.toLowerCase().includes(lower) ||
    w.translation.toLowerCase().includes(lower) ||
    w.tags.some(t => t.toLowerCase().includes(lower))
  )
}

这种搜索方式对用户很友好:

  • 输入英文可以找到单词;
  • 输入中文可以找到释义;
  • 输入标签可以找到专题词。

十、添加单词表单

CustomWordBook.ets 中定义了 AddWordSheet 子组件,用于处理添加表单。

@Component
struct AddWordSheet {
  @Link visible: boolean
  onWordAdded: () => void = () => {}

  @State wordText: string = ''
  @State phonetic: string = ''
  @State translation: string = ''
  @State example: string = ''
  @State tagsInput: string = ''
}

这里把表单做成独立子组件,而不是直接放在页面里,可以减少整页重建,也更利于维护。

提交时处理标签:

const tags = this.tagsInput
  .split(',')
  .map(t => t.trim())
  .filter(t => t.length > 0)

标签支持逗号分隔,适合用户输入“考试, 写作, 高频词”这样的内容。

十一、详情弹窗:WordDetailSheet

项目还实现了单词详情弹窗:

@Component
struct WordDetailSheet {
  @Link visible: boolean
  @Prop word: CustomWord = {
    id: '',
    word: '',
    translation: '',
    tags: [],
    createdAt: '',
    reviewCount: 0,
    mastered: false
  }
}

弹窗进入时有缩放和透明度动画:

aboutToAppear() {
  this.dialogScale = 0.88
  this.dialogOpacity = 0
  animateTo({
    duration: 260,
    curve: Curve.EaseOut
  }, () => {
    this.dialogScale = 1
    this.dialogOpacity = 1
  })
}

展示内容包括:

  • 单词;
  • 音标;
  • 中文释义;
  • 例句;
  • 标签;
  • 添加时间;
  • 复习次数或掌握状态。

十二、本地持久化的另一层:Preferences 封装

项目中还有 YingYuPreferences.ts,用于 Preferences 和 AppStorage 的兜底封装。

export function initYingYuUserData(context: common.UIAbilityContext): void {
  if (pref) {
    return
  }
  try {
    pref = preferences.getPreferencesSync(context, { name: 'yingyu_user_data_v1' })
  } catch (e) {
    console.error('initYingYuUserData failed:', JSON.stringify(e))
  }
}

读取时优先 Preferences,失败则回退 AppStorage:

export function yingyuPrefGet(key: string): string | undefined {
  if (pref) {
    const v = pref.getSync(key, '') as string
    return v.length > 0 ? v : undefined
  }

  const fallback = AppStorage.get<string>(key)
  if (fallback !== undefined && fallback.length > 0) {
    return fallback
  }
  return undefined
}

这种封装让数据层更稳,也便于后续统一替换存储方案。

十三、小结

本文结合「英语视界 YingYu」项目,拆解了自定义生词本的完整实现:

  • 使用 CustomWord 描述用户单词;
  • 使用 StorageService 封装增删改查;
  • 使用 JSON + AppStorage 保存轻量数据;
  • 使用 searchCustomWords() 支持英文、中文、标签搜索;
  • 使用 AddWordSheet 管理添加表单;
  • 使用 WordDetailSheet 展示单词详情;
  • 使用 Preferences 封装增强持久化可靠性。

自定义生词本让学习 App 从“固定内容工具”变成“用户自己的学习资料库”。这类功能虽然不 flashy,但非常能提升长期使用价值。📖

img

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐