【口算王|02】HarmonyOS ArkTS 答题提交实战:防止重复提交并推进下一题

【口算王|02】HarmonyOS ArkTS 答题提交实战:防止重复提交并推进下一题

答题页最容易被低估的不是布局,而是状态变化。用户一次点击会同时影响选中项、答案解析、答题记录、错题本、自动下一题、进度统计和页面路由。只要其中一个动作重复执行,就可能出现同一道题记录两次、学习进度翻倍、最后一题重复结算,或者自动推进与手动点击互相抢状态。

本文基于口算王项目 本地口算王工程(脱敏路径) 的真实源码,复核 PracticePage.etsUserDataManager.etsQuestionUtils.etsMathModels.ets。当前实现已经用 showAnalysis 阻止同一道题重复选项提交,并让错题记录按 questionId 去重;但整场练习完成仍缺少独立的一次性结算锁,自动下一题的延迟任务也没有保存句柄。文章会明确区分现有保护和仍需补强的竞态,不把改进方案写成已经实现。

**证据边界:**本文"当前实现"来自本轮对 PracticePage、UserDataManager、QuestionUtils 与 MathModels 的静态复核;任务令牌、显式状态机、可取消调度器、sessionId 和幂等提交均为建议设计。本轮没有执行构建、模拟器或真机快速点击测试,也没有把建议代码合入工程。

一、一次答题不是一次赋值

用户点击选项后,真正的业务链路是:

text 复制代码
读取当前题
拒绝非法模式和重复提交
保存选择
判断正误
写入 AnswerRecord
更新错题集合
展示解析
可选延迟推进
最后一题结算进度
考试模式跳转结果页

如果只在按钮上做视觉禁用,却没有在方法入口做状态守卫,快速连点、辅助功能事件或异步回调仍可能重复进入业务逻辑。口算王把第一道守卫放在 selectOption() 内部,这是正确方向。

二、当前页面有哪些关键状态

PracticePage 与答题提交直接相关的状态包括:

typescript 复制代码
@State questions: Question[] = []
@State currentIdx: number = 0
@State selectedKey: string = ''
@State showAnalysis: boolean = false
@State records: AnswerRecord[] = []
@State mode: string = 'chapter'
@State timerSec: number = 0
@StorageLink('autoNextQuestion')
autoNextQuestion: boolean = false

它们分别回答:

  • 当前练习有哪些题?
  • 当前位于哪一道?
  • 用户选了哪个答案?
  • 这道题是否已经提交并进入解析态?
  • 已形成哪些答题记录?
  • 当前是章节、随机、考试、错题还是单题模式?
  • 是否开启自动推进?

showAnalysis 在当前实现中同时承担"展示解析"和"本题已提交"两个语义。这能减少字段,但也会让显示状态与业务锁耦合。

三、提交入口先拒绝两种情况

真实代码开头是:

typescript 复制代码
private selectOption(key: string): void {
  if (this.mode === 'wrongAnalysis') return
  if (this.showAnalysis) return
  // ...
}

第一条防止"错题解析模式"被当成可作答模式。第二条防止用户在解析已经展示后再次选择其他答案。由于 showAnalysis = true 是同步赋值,连续点击同一选项或快速点击不同选项时,第一次进入后,后续调用会在方法入口返回。

这比单纯给选项设置灰色背景可靠,因为守卫位于业务方法内部。

四、为什么当前同题不会重复写 AnswerRecord

通过守卫后,代码立即执行:

typescript 复制代码
this.selectedKey = key
this.showAnalysis = true
const q = this.currentQ()

随后才构造记录:

typescript 复制代码
const correct = key === q.answer
this.records.push({
  questionId: q.id,
  selected: key,
  correct
})

关键顺序是"先锁定解析态,再写记录"。ArkTS 这一段没有 await,所以不会在设置 showAnalysispush() 之间让出执行权。对于同一页面实例中的同步连续点击,第二次调用会看到 showAnalysis === true

五、为什么不能只靠按钮颜色判断禁用

选项的背景和边框会依据 showAnalysis、正确答案和用户选择变化,但 .onClick() 始终存在:

typescript 复制代码
.onClick(() => {
  this.selectOption(opt.key)
})

也就是说,UI 没有通过 .enabled(false) 移除交互,真正防重依赖 selectOption() 守卫。这种设计仍然有效,但测试不能只观察颜色;必须检查 records.length 在重复点击后是否只增加 1。

