stellar-ui-plus 1.24.26 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ste-app-update/method.ts +1 -0
- package/components/ste-app-update/props.ts +11 -11
- package/components/ste-app-update/ste-app-update.vue +2 -7
- package/components/ste-radio/README.md +1 -1
- package/components/ste-radio/ste-radio.vue +2 -7
- package/components/ste-radio-group/ste-radio-group.vue +2 -1
- package/components/ste-select-seat/ATTRIBUTES.md +18 -0
- package/components/ste-select-seat/README.md +280 -0
- package/components/ste-select-seat/canvasUtils.ts +42 -0
- package/components/ste-select-seat/config.json +5 -0
- package/components/ste-select-seat/internals/gridUtils.ts +23 -0
- package/components/ste-select-seat/internals/seatLayout.ts +169 -0
- package/components/ste-select-seat/internals/useSeatInteraction.ts +540 -0
- package/components/ste-select-seat/props.ts +37 -0
- package/components/ste-select-seat/ste-select-seat.easycom.json +62 -0
- package/components/ste-select-seat/ste-select-seat.vue +517 -0
- package/components/ste-select-seat/types.d.ts +33 -0
- package/components/ste-select-seat/useData.ts +179 -0
- package/components/ste-select-seat/useTouchCompat.ts +89 -0
- package/components/ste-simple-calendar/ATTRIBUTES.md +17 -0
- package/components/ste-simple-calendar/README.md +112 -0
- package/components/ste-simple-calendar/config.json +5 -0
- package/components/ste-simple-calendar/props.ts +32 -0
- package/components/ste-simple-calendar/ste-simple-calendar.easycom.json +60 -0
- package/components/ste-simple-calendar/ste-simple-calendar.vue +265 -0
- package/components/ste-simple-calendar/type.d.ts +30 -0
- package/components/ste-simple-calendar/useData.ts +60 -0
- package/components/ste-skeleton/ATTRIBUTES.md +7 -0
- package/components/ste-skeleton/README.md +82 -0
- package/components/ste-skeleton/config.json +5 -0
- package/components/ste-skeleton/props.ts +7 -0
- package/components/ste-skeleton/ste-skeleton.easycom.json +38 -0
- package/components/ste-skeleton/ste-skeleton.vue +90 -0
- package/components/ste-slide-verify/ATTRIBUTES.md +27 -0
- package/components/ste-slide-verify/README.md +118 -0
- package/components/ste-slide-verify/config.json +5 -0
- package/components/ste-slide-verify/props.ts +43 -0
- package/components/ste-slide-verify/ste-slide-verify.easycom.json +119 -0
- package/components/ste-slide-verify/ste-slide-verify.vue +535 -0
- package/index.ts +8 -0
- package/package.json +1 -1
- package/types/components.d.ts +8 -0
- package/types/index.d.ts +2 -0
- package/types/refComponents.d.ts +8 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export interface SteSelectSeatContentSize {
|
|
2
|
+
width: number
|
|
3
|
+
height: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface SteSelectSeatViewport {
|
|
7
|
+
scale: number
|
|
8
|
+
translateX: number
|
|
9
|
+
translateY: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SteSelectSeatRowLabelItem {
|
|
13
|
+
row: number
|
|
14
|
+
top: number
|
|
15
|
+
rowHeight: number
|
|
16
|
+
style: {
|
|
17
|
+
top: string
|
|
18
|
+
height: string
|
|
19
|
+
lineHeight: string
|
|
20
|
+
fontSize: string
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const INTERNAL_MAX_SCALE = 3
|
|
25
|
+
|
|
26
|
+
export const getLabelWidth = (showRowLabels: boolean, seatSize: number, seatGap: number) => {
|
|
27
|
+
return showRowLabels ? seatSize + seatGap : 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const getSeatContentSize = (params: {
|
|
31
|
+
rows: number
|
|
32
|
+
cols: number
|
|
33
|
+
seatSize: number
|
|
34
|
+
seatGap: number
|
|
35
|
+
labelWidth: number
|
|
36
|
+
}): SteSelectSeatContentSize => {
|
|
37
|
+
const { rows, cols, seatSize, seatGap, labelWidth } = params
|
|
38
|
+
if (!rows || !cols) return { width: 0, height: 0 }
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
width: labelWidth + cols * (seatSize + seatGap) + seatGap,
|
|
42
|
+
height: rows * (seatSize + seatGap) + seatGap,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const getFitScale = (params: {
|
|
47
|
+
width: number
|
|
48
|
+
height: number
|
|
49
|
+
contentWidth: number
|
|
50
|
+
contentHeight: number
|
|
51
|
+
}) => {
|
|
52
|
+
const { width, height, contentWidth, contentHeight } = params
|
|
53
|
+
if (!contentWidth || !contentHeight) return 1
|
|
54
|
+
return Math.min(1, width / contentWidth, height / contentHeight)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const clampSeatScale = (scale: number, fitScale: number, maxScale = INTERNAL_MAX_SCALE) => {
|
|
58
|
+
return Math.min(Math.max(scale, fitScale), maxScale)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const getSeatTranslateBounds = (params: {
|
|
62
|
+
scale: number
|
|
63
|
+
width: number
|
|
64
|
+
height: number
|
|
65
|
+
contentWidth: number
|
|
66
|
+
contentHeight: number
|
|
67
|
+
}) => {
|
|
68
|
+
const { scale, width, height, contentWidth, contentHeight } = params
|
|
69
|
+
if (!contentWidth || !contentHeight) {
|
|
70
|
+
return { minX: 0, maxX: 0, minY: 0, maxY: 0 }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const scaledWidth = contentWidth * scale
|
|
74
|
+
const scaledHeight = contentHeight * scale
|
|
75
|
+
const centerX = (width / scale - contentWidth) / 2
|
|
76
|
+
const centerY = (height / scale - contentHeight) / 2
|
|
77
|
+
const marginX = width * 0.2 / scale
|
|
78
|
+
const marginY = height * 0.2 / scale
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
minX: scaledWidth > width ? width / scale - contentWidth - marginX : centerX - marginX,
|
|
82
|
+
maxX: scaledWidth > width ? marginX : centerX + marginX,
|
|
83
|
+
minY: scaledHeight > height ? height / scale - contentHeight - marginY : centerY - marginY,
|
|
84
|
+
maxY: scaledHeight > height ? marginY : centerY + marginY,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const getDefaultSeatViewport = (params: {
|
|
89
|
+
fitScale: number
|
|
90
|
+
width: number
|
|
91
|
+
height: number
|
|
92
|
+
contentWidth: number
|
|
93
|
+
contentHeight: number
|
|
94
|
+
maxScale?: number
|
|
95
|
+
}): SteSelectSeatViewport => {
|
|
96
|
+
const { fitScale, width, height, contentWidth, contentHeight, maxScale = INTERNAL_MAX_SCALE } = params
|
|
97
|
+
if (!contentWidth || !contentHeight) {
|
|
98
|
+
return {
|
|
99
|
+
scale: 1,
|
|
100
|
+
translateX: 0,
|
|
101
|
+
translateY: 0,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const scale = clampSeatScale(fitScale, fitScale, maxScale)
|
|
106
|
+
return {
|
|
107
|
+
scale,
|
|
108
|
+
translateX: (width / scale - contentWidth) / 2,
|
|
109
|
+
translateY: (height / scale - contentHeight) / 2,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const getScreenTranslateX = (params: {
|
|
114
|
+
scale: number
|
|
115
|
+
translateX: number
|
|
116
|
+
width: number
|
|
117
|
+
defaultViewport: SteSelectSeatViewport
|
|
118
|
+
}) => {
|
|
119
|
+
const { scale, translateX, width, defaultViewport } = params
|
|
120
|
+
const anchorX = width / (2 * defaultViewport.scale) - defaultViewport.translateX
|
|
121
|
+
return (anchorX + translateX) * scale - width / 2
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const buildRowLabelItems = (params: {
|
|
125
|
+
rows: number
|
|
126
|
+
height: number
|
|
127
|
+
seatSize: number
|
|
128
|
+
seatGap: number
|
|
129
|
+
translateY: number
|
|
130
|
+
scale: number
|
|
131
|
+
}): SteSelectSeatRowLabelItem[] => {
|
|
132
|
+
const { rows, height, seatSize, seatGap, translateY, scale } = params
|
|
133
|
+
const rowHeight = seatSize * scale
|
|
134
|
+
const fontSize = Math.max(10, Math.min(13, rowHeight * 0.3))
|
|
135
|
+
|
|
136
|
+
return Array.from({ length: rows }, (_, row) => {
|
|
137
|
+
const top = (row * (seatSize + seatGap) + seatGap / 2 + translateY) * scale
|
|
138
|
+
return {
|
|
139
|
+
row,
|
|
140
|
+
top,
|
|
141
|
+
rowHeight,
|
|
142
|
+
style: {
|
|
143
|
+
top: `${top}px`,
|
|
144
|
+
height: `${rowHeight}px`,
|
|
145
|
+
lineHeight: `${rowHeight}px`,
|
|
146
|
+
fontSize: `${fontSize}px`,
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
}).filter(item => item.top + rowHeight > 0 && item.top < height)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const getRowLabelTrackStyle = (items: SteSelectSeatRowLabelItem[], height: number) => {
|
|
153
|
+
if (!items.length) {
|
|
154
|
+
return {
|
|
155
|
+
display: 'none',
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const first = items[0]
|
|
160
|
+
const last = items[items.length - 1]
|
|
161
|
+
const padding = 8
|
|
162
|
+
const top = Math.max(0, first.top - padding)
|
|
163
|
+
const bottom = Math.min(height, last.top + last.rowHeight + padding)
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
top: `${top}px`,
|
|
167
|
+
height: `${Math.max(32, bottom - top)}px`,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import type { ComponentPublicInstance } from 'vue'
|
|
3
|
+
import type { UniTouch, UniTouchEvent } from '../../../types/event'
|
|
4
|
+
import type { SteSelectSeatItem, SteSelectSeatValue } from '../types'
|
|
5
|
+
import { getTouchCenter, getTouchDistance, getTouchIdentifier, getTouchX, getTouchY, toTouchArray } from '../useTouchCompat'
|
|
6
|
+
|
|
7
|
+
interface UseSeatInteractionOptions {
|
|
8
|
+
instance: ComponentPublicInstance
|
|
9
|
+
canvasId: string
|
|
10
|
+
getShowRowLabels: () => boolean
|
|
11
|
+
touchHandler: {
|
|
12
|
+
scale: number
|
|
13
|
+
baseScale: number
|
|
14
|
+
translateX: number
|
|
15
|
+
translateY: number
|
|
16
|
+
baseTranslateX: number
|
|
17
|
+
baseTranslateY: number
|
|
18
|
+
reset: () => void
|
|
19
|
+
}
|
|
20
|
+
clampScale: (scale: number) => number
|
|
21
|
+
applyTranslateResistance: (x: number, y: number, scale?: number) => { x: number; y: number }
|
|
22
|
+
clampTranslate: (x: number, y: number, scale?: number) => { x: number; y: number }
|
|
23
|
+
getTouchSeat: (touchX: number, touchY: number) => SteSelectSeatItem | null
|
|
24
|
+
getTouchLocalPoint: (touch: UniTouch | undefined | null, rect?: { left?: number; top?: number } | null) => { x: number; y: number }
|
|
25
|
+
applyDefaultViewport: () => void
|
|
26
|
+
draw: () => void
|
|
27
|
+
emitMove: () => void
|
|
28
|
+
emitSeatClick: (seat: SteSelectSeatItem) => void
|
|
29
|
+
emitModelValue: (value: SteSelectSeatValue[]) => void
|
|
30
|
+
toggleSeat: (row: number, col: number) => SteSelectSeatValue[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PointerEventLike {
|
|
34
|
+
clientX: number
|
|
35
|
+
clientY: number
|
|
36
|
+
target?: EventTarget | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useSeatInteraction(options: UseSeatInteractionOptions) {
|
|
40
|
+
const {
|
|
41
|
+
instance,
|
|
42
|
+
canvasId,
|
|
43
|
+
getShowRowLabels,
|
|
44
|
+
touchHandler,
|
|
45
|
+
clampScale,
|
|
46
|
+
applyTranslateResistance,
|
|
47
|
+
clampTranslate,
|
|
48
|
+
getTouchSeat,
|
|
49
|
+
getTouchLocalPoint,
|
|
50
|
+
applyDefaultViewport,
|
|
51
|
+
draw,
|
|
52
|
+
emitMove,
|
|
53
|
+
emitSeatClick,
|
|
54
|
+
emitModelValue,
|
|
55
|
+
toggleSeat,
|
|
56
|
+
} = options
|
|
57
|
+
|
|
58
|
+
const panThreshold = 4
|
|
59
|
+
const reboundThreshold = 0.5
|
|
60
|
+
const reboundDuration = 180
|
|
61
|
+
const momentumMinVelocity = 0.02
|
|
62
|
+
const momentumDecayPerFrame = 0.92
|
|
63
|
+
|
|
64
|
+
const rowLabelsVisible = ref(getShowRowLabels())
|
|
65
|
+
const activeTouches = new Map<number | string, UniTouch>()
|
|
66
|
+
|
|
67
|
+
let dragMoved = false
|
|
68
|
+
let gestureMode: 'none' | 'pan' | 'pinch' = 'none'
|
|
69
|
+
let panStartX = 0
|
|
70
|
+
let panStartY = 0
|
|
71
|
+
let panBaseTranslateX = 0
|
|
72
|
+
let panBaseTranslateY = 0
|
|
73
|
+
let pinchStartDistance = 0
|
|
74
|
+
let pinchStartScale = 1
|
|
75
|
+
let pinchStartCenterX = 0
|
|
76
|
+
let pinchStartCenterY = 0
|
|
77
|
+
let pinchLockedTranslateX = 0
|
|
78
|
+
let pinchLockedTranslateY = 0
|
|
79
|
+
let reboundTimer: ReturnType<typeof setTimeout> | null = null
|
|
80
|
+
let momentumTimer: ReturnType<typeof setTimeout> | null = null
|
|
81
|
+
let rowLabelTimer: ReturnType<typeof setTimeout> | null = null
|
|
82
|
+
let lastPanSampleTime = 0
|
|
83
|
+
let lastPanSampleX = 0
|
|
84
|
+
let lastPanSampleY = 0
|
|
85
|
+
let panVelocityX = 0
|
|
86
|
+
let panVelocityY = 0
|
|
87
|
+
let mouseDown = false
|
|
88
|
+
let mouseStartX = 0
|
|
89
|
+
let mouseStartY = 0
|
|
90
|
+
|
|
91
|
+
// ─── Touch State Sync ─────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
const syncActiveTouches = (touches: UniTouch[]) => {
|
|
94
|
+
activeTouches.clear()
|
|
95
|
+
touches.forEach((touch, index) => {
|
|
96
|
+
activeTouches.set(getTouchIdentifier(touch, index), touch)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const patchActiveTouches = (touches: UniTouch[]) => {
|
|
101
|
+
touches.forEach((touch, index) => {
|
|
102
|
+
activeTouches.set(getTouchIdentifier(touch, index), touch)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const removeActiveTouches = (touches: UniTouch[]) => {
|
|
107
|
+
touches.forEach((touch, index) => {
|
|
108
|
+
activeTouches.delete(getTouchIdentifier(touch, index))
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const getEventTouches = (e: UniTouchEvent, phase: 'start' | 'move' | 'end') => {
|
|
113
|
+
const touches = toTouchArray(e.touches)
|
|
114
|
+
const changedTouches = toTouchArray(e.changedTouches)
|
|
115
|
+
|
|
116
|
+
if (phase === 'end') {
|
|
117
|
+
if (touches.length) {
|
|
118
|
+
syncActiveTouches(touches)
|
|
119
|
+
} else {
|
|
120
|
+
removeActiveTouches(changedTouches)
|
|
121
|
+
if (!activeTouches.size) {
|
|
122
|
+
activeTouches.clear()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return Array.from(activeTouches.values())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (touches.length) {
|
|
129
|
+
syncActiveTouches(touches)
|
|
130
|
+
} else {
|
|
131
|
+
patchActiveTouches(changedTouches)
|
|
132
|
+
}
|
|
133
|
+
return Array.from(activeTouches.values())
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const getChangedTouches = (e: UniTouchEvent) => toTouchArray(e.changedTouches)
|
|
137
|
+
|
|
138
|
+
// ─── Overlay State ────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
const clearRowLabelTimer = () => {
|
|
141
|
+
if (rowLabelTimer) {
|
|
142
|
+
clearTimeout(rowLabelTimer)
|
|
143
|
+
rowLabelTimer = null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const setRowLabelsVisible = (visible: boolean) => {
|
|
148
|
+
clearRowLabelTimer()
|
|
149
|
+
rowLabelsVisible.value = visible
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const showRowLabelOverlay = () => {
|
|
153
|
+
clearRowLabelTimer()
|
|
154
|
+
rowLabelsVisible.value = getShowRowLabels()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Motion State ─────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
const resetPanVelocity = () => {
|
|
160
|
+
panVelocityX = 0
|
|
161
|
+
panVelocityY = 0
|
|
162
|
+
lastPanSampleTime = 0
|
|
163
|
+
lastPanSampleX = 0
|
|
164
|
+
lastPanSampleY = 0
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const recordPanVelocity = (x: number, y: number) => {
|
|
168
|
+
const now = Date.now()
|
|
169
|
+
if (!lastPanSampleTime) {
|
|
170
|
+
lastPanSampleTime = now
|
|
171
|
+
lastPanSampleX = x
|
|
172
|
+
lastPanSampleY = y
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const dt = Math.max(1, now - lastPanSampleTime)
|
|
177
|
+
const nextVelocityX = (x - lastPanSampleX) / dt
|
|
178
|
+
const nextVelocityY = (y - lastPanSampleY) / dt
|
|
179
|
+
|
|
180
|
+
panVelocityX = panVelocityX * 0.35 + nextVelocityX * 0.65
|
|
181
|
+
panVelocityY = panVelocityY * 0.35 + nextVelocityY * 0.65
|
|
182
|
+
lastPanSampleTime = now
|
|
183
|
+
lastPanSampleX = x
|
|
184
|
+
lastPanSampleY = y
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const stopMomentum = () => {
|
|
188
|
+
if (!momentumTimer) return
|
|
189
|
+
clearTimeout(momentumTimer)
|
|
190
|
+
momentumTimer = null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const stopRebound = () => {
|
|
194
|
+
if (!reboundTimer) return
|
|
195
|
+
clearTimeout(reboundTimer)
|
|
196
|
+
reboundTimer = null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const stopMotion = () => {
|
|
200
|
+
stopMomentum()
|
|
201
|
+
stopRebound()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const updateBaseTransform = () => {
|
|
205
|
+
touchHandler.baseScale = touchHandler.scale
|
|
206
|
+
touchHandler.baseTranslateX = touchHandler.translateX
|
|
207
|
+
touchHandler.baseTranslateY = touchHandler.translateY
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const applyTranslate = (x: number, y: number, scale = touchHandler.scale) => {
|
|
211
|
+
const nextTranslate = applyTranslateResistance(x, y, scale)
|
|
212
|
+
touchHandler.translateX = nextTranslate.x
|
|
213
|
+
touchHandler.translateY = nextTranslate.y
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const beginPan = (touch: UniTouch, moved: boolean) => {
|
|
217
|
+
gestureMode = 'pan'
|
|
218
|
+
dragMoved = moved
|
|
219
|
+
panStartX = getTouchX(touch)
|
|
220
|
+
panStartY = getTouchY(touch)
|
|
221
|
+
panBaseTranslateX = touchHandler.translateX
|
|
222
|
+
panBaseTranslateY = touchHandler.translateY
|
|
223
|
+
resetPanVelocity()
|
|
224
|
+
recordPanVelocity(panStartX, panStartY)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const beginPinch = (touches: UniTouch[]) => {
|
|
228
|
+
gestureMode = 'pinch'
|
|
229
|
+
dragMoved = true
|
|
230
|
+
resetPanVelocity()
|
|
231
|
+
pinchStartDistance = getTouchDistance(touches)
|
|
232
|
+
pinchStartScale = touchHandler.scale
|
|
233
|
+
const center = getTouchCenter(touches)
|
|
234
|
+
pinchStartCenterX = center.x
|
|
235
|
+
pinchStartCenterY = center.y
|
|
236
|
+
pinchLockedTranslateX = touchHandler.translateX
|
|
237
|
+
pinchLockedTranslateY = touchHandler.translateY
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── Motion Animation ─────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
const reboundToBounds = (onComplete?: () => void) => {
|
|
243
|
+
stopMomentum()
|
|
244
|
+
stopRebound()
|
|
245
|
+
const fromX = touchHandler.translateX
|
|
246
|
+
const fromY = touchHandler.translateY
|
|
247
|
+
const target = clampTranslate(fromX, fromY)
|
|
248
|
+
|
|
249
|
+
if (Math.abs(target.x - fromX) <= reboundThreshold && Math.abs(target.y - fromY) <= reboundThreshold) {
|
|
250
|
+
touchHandler.translateX = target.x
|
|
251
|
+
touchHandler.translateY = target.y
|
|
252
|
+
updateBaseTransform()
|
|
253
|
+
draw()
|
|
254
|
+
emitMove()
|
|
255
|
+
onComplete?.()
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const startTime = Date.now()
|
|
260
|
+
const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
|
|
261
|
+
|
|
262
|
+
const animate = () => {
|
|
263
|
+
const elapsed = Date.now() - startTime
|
|
264
|
+
const progress = Math.min(1, elapsed / reboundDuration)
|
|
265
|
+
const eased = easeOutCubic(progress)
|
|
266
|
+
|
|
267
|
+
touchHandler.translateX = fromX + (target.x - fromX) * eased
|
|
268
|
+
touchHandler.translateY = fromY + (target.y - fromY) * eased
|
|
269
|
+
updateBaseTransform()
|
|
270
|
+
draw()
|
|
271
|
+
emitMove()
|
|
272
|
+
|
|
273
|
+
if (progress >= 1) {
|
|
274
|
+
touchHandler.translateX = target.x
|
|
275
|
+
touchHandler.translateY = target.y
|
|
276
|
+
updateBaseTransform()
|
|
277
|
+
draw()
|
|
278
|
+
emitMove()
|
|
279
|
+
reboundTimer = null
|
|
280
|
+
onComplete?.()
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
reboundTimer = setTimeout(animate, 16)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
animate()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const startMomentum = (onComplete?: () => void) => {
|
|
291
|
+
stopMomentum()
|
|
292
|
+
|
|
293
|
+
if (Math.abs(panVelocityX) < momentumMinVelocity && Math.abs(panVelocityY) < momentumMinVelocity) {
|
|
294
|
+
reboundToBounds(onComplete)
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let velocityX = panVelocityX
|
|
299
|
+
let velocityY = panVelocityY
|
|
300
|
+
let lastTime = Date.now()
|
|
301
|
+
|
|
302
|
+
const animate = () => {
|
|
303
|
+
const now = Date.now()
|
|
304
|
+
const dt = Math.min(24, Math.max(8, now - lastTime))
|
|
305
|
+
lastTime = now
|
|
306
|
+
|
|
307
|
+
const decay = Math.pow(momentumDecayPerFrame, dt / 16)
|
|
308
|
+
velocityX *= decay
|
|
309
|
+
velocityY *= decay
|
|
310
|
+
|
|
311
|
+
const nextTranslate = applyTranslateResistance(
|
|
312
|
+
touchHandler.translateX + velocityX * dt,
|
|
313
|
+
touchHandler.translateY + velocityY * dt,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
touchHandler.translateX = nextTranslate.x
|
|
317
|
+
touchHandler.translateY = nextTranslate.y
|
|
318
|
+
updateBaseTransform()
|
|
319
|
+
draw()
|
|
320
|
+
emitMove()
|
|
321
|
+
|
|
322
|
+
const clamped = clampTranslate(touchHandler.translateX, touchHandler.translateY)
|
|
323
|
+
const outOfBounds =
|
|
324
|
+
Math.abs(clamped.x - touchHandler.translateX) > reboundThreshold ||
|
|
325
|
+
Math.abs(clamped.y - touchHandler.translateY) > reboundThreshold
|
|
326
|
+
|
|
327
|
+
if ((Math.abs(velocityX) < momentumMinVelocity && Math.abs(velocityY) < momentumMinVelocity) || outOfBounds) {
|
|
328
|
+
momentumTimer = null
|
|
329
|
+
panVelocityX = velocityX
|
|
330
|
+
panVelocityY = velocityY
|
|
331
|
+
reboundToBounds(onComplete)
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
momentumTimer = setTimeout(animate, 16)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
animate()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const emitSeatSelection = (seat: SteSelectSeatItem) => {
|
|
342
|
+
emitSeatClick(seat)
|
|
343
|
+
emitModelValue(toggleSeat(seat.row, seat.col))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const selectSeatByPoint = (x: number, y: number) => {
|
|
347
|
+
const seat = getTouchSeat(x, y)
|
|
348
|
+
if (seat && !seat.disabled && !seat.empty) {
|
|
349
|
+
emitSeatSelection(seat)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const selectSeatFromTouch = (touch: UniTouch) => {
|
|
354
|
+
uni.createSelectorQuery()
|
|
355
|
+
.in(instance)
|
|
356
|
+
.select(`#${canvasId}`)
|
|
357
|
+
.boundingClientRect((rect: any) => {
|
|
358
|
+
if (!rect) return
|
|
359
|
+
const { x: localX, y: localY } = getTouchLocalPoint(touch, rect)
|
|
360
|
+
selectSeatByPoint(localX, localY)
|
|
361
|
+
})
|
|
362
|
+
.exec()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const selectSeatFromMouse = (event: PointerEventLike) => {
|
|
366
|
+
const rect = (event.target as HTMLElement | null)?.getBoundingClientRect?.()
|
|
367
|
+
if (!rect) return
|
|
368
|
+
selectSeatByPoint(event.clientX - rect.left, event.clientY - rect.top)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Pointer Events ───────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
const onTouchStart = (e: UniTouchEvent) => {
|
|
374
|
+
stopMotion()
|
|
375
|
+
const touches = getEventTouches(e, 'start')
|
|
376
|
+
const touchCount = touches.length
|
|
377
|
+
|
|
378
|
+
if (touchCount >= 2) {
|
|
379
|
+
beginPinch(touches)
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
beginPan(touches[0], false)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const onTouchMove = (e: UniTouchEvent) => {
|
|
387
|
+
const touches = getEventTouches(e, 'move')
|
|
388
|
+
const touchCount = touches.length
|
|
389
|
+
|
|
390
|
+
if (touchCount >= 2) {
|
|
391
|
+
if (!pinchStartDistance) {
|
|
392
|
+
beginPinch(touches)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const currentDistance = getTouchDistance(touches)
|
|
396
|
+
const currentCenter = getTouchCenter(touches)
|
|
397
|
+
if (pinchStartDistance > 0 && currentDistance > 0) {
|
|
398
|
+
const nextScale = clampScale((currentDistance / pinchStartDistance) * pinchStartScale)
|
|
399
|
+
touchHandler.scale = nextScale
|
|
400
|
+
|
|
401
|
+
const nextTranslateX = currentCenter.x / nextScale - pinchStartCenterX / pinchStartScale + pinchLockedTranslateX
|
|
402
|
+
const nextTranslateY = currentCenter.y / nextScale - pinchStartCenterY / pinchStartScale + pinchLockedTranslateY
|
|
403
|
+
applyTranslate(nextTranslateX, nextTranslateY, nextScale)
|
|
404
|
+
}
|
|
405
|
+
draw()
|
|
406
|
+
emitMove()
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (gestureMode !== 'pan' || touchCount !== 1) return
|
|
411
|
+
|
|
412
|
+
const touch = touches[0]
|
|
413
|
+
const currentX = getTouchX(touch)
|
|
414
|
+
const currentY = getTouchY(touch)
|
|
415
|
+
const dx = currentX - panStartX
|
|
416
|
+
const dy = currentY - panStartY
|
|
417
|
+
|
|
418
|
+
if (Math.abs(dx) > panThreshold || Math.abs(dy) > panThreshold) {
|
|
419
|
+
dragMoved = true
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
applyTranslate(panBaseTranslateX + dx, panBaseTranslateY + dy)
|
|
423
|
+
recordPanVelocity(currentX, currentY)
|
|
424
|
+
draw()
|
|
425
|
+
emitMove()
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const onTouchEnd = (e: UniTouchEvent) => {
|
|
429
|
+
const touches = getEventTouches(e, 'end')
|
|
430
|
+
const changedTouches = getChangedTouches(e)
|
|
431
|
+
const touchCount = touches.length
|
|
432
|
+
|
|
433
|
+
if (gestureMode === 'pinch') {
|
|
434
|
+
touchHandler.scale = clampScale(touchHandler.scale)
|
|
435
|
+
applyTranslate(touchHandler.translateX, touchHandler.translateY, touchHandler.scale)
|
|
436
|
+
pinchStartDistance = 0
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
updateBaseTransform()
|
|
440
|
+
|
|
441
|
+
if (touchCount >= 2) {
|
|
442
|
+
beginPinch(touches)
|
|
443
|
+
draw()
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (touchCount === 1) {
|
|
448
|
+
beginPan(touches[0], true)
|
|
449
|
+
} else if (touchCount === 0) {
|
|
450
|
+
gestureMode = 'none'
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
draw()
|
|
454
|
+
if (touchCount === 0 && dragMoved) {
|
|
455
|
+
startMomentum(() => showRowLabelOverlay())
|
|
456
|
+
} else if (touchCount === 0 || gestureMode === 'pinch') {
|
|
457
|
+
reboundToBounds(() => {
|
|
458
|
+
if (touchCount === 0) showRowLabelOverlay()
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!dragMoved && changedTouches.length === 1) {
|
|
463
|
+
selectSeatFromTouch(changedTouches[0])
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const onMouseDown = (e: PointerEventLike) => {
|
|
468
|
+
stopMotion()
|
|
469
|
+
mouseDown = true
|
|
470
|
+
dragMoved = false
|
|
471
|
+
mouseStartX = e.clientX
|
|
472
|
+
mouseStartY = e.clientY
|
|
473
|
+
resetPanVelocity()
|
|
474
|
+
recordPanVelocity(mouseStartX, mouseStartY)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const onMouseMove = (e: PointerEventLike) => {
|
|
478
|
+
if (!mouseDown) return
|
|
479
|
+
const dx = e.clientX - mouseStartX
|
|
480
|
+
const dy = e.clientY - mouseStartY
|
|
481
|
+
if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
|
|
482
|
+
dragMoved = true
|
|
483
|
+
}
|
|
484
|
+
applyTranslate(touchHandler.baseTranslateX + dx, touchHandler.baseTranslateY + dy)
|
|
485
|
+
recordPanVelocity(e.clientX, e.clientY)
|
|
486
|
+
draw()
|
|
487
|
+
emitMove()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const onMouseUp = (e: PointerEventLike) => {
|
|
491
|
+
if (!mouseDown) return
|
|
492
|
+
mouseDown = false
|
|
493
|
+
updateBaseTransform()
|
|
494
|
+
if (dragMoved) {
|
|
495
|
+
startMomentum(() => showRowLabelOverlay())
|
|
496
|
+
} else {
|
|
497
|
+
reboundToBounds(() => showRowLabelOverlay())
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!dragMoved) {
|
|
501
|
+
selectSeatFromMouse(e)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ─── Public API ───────────────────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
const reset = () => {
|
|
508
|
+
stopMotion()
|
|
509
|
+
showRowLabelOverlay()
|
|
510
|
+
activeTouches.clear()
|
|
511
|
+
touchHandler.reset()
|
|
512
|
+
gestureMode = 'none'
|
|
513
|
+
dragMoved = false
|
|
514
|
+
pinchStartDistance = 0
|
|
515
|
+
pinchStartScale = 1
|
|
516
|
+
pinchStartCenterX = 0
|
|
517
|
+
pinchStartCenterY = 0
|
|
518
|
+
pinchLockedTranslateX = 0
|
|
519
|
+
pinchLockedTranslateY = 0
|
|
520
|
+
panStartX = 0
|
|
521
|
+
panStartY = 0
|
|
522
|
+
panBaseTranslateX = 0
|
|
523
|
+
panBaseTranslateY = 0
|
|
524
|
+
resetPanVelocity()
|
|
525
|
+
applyDefaultViewport()
|
|
526
|
+
draw()
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
rowLabelsVisible,
|
|
531
|
+
setShowRowLabelsVisible: setRowLabelsVisible,
|
|
532
|
+
onTouchStart,
|
|
533
|
+
onTouchMove,
|
|
534
|
+
onTouchEnd,
|
|
535
|
+
onMouseDown,
|
|
536
|
+
onMouseMove,
|
|
537
|
+
onMouseUp,
|
|
538
|
+
reset,
|
|
539
|
+
}
|
|
540
|
+
}
|