news 2026/10/27 9:59:17

【中国方言题库|10】HarmonyOS ArkTS 语音播放实战:管理读音播放与页面生命周期

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【中国方言题库|10】HarmonyOS ArkTS 语音播放实战:管理读音播放与页面生命周期

【中国方言题库|10】HarmonyOS ArkTS 语音播放实战:管理读音播放与页面生命周期

证据边界:本轮只复核 PracticePage、题目模型、题库数据与模块配置的静态源码,没有执行构建、设备播放、断网回退或引擎故障测试。当前实现把 audioHint 文本交给 zh-CN TTS 合成,不是预置音频文件或真人方言录音;requestId 虽已生成,但回调状态没有按请求标识过滤,快速重复点击仍需运行验证。

摘要:中国方言题库的听音能力并不是播放预置方言音频文件,而是在PracticePage中调用@hms.ai.textToSpeech,把题目的audioHint文本交给系统语音引擎合成。实现采用本地引擎优先、在线引擎回退的策略,用监听器同步开始、完成、停止和错误状态;点击播放时先打开文字兜底弹窗,再尽力启动 TTS;页面离开时停止并关闭引擎。本文面向 HarmonyOS 5.0 及以上版本,基于真实 ArkTS 源码复核引擎创建、重复点击、请求标识、状态恢复、底部弹窗和生命周期释放,并明确zh-CN合成不能等同于真实方言录音。

一、先说清楚:这是 TTS 合成,不是真实方言音频

项目的Question模型定义:

export interface Question { id: string bankId: string chapterId: string type: string stem: string audioHint?: string options: Option[] answer: string analysis: string example?: string }

audioHint的注释是“听音题占位描述”。题目数据中存放的是:

{ type: 'audio', stem: '听音选择:这段四川话表达的意思是?', audioHint: '莫挨老子', options: [ '请离我远点', '请帮我搬东西', '我饿了', '走慢点' ], answer: 0, analysis: '“莫挨老子”表示“别靠近我”,语气较强。' }

源码没有.mp3、.wav资源地址,也没有媒体播放器。它使用language: 'zh-CN'的文本转语音引擎朗读短语。因此可以描述为“系统语音合成示例”,不能宣传成“真人方言发音”“标准母语者录音”或“原生方言音色”。

唯一校验标记:CSDN-SERIES:ALL-163195219。

二、真实调用链位于 PracticePage

语音能力的真实核心文件是:

entry/src/main/ets/pages/PracticePage.ets entry/src/main/ets/mock/MockBanks.ets librarya/src/main/ets/models/Dialect.ets entry/src/main/module.json5

调用链为:

音频题卡片点击 -> toggleAudioPreview(question) -> 立即打开文字弹窗 -> ensureTtsEngine() -> 本地引擎或在线引擎 -> speak(audioText) -> listener 更新状态 -> close / 页面离开时 stop -> 页面离开时 shutdown

页面详情模块本身不负责播放,播放能力跟随答题会话存在。这样语音状态、当前题目和答题弹窗属于同一个组件生命周期。

三、为什么引擎不是 @State

页面声明:

private ttsEngine?: textToSpeech.TextToSpeechEngine = undefined

引擎对象是平台资源句柄,不是直接渲染到 UI 的业务数据,因此不需要@State。真正驱动界面的状态是:

@State activeAudioQuestionId: string = '' @State audioStatusText: string = '' @State showAudioDialog: boolean = false @State audioDialogText: string = '' @State audioDialogStem: string = ''

这形成清晰分工:

类型字段作用
平台资源ttsEngine创建、朗读、停止、关闭
活动目标activeAudioQuestionId标记当前播放题目
用户反馈audioStatusText显示播放或错误状态
弹窗状态showAudioDialog决定兜底弹窗是否可见
弹窗内容audioDialogText显示可参考的发音文本

平台对象和声明式状态分开,页面重建时不会把引擎当作普通可观察数据反复替换。

四、文本来源优先 audioHint

页面准备朗读文本:

private audioText(question: Question): string { return ( question.audioHint || question.stem ).replace(/["“”]/g, '').trim() }

优先使用audioHint,缺失时回退到题干。随后去除直引号和中文引号,并清理首尾空格。

例如:

audioHint = "“莫挨老子”" output = "莫挨老子"

这样可以避免 TTS 把引号或题目提示一并朗读。由于 UI 只有在type === 'audio' && audioHint时显示播放区,正常路径几乎总会使用audioHint;题干回退主要是方法级防御。

五、本地引擎优先,在线引擎回退

ensureTtsEngine()先检查是否已有引擎:

if (this.ttsEngine) return true

随后尝试本地模式:

this.ttsEngine = await textToSpeech.createEngine({ language: 'zh-CN', person: 0, online: 0 })

失败后再尝试在线模式:

this.ttsEngine = await textToSpeech.createEngine({ language: 'zh-CN', person: 0, online: 1 })

两次都失败时:

this.audioStatusText = '当前设备语音引擎不可用' return false

这个顺序能优先利用设备本地语音服务;在线模式是降级补充。项目的module.json5真实声明了:

"requestPermissions": [ { "name": "ohos.permission.INTERNET" } ]

因此不能把整个语音链路描述成“纯离线”。即使本地引擎通常可用,代码仍明确包含在线回退。

六、引擎复用避免每次点击重复创建

这句短路:

if (this.ttsEngine) return true

意味着首次点击完成创建后,后续题目复用同一个引擎。创建平台能力通常比调用speak()更重,复用能降低延迟和资源波动。

但复用有一个前提:引擎一旦进入不可恢复错误,当前代码仍保留对象,下一次ensureTtsEngine()会直接返回true。如果平台错误需要重建,onError中应考虑关闭并置空:

try { this.ttsEngine?.shutdown() } catch (_) { } this.ttsEngine = undefined

当前源码的错误监听只更新 UI,没有自动重建。这是可改进项,不是现成功能。

七、监听器把平台回调映射为页面状态

引擎创建成功后设置监听器:

this.ttsEngine.setListener({ onStart: (requestId: string) => { this.audioStatusText = '正在播放示例' }, onComplete: (requestId: string) => { this.activeAudioQuestionId = '' this.audioStatusText = '' }, onStop: (requestId: string) => { this.activeAudioQuestionId = '' this.audioStatusText = '' }, onError: ( requestId: string, errorCode: number, errorMessage: string ) => { this.activeAudioQuestionId = '' this.audioStatusText = '语音播放失败,请检查系统语音服务' } })

四个回调覆盖了开始、自然完成、主动停止和失败。UI 不需要主动轮询引擎,而是由回调收敛状态。

八、requestId 已生成,但回调没有校验

播放请求使用:

requestId: `${question.id}_${Date.now()}`

这个 ID 同时包含题目和时间,能区分连续请求。但监听器接收requestId后没有判断它是否仍是最新请求。

可能的竞态是:

  1. 用户播放 A;
  2. 很快切换并播放 B;
  3. A 的停止回调晚到;
  4. 回调清空 B 的活动状态。

当前页面在新播放前调用stop(),大多数情况下足够,但严格状态机可以保存:

private activeAudioRequestId: string = ''

回调只处理匹配的请求:

if (requestId !== this.activeAudioRequestId) return

这能防止旧请求改变新请求的 UI。现有源码尚未做该校验。

九、点击后先打开弹窗,再等待引擎

toggleAudioPreview()的顺序是:

this.activeAudioQuestionId = question.id this.audioStatusText = '正在播放示例' this.audioDialogText = this.audioText(question) this.audioDialogStem = question.stem this.showAudioDialog = true

完成 UI 状态后,才异步执行:

const ready = await this.ensureTtsEngine()

这个顺序非常关键。用户点击后立刻得到可见反馈,不会因为引擎创建耗时而误以为按钮失效。即使引擎不可用,弹窗仍显示发音文字,用户仍可继续完成题目。

十、播放前先停止旧内容

引擎准备完成后:

if (this.ttsEngine.isBusy()) { this.ttsEngine.stop() }

再发起新请求:

this.ttsEngine.speak( this.audioText(question), { requestId: `${question.id}_${Date.now()}` } )

这避免两段语音同时播放或排队叠加。对于用户连续点不同听音题,最新意图应优先。

需要注意,方法名叫toggleAudioPreview,但行为不是“点击同一项切换播放/暂停”:无论当前是否播放,都会打开弹窗并重新请求。更准确的命名是openAudioPreview()或playAudioPreview()。

十一、外层 try/catch 为什么保证弹窗可用

语音尝试被包在:

try { const ready = await this.ensureTtsEngine() if (ready && this.ttsEngine) { // stop + speak } } catch (_) { }

即使isBusy()、stop()或speak()抛出异常,前面设置的showAudioDialog=true仍然有效。注释也明确写着:

后台尽力尝试 TTS,失败也不影响弹窗使用

这体现了能力降级原则:平台语音是增强能力,文字弹窗是任务可完成的基础路径。

不过空catch会让audioStatusText可能长期停留在“正在播放示例”。更好的做法是在异常分支显式写入失败状态。

十二、为什么文字兜底对听音题很重要

弹窗展示:

Text(this.audioDialogText)

并提示:

部分设备未开启语音服务时, 可参考上方文字辨识发音并选出正确答案。

这确保设备没有语音服务时,用户不会卡死在无法作答的题目。对于教育题库,功能完整性比强制依赖某个引擎更重要。

但文字提示会降低纯听力测验的难度。产品应明确它是“学习辅助模式”还是“严格听力考试”。当前实现选择了可完成性优先,不能宣传为封闭式听力测评。

十三、zh-CN 合成不能承诺方言音准

创建参数固定为:

language: 'zh-CN' person: 0

系统会按可用的中文普通话语音能力合成。“唔该晒”“阿拉上海宁”“涯爱转屋下”等文本可能被按普通话字音朗读,不能保证声调、变调、声母韵母和地域口音符合对应方言。

若产品需要可评测的方言发音,应使用:

  • 获得授权的真人录音;
  • 明确支持目标方言的语音模型;
  • 音频资源版本与题目版本绑定;
  • 播放失败时保留文本兜底;
  • 在隐私和版权材料中说明音频来源。

当前源码没有这些录音资源,本文不能伪造。

十四、活动题目标记驱动卡片视觉

播放卡片颜色根据:

this.activeAudioQuestionId === this.currentQ()!.id

活动时,图标与波形使用主色;非活动时使用提示色。文案也会切换:

active ? `${audioHint} · ${audioStatusText || '正在播放示例'}` : `${audioHint} · 点击播放`

activeAudioQuestionId不保存布尔值,而是保存题目 ID,这比isPlaying更能说明“哪一道题正在播放”。当页面允许多个可播放项时,这种状态更可靠。

十五、波形只是静态视觉,不是音频分析

播放卡片和弹窗中的波形来自固定高度数组:

ForEach( [3, 6, 10, 8, 5, 12, 7, 4, 9, 6, 3, 8, 11, 5, 7], (h: number) => { Column() .width(3) .height(h) } )

它没有读取音频振幅、播放进度或实时频谱。因此应描述为“波形样式装饰”或“播放状态图形”,不能叫实时声波可视化。

十六、关闭弹窗时停止当前语音

关闭逻辑:

private closeAudioDialog(): void { if (this.ttsEngine) { try { this.ttsEngine.stop() } catch (_) { } } this.showAudioDialog = false this.activeAudioQuestionId = '' this.audioStatusText = '' }

用户点击关闭按钮或遮罩,都会调用该方法。先尝试停止语音,再收起弹窗并清状态,避免弹窗消失后声音继续播放。

如果停止失败,UI 仍会关闭。此时平台音频是否继续取决于引擎状态,所以页面离开时还有第二层shutdown()保障。

十七、“再听一次”复用现有引擎

弹窗按钮逻辑:

if (this.ttsEngine) { try { if (this.ttsEngine.isBusy()) { this.ttsEngine.stop() } this.ttsEngine.speak( this.audioDialogText, { requestId: `replay_${Date.now()}` } ) } catch (_) { } }

只有引擎已经创建成功时才执行。若首次创建失败,按钮不会再次调用ensureTtsEngine(),因此无法自动重试恢复。

更稳妥的实现是让“再听一次”调用统一异步方法:

private async replayAudio(): Promise<void> { const ready = await this.ensureTtsEngine() if (!ready || !this.ttsEngine) return if (this.ttsEngine.isBusy()) { this.ttsEngine.stop() } this.ttsEngine.speak( this.audioDialogText, { requestId: `replay_${Date.now()}` } ) }

这是建议方案,当前源码尚未采用。

十八、切换下一题时重置播放标记

goNext()在普通模式中执行:

this.selectedKey = '' this.showAnalysis = false this.activeAudioQuestionId = '' this.audioStatusText = ''

在错题解析模式中,applyAnalysisState()也会:

this.activeAudioQuestionId = ''

这能避免下一题错误继承上一题的活动颜色。但这里没有主动stop();如果用户在语音仍播放时通过其他交互切题,声音可能继续到自然完成。

严格处理可以在题目切换前统一调用stopAudio()。当前主要关闭路径依赖弹窗关闭,实际交互中用户通常先关闭弹窗。

十九、页面离开时必须 stop 和 shutdown

生命周期释放是:

aboutToDisappear(): void { if (this.timerId !== -1) { clearInterval(this.timerId) } if (this.ttsEngine) { try { this.ttsEngine.stop() this.ttsEngine.shutdown() } catch (_) { } this.ttsEngine = undefined } }

stop()终止当前朗读,shutdown()释放引擎资源,最后置空引用。这样返回上一页、跳到结果页或离开答题页后,不会继续占用语音引擎。

唯一需要注意的是stop()和shutdown()放在同一个try中:如果stop()抛出异常,shutdown()可能不会执行。更稳妥的是分两个保护块,或者使用finally确保关闭。

二十、生命周期资源和 UI 状态应统一收口

当前释放引擎后,没有显式重置:

showAudioDialog activeAudioQuestionId audioStatusText

页面已经离开时通常不会继续渲染,因此问题不大。但如果组件实例因导航缓存后再次出现,旧弹窗状态可能需要重新确认。

可以提供统一方法:

private releaseAudio(): void { try { this.ttsEngine?.stop() } catch (_) { } try { this.ttsEngine?.shutdown() } catch (_) { } this.ttsEngine = undefined this.showAudioDialog = false this.activeAudioQuestionId = '' this.audioStatusText = '' }

关闭弹窗只停止,页面离开则完整释放,职责会更清晰。

二十一、在线回退带来的权限与隐私边界

项目真实声明ohos.permission.INTERNET,语音创建也设置过online: 1。因此发布材料需要确认:

  1. 在线语音服务是否实际发送待合成文本;
  2. 服务由系统还是应用后端提供;
  3. 隐私政策是否覆盖联网语音处理;
  4. 无网络时是否仍可完成核心流程;
  5. 应用是否仍适合描述为“主要本地运行”;
  6. 语音文本是否含个人信息。

本项目的题目短语来自内置题库,不包含用户输入,风险相对可控,但不能因为文本固定就忽略网络行为说明。

二十二、错误反馈目前有两套语义

引擎创建失败:

当前设备语音引擎不可用

播放回调失败:

语音播放失败,请检查系统语音服务

这两条能区分“没有可用引擎”和“引擎存在但播放失败”。但toggleAudioPreview()外层异常被吞掉,可能显示错误的“正在播放示例”。

建议把语音状态收窄为联合类型:

type AudioState = | 'idle' | 'preparing' | 'playing' | 'unavailable' | 'failed'

显示文案由纯函数映射,避免在多个回调中直接拼接字符串。

二十三、重复点击与并发创建风险

首次点击时ensureTtsEngine()是异步的。若用户在引擎创建完成前快速点击两次,两次调用都可能看到:

this.ttsEngine === undefined

然后并发创建两个引擎。后完成的一个覆盖字段,另一个对象可能失去释放路径。

可以缓存创建中的 Promise:

private ttsCreating?: Promise<textToSpeech.TextToSpeechEngine> = undefined

所有调用等待同一个 Promise。或者在preparing状态下禁用重复点击。当前源码没有创建锁,这应纳入高频点击测试。

二十四、弹窗布局如何保障小屏可达

听音弹窗使用整页Column:

上部半透明遮罩 -> layoutWeight(1) 底部内容面板 -> 自适应内容高度

底部面板 padding 包含:

bottom: Sizes.PADDING_LARGE + this.bottomSafePadding()

因此“再听一次”按钮不会压入系统手势区。背景遮罩与“关闭”文字都可退出,用户不会被不可用的 TTS 困在弹窗中。

如果未来增加更长说明、语速或音色控制,底部内容应增加最大高度和内部Scroll,防止横屏或大字体下溢出。

二十五、按钮行为与可访问性

播放入口当前是一个Row,整行绑定onClick;图标本身 40x40,外层还有 12vp padding,触控范围较好。活动状态不仅通过颜色表达,还带有“正在播放示例”文字。

仍可补充:

  • 播放入口的可访问性文本;
  • “再听一次”禁用态;
  • 引擎准备时的进度反馈;
  • 错误状态下“重试”动作;
  • 2in1 键盘焦点顺序;
  • 屏幕阅读器对波形装饰的忽略。

装饰性波形不应被逐条朗读。

二十六、测试矩阵要覆盖资源生命周期

场景期望
首次播放且本地引擎可用创建一次并开始朗读
本地失败、在线可用在线回退成功
两种引擎都失败弹窗仍可见,显示不可用
连续点击两道题旧播放停止,新内容开始
点击关闭停止播放并清空活动状态
点击遮罩与关闭按钮行为一致
点击再听一次停止当前后重新朗读
播放过程中切题不保留上一题活动样式
播放过程中离页stop、shutdown、引用置空
回调发生错误显示失败文案,不崩溃
无网络本地可用则继续,不可用则文字兜底
横屏、小窗、大字体弹窗按钮可达且不遮挡

还应观察日志确认没有重复创建和离页后回调写入失效页面。

二十七、不要为系统能力异常伪造成功状态

语音引擎不可用时,页面没有伪造“播放完成”,而是保留文字兜底。技术文章和测试记录同样应区分:

TTS 本地引擎成功 TTS 在线回退成功 TTS 不可用但文字兜底可用 TTS 播放失败

不能把“弹窗打开成功”写成“语音播放成功”,也不能根据波形变色推断系统真的发声。唯一可靠证据是引擎回调与真实设备听测。

二十八、如果要升级为真实方言音频

更高质量方案可以保留现有状态机,但替换音源层:

interface PronunciationSource { kind: 'asset' | 'tts' uri?: string text?: string dialectCode: string speaker?: string license?: string }

优先播放授权音频资源,缺失时再使用 TTS,并在 UI 标注来源。资源层还要处理:

  • 下载或包内资源;
  • 文件校验;
  • 播放器状态;
  • 耳机和音频焦点;
  • 暂停与继续;
  • 版权和音色授权;
  • 题库版本迁移。

这是一条产品升级路线,不是当前项目已经实现的功能。

二十九、发布前合规检查

语音功能上架前,应验证:

  1. INTERNET权限与在线 TTS 回退一致;
  2. 隐私政策准确描述可能的在线语音处理;
  3. 不宣称真人录音或标准方言音色;
  4. 无语音服务时核心答题仍可完成;
  5. 页面离开后不继续播放;
  6. 系统深浅色下播放卡片和弹窗可读;
  7. 手机、平板、2in1 和小窗下按钮不被遮挡;
  8. 状态栏与底部导航区安全;
  9. 安装、启动、播放、答题、切题、退出和卸载无异常;
  10. 不记录或上传用户私密语音;
  11. 错误文案不暴露内部错误细节;
  12. 网络断开和系统语音服务关闭均有真实测试结果。

三十、把“能播放”拆成内容、状态与资源三层

语音能力不能只验证扬声器是否发声。内容层要确认传入引擎的是当前题目的 audioHint,缺失时使用明确文字兜底,不能朗读上一题;状态层要确认开始、完成、停止和错误回调只影响对应请求;资源层则要保证页面离开后停止播放并关闭引擎。三层任一失配,都可能出现“有声音但内容错了”或“页面已经离开仍继续播”的问题。

当前 requestId 已用于发起合成,却没有在回调处筛选请求归属。当用户快速切换题目,旧请求的完成回调可能晚于新请求到达,从而错误清除新题目的播放状态。后续应维护活动请求标识,回调先比较再更新 UI;停止旧内容后也要等待或确认状态转换,避免两个请求在引擎内部交叠。

三十一、文字兜底是可用性边界,不是播放成功证明

点击后先打开文字弹窗,使本地引擎不存在、在线引擎不可用或 speak 报错时,用户仍能读到题目提示并继续答题。这种设计保障的是任务可完成,并不代表语音播放成功。界面应区分“正在准备”“正在播放”“播放失败但可阅读”和“已停止”,错误提示不能与成功状态共用同一文案。

在线引擎回退还意味着可能存在网络依赖。是否需要网络权限、数据是否离开设备、服务不可用时如何降级,都应与真实 SDK 行为、隐私说明和发布材料一致。本文只确认代码存在本地优先与在线回退创建策略,不推断实际设备一定安装本地引擎,也不承诺离线必然可播。

三十二、生命周期验收要覆盖快速离开与重建

常规路径是首次播放、完成、关闭弹窗、离开页面。边界路径还包括播放中按返回键、快速切到下一题、连续点击播放、应用切后台、页面重建、引擎创建失败和 shutdown 抛错。每条路径都应检查当前播放标记、弹窗可操作性、扬声器是否停止以及再次进入页面后能否重新创建引擎。

如果 shutdown 只在正常离开调用,而异常路径遗漏,平台资源可能跨页面存活;反过来,如果多次关闭同一引擎没有幂等保护,也可能触发新的错误。测试日志应记录创建次数、活动请求、回调顺序、停止与关闭结果,确认一次页面生命周期只拥有一套有效资源。

三十三、方言产品还需要单独的音质与版权方案

zh-CN 系统合成只能提供普通话或系统音色能力,不能作为四川话、粤语等方言音准证据。若产品目标升级为真实方言听辨,应准备来源清晰、授权明确、发音经过审校的录音,并设计本地资源或受控下载、缓存、校验、播放进度和失败回退。合成与录音是两条不同技术链路,不能只替换文案就宣称完成升级。

三十四、建议用显式状态机约束并发回调

播放状态不宜只靠一个布尔值表达。至少可以区分空闲、准备、播放、停止和失败,并保存活动题目与活动请求标识。新点击到来时先使旧请求失效,再停止旧内容、更新活动标识并启动新合成;回调到达后先比对 requestId,只有当前请求才能改变页面状态。这样即使平台回调乱序,旧完成事件也不会覆盖新播放状态。

typeAudioState='idle'|'preparing'|'playing'|'stopped'|'error'
if(callbackRequestId!==this.activeRequestId) {return}this.audioState='playing'

页面离开时先使活动标识失效,再调用停止与关闭。即便后续仍收到延迟回调,也会被请求校验丢弃。错误信息应映射成用户可理解的提示,平台错误码则留在诊断日志中,两者职责分开。

this.activeRequestId=''this.ttsEngine?.stop()this.ttsEngine?.shutdown()this.audioState='idle'

三十五、结语

中国方言题库的语音实现最有价值的不是“调用一次speak()”,而是把平台能力放在可降级的答题流程中:先显示文字弹窗保证任务可完成,再本地优先创建引擎,必要时尝试在线回退;播放前停止旧请求;通过监听器同步状态;关闭弹窗停止声音;页面离开时关闭引擎。

它的边界也必须讲清楚:当前是zh-CN系统 TTS,不是真实方言录音;波形是静态装饰;请求 ID 尚未用于过滤旧回调;首次快速重复点击可能并发创建;“再听一次”在首次引擎失败后不会主动重建。只有把这些事实写清楚,后续性能优化、生命周期修复和真实音频升级才有可靠起点。


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 9:37:56

Agent Skills深度解析:从提示词到AI可复用技能资产

说实话&#xff0c;我最近无论刷技术社区还是看各大 AI 工具的更新日志&#xff0c;都绕不开“skills”这个词。Claude 官方端出了 Agent Skills&#xff0c;OpenAI 的 Codex 也紧随其后搞了自己的 Skills 体系&#xff0c;再加上各路效率博主整理的“skill 推荐”“skill 下载…

作者头像 李华
网站建设 2026/10/27 9:24:00

NVMe Linux驱动系列一:host端<11>

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/27 9:22:22

vue directive自定义指令如v-focus

一、全局指令 <div id"app"><p>页面载入时&#xff0c;input 元素自动获取焦点&#xff1a;</p><input v-focus> </div><script> // 注册一个全局自定义指令 v-focus Vue.directive(focus, {// 当绑定元素插入到 DOM 中。insert…

作者头像 李华
网站建设 2026/10/27 9:21:55

显示驱动板卡启动引导流程详解:从电源时序到点屏的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/27 9:21:14

2025多模态大模型技术解析与应用实践

1. 2025年多模态大模型技术全景扫描当我在Google Research的实验室第一次看到多模态大模型同时解析图像、文本和音频时&#xff0c;那种震撼至今难忘。如今这项技术已从实验室走向产业前沿&#xff0c;特别是在搜索、广告和内容推荐领域&#xff08;简称"搜推广"&…

作者头像 李华