建议把视觉状态与业务状态同时验证:

text 复制代码
第一次点击 -> 展示解析,records +1
第二次点击 -> records 不变,selectedKey 不变
点击其他选项 -> records 不变,正确答案展示不变

六、错题写入为什么具备集合幂等性

答错后调用:

typescript 复制代码
this.wrongRecords =
  UserDataManager.addWrong(
    this.wrongRecords,
    q.id,
    q.bankId
  )

addWrong() 不是直接追加,而是先移除同题旧记录:

typescript 复制代码
const filtered =
  records.filter(r => r.questionId !== questionId)
const result: WrongRecord[] = [
  { questionId, bankId, wrongAt: nowStr() },
  ...filtered
]

因此同一个 questionId 在错题集合中最多保留一条。即使未来某个入口意外重复调用,集合也不会无限堆积同一道题,只会刷新错误时间并移动到列表前面。

这是一种数据层幂等保护,与页面层 showAnalysis 守卫形成两道防线。

七、错题模式答对后的删除同样幂等

mode === 'wrong' 且答对时:

typescript 复制代码
this.wrongRecords =
  UserDataManager.removeWrong(
    this.wrongRecords,
    q.id
  )

实现只是过滤:

typescript 复制代码
const result =
  records.filter(r => r.questionId !== questionId)

目标不存在时结果不变,重复删除不会报错。这样从错题练习中答对同一道题,无论删除被触发一次还是两次,最终集合状态一致。

八、自动下一题的 800ms 是什么语义

提交成功后,如果用户开启自动推进:

typescript 复制代码
if (this.autoNextQuestion) {
  setTimeout(() => {
    if (this.showAnalysis) {
      this.goNext()
    }
  }, 800)
}

800ms 给用户短暂查看正误与解析,然后调用 goNext()。回调再次检查 showAnalysis,可以避免用户已经手动进入下一道后重复推进,因为手动 goNext() 会把新题的 showAnalysis 重置为 false

这个判断解决了最常见的"自动推进与手动下一题同时发生"场景,但它不是完整的任务取消机制。

九、手动下一题为何通常不会连跳

底部按钮执行:

typescript 复制代码
if (this.showAnalysis ||
    this.mode === 'wrongAnalysis') {
  this.goNext()
}

普通模式下,第一下点击进入 goNext()

typescript 复制代码
this.currentIdx++
this.selectedKey = ''
this.showAnalysis = false

第二下快速点击时,按钮回调仍可能被触发,但条件发现 showAnalysis === false,不会再次调用 goNext()。因此普通中间题的快速连点通常只推进一题。

十、答题卡跳转让延迟回调变得更复杂

答题卡允许用户直接修改 currentIdx,并根据已有记录恢复:

typescript 复制代码
const record = this.records.find(
  r => r.questionId === this.questions[idx].id
)
if (record) {
  this.selectedKey = record.selected
  this.showAnalysis = true
} else {
  this.selectedKey = ''
  this.showAnalysis = false
}

假设用户答完当前题后开启了 800ms 自动推进,在定时器触发前打开答题卡并跳到另一道已答题,showAnalysis 仍为 true。旧定时器无法识别页面已切换,会从新的 currentIdx 再推进一次。

这说明用一个布尔值不足以证明"回调仍属于原题"。延迟任务至少要捕获提交时的 questionId 或序号,并在执行前比较。

十一、最后一题存在怎样的重复结算风险

最后一题调用 goNext() 时会:

text 复制代码
统计 correctCount
updateProgress()
可选 updateChapterProgress()
考试模式 replaceUrl()
普通模式 router.back()

updateProgress() 使用累加:

typescript 复制代码
finished: old.finished + addFinished
correct: old.correct + addCorrect

它不是基于 sessionId 的幂等写入。如果最后一题结算路径重复执行,同一场练习的完成数和正确数会再次累加。

当前 goNext() 没有 isFinalizingsessionFinalized 守卫,自动下一题的定时器也没有保存 ID 并在页面消失时取消。因此不能声称最后一题已经具备严格的一次性结算保证。

十二、页面消失时只清理了哪些资源

aboutToDisappear() 会:

typescript 复制代码
if (this.timerId !== -1) {
  clearInterval(this.timerId)
}

还会停止并关闭 TTS 引擎。但是自动下一题使用的 setTimeout() 没有句柄字段,因此页面离开时无法显式 clearTimeout()

