摘要:结果页不是把几个数字摆进卡片,而是一次答题会话的“只读结算边界”。中国方言题库的 ExamResultPage 接收 PracticePage 传来的题库、分数、题量、正确数、用时和逐题记录,计算正确率与等级,生成答题状态网格,并把考试快照写入本地历史。本文面向 HarmonyOS 5.0 及以上版本,基于真实 ArkTS 源码复核参数契约、状态恢复、零题保护、结果持久化、错题解析和再次考试链路。同时会明确一个容易被标题掩盖的事实:当前页面没有按地区统计薄弱项;若要增加该能力,必须先把 questionId 关联回题目与题库地区,不能从现有三个汇总数字中猜测。

结果页封面

一、先划清源码边界:已有结算,没有地区聚合

本文复核的核心文件是:

entry/src/main/ets/pages/ExamResultPage.ets
entry/src/main/ets/pages/PracticePage.ets
entry/src/main/ets/common/constants/ThemeConstants.ets
librarya/src/main/ets/models/Dialect.ets
librarya/src/main/ets/utils/UserDataManager.ets
libraryb/src/main/ets/utils/QuestionUtils.ets

真实页面已经实现以下能力:

  1. 从路由参数恢复考试快照;
  2. 展示百分制分数、正确率、用时和等级印章;
  3. 按题号生成正确、错误、未答三态网格;
  4. 把本次结果追加到 examHistory
  5. 跳转到错题解析;
  6. 使用 replaceUrl 发起再次考试;
  7. 根据系统底部避让区增加安全间距。

但源码中没有“地区薄弱项”列表,也没有在结果页读取 RegionQuestion.bankId 或题型分布。页面中显示的 ${236}/${1258} 排名也是固定展示值,不是真实联网排名。工程分析必须把“已经上线的能力”和“可设计的增强方案”分开。本文后半部分讨论地区薄弱项时,会明确标注为基于现有数据模型的可复核扩展,不把设计稿写成既有功能。

这条边界也决定了全文的核心原则:

结果页只消费快照,不重新推导考试事实。

唯一校验标记:结果页只消费快照,不重新推导考试事实

二、为什么结果页要接收完整快照

PracticePage 在考试正常结束或倒计时结束时,会把本次会话数据放入路由参数:

router.replaceUrl({
  url: 'pages/ExamResultPage',
  params: {
    bankId: this.bankId,
    score: score,
    total: this.questions.length,
    correct: correctCount,
    durationSec: this.timerSec,
    records: JSON.stringify(this.records)
  }
})

这组参数不是可以随意删减的 UI 文案,而是一份结算快照。score 决定分数与等级,total 决定正确率分母和答题网格长度,correct 决定正确率,durationSec 决定用时,records 决定每道题的状态与错题解析内容,bankId 则决定历史归属和再次考试的题库。

如果结果页只接收 score,它无法区分“8/10”和“16/20”;如果只接收 records,超时未答题不会出现在记录数组中,页面也无法知道应补多少个“未答”格子。因此,totalrecords.length 不能互相替代。

三、用接口固定路由契约

源码用 ExamResultParams 描述参数形状:

interface ExamResultParams {
  bankId: string
  score: number
  total: number
  correct: number
  durationSec: number
  records: string
}

在 HarmonyOS 5.0+ 的 Stage 模型应用中,页面间通过 router 传值很常见。接口的价值不是让运行时自动验证,而是让 ArkTS 编译期知道页面期待什么。调用方和消费方仍要共同维护契约,尤其要注意以下约束:

字段 语义 合法性要求
bankId 本次考试所属题库 应能关联真实题库
score 百分制结果 建议限制在 0 到 100
total 试卷题量 不应为负数
correct 正确题数 应满足 0 <= correct <= total
durationSec 实际用时 不应为负数
records 逐题记录 JSON 解析后应为 AnswerRecord[]

当前源码直接赋值,没有额外归一化。这在调用链完全受控时可以工作,但如果未来从通知、卡片、跨设备接续或外部 Want 打开结果页,就应增加输入校验。

四、页面状态如何从路由参数恢复

aboutToAppear() 是结算初始化的关键:

aboutToAppear(): void {
  const params = router.getParams() as ExamResultParams | undefined
  if (params) {
    this.bankId = params.bankId
    this.score = params.score
    this.total = params.total
    this.correct = params.correct
    this.durationSec = params.durationSec
    try {
      this.records = JSON.parse(params.records) as AnswerRecord[]
    } catch (_) {}
  }
  this.rankInfo = getRankByScore(this.score)
  this.examHistory = UserDataManager.addExamHistory(
    this.examHistory, this.bankId, this.score, this.total, this.correct, this.durationSec)
}

这段代码完成三件事:

  1. 把路由快照转为 ArkUI @State
  2. 根据分数选择等级文案、颜色和印章资源;
  3. 把考试结果追加到 @StorageLink('examHistory')

状态字段全部给了确定初值:

@State score: number = 0
@State total: number = 0
@State correct: number = 0
@State durationSec: number = 0
@State bankId: string = ''
@State records: AnswerRecord[] = []

因此,即使参数缺失,页面也不会因为未初始化字段直接崩溃。但“能显示”不等于“数据有效”:参数缺失时仍会写入一个空题库、零分、零题的历史记录,这是当前实现值得修正的地方。

五、JSON 解析失败时为什么不能只吞异常

源码对 records 做了 try/catch

try {
  this.records = JSON.parse(params.records) as AnswerRecord[]
} catch (_) {}

这样可以避免损坏的 JSON 让页面退出,具备基本稳定性。不过空 catch 会隐藏数据问题。用户看到的结果可能是:

  • 分数卡仍显示传入分数;
  • 正确率仍来自 correct / total
  • 答题网格因为 records=[] 而全部显示“未答”;
  • “错题解析”没有可供关联的错误记录。

同一页面出现互相矛盾的指标,比显式错误态更难排查。更稳妥的方案是增加解析状态:

@State recordsValid: boolean = true

private parseRecords(raw: string): AnswerRecord[] {
  try {
    const parsed = JSON.parse(raw) as AnswerRecord[]
    return Array.isArray(parsed) ? parsed : []
  } catch (_) {
    this.recordsValid = false
    return []
  }
}

recordsValidfalse 时,可以保留分数卡,同时把逐题网格替换为“逐题记录不可用”,并禁用错题解析按钮。这里的重点是保持页面事实一致。

六、正确率计算中的零题保护

页面显示正确率的表达式是:

Math.round(this.correct / Math.max(this.total, 1) * 100)

Math.max(this.total, 1) 避免了除零。若 total=0,正确率显示 0%,不会出现 NaN%Infinity%。这是小代码,却是结果页稳定性的必要防线。

但它只解决算术安全,没有解决业务合法性。若收到 correct=8, total=0,页面仍会显示 800%。建议先归一化:

private normalizedTotal(): number {
  return Math.max(0, this.total)
}

private normalizedCorrect(): number {
  return Math.min(Math.max(0, this.correct), this.normalizedTotal())
}

private accuracy(): number {
  const total = this.normalizedTotal()
  if (total === 0) return 0
  return Math.round(this.normalizedCorrect() / total * 100)
}

把计算移出 build() 也更符合 ArkUI 声明式页面的职责:UI 树负责表达布局,方法负责准备稳定数据。

七、分数、正确率和逐题记录要保持同源

当前调用方生成数据时,正常结束分支使用:

const correctCount = this.records.filter(r => r.correct).length
const score = Math.round(correctCount / this.questions.length * 100)

超时分支使用公共工具:

const correctCount = QuestionUtils.correctCount(this.records)
const score = QuestionUtils.calcScore(this.records, this.questions.length)

两条路径的公式一致:正确数来自 records,分母来自试卷题量。这个同源关系很重要。若以后修改评分规则,例如不同题型权重不同,不能只改一条路径,否则同一份答题记录会因“手动提交”和“超时提交”得到不同分数。

更好的做法是让两条路径都调用一个纯函数,返回统一结算对象:

interface ExamSnapshot {
  total: number
  correct: number
  score: number
}

function settle(records: AnswerRecord[], total: number): ExamSnapshot {
  const correct = QuestionUtils.correctCount(records)
  return {
    total,
    correct,
    score: QuestionUtils.calcScore(records, total)
  }
}

结果页仍然只消费快照,评分规则则集中在练习会话的结算边界。

结算数据流

八、等级印章是可复核的纯映射

页面通过 getRankByScore() 生成 RankInfo

