更新记录

1.0.1(2026-08-10)

1.0.0(2026-08-07)

初始版本发布


平台兼容性

uni-app(3.8.1)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× × × × 9.0 × ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

其他

多语言 暗黑模式 宽屏模式
× ×

zxz-tv-kit

TV 端焦点导航布局缩放框架(Zone-Grid 焦点模型,nvue 专用)。

用于 Android TV / 投影仪等只有遥控器、没有触摸的设备:用方向键在页面元素间移动高亮框,用确认键激活。本 kit 负责「按了下键,焦点应该去哪」这件事,以及「设计稿 960×540,到 1920×1080 电视上怎么放大」这件事。


目录


它解决什么问题

浏览器里用鼠标点击,元素在哪不重要。但在电视上,用户按「下」键时,你必须回答:下一个焦点是哪个元素?

常见做法是运行时测量所有元素的 DOM 坐标,再按方向做空间评分搜索。这套方案在 nvue + TV 上有三个致命问题:

问题 后果
dom.getComponentRect 异步且开销大 首次按键延迟 \~200ms,遥控器体感明显黏滞
滚动后坐标失效 列表滚一屏后,导航跳到错误元素
搜索范围要靠调参 跨区域(如从网格跳到分页器)时常失败

本 kit 的思路:不测量,用算的。

TV 布局本质上就是网格和列表。只要你声明「这块区域是 5 列网格」,那么第 7 个元素往右就是第 8 个,往下就是第 12 个——纯整数运算,O(1),0ms 延迟,且不受滚动影响


核心概念

理解这三个词,就理解了整个 kit:

1. Zone(区域)

一块布局规则一致的区域。比如首页可以划成 5 个 Zone:

┌──────────────────────────────────────────┐
│  [search]              [fav]             │  ← 两个独立 Zone
├───────┬──────────────────────────────────┤
│       │  ■ ■ ■ ■ ■                       │
│ [cat] │  ■ ■ ■ ■ ■   ← zone: 'mov'       │
│ 垂直  │  ■ ■ ■ ■ ■      grid, columns: 5 │
│ 列表  │                                  │
├───────┴──────────────────────────────────┤
│  [pager]  上一页 1 2 3 … 下一页 跳至 __  │  ← 水平 Zone
└──────────────────────────────────────────┘

Zone 声明自己的 layout(grid / vertical / horizontal)、列数、以及撞到边界时该去哪个 Zone

2. index(序号)

元素在 Zone 内的序号,从 0 开始。这是坐标的唯一来源——不存在「x/y 坐标」的概念,位置完全由 index + Zone 规则推导:

// grid, columns = 5,index = 7 的元素:
row = Math.floor(7 / 5) = 1   // 第 1 行
col = 7 % 5 = 2               // 第 2 列
// 按「右」→ col+1 = 3 → index = 1*5+3 = 8
// 按「下」→ row+1 = 2 → index = 2*5+2 = 12

⚠️ index 必须与视觉顺序严格一致,否则导航会跳到肉眼看起来不相邻的元素。这是使用本 kit 最常见的出错点。

3. pageId(页面标识)

焦点状态按 pageId 分组隔离。nvue 里「详情页」常常只是同一个 nvue 文件里 v-if 出来的一个组件,并不是真的 uni-app 页面。用 pageId 就能让浏览页和详情页各自维护一套焦点,互不干扰:

// 浏览组件用 'ZINDEX',播放器组件用 'ZDETAIL'
ZoneFocusManager.activatePage('ZDETAIL', { zone: 'player', index: 0 })
// 此时 'ZINDEX' 的所有注册都还在,只是不再响应按键;返回时 activatePage('ZINDEX') 即可恢复原焦点

安装

uni_modules/zxz-tv-kit 整个目录放入项目。两个组件(zxz-focusable / zxz-paginator)经 easycom 自动注册,模板里直接写标签即可,无需 import、无需 components 声明

运行环境要求:

要求
渲染引擎 nvue 专用(vue 页面不支持)
平台 App(Android TV / 投影仪);按键依赖 plus.key,H5/小程序不可用
Vue Vue 3
HBuilderX ≥ 3.6.0