回调触发后是否还能造成状态或路由副作用,要依赖组件生命周期和运行时行为。工程上更稳妥的做法是保存延迟任务 ID,并在推进、跳题、结算和页面消失时统一取消。

十三、考试倒计时和手动交卷可能竞争

页面每秒更新 timerSec

typescript 复制代码
if (this.mode === 'exam' &&
    this.examRemainSec() <= 0) {
  this.autoSubmitExam()
}

倒计时归零时,autoSubmitExam() 会停止 interval、更新进度并 replaceUrl() 到结果页。与此同时,用户可能正点击最后一题的"交卷"按钮,后者也会进入 goNext() 的考试结算分支。

两个分支都能更新进度和路由,但没有共享的 finalized 守卫。这是典型的"两个入口提交同一事务"。停止 interval 只能防止下一次时钟触发,不能让已经进入的另一个调用自动退出。

十四、提交状态应该用枚举表达

建议把答题状态从多个布尔值升级为明确状态机:

typescript 复制代码
enum QuestionPhase {
  Answering,
  Reviewing,
  Advancing
}

enum SessionPhase {
  Active,
  Finalizing,
  Finalized
}

规则变得清晰:

text 复制代码
只有 Answering 可以提交选项
提交成功后进入 Reviewing
推进时进入 Advancing
到达下一题后回到 Answering
只有 Active 可以开始整场结算
Finalizing 和 Finalized 拒绝所有结算入口

这是基于源码风险提出的改进模型,不是当前项目已经存在的枚举。

十五、用题目令牌约束延迟推进

自动推进任务可以捕获当前题 ID:

typescript 复制代码
const submittedQuestionId = q.id
this.autoNextTimerId = setTimeout(() => {
  const current = this.currentQ()
  if (this.sessionPhase !== SessionPhase.Active) return
  if (this.questionPhase !== QuestionPhase.Reviewing) return
  if (!current || current.id !== submittedQuestionId) return
  this.goNext()
}, 800)

如果用户打开答题卡跳走、页面路由离开、考试已经结算或当前题发生变化,旧回调都会失效。这比仅检查 showAnalysis 更接近"任务属于哪个状态"的真实问题。

十六、整场结算应集中到一个出口

当前 autoSubmitExam()goNext() 都包含更新进度、计算分数和路由逻辑。重复实现会增加竞态和字段不一致风险。

可以收敛为:

typescript 复制代码
private finalizeSession(reason: FinishReason): void {
  if (this.sessionPhase !== SessionPhase.Active) return
  this.sessionPhase = SessionPhase.Finalizing
  this.cancelAutoNext()
  this.stopExamTimer()

  const summary = this.buildSummary(reason)
  this.persistSummaryOnce(summary)
  this.sessionPhase = SessionPhase.Finalized
  this.openResult(summary)
}

手动完成、自动下一题到末题、倒计时结束和交卷按钮都只调用这一方法。一次性守卫位于最靠近持久化的位置。

十七、为何进度更新需要 sessionId

仅靠内存中的 sessionPhase 可以防止当前页面实例重复结算,但无法覆盖应用异常退出后重试、结果页返回、跨页面重复回调等情况。

更强的幂等模型是给每场练习生成 sessionId

typescript 复制代码
interface PracticeSessionSummary {
  sessionId: string
  bankId: string
  chapterId: string
  total: number
  correct: number
  durationSec: number
  finishedAt: string
}

持久化层先检查该 sessionId 是否已结算,再决定是否累加进度。当前 BankProgress 只有累计值,没有 session 明细,所以这一能力尚未实现。

十八、records.push 的可变更新有什么影响

records@State 数组,当前代码使用:

typescript 复制代码
this.records.push(record)

业务上它确实增加了记录,但对于声明式 UI,使用不可变赋值更容易让状态变化明确:

typescript 复制代码
this.records = [...this.records, record]

更重要的是,追加前可以按题目 ID 防御性去重:

typescript 复制代码
if (this.records.some(
  item => item.questionId === q.id
)) return

页面守卫已经能挡住普通重复点击,数据级去重则能覆盖答题卡回跳、状态恢复或未来异步改造带来的重复入口。

十九、错题更新与进度更新的幂等性不同

口算王的两个数据动作容易被误认为一样:

