更新记录

1.1.2(2026-09-04) 下载此版本

  • 组件零 performance 依赖:内部计时(点击反馈动画 / sync 计时 / 定位动画 / 构建耗时统计)统一 Date.now(), 删除环境探测守卫——iOS 小程序 JSCore 无 performance 全局,组件从此不存在触发 Can't find variable: performance 的任何可能(毫秒精度对百毫秒级动画与耗时统计足够)。

  • 示例页错误处理收敛:页面不再显示任何错误提示(不遮挡用户操作),组件 error 事件统一 console.error 输出完整上下文([code] stage:message(venue_id @ 时间) + 原始错误对象); 页面处理函数命名 onSeatmapError, 避免与 App.onError 生命周期同名

1.1.1(2026-09-03) 下载此版本

修复拖动座位图时整个页面跟着上下滚动的问题(双端):

  • H5(iOS Safari/微信浏览器):画布容器补 touch-action: none + overscroll-/code> (合成器层面禁掉浏览器滚动手势——leafer 只在 touchstart 上 preventDefault, touchmove 是 window 级 passive 监听,拦不住 iOS 滚动链);
  • 微信小程序:canvas 触摸事件 bind → catch(catchtouch*)拦截冒泡——bind 只监听不拦截, 页面恰好可滚动的设备(视口/安全区差异)上手势会冒泡触发页面滚动;
  • 示例页 pages.json 补 "disableScroll": true。接入方选座页建议同样设置,详见 README 注意事项。

1.0.8(2026-08-21) 下载此版本

  • 修复 H5 接入页误调用小程序专用 init() 时,组件错误查询 #sp-canvas 并报“未找到 canvas 节点”的问题:示例页移除无条件兜底调用;H5 端由组件挂载后自动初始化。
  • 优化 H5 容器尺寸错误提示:明确要求为外层页面建立确定高度,并让座位图区域使用 flex:1; min-height:0;不再建议直接给 <seatmap-picker> 写 height:100vh,避免页面存在头部或底栏时溢出。
查看更多

平台兼容性

uni-app

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

其他

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

seatmap-picker 座位图选座组件(uni-app 插件)

Leafer 画布实现的只读座位图选座组件,支持 H5 与 微信小程序。与设计器同源数据模型,跨场馆座位尺寸一致。

安装

把 uni_modules/shuai-seatmap-picker 整个目录拷入你的 uni-app 项目 src/uni_modules/ 下(HBuilderX 可直接导入插件)。

组件按 easycom 自动注册,无需手动 import。

npm 依赖:插件的 Leafer 运行时依赖需安装在宿主项目的 node_modules:

npm i leafer-ui @leafer-ui/miniapp @leafer-in/view @leafer-in/viewport @leafer-in/animate

快速使用

<template>
    <!-- 页面容器必须有确定高度;头部、底部栏可作为同级元素加入。 -->
    <view class="picker-page">
        <view class="map-slot">
            <seatmap-picker
                :venue="venueMeta"
                :seatlist="seatlist"
                :max-seats="6"
                @selection-change="ion"
                @limit-reached="onLimit"
            />
        </view>
    </view>
</template>

<script>
import { ref } from 'vue'

export default {
    setup() {
        const venueMeta = ref(null)
        const seatlist = ref(null)
        // 接入方自行从后端拉取数据,原始数据直接传给组件,
        // 归一化(fromBackend)在组件内部完成,无需 import 插件任何模块
        // (示例工程的 pages/index/index.vue 有完整请求封装)
        venueMeta.value = venueData          // 场馆主体
        seatlist.value = seatlistData        // 座位列表
        function ion(list) { /* list: [座位记录],字段与后端 seatlist 对齐 + name/price,见下方"选中返回结构" */ }
        function onLimit(limit) { /* 限购拦截 */ }
        return { venueMeta, seatlist, ion, onLimit }
    },
}
</script>

