找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 139|回复: 2

QuickTVUI 之 qt-grid-view 使用指南

[复制链接]

18

主题

6

回帖

112

积分

注册会员

积分
112
发表于 2026-7-22 22:27:47 | 显示全部楼层 |阅读模式

QuickTVUI 之 qt-grid-view 使用指南

一、组件简介

qt-grid-view 是 QuickTVUI 提供的 TV 端网格列表组件,基于原生 tv-list 封装,具备以下特性:

  • 支持自定义列数(spanCount)的网格布局
  • 支持分页加载(loadMore)与上拉加载触发
  • 支持多 type 模板渲染(普通 item、loading、底部提示等)
  • 支持焦点记忆、默认焦点、阻止焦点方向等 TV 焦点能力
  • 支持两种数据驱动模式:listData(推荐)与 init() 函数(兼容旧版)
  • 内置 ${variable} 数据绑定语法,性能优于 Vue 模板渲染

二、安装

[code=bash]pnpm add @quicktvui/quicktvui3[/code]

三、最小示例

[code=vue]<template>
  <div class="page">
    <qt-grid-view
      class="grid_view"
      ref="gridViewRef"
      :clipChildren="false"
      :list-data="listData"
      :spanCount="6"
      @item-click="onItemClick"
    >
      <!-- 普通 item 模板,type 与数据项 type 对应 -->
      <qt-view type="1" class="qt_list_item" :focusable="true" eventClick>
        <qt-text text="${text}" class="qt_list_item_text" />
      </qt-view>
    </qt-grid-view>
  </div>
</template>

<script lang="ts" setup>
import { qtRef } from '@quicktvui/quicktvui3'
import type { QTListViewItem } from '@quicktvui/quicktvui3'

const gridViewRef = ref()
const listData = qtRef<QTListViewItem[]>()

function onItemClick(e: any) {
  console.log('item click:', e.position, e.item)
}

defineExpose({
  onESCreate() {
    listData.value = Array.from({ length: 30 }, (_, i) => ({
      text: '第 ' + i + ' 项',
      type: 1
    }))
  }
})
</script>[/code]

几个关键点:

  • 数据传递用 :list-data(推荐)或 init() 函数,两者不可同时使用,listData 优先级更高。
  • 模板直接写在 <qt-grid-view> 内部,不使用 <template #item> 插槽。
  • 每个模板根元素必须带 type 属性,数据项里也要有相同 type 字段,组件据此匹配模板。
  • 数据绑定使用 ${variable} 语法,不是 Vue 的 {{ }}。变量名与数据项字段名一一对应。
  • 开启点击事件需加 eventClick 属性,否则 @item-click 不会触发。


四、核心 Props

