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.
Files changed (46) hide show
  1. package/components/ste-app-update/method.ts +1 -0
  2. package/components/ste-app-update/ste-app-update.vue +2 -7
  3. package/components/ste-dropdown-menu/props.ts +1 -0
  4. package/components/ste-dropdown-menu/ste-dropdown-menu.vue +1 -0
  5. package/components/ste-filter-tool/ATTRIBUTES.md +1 -0
  6. package/components/ste-filter-tool/props.ts +1 -0
  7. package/components/ste-filter-tool/ste-filter-tool.easycom.json +6 -0
  8. package/components/ste-filter-tool/ste-filter-tool.vue +6 -2
  9. package/components/ste-filter-tool/useData.ts +8 -0
  10. package/components/ste-select-seat/ATTRIBUTES.md +18 -0
  11. package/components/ste-select-seat/README.md +280 -0
  12. package/components/ste-select-seat/canvasUtils.ts +42 -0
  13. package/components/ste-select-seat/config.json +5 -0
  14. package/components/ste-select-seat/internals/gridUtils.ts +23 -0
  15. package/components/ste-select-seat/internals/seatLayout.ts +169 -0
  16. package/components/ste-select-seat/internals/useSeatInteraction.ts +540 -0
  17. package/components/ste-select-seat/props.ts +37 -0
  18. package/components/ste-select-seat/ste-select-seat.easycom.json +62 -0
  19. package/components/ste-select-seat/ste-select-seat.vue +517 -0
  20. package/components/ste-select-seat/types.d.ts +33 -0
  21. package/components/ste-select-seat/useData.ts +179 -0
  22. package/components/ste-select-seat/useTouchCompat.ts +89 -0
  23. package/components/ste-simple-calendar/ATTRIBUTES.md +17 -0
  24. package/components/ste-simple-calendar/README.md +112 -0
  25. package/components/ste-simple-calendar/config.json +5 -0
  26. package/components/ste-simple-calendar/props.ts +32 -0
  27. package/components/ste-simple-calendar/ste-simple-calendar.easycom.json +60 -0
  28. package/components/ste-simple-calendar/ste-simple-calendar.vue +265 -0
  29. package/components/ste-simple-calendar/type.d.ts +30 -0
  30. package/components/ste-simple-calendar/useData.ts +60 -0
  31. package/components/ste-skeleton/README.md +45 -0
  32. package/components/ste-skeleton/config.json +5 -0
  33. package/components/ste-skeleton/props.ts +7 -0
  34. package/components/ste-skeleton/ste-skeleton.json +38 -0
  35. package/components/ste-skeleton/ste-skeleton.vue +108 -0
  36. package/components/ste-slide-verify/ATTRIBUTES.md +27 -0
  37. package/components/ste-slide-verify/README.md +118 -0
  38. package/components/ste-slide-verify/config.json +5 -0
  39. package/components/ste-slide-verify/props.ts +43 -0
  40. package/components/ste-slide-verify/ste-slide-verify.easycom.json +119 -0
  41. package/components/ste-slide-verify/ste-slide-verify.vue +535 -0
  42. package/index.ts +8 -0
  43. package/package.json +1 -1
  44. package/types/components.d.ts +8 -0
  45. package/types/index.d.ts +2 -0
  46. package/types/refComponents.d.ts +8 -0
@@ -88,6 +88,7 @@ export function download(
88
88
  },
89
89
  });
90
90
  } else {
91
+ // 整包升级时,执行到此处更新并未完成,只是弹出了安装提示,无法获悉用户是否安装了更新包,若是在此处清除资源,会导致升级包被删除,后续流程无法继续执行
91
92
  success?.();
92
93
  }
93
94
  },
@@ -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
- downloadTask = downloadMethod(data, {
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
- downloadTask = downloadMethod(data, {
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),
@@ -235,6 +235,7 @@ function choose(item: DropdownItem) {
235
235
  function handleMaskClick() {
236
236
  if (props.isMaskClick) {
237
237
  close();
238
+ emits('maskClick');
238
239
  }
239
240
  }
240
241
  function handleMenuConentClick() {}
@@ -15,3 +15,4 @@
15
15
  | `confirm` | 点击确认按钮时触发 | `values`:当前所有选中的值 | - |
16
16
  | `reset` | 点击重置按钮时触发 | - | - |
17
17
  | `itemClick` | 点击任意筛选项时触发(实时) | `item`:当前操作的筛选项数据 | - |
18
+ | `maskClose` | 点击蒙层关闭了弹窗后触发 | - | - |
@@ -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>;
@@ -67,6 +67,12 @@
67
67
  "description": "当前操作的筛选项数据"
68
68
  }
69
69
  ]
70
+ },
71
+ {
72
+ "name": "[event]maskClose",
73
+ "description": "点击蒙层关闭了弹窗后触发",
74
+ "type": "() => void",
75
+ "params": []
70
76
  }
71
77
  ]
72
78
  }
@@ -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,5 @@
1
+ {
2
+ "group": "业务组件",
3
+ "title": "SelectSeat 座位选择",
4
+ "icon": "https://image.whzb.com/chain/StellarUI/组件图标/select-seat.png"
5
+ }
@@ -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
+ }