5 分钟上手

第 1 步:装载插件(main.js,可选)

import { createTvKit } from '@/uni_modules/zxz-tv-kit'
app.use(createTvKit())

这一步只是注册全局属性 this.$tvKit(内含 ZoneFocusManager / px / createStyles)。不装也能用,直接 import 即可。

插件不会自动注册按键监听。因为 plus.key 是全局的,app 启动就注册会导致非 TV 页面也被抢键。必须由 TV 页面自己在 onLoad 里装、onUnload 里卸。

第 2 步:写一个 TV 页面(.nvue)

<template>
  <view>
    <!-- 一个垂直列表,3 个可聚焦项 -->
    <zxz-focusable v-for="(item, i) in list" :key="item.id"
      :item-id="'row-' + item.id" zone="list" :index="i" :page-id="pageId"
      @focus="onItemFocus" @blur="onItemBlur" @activate="(item)">
      <!-- 高亮样式由你自己画:kit 只告诉你「谁被聚焦了」 -->
      <view :style="[styles.row, focusedId === 'row-' + item.id ? styles.rowFocused : {}]">
        <text :style="styles.text">{{ item.name }}</text>
      </view>
    </zxz-focusable>
  </view>
</template>

<script>
import {
  initKeyListener, removeKeyListener,
  ZoneFocusManager, createStyles
} from '@/uni_modules/zxz-tv-kit'

export default {
  data() {
    return {
      pageId: 'demo',
      focusedId: null,
      list: [{ id: 1, name: '第一项' }, { id: 2, name: '第二项' }, { id: 3, name: '第三项' }]
    }
  },
  computed: {
    // createStyles:数字自动按设计稿缩放成实际 px
    styles() {
      return createStyles({
        row:        { height: 48, paddingLeft: 20, justifyContent: 'center',
                      backgroundColor: 'rgba(255,255,255,0.1)', marginBottom: 8 },
        rowFocused: { backgroundColor: 'rgba(255,255,255,0.5)' },
        text:       { fontSize: 16, color: '#ffffff' }
      })
    }
  },
  onLoad() {
    initKeyListener()               // ① 接管遥控器按键
  },
  onUnload() {
    removeKeyListener()             // ② 交还按键
    ZoneFocusManager.clearPage(this.pageId)   // ③ 清空注册,防止元素残留
  },
  mounted() {
    this.registerZones()            // ④ 先声明区域
    this.$nextTick(() => {          // ⑤ 等元素注册完,再给默认焦点
      ZoneFocusManager.activatePage(this.pageId, { zone: 'list', index: 0 })
    })
  },
  methods: {
    registerZones() {
      ZoneFocusManager.registerZone(this.pageId, {
        id: 'list',
        layout: 'vertical'
      })
    },
    onItemFocus({ itemId }) { this.focusedId = itemId },        // 注意:对象
    onItemBlur(itemId) {                                        // 注意:字符串
      if (this.focusedId === itemId) this.focusedId = null
    },
    (item) { uni.showToast({ title: '打开 ' + item.name }) }
  }
}
</script>

生命周期顺序(重要)

这个顺序错了就会「焦点不出现」或「按键无反应」:

onLoad          →  initKeyListener()
mounted         →  registerZone()        ← 先声明 Zone
  ↓ (子组件 mounted 自动 registerItem)
$nextTick       →  activatePage(默认焦点) ← 元素就位后才聚焦
...
onUnload        →  removeKeyListener() + clearPage()

zxz-focusable 在自己的 mounted 里自动向管理器注册,你不用管。若数据是异步来的(接口还没返回,列表是空的),activatePage 传的默认焦点会被存为 pendingFocus,等元素真正注册上来时自动补聚焦——所以不需要自己 setTimeout 等数据。


Zone 配置详解

