stellar-ui-plus 1.25.6 → 1.25.8

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.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { watch, onMounted, computed, nextTick, onUnmounted } from 'vue';
2
+ import { watch, onMounted, computed, nextTick, onUnmounted, ref, getCurrentInstance } from 'vue';
3
3
  import { useColorStore } from '../../store/color';
4
4
  let { getColor } = useColorStore();
5
5
  import { formatDate, type DateType, type WeekType } from './date';
@@ -75,8 +75,81 @@ const clearViewTimer = () => {
75
75
  }
76
76
  };
77
77
 
78
+ // 根节点引用,用于可见性监听
79
+ const rootRef = ref<any>(null);
80
+ // 是否已可见(在视口内)
81
+ let isVisible = false;
82
+ // 待执行的滚动目标,组件不可见时暂存
83
+ let pendingScrollTop: number | null = null;
84
+ // 在 setup 顶层获取实例,供小程序端观察器使用
85
+ const instance = getCurrentInstance();
86
+
87
+ const doScroll = (top: number, _viewMonth: string) => {
88
+ if (scrollTop.value === top) {
89
+ viewMonth.value = _viewMonth;
90
+ initing.value = false;
91
+ return;
92
+ }
93
+ contentScrollTop.value = scrollTop.value;
94
+ nextTick(() => {
95
+ contentScrollTop.value = top;
96
+ scrollTop.value = top;
97
+ viewMonth.value = _viewMonth;
98
+ initing.value = false;
99
+ });
100
+ };
101
+
102
+ let observer: any = null;
103
+ const startObserver = () => {
104
+ // #ifdef H5
105
+ if (typeof IntersectionObserver !== 'undefined' && rootRef.value) {
106
+ // uni-app H5 中 <view> 的 ref 直接是 DOM 元素
107
+ const el = rootRef.value.$el ?? rootRef.value;
108
+ observer = new IntersectionObserver(entries => {
109
+ const visible = entries[0]?.isIntersecting;
110
+ if (visible && !isVisible) {
111
+ isVisible = true;
112
+ if (pendingScrollTop !== null) {
113
+ const top = pendingScrollTop;
114
+ const _viewMonth = viewDate.value.format('YYYY-MM');
115
+ pendingScrollTop = null;
116
+ nextTick(() => doScroll(top, _viewMonth));
117
+ }
118
+ } else if (!visible) {
119
+ isVisible = false;
120
+ }
121
+ });
122
+ observer.observe(el);
123
+ return;
124
+ }
125
+ // #endif
126
+ // 小程序端使用 uni.createIntersectionObserver
127
+ try {
128
+ observer = uni.createIntersectionObserver(instance).relativeToViewport();
129
+ observer.observe('.ste-calendar-root', (res: any) => {
130
+ const visible = res.intersectionRatio > 0;
131
+ if (visible && !isVisible) {
132
+ isVisible = true;
133
+ if (pendingScrollTop !== null) {
134
+ const top = pendingScrollTop;
135
+ const _viewMonth = viewDate.value.format('YYYY-MM');
136
+ pendingScrollTop = null;
137
+ nextTick(() => doScroll(top, _viewMonth));
138
+ }
139
+ } else if (!visible) {
140
+ isVisible = false;
141
+ }
142
+ });
143
+ } catch (_) {}
144
+ };
145
+
78
146
  onUnmounted(() => {
79
147
  clearViewTimer();
148
+ if (observer) {
149
+ observer.disconnect?.();
150
+ observer.unobserve?.();
151
+ observer = null;
152
+ }
80
153
  });
81
154
 