export function getRankByScore(score: number): RankInfo {
  if (score >= 90) return { label: '优秀', stamp: $r('app.media.img_exam_stamp_excellent'), color: Colors.ERROR }
  if (score >= 75) return { label: '良好', stamp: $r('app.media.img_exam_stamp_good'), color: Colors.ACCENT }
  if (score >= 60) return { label: '及格', stamp: $r('app.media.img_exam_stamp_pass'), color: Colors.SUCCESS }
  return { label: '继续努力', stamp: $r('app.media.img_exam_stamp_fail'), color: Colors.TEXT_HINT }
}

边界值是 90、75、60。测试时不能只测中间值,应至少覆盖:

59 -> 继续努力
60 -> 及格
74 -> 及格
75 -> 良好
89 -> 良好
90 -> 优秀

该函数没有副作用,适合做单元测试。视觉层只消费 labelstampcolor,不会重复写判断分支。

九、答题网格如何表达正确、错误和未答

AnswerRecord 的真实结构很小:

export interface AnswerRecord {
  questionId: string
  selected: string
  correct: boolean
}

结果页没有直接遍历 records,而是以 total 为上限生成网格:

private makeAnswerGrid(): GridItem[] {
  const result: GridItem[] = []
  for (let i = 0; i < this.total; i++) {
    let state: string = 'empty'
    if (i < this.records.length) {
      state = this.records[i].correct ? 'correct' : 'wrong'
    }
    result.push({ index: i + 1, state })
  }
  return result
}

这个设计正确表达了超时场景:已经产生记录的题目显示正确或错误,尚未提交的题目显示未答。若直接遍历 records,后面的未答题会从页面消失,用户无法分辨“试卷只有 12 题”还是“20 题只答了 12 题”。

十、网格默认依赖答题顺序

makeAnswerGrid() 默认 records[i] 就是第 i+1 道题。这个假设来自 PracticePage 每次提交后按顺序 push

this.records.push({
  questionId: q.id,
  selected: key,
  correct
})

当前页面没有答题卡跳题功能,因此顺序成立。若未来允许先答第十题、再回到第一题,单纯依赖数组索引就会失真。届时记录模型应增加 questionIndex,或者结果页拿到试卷题目 ID 顺序,再按 questionId 建立映射:

const recordMap = new Map<string, AnswerRecord>()
for (const record of records) {
  recordMap.set(record.questionId, record)
}

这不是当前代码已经实现的能力,而是从现有顺序约束推导出的演进边界。

十一、状态颜色集中处理,避免 UI 分支扩散

源码把颜色映射放在方法中:

private stateColor(state: string): string {
  if (state === 'correct') return Colors.SUCCESS
  if (state === 'wrong') return Colors.ERROR
  return Colors.DIVIDER
}

build() 中只需要:

.backgroundColor(this.stateColor(item.state))

这种写法比在每个修饰器中嵌套三元表达式更容易审查。不过 state 目前是普通 string,类型过宽。ArkTS 中可以收窄为联合类型:

type AnswerState = 'correct' | 'wrong' | 'empty'

interface GridItem {
  index: number
  state: AnswerState
}

这样新增状态时,编译器更容易帮助发现遗漏。

十二、用时格式化复用 QuestionUtils

结果页没有自行拼接分钟和秒,而是调用:

private formatDuration(sec: number): string {
  return QuestionUtils.formatTime(sec)
}

公共实现为:

static formatTime(sec: number): string {
  const m = Math.floor(sec / 60)
  const s = sec % 60
  return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
}

因此 65 秒显示为 01:05。练习页倒计时和结果页用时使用同一格式化函数,避免同一应用出现 1:501:051分5秒 三套不一致格式。

输入仍应在调用前保证非负整数。若 durationSec=-1,当前函数会生成异常格式;若来自可信的内部计时器问题不大,若未来允许导入历史则应校验。

十三、考试历史为何使用 @StorageLink

页面声明:

@StorageLink('examHistory') examHistory: ExamHistory[] = []

@StorageLink 让组件字段与 AppStorage 中同名数据保持双向联系。结果页调用 addExamHistory() 后,将返回的新数组重新赋给 examHistory

this.examHistory = UserDataManager.addExamHistory(
  this.examHistory,
  this.bankId,
  this.score,
  this.total,
  this.correct,
  this.durationSec
)

