stellar-ui-plus 1.24.25 → 1.24.27
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/ste-app-update.vue +2 -7
- package/components/ste-dropdown-menu/props.ts +1 -0
- package/components/ste-dropdown-menu/ste-dropdown-menu.vue +1 -0
- package/components/ste-filter-tool/ATTRIBUTES.md +1 -0
- package/components/ste-filter-tool/props.ts +1 -0
- package/components/ste-filter-tool/ste-filter-tool.easycom.json +6 -0
- package/components/ste-filter-tool/ste-filter-tool.vue +6 -2
- package/components/ste-filter-tool/useData.ts +8 -0
- 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/README.md +45 -0
- package/components/ste-skeleton/config.json +5 -0
- package/components/ste-skeleton/props.ts +7 -0
- package/components/ste-skeleton/ste-skeleton.json +38 -0
- package/components/ste-skeleton/ste-skeleton.vue +108 -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
|
@@ -33,7 +33,6 @@ const packageFileSize = ref('0');
|
|
|
33
33
|
const tempFilePath = ref('');
|
|
34
34
|
|
|
35
35
|
// 资源管理
|
|
36
|
-
let downloadTask: UniApp.DownloadTask | null = null;
|
|
37
36
|
let timeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
38
37
|
|
|
39
38
|
// 跳过版本相关
|
|
@@ -90,10 +89,6 @@ const skipVersion = () => {
|
|
|
90
89
|
|
|
91
90
|
// 清理函数
|
|
92
91
|
const cleanup = () => {
|
|
93
|
-
if (downloadTask) {
|
|
94
|
-
downloadTask.abort();
|
|
95
|
-
downloadTask = null;
|
|
96
|
-
}
|
|
97
92
|
if (timeoutTimer) {
|
|
98
93
|
clearTimeout(timeoutTimer);
|
|
99
94
|
timeoutTimer = null;
|
|
@@ -244,7 +239,7 @@ const confirm = () => {
|
|
|
244
239
|
if (data.package_type == 0) {
|
|
245
240
|
if (data.updateFile.includes('.apk')) {
|
|
246
241
|
updateBtn.value = false;
|
|
247
|
-
|
|
242
|
+
downloadMethod(data, {
|
|
248
243
|
onProgressUpdate,
|
|
249
244
|
downloadSuccess: path => (tempFilePath.value = path),
|
|
250
245
|
error: () => {
|
|
@@ -263,7 +258,7 @@ const confirm = () => {
|
|
|
263
258
|
}
|
|
264
259
|
} else {
|
|
265
260
|
updateBtn.value = false;
|
|
266
|
-
|
|
261
|
+
downloadMethod(data, {
|
|
267
262
|
onProgressUpdate,
|
|
268
263
|
downloadSuccess: path => (tempFilePath.value = path),
|
|
269
264
|
error: () => {
|
|
@@ -26,6 +26,7 @@ export default dropDownMenuProps;
|
|
|
26
26
|
export const dropDownMenuEmits = {
|
|
27
27
|
close: () => true,
|
|
28
28
|
open: () => true,
|
|
29
|
+
maskClick: () => true,
|
|
29
30
|
change: (value: Array<any>) => Array.isArray(value),
|
|
30
31
|
'item-choose': (item: any) => item,
|
|
31
32
|
'update:modelValue': (value: Array<any>) => Array.isArray(value),
|
|
@@ -35,6 +35,7 @@ export const filterToolEmits = {
|
|
|
35
35
|
// 值变化(实时)
|
|
36
36
|
'update:value': (values: FilterValue[]) => Array.isArray(values),
|
|
37
37
|
itemClick: (item: FilterValue) => item instanceof Object,
|
|
38
|
+
maskClose: () => true,
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
export type FilterToolProps = ExtractPropTypes<typeof props>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<view class="ste-filter-tool--root" :style="[rootStyleVar, { '--category-count': categoryData.length }]">
|
|
3
|
-
<ste-dropdown-menu ref="steDropMenu" class="filter-box-menu" :activeColor="activeColor" dropDownIconColor="#000" v-model:showPopup="showMenu">
|
|
3
|
+
<ste-dropdown-menu ref="steDropMenu" class="filter-box-menu" :activeColor="activeColor" dropDownIconColor="#000" v-model:showPopup="showMenu" @maskClick="handleMaskClick">
|
|
4
4
|
<template #title>
|
|
5
5
|
<slot>
|
|
6
6
|
<view></view>
|
|
@@ -146,7 +146,7 @@ const rootStyleVar = computed(() => ({
|
|
|
146
146
|
}));
|
|
147
147
|
|
|
148
148
|
// 使用简化的筛选逻辑组合式函数
|
|
149
|
-
const { handleFilterClick, handleCheckboxChange, handleReset, handleConfirm } = useData(props, emits, filtersData);
|
|
149
|
+
const { handleFilterClick, handleCheckboxChange, handleReset, handleConfirm, handleMaskClose } = useData(props, emits, filtersData);
|
|
150
150
|
|
|
151
151
|
// 工具类实例
|
|
152
152
|
const calculator = new ScrollCalculator(instance, filtersData);
|
|
@@ -217,6 +217,10 @@ const toggleExpand = (item: FilterItem) => {
|
|
|
217
217
|
item.expand = !item.expand;
|
|
218
218
|
};
|
|
219
219
|
|
|
220
|
+
function handleMaskClick() {
|
|
221
|
+
handleMaskClose();
|
|
222
|
+
}
|
|
223
|
+
|
|
220
224
|
// 监听器
|
|
221
225
|
watch(
|
|
222
226
|
() => props.data,
|
|
@@ -138,6 +138,13 @@ export default function useSimpleFilterLogic(props: FilterToolProps, emits: Setu
|
|
|
138
138
|
emits('confirm', currentValues);
|
|
139
139
|
};
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* 点击蒙层关闭了弹窗后触发
|
|
143
|
+
*/
|
|
144
|
+
const handleMaskClose = () => {
|
|
145
|
+
emits('maskClose');
|
|
146
|
+
};
|
|
147
|
+
|
|
141
148
|
// 监听props.value变化,同步到组件内部状态
|
|
142
149
|
watch(
|
|
143
150
|
() => props.value,
|
|
@@ -156,6 +163,7 @@ export default function useSimpleFilterLogic(props: FilterToolProps, emits: Setu
|
|
|
156
163
|
handleCheckboxChange,
|
|
157
164
|
handleReset,
|
|
158
165
|
handleConfirm,
|
|
166
|
+
handleMaskClose,
|
|
159
167
|
collectCurrentValues,
|
|
160
168
|
setValuesFromProps,
|
|
161
169
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#### Props
|
|
2
|
+
| 属性名 | 说明 | 类型 | 默认值 | 可选值 | 支持版本 |
|
|
3
|
+
| ----- | ----- | --- | ------- | ------ | -------- |
|
|
4
|
+
| `modelValue` | 已选座位坐标列表(仅包含 row/col) | `SteSelectSeatValue[]` | - | - | - |
|
|
5
|
+
| `rows` | 行数 | `number` | - | - | - |
|
|
6
|
+
| `cols` | 列数 | `number` | - | - | - |
|
|
7
|
+
| `width` | 组件宽度(px) | `number` | `350` | - | - |
|
|
8
|
+
| `height` | 组件高度(px) | `number` | `400` | - | - |
|
|
9
|
+
| `seats` | 座位属性配置(未配置的位置会自动补齐为默认座位) | `SteSelectSeatItem[]` | - | - | - |
|
|
10
|
+
| `seatSize` | 座位尺寸(rpx) | `number` | `40` | - | - |
|
|
11
|
+
| `seatGap` | 座位间距(rpx) | `number` | `8` | - | - |
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
#### Events
|
|
15
|
+
| 事件名 | 说明 | 事件参数 | 支持版本 |
|
|
16
|
+
| ----- | ----- | ------- | -------- |
|
|
17
|
+
| `seat-click` | 点击有效座位事件(empty/disabled 不触发) | - | - |
|
|
18
|
+
| `move` | 拖动/缩放事件 | - | - |
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# SelectSeat 座位选择
|
|
2
|
+
|
|
3
|
+
用于电影院、演出、车厢选座等场景的座位选择组件,基于 Canvas 渲染,支持单指拖拽、双指缩放,适合中大规模座位图展示。
|
|
4
|
+
|
|
5
|
+
---$
|
|
6
|
+
|
|
7
|
+
## 代码演示
|
|
8
|
+
|
|
9
|
+
### 基础用法
|
|
10
|
+
|
|
11
|
+
最简单的用法只需要传入行列数和画布尺寸。
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<template>
|
|
15
|
+
<ste-select-seat v-model="selected" :rows="5" :cols="8" :width="340" :height="240" />
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script lang="ts" setup>
|
|
19
|
+
import { ref } from 'vue';
|
|
20
|
+
import type { SteSelectSeatValue } from 'stellar-ui-plus/components/ste-select-seat/types';
|
|
21
|
+
|
|
22
|
+
const selected = ref<SteSelectSeatValue[]>([]);
|
|
23
|
+
</script>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 自定义座位
|
|
27
|
+
|
|
28
|
+
通过 `seats` 可以覆盖指定坐标的座位属性,常见场景包括:
|
|
29
|
+
|
|
30
|
+
- `empty: true` 表示该位置留空,不渲染座位
|
|
31
|
+
- `disabled: true` 表示该位置不可选
|
|
32
|
+
- 也可以单独配置某个区域的颜色
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<template>
|
|
36
|
+
<ste-select-seat v-model="selected" :rows="5" :cols="8" :width="340" :height="240" :seats="customSeats" selectedBgColor="#007aff" />
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script lang="ts" setup>
|
|
40
|
+
import { ref } from 'vue';
|
|
41
|
+
import type { SteSelectSeatItem, SteSelectSeatValue } from 'stellar-ui-plus/components/ste-select-seat/types';
|
|
42
|
+
|
|
43
|
+
const selected = ref<SteSelectSeatValue[]>([]);
|
|
44
|
+
|
|
45
|
+
const customSeats: SteSelectSeatItem[] = [
|
|
46
|
+
{ row: 0, col: 2, empty: true },
|
|
47
|
+
{ row: 0, col: 3, empty: true },
|
|
48
|
+
{ row: 1, col: 0, disabled: true },
|
|
49
|
+
{ row: 1, col: 1, disabled: true },
|
|
50
|
+
{ row: 2, col: 4, bgColor: '#fff3e0', borderColor: '#ff9800', selectedBgColor: '#ff9800' },
|
|
51
|
+
{ row: 2, col: 5, bgColor: '#fff3e0', borderColor: '#ff9800', selectedBgColor: '#ff9800' },
|
|
52
|
+
{ row: 4, col: 0, empty: true },
|
|
53
|
+
{ row: 4, col: 7, empty: true },
|
|
54
|
+
];
|
|
55
|
+
</script>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 预选座位
|
|
59
|
+
|
|
60
|
+
预选本质上就是给 `v-model` 初始值。这里的坐标只表示“已选状态”,不负责定义座位是否禁用、是否留空。
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<template>
|
|
64
|
+
<ste-select-seat v-model="selected" :rows="5" :cols="8" :width="340" :height="240" />
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script lang="ts" setup>
|
|
68
|
+
import { ref } from 'vue';
|
|
69
|
+
import type { SteSelectSeatValue } from 'stellar-ui-plus/components/ste-select-seat/types';
|
|
70
|
+
|
|
71
|
+
const selected = ref<SteSelectSeatValue[]>([
|
|
72
|
+
{ row: 1, col: 3 },
|
|
73
|
+
{ row: 1, col: 4 },
|
|
74
|
+
{ row: 2, col: 3 },
|
|
75
|
+
{ row: 2, col: 4 },
|
|
76
|
+
]);
|
|
77
|
+
</script>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 重置位置
|
|
81
|
+
|
|
82
|
+
组件实例暴露了 `reset()`,用于把当前缩放和拖拽视口恢复到初始状态,不会清空已选座位。
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<template>
|
|
86
|
+
<ste-select-seat ref="seatRef" v-model="selected" :rows="5" :cols="8" :width="340" :height="240" />
|
|
87
|
+
<button @click="reset">重置位置</button>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<script lang="ts" setup>
|
|
91
|
+
import { ref } from 'vue';
|
|
92
|
+
import type { SteSelectSeatValue } from 'stellar-ui-plus/components/ste-select-seat/types';
|
|
93
|
+
|
|
94
|
+
const seatRef = ref<any>(null);
|
|
95
|
+
const selected = ref<SteSelectSeatValue[]>([]);
|
|
96
|
+
|
|
97
|
+
const reset = () => {
|
|
98
|
+
seatRef.value?.reset();
|
|
99
|
+
};
|
|
100
|
+
</script>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 电影院座位图
|
|
104
|
+
|
|
105
|
+
复杂场景通常会同时用到以下能力:
|
|
106
|
+
|
|
107
|
+
- 通过 `seats` 区分已售、优选区、情侣座
|
|
108
|
+
- 通过 `empty` 留出过道、边角空位
|
|
109
|
+
- 通过 `move` 事件同步顶部银幕等浮层位置
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<script lang="ts" setup>
|
|
113
|
+
import { computed, ref } from 'vue';
|
|
114
|
+
import type { SteSelectSeatItem, SteSelectSeatMoveEvent, SteSelectSeatValue } from 'stellar-ui-plus/components/ste-select-seat/types';
|
|
115
|
+
|
|
116
|
+
type SeatCoord = { row: number; col: number };
|
|
117
|
+
|
|
118
|
+
const rows = 12;
|
|
119
|
+
const cols = 20;
|
|
120
|
+
const selected = ref<SteSelectSeatValue[]>([
|
|
121
|
+
{ row: 6, col: 7 },
|
|
122
|
+
{ row: 6, col: 8 },
|
|
123
|
+
]);
|
|
124
|
+
const viewport = ref<SteSelectSeatMoveEvent>({
|
|
125
|
+
translateX: 0,
|
|
126
|
+
translateY: 0,
|
|
127
|
+
scale: 1,
|
|
128
|
+
screenTranslateX: 0,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const soldSeatCoords: SeatCoord[] = [
|
|
132
|
+
{ row: 2, col: 3 },
|
|
133
|
+
{ row: 2, col: 4 },
|
|
134
|
+
{ row: 4, col: 6 },
|
|
135
|
+
{ row: 7, col: 14 },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const vipSeatCoords: SeatCoord[] = [
|
|
139
|
+
{ row: 6, col: 6 },
|
|
140
|
+
{ row: 6, col: 7 },
|
|
141
|
+
{ row: 6, col: 8 },
|
|
142
|
+
{ row: 6, col: 11 },
|
|
143
|
+
{ row: 6, col: 12 },
|
|
144
|
+
{ row: 6, col: 13 },
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const coupleSeatCoords: SeatCoord[] = [
|
|
148
|
+
{ row: 10, col: 5 },
|
|
149
|
+
{ row: 10, col: 6 },
|
|
150
|
+
{ row: 10, col: 13 },
|
|
151
|
+
{ row: 10, col: 14 },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const toSeatKey = ({ row, col }: SeatCoord) => `${row}-${col}`;
|
|
155
|
+
const soldSeatSet = new Set(soldSeatCoords.map(toSeatKey));
|
|
156
|
+
const vipSeatSet = new Set(vipSeatCoords.map(toSeatKey));
|
|
157
|
+
const coupleSeatSet = new Set(coupleSeatCoords.map(toSeatKey));
|
|
158
|
+
|
|
159
|
+
const isAisleCol = (col: number) => col === 9 || col === 10;
|
|
160
|
+
const isFrontSideGap = (row: number, col: number) => (row === 0 || row === 1) && (col <= 1 || col >= 18);
|
|
161
|
+
const isBackCornerGap = (row: number, col: number) => row === 11 && (col === 0 || col === 19);
|
|
162
|
+
const isEmptySeat = (row: number, col: number) => isAisleCol(col) || isFrontSideGap(row, col) || isBackCornerGap(row, col);
|
|
163
|
+
|
|
164
|
+
const seats = computed<SteSelectSeatItem[]>(() => {
|
|
165
|
+
const list: SteSelectSeatItem[] = [];
|
|
166
|
+
|
|
167
|
+
for (let row = 0; row < rows; row++) {
|
|
168
|
+
for (let col = 0; col < cols; col++) {
|
|
169
|
+
if (isEmptySeat(row, col)) {
|
|
170
|
+
list.push({ row, col, empty: true });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const key = `${row}-${col}`;
|
|
175
|
+
|
|
176
|
+
if (soldSeatSet.has(key)) {
|
|
177
|
+
list.push({ row, col, disabled: true, bgColor: '#cfd4dc', borderColor: '#cfd4dc' });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (vipSeatSet.has(key)) {
|
|
182
|
+
list.push({ row, col, bgColor: '#fff4d6', borderColor: '#ffb400', selectedBgColor: '#ffb400' });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (coupleSeatSet.has(key)) {
|
|
187
|
+
list.push({ row, col, bgColor: '#ffe4ea', borderColor: '#ff7a9d', selectedBgColor: '#ff5c8a' });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return list;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const onMove = (event: SteSelectSeatMoveEvent) => {
|
|
197
|
+
viewport.value = event;
|
|
198
|
+
};
|
|
199
|
+
</script>
|
|
200
|
+
|
|
201
|
+
<template>
|
|
202
|
+
<view class="seat-stage">
|
|
203
|
+
<view class="screen" :style="{ transform: `translateX(${viewport.screenTranslateX}px)` }">银幕中央</view>
|
|
204
|
+
|
|
205
|
+
<ste-select-seat
|
|
206
|
+
v-model="selected"
|
|
207
|
+
:rows="rows"
|
|
208
|
+
:cols="cols"
|
|
209
|
+
:width="340"
|
|
210
|
+
:height="200"
|
|
211
|
+
:seat-size="30"
|
|
212
|
+
:seat-gap="6"
|
|
213
|
+
:border-radius="8"
|
|
214
|
+
:seats="seats"
|
|
215
|
+
selected-bg-color="#2d6cdf"
|
|
216
|
+
@move="onMove"
|
|
217
|
+
/>
|
|
218
|
+
</view>
|
|
219
|
+
</template>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---$
|
|
223
|
+
|
|
224
|
+
## API
|
|
225
|
+
|
|
226
|
+
<!-- props -->
|
|
227
|
+
|
|
228
|
+
### 组件方法(Methods)
|
|
229
|
+
|
|
230
|
+
通过组件实例 `ref` 可调用以下方法:
|
|
231
|
+
|
|
232
|
+
| 方法名 | 说明 | 参数 |
|
|
233
|
+
| ---------- | ------------------------------------------------ | -------------------------------------------------------------- |
|
|
234
|
+
| `setSeat` | 设置指定座位属性,不直接控制选中状态 | `(row: number, col: number, data: Partial<SteSelectSeatItem>)` |
|
|
235
|
+
| `getSeats` | 获取内部当前全量座位数据,包含自动补齐的普通座位 | - |
|
|
236
|
+
| `reset` | 重置当前缩放和拖拽位置,不清空 `modelValue` | - |
|
|
237
|
+
|
|
238
|
+
`setSeat()` 常用于接口返回后,动态更新某个座位的可售状态或颜色。
|
|
239
|
+
|
|
240
|
+
## 数据说明
|
|
241
|
+
|
|
242
|
+
### 坐标约定
|
|
243
|
+
|
|
244
|
+
- 所有 `row`、`col` 均从 `0` 开始计数
|
|
245
|
+
- `row: 0, col: 0` 表示第一行第一列
|
|
246
|
+
- 页面展示给用户时,通常会自行转成 `row + 1`、`col + 1`
|
|
247
|
+
|
|
248
|
+
### `modelValue` 与 `seats` 的区别
|
|
249
|
+
|
|
250
|
+
- `modelValue` 只表示当前选中了哪些座位
|
|
251
|
+
- `seats` 只表示这些座位是什么属性
|
|
252
|
+
- 两者职责分离,避免把“选中状态”和“座位配置”混在一起
|
|
253
|
+
|
|
254
|
+
例如:
|
|
255
|
+
|
|
256
|
+
- 某个座位是已售:应在 `seats` 中配置 `disabled: true`
|
|
257
|
+
- 某个座位默认选中:应在 `modelValue` 中写入对应坐标
|
|
258
|
+
- 某个位置是过道:应在 `seats` 中配置 `empty: true`
|
|
259
|
+
|
|
260
|
+
## 注意事项
|
|
261
|
+
|
|
262
|
+
- `rows` 和 `cols` 必须为大于 `0` 的整数,否则不会正常渲染座位,并输出告警。
|
|
263
|
+
- `seats` 中缺少合法 `row/col`、或坐标超出 `rows/cols` 范围的项会被忽略,并输出告警。
|
|
264
|
+
- `seats` 中如果存在重复坐标,后一个配置会覆盖前一个,并输出告警。
|
|
265
|
+
- `setSeat()` 同样会校验坐标合法性,越界或非法时不会生效。
|
|
266
|
+
- `empty: true` 的位置不会渲染,也不会触发点击。
|
|
267
|
+
- `disabled: true` 的位置会渲染,但不可选、不会切换选中状态。
|
|
268
|
+
- `width`、`height` 决定可视区域大小;如果座位图较大,建议配合拖拽缩放使用。
|
|
269
|
+
- `showRowLabels` 适合影院、车厢等纵向较长的座位图场景,小型座位图可关闭以减少干扰。
|
|
270
|
+
|
|
271
|
+
## 使用建议
|
|
272
|
+
|
|
273
|
+
- 普通场景只传 `rows`、`cols`、`v-model` 即可。
|
|
274
|
+
- 有过道、不可售、分区价格时,再补充 `seats`。
|
|
275
|
+
- 如果需要“银幕跟随”“顶部浮层跟随”等效果,监听 `move` 事件即可。
|
|
276
|
+
- 如果业务里存在实时锁座、出票状态刷新,建议用 `setSeat()` 做局部更新,而不是每次重建整张座位图。
|
|
277
|
+
|
|
278
|
+
---$
|
|
279
|
+
|
|
280
|
+
{{fuyuwei}}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas 绘制工具函数(纯函数,无外部依赖)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 绘制圆角矩形路径(不填充/描边,需调用方自行 fill/stroke)
|
|
7
|
+
*/
|
|
8
|
+
export const drawRoundRect = (ctx: any, x: number, y: number, w: number, h: number, r: number) => {
|
|
9
|
+
ctx.beginPath();
|
|
10
|
+
ctx.moveTo(x + r, y);
|
|
11
|
+
ctx.lineTo(x + w - r, y);
|
|
12
|
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
13
|
+
ctx.lineTo(x + w, y + h - r);
|
|
14
|
+
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
15
|
+
ctx.lineTo(x + r, y + h);
|
|
16
|
+
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
17
|
+
ctx.lineTo(x, y + r);
|
|
18
|
+
ctx.arcTo(x, y, x + r, y, r);
|
|
19
|
+
ctx.closePath();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 在指定中心点绘制勾选标记(stroke)
|
|
24
|
+
* 视觉上尽量贴近 ste-radio 里的选中 icon,但保持纯 Canvas path,
|
|
25
|
+
* 避免依赖 iconfont 在各端 Canvas 中的字体加载能力。
|
|
26
|
+
* @param cx 中心 x
|
|
27
|
+
* @param cy 中心 y
|
|
28
|
+
* @param size 座位宽度,用于计算线宽与比例
|
|
29
|
+
* @param color 线条颜色
|
|
30
|
+
*/
|
|
31
|
+
export const drawCheck = (ctx: any, cx: number, cy: number, size: number, color: string) => {
|
|
32
|
+
ctx.beginPath();
|
|
33
|
+
ctx.strokeStyle = color;
|
|
34
|
+
ctx.lineWidth = Math.max(1.4, size * 0.1);
|
|
35
|
+
ctx.lineCap = 'round';
|
|
36
|
+
ctx.lineJoin = 'round';
|
|
37
|
+
const s = size * 0.34;
|
|
38
|
+
ctx.moveTo(cx - s * 0.62, cy - s * 0.02);
|
|
39
|
+
ctx.lineTo(cx - s * 0.14, cy + s * 0.42);
|
|
40
|
+
ctx.lineTo(cx + s * 0.62, cy - s * 0.36);
|
|
41
|
+
ctx.stroke();
|
|
42
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SteSelectSeatGridSize {
|
|
2
|
+
rows: number
|
|
3
|
+
cols: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const isInteger = (value: unknown): value is number => {
|
|
7
|
+
return typeof value === 'number' && Number.isInteger(value)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const isPositiveInteger = (value: unknown): value is number => {
|
|
11
|
+
return isInteger(value) && value > 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const getSafeGridSize = (rows: number, cols: number): SteSelectSeatGridSize => {
|
|
15
|
+
return {
|
|
16
|
+
rows: isPositiveInteger(rows) ? rows : 0,
|
|
17
|
+
cols: isPositiveInteger(cols) ? cols : 0,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const isSeatInBounds = (row: number, col: number, rows: number, cols: number) => {
|
|
22
|
+
return row >= 0 && row < rows && col >= 0 && col < cols
|
|
23
|
+
}
|
|
@@ -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
|
+
}
|