82
155
  const showMonth = (date?: DateType) => {
@@ -84,6 +157,7 @@ const showMonth = (date?: DateType) => {
84
157
  if (newDate.format('YYYY-MM-DD') !== viewDate.value.format('YYYY-MM-DD')) {
85
158
  viewDate.value = newDate;
86
159
  }
160
+ initing.value = true;
87
161
  clearViewTimer();
88
162
  viewTimer = setTimeout(() => {
89
163
  if (props.minDate && props.maxDate) {
@@ -99,18 +173,17 @@ const showMonth = (date?: DateType) => {
99
173
  }
100
174
  const _viewMonth = viewDate.value.format('YYYY-MM');
101
175
  const tops = cmpMonthTops.value;
102
- const top = tops[_viewMonth]?.top || 0;
103
- if (top === undefined || scrollTop.value === top) {
176
+ const top = tops[_viewMonth]?.top;
177
+ if (top === undefined) {
104
178
  initing.value = false;
105
179
  return;
106
180
  }
107
- contentScrollTop.value = scrollTop.value;
108
- nextTick(() => {
109
- contentScrollTop.value = top;
110
- scrollTop.value = top;
111
- viewMonth.value = _viewMonth;
112
- initing.value = false;
113
- });
181
+ // 组件不可见时,暂存目标位置,等可见后再执行
182
+ if (!isVisible) {
183
+ pendingScrollTop = top;
184
+ return;
185
+ }
186
+ doScroll(top, _viewMonth);
114
187
  }, VIEW_MONTH_DELAY);
115
188
  };
116
189
 
@@ -208,7 +281,6 @@ watch(
208
281
  () => props.defaultDate,
209
282
  v => {
210
283
  viewDate.value = v ? utils.dayjs(v) : utils.dayjs();
211
- showMonth();
212
284
  },
213
285
  { immediate: true }
214
286
  );
@@ -217,6 +289,7 @@ const confirm = () => {
217
289
  emits('confirm', dataList.value);
218
290
  };
219
291
  onMounted(() => {
292
+ startObserver();
220
293
  showMonth();
221
294
  });
222
295
 
@@ -241,7 +314,7 @@ const onScroll = (e: any) => {
241
314
  };
242
315
  </script>
243
316
  <template>
244
- <view class="ste-calendar-root" :style="[cmpRootStyle, { opacity: initing ? 0 : 1 }]">
317
+ <view ref="rootRef" class="ste-calendar-root" :style="[cmpRootStyle, { opacity: initing ? 0 : 1 }]">
245
318
  <view v-if="showTitle" class="calendar-title">{{ title }}</view>
246
319
  <view class="week-head">
247
320
  <view class="week-row">
@@ -3,7 +3,7 @@ import utils from '../../utils/utils'
3
3
  import type { Dayjs } from '../../types'
4
4
 
5
5
  export default function useData() {
6
- const initing = ref(false)
6
+ const initing = ref(true)
7
7
  const setIniting = (val: boolean) => initing.value = val
8
8
  const startDate = ref<number | string | null>(null)
9
9
  const setStartDate = (val: number | string | null) => startDate.value = val
@@ -177,10 +177,195 @@
177
177
  }
178
178
 
179
179
  .action-box {
180
- padding: 0 40rpx;
180
+ padding: 0 40rpx 20rpx 40rpx;
181
181
  display: flex;
182
182
  justify-content: space-between;
183
+ }
184
+ }
185
+ }
186
+ </style>
187
+ ```
188
+
189
+ ## 单据类型筛选
190
+
191
+ 此示例展示了如何实现一个单据类型筛选菜单,包含左侧分类选择、右侧输入框和子选项选择。
192
+
193
+ ```html
194
+ <script lang="ts" setup>
195
+ import type { RefDropdownMenu } from 'stellar-ui-plus/types/refComponents';
196
+ import { computed, reactive, ref } from 'vue';
197
+
198
+ const orderMenus = [
199
+ {
200
+ label: '单据类型1',
201
+ value: '1',
202
+ children: [
203
+ { label: '类型1A', value: '1-1' },
204
+ { label: '类型1B', value: '1-2' },
205
+ { label: '类型1C', value: '1-3' },
206
+ { label: '类型1D', value: '1-4' },
207
+ ],
208
+ },
209
+ {
210
+ label: '单据类型2',
211
+ value: '2',
212
+ children: [
213
+ { label: '类型2A', value: '2-1' },
214
+ { label: '类型2B', value: '2-2' },
215
+ { label: '类型2C', value: '2-3' },
216
+ { label: '类型2D', value: '2-4' },
217
+ ],
218
+ },
219
+ ];
220
+
221
+ const orderData = reactive({
222
+ parent: '1',
223
+ type: '1-1',
224
+ code: '',
225
+ });
226
+
227
+ const orderChildren = computed(() => orderMenus.find(item => item.value == orderData.parent)?.children || []);
228
+
229
+ const setOrder = (key: keyof typeof orderData, v: string) => {
230
+ orderData[key] = v;
231
+ if (key == 'parent') {
232
+ const children = orderMenus.find(item => item.value == v)?.children || [];
233
+ orderData.type = children[0]?.value || '';
234
+ }
235
+ };
236
+
237
+ const orderMenuRef = ref<RefDropdownMenu>();
238
+ function confirmOrderMenu() {
239
+ uni.showToast({
240
+ title: `确认订单 parent:${orderData.parent}, type:${orderData.type}, code:${orderData.code}`,
241
+ icon: 'none',
242
+ });
243
+ orderMenuRef.value?.close();
244
+ }
245
+ </script>
246
+ <template>
247
+ <view class="menu-item">
248
+ <view>
249
+ <ste-dropdown-menu value="2" title="单据类型" ref="orderMenuRef">
250
+ <view class="order-menu-box">
251
+ <view class="content-box">
252
+ <view class="content-left">
253
+ <view v-for="(m, i) in orderMenus" :key="i" class="left-menu-item" :class="orderData.parent == m.value ? 'active' : ''" @click="setOrder('parent', m.value)">
254
+ {{ m.label }}
255
+ </view>
256
+ </view>
257
+ <view class="content-right">
258
+ <view class="right-codes">
259
+ <view class="content-title">订单尾号</view>
260
+ <ste-input style="width: 356rpx" placeholder="请输入订单最后5位" v-model="orderData.code" background="#f5f5f5"></ste-input>
261
+ </view>
262
+ <view class="right-types">
263
+ <view class="content-title">单据类型</view>
264
+ <view class="right-menus">
265
+ <view v-for="(m, i) in orderChildren" :key="i" class="right-menu-item" :class="orderData.type == m.value ? 'active' : ''" @click="setOrder('type', m.value)">
266
+ {{ m.label }}
267
+ </view>
268
+ </view>
269
+ </view>
270
+ </view>
271
+ </view>
272
+ <view class="action-box">
273
+ <ste-button width="320" background="rgba(0,0,0,0)" borderColor="#0090FF" color="#0090FF" @click="confirmOrderMenu">重置</ste-button>
274
+ <ste-button width="320" @click="confirmOrderMenu">确认</ste-button>
275
+ </view>
276
+ </view>
277
+ </ste-dropdown-menu>
278
+ </view>
279
+ </view>
280
+ </template>
281
+ <style lang="scss">
282
+ .menu-item {
283
+ display: flex;
284
+ padding: 0 20rpx;
285
+ width: 100%;
286
+ box-shadow: 0 0 10px #ddd;
287
+ > view {
288
+ flex: 1;
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ }
293
+
294
+ .order-menu-box {
295
+ background-color: #fff;
296
+ padding-top: 24rpx;
297
+ border-top: solid 4rpx #f5f5f5;
183
298
 
299
+ .content-box {
300
+ width: 100%;
301
+ display: flex;
302
+ margin-bottom: 56rpx;
303
+ font-size: 28rpx;
304
+
305
+ .content-left {
306
+ width: 150rpx;
307
+ background-color: #f9f9f9;
308
+
309
+ .left-menu-item {
310
+ height: 90rpx;
311
+ display: flex;
312
+ align-items: center;
313
+ justify-content: center;
314
+ font-size: 24rpx;
315
+ color: #a4a4a4;
316
+
317
+ &.active {
318
+ background-color: #fff;
319
+ color: #0090ff;
320
+ }
321
+ }
322
+ }
323
+
324
+ .content-right {
325
+ flex: 1;
326
+ margin-left: 26rpx;
327
+ margin-right: 18rpx;
328
+ background-color: #fff;
329
+
330
+ .content-title {
331
+ height: 90rpx;
332
+ display: flex;
333
+ align-items: center;
334
+ font-size: 24rpx;
335
+ color: #1c1f23;
336
+ }
337
+
338
+ .right-types {
339
+ .right-menus {
340
+ display: flex;
341
+ flex-wrap: wrap;
342
+ align-items: center;
343
+ gap: 20rpx;
344
+
345
+ .right-menu-item {
346
+ width: 162rpx;
347
+ height: 64rpx;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ background: #f9f9f9;
352
+ border-radius: 8rpx;
353
+
354
+ &.active {
355
+ background-color: #0090ff;
356
+ font-weight: bold;
357
+ color: #fff;
358
+ }
359
+ }
360
+ }
361
+ }
362
+ }
363
+ }
364
+
365
+ .action-box {
366
+ padding: 0 40rpx;
367
+ display: flex;
368
+ justify-content: space-between;
184
369
  padding-bottom: 20rpx;
185
370
  }
186
371
  }
@@ -1,14 +1,17 @@
1
1
  .ste-dropdown-menu-root {
2
2
  .menu-box {
3
3
  padding: 20rpx 0;
4
+
4
5
  .menu-title-icon {
5
6
  display: inline-flex;
6
7
  transition: all var(--duration) linear;
7
8
  }
9
+
8
10
  .title {
9
11
  font-size: var(--font-size-24, 24rpx);
10
12
  margin-right: 16rpx;
11
13
  }
14
+
12
15
  display: flex;
13
16
  align-items: center;
14
17
  }
@@ -59,6 +62,7 @@
59
62
  .menu-title-icon {
60
63
  transform: rotate(180deg);
61
64
  }
65
+
62
66
  .dropdown-content {
63
67
  opacity: 1;
64
68
  z-index: var(--menu-z-index);
@@ -73,6 +77,7 @@
73
77
  .menu-title-icon {
74
78
  transform: rotate(180deg);
75
79
  }
80
+
76
81
  .dropdown-content {
77
82
  display: flex;
78
83
  flex-direction: column;
@@ -82,10 +87,12 @@
82
87
  transform: translateY(100%);
83
88
  }
84
89
  }
90
+
85
91
  &.open {
86
92
  .menu-title-icon {
87
93
  transform: rotate(0);
88
94
  }
95
+
89
96
  .menu-item-content {
90
97
  transform: translateY(0);
91
98
  }
@@ -96,4 +103,4 @@
96
103
  }
97
104
  }
98
105
  }
99
- }
106
+ }
@@ -21,9 +21,9 @@
21
21
  ### Events
22
22
  | 事件名 | 说明 | 事件参数 | 支持版本 |
23
23
  | ----- | ----- | ------- | -------- |
24
- | `close` | 弹窗关闭动画执行完毕事件 | `data`:() => void | - |
25
- | `open` | 弹窗打开动画执行完毕事件 | `data`:() => void | - |
26
- | `maskClick` | 点击遮罩时触发 | `data`:() => void | - |
24
+ | `close` | 弹窗关闭前触发,可用于异步拦截关闭 | `suspend`:开启等待的回调函数<br/>`next`:执行后续关闭操作的回调函数<br/>`stop`:阻止后续关闭操作的回调函数 | - |
25
+ | `open-after` | 弹窗打开动画执行完毕事件 | - | - |
26
+ | `clickMask` | 点击遮罩时触发 | - | - |
27
27
 
28
28
 
29
29
  ### Slots