更新记录
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 做全量、方法做增量。
seatlistprop 变化是全量替换语义(整馆重建); 分批叠加/单分区刷新请走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)。

收藏人数:
下载插件并导入HBuilderX
下载示例项目ZIP
赞赏(0)
下载 233
赞赏 1
下载 12641179
赞赏 1950
赞赏
京公网安备:11010802035340号