动作 实现 重复调用结果
addWrong 先过滤同 ID 再插入 集合不重复
removeWrong 按 ID 过滤 状态不变
updateProgress 在旧值上累加 会重复增长
updateChapterProgress 在旧值上累加 会重复增长

因此防重重点不是错题集合,而是整场完成后的累加式统计。幂等性必须按副作用分别判断,不能因为一个方法安全就推导整个提交链安全。

二十、测试矩阵要覆盖交互顺序

最小回归矩阵包括:

text 复制代码
同一选项连续点击两次
不同选项快速各点一次
答题后立即手动下一题
答题后等待自动下一题
答题后在 800ms 内打开答题卡跳到未答题
答题后在 800ms 内跳到已答题
最后一题答完后立即点完成
最后一题开启自动推进并手动完成
考试倒计时归零时点击交卷
页面退出后等待旧延迟任务时间到达
错误题重复加入错题本
错题模式答对后重复删除

断言不仅看页面:

text 复制代码
每个 questionId 最多一条 AnswerRecord
currentIdx 最多推进一次
错题集合无重复 questionId
finished 和 correct 只累加一次
结果页只打开一次
离开页面后没有旧任务继续导航

二十一、当前实现可以确认什么

从真实源码可以确认:

  • 错题解析模式拒绝答题。
  • showAnalysis 同步阻止同题重复选择。
  • 一次有效选择只追加一条 AnswerRecord
  • 错题添加和删除按 questionId 具备集合幂等性。
  • 自动下一题延迟 800ms,并再次检查 showAnalysis
  • 手动推进后会重置选择和解析态,普通快速连点不会连续跳题。
  • 页面消失会停止考试 interval 和 TTS。

不能确认:

  • 自动下一题延迟任务会在页面离开时被取消。
  • 答题卡跳转后旧延迟任务一定不会推进新位置。
  • 最后一题结算已有独立 finalized 锁。
  • 倒计时提交与手动交卷不会同时更新进度。
  • 进度持久化按 sessionId 去重。
  • 上述竞态已经通过自动化或真机测试。

二十二、最小改造顺序

按风险和改动量排序:

text 复制代码
第一步:保存 autoNextTimerId,并统一 cancelAutoNext()
第二步:延迟回调捕获并比较 questionId
第三步:增加 SessionPhase,一次性锁住结算
第四步:合并 autoSubmitExam 与末题完成出口
第五步:AnswerRecord 按 questionId 防御性去重
第六步:为持久化结算引入 sessionId
第七步:补齐快速点击、答题卡和倒计时竞态测试

答题提交的可靠性不取决于按钮动画,而取决于每个副作用是否有唯一入口、明确状态和幂等边界。口算王已经解决了同题同步重复选择和错题集合去重,下一步应把保护范围从"这一道题"扩展到"这一整场练习"。只有自动推进、手动推进、倒计时和结果路由共享同一个一次性结算出口,学习进度才不会因为一次快速点击变成两次完成。

二十三、先定义一次提交的身份

防重复不能只回答"按钮现在是否可点",还要回答"这次回调属于哪道题、哪一场练习、哪一轮页面状态"。当前同步点击由 showAnalysis 挡住,但 800ms 后触发的任务只看到执行时的布尔值,无法知道创建任务时的题目是否仍是当前题。

身份字段 解决的问题 失配时动作
sessionId 区分两场练习 拒绝旧场次副作用
questionId 确认任务属于原题 取消推进
questionRevision 识别同题状态已重置 丢弃旧回调
sessionPhase 确认整场仍可提交 拒绝记录与结算

这些字段不是为了让模型复杂,而是让每个异步动作携带可比较的因果关系。没有身份的延迟回调,即使时间只有 800ms,也可能在答题卡切换、路由离开或考试归零后变成过期任务。

二十四、把 timeout 变成可取消的单一资源

当前 interval 有 timerId 字段,自动推进 timeout 却没有句柄。建议为延迟推进建立与页面生命周期一致的管理方法,并在创建新任务前先取消旧任务。

arkts 复制代码
private autoNextTimerId: number = -1

private cancelAutoNext(): void {
  if (this.autoNextTimerId === -1) return
  clearTimeout(this.autoNextTimerId)
  this.autoNextTimerId = -1
}