<style>
.picker-page {
    height: 100vh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.map-slot {
    flex: 1;
    min-height: 0;
    width: 100%;
    position: relative;
    overflow: hidden;
}
</style>

组件会自动填满 .map-slot,无需在 <seatmap-picker> 上写 height:100vh。若页面已有固定高度的父容器,.picker-page 可改为 height:100%;此时需保证 html、body、uni-app、uni-page-wrapper、uni-page-body 到页面根节点的高度链均为 100%。

不要在 display:none、未激活的 tab / popup 中初始化组件;应在容器显示后再挂载组件,否则无法取得有效尺寸。

Props

名称 类型 默认 说明
venue Object null 场馆主体:后端原始场馆对象(推荐)、fromBackend 后的模型、或 { venue, seatlist } 组合形态;变化时整馆重渲染
seatlist Array null 座位列表,与 venue 分离传递:先后到达皆可(内部各自缓存、合并重建)。先传 venue 再传 seatlist = 渐进加载;同时赋值 = 一次渲染
activeCat String/Number '' 座位类别筛选:传分类 key 后不匹配座位不渲染、无该类别座位的售票分区轮廓灰化、不匹配的已选座位自动取消;'' = 全部
maxSeats Number 6 限购上限(0 = 不限购)
seatMaxPx Number 0 座位最大屏幕边长覆盖(px,默认 0 = 18 = 12+6);跨场馆"放大到底"尺寸一致
debug Boolean false 调试模式:渲染耗时/可见座位等门控日志进 console;耗时统计与非致命错误始终收集,getDebugInfo() 拉取(见「调试模式」)

渲染像素比组件内部固定 min(设备DPR, 2):高清屏 DPR 3+ 时 backing store 按 DPR² 膨胀、拖动帧成本翻 4~12 倍,封顶后降 ~68%,纯色座位图无视觉差异。该参数不对接入方开放。

Events

名称 参数 说明
selection-change [座位记录] 选中变化(选中/取消/清空都会触发)。字段与后端 seatlist 对齐 + name/price,与 getSelection() 同构,见下方「选中返回结构」
seat-click { seat, action, reason? } 用户点座位动作(与 selection-change 状态同步双轨):action = select 选中 / deselect 取消 / blocked 被拦截(reason = limit 限购 / unavailable 不可售)。埋点/震动反馈/详情浮层用,无需 diff 新旧选中集
limit-reached (limit) 达到上限后再点座位的拦截回调
error ({ code, stage, message }) 初始化/渲染失败(结构化载荷,message 为可读文案,渲染类错误带 场馆渲染失败: 前缀)

error 事件的两个实践建议(示例工程 pages/index 已按此实现):

  • 与小程序 App.onError 不冲突:组件自定义事件作用域在组件实例,和全局生命周期是两套机制;但页面处理函数建议命名 onSeatmapError 而非 onError,避免与生命周期同名误读。
  • 错误不要在页面上显示(不遮挡用户操作):统一 console.error 输出完整上下文([code] stage:message(venue_id @ 时间) + 原始错误对象),开发者工具控制台即可分析;生产环境需要远程收集时再接 wx.getRealtimeLogManager 或自建上报。若界面上出现无前缀的裸报错文本(如 Can't find variable: xxx),那不是组件 error 事件的内容,而是宿主页面自行渲染的——请全局搜索宿主工程代码定位来源。

选中返回结构

selection-change 与 getSelection() 返回相同的座位记录数组,每条记录字段与后端 seatlist 记录对齐,另附展示与价格字段:

{
  id: "s_xxx",           // 座位 id(与后端一致)
  ven_id: "venue_xxx",   // 场馆 id
  sec_id: "sec_xxx",     // 分区 id
  row_id: "r_xxx",       // 排 id
  cat_id: 1,             // 类别 key(后端数字码,未分类为 0)
  label: "12",           // 座位号(后端 label 字段)
  x: 4.21, y: -0.01,     // 相对排原点的局部坐标(后端口径)
  status: 1,             // 后端状态码(1 可选 / 2 已订 / 3 保留 / 0 禁用)
  type: 1,               // 座位类型
  name: "看台101区A排12座", // 展示串:区排座(拼接规则见下)
  price: 380,            // 按类别取价;未分类/无对应类别为 null
}

name 展示串拼接规则:分区名 + 排号 + 座位号 连写(中文票务惯例,无分隔符),各部分仅在存在时拼接,全空回退「未分区座位」。 分区名做单位后缀去重:名字已以单位字结尾(区/场/台/楼/层/厅/厢)就直接使用,否则补「区」—— 7区 → 7区12排5座(不会叠加成「7区区」)、7 → 7区12排5座、内场 → 内场12排5座(不补区)、VIP看台 → VIP看台12排5座。

设计师侧建议:分区命名可自带单位(如「东看台」「VIP区」),也可只写编号(如「7」),展示效果都正确。

坐标已换算为相对排原点的局部坐标、status 已是后端数字码——与后端接口口径一致,直接用于订单提交即可,无需再按内部模型转换。

调试模式

传 debug prop(或 createSeatPicker({ debug: true }))开启:

  • 门控日志进 console(H5 浏览器 / 小程序开发者工具):每次场馆加载打一条多行总览(场馆名 / 规模 分区-排-座位 / 首屏可用时间,见下方示例),不再逐帧打印;另含 getSelection 选中等、点击命中诊断(命中的区/排/座 + 是否被不可售/类别筛选/限购拦截);逐帧 syncView 耗时累计进 getDebugInfo().timing(syncCount / syncAvgMs / syncMaxMs);
  • 客户端与 console 同口径:总览日志 = getDebugReportText() = formatDebugInfo(getDebugInfo()) 的格式化视图,同一数据源不会漂移;客户端要展示同样的文本直接调 getDebugReportText(),要结构化数据调 getDebugInfo();
  • 渲染耗时统计与单分区/单块非致命错误始终收集(不依赖 debug 开关),经 getDebugInfo() 拉取:
const info = pickerEl.value.getDebugInfo()
// {
//   debug: true,
//   stats: { visibleSeats, scale },                      // 当前帧可见座位数 / 缩放
//   venue: {                                              // 场馆规模 + 数据分布(仅 debug 开启时统计)
//     sectionCount, rowCount, seatCount, baseScale, name,
//     byStatus,     // 座位状态分布:{ available: n, sold: n, ... }(内部字符串 key)
//     byCat,        // 类别分布:{ '1': n, '2': n, null: n }
//   },
//   timing: {
//     buildMs,        // 整馆同步构建耗时(分区/网格/缩放)
//     chunkMs,        // 座位节点分帧铺设总耗时
//     chunkFrames,    // 分帧铺设帧数
//     revealMs,       // 首次显示:铺完后视口内座位切可见的耗时
//     firstSyncMs,    // 首次视口同步耗时
//     syncCount, syncAvgMs, syncMaxMs, // 视口同步会话级统计
//     normalize,      // 组件侧 fromBackend 归一化耗时(reportTiming 打点)
//     ttfMs,          // 首屏可用时间 = normalize + buildMs + chunkMs + revealMs(数据到位→座位显示)
//   },
//   errors: [{ code, stage, message, sectionId? }],      // 非致命渲染错误(封顶 50 条)
// }

控制台总览(debug 开启时每次加载打印,与 getDebugReportText() 逐字一致):

[seatmap][debug] ── 场馆加载 ──
  场馆: 济南奥体中心体育场
  规模: 分区 37 / 排 58 / 座位 2529
  场馆渲染成功: 1077.1ms(从数据就绪到座位全部渲染完成,可见可点)
    数据归一化(后端格式→内部模型): 227ms
    场馆构建(分区轮廓/网格/缩放): 97ms
    座位分帧铺设(逐帧创建座位节点): 753ms / 20帧

小程序真机 console 不可见时,可在页面里调用 getDebugReportText() 拿到同款文本展示(如开发态长按弹调试面板)。 单分区/单块渲染失败不会拖垮整图(自动跳过并记入 errors);error 事件承载的是初始化/重建的致命错误。

组件方法(ref 调用)

方法 说明
setVenue(v) 全量替换场馆数据并重渲染(v 同 venue prop;{ venue, seatlist } 形态座位一并更新,否则清空座位)
getSelection() 获取选中座位记录数组(与 selection-change 同构)
getDebugInfo() 调试报告快照:场馆规模 + 渲染耗时 + 非致命错误列表(结构化对象,见「调试模式」)
getDebugReportText() 调试报告格式化为控制台同款可读多行文本(与 getDebugInfo 同一数据源)
clearSelection() / deselectSeat(id) 清空选中 / 取消选中单个座位(下方已选列表点 × 删除用)
updateSectionSeats(sectionId, seats) 按分区批量刷新座位状态(并发购票场景)
setActiveCat(cat) 座位类别筛选(同 active-cat prop 的命令式入口),'' = 全部
getVisibleSectionIds() 当前视口可见分区 id(用于按需刷新)
zoomFit() 缩回全景
init() 小程序画布节点就绪后的兜底初始化

数据更新方式

场景 入口 行为
首次加载(一步到位) :venue + :seatlist 同时赋值 合并后一次整馆渲染
渐进加载(先骨架后座位) 先赋 :venue,再赋 :seatlist 先渲染分区骨架,座位到达后并入重渲染
换场馆 更新 :venue(保留 :seatlist)或 setVenue(v) 整馆重建
单分区座位状态刷新(已售/锁定轮询) updateSectionSeats(sectionId, seats) 局部更新:不动视口、不动其他分区、不重渲整馆

原则:prop 做全量、方法做增量。seatlist prop 变化是全量替换语义(整馆重建); 分批叠加/单分区刷新请走 updateSectionSeats,不要依赖 prop 做部分更新。

完整示例

插件本身只含组件与运行时(不含任何页面——页面布局、价格图例、已选栏、接口请求均由接入方按自己的业务实现)。 插件市场详情页可下载示例工程,其中的 pages/index/index.vue 是带价格图例、底部已选栏、限购提示的完整选座页, 演示了组件的全部 props / events / ref 方法,可直接参考或拷贝改造。

内部结构

uni_modules/shuai-seatmap-picker/
├── index.js                 # 根入口(转发 js_sdk;仅直引渲染器等进阶用法需要,常规接入无需 import)
├── components/seatmap-picker/seatmap-picker.vue   # 选座组件(easycom 注册为 <seatmap-picker>)
└── js_sdk/seatmap/          # 组件运行时(组件内部引用;直引渲染器时可单独使用)
    ├── index.js             # 公共入口:createSeatPicker / fromBackend / sectionDisplayName 一处导出
    ├── render.js            # Leafer 渲染器(视口裁剪 + 点选 + 点按缩放)
    ├── leafer-loader.js     # 平台分流(#ifdef H5 / MP-WEIXIN 导入对应 Leafer 包)
    └── core/                # 共享数据模型(fromBackend / model / geometry / path / color / text / constants)

在组件里用 canvas 还有哪些注意点?

  • performance is not defined(iOS 小程序):组件自身零 performance 依赖(计时全部用 Date.now()),不会触发此报错。若接入后仍看到该报错,说明宿主工程里存在旧版组件源码或其它裸调 performance.now() 的代码(小程序 JSCore 尤其 iOS 没有 performance 全局),请在宿主工程全局搜索 performance.now 定位清除。
  • 页面滚动穿透:H5 画布区域已内置 touch-action: none + overscroll-/code>;小程序端 canvas 触摸事件全部用 catchtouch* 拦截冒泡(bind 只监听不拦截,页面恰好可滚动的设备——视口/安全区差异——上拖座位图会带着页面滚)。接入方需保证选座页自身无页面级滚动需求(推荐 pages.json 里该页 "disableScroll": true,或页面固定 100vh 布局)——若页面整体可滚动,画布之外区域的滚动不受组件控制。
  • id 冲突:canvas 的 id 在页面内必须唯一。本组件当前为固定 id="sp-canvas",同一页面放一个实例没问题;如需多实例,请自行改为动态 id。
  • 避免频繁销毁重建:不要对组件高频 v-if 切换,原生组件重建开销大且有闪烁;数据变化请用 setVenue / updateSectionSeats 等方法增量刷新。
  • 同层渲染的适用范围:仅对 canvas type="2d"、video 等新版接口生效;旧版 canvas-id 那套接口及 live-player 等原生组件的层级坑仍在。
  • minimap 是第二个独立 canvas:小程序端的小地图使用独立的 #sp-minimap 节点(CSS 宽 130px、高 157px:130px 小地图 + 5px 间隔 + 76×22px 画布内返回按钮,与主画布平级绝对定位)。曾合并渲染在主 canvas 的 sky 图层中,但微信平台限制节点 canvas 无法 drawImage 离屏 canvas(iOS 基础库 3.0.0+ 及新版开发者工具抛 Cannot mix different types of Canvas),故小程序端整体弃用 Leafer App 多层合成:主画布改为单 Leafer 直渲,小地图改为独立 canvas 裸 2D 绘制(与 H5 同路径)。同一页面多实例时请同时保证 #sp-canvas 与 #sp-minimap 的 id 唯一。
  • minimap 显隐:仅首次加载、场馆数据尚未完成时隐藏;首次显示后即使回到全景也始终保持显示,避免缩放或定位时闪烁。缩略图底部的画布内「返回全局预览」按钮一键回到全景。小程序端显隐用 0↔130×157px 尺寸切换实现——不要用 v-show/display:none 控制 canvas 节点(隐藏节点可能取不到 canvas node)。

隐私、权限声明

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

无

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

插件不采集任何数据。组件为纯前端渲染,venue 数据由接入方自行从后端拉取;示例页中的接口地址需替换为接入方自己的后端

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

无

许可协议

MIT协议

暂无用户评论。