ZoneFocusManager.registerZone(pageId, {
  id: 'mov',                    // 【必填】区域唯一标识
  layout: 'grid',               // 【必填】'grid' | 'vertical' | 'horizontal'
  columns: 5,                   // grid 列数(layout='grid' 时必填,默认 1)

  // ↓ 以下三项仅「焦点跟随滚动」时需要,填设计稿原始值,内部自动 px() 缩放
  origin:   { x: 0, y: 14 },    // 区域左上角在滚动容器内的起点
  itemSize: { w: 140, h: 190 }, // 单个元素尺寸
  gap:      { x: 0, y: 0 },     // 元素间距(默认 0)

  // 焦点跟随滚动:焦点移到可视区外时,自动改写你的 scrollTop 变量
  scroll: {
    getter:   () => this.movieScrollTop,
    setter:   (v) => { this.movieScrollTop = v },
    viewport: () => this.listHeight       // 可视区高度(横向列表则为宽度)
  },

  // 撞到本区域边界时,跳到哪个 Zone
  focusGuide: {
    up:    { zone: 'search', index: 0 },
    left:  { zone: 'cat',    index: 'remember' },
    down:  { zone: 'pager',  index: 'remember' }
  },

  // 撞边界时的业务处理(优先级高于 focusGuide)
  onBoundary: {
    down: (page, index) => { /* ... */ return false }
  }
})

各 layout 的导航规则

layout 响应的方向键 规则
vertical 上 / 下 index ± 1,目标不存在则视为撞边界
horizontal 左 / 右 index ± 1,同上
grid 上下左右 columns 换算行列;左右不跨行(col 越界即边界)

grid 向下时若目标格子为空(最后一行不满),会自动钳制到该行最后一个有效元素。例如 13 个元素、5 列,从 index 9 按下 → 目标 14 不存在 → 落到 12。

focusGuideindex 取值

含义
数字 固定跳到该 index
'remember' 跳到目标 Zone 最后一次停留过的元素(体验最好,推荐用于主区域互跳)
省略 跳到目标 Zone 的最小 index

'remember' 若无历史记录,或目标 index 的元素已被注销,会自动回退到最小 index,不会卡死。

onBoundary:把边界变成业务动作

onBoundary[dir] 优先于 focusGuide 执行,回调签名 (page, index),返回值决定后续行为:

返回值 行为
false 放弃处理,继续走 focusGuide
{ zone, index } 立即聚焦到该位置
'pending' 你已自行调用 setPendingFocus,管理器不动,等新元素注册后自动聚焦
Promise 视为异步。resolve {zone,index} 则补聚焦;resolve false 则回退到 focusGuide;resolve 'pending' 同上

回调的第一个参数 page 是内部页面状态对象,可用于查询:page.zoneItemsMap<zoneId, Map<index, itemId>>)、page.lastIndexMap<zoneId, index>)。典型用法是判断目标区域是否为空再决定去向:

onBoundary: {
  down: (page) => {
    const zi = page.zoneItems.get('mov')
    if (!zi || zi.size === 0) {
      // 网格是空的(如收藏夹为空),别往下跳,改去分类栏,避免焦点掉进虚空
      const idx = page.lastIndex.has('cat') ? page.lastIndex.get('cat') : 0
      return { zone: 'cat', index: idx }
    }
    return false   // 网格有内容,交给 focusGuide 正常处理
  }
}

组件 API

<zxz-focusable>

可聚焦包装组件。它本身不带任何视觉样式——只负责注册、接收焦点、抛事件。高亮长什么样由你在 slot 里自己画。

Props

属性 类型 必填 说明
page-id String 所属页面标识
zone String 所属区域 id
index Number 在区域内的序号(必须与视觉顺序一致)
item-id String | Number 元素唯一标识(同页面内不可重复)
auto-focus Boolean
注册完成后立即抢占焦点。用于浮层/弹窗的关闭按钮
wrap-style Object
透传给根 view 的样式(如需撑满父级时)

Events

事件 载荷 触发时机
@activate 遥控器确认键,鼠标/触摸点击
@focus { itemId } 对象 获得焦点
@blur itemId 字符串 失去焦点
@click 仅点击(activate 也会同时触发)