ExamHistory 保存的是真实结算字段:

export interface ExamHistory {
  bankId: string
  score: number
  total: number
  correct: number
  durationSec: number
  finishedAt: string
}

它没有保存逐题 records,因此历史列表能展示分数和用时,却不能从历史项恢复逐题解析。这是明确的数据边界。

十四、本地持久化采用“新记录在前”

UserDataManager.addExamHistory() 的实现是:

static addExamHistory(
  records: ExamHistory[],
  bankId: string,
  score: number,
  total: number,
  correct: number,
  durationSec: number
): ExamHistory[] {
  const result: ExamHistory[] = [{
    bankId,
    score,
    total,
    correct,
    durationSec,
    finishedAt: nowStr()
  }, ...records]
  UserDataManager.persist(UserDataManager.K_EXAM, result)
  return result
}

新记录放在数组头部,考试列表天然按最近优先展示。persist() 使用 Preferences 同步写入并 flushSync(),适合体量较小的本地历史。随着记录持续增长,JSON 数组会不断变大;如果产品要保留数千次考试、支持组合筛选或统计趋势,应该评估关系型存储,而不是无限扩张单个 Preferences 字符串。

十五、aboutToAppear 可能导致重复写历史

当前代码每次进入 aboutToAppear() 都追加历史。首次打开结果页符合预期,但如果页面因为生命周期变化再次触发,或者某种导航回退使页面重新出现,就可能写入重复记录。

可以为本次考试引入稳定 sessionId,并在历史记录中保存:

interface ExamHistory {
  sessionId: string
  bankId: string
  score: number
  total: number
  correct: number
  durationSec: number
  finishedAt: string
}

写入前按 sessionId 去重。另一种做法是在 PracticePage 完成结算时持久化,结果页只展示;这样更符合“结果页只消费快照”的原则。当前源码尚未实现去重,文章不能把重复保护当作现成功能。

十六、错题解析链路为什么传递 records

点击“错题解析”时,页面跳转到练习页:

router.pushUrl({
  url: 'pages/PracticePage',
  params: {
    bankId: this.bankId,
    mode: 'wrongAnalysis',
    records: JSON.stringify(this.records)
  }
})

PracticePage 会解析记录、筛出 correct=false 的项,再按 questionId 从当前题库查回问题:

this.records = examRecords.filter(
  (record: AnswerRecord) => !record.correct
)

const all = getQuestions(params.bankId)
for (const record of this.records) {
  const found = all.find(q => q.id === record.questionId)
  if (found) {
    wrongQs.push(found)
  }
}

因此,错题解析依赖三者一致:

  1. bankId 指向原考试题库;
  2. record.questionId 仍能在该题库中找到;
  3. 题库版本没有删除或改写这些 ID。

这也是为什么题目 ID 应是稳定业务键,而不是列表下标。

十七、“再考一次”使用 replaceUrl 的导航语义

再次考试的代码是:

router.replaceUrl({
  url: 'pages/PracticePage',
  params: {
    bankId: this.bankId,
    mode: 'exam'
  }
})

使用 replaceUrl 而不是 pushUrl,意味着当前结果页被新考试页替换。用户完成第二次考试后,不会通过系统返回键回到上一张旧结果页。这符合重复考试的任务流:结果是阶段终点,再考一次开始新会话。

错题解析则用 pushUrl,因为用户看完解析后仍可能返回当前结果。这两个路由选择体现的是不同返回栈语义,不是随意替换。

十八、底部安全区适配真实系统导航区域

页面通过:

@StorageLink('navigationIndicatorHeightPx')
navigationIndicatorHeightPx: number = 0

计算底部间距:

private bottomSafePadding(): number {
  return Math.max(
    Sizes.BOTTOM_NAV_MIN_PADDING,
    this.getUIContext().px2vp(this.navigationIndicatorHeightPx)
  )
}

按钮区最终使用:

.padding({
  left: Sizes.PADDING_LARGE,
  right: Sizes.PADDING_LARGE,
  top: 10,
  bottom: 16 + this.bottomSafePadding()
})

这让底部按钮在不同导航模式下保持可点击,不会压进手势指示区。px2vp() 也避免把系统返回的像素直接当 vp 使用。对 HarmonyOS 多设备和小窗适配来说,这类边界比单纯加固定 16vp 更可靠。

