seat-engine
v0.2.0
Published
高性能在线选座引擎
Downloads
235
Maintainers
Readme
Seat Engine - 在线选座引擎
高性能在线选座引擎,支持十万级座位渲染,采用 Canvas + SVG 混合渲染技术。
特性
- 高性能渲染:支持十万级座位流畅渲染
- Canvas + SVG 混合:兼顾性能与交互体验
- 智能虚拟化:视口裁剪,按需渲染
- 模块化设计:支持 Git Submodule 和 NPM 包
- TypeScript:完整的类型支持
- 轻量级:最小化依赖
安装
npm install seat-engine如果你使用 pnpm 或 yarn:
pnpm add seat-engine
# 或
yarn add seat-engine安装前说明
seat-engine 依赖以下运行时包,请确保宿主项目已安装:
react >= 18.0.0react-dom >= 18.0.0@deck.gl/core >= 9.0.0@deck.gl/layers >= 9.0.0@deck.gl/react >= 9.0.0
推荐直接按下面方式安装:
npm install seat-engine react@^18 react-dom@^18 @deck.gl/core@^9 @deck.gl/layers@^9 @deck.gl/react@^9如果你使用 pnpm:
pnpm add seat-engine react@^18 react-dom@^18 @deck.gl/core@^9 @deck.gl/layers@^9 @deck.gl/react@^9如果你使用 yarn:
yarn add seat-engine react@^18 react-dom@^18 @deck.gl/core@^9 @deck.gl/layers@^9 @deck.gl/react@^9项目状态
- 当前以代码开源为主,仓库保持尽量精简
开源相关文档
快速开始
最小接入步骤:
- 安装
seat-engine和对应的 peer dependencies - 在页面中引入样式
import 'seat-engine/styles' - 给组件一个有高度的容器
- 通过
venueGeoData或loadVenueGeoData传入场馆数据
下面是一个最小可运行示例:
import { SeatSelector } from 'seat-engine';
import 'seat-engine/styles';
function App() {
return (
<div style={{ width: '100%', height: '600px' }}>
<SeatSelector
loadVenueGeoData={() => fetch('/api/venue/venue-001').then(res => res.json())}
onSeatSelect={seat => console.log('选中座位:', seat)}
onSelectionChange={seats => console.log('已选座位:', seats)}
/>
</div>
);
}npm 使用建议
- 推荐在 React 18+ 项目中使用
- 推荐通过 ESM 方式引入
- 样式不是自动注入的,请显式引入
seat-engine/styles - 场馆数据量较大时,建议通过异步接口按需加载
- 正式环境建议由服务端提供座位状态和价格,前端只负责展示与交互
模块引入方式
ESM(推荐)
import { SeatSelector } from 'seat-engine';
import 'seat-engine/styles';CJS
const { SeatSelector } = require('seat-engine');
require('seat-engine/styles');浏览器直接引用(dist)
构建后提供以下产物:
dist/esm:<script type="module">直接使用dist/umd:传统<script>引用
ESM 方式
注意:需要自行引入 react、react-dom、@deck.gl/* 的 CDN 版本。
<link rel="stylesheet" href="dist/esm/styles.css" />
<script type="module">
import { SeatSelector } from './dist/esm/index.js';
</script>UMD 方式
注意:需要先加载 react、react-dom、@deck.gl/* 的 UMD 版本。
<link rel="stylesheet" href="dist/umd/styles.css" />
<script src="dist/umd/index.umd.js"></script>
<script>
const { SeatSelector } = window.SeatEngine;
</script>依赖说明
seat-engine 依赖以下:
reactreact-dom@deck.gl/core@deck.gl/layers@deck.gl/react
数据接入方式
支持两种方式,二选一:
方式一:直接传入数据
适合数据已经在页面初始化阶段拿到的场景。
import { SeatSelector, type VenueGeoData } from 'seat-engine';
import 'seat-engine/styles';
const venueGeoData: VenueGeoData = {
sectionsGeoJson: {
type: 'FeatureCollection',
features: [],
},
seatsGeoJson: {
type: 'FeatureCollection',
features: [],
},
};
export function App() {
return (
<div style={{ width: '100%', height: '600px' }}>
<SeatSelector venueGeoData={venueGeoData} />
</div>
);
}方式二:异步加载数据
适合按场馆 ID 请求接口、切换场次或延迟加载的场景。
import { SeatSelector } from 'seat-engine';
import 'seat-engine/styles';
export function App() {
return (
<div style={{ width: '100%', height: '600px' }}>
<SeatSelector
loadVenueGeoData={() => fetch('/api/venue/venue-001').then(res => res.json())}
/>
</div>
);
}发布到 npm 后用户最常见的坑
- 没有给组件容器高度,导致看起来像“没渲染”
- 忘记引入
seat-engine/styles - 没有安装
react、react-dom、@deck.gl/*这些 peer dependencies - 后端返回的数据字段不符合
VenueGeoData结构 - 把座位状态、价格逻辑完全放在前端,导致正式环境状态不一致
开发检查
日常提交建议执行:
pnpm lint
pnpm type-check发布前建议额外执行:
pnpm buildSeatSelector API
基础用法
<SeatSelector
loadVenueGeoData={() => fetch('/api/venue/venue-001').then(res => res.json())}
onSeatSelect={seat => console.log('selected', seat)}
/>必填/数据输入
二选一:
venueGeoData?: VenueGeoData
直接传入 GeoJSON 数据(推荐,最灵活)。loadVenueGeoData?: () => Promise<VenueGeoData>
异步加载数据,返回VenueGeoData。
若两者都未提供,组件会显示错误提示。
Props
| Prop | 类型 | 说明 | 默认值 |
| --------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| locale | 'zh-CN' \| 'en-GB' \| 'en-US' | UI 文案语言 | zh-CN |
| messages | Partial<I18nStrings> | 覆盖默认文案 | — |
| maxSelections | number | 最大可选座位数 | Infinity |
| theme | { colors?: { available?; selected?; sold?; unavailable?; locked? } } | 状态颜色覆盖 | — |
| seatSize | { width: number; height: number } | 座位默认尺寸(世界坐标单位) | — |
| seatIconAtlas | string | 自定义座位图标图集,支持图片 URL 或 SVG data URL | 内置座位图标 |
| seatIconMapping | Record<string, IconMappingValue> | 自定义图标映射,key 与 getSeatIcon 返回值对应 | 内置 seat 映射 |
| getSeatIcon | (feature: SeatGeoFeature) => string | 根据座位数据返回图标 key | () => 'seat' |
| worldScale | number | 世界坐标缩放比例(不影响座位尺寸) | 1 |
| viewPadding | number | 自动居中边距(像素) | 20 |
| seatPickingRadius | number \| { mouse?: number; touch?: number } | 座位点击容错半径(像素),适合提升移动端可点性 | 桌面 6,触摸设备 24 |
| showSectionNavigator | boolean | 是否显示区域导航控件 | true |
| sectionFocusSeatPixelSize | number | 点击区域导航后座位至少放大到的像素尺寸 | 28 |
| rotationConfig | RotationConfig | 旋转解释(角度语义近似 AutoCAD,Y 轴默认向下) | { unit:'deg', positiveDirection:'ccw', zeroDirection:'+x', offsetDeg:0, yAxisDirection:'down' } |
| sectionLabelLayerStyle | SectionLabelLayerStyle | 分区标签图层样式覆盖 | — |
| seatLabelLayerStyle | SeatLabelLayerStyle | 座位标签图层样式覆盖 | — |
| onSeatSelect | (seat: SeatSelectionInfo) => void | 选中回调 | — |
| onSeatDeselect | (seat: SeatSelectionInfo) => void | 取消选中回调 | — |
| onSelectionChange | (seats: SeatSelectionInfo[]) => void | 选中集合变化回调 | — |
| renderSeatInfo | (props: SeatInfoRenderProps) => ReactNode | 自定义选座信息 UI(替代默认面板) | — |
| className | string | 外层容器类名 | — |
| style | CSSProperties | 外层容器样式 | — |
| disabled | boolean | 禁用选座并隐藏选座信息面板 | false |
自定义座位图标
默认座位图标是一个可着色的矩形 SVG。如果需要区分普通座、VIP 座、无障碍座等,可以通过 seatIconAtlas、seatIconMapping 和 getSeatIcon 自定义图标。
seatIconMapping 的 key 必须和 getSeatIcon 返回值一致;如果 getSeatIcon 没有返回值,组件会默认使用 seat。
import { SeatSelector, type SeatGeoFeature } from 'seat-engine';
import 'seat-engine/styles';
const seatIconAtlas = `data:image/svg+xml;utf8,${encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="64" viewBox="0 0 128 64">
<rect x="0" y="0" width="64" height="64" rx="10" fill="white" />
<circle cx="96" cy="32" r="28" fill="white" />
</svg>
`)}`;
const seatIconMapping = {
seat: { x: 0, y: 0, width: 64, height: 64, mask: true, anchorX: 32, anchorY: 32 },
vip: { x: 64, y: 0, width: 64, height: 64, mask: true, anchorX: 32, anchorY: 32 },
};
export function App() {
return (
<div style={{ width: '100%', height: '600px' }}>
<SeatSelector
loadVenueGeoData={() => fetch('/api/venue/venue-001').then(res => res.json())}
seatIconAtlas={seatIconAtlas}
seatIconMapping={seatIconMapping}
getSeatIcon={(feature: SeatGeoFeature) =>
feature.properties.seatType === 'vip' ? 'vip' : 'seat'
}
/>
</div>
);
}类型定义
VenueGeoData(组件数据输入)
interface VenueGeoData {
sectionsGeoJson: SectionGeoJson;
seatsGeoJson: SeatGeoJson;
metadata?: {
version?: string;
createdAt?: string;
updatedAt?: string;
generator?: string;
};
}SectionGeoJson(区域几何数据)
type SectionGeoJson = FeatureCollection<SectionGeoProperties, Polygon | MultiPolygon>;
interface SectionGeoProperties {
sectionId: string;
name: string;
fillColor?: string;
}SeatGeoJson(座位几何数据)
type SeatGeoJson = FeatureCollection<SeatGeoProperties, Point>;
interface SeatGeoProperties {
seatId: string; // 唯一标识
seatLabel?: string; // 展示用座位号
sectionId: string;
status: 'available' | 'sold' | 'unavailable' | 'locked';
price?: number;
seatType?: string;
rotation?: number;
width?: number;
height?: number;
rowLabel?: string;
colLabel?: string;
iconKey?: string;
}SeatSelectionInfo(回调参数)
interface SeatSelectionInfo {
seatId: string;
seatLabel?: string;
sectionId: string;
sectionName?: string;
price?: number;
status?: 'available' | 'sold' | 'unavailable' | 'locked';
seatType?: string;
rowLabel?: string;
colLabel?: string;
rotation?: number;
}SeatInfoRenderProps(自定义选座信息 UI 参数)
interface SeatInfoRenderProps {
selectedSeats: SeatSelectionInfo[];
maxSelections: number;
highlightedSeatId?: string | null;
onSeatItemClick: (seat: SeatSelectionInfo) => void;
onSeatRemove: (seat: SeatSelectionInfo) => void;
locale?: 'zh-CN' | 'en-GB' | 'en-US';
messages?: Partial<I18nStrings>;
}RotationConfig(旋转解释)
interface RotationConfig {
unit?: 'deg' | 'rad';
positiveDirection?: 'cw' | 'ccw';
zeroDirection?: '+x' | '+y' | '-x' | '-y';
offsetDeg?: number;
yAxisDirection?: 'up' | 'down';
}各参数控制说明:
unit: 输入角度的单位。deg表示输入就是度;rad会先转成度。positiveDirection: 输入“正角”的方向语义。ccw表示正角逆时针;cw表示正角顺时针(内部会取反)。zeroDirection: 输入0°对应哪条轴。可选:+x/+y/-x/-y。offsetDeg: 在上述换算后再统一加一个偏移角(度),用于整体微调。yAxisDirection: 世界坐标里y的方向定义(影响几何坐标,不只是图标角度)。up:y越大越靠上(数学/CAD 习惯)。down:y越大越靠下(屏幕坐标习惯)。
默认值:
{ unit: 'deg', positiveDirection: 'ccw', zeroDirection: '+x', offsetDeg: 0, yAxisDirection: 'down' }- 说明:默认
yAxisDirection是down,这一点与传统 AutoCAD 世界坐标(Y 向上)不同。
SectionLabelLayerStyle(分区标签图层样式)
interface SectionLabelLayerStyle {
getSize?: number; // 基准字号(zoom=1),放大时会随缩放增大,默认 16
getColor?: [number, number, number, number]; // 默认 [31, 41, 55, 255]
fontFamily?: string; // 默认 'Inter, SF Pro Text, PingFang SC, Microsoft YaHei, sans-serif'
fontWeight?: string | number; // 默认 '400'
getTextAnchor?: 'start' | 'middle' | 'end'; // 默认 'middle'
getAlignmentBaseline?: 'top' | 'center' | 'bottom'; // 默认 'center'
}SeatLabelLayerStyle(座位标签图层样式)
interface SeatLabelLayerStyle {
getColor?: [number, number, number, number]; // 默认 [51, 51, 51, 255]
fontFamily?: string; // 默认 'Inter, SF Pro Text, PingFang SC, Microsoft YaHei, sans-serif'
fontWeight?: string | number; // 默认 '400'
getTextAnchor?: 'start' | 'middle' | 'end'; // 默认 'middle'
getAlignmentBaseline?: 'top' | 'center' | 'bottom'; // 默认 'center'
}GeoJSON 最小结构
sectionsGeoJson
FeatureCollection<Polygon|MultiPolygon>properties.sectionIdproperties.nameproperties.fillColor?
seatsGeoJson
FeatureCollection<Point>properties.seatId(唯一标识)properties.seatLabel?(显示用座位号)properties.sectionIdproperties.status(available/sold/unavailable/locked)
FeatureCollection 说明
Seat Engine 使用 GeoJSON RFC 7946 中的 FeatureCollection 描述场馆区域与座位数据。
- FeatureCollection 由
type: "FeatureCollection"和features数组组成;每个 Feature 包含geometry(几何)和properties(属性)。 - sectionsGeoJson:
FeatureCollection,几何类型为Polygon或MultiPolygon,表示看台/区域轮廓;properties需包含sectionId、name,可选fillColor。 - seatsGeoJson:
FeatureCollection,几何类型为Point,表示座位中心点;properties需包含seatId、sectionId、status,可选seatLabel、price、rotation、width、height、rowLabel、colLabel等。
与类型对应关系:SectionGeoJson = FeatureCollection<SectionGeoProperties, Polygon | MultiPolygon>,SeatGeoJson = FeatureCollection<SeatGeoProperties, Point>。
License
MIT