⚠️ @focus 给的是对象 { itemId }@blur 给的是裸字符串。这个不对称是既有约定,接线时容易写错:

onItemFocus({ itemId }) { this.focusedId = itemId }   // 解构
onItemBlur(itemId)      { ... }                       // 不解构

关于 :index 动态变化:组件内部 watchindex,变化时自动调 updateItemIndex 同步映射。所以像分页器页码开窗这种「组件被 Vue 复用、但 index 变了」的场景是安全的——mounted 不会重跑,但 index 会正确迁移。

<zxz-paginator>

开箱即用的分页器(上一页 / 页码 / 下一页 / 跳转输入框),内部元素全部可聚焦。总页数 > 7 时中间页自动折叠为

Props

属性 类型 必填 说明
page-id String 所属页面标识
current-page Number
当前页(1 起),默认 1
total-pages Number
总页数,默认 1

Events@prev@next@goto(page)

⚠️ 它硬编码使用 zone="pager" 你必须在页面里注册一个 id: 'pager' 的 Zone,否则分页器的焦点无处安放:

ZoneFocusManager.registerZone(page, {
  id: 'pager', layout: 'horizontal',
  focusGuide: { up: { zone: 'mov', index: 'remember' } }
})

内部 index 分配为:0 = 上一页,1..N = 页码,N+1 = 下一页,N+2 = 跳转框。

@goto 可能抛出超过 total-pages 的页码(用户在跳转框里手输)。这是刻意设计——由父组件钳制到末页,语义与 prev/next 的边界钳制一致,避免静默忽略让用户以为没响应:

onPagerGoto(page) {
  this.changePage(Math.min(Math.max(1, page), this.totalPages), 0)
}

跳转输入框内部已自行处理 setInputFocused,你不用管。


核心 API

按键监听

函数 说明
initKeyListener() 注册 plus.key 全局监听。在 TV 页面 onLoad 调用
removeKeyListener() 注销。在 onUnload 调用
setInputFocused(bool) true 时焦点管理器完全不拦截按键,交给输入框/输入法
setVideoFullscreen(bool) true 时按键转发给业务层(uni.$emit('video-keydown', keyCode)),返回键除外

按键映射