private scheduleAutoNext(questionId: string): void {
  this.cancelAutoNext()
  this.autoNextTimerId = setTimeout(() => {
    this.autoNextTimerId = -1
    const current = this.currentQ()
    if (!current || current.id !== questionId) return
    if (this.questionPhase !== QuestionPhase.Reviewing) return
    if (this.sessionPhase !== SessionPhase.Active) return
    this.goNext()
  }, 800)
}

aboutToDisappear()、答题卡跳题、手动下一题和整场结算都调用 cancelAutoNext()。这样"旧任务不会继续导航"由明确代码保证,不再依赖组件销毁后的运行时细节。

二十五、选项提交先做纯校验,再执行副作用

当前 selectOption() 的同步顺序已经能挡住常见连点。进一步收口时,可以把"能否提交"和"提交后写哪些数据"分开,使状态转换、答题记录与错题更新更容易分别验证。

arkts 复制代码
interface SubmitDecision {
  accepted: boolean
  reason: string
}

private canSubmit(questionId: string): SubmitDecision {
  if (this.mode === 'wrongAnalysis') {
    return { accepted: false, reason: 'readonly' }
  }
  if (this.sessionPhase !== SessionPhase.Active) {
    return { accepted: false, reason: 'session_closed' }
  }
  if (this.questionPhase !== QuestionPhase.Answering) {
    return { accepted: false, reason: 'question_closed' }
  }
  if (this.records.some(item => item.questionId === questionId)) {
    return { accepted: false, reason: 'duplicate' }
  }
  return { accepted: true, reason: '' }
}

方法入口先取得当前题并校验,再把阶段同步切到 Reviewing,随后构造不可变 records、更新错题集合、展示解析,最后才安排自动推进。即使未来加入异步存储,业务锁也应在第一个 await 之前完成。

二十六、状态机要约束按钮,也要约束业务方法

视觉上禁用按钮能减少误触,却不能替代方法守卫。更完整的对应关系如下:

QuestionPhase 选项 下一题 答题卡
Answering 可提交一次 禁用 可切换
Reviewing 不可再提交 可推进 切换前取消旧任务
Advancing 禁用 禁用 暂时禁用

UI 根据阶段设置颜色、可用态和无障碍提示,方法内部再次检查阶段。这样鼠标、触控、键盘、辅助功能和延迟回调都会经过同一业务规则,测试也不必用"按钮变灰"来间接推断记录没有重复。

二十七、把整场结算写成可重入但只生效一次

最后一题、倒计时归零和显式交卷都属于同一事务的入口。入口可以被调用多次,但只有第一次从 Active 切换到 Finalizing 的调用能够产生持久化与路由副作用。

arkts 复制代码
private finalizeSession(reason: FinishReason): void {
  if (this.sessionPhase !== SessionPhase.Active) return
  this.sessionPhase = SessionPhase.Finalizing
  this.cancelAutoNext()
  this.stopExamTimer()

  const records = this.records.slice()
  const summary = this.buildSummary(this.sessionId, records, reason)
  const saved = this.sessionService.saveOnce(summary)
  if (!saved.success) {
    this.sessionPhase = SessionPhase.Active
    this.showSaveError(saved.message)
    return
  }
  this.sessionPhase = SessionPhase.Finalized
  this.openResult(summary)
}

建议代码把写入结果显式带回页面,是因为当前 UserDataManager.persist() 会捕获异常而不通知调用方。若保存失败仍直接跳转,用户看到完成页却可能没有真实进度;恢复到 Active 时还要保持题目和答案,允许安全重试。

二十八、累计进度之前先记录结算凭证

updateProgress()updateChapterProgress() 都在旧值上增加 finished 和 correct。它们适合处理一次明确提交,却不具备重复调用后的同结果特性。只把 sessionPhase 放在页面内,仍挡不住进程重建后的重试。

arkts 复制代码
interface FinalizedSession {
  sessionId: string
  bankId: string
  chapterId: string
  questionIds: string[]
  answeredCount: number
  correctCount: number
  finishedAt: string
}

interface SaveOnceResult {
  success: boolean
  inserted: boolean
  message: string
}

持久化层先按 sessionId 查询:不存在才写入结算凭证并更新累计值,已存在则返回 inserted=false,页面可以继续打开同一结果而不重复计数。若使用 Preferences,需要设计有界的已结算 ID 集合;数据量和查询需求增长后,应迁移到带唯一键的结构化存储。

二十九、测试要控制调度器,而不是等待 800ms

