在 airi 中使用 VueUse useFocus:DOM 元素焦点状态追踪与双向控制实战
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
导读
本文围绕 VueUse 的useFocus组合式函数展开,讲解如何在 Vue 3 / Nuxt 项目中以响应式方式追踪、读取并主动设置 DOM 元素的焦点(focus)状态。结合 airi 开源仓库(一个自托管、面向 Web / macOS / Windows 的陪伴型 AI 项目,采用 Vue 3 + Vite + pnpm workspace 架构)的实际工程环境,你将掌握useFocus的调用方式、initialValue与focusVisible等核心选项的含义,以及用它替代手工操作focus()/blur()事件监听来管理输入框、搜索框、弹窗等交互焦点的实战方案。
在 airi 工程中的适用场景
airi 的各个前端应用(如 apps/stage-web、apps/stage-pocket)以及舞台相关 UI 组件库(如 packages/stage-ui)均基于 Vue 3 构建,并在依赖中引入了@vueuse/core(参见 apps/stage-web/package.json 中的"@vueuse/core": "catalog:",由仓库根目录 pnpm-workspace.yaml 的 catalog 统一管理版本)。
当你在这些应用里需要实现以下交互时,useFocus是首选方案:
- 语音/文字输入框的自动聚焦与失焦状态检测;
- 聊天输入区在点击按钮后聚焦、提交后失焦的完整状态机;
- 弹窗或设置面板打开时将焦点交给第一个可聚焦控件;
- 根据焦点状态动态切换输入框的高亮样式(对应 CSS 的
:focus-visible行为)。
.agents/skills/vueuse-functions/SKILL.md中也将useFocus归入Sensors(传感器)类别,标注调用规则为AUTO——即在合适的场景下应优先使用该组合式函数而非手写事件监听代码,以保持实现的简洁、可维护与高性能。
基本用法:响应式追踪焦点状态
useFocus的核心价值在于:把一个普通 DOM 元素变成一个"可读可写"的响应式焦点状态。它返回一个focused的WritableComputedRef<boolean>——读取时反映目标元素当前是否拥有焦点,写入时则会真正触发元素的focus()或blur()。
最基本的使用方式如下(与文档 useFocus.md 中的示例一致):
import { useFocus } from '@vueuse/core' const target = shallowRef() const { focused } = useFocus(target) watch(focused, (focused) => { if (focused) console.log('input element has been focused') else console.log('input element has lost focus') })几个关键点:
target通过shallowRef()声明,并在模板中通过ref="target"绑定到真实元素;useFocus接受MaybeElementRef,即它可以接收 ref、getter 或直接的元素引用。focused是可写的计算引用:focused.value为true表示元素当前被聚焦,为false表示失焦。- 你无需手动添加
focus/blur事件监听器,useFocus内部会为你完成事件绑定与清理,避免内存泄漏。
首次渲染自动聚焦:initialValue 选项
如果希望在元素首次渲染时就获得焦点,可以传入initialValue: true。该选项会在目标元素就绪后主动触发一次focus事件:
import { useFocus } from '@vueuse/core' const target = shallowRef() const { focused } = useFocus(target, { initialValue: true })这对"页面打开即聚焦输入框"的场景非常实用,例如 airi 的聊天对话框打开后立即聚焦消息输入区,用户无需再点击输入框即可直接打字。
由外部动作改变焦点状态
focused的双向性意味着你可以通过任何响应式逻辑来控制焦点。下面的例子展示了点击按钮后让下方输入框获得焦点(与文档 useFocus.md 中的 Vue 单文件组件示例一致):
<script setup lang="ts"> import { useFocus } from '@vueuse/core' import { shallowRef } from 'vue' const input = shallowRef() const { focused } = useFocus(input) </script> <template> <div> <button type="button" @click="focused = true"> Click me to focus input below </button> <input ref="input" type="text"> </div> </template>当focused被赋值为true时,useFocus会调用目标元素的focus();赋值为false时则调用blur()。这一机制让你可以把"是否聚焦"提升为应用状态的一部分——例如在弹窗关闭、路由切换或语音唤醒等事件中统一管理焦点归属,而不是散落各处的document.querySelector().focus()调用。
选项与类型声明全解
从文档 useFocus.md 的类型声明部分,useFocus的完整签名如下:
export interface UseFocusOptions extends ConfigurableWindow { /** * Initial value. If set true, then focus will be set on the target * * @default false */ initialValue?: boolean /** * Replicate the :focus-visible behavior of CSS * * @default false */ focusVisible?: boolean /** * Prevent scrolling to the element when it is focused. * * @default false */ preventScroll?: boolean } export interface UseFocusReturn { /** * If read as true, then the element has focus. If read as false, then the element does not have focus * If set to true, then the element will be focused. If set to false, the element will be blurred. */ focused: WritableComputedRef<boolean> } export declare function useFocus( target: MaybeElementRef, options?: UseFocusOptions, ): UseFocusReturn三个选项的实战含义:
| 选项 | 默认值 | 作用 |
|---|---|---|
initialValue | false | 为true时,元素首次渲染后立即聚焦 |
focusVisible | false | 为true时,复刻 CSS 的:focus-visible语义,仅在键盘导航(如 Tab 键)触发的聚焦下才将focused置为true,鼠标点击聚焦不会触发 |
preventScroll | false | 为true时,聚焦元素时阻止浏览器滚动到该元素(内部透传给focus({ preventScroll: true })),适合弹窗/抽屉等不希望页面跳动的内容区 |
需要说明的是,focusVisible: true需要浏览器支持:focus-visible相关 API,在不支持的环境下会回退为普通聚焦行为。ConfigurableWindow扩展自@vueuse/core的窗口配置,通常无需显式指定。
与其他 VueUse 函数的配合
useFocus是 VueUse 焦点管理族的一员,可根据需求组合使用:
useActiveElement:响应式追踪document.activeElement,可与useFocus配合判断"当前焦点元素"与"目标元素"是否一致;useFocusWithin:追踪"元素或其任意后代"是否拥有焦点,适合下拉面板、组合输入框等含多个子控件的场景;useFocusTrap:在弹窗/模态框中锁定焦点循环,防止 Tab 焦点逃逸到背景层(该函数为@Integrations类别的外部依赖包装器)。
在 airi 的 Live2D 舞台场景中也可以看到"焦点/坐标"类响应式推导的同类实践:packages/stage-ui-live2d/src/composables/live2d/eye-tracking.ts中的useLive2DEyeFocusFor通过computed把鼠标光标坐标映射为 Live2D 模型的视线焦点(Live2DModel.focus(x, y)),并在packages/stage-ui-live2d/src/components/scenes/Live2D.vue#L58与:focus-at绑定。这展示了 airi 项目中"以响应式推导取代命令式更新"的一贯风格,useFocus正是这一思想在 DOM 焦点管理上的体现。
在 airi 聊天输入场景的落地示例
结合 airi 这类陪伴型 AI 应用的实际交互,下面给出一个"语音唤醒后自动聚焦输入框,按下回车发送后失焦"的完整示例:
<script setup lang="ts"> import { useFocus } from '@vueuse/core' import { ref, shallowRef } from 'vue' const inputEl = shallowRef<HTMLInputElement>() const { focused } = useFocus(inputEl, { initialValue: true }) const message = ref('') function onVoiceWakeup() { focused.value = true // 语音唤醒后立即聚焦输入框 } function onSend() { // ... 发送逻辑 focused.value = false // 发送后收起键盘焦点 } </script> <template> <div class="chat-box" @click="focused = true"> <input ref="inputEl" v-model="message" type="text" placeholder="输入消息…" :class="{ 'input-focus-visible': focused }" @keyup.enter="onSend" > </div> </template>这段代码集中体现了useFocus的全部优势:状态即数据、数据驱动行为,既无需手写事件监听,又天然与 Vue 的响应式体系(watch、computed、模板绑定)无缝集成。
小结
useFocus(target)返回可读可写的focused引用,读反映焦点、写触发focus()/blur();- 三个核心选项:
initialValue(首次渲染聚焦)、focusVisible(复刻:focus-visible)、preventScroll(聚焦不滚动); - 结合
watch、模板绑定或外部事件(按钮、语音唤醒、弹窗打开)即可构建完整的焦点状态机; - 如需更复杂的焦点管理,可组合
useActiveElement、useFocusWithin、useFocusTrap。
仓库中该函数的完整参考文档位于 .agents/skills/vueuse-functions/references/useFocus.md,其所属技能的总览与函数索引见 .agents/skills/vueuse-functions/SKILL.md,配合 apps/stage-web/package.json 中@vueuse/core的依赖声明,即可在 airi 的任何 Vue 应用中直接开始使用。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考