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,37 @@
|
|
|
1
|
+
import type { PropType } from 'vue'
|
|
2
|
+
import type { SteSelectSeatItem, SteSelectSeatValue } from './types'
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
// 已选座位 v-model
|
|
6
|
+
modelValue: { type: Array as PropType<SteSelectSeatValue[]>, default: () => [] },
|
|
7
|
+
// 行数
|
|
8
|
+
rows: { type: Number, default: 0 },
|
|
9
|
+
// 列数
|
|
10
|
+
cols: { type: Number, default: 0 },
|
|
11
|
+
// 组件宽度(px)
|
|
12
|
+
width: { type: Number, default: 350 },
|
|
13
|
+
// 组件高度(px)
|
|
14
|
+
height: { type: Number, default: 400 },
|
|
15
|
+
// 自定义座位数据
|
|
16
|
+
seats: { type: Array as PropType<SteSelectSeatItem[]>, default: () => [] },
|
|
17
|
+
// 座位尺寸(rpx)
|
|
18
|
+
seatSize: { type: Number, default: 40 },
|
|
19
|
+
// 座位间距(rpx)
|
|
20
|
+
seatGap: { type: Number, default: 8 },
|
|
21
|
+
// 座位圆角(rpx)
|
|
22
|
+
borderRadius: { type: Number, default: 8 },
|
|
23
|
+
// 边框宽度
|
|
24
|
+
borderWidth: { type: Number, default: 1 },
|
|
25
|
+
// 座位背景色
|
|
26
|
+
bgColor: { type: String, default: '#ffffff' },
|
|
27
|
+
// 边框颜色
|
|
28
|
+
borderColor: { type: String, default: '#e5e5e5' },
|
|
29
|
+
// 选中背景色(默认用主题色)
|
|
30
|
+
selectedBgColor: { type: String, default: '' },
|
|
31
|
+
// 选中图标颜色
|
|
32
|
+
selectedColor: { type: String, default: '#ffffff' },
|
|
33
|
+
// 禁用背景色
|
|
34
|
+
disabledBgColor: { type: String, default: '#cccccc' },
|
|
35
|
+
// 显示行号
|
|
36
|
+
showRowLabels: { type: Boolean, default: true },
|
|
37
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ste-select-seat",
|
|
3
|
+
"description": "基于 Canvas 的座位选择组件",
|
|
4
|
+
"example": "<ste-select-seat v-model='selected' :rows='5' :cols='10'></ste-select-seat>",
|
|
5
|
+
"tutorial": "https://stellar-ui.intecloud.com.cn/?projectName=stellar-ui-plus&menu=%E7%BB%84%E4%BB%B6&active=ste-select-seat",
|
|
6
|
+
"attributes": [
|
|
7
|
+
{
|
|
8
|
+
"name": "modelValue",
|
|
9
|
+
"description": "已选座位坐标列表(仅包含 row/col)",
|
|
10
|
+
"type": "SteSelectSeatValue[]"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "rows",
|
|
14
|
+
"description": "行数",
|
|
15
|
+
"type": "number"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "cols",
|
|
19
|
+
"description": "列数",
|
|
20
|
+
"type": "number"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "width",
|
|
24
|
+
"description": "组件宽度(px)",
|
|
25
|
+
"type": "number",
|
|
26
|
+
"default": "350"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "height",
|
|
30
|
+
"description": "组件高度(px)",
|
|
31
|
+
"type": "number",
|
|
32
|
+
"default": "400"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "seats",
|
|
36
|
+
"description": "座位属性配置(未配置的位置会自动补齐为默认座位)",
|
|
37
|
+
"type": "SteSelectSeatItem[]"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "seatSize",
|
|
41
|
+
"description": "座位尺寸(rpx)",
|
|
42
|
+
"type": "number",
|
|
43
|
+
"default": "40"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "seatGap",
|
|
47
|
+
"description": "座位间距(rpx)",
|
|
48
|
+
"type": "number",
|
|
49
|
+
"default": "8"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "[event]seat-click",
|
|
53
|
+
"description": "点击有效座位事件(empty/disabled 不触发)",
|
|
54
|
+
"type": "(seat: SteSelectSeatItem) => void"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "[event]move",
|
|
58
|
+
"description": "拖动/缩放事件",
|
|
59
|
+
"type": "(data: { translateX, translateY, scale, screenTranslateX }) => void"
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
}
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { watch, nextTick, onMounted, getCurrentInstance, computed, ref } from 'vue';
|
|
3
|
+
import type { ComponentPublicInstance } from 'vue';
|
|
4
|
+
import propsData from './props';
|
|
5
|
+
import { useData } from './useData';
|
|
6
|
+
import { useColorStore } from '../../store/color';
|
|
7
|
+
import utils from '../../utils/utils';
|
|
8
|
+
import TouchScaleing from '../ste-media-preview/TouchScaleing';
|
|
9
|
+
import type { SteSelectSeatItem, SteSelectSeatValue, SteSelectSeatMoveEvent } from './types';
|
|
10
|
+
import { drawRoundRect, drawCheck } from './canvasUtils';
|
|
11
|
+
import { getSafeGridSize } from './internals/gridUtils';
|
|
12
|
+
import {
|
|
13
|
+
INTERNAL_MAX_SCALE,
|
|
14
|
+
buildRowLabelItems,
|
|
15
|
+
clampSeatScale,
|
|
16
|
+
getDefaultSeatViewport,
|
|
17
|
+
getFitScale,
|
|
18
|
+
getLabelWidth,
|
|
19
|
+
getRowLabelTrackStyle,
|
|
20
|
+
getScreenTranslateX as getSeatScreenTranslateX,
|
|
21
|
+
getSeatContentSize,
|
|
22
|
+
getSeatTranslateBounds,
|
|
23
|
+
} from './internals/seatLayout';
|
|
24
|
+
import { useSeatInteraction } from './internals/useSeatInteraction';
|
|
25
|
+
import { getTouchX, getTouchY } from './useTouchCompat';
|
|
26
|
+
|
|
27
|
+
const componentName = 'ste-select-seat';
|
|
28
|
+
const { getColor } = useColorStore();
|
|
29
|
+
const themeColor = getColor()?.steThemeColor || '#0090FF';
|
|
30
|
+
|
|
31
|
+
defineOptions({
|
|
32
|
+
name: componentName,
|
|
33
|
+
options: {
|
|
34
|
+
virtualHost: true,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const instance = getCurrentInstance() as unknown as ComponentPublicInstance;
|
|
39
|
+
const props = defineProps(propsData);
|
|
40
|
+
const emit = defineEmits<{
|
|
41
|
+
(e: 'update:modelValue', value: SteSelectSeatValue[]): void;
|
|
42
|
+
(e: 'seat-click', seat: SteSelectSeatItem): void;
|
|
43
|
+
(e: 'move', data: SteSelectSeatMoveEvent): void;
|
|
44
|
+
}>();
|
|
45
|
+
|
|
46
|
+
const { getSeat, setSeat, getSeats, isSelected, toggleSeat } = useData(props);
|
|
47
|
+
|
|
48
|
+
const canvasId = 'ste-select-seat-' + utils.guid(8);
|
|
49
|
+
const touchHandler = new TouchScaleing();
|
|
50
|
+
|
|
51
|
+
// ─── 手势状态变量 ────────────────────────────────────────────────────────────
|
|
52
|
+
let canvasCtx: any = null;
|
|
53
|
+
let dpr = 1;
|
|
54
|
+
|
|
55
|
+
// ─── 响应式视口状态 ───────────────────────────────────────────────────────────
|
|
56
|
+
const viewportTranslateY = ref(0);
|
|
57
|
+
const viewportScale = ref(1);
|
|
58
|
+
|
|
59
|
+
// ─── 尺寸 / 布局计算 ─────────────────────────────────────────────────────────
|
|
60
|
+
const seatSizePx = computed(() => utils.formatPx(props.seatSize, 'num') as number);
|
|
61
|
+
const seatGapPx = computed(() => utils.formatPx(props.seatGap, 'num') as number);
|
|
62
|
+
const borderRadiusPx = computed(() => utils.formatPx(props.borderRadius, 'num') as number);
|
|
63
|
+
const safeGrid = computed(() => getSafeGridSize(props.rows, props.cols));
|
|
64
|
+
const safeRows = computed(() => safeGrid.value.rows);
|
|
65
|
+
const safeCols = computed(() => safeGrid.value.cols);
|
|
66
|
+
const labelWidthPx = computed(() => getLabelWidth(props.showRowLabels, seatSizePx.value, seatGapPx.value));
|
|
67
|
+
const rowLabelTrackWidthPx = computed(() => 18);
|
|
68
|
+
const labelOverlayWidth = computed(() => `${rowLabelTrackWidthPx.value}px`);
|
|
69
|
+
|
|
70
|
+
const canvasStyle = computed(() => ({
|
|
71
|
+
width: `${props.width}px`,
|
|
72
|
+
height: `${props.height}px`,
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const getContentSize = () => {
|
|
76
|
+
return getSeatContentSize({
|
|
77
|
+
rows: safeRows.value,
|
|
78
|
+
cols: safeCols.value,
|
|
79
|
+
seatSize: seatSizePx.value,
|
|
80
|
+
seatGap: seatGapPx.value,
|
|
81
|
+
labelWidth: labelWidthPx.value,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const fitScale = computed(() => {
|
|
86
|
+
const contentSize = getContentSize();
|
|
87
|
+
return getFitScale({
|
|
88
|
+
width: props.width,
|
|
89
|
+
height: props.height,
|
|
90
|
+
contentWidth: contentSize.width,
|
|
91
|
+
contentHeight: contentSize.height,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const effectiveMinScale = computed(() => fitScale.value);
|
|
96
|
+
|
|
97
|
+
const clampScale = (scale: number): number => {
|
|
98
|
+
return clampSeatScale(scale, effectiveMinScale.value, INTERNAL_MAX_SCALE);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ─── Canvas 绘制 ───────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const draw = () => {
|
|
104
|
+
const ctx = canvasCtx;
|
|
105
|
+
if (!ctx) return;
|
|
106
|
+
|
|
107
|
+
const userScale = clampScale(touchHandler.scale);
|
|
108
|
+
viewportTranslateY.value = touchHandler.translateY;
|
|
109
|
+
viewportScale.value = userScale;
|
|
110
|
+
const tx = touchHandler.translateX;
|
|
111
|
+
const ty = touchHandler.translateY;
|
|
112
|
+
const size = seatSizePx.value;
|
|
113
|
+
const gap = seatGapPx.value;
|
|
114
|
+
const radius = borderRadiusPx.value;
|
|
115
|
+
const labelWidth = labelWidthPx.value;
|
|
116
|
+
const defaultSelectedBg = props.selectedBgColor || themeColor;
|
|
117
|
+
|
|
118
|
+
ctx.clearRect(0, 0, props.width, props.height);
|
|
119
|
+
|
|
120
|
+
// #ifndef APP
|
|
121
|
+
ctx.save();
|
|
122
|
+
ctx.translate(tx * userScale, ty * userScale);
|
|
123
|
+
ctx.scale(userScale, userScale);
|
|
124
|
+
// #endif
|
|
125
|
+
|
|
126
|
+
for (let r = 0; r < safeRows.value; r++) {
|
|
127
|
+
for (let c = 0; c < safeCols.value; c++) {
|
|
128
|
+
const seat = getSeat(r, c);
|
|
129
|
+
if (!seat || seat.empty) continue;
|
|
130
|
+
|
|
131
|
+
const selected = isSelected(r, c);
|
|
132
|
+
|
|
133
|
+
// #ifndef APP
|
|
134
|
+
const x = labelWidth + c * (size + gap) + gap / 2;
|
|
135
|
+
const y = r * (size + gap) + gap / 2;
|
|
136
|
+
const w = size;
|
|
137
|
+
const h = size;
|
|
138
|
+
const r_ = radius;
|
|
139
|
+
// #endif
|
|
140
|
+
// #ifdef APP
|
|
141
|
+
const x = tx * userScale + (labelWidth + c * (size + gap) + gap / 2) * userScale;
|
|
142
|
+
const y = ty * userScale + (r * (size + gap) + gap / 2) * userScale;
|
|
143
|
+
const w = size * userScale;
|
|
144
|
+
const h = size * userScale;
|
|
145
|
+
const r_ = radius * userScale;
|
|
146
|
+
// #endif
|
|
147
|
+
|
|
148
|
+
if (seat.disabled) {
|
|
149
|
+
ctx.fillStyle = props.disabledBgColor;
|
|
150
|
+
} else if (selected) {
|
|
151
|
+
ctx.fillStyle = seat.selectedBgColor || defaultSelectedBg;
|
|
152
|
+
} else {
|
|
153
|
+
ctx.fillStyle = seat.bgColor || props.bgColor;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
drawRoundRect(ctx, x, y, w, h, r_);
|
|
157
|
+
ctx.fill();
|
|
158
|
+
|
|
159
|
+
if (!seat.disabled && !selected) {
|
|
160
|
+
ctx.strokeStyle = seat.borderColor || props.borderColor;
|
|
161
|
+
ctx.lineWidth = props.borderWidth;
|
|
162
|
+
drawRoundRect(ctx, x, y, w, h, r_);
|
|
163
|
+
ctx.stroke();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (selected && !seat.disabled) {
|
|
167
|
+
drawCheck(ctx, x + w / 2, y + h / 2, w, seat.selectedColor || props.selectedColor);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// #ifndef APP
|
|
173
|
+
ctx.restore();
|
|
174
|
+
// #endif
|
|
175
|
+
|
|
176
|
+
// #ifdef H5 || APP
|
|
177
|
+
if (ctx.draw) ctx.draw(true);
|
|
178
|
+
// #endif
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ─── Canvas 初始化 / 生命周期 ──────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
const initCanvas = () => {
|
|
184
|
+
nextTick(() => {
|
|
185
|
+
// #ifdef H5 || APP
|
|
186
|
+
canvasCtx = uni.createCanvasContext(canvasId, instance);
|
|
187
|
+
dpr = 1;
|
|
188
|
+
applyDefaultViewport();
|
|
189
|
+
draw();
|
|
190
|
+
// #endif
|
|
191
|
+
|
|
192
|
+
// #ifdef MP-WEIXIN || MP-ALIPAY
|
|
193
|
+
uni.createSelectorQuery()
|
|
194
|
+
.in(instance)
|
|
195
|
+
.select(`#${canvasId}`)
|
|
196
|
+
.node((res: any) => {
|
|
197
|
+
if (!res || !res.node) return;
|
|
198
|
+
const canvasNode = res.node;
|
|
199
|
+
dpr = utils.System.getWindowInfo().pixelRatio;
|
|
200
|
+
canvasNode.width = props.width * dpr;
|
|
201
|
+
canvasNode.height = props.height * dpr;
|
|
202
|
+
canvasCtx = canvasNode.getContext('2d');
|
|
203
|
+
canvasCtx.scale(dpr, dpr);
|
|
204
|
+
applyDefaultViewport();
|
|
205
|
+
draw();
|
|
206
|
+
})
|
|
207
|
+
.exec();
|
|
208
|
+
// #endif
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// ─── 触摸命中检测 ────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
const getTouchSeat = (touchX: number, touchY: number): SteSelectSeatItem | null => {
|
|
215
|
+
const scale = clampScale(touchHandler.scale);
|
|
216
|
+
const size = seatSizePx.value;
|
|
217
|
+
const gap = seatGapPx.value;
|
|
218
|
+
const labelWidth = labelWidthPx.value;
|
|
219
|
+
const tx = touchHandler.translateX;
|
|
220
|
+
const ty = touchHandler.translateY;
|
|
221
|
+
|
|
222
|
+
const col = Math.floor((touchX / scale - tx - labelWidth - gap / 2) / (size + gap));
|
|
223
|
+
const row = Math.floor((touchY / scale - ty - gap / 2) / (size + gap));
|
|
224
|
+
|
|
225
|
+
if (row < 0 || row >= safeRows.value || col < 0 || col >= safeCols.value) return null;
|
|
226
|
+
return getSeat(row, col) || null;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const getTouchLocalPoint = (touch: any, rect?: { left?: number; top?: number } | null) => {
|
|
230
|
+
if (!touch) return { x: 0, y: 0 };
|
|
231
|
+
if (typeof touch.x === 'number' && typeof touch.y === 'number') {
|
|
232
|
+
return { x: touch.x, y: touch.y };
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
x: getTouchX(touch) - (rect?.left ?? 0),
|
|
236
|
+
y: getTouchY(touch) - (rect?.top ?? 0),
|
|
237
|
+
};
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// ─── 视口变换辅助 ─────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
const getScreenTranslateX = (scale = clampScale(touchHandler.scale), translateX = touchHandler.translateX) => {
|
|
243
|
+
return getSeatScreenTranslateX({
|
|
244
|
+
scale,
|
|
245
|
+
translateX,
|
|
246
|
+
width: props.width,
|
|
247
|
+
defaultViewport: getDefaultViewport(),
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const emitMove = () => {
|
|
252
|
+
const scale = clampScale(touchHandler.scale);
|
|
253
|
+
emit('move', {
|
|
254
|
+
translateX: touchHandler.translateX,
|
|
255
|
+
translateY: touchHandler.translateY,
|
|
256
|
+
scale,
|
|
257
|
+
screenTranslateX: getScreenTranslateX(scale, touchHandler.translateX),
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const getTranslateBounds = (scale = touchHandler.scale) => {
|
|
262
|
+
const contentSize = getContentSize();
|
|
263
|
+
return getSeatTranslateBounds({
|
|
264
|
+
scale: clampScale(scale),
|
|
265
|
+
width: props.width,
|
|
266
|
+
height: props.height,
|
|
267
|
+
contentWidth: contentSize.width,
|
|
268
|
+
contentHeight: contentSize.height,
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const applyAxisResistance = (value: number, min: number, max: number) => {
|
|
273
|
+
const overscrollResistance = 0.35;
|
|
274
|
+
if (value < min) {
|
|
275
|
+
return min + (value - min) * overscrollResistance;
|
|
276
|
+
}
|
|
277
|
+
if (value > max) {
|
|
278
|
+
return max + (value - max) * overscrollResistance;
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const applyTranslateResistance = (x: number, y: number, scale = touchHandler.scale) => {
|
|
284
|
+
const bounds = getTranslateBounds(scale);
|
|
285
|
+
return {
|
|
286
|
+
x: applyAxisResistance(x, bounds.minX, bounds.maxX),
|
|
287
|
+
y: applyAxisResistance(y, bounds.minY, bounds.maxY),
|
|
288
|
+
};
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const clampTranslate = (x: number, y: number, scale = touchHandler.scale) => {
|
|
292
|
+
const bounds = getTranslateBounds(scale);
|
|
293
|
+
return {
|
|
294
|
+
x: Math.min(bounds.maxX, Math.max(bounds.minX, x)),
|
|
295
|
+
y: Math.min(bounds.maxY, Math.max(bounds.minY, y)),
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const rowLabelItems = computed(() => {
|
|
300
|
+
if (!props.showRowLabels) return [];
|
|
301
|
+
return buildRowLabelItems({
|
|
302
|
+
rows: safeRows.value,
|
|
303
|
+
height: props.height,
|
|
304
|
+
seatSize: seatSizePx.value,
|
|
305
|
+
seatGap: seatGapPx.value,
|
|
306
|
+
translateY: viewportTranslateY.value,
|
|
307
|
+
scale: viewportScale.value,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const rowLabelTrackStyle = computed(() => {
|
|
312
|
+
return getRowLabelTrackStyle(rowLabelItems.value, props.height);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const getDefaultViewport = () => {
|
|
316
|
+
const contentSize = getContentSize();
|
|
317
|
+
return getDefaultSeatViewport({
|
|
318
|
+
fitScale: fitScale.value,
|
|
319
|
+
width: props.width,
|
|
320
|
+
height: props.height,
|
|
321
|
+
contentWidth: contentSize.width,
|
|
322
|
+
contentHeight: contentSize.height,
|
|
323
|
+
maxScale: INTERNAL_MAX_SCALE,
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const applyDefaultViewport = () => {
|
|
328
|
+
const viewport = getDefaultViewport();
|
|
329
|
+
touchHandler.scale = viewport.scale;
|
|
330
|
+
touchHandler.baseScale = viewport.scale;
|
|
331
|
+
touchHandler.translateX = viewport.translateX;
|
|
332
|
+
touchHandler.translateY = viewport.translateY;
|
|
333
|
+
touchHandler.baseTranslateX = viewport.translateX;
|
|
334
|
+
touchHandler.baseTranslateY = viewport.translateY;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const { rowLabelsVisible, setShowRowLabelsVisible, onTouchStart, onTouchMove, onTouchEnd, onMouseDown, onMouseMove, onMouseUp, reset } = useSeatInteraction({
|
|
338
|
+
instance,
|
|
339
|
+
canvasId,
|
|
340
|
+
getShowRowLabels: () => props.showRowLabels,
|
|
341
|
+
touchHandler,
|
|
342
|
+
clampScale,
|
|
343
|
+
applyTranslateResistance,
|
|
344
|
+
clampTranslate,
|
|
345
|
+
getTouchSeat,
|
|
346
|
+
getTouchLocalPoint,
|
|
347
|
+
applyDefaultViewport,
|
|
348
|
+
draw,
|
|
349
|
+
emitMove,
|
|
350
|
+
emitSeatClick: seat => emit('seat-click', seat),
|
|
351
|
+
emitModelValue: value => emit('update:modelValue', value),
|
|
352
|
+
toggleSeat,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
onMounted(() => {
|
|
356
|
+
initCanvas();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
watch(
|
|
360
|
+
() => props.showRowLabels,
|
|
361
|
+
value => {
|
|
362
|
+
setShowRowLabelsVisible(value);
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
watch(
|
|
367
|
+
() => [
|
|
368
|
+
props.modelValue,
|
|
369
|
+
props.seats,
|
|
370
|
+
props.rows,
|
|
371
|
+
props.cols,
|
|
372
|
+
props.width,
|
|
373
|
+
props.height,
|
|
374
|
+
props.seatSize,
|
|
375
|
+
props.seatGap,
|
|
376
|
+
props.borderRadius,
|
|
377
|
+
props.borderWidth,
|
|
378
|
+
props.bgColor,
|
|
379
|
+
props.borderColor,
|
|
380
|
+
props.selectedBgColor,
|
|
381
|
+
props.selectedColor,
|
|
382
|
+
props.disabledBgColor,
|
|
383
|
+
props.showRowLabels,
|
|
384
|
+
],
|
|
385
|
+
() => {
|
|
386
|
+
if (canvasCtx) draw();
|
|
387
|
+
},
|
|
388
|
+
{ deep: true }
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// ─── 对外暴露接口 ─────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
defineExpose({
|
|
394
|
+
setSeat: (row: number, col: number, data: Partial<SteSelectSeatItem>) => {
|
|
395
|
+
setSeat(row, col, data);
|
|
396
|
+
nextTick(() => draw());
|
|
397
|
+
},
|
|
398
|
+
getSeats,
|
|
399
|
+
reset,
|
|
400
|
+
});
|
|
401
|
+
</script>
|
|
402
|
+
|
|
403
|
+
<template>
|
|
404
|
+
<view class="ste-select-seat-root" :style="canvasStyle">
|
|
405
|
+
<!-- #ifdef H5 -->
|
|
406
|
+
<canvas
|
|
407
|
+
:canvas-id="canvasId"
|
|
408
|
+
:id="canvasId"
|
|
409
|
+
:style="canvasStyle"
|
|
410
|
+
class="seat-canvas"
|
|
411
|
+
disable-scroll
|
|
412
|
+
@touchstart="onTouchStart"
|
|
413
|
+
@touchmove.stop.prevent="onTouchMove"
|
|
414
|
+
@touchend="onTouchEnd"
|
|
415
|
+
@mousedown="onMouseDown"
|
|
416
|
+
@mousemove="onMouseMove"
|
|
417
|
+
@mouseup="onMouseUp"
|
|
418
|
+
@mouseleave="onMouseUp"
|
|
419
|
+
/>
|
|
420
|
+
<!-- #endif -->
|
|
421
|
+
|
|
422
|
+
<!-- #ifdef APP -->
|
|
423
|
+
<canvas
|
|
424
|
+
:canvas-id="canvasId"
|
|
425
|
+
:id="canvasId"
|
|
426
|
+
:style="canvasStyle"
|
|
427
|
+
class="seat-canvas"
|
|
428
|
+
disable-scroll
|
|
429
|
+
@touchstart="onTouchStart"
|
|
430
|
+
@touchmove="onTouchMove"
|
|
431
|
+
@touchend="onTouchEnd"
|
|
432
|
+
@touchcancel="onTouchEnd"
|
|
433
|
+
/>
|
|
434
|
+
<!-- #endif -->
|
|
435
|
+
|
|
436
|
+
<!-- #ifdef MP-WEIXIN || MP-ALIPAY -->
|
|
437
|
+
<canvas
|
|
438
|
+
type="2d"
|
|
439
|
+
:id="canvasId"
|
|
440
|
+
:style="canvasStyle"
|
|
441
|
+
class="seat-canvas"
|
|
442
|
+
disable-scroll
|
|
443
|
+
@touchstart="onTouchStart"
|
|
444
|
+
@touchmove.stop.prevent="onTouchMove"
|
|
445
|
+
@touchend="onTouchEnd"
|
|
446
|
+
@touchcancel="onTouchEnd"
|
|
447
|
+
/>
|
|
448
|
+
<!-- #endif -->
|
|
449
|
+
|
|
450
|
+
<view v-if="props.showRowLabels" class="row-label-overlay" :class="{ 'is-visible': rowLabelsVisible }">
|
|
451
|
+
<view class="row-label-track" :style="rowLabelTrackStyle" />
|
|
452
|
+
<view v-for="item in rowLabelItems" :key="item.row" class="row-label-item" :style="item.style">
|
|
453
|
+
{{ item.row + 1 }}
|
|
454
|
+
</view>
|
|
455
|
+
</view>
|
|
456
|
+
</view>
|
|
457
|
+
</template>
|
|
458
|
+
|
|
459
|
+
<style lang="scss" scoped>
|
|
460
|
+
.ste-select-seat-root {
|
|
461
|
+
overflow: hidden;
|
|
462
|
+
position: relative;
|
|
463
|
+
touch-action: none;
|
|
464
|
+
user-select: none;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.seat-canvas {
|
|
468
|
+
display: block;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.row-label-overlay {
|
|
472
|
+
position: absolute;
|
|
473
|
+
left: 6px;
|
|
474
|
+
top: 0;
|
|
475
|
+
bottom: 0;
|
|
476
|
+
width: v-bind(labelOverlayWidth);
|
|
477
|
+
pointer-events: none;
|
|
478
|
+
opacity: 0;
|
|
479
|
+
transform: translateX(-4px);
|
|
480
|
+
transition:
|
|
481
|
+
opacity 160ms ease,
|
|
482
|
+
transform 160ms ease;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.row-label-overlay.is-visible {
|
|
486
|
+
opacity: 1;
|
|
487
|
+
transform: translateX(0);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.row-label-track {
|
|
491
|
+
position: absolute;
|
|
492
|
+
left: 0;
|
|
493
|
+
width: 100%;
|
|
494
|
+
border-radius: 999px;
|
|
495
|
+
background: rgba(199, 199, 199, 0.68);
|
|
496
|
+
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
|
497
|
+
transition:
|
|
498
|
+
opacity 160ms ease,
|
|
499
|
+
transform 160ms ease;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.row-label-item {
|
|
503
|
+
position: absolute;
|
|
504
|
+
left: 0;
|
|
505
|
+
display: flex;
|
|
506
|
+
align-items: center;
|
|
507
|
+
justify-content: center;
|
|
508
|
+
width: 100%;
|
|
509
|
+
color: rgba(255, 255, 255, 0.98);
|
|
510
|
+
font-weight: 400;
|
|
511
|
+
letter-spacing: 0;
|
|
512
|
+
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
|
|
513
|
+
transition:
|
|
514
|
+
opacity 160ms ease,
|
|
515
|
+
transform 160ms ease;
|
|
516
|
+
}
|
|
517
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 选中的座位值,仅表示已选中的座位坐标 */
|
|
2
|
+
export interface SteSelectSeatValue {
|
|
3
|
+
row: number
|
|
4
|
+
col: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** 座位项数据,用于描述座位属性,不表示选中状态 */
|
|
8
|
+
export interface SteSelectSeatItem {
|
|
9
|
+
/** 行号(从0开始) */
|
|
10
|
+
row: number
|
|
11
|
+
/** 列号(从0开始) */
|
|
12
|
+
col: number
|
|
13
|
+
/** 是否禁用 */
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
/** 是否为空位(不渲染) */
|
|
16
|
+
empty?: boolean
|
|
17
|
+
/** 自定义背景色 */
|
|
18
|
+
bgColor?: string
|
|
19
|
+
/** 自定义边框颜色 */
|
|
20
|
+
borderColor?: string
|
|
21
|
+
/** 自定义选中背景色 */
|
|
22
|
+
selectedBgColor?: string
|
|
23
|
+
/** 自定义选中文字/图标颜色 */
|
|
24
|
+
selectedColor?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 移动事件参数 */
|
|
28
|
+
export interface SteSelectSeatMoveEvent {
|
|
29
|
+
translateX: number
|
|
30
|
+
translateY: number
|
|
31
|
+
scale: number
|
|
32
|
+
screenTranslateX: number
|
|
33
|
+
}
|