keyCode 行为
19 / 20 / 21 / 22 上 / 下 / 左 / 右 moveFocus(dir)
23 / 66 / 13 确认 / 回车 activate()
4 返回 不处理,交给系统(走页面 onBackPress

长按连发按 40ms 节流,首次按下立即响应(不做防抖,避免遥控器体感黏滞)。

ZoneFocusManager

单例,import { ZoneFocusManager } from '@/uni_modules/zxz-tv-kit'

页面级

方法 说明
activatePage(pageId, defaultFocus?) 激活页面并接管按键。已有焦点则恢复,否则用 defaultFocus(格式 { zone, index })。元素未就位时存为 pendingFocus,稍后自动补聚焦
deactivatePage(pageId) 当前元素失焦,但保留 currentFocus 供下次 activatePage 恢复
clearCurrentFocus(pageId) 失焦并置空 currentFocus,注册不动。用于「焦点要交给 pendingFocus 接管」的场景
clearPage(pageId) 彻底清空该页面全部数据。页面销毁时必调

deactivatePage vs clearCurrentFocus:前者是「暂时离开,回来还在原位」,后者是「主动放空,让别人来接管」。若不放空,pendingFocus 会被仍然有效的旧焦点挡住而不生效。

Zone / 元素

方法 说明
registerZone(pageId, config) 注册区域(见上文)
unregisterZone(pageId, zoneId) 注销区域及其全部元素
registerItem(pageId, zoneId, index, itemId, ref) 注册元素。zxz-focusable 自动调用,一般不用手写。ref 需提供 { setFocused(bool), activate() }
removeItem(pageId, itemId) 注销元素。组件 beforeUnmount 自动调用
updateItemIndex(pageId, itemId, newIndex) 迁移元素 index。组件 watch index 自动调用
clearZoneItems(pageId, zoneId) 清空某 Zone 全部元素。翻页全量重建列表前必调(见下方配方)

焦点控制

方法 说明
setFocus(pageId, zoneId, index) 立即聚焦,返回是否成功(元素不存在返回 false
setPendingFocus(pageId, { zone, index }) 设置待聚焦目标:元素还没渲染出来时先挂着,注册到位后自动生效
getCurrentFocus() 返回当前页焦点 { zoneId, index }null。注意字段是 zoneId
moveFocus(direction) 手动移动,'up'\|'down'\|'left'\|'right'
activate() 手动激活当前焦点元素
destroy() 解绑全局事件监听(单测 teardown 用)

也支持全局事件触发:uni.$emit('focus-move', 'down') / uni.$emit('focus-activate')

布局缩放

设计稿基准 960×540,默认按宽度缩放(TV 恒为横屏)。1920 宽的电视上 scale = 2.0。

函数 说明
px(n, options?) 设计稿 px → 实际 px,返回数字(已四舍五入避免半像素模糊)
createStyles(obj) 批量转换样式对象,数字 → 'Npx' 字符串,支持嵌套;字符串/数组原样保留
clearPxCache() 清除缓存。屏幕方向/尺寸变化后调用
TV_DESIGN { width: 960, height: 540 }
getScale(strategy?) 直接取缩放系数(不带缓存)
px(20)                          // → 40(1920 宽屏)
px(14, { min: 24 })             // 最小值保护,防止字体在小屏上糊成一团
px(100, { strategy: 'height' }) // 按高度缩放
px(300, { max: 500 })           // 上限

createStyles({
  box:  { width: 140, height: 190, borderRadius: 8 },  // 数字 → 缩放 + 'px'
  text: { fontSize: 16, color: '#fff' }                // 字符串原样保留
})
// → { box: { width: '280px', height: '380px', borderRadius: '16px' },
//     text: { fontSize: '32px', color: '#fff' } }

createStyles 不支持 min/max。需要保护值时先用 px(n, {min}) 单独算好再填进去。


常见场景配方

焦点跟随滚动

给 Zone 补上 origin / itemSize / gap / scroll 四项,焦点移出可视区时管理器会自动改写你的 scrollTop:

ZoneFocusManager.registerZone(page, {
  id: 'mov', layout: 'grid', columns: 5,
  origin: { x: 0, y: 14 }, itemSize: { w: 140, h: 190 }, gap: { x: 0, y: 0 },
  scroll: {
    getter: () => this.movieScrollTop,
    setter: (v) => { this.movieScrollTop = v },
    viewport: () => Math.max(0, this.listHeight - this.pagerHeight)
  }
})
<scroll-view scroll-y scroll-with-animation :scroll-top="movieScrollTop">

viewport 建议返回你自己算出来的高度,不要用 dom.getComponentRect 量原生 scroll-view——部分固件返回的是 contentSize 而非 viewportSize,会导致滚动目标算得过小,焦点卡片整体落在可视区外。

翻页时的焦点接管

翻页会把整批元素销毁重建,index 复用,焦点极易卡死或错位。固定套路

goToPage(uiPage, focusIndex = 0) {
  // ① 主动清空旧注册 —— 不要依赖组件 beforeUnmount 的时序
  ZoneFocusManager.clearZoneItems(this.currentRoute, 'mov')
  this.movieListKey++          // ② 换 key 强制整批重建
  this.movies = newList
  this.resetMovieScroll()
  // ③ 挂上待聚焦目标,新元素注册后自动生效
  ZoneFocusManager.setPendingFocus(this.currentRoute, {
    zone: 'mov', index: Math.min(focusIndex, newList.length - 1)
  })
  return 'pending'             // ④ 告诉 onBoundary:我接管了,你别动
}

为什么必须 clearZoneItems:nvue 下旧组件的销毁可能延迟。不主动清的话,currentFocus 会长期停留在旧 index 上,pendingFocus 判定「当前焦点仍有效」就不会接管,焦点卡在旧位置反复触发翻页。

输入框(搜索 / 跳转页码)

输入框聚焦时软键盘弹起,此时必须让出按键,否则确认键会被焦点管理器抢走:

onSearchActivate() { this.searchInputFocused = true },  // 确认键 → 进入输入态
onSearchInputFocus() { setInputFocused(true) },         // 让出按键
onSearchInputBlur()  { setInputFocused(false) },        // 收回按键
searchVideos() {
  // 确认搜索:主动退出输入态,别依赖 @blur
  this.searchInputFocused = false
  setInputFocused(false)
  this.loadMovies({ isSearch: true })
}
<!-- v-if 重建 input + :focus="true" 创建即聚焦 -->
<input v-if="searchInputFocused" :focus="true" v-model="keyword"
       @focus="onSearchInputFocus" @blur="onSearchInputBlur" @confirm="searchVideos" />

两个关键点:① 不要常驻 <input>,非输入态时用 <text> 占位,否则它会一直抢键。② 不要用 ref.focus(),TV nvue 下不可靠;用 v-if 重建 + :focus="true"

最容易踩的坑是 setInputFocused(true) 后忘了置回 false——此时焦点管理器永久静默,整个遥控器像死机一样。任何退出输入态的路径都要显式 setInputFocused(false)

全屏视频播放

全屏时方向键应该控制快进/暂停,而不是移动焦点:

// 进入全屏
setVideoFullscreen(true)
uni.$on('video-keydown', this.onVideoKey)

onVideoKey(keyCode) {
  switch (keyCode) {
    case 23: case 66: case 13: this.togglePlay(); break
    case 21: this.requestSeek(-this.seekStep()); break
    case 22: this.requestSeek(this.seekStep()); break
  }
}

// 退出全屏 / 组件销毁
setVideoFullscreen(false)
uni.$off('video-keydown', this.onVideoKey)

返回键(keyCode 4)不会被转发,始终交给系统,用于退出全屏。

快进键必须做合并,不能一次按键一次 seek() 遥控连点/长按会在几百毫秒内产生多次按键,逐次下发 seek 会让原生播放器(ExoPlayer / HLS)反复中断重缓冲、seek 相互抢占,最终不再回调任何事件——表现是进度条不动、loading 圈常驻的"卡死"。正确做法:按键只累加目标位置并即时更新 UI,停手 ~350ms 后才下发一次 seek()

配套的两个坑: 累加基准要用「上一次的目标位置」,不能用 @timeupdate 写入的当前进度——seek 未落地前它还是旧值,第二次按键会把目标打回原点。 seek 待定期间要忽略 @timeupdate 回调,并配一个看门狗超时兜底,否则待定态卡住会让进度条永远不再跟随。

弹窗 / 浮层抢焦点

给关闭按钮加 auto-focus,注册完成即接管焦点:

<zxz-focusable v-if="dialogVisible" item-id="dlg-close" zone="dialog"
  :index="0" :page-id="pageId" auto-focus @activate="close">
  <text>关闭</text>
</zxz-focusable>

浮层关闭后,用 setFocus 把焦点还给原位置。


踩坑与注意事项

后果 解法
index 与视觉顺序不一致 导航跳到肉眼不相邻的元素 v-for 的下标;有条件渲染(如分组标题)时单独算一份连续的 focusIndex,别直接用数组下标
忘记 clearPage 页面重进后旧元素残留,焦点跳到已销毁元素 onUnload 必调
setInputFocused(true) 后没置回 遥控器完全失灵 所有退出输入态的路径都要 setInputFocused(false)
ref.focus() 聚焦 input TV nvue 下不生效 v-if 重建 + :focus="true"
常驻 <input> 持续抢键,方向键失效 非输入态用 <text> 占位
翻页不调 clearZoneItems 焦点卡在旧 index,反复触发翻页 见上方翻页配方
用了 zxz-paginator 但没注册 pager Zone 分页器无法聚焦 注册 id: 'pager' 的 horizontal Zone
origin/itemSize 填了实际 px 滚动位置在不同分辨率上错乱 填设计稿原值,管理器内部会 px()
mounted 同步调 activatePage 元素尚未注册,默认焦点落空 放进 $nextTick(或依赖 pendingFocus 自动补)
@blur 当对象解构 高亮不消失 @focus 给对象、@blur 给字符串

nvue 样式补充(本 kit 组件内部遵循的约定):

  • borderinline style,不要写在 scss 里——nvue 对 border 简写/独立属性的解析行为不稳定。
  • 聚焦态用「透明 border 占位 + 聚焦时换实色」,避免按钮尺寸跳动 2px。
  • nvue 不支持 border-color 过渡transition 只对 background-color 之类有效。
  • 不要显式写 overflow: visible(nvue 默认即是),会触发 [plugin:vite:nvue-css] 编译错误。

FAQ

Q:能在普通 .vue 页面用吗? 不能。本 kit 是 nvue 专用,且按键依赖 plus.key(仅 App 端)。

Q:一个页面里能有几个 Zone? 不限。首页 5 个(search / fav / cat / mov / pager)是典型规模。

Q:Zone 之间怎么才能互相到达?focusGuide。它是单向的——mov 声明了 left → cat,不代表 cat 能靠右键回到 mov,两边都要各自声明。

Q:焦点为什么不动? 按顺序排查:① initKeyListener() 调了吗?② activatePage() 调了吗、getCurrentFocus() 返回 null 吗?③ 是不是 setInputFocused(true) 没置回?④ 目标 index 的元素真的注册了吗(page.zoneItems)?

Q:撞到边界有反馈吗? 有,会触发 uni.vibrateShort() 震动(App 端支持时)。

Q:数据是异步加载的,要不要等接口回来再设焦点? 不用。直接 activatePage(pageId, { zone, index }),元素没就位时会存为 pendingFocus,注册到位后自动补聚焦。

Q:getCurrentFocus() 返回的字段是 zone 还是 zoneIdzoneId。但传入 activatePage / setPendingFocus 的参数字段是 zone。这两处不一致,注意别写混。


许可与免责

软件无担保

本软件按 「原样」(AS IS) 提供,不附带任何形式的明示或暗示担保,包括但不限于:适销性担保、特定用途适用性担保、不侵权担保。在任何情况下,作者或版权持有人均不对因本软件或本软件的使用、其它处置而产生的任何索赔、损害或其它责任负责(无论合同、侵权或其它形式的诉讼依据为何)。

第三方视频源免责声明

本 kit 是纯技术框架,仅提供 TV 端焦点导航与布局缩放能力,不接入、不存储、不分发任何视频内容。使用者自行决定接入的第三方视频源(含但不限于采集站、CDN、图床、播放器后端)的内容、合规性、可用性、版权归属及变更风险,均由使用者独立承担

具体包括但不限于:

  • 第三方源站接口变更、失效、数据漂移导致的播放失败或内容错乱
  • 第三方源站所提供内容的版权、合法性、广告、违规信息
  • 第三方图床 / CDN 的可用性、隐私策略、数据安全
  • 接入第三方服务引发的合规、备案、内容审查风险

作者不为上述任何情形承担任何责任,亦不对使用本 kit 开发的最终产品做任何背书。

使用范围与责任

  • 本 kit 仅供学习研究、内部技术评估及合法业务场景使用
  • 禁止将本 kit 或基于本 kit 修改的版本用于任何违法违规用途,包括但不限于:传播侵权内容、绕过平台版权保护、违反所在地区法律法规的行为
  • 禁止将本 kit 未经授权用于商业转售、SaaS 化再授权或二次商业包装
  • 因使用、修改、分发本 kit 所产生的一切合规、安全、版权、商业纠纷及法律责任,均由使用者自行承担
  • 作者保留在发现违规用途时公开声明、撤销授权、停止维护的权利

⚠️ 在中国大陆地区使用 TV 类应用涉及《互联网视听节目服务管理规定》《广播电视管理条例》等多项法规,使用者须自行完成相应资质备案与内容审核,并确保接入内容符合所在司法辖区的法律要求。本 kit 不提供任何合规咨询。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。