| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- import CanvasOperator from './canvasOperator.js'
- import CanvasRenderer from './canvasRenderer.js'
- import SeatCalculator from './canvasCalculator.js'
- import seatmapConfig from './config.js'
- import { StaticUrl } from '@/config'
- export default class seatmap {
- constructor({ canvas, seatList, options }) {
- this.ctx = canvas.getContext('2d')
- this.seatList = seatList
- this.options = this._deepMerge(seatmapConfig, options)
- this.checkOptions()
- this.operator = new CanvasOperator(canvas)
- this.renderer = new CanvasRenderer(this.ctx, this.options)
- this.calc = new SeatCalculator(this.seatList, this.options)
- // 运行时状态
- this.cellSize = 0
- this.panX = 0
- this.panY = 0
- this.boundaries = {}
- this.imageScreen = null
- this.imageGou = null
- this._drawing = false
- this._rafId = null
- this.seatRow = 0
- this.seatCol = 0
- this.miniMapShow = false
- this.firstClick = true
- this.init()
- }
- checkOptions() {
- const { SEAT_FIELDS, canvasWidth, canvasHeight, footerHeight, SEAT_STYLE, areaList } = this.options
- if (!SEAT_FIELDS.ROW_ID || typeof SEAT_FIELDS.ROW_ID !== 'string')
- throw new Error(`options.SEAT_FIELDS.ROW_ID 配置不正确`)
- if (!SEAT_FIELDS.COLUMN_ID || typeof SEAT_FIELDS.COLUMN_ID !== 'string')
- throw new Error(`options.SEAT_FIELDS.COLUMN_ID 配置不正确`)
- if (!SEAT_FIELDS.STATUS_NAME || typeof SEAT_FIELDS.STATUS_NAME !== 'string')
- throw new Error(`options.SEAT_FIELDS.STATUS_NAME 配置不正确`)
- if (!SEAT_FIELDS.TYPE_NAME || typeof SEAT_FIELDS.TYPE_NAME !== 'string')
- throw new Error(`options.SEAT_FIELDS.TYPE_NAME 配置不正确`)
- if (!SEAT_FIELDS.AREA_ID || typeof SEAT_FIELDS.AREA_ID !== 'string')
- throw new Error(`options.SEAT_FIELDS.AREA_ID 配置不正确`)
- if (!canvasWidth || canvasWidth <= 0)
- throw new Error(`options.canvasWidth 必须大于0`)
- if (!canvasHeight || canvasHeight <= 0)
- throw new Error(`options.canvasHeight 必须大于0`)
- if (footerHeight < 0)
- throw new Error(`options.footerHeight 不能为负`)
- const validateSeatStyle = SEAT_STYLE => ['DISABLED', 'LOCKED', 'SOLD', 'AVAILABLE', 'SELECTED'].every(prop =>
- SEAT_STYLE[prop]?.fillStyle !== undefined && SEAT_STYLE[prop]?.strokeStyle !== undefined,
- )
- if (validateSeatStyle(SEAT_STYLE) === false)
- throw new Error(`options.SEAT_STYLE 配置不正确,每个状态都需要配置 fillStyle 和 strokeStyle`)
- areaList.forEach((item) => {
- if (item[SEAT_FIELDS.AREA_ID] === undefined)
- throw new Error(`options.areaList 中缺少 AREA_ID 字段`)
- if (item.strokeStyle === undefined)
- throw new Error(`options.areaList 中缺少 strokeStyle 字段(颜色)`)
- })
- }
- async init() {
- const { canvasWidth, canvasHeight, footerHeight } = this.options
- // 加载图片
- // this.imageScreen = await this.operator.loadImage(`${StaticUrl}/film-choose-icon.png`)
- // this.imageGou = await this.operator.loadImage(`${StaticUrl}/film-choose-icon.png`)
- this.imageScreen = await this.operator.loadImage(`${StaticUrl}/film-hall.png`)
- this.imageGou = await this.operator.loadImage(`${StaticUrl}/film-gou-old.png`)
- // 初始布局
- this.cellSize = this.calc.calcCellSize(canvasWidth)
- const { seatRow, seatCol } = this.calc.calcSeatRowCol()
- const { panX, panY } = this.calc.calcInitialPan(canvasWidth, canvasHeight, this.cellSize)
- this.boundaries = this.calc.getBoundaries(canvasWidth, canvasHeight, footerHeight, this.cellSize)
- this.seatRow = seatRow
- this.seatCol = seatCol
- this.panX = panX
- this.panY = panY
- this.requestDraw()
- }
- /* -------------------- 座位图绘制 -------------------- */
- requestDraw() {
- if (this._drawing)
- return
- this._drawing = true
- this._rafId = this.operator.raf(() => {
- const { panX, panY, cellSize, seatRow, seatCol, seatList, imageGou, imageScreen, miniMapShow } = this
- const { title, canvasWidth, hallHeight, areaColorMap, hallName } = this.options
- this.renderer.clear(0, 0, this.options.canvasWidth, this.options.canvasHeight)
- this.renderer.drawSeatMap({ panX, panY, cellSize, seatRow, seatCol, seatList, areaColorMap, imageGou })
- this.renderer.drawCenterLine({ panX, panY, cellSize, seatRow, seatCol, title })
- this.renderer.drawScreen({ panX, panY, cellSize, seatRow, seatCol, imageScreen, hallName, canvasWidth, hallHeight })
- this.renderer.drawRowLabels({ panX, panY, cellSize, seatRow, seatCol, seatArray: this.calc.seatArray })
- this.renderer.drawMiniMap({ panX, panY, cellSize, seatRow, seatCol, seatList, canvasWidth, miniMapShow, hallHeight })
- this._drawing = false
- })
- }
- /* -------------------- 触摸事件 -------------------- */
- onTouchStart(e) {
- this._startPanX = this.panX
- this._startPanY = this.panY
- this._lastTouch = e.touches[0]
- this._startTouchEvent = e
- this._isPinching = false
- if (e.touches.length === 2) {
- this._isPinching = true
- this._lastPinchDist = this.calc.calcDistance(e.touches[0], e.touches[1])
- }
- }
- onTouchMove(e) {
- if (e.touches.length === 1 && !this._isPinching) {
- this.miniMapShow = true // 拖拽时显示小图
- const dx = e.touches[0].x - this._lastTouch.x
- const dy = e.touches[0].y - this._lastTouch.y
- this.panX = this._startPanX + dx
- this.panY = this._startPanY + dy
- this.requestDraw()
- return
- }
- if (e.touches.length === 2 && this._isPinching) {
- this.miniMapShow = true
- if (!this._lastPinchDist)
- return
- const dist = this.calc.calcDistance(e.touches[0], e.touches[1])
- const centerX = (e.touches[0].x + e.touches[1].x) / 2
- const centerY = (e.touches[0].y + e.touches[1].y) / 2
- const scale = dist / this._lastPinchDist
- const cellSize = this.cellSize * scale
- this.applyZoom(cellSize, centerX, centerY)
- this._lastPinchDist = dist
- this.requestDraw()
- }
- }
- onTouchEnd() {
- this._afterDrawTimer()
- const { targetX, targetY } = this.calc.clampOffset(this.panX, this.panY, this.boundaries)
- if (this.panX === targetX && this.panY === targetY)
- return
- this._animateTo(targetX, targetY)
- }
- /* -------------------- 选座 -------------------- */
- async onSeatTap({ x, y }) {
- const { panX, panY } = this
- const { AVAILABLE, SELECTED } = this.options.SEAT_STATUS
- const { STATUS_NAME } = this.options.SEAT_FIELDS
- const { seatMaxWidth, maxSelectNum, isolateSeats } = this.options
- const { canvasX, canvasY } = this.calc.clientToCanvas({ x, y, panX, panY })
- const seat = this.seatList.find((item) => {
- if (!item.points)
- return false
- const { startX, startY, endX, endY } = item.points
- return (canvasX >= startX && canvasX <= endX && canvasY >= startY && canvasY <= endY)
- })
- if (!seat || ![AVAILABLE, SELECTED].includes(seat[STATUS_NAME])) {
- return { ok: true, msg: '不是有效座位' }
- }
- const indices = this.calc.getRelatedSeatIndices(seat)
- const adjustedNum = seat[STATUS_NAME] === SELECTED ? -indices.length : indices.length
- if (maxSelectNum && this.chooseSeatList().length + adjustedNum > maxSelectNum) {
- return { ok: false, msg: `最多可选${maxSelectNum}个座位` }
- };
- const targetStatus = seat[STATUS_NAME] === SELECTED ? AVAILABLE : SELECTED
- this.calc.updateSeatStatus(targetStatus, indices)
- if (this.firstClick) {
- this.applyZoom(seatMaxWidth, canvasX, canvasY)
- this.firstClick = false
- }
- this.requestDraw()
- if (seat[STATUS_NAME] === SELECTED && isolateSeats && this.calc.validateSeat(seat)) {
- return { ok: false, msg: '左右两边不要留空哦' }
- }
- return { ok: true, msg: '点击座位成功' }
- }
- applyZoom(cellSize, centerX, centerY) {
- const { panX, panY, cellSize: oldCellSize, seatCol } = this
- const { canvasWidth, seatMaxWidth, seatMinWidth } = this.options
- const zoom = this.calc.calcPinchZoom({ oldCellSize, scale: cellSize / oldCellSize, centerX, centerY, panX, panY, seatCol, canvasWidth, seatMaxWidth, seatMinWidth })
- if (!zoom)
- return
- this.boundaries = this.calc.getBoundaries(this.options.canvasWidth, this.options.canvasHeight, this.options.footerHeight, zoom.cellSize)
- const { targetX, targetY } = this.calc.clampOffset(zoom.panX, zoom.panY, this.boundaries)
- this.cellSize = cellSize
- this.panX = targetX
- this.panY = targetY
- }
- chooseSeatList() {
- const { SELECTED } = this.options.SEAT_STATUS
- const { STATUS_NAME } = this.options.SEAT_FIELDS
- const chooseList = this.seatList.filter((item) => {
- return item[STATUS_NAME] === SELECTED
- })
- chooseList.sort((a, b) => {
- return a.clickTime - b.clickTime
- })
- return chooseList
- }
- cancelSeat(seat) {
- this.calc.cancelSeat(seat)
- this.requestDraw()
- }
- /* ========== 孤座检测 ========== */
- validateIsolates() {
- if (this.options.isolateSeats === false)
- return 0
- let errNo = 0
- this.chooseSeatList().forEach((seat) => {
- errNo += this.calc.validateSeat(seat)
- })
- return errNo
- }
- /* -------------------- 工具 -------------------- */
- _afterDrawTimer() {
- if (this._timer)
- clearTimeout(this._timer)
- this._timer = setTimeout(() => {
- this.miniMapShow = false
- this.requestDraw()
- }, this.options.miniMapShowTime)
- }
- _animateTo(tx, ty) {
- const startX = this.panX
- const startY = this.panY
- const duration = 300
- const startTime = Date.now()
- const animate = () => {
- const elapsed = Date.now() - startTime
- const prog = Math.min(elapsed / duration, 1)
- const ease = 1 - (1 - prog) ** 3
- this.panX = startX + (tx - startX) * ease
- this.panY = startY + (ty - startY) * ease
- this.requestDraw()
- if (prog < 1)
- this.operator.raf(animate)
- }
- this.operator.raf(animate)
- }
- /* ---------- 深度合并 ---------- */
- _deepMerge(target, source) {
- // 如果目标或源不是对象,直接返回源
- if (typeof target !== 'object' || target === null || Array.isArray(target)) {
- return source
- }
- // 遍历源对象的每个属性
- for (const key in source) {
- if (Object.prototype.hasOwnProperty.call(source, key)) {
- const targetValue = target[key]
- const sourceValue = source[key]
- // 如果源属性是对象,递归合并
- if (typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue)) {
- if (typeof targetValue !== 'object' || targetValue === null || Array.isArray(targetValue)) {
- target[key] = {}
- }
- this._deepMerge(target[key], sourceValue)
- }
- else {
- // 否则,直接覆盖目标属性
- target[key] = sourceValue
- }
- }
- }
- return target
- }
- }
|