十九、页面滚动与底部操作区分离

结果主体放在 Scroll 中,并使用 .layoutWeight(1);底部按钮则位于滚动区域之外。这样在小屏、横屏或字体变大时:

  • 分数卡、指标和答题网格仍可滚动访问;
  • “错题解析”和“再考一次”始终保持在底部操作区;
  • 系统避让区只影响操作区,不会重复叠加到全部内容。

答题网格使用 Flex({ wrap: FlexWrap.Wrap }),每个题号格固定 36 x 36,通过换行适应宽度。这种结构比固定列数的手写网格更适合不同窗口宽度。

结果页职责结构

二十、当前“排名”是静态文本,不能当真实平台数据

指标区包含:

this.MetricItem(`${236}/${1258}`, '排名')

源码没有网络请求、排行榜服务、用户账号或本地排名算法。因此 236/1258 是固定展示值,不是实时排名,也不能在技术文章中描述成“超过多少用户”。

如果应用定位为纯本地题库,建议把该指标改成可由本地数据复核的内容,例如:

  • 本次正确题数;
  • 本次未答题数;
  • 历史最高分;
  • 相比上次提升分值。

这比伪造在线排名更符合隐私声明和离线产品边界。

二十一、如何基于真实记录增加地区薄弱项

当前 AnswerRecord 只有 questionIdselectedcorrect。要统计地区薄弱项,不能直接按结果数组分组,因为记录中没有 regionId。但题目本身含有 bankId,题库模型又能关联地区,因此可以在结算时执行一条可复核链路:

AnswerRecord.questionId
  -> 试卷题目 Question
  -> Question.bankId
  -> DialectBank.regionId
  -> Region

若一场考试只属于一个 bankId,地区维度只有一个,所谓“地区薄弱项”没有比较意义。更合理的场景是综合卷:题目来自四川话、粤语、东北话等多个题库,再按真实 Question.bankId 聚合。

因此扩展前要先回答:产品是否真的有跨地区综合卷?当前 PracticePage 的考试模式从单个 bankId 取题,答案是否定的。此时结果页更适合展示“题型薄弱项”或“章节薄弱项”,而不是伪造多个地区。

二十二、一个可审计的薄弱项聚合模型

如果未来增加综合卷,可以扩展结算记录,而不是在结果页反复扫描全量题库:

interface AnswerSnapshot {
  questionId: string
  bankId: string
  regionId: string
  type: string
  selected: string
  correct: boolean
}

interface WeakGroup {
  key: string
  total: number
  wrong: number
  accuracy: number
}

聚合函数保持纯粹:

function aggregateByRegion(records: AnswerSnapshot[]): WeakGroup[] {
  const groups = new Map<string, WeakGroup>()
  for (const item of records) {
    const group = groups.get(item.regionId) ?? {
      key: item.regionId,
      total: 0,
      wrong: 0,
      accuracy: 0
    }
    group.total += 1
    if (!item.correct) group.wrong += 1
    group.accuracy = Math.round(
      (group.total - group.wrong) / group.total * 100
    )
    groups.set(item.regionId, group)
  }
  return Array.from(groups.values())
    .sort((a, b) => a.accuracy - b.accuracy)
}

这段是基于现有模型的增强示例,不是当前项目源码。它之所以可审计,是因为每个分组字段都来自答题当时的快照,不依赖后来可能变化的题库元数据。

二十三、薄弱项不能只看最低正确率

假设两个地区结果如下:

四川:1 题,0 正确,正确率 0%
粤语:20 题,12 正确,正确率 60%

只按正确率排序会把四川列为最薄弱,但样本只有一题,置信度很低。工程上至少要同时展示:

  • 答题总数;
  • 错题数;
  • 正确率;
  • 最小样本阈值。

例如只有 total >= 5 才进入“薄弱项”排序,其余显示为“样本不足”。这不是复杂统计模型,却能避免 UI 给用户过度确定的结论。

二十四、结果快照要考虑题库版本

目前历史记录只保存 bankId,逐题记录只保存 questionId。如果题库更新后删除问题或复用 ID,旧结果的错题解析会失去依据。面向长期维护,可增加:

interface ExamSnapshotMeta {
  sessionId: string
  bankId: string
  bankVersion: number
  startedAt: string
  finishedAt: string
}

对离线应用而言,不一定需要保存完整题干,但至少应保证题目 ID 稳定,并记录题库版本。若版本不兼容,历史页可以继续展示分数,却不再提供逐题解析入口。

二十五、测试清单:先测数据一致性,再测漂亮卡片

结果页的最小测试矩阵应包括:

场景 期望
20 题答对 15 题 正确率 75%,等级“良好”
0 题 正确率 0%,不出现 NaN
超时只答 8/20 后 12 格显示未答
records 非法 JSON 页面不崩溃,并显示数据异常状态
59/60/74/75/89/90 分 等级边界正确
点击错题解析 只加载本场错误题
点击再考一次 替换结果页并进入同一题库考试
手势导航模式 底部按钮不被遮挡
小窗与横屏 网格换行,内容可滚动
页面重复出现 不重复写入同一场历史

当前源码已经覆盖其中部分表现,但非法 JSON 提示、历史幂等和参数归一化仍需增强。测试报告必须区分“已有通过项”和“建议项”。

二十六、性能上避免在 build 中做重聚合

当前 makeAnswerGrid() 每次调用都会创建一个新数组,题量最多约 20 时开销很小。若未来综合卷达到数百题,并加入地区、题型、章节多维聚合,就不应在 ArkUI build() 重建全部统计。

可以在参数解析完成后生成一次 ResultViewModel

interface ResultViewModel {
  accuracy: number
  durationText: string
  rank: RankInfo
  grid: GridItem[]
  weakGroups: WeakGroup[]
}

状态只在输入快照变化时更新,UI 层负责渲染。这样既降低重复计算,也让统计逻辑更容易独立测试。

二十七、面向多设备的结果信息层级

在手机上,当前布局按“分数卡 -> 指标 -> 答题网格 -> 操作按钮”纵向展开,符合单列阅读。到了平板或 2in1,可以把分数卡和答题网格放入左右分栏,但不应改变数据语义:

紧凑宽度:单列,底部固定操作区
中等宽度:分数与指标一列,答题网格一列
展开宽度:结果概览、逐题状态、薄弱项三段并列或主从布局

不论使用何种布局,都要保留:

  • 分数和正确率的文本标签,不能只靠颜色;
  • 错误、正确、未答的图例;
  • 键盘与鼠标可达的操作按钮;
  • 系统返回能力;
  • 深浅色下足够的文字与背景对比度。

二十八、上线前的真实性检查

结果页很容易出现“视觉像真的,数据却不可复核”的问题。发布前应逐项检查:

  1. 分数、正确数、题量是否来自同一会话;
  2. 排名是否有真实数据源,没有就删除或改为本地指标;
  3. 地区薄弱项是否真的存在跨地区样本;
  4. 正确率是否处理零题和非法范围;
  5. 历史记录是否防重复;
  6. 逐题解析是否能关联原题;
  7. Preferences 中是否只保存必要的本地数据;
  8. 应用说明和隐私政策是否与“离线、本地统计”一致;
  9. 状态栏、底部导航区、横屏和小窗是否可用;
  10. 安装、启动、考试、结算、再考、返回和卸载流程是否稳定。

尤其不要把静态排名、模拟数字或设计占位内容写进平台宣传文案。

二十九、结语

中国方言题库的 ExamResultPage 代码并不长,但它连接了答题会话、等级映射、逐题状态、错题解析、考试历史和再次考试,是一个典型的结算边界。真实实现最值得复用的设计有三点:

第一,调用方传递完整快照,结果页不重新猜测考试过程;第二,以 total 生成网格,使超时未答也有明确位置;第三,通过 @StorageLinkUserDataManager 把本次考试写入本地历史。

同样重要的是承认当前边界:页面没有地区薄弱项计算,排名也是固定文本。若产品需要薄弱项,应先建立可追溯的题目、题库、地区映射,再设计聚合与样本阈值;若没有真实排名源,就使用本地可复核指标。技术文章的可信度,来自把已经存在、可以改进和尚未实现三者讲清楚。

---

AI 辅助声明:本文部分内容由 AI 辅助整理,所有功能描述、代码片段和工程结论均依据项目真实源码人工复核;未将建议方案表述为已实现能力。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