真实等待会让竞态用例变慢且偶发。可注入一个最小 Scheduler,在测试中保存任务并手动触发,再精确排列"提交、跳题、到时、离页"的顺序。

场景 事件顺序 关键断言
同题连点 submit A、submit B records 只增加一条
跳到已答题 submit、sheetJump、runTimeout currentIdx 不被旧任务改变
手动抢先 submit、manualNext、runTimeout 只推进一题
末题竞态 timeout、manualFinish 累计值和路由各执行一次
页面离开 submit、disappear、runTimeout 没有记录、推进或导航副作用
保存失败 finalize、persistFail 保留答案并提供重试

真机仍需覆盖快速触控、键盘确认、系统返回、前后台切换和最后一秒交卷。本轮没有执行这些操作,因此文章给出的是测试设计,不是运行通过结论。

三十、按最小风险分三步落地

第一步只增加 autoNextTimerId、取消方法和 questionId 校验,不改变数据结构,就能封住最直接的旧回调问题。第二步引入 QuestionPhase 与 SessionPhase,并把所有完成入口收敛到 finalizeSession()。第三步才增加 sessionId、保存结果反馈和持久化幂等凭证。

每一步都应保持现有模式行为:错题解析仍只读,错题练习答对仍移除记录,普通练习完成仍返回,考试仍进入结果页。分层改造比一次重写页面更容易定位回归,也能让现有 showAnalysis 在过渡期继续承担显示职责。

每完成一步都要复跑同一组基线:普通题首次提交、重复点击、手动推进、自动推进、答题卡切换、末题完成和考试超时。只有新增守卫生效且原有导航、错题更新与解析展示保持一致,才进入下一步;如果出现回归,应先缩小到当前阶段的状态转换,不要同时改动 UI、存储结构和路由参数。

三十一、总结:同步守卫保护一题,幂等事务保护整场

口算王当前已经用同步 showAnalysis 守卫解决了常见的同题重复点击,并让错题添加与删除按 questionId 保持集合一致。真正需要继续补强的,是无句柄自动推进、答题卡切换后的过期任务,以及末题与倒计时共享累加式进度时的一次性结算。

可靠方案的核心是给任务和事务都赋予身份:questionId 约束延迟推进,sessionId 约束持久化,QuestionPhase 管理单题状态,SessionPhase 管理整场结束。再配合统一取消、显式保存结果与可控调度测试,页面才能在快速点击和复杂生命周期中仍然只记录一次、只推进一步、只完成一场。


**AI 辅助声明:**本文在人工核对真实工程源码、答题守卫、800ms 自动推进、答题卡跳转和进度写入路径后,使用 AI 辅助整理结构、润色表达并生成示意图;当前实现、建议扩展与未执行验证均已分别说明。

CSDN-SERIES:ALL-163186345

相关推荐
承渊政道3 小时前
Python IDLE鸿蒙PC适配全记录:从Tkinter桌面程序到ArkUI原生开发闭环
开发语言·python·harmonyos·鸿蒙系统·桌面程序
万物智能信息科技3 小时前
电容触摸 FT5406,另一颗芯片、同一条总线—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
人工智能·华为·开源·harmonyos·鸿蒙
ChinaDragonDreamer3 小时前
HarmonyOS:应用程序包管理模块(获取当前应用信息)
harmonyos·鸿蒙
●VON4 小时前
Flutter 鸿蒙插件适配实战:用 boot_time_plugin 1.0.0 读取启动时间与运行时长
flutter·华为·harmonyos
颜颜yan_4 小时前
Git Cola 鸿蒙 PC 版适配全记录:从 Qt 桌面工具到 ArkTS 原生应用
git·qt·harmonyos
承渊政道5 小时前
Python IDLE鸿蒙PC适配全记录:用 ArkUI 重建编辑、运行、Shell 与基础调试闭环
python·microsoft·harmonyos·鸿蒙系统·pc端
User_芊芊君子6 小时前
RapidSVN 鸿蒙 PC 适配全记录:用 ArkUI 重建工作台,打通本地 SVN 操作闭环
华为·svn·harmonyos
lqj_本人6 小时前
WinMerge 鸿蒙 PC 适配全记录:以 Qt 重建桌面外壳,打通文件与文件夹差异闭环
qt·华为·harmonyos
不羁的木木6 小时前
给鸿蒙 App 增加唤起系统邮件发送能力 —— flutter_email_sender 的鸿蒙使用指南
flutter·华为·harmonyos