Prop类型默认值说明
listDataArray-推荐的数据驱动方式,配合 qtRef 使用可自动监听数组变化
spanCountNumber0网格列数(每行显示几个 item)
openPageBooleanfalse是否开启分页加载
loadMoreFunction-分页加载回调,参数为 pageNo(从 1 开始)
pageSizeNumber0每页数据量,用于控制 loading 显示时机
preloadNoNumber0预加载阈值,距离底部多少条时触发 loadMore
defaultFocusNumber-1默认聚焦的 item 索引
loadingDecorationObject{top:15, left:30}loading 项的 decoration(边距等)
blockFocusDirectionsArray[]阻止焦点方向,如 ['left','right']
areaWidthNumber1200列表区域宽度
enablePlaceholderBooleanfalse是否开启占位图(防止滚动闪烁)
listenBoundEventBooleanfalse是否监听 item bind 事件(开启预加载必须)
paddingString-内边距,格式 "left,top,right,bottom"
clipChildrenBooleantrue是否裁剪子元素超出部分(焦点放大需设为 false
useDiffBooleanfalse是否启用 diff 更新(局部更新场景使用)


五、事件

事件触发时机回调参数
item-clickitem 被点击(需 eventClick{ position, item, ... }
item-binditem 绑定到视图{ position, item, ... }
item-focuseditem 获取焦点(需 eventFocus{ position, item, hasFocus, ... }
item-unbinditem 解绑{ position, item, ... }
scroll列表滚动滚动事件对象
scroll-state-changed滚动状态变化状态事件对象


18

主题

6

回帖

112

积分

注册会员

积分
112
 楼主| 发表于 2026-7-22 22:30:19 | 显示全部楼层

六、模板与插槽

1. 普通 item 模板

通过 type 属性区分不同类型的 item,数据项的 type 字段与之匹配:

[code=vue]<qt-grid-view :list-data="listData" :spanCount="4">
  <!-- type=1:标准卡片 -->
  <qt-view type="1" :focusable="true" eventClick eventFocus>
    <qt-image src="${cover}" style="width: 260px; height: 360px;" />
    <qt-text text="${title}" :lines="1" :ellipsizeMode="3" />
  </qt-view>

  <!-- type=2:带角标的卡片 -->
  <qt-view type="2" :focusable="true" eventClick>
    <qt-image src="${cover}" />
    <qt-view showIf="${isVip}" class="vip-badge">
      <qt-text text="VIP" />
    </qt-view>
  </qt-view>
</qt-grid-view>[/code]

对应数据:

[code=ts]listData.value = [
  { type: 1, cover: '...', title: '普通卡片' },
  { type: 2, cover: '...', isVip: true }
][/code]

2. 内置 type(重要)

type 值用途设置方式
1002加载更多 loading通过 v-slot:loading 插槽
1003分页结束提示语直接在组件内放置带 type="1003" 的元素


[code=vue]<qt-grid-view penPage="true" :loadMore="loadMore" :pageSize="30">
  <!-- 普通 item -->
  <qt-view type="1" :focusable="true" eventClick>...</qt-view>

  <!-- loading 模板(type 必须是 1002) -->
  <template v-slot:loading>
    <qt-loading-view :type="1002" name="loading" color="#409eff"
      style="height: 40px; width: 40px;" :focusable="false" />
  </template>

  <!-- 分页结束提示(type 必须是 1003) -->
  <qt-text :type="1003" text="已经到底啦"
    style="width: 1920px; height: 100px;" gravity="center" :focusable="false" />
</qt-grid-view>[/code]

3. header / footer 插槽

[code=vue]<qt-grid-view :list-data="listData" :spanCount="5">
  <template v-slot:header>
    <qt-view type="1003" name="type0" :focusable="false">
      <qt-text text="${assetTitle}" />
    </qt-view>
  </template>

  <!-- 普通 item -->
  <qt-view type="1" :focusable="true" eventClick>...</qt-view>

  <template v-slot:footer>
    <!-- 底部固定内容 -->
  </template>

  <template v-slot:loading>
    <!-- loading -->
  </template>
</qt-grid-view>[/code]

七、分页加载

1. 配置分页

[code=vue]<qt-grid-view
  ref="gridViewRef"
  :list-data="listData"
  :spanCount="6"
  penPage="true"
  :pageSize="30"
  :preloadNo="5"
  :listenBoundEvent="true"
  :loadMore="loadMore"
  :loadingDecoration="{ left: 950, top: 20, bottom: 20 }"
>
  <qt-view type="1" :focusable="true" eventClick>
    <qt-text text="${text}" />
  </qt-view>

  <template v-slot:loading>
    <qt-loading-view :type="1002" name="loading" color="rgba(255,255,255,0.3)" />
  </template>

  <qt-text :type="1003" text="已经到底啦" gravity="center" :focusable="false" />
</qt-grid-view>[/code]

2. loadMore 回调

[code=ts]const loadMore = async (pageNo: number) => {
  const res = await fetchMoreData(pageNo)
  if (res.length > 0) {
    // 拼接到现有数据
    listData.value = listData.value.concat(res)
  } else {
    // 数据加载完毕,结束分页
    gridViewRef.value.stopPage(true)  // true: 显示 "已经到底啦" 提示
    // gridViewRef.value.stopPage()    // 不带提示
  }
}[/code]

3. 预加载触发时机

openPage=truelistenBoundEvent=true 时,组件在 item-bind 事件中判断:当绑定到倒数第 preloadNo 条数据时,自动调用 loadMore(pageNo+1)

八、组件方法

通过 ref 调用:

方法说明
init(data, isInit?)旧版数据初始化方式,返回响应式数组引用(推荐用 listData 替代)
stopPage(isTip?)结束分页,isTip=true 显示 "已经到底啦"
restartPage()重置分页,重新从第 1 页加载(仅 init 模式生效)
setItemFocused(pos)让指定位置 item 获取焦点
scrollToFocused(pos)滚动到并聚焦指定位置
setItemSelected(pos, bool)设置选中状态
scrollToSelected(pos, bool)滚动到并选中
setInitPosition(pos)设置初始位置
updateItemProps(pos, name, data)局部更新某个 item 的某个属性
insertItem(pos, data)在指定位置插入数据
scrollToPosition(pos)滚动到指定位置
scrollToTop()滚动到顶部


通过 qt 全局 API 调用(基于 sid)

[code=ts]// 给组件加 sid
<qt-grid-view sid="my_grid" ... />

// 通过 sid 调用
qt.gridView.scrollToIndex('my_grid', 0, 0)
qt.gridView.scrollToPosition('my_grid', 0, 0)[/code]

九、两种数据驱动方式对比

方式一:listData(推荐)

[code=ts]import { qtRef } from '@quicktvui/quicktvui3'

const listData = qtRef<QTListViewItem[]>()

// 初始化
listData.value = getData()

// 追加
listData.value = listData.value.concat(newData)

// 清空
listData.value = []

// 重置
listData.value = newData[/code]

特点:声明式、自动监听数组变化(push/splice/concat/pop 均可)。

方式二:init 函数(兼容旧版)

[code=ts]const gridViewRef = ref()
let listDataRec: Array<QTGridViewItem> = []

defineExpose({
  onESCreate() {
    const arr = getData()
    listDataRec = gridViewRef.value.init(arr)
  }
})

// 后续操作直接操作 listDataRec
listDataRec.push(newItem)
listDataRec.splice(0, 2)
listDataRec[0].title.text = '修改标题'  // 修改 item 字段[/code]

特点:命令式、返回响应式数组引用,需自行管理。适合需要精细控制单个 item 字段的场景。

十、数据操作示例

[code=ts]// push 单条
listDataRec.push(item)

// push 多条
listDataRec.push(...items)

// splice 删除
listDataRec.splice(2, 1)          // 从索引 2 开始删 1 条
listDataRec.splice(0)              // 清空

// splice 替换
listDataRec.splice(4, 2, ...newItems)

// splice 插入
listDataRec.splice(4, 0, ...newItems)

// concat 拼接
listDataRec = listDataRec.concat(newItems)

// pop 删除末尾
listDataRec.pop()[/code]

18

主题

6

回帖

112

积分

注册会员

积分
112
 楼主| 发表于 2026-7-22 22:31:26 | 显示全部楼层

十一、完整实战示例(带分页 + 多类型模板)

[code=vue]<template>
  <div class="page">
    <qt-grid-view
      class="grid_view"
      ref="gridViewRef"
      :clipChildren="false"
      :clipPadding="false"
      :enablePlaceholder="true"
      padding="0,0,0,20"
      :list-data="listData"
      :preloadNo="5"
      :loadMore="loadMore"
      :listenBoundEvent="true"
      penPage="true"
      :spanCount="6"
      :loadingDecoration="{ left: 950, top: 20, bottom: 20 }"
      :focusable="false"
      @item-click="onItemClick"
      @item-focused="onItemFocused"
    >
      <!-- 普通 item -->
      <qt-view type="1" :focusable="true" eventClick eventFocus
        style="width: 260px; height: 320px; background-color: transparent;">
        <qt-image src="${cover}" style="width: 260px; height: 260px; border-radius: 8px;" />
        <qt-text text="${title}" :lines="1" :ellipsizeMode="3"
          style="width: 260px; height: 60px; font-size: 28px; color: #fff;" />
      </qt-view>

      <!-- loading -->
      <template v-slot:loading>
        <qt-loading-view :type="1002" name="loading" color="rgba(255,255,255,0.3)"
          style="height: 40px; width: 40px;" :focusable="false" />
      </template>

      <!-- 分页结束 -->
      <qt-text :type="1003" text="已经到底啦" gravity="center" :focusable="false"
        style="width: 1920px; height: 100px; color: #999; font-size: 24px;" />
    </qt-grid-view>
  </div>
</template>

<script lang="ts" setup>
import { qtRef } from '@quicktvui/quicktvui3'
import type { QTListViewItem } from '@quicktvui/quicktvui3'

const gridViewRef = ref()
const listData = qtRef<QTListViewItem[]>()

function mockApi(num: number, flag = ''): Promise<any[]> {
  return new Promise((resolve) => {
    const arr = Array.from({ length: num }, (_, i) => ({
      type: 1,
      cover: 'https://img1.baidu.com/it/u=2666955302,2339578501&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=750',
      title: '标题 ' + i + flag,
      decoration: { left: 10, right: 20, bottom: 20 }
    }))
    setTimeout(() => resolve(arr), 800)
  })
}

async function loadMore(pageNo: number) {
  const arr = await mockApi(30, '-p' + pageNo)
  if (pageNo < 5) {
    listData.value = listData.value.concat(arr)
  } else {
    gridViewRef.value.stopPage(true)
  }
}

function onItemClick(e: any) {
  console.log('click:', e.position, e.item.title)
}

function onItemFocused(e: any) {
  console.log('focus:', e.position)
}

defineExpose({
  async onESCreate() {
    listData.value = await mockApi(30)
  }
})
</script>

<style scoped>
.page {
  width: 1920px;
  height: 1080px;
  background-color: transparent;
}
.grid_view {
  width: 1920px;
  height: 1080px;
  background-color: transparent;
}
</style>[/code]

十二、常见问题与注意事项

1. 焦点放大被裁剪

子元素使用 focusScale 时,父容器(包括 qt-grid-view 本身)必须设置 :clipChildren="false"

[code=vue]<qt-grid-view :clipChildren="false" :clipPadding="false">
  <qt-view type="1" :focusable="true" focusScale="1.1">...</qt-view>
</qt-grid-view>[/code]

2. listData 与 init 不可同时使用

listData 优先级高于 init 函数。一旦设置了 listDatainit 会被忽略。推荐统一使用 listData

3. 点击事件不触发

模板根元素必须加 eventClick 属性:

[code=vue]<qt-view type="1" :focusable="true" eventClick>...</qt-view>[/code]

聚焦事件需加 eventFocus

4. 模板根元素必须有 type 属性

type 是模板匹配的依据,数据项也必须包含相同 type 字段。

5. 数据绑定用 ${} 而非 {{ }}

[code=vue]<!-- 正确 -->
<qt-text text="${title}" />

<!-- 错误(Vue 模板语法不生效) -->
<qt-text :text="item.title" />[/code]

6. 内置 type 不要复用

1002(loading)、1003(分页结束提示)是内置 type,业务数据 type 不要与之冲突。

7. 分页结束调用 stopPage

数据加载完毕必须调用 gridViewRef.value.stopPage(true),否则 loading 会一直显示。

8. 样式属性命名规范

  • CSS class 选择器 / style 字符串:kebab-case + px 单位
  • :style 对象:camelCase + 无单位数字
  • 不支持 CSS 简写(如 margin: 10px


9. item 局部更新

[code=ts]// 更新单个 item 的字段
listData.value[0].title = '新标题'

// 或通过 API
gridViewRef.value.updateItemProps(0, 'title', { text: '新标题' })[/code]

10. 阻止焦点越界

当 grid 旁边有其他可聚焦区域时,可通过 blockFocusDirections 阻止焦点往特定方向移动:

[code=vue]<qt-grid-view :blockFocusDirections="['left','right']">[/code]

十三、小结

qt-grid-view 是 QuickTVUI 中最常用的网格列表组件,掌握以下几点即可覆盖大部分 TV 端网格场景:

  • 数据驱动优先用 listData + qtRef
  • 模板直接写在组件内部,用 type 区分多类型
  • 数据绑定用 ${variable} 语法
  • 分页靠 openPage + loadMore + stopPage 三件套
  • 焦点放大必须配合 clipChildren="false"
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|QuickTVUI论坛

GMT+8, 2026-8-24 13:34 , Processed in 0.112889 second(s), 27 queries .

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表