@neatui/vue 5.1.11 → 5.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @neatui/vue 更新记录
2
2
 
3
+ ## 5.2.0
4
+
5
+ ### Layer
6
+ - 新增 `on.cancel` 回调,修正 show / hide 回调的语义。
7
+ - 加强注册逻辑;动画失败时有兜底;点击遮罩关闭更可靠。
8
+ - 离开分组时释放遮罩;校验挂载区域的容器。
9
+ - 排队中的弹层在同一时刻先 show 再 hide 时,仍按队列规则处理;任意 hide 都会丢弃排队中的请求。
10
+ - 关闭或立即挂起后,下层弹层能正确恢复。
11
+
12
+ ### IFollowView
13
+ - 重写点击外部关闭的逻辑;同步嵌套浮层、`v-model` 和 hover 的状态。
14
+ - 挂载时就是打开状态的,也会初始化定位。
15
+ - 手动切换开关时取消 hover 计时器;鼠标在嵌套子浮层上时父浮层保持打开。
16
+ - 释放已失效的 Follow 实例。
17
+
18
+ ### Select
19
+ - 选中“其他”后立即显示输入框,有独立字段时也不再要求先有值;输入框会自动聚焦。
20
+ - 回车和失焦确认不再重复触发 `change`。
21
+ - 有独立字段时,空输入会保留已选的“其他”,等待补填。
22
+ - 新增 `footer` 插槽,可通过插槽参数拿到 `close` 方法。
23
+
24
+ ### DateView
25
+ - 手机端宽度撑满。
26
+
27
+ ### 依赖
28
+ - CSS 依赖更新为 `@neatui/css@^5.2.0`。Grid 间距的不兼容变更见 5.1.11 一节。
29
+
3
30
  ## 5.1.11
4
31
 
5
32
  - FormLoader 新增 `mode="read"`:保留表单控件,统一设置 `disabled`;禁止字段写回、数组增删和排序,隐藏校验提示。插槽的 `bind.disabled` 同步为 `true`,自定义控件需绑定该属性。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neatui/vue",
3
- "version": "5.1.11",
3
+ "version": "5.2.0",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "@fekit/scrollto": "^3.0.3",
37
37
  "@fekit/toast": "^2.7.2",
38
38
  "@fekit/utils": "^6.0.0",
39
- "@neatui/css": "^5.1.11",
39
+ "@neatui/css": "^5.2.0",
40
40
  "@rollup/plugin-commonjs": "^26.0.1",
41
41
  "@rollup/plugin-node-resolve": "^15.2.3",
42
42
  "@vue/compiler-sfc": "^3.4.30",
@@ -12,11 +12,11 @@
12
12
  :style="`position: fixed; pointer-events: none; ${tipStyle}`"
13
13
  :ui-tips="uiTips"
14
14
  :ui-tips-view="state.show ? 1 : 0"
15
- @mouseover="hovershow"
16
- @mouseout="hoverhide"
15
+ @mouseover="tipHover(1)"
16
+ @mouseout="tipHover(0)"
17
17
  @click="clickhide"
18
18
  >
19
- <div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="[tipBoxStyle, dragPassing ? 'pointer-events: none;' : '']">
19
+ <div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="tipBoxStyle">
20
20
  <div ui-hide=">mob" class="mob-follow-tips w-full n-ss" ui-flex="row xm">
21
21
  <div class="w-xs*s"></div>
22
22
  <div>
@@ -35,8 +35,15 @@
35
35
  </Teleport>
36
36
  </div>
37
37
  </template>
38
+ <script lang="ts">
39
+ // 每个实例只登记自己的后代,Teleport 后仍可辨认嵌套浮层。
40
+ type FollowBoundary = { contains: (target: Node) => boolean; hide: () => void };
41
+ // hover:Teleport 内容的鼠标进出不会冒泡到祖先,需要逐级通知。
42
+ type FollowContext = { children: Set<FollowBoundary>; hover: (on: boolean) => void };
43
+ const followContextKey = Symbol('follow-context');
44
+ </script>
38
45
  <script setup lang="ts">
39
- import { reactive, ref, onMounted, onUnmounted, watch } from 'vue';
46
+ import { reactive, ref, onMounted, onUnmounted, watch, inject, provide } from 'vue';
40
47
  import Follow from '@fekit/follow';
41
48
 
42
49
  const emits = defineEmits(['update:modelValue', 'onhide', 'onshow']);
@@ -55,6 +62,7 @@
55
62
  tipBoxStyle?: string;
56
63
  arrow?: any;
57
64
  clickTipHide?: boolean;
65
+ /** @deprecated 已移除,不再生效,将在 6.0 删除该属性。 */
58
66
  dragTipHide?: boolean;
59
67
  mobile?: boolean;
60
68
  title?: string;
@@ -75,7 +83,7 @@
75
83
  tipBoxStyle: '',
76
84
  arrow: {},
77
85
  clickTipHide: false,
78
- dragTipHide: false,
86
+ dragTipHide: undefined,
79
87
  mobile: true,
80
88
  title: '',
81
89
  zIndex: undefined,
@@ -87,87 +95,23 @@
87
95
  const teleportReady = ref(false);
88
96
  const state = reactive({
89
97
  view: 0,
90
- show: 0,
98
+ show: props.modelValue ? 1 : 0,
91
99
  pos: props.pos,
92
100
  hover: 0,
93
- tipdom: false,
94
- isMobile: false,
95
101
  });
96
102
 
103
+ if (props.dragTipHide !== undefined) {
104
+ console.warn('「IFollowView」 dragTipHide 已移除且不再生效,将在 6.0 删除该属性,请移除 drag-tip-hide。');
105
+ }
106
+
97
107
  const showTimer: any = ref(null);
98
108
  const hideTimer: any = ref(null);
99
- const dragPointerId = ref<number | null>(null);
100
- const dragFromInternal = ref(false);
101
- const dragStartX = ref(0);
102
- const dragStartY = ref(0);
103
- const dragPassing = ref(false);
104
- const DRAG_HIDE_PX = 6;
105
-
106
109
  const containsPoint = (el: HTMLElement | null, x: number, y: number) => {
107
110
  if (!el) return false;
108
111
  const rect = el.getBoundingClientRect();
109
112
  return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
110
113
  };
111
114
 
112
- const isOutsideFollow = (x: number, y: number) => {
113
- return !containsPoint(box.value, x, y) && !containsPoint(dom.value, x, y);
114
- };
115
-
116
- const isInternalDragSource = (target: EventTarget | null) => {
117
- if (!(target instanceof Element)) return false;
118
- if (target.closest('input, textarea, select, [contenteditable="true"], [role="slider"]')) return true;
119
- const stop = box.value?.parentElement || null;
120
- let el: Element | null = target;
121
- while (el && el !== stop) {
122
- const style = window.getComputedStyle(el);
123
- const canScroll =
124
- ((style.overflowY === 'auto' || style.overflowY === 'scroll') && el.scrollHeight > el.clientHeight) ||
125
- ((style.overflowX === 'auto' || style.overflowX === 'scroll') && el.scrollWidth > el.clientWidth);
126
- if (canScroll) return true;
127
- el = el.parentElement;
128
- }
129
- return false;
130
- };
131
-
132
- const clearDragSession = () => {
133
- dragPointerId.value = null;
134
- dragFromInternal.value = false;
135
- };
136
-
137
- const hideFromDrag = () => {
138
- dragPassing.value = true;
139
- state.show = 0;
140
- state.hover = 0;
141
- clearDragSession();
142
- };
143
-
144
- const onDragPointerDown = (ev: PointerEvent) => {
145
- if (!props.dragTipHide || !state.show || ev.button !== 0) return;
146
- if (!box.value?.contains(ev.target as Node)) return;
147
- dragPointerId.value = ev.pointerId;
148
- dragFromInternal.value = isInternalDragSource(ev.target);
149
- dragStartX.value = ev.clientX;
150
- dragStartY.value = ev.clientY;
151
- };
152
-
153
- const onDragPointerMove = (ev: PointerEvent) => {
154
- if (!props.dragTipHide || dragPointerId.value !== ev.pointerId || ev.buttons === 0) return;
155
- if (isOutsideFollow(ev.clientX, ev.clientY)) {
156
- hideFromDrag();
157
- return;
158
- }
159
- if (dragFromInternal.value) return;
160
- const dx = ev.clientX - dragStartX.value;
161
- const dy = ev.clientY - dragStartY.value;
162
- if (dx * dx + dy * dy >= DRAG_HIDE_PX * DRAG_HIDE_PX) {
163
- hideFromDrag();
164
- }
165
- };
166
-
167
- const onDragPointerUp = (ev: PointerEvent) => {
168
- if (dragPointerId.value === ev.pointerId) clearDragSession();
169
- };
170
-
171
115
  watch(
172
116
  () => state.hover,
173
117
  () => {
@@ -183,26 +127,45 @@
183
127
  }, 100);
184
128
  }
185
129
  },
130
+ // 同步创建定时器,使同一事件中随后的 hide()/show() 能够取消它。
131
+ { flush: 'sync' },
186
132
  );
187
133
 
134
+ // 主动切换状态时取消待执行的 hover 定时器,避免被延迟回调覆盖。
135
+ const setShow = (value: number) => {
136
+ clearTimeout(showTimer.value);
137
+ clearTimeout(hideTimer.value);
138
+ state.show = value;
139
+ };
140
+
188
141
  // 开关
189
142
  const toggle = () => {
190
143
  if (props.event === 'click') {
191
- state.show = state.show ? 0 : 1;
144
+ setShow(state.show ? 0 : 1);
192
145
  }
193
146
  };
194
147
 
148
+ // 外部 v-model 控制显示状态。
195
149
  watch(
196
- [() => state.show, () => state.hover],
197
- () => {
198
- if (state.show) {
150
+ () => props.modelValue,
151
+ (value) => {
152
+ const next = value ? 1 : 0;
153
+ if (next !== state.show) setShow(next);
154
+ },
155
+ );
156
+
157
+ // 只在实际显示状态变化时通知;父层关闭时同步关闭后代浮层(Teleport 后不会随父层隐藏)。
158
+ watch(
159
+ () => state.show,
160
+ (show) => {
161
+ if (show) {
199
162
  emits('onshow');
200
163
  } else {
164
+ children.forEach(item => item.hide());
201
165
  emits('onhide');
202
166
  }
203
- emits('update:modelValue', state.show);
167
+ emits('update:modelValue', show);
204
168
  },
205
- { deep: true },
206
169
  );
207
170
 
208
171
  const hovershow = () => {
@@ -219,35 +182,112 @@
219
182
 
220
183
  const clickhide = () => {
221
184
  if (props.clickTipHide) {
222
- state.show = 0;
185
+ setShow(0);
223
186
  }
224
187
  };
225
188
 
226
189
  const show = () => {
227
- dragPassing.value = false;
228
- state.show = 1;
190
+ setShow(1);
229
191
  };
230
192
 
231
193
  const hide = () => {
232
- state.show = 0;
233
- clearDragSession();
194
+ setShow(0);
195
+ };
196
+
197
+ const parentContext = inject<FollowContext | null>(followContextKey, null);
198
+ const parentChildren = parentContext?.children ?? null;
199
+ const children = new Set<FollowBoundary>();
200
+ // 后代浮层内容的进出视同本层;本层 tip 也是 Teleport 内容,继续向上通知。
201
+ const notifyHover = (on: boolean) => {
202
+ if (props.event === 'hover') state.hover = on ? 1 : 0;
203
+ parentContext?.hover(on);
204
+ };
205
+ const tipHover = (on: number) => notifyHover(!!on);
206
+ provide(followContextKey, { children, hover: notifyHover });
207
+ const inChildren = (target: Node): boolean => [...children].some(item => item.contains(target));
208
+ const containsFollow: FollowBoundary = {
209
+ contains: target => !!dom.value?.contains(target) || (!!state.show && (!!tip.value?.contains(target) || inChildren(target))),
210
+ hide: () => {
211
+ state.hover = 0;
212
+ setShow(0);
213
+ },
214
+ };
215
+ parentChildren?.add(containsFollow);
216
+ const isInside = (target: Node) => !!dom.value?.contains(target) || !!box.value?.contains(target) || inChildren(target);
217
+ const isBackdrop = (target: EventTarget | null) =>
218
+ props.mobile && window.matchMedia('(max-width: 799px)').matches && target === tip.value;
219
+
220
+ const dismissOutside = () => {
221
+ // 暗层 touchstart 被取消默认行为时,主动失焦,让表单先提交再关闭。
222
+ const active = document.activeElement;
223
+ if (active instanceof HTMLElement && isInside(active)) active.blur();
224
+ clearTimeout(showTimer.value);
225
+ clearTimeout(hideTimer.value);
226
+ state.hover = 0;
227
+ hide();
228
+ };
229
+
230
+ // 记录本次按下是否发生在浮层内:在内部按下、拖到外部松开(拖选文字、拖滑块等)
231
+ // 产生的 click 目标是共同祖先,不能当作外部点击。
232
+ let pressedInside = false;
233
+ const onPressStart = (event: PointerEvent) => {
234
+ pressedInside = !!state.show && event.target instanceof Node && isInside(event.target);
235
+ };
236
+ // 松开后紧随的 click 处理完再清除;没有 click(触摸滚动等)时也不残留到下一次键盘点击。
237
+ const onPressEnd = () => {
238
+ if (pressedInside) setTimeout(() => { pressedInside = false; }, 0);
234
239
  };
235
240
 
236
- // 取消
237
- const cancel = ({ target }: any = {}) => {
238
- if (tip.value && !state.tipdom && !tip.value?.contains(target) && dom.value && !dom.value.contains(target) && !tip.value.querySelector('input:focus')) {
239
- state.show = 0;
241
+ // 外部点击以内容盒子为边界;按下位置在内部时不关闭。
242
+ const cancel = (event?: MouseEvent) => {
243
+ const fromInside = pressedInside;
244
+ pressedInside = false;
245
+ if (!state.show) return;
246
+ if (event && fromInside) return;
247
+ const target = event?.target;
248
+ if (target instanceof Node && isInside(target)) return;
249
+ if (event && isBackdrop(target ?? null)) {
250
+ event.preventDefault();
251
+ // 同一 document 上父浮层的监听器也不能继续处理子暗层点击。
252
+ event.stopImmediatePropagation();
240
253
  }
254
+ dismissOutside();
241
255
  };
242
256
 
243
- // 阻止触摸事件冒泡和默认行为
244
- const preventTouchScroll = (e: TouchEvent) => {
245
- // 只有当触摸点在遮罩层本身时才阻止,不影响子元素
246
- if (e.target === tip.value && tip.value?.classList.contains('fekit-follow-mobile')) {
247
- e.preventDefault();
248
- e.stopPropagation();
257
+ // 轻触跟踪:暗层轻触由组件自己处理(阻止点穿);外部轻触在 touchend 关闭,
258
+ // 因为 iOS/WebKit 轻触非可点击元素不会派发 click。
259
+ const TAP_SLOP = 10;
260
+ let outsideTouch: { id: number; x: number; y: number; moved: boolean; backdrop: boolean } | null = null;
261
+ const onTouchStart = (event: TouchEvent) => {
262
+ outsideTouch = null;
263
+ if (!state.show) return;
264
+ const target = event.target;
265
+ const backdrop = isBackdrop(target);
266
+ if (!backdrop && (!(target instanceof Node) || isInside(target))) return;
267
+ if (backdrop) event.preventDefault();
268
+ const touch = event.touches[0];
269
+ if (event.touches.length === 1 && touch) {
270
+ outsideTouch = { id: touch.identifier, x: touch.clientX, y: touch.clientY, moved: false, backdrop };
249
271
  }
250
272
  };
273
+ const onTouchMove = (event: TouchEvent) => {
274
+ if (!outsideTouch) return;
275
+ // 暗层上阻止页面滚动;外部区域保持原生滚动,滚动则不视为轻触。
276
+ if (outsideTouch.backdrop) event.preventDefault();
277
+ const start = outsideTouch;
278
+ const touch = [...event.touches].find(item => item.identifier === start.id);
279
+ if (!touch || event.touches.length !== 1 || Math.hypot(touch.clientX - start.x, touch.clientY - start.y) > TAP_SLOP) start.moved = true;
280
+ };
281
+ const onTouchEnd = (event: TouchEvent) => {
282
+ const start = outsideTouch;
283
+ outsideTouch = null;
284
+ if (!start) return;
285
+ if (start.backdrop) event.preventDefault();
286
+ const touch = [...event.changedTouches].find(item => item.identifier === start.id);
287
+ if (!state.show || start.moved || !touch || Math.hypot(touch.clientX - start.x, touch.clientY - start.y) > TAP_SLOP) return;
288
+ if (start.backdrop ? isBackdrop(event.target) && !containsPoint(box.value, touch.clientX, touch.clientY) : true) dismissOutside();
289
+ };
290
+ const onTouchCancel = () => { outsideTouch = null; };
251
291
 
252
292
  const ensureTeleportTarget = () => {
253
293
  let area = document.querySelector('#ifollow');
@@ -261,24 +301,21 @@
261
301
 
262
302
  onMounted(() => {
263
303
  ensureTeleportTarget();
304
+ document.addEventListener('pointerdown', onPressStart, true);
305
+ document.addEventListener('pointerup', onPressEnd, true);
306
+ document.addEventListener('pointercancel', onPressEnd, true);
264
307
  document.addEventListener('click', cancel, true);
265
- document.addEventListener('pointerdown', onDragPointerDown, true);
266
- window.addEventListener('pointermove', onDragPointerMove, true);
267
- window.addEventListener('pointerup', onDragPointerUp, true);
268
- window.addEventListener('pointercancel', onDragPointerUp, true);
269
- // 添加触摸事件监听器以防止页面滚动
270
- document.addEventListener('touchstart', preventTouchScroll, {
271
- passive: false,
272
- });
273
- document.addEventListener('touchmove', preventTouchScroll, {
274
- passive: false,
275
- });
276
- // 判断是否为移动端
277
- state.isMobile = window.innerWidth < 800;
308
+ // 暗层需要阻止默认行为,触摸监听不能是 passive。
309
+ document.addEventListener('touchstart', onTouchStart, { passive: false });
310
+ document.addEventListener('touchmove', onTouchMove, { passive: false });
311
+ document.addEventListener('touchend', onTouchEnd, { passive: false });
312
+ document.addEventListener('touchcancel', onTouchCancel);
278
313
  });
279
314
 
280
315
  const ex: any = ref(null);
281
316
  watch([() => dom.value, () => tip.value], () => {
317
+ // 替换或移除 tips 前先注销旧实例的监听。
318
+ ex.value?.remove();
282
319
  if (dom.value && tip.value) {
283
320
  ex.value = new Follow(
284
321
  dom.value,
@@ -300,6 +337,8 @@
300
337
  },
301
338
  { hideOnScrollOut: false, zIndex: props.zIndex },
302
339
  );
340
+ // 初始即显示(如 modelValue=1)时 state.show 不会再变化,需要立即启动定位。
341
+ if (state.show) ex.value.listen();
303
342
  } else {
304
343
  ex.value = null;
305
344
  }
@@ -313,14 +352,16 @@
313
352
  );
314
353
 
315
354
  onUnmounted(() => {
355
+ document.removeEventListener('pointerdown', onPressStart, true);
356
+ document.removeEventListener('pointerup', onPressEnd, true);
357
+ document.removeEventListener('pointercancel', onPressEnd, true);
316
358
  document.removeEventListener('click', cancel, true);
317
- document.removeEventListener('pointerdown', onDragPointerDown, true);
318
- window.removeEventListener('pointermove', onDragPointerMove, true);
319
- window.removeEventListener('pointerup', onDragPointerUp, true);
320
- window.removeEventListener('pointercancel', onDragPointerUp, true);
321
- // 移除触摸事件监听器
322
- document.removeEventListener('touchstart', preventTouchScroll);
323
- document.removeEventListener('touchmove', preventTouchScroll);
359
+ document.removeEventListener('touchstart', onTouchStart);
360
+ document.removeEventListener('touchmove', onTouchMove);
361
+ document.removeEventListener('touchend', onTouchEnd);
362
+ document.removeEventListener('touchcancel', onTouchCancel);
363
+ parentChildren?.delete(containsFollow);
364
+ outsideTouch = null;
324
365
  state.show = 0;
325
366
 
326
367
  if (ex.value) {
@@ -331,11 +372,6 @@
331
372
  watch(
332
373
  () => state.show,
333
374
  (show: any) => {
334
- if (show) {
335
- dragPassing.value = false;
336
- } else {
337
- clearDragSession();
338
- }
339
375
  if (ex.value) {
340
376
  if (show) {
341
377
  ex.value.listen();
@@ -371,7 +407,6 @@
371
407
  &[ui-tips-view='1'] {
372
408
  background-color: rgba(0, 0, 0, 0.5);
373
409
  pointer-events: auto !important;
374
- touch-action: none !important;
375
410
  }
376
411
 
377
412
  &[ui-tips] [ui-tips-box] {
@@ -12,7 +12,7 @@
12
12
  :view="state.view"
13
13
  class="fekit-layer"
14
14
  >
15
- <div :ui-flex="`col ${flex}`" :style="wrapStyle" class="fekit-layer-wrap" @animationend="anim" @click="_sys_hide">
15
+ <div ref="wrapRef" :ui-flex="`col ${flex}`" :style="wrapStyle" class="fekit-layer-wrap" @animationend="anim" @pointerdown="_sys_press" @pointerup="_sys_release" @click="_sys_hide">
16
16
  <div v-if="state.coverDark !== ''" class="fekit-layer-cover-dark"></div>
17
17
  <slot :param="_tasks[id]?.params"></slot>
18
18
  </div>
@@ -21,7 +21,7 @@
21
21
  </div>
22
22
  </template>
23
23
  <script lang="ts" setup>
24
- import { computed, nextTick, onBeforeUnmount, reactive, watch } from 'vue';
24
+ import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue';
25
25
  import { env, isFunction } from '@fekit/utils';
26
26
 
27
27
  // 弹层队列
@@ -73,6 +73,7 @@
73
73
  el: HTMLElement | null;
74
74
  listening: boolean;
75
75
  owners: Set<string>;
76
+ timer?: ReturnType<typeof setTimeout>;
76
77
  view: number;
77
78
  };
78
79
  queue: Array<{ id: string; kind: 'open' | 'show'; params: any; then: any }>;
@@ -186,9 +187,65 @@
186
187
  return task.view === 1 && !!task.parkAfterHide;
187
188
  };
188
189
 
190
+ const parseTimeList = (value: string) =>
191
+ value.split(',').map((item) => {
192
+ const text = item.trim();
193
+ const num = Number.parseFloat(text) || 0;
194
+ return text.endsWith('ms') ? num : num * 1000;
195
+ });
196
+
197
+ // 读取元素当前动画的最长耗时(ms);没有动画返回 0
198
+ const getAnimationWait = (el: Element | null | undefined) => {
199
+ if (!el || !env.client) return 0;
200
+ const style = window.getComputedStyle(el);
201
+ const names = style.animationName.split(',');
202
+ const durations = parseTimeList(style.animationDuration);
203
+ const delays = parseTimeList(style.animationDelay);
204
+ let wait = 0;
205
+ names.forEach((name, index) => {
206
+ if (name.trim() === 'none') return;
207
+ const duration = durations[index % durations.length] || 0;
208
+ const delay = delays[index % delays.length] || 0;
209
+ wait = Math.max(wait, duration + delay);
210
+ });
211
+ return wait;
212
+ };
213
+
214
+ // animationend 兜底:动画缺失/被禁用/事件丢失时也能推进状态
215
+ const ANIMATION_FALLBACK_SLACK = 100;
216
+
217
+ const onGlobalDarkAnimationDone = (group: AreaGroup) => {
218
+ if (group.dark.timer) {
219
+ clearTimeout(group.dark.timer);
220
+ group.dark.timer = undefined;
221
+ }
222
+ if (group.dark.view === 2) setGlobalDarkView(group, 3);
223
+ else if (group.dark.view === 1) {
224
+ setGlobalDarkView(group, 0);
225
+ const afterHide = group.dark.afterHide;
226
+ group.dark.afterHide = undefined;
227
+ afterHide?.();
228
+ }
229
+ };
230
+
189
231
  const setGlobalDarkView = (group: AreaGroup, view: number) => {
190
232
  group.dark.view = view;
191
- if (group.dark.el) group.dark.el.setAttribute('view', `${view}`);
233
+ if (group.dark.timer) {
234
+ clearTimeout(group.dark.timer);
235
+ group.dark.timer = undefined;
236
+ }
237
+ const el = group.dark.el;
238
+ if (!el) return;
239
+ el.setAttribute('view', `${view}`);
240
+ if (view !== 1 && view !== 2) return;
241
+ const wait = getAnimationWait(el);
242
+ group.dark.timer = setTimeout(
243
+ () => {
244
+ group.dark.timer = undefined;
245
+ if (group.dark.view === view) onGlobalDarkAnimationDone(group);
246
+ },
247
+ wait > 0 ? wait + ANIMATION_FALLBACK_SLACK : 0,
248
+ );
192
249
  };
193
250
 
194
251
  const ensureGlobalDarkEl = (area: string, group: AreaGroup) => {
@@ -208,13 +265,7 @@
208
265
  group.dark.el = el;
209
266
  el.addEventListener('animationend', (ev) => {
210
267
  if (ev.target !== el) return;
211
- if (group.dark.view === 2) setGlobalDarkView(group, 3);
212
- if (group.dark.view === 1) {
213
- setGlobalDarkView(group, 0);
214
- const afterHide = group.dark.afterHide;
215
- group.dark.afterHide = undefined;
216
- afterHide?.();
217
- }
268
+ onGlobalDarkAnimationDone(group);
218
269
  });
219
270
  group.dark.listening = true;
220
271
  return el;
@@ -316,8 +367,17 @@
316
367
  syncStackIndex(group);
317
368
  };
318
369
 
370
+ const runQueuedCancel = (job: { params: any }) => {
371
+ const cancel = job.params?.on?.cancel;
372
+ if (typeof cancel === 'function') cancel();
373
+ };
374
+
375
+ // 撤掉队列中的显示请求:该请求不会再显示,视为取消
319
376
  const removeFromQueue = (group: AreaGroup, layerId: string) => {
377
+ const removed = group.queue.filter((job) => job.id === layerId);
378
+ if (!removed.length) return;
320
379
  group.queue = group.queue.filter((job) => job.id !== layerId);
380
+ removed.forEach(runQueuedCancel);
321
381
  };
322
382
 
323
383
  const getVisibleTop = (group: AreaGroup, exceptId = '') => {
@@ -506,13 +566,23 @@
506
566
  wrapStyle: '',
507
567
  });
508
568
 
509
- if (env.client) {
510
- const _area: any = document.querySelector(props.area);
569
+ // 容器不存在时:仅 #id 形式可自动创建;其它选择器无法据此建出可匹配的节点,只提示
570
+ if (env.client && props.area) {
571
+ let _area: Element | null = null;
572
+ try {
573
+ _area = document.querySelector(props.area);
574
+ } catch {
575
+ _area = null;
576
+ }
511
577
  if (!_area) {
512
- const el = document.createElement('div');
513
- el.id = (props.area || '#fekit').substring(1);
514
- el.style.position = 'absolute';
515
- document.body.appendChild(el);
578
+ if (/^#[\w-]+$/.test(props.area)) {
579
+ const el = document.createElement('div');
580
+ el.id = props.area.substring(1);
581
+ el.style.position = 'absolute';
582
+ document.body.appendChild(el);
583
+ } else {
584
+ console.warn('Layer 的 area 容器不存在,且只有 #id 形式可自动创建:', props.area);
585
+ }
516
586
  }
517
587
  }
518
588
 
@@ -528,6 +598,18 @@
528
598
  view: 0,
529
599
  });
530
600
  const scrollLockOwner = {};
601
+ const wrapRef = ref<HTMLElement | null>(null);
602
+ // 本实例注册到 _tasks 的对象;同 id 被其它实例覆盖后不再写全局
603
+ let self: any = null;
604
+ const ownsRegistration = () => !!self && _tasks[props.id] === self;
605
+
606
+ let animTimer: ReturnType<typeof setTimeout> | undefined;
607
+ const clearAnimTimer = () => {
608
+ if (animTimer) {
609
+ clearTimeout(animTimer);
610
+ animTimer = undefined;
611
+ }
612
+ };
531
613
 
532
614
  const layerStyle = computed(() => {
533
615
  let style = '';
@@ -539,12 +621,12 @@
539
621
  });
540
622
 
541
623
  const syncTaskView = (view: number) => {
542
- if (!_tasks[props.id]) return;
543
- _tasks[props.id].view = view;
544
- _tasks[props.id].openMode = state.openMode;
545
- _tasks[props.id].parkAfterHide = state.parkAfterHide;
546
- _tasks[props.id].participates = participatesInGroup(props.dark);
547
- _tasks[props.id].dark = props.dark;
624
+ if (!self) return;
625
+ self.view = view;
626
+ self.openMode = state.openMode;
627
+ self.parkAfterHide = state.parkAfterHide;
628
+ self.participates = participatesInGroup(props.dark);
629
+ self.dark = props.dark;
548
630
  };
549
631
 
550
632
  const syncOwnDarkOccupancy = () => {
@@ -575,6 +657,7 @@
575
657
  else releaseScrollLock(scrollLockOwner);
576
658
  syncOwnDarkOccupancy();
577
659
  state.immediate = false;
660
+ armAnimFallback();
578
661
  if (participatesInGroup(props.dark) && props.id) {
579
662
  nextTick(() => recomputeRecess(getAreaGroup(props.area)));
580
663
  }
@@ -584,8 +667,30 @@
584
667
 
585
668
  watch(
586
669
  () => props.dark,
587
- () => {
670
+ (dark, prevDark) => {
671
+ const was = participatesInGroup(prevDark);
672
+ const now = participatesInGroup(dark);
588
673
  syncTaskView(state.view);
674
+ if (was !== now && env.client && props.id && ownsRegistration()) {
675
+ const group = getAreaGroup(props.area);
676
+ if (!now) {
677
+ // 退出分组:释放暗层占用并移出栈/队列;不恢复其下被 park 的弹层(不视为关闭)
678
+ removeFromQueue(group, props.id);
679
+ removeFromStack(group, props.id);
680
+ releaseGlobalDark(props.area, group, props.id);
681
+ state.coverDark = '';
682
+ state.recess = 0;
683
+ state.stackIndex = 0;
684
+ recomputeRecess(group);
685
+ scheduleFlushQueue(group);
686
+ return;
687
+ }
688
+ // 加入分组:已打开时补上栈位置,暗层占用由 syncOwnDarkOccupancy 接管
689
+ if (state.view > 0) {
690
+ ensureOnStack(group, props.id);
691
+ recomputeRecess(group);
692
+ }
693
+ }
589
694
  syncOwnDarkOccupancy();
590
695
  },
591
696
  );
@@ -593,7 +698,7 @@
593
698
  watch(
594
699
  () => props.escClose,
595
700
  (escClose) => {
596
- if (_tasks[props.id]) _tasks[props.id].escClose = escClose;
701
+ if (self) self.escClose = escClose;
597
702
  },
598
703
  { immediate: true },
599
704
  );
@@ -601,7 +706,7 @@
601
706
  watch(
602
707
  () => props.darkClickClose,
603
708
  (darkClickClose) => {
604
- if (_tasks[props.id]) _tasks[props.id].darkClickClose = darkClickClose;
709
+ if (self) self.darkClickClose = darkClickClose;
605
710
  },
606
711
  { immediate: true },
607
712
  );
@@ -612,7 +717,10 @@
612
717
  hide: [],
613
718
  open: [],
614
719
  none: [],
720
+ cancel: [],
615
721
  };
722
+ // 已发起显示但尚未完成(show 回调未执行)
723
+ let showPending = false;
616
724
 
617
725
  const resolveMode = (params: any): LayerMode => {
618
726
  const mode = params?.mode;
@@ -623,14 +731,28 @@
623
731
  const bindOpenCallbacks = (params: any = {}, then = () => {}) => {
624
732
  if (params?.on?.show) on.show.push(params.on.show);
625
733
  if (params?.on?.hide) on.hide.push(params.on.hide);
734
+ if (params?.on?.cancel) on.cancel.push(params.on.cancel);
626
735
  on.show.push(then);
627
736
  };
628
737
 
629
- const runCallbacks = (key: 'show' | 'hide' | 'none') => {
630
- on[key].forEach((fun: any) => {
738
+ // 回调执行前,LayerById(id).view 与分组栈都已更新为最终状态
739
+ const runCallbacks = (key: 'show' | 'hide' | 'none' | 'cancel') => {
740
+ const list = on[key].splice(0);
741
+ if (key === 'show') {
742
+ showPending = false;
743
+ on.cancel.length = 0;
744
+ }
745
+ list.forEach((fun: any) => {
631
746
  if (isFunction(fun)) fun();
632
747
  });
633
- on[key].length = 0;
748
+ };
749
+
750
+ // 显示尚未完成就被关闭:丢弃 show 回调,改为触发 cancel
751
+ const cancelPendingShow = () => {
752
+ if (!showPending) return;
753
+ showPending = false;
754
+ on.show.length = 0;
755
+ runCallbacks('cancel');
634
756
  };
635
757
 
636
758
  const flushQueue = (group: AreaGroup) => {
@@ -639,6 +761,7 @@
639
761
  if (!job) return;
640
762
  const task = _tasks[job.id];
641
763
  if (!task) {
764
+ runQueuedCancel(job);
642
765
  flushQueue(group);
643
766
  return;
644
767
  }
@@ -666,7 +789,9 @@
666
789
  const group = getAreaGroup(props.area);
667
790
  removeFromStack(group, layerId);
668
791
  const topId = group.stack[group.stack.length - 1];
669
- if (topId && _tasks[topId]?.view === 4) _tasks[topId].unpark();
792
+ // 恢复已停放或正在退往停放的下层(unpark 自行判断)
793
+ const top = topId ? _tasks[topId] : undefined;
794
+ if (top && (top.view === 4 || top.parkAfterHide)) top.unpark();
670
795
  recomputeRecess(group);
671
796
  scheduleFlushQueue(group);
672
797
  };
@@ -726,16 +851,24 @@
726
851
  state.parkAfterHide = false;
727
852
  state.immediate = true;
728
853
  state.view = 4;
854
+ syncTaskView(4);
729
855
  return;
730
856
  }
731
857
  if (state.view === 2 || state.view === 3) {
732
858
  state.parkAfterHide = true;
733
- syncTaskView(state.view);
734
859
  state.view = 1;
860
+ syncTaskView(1);
735
861
  }
736
862
  };
737
863
 
738
864
  const unpark = (options: any = {}) => {
865
+ // 退场动画未结束(尚未进入停放):取消停放,重新显示
866
+ if (state.view === 1 && state.parkAfterHide) {
867
+ state.parkAfterHide = false;
868
+ state.view = 2;
869
+ syncTaskView(2);
870
+ return;
871
+ }
739
872
  if (state.view !== 4) return;
740
873
  state.parkAfterHide = false;
741
874
  if (options.immediate) {
@@ -746,16 +879,38 @@
746
879
  state.view = 2;
747
880
  };
748
881
 
882
+ // animationend 兜底:am 无对应样式、动画被禁用或事件丢失时,按动画时长推进 1→0 / 2→3
883
+ function armAnimFallback() {
884
+ clearAnimTimer();
885
+ const view = state.view;
886
+ if (!env.client || (view !== 1 && view !== 2)) return;
887
+ nextTick(() => {
888
+ if (state.view !== view) return;
889
+ clearAnimTimer();
890
+ const wait = getAnimationWait(wrapRef.value);
891
+ animTimer = setTimeout(
892
+ () => {
893
+ animTimer = undefined;
894
+ if (state.view === view) anim();
895
+ },
896
+ wait > 0 ? wait + ANIMATION_FALLBACK_SLACK : 0,
897
+ );
898
+ });
899
+ }
900
+
749
901
  // 注入参数
750
902
  const init = (params: any = {}) => {
751
- _tasks[id].params = params;
903
+ self.params = params;
752
904
  if (params?.on?.show) {
753
905
  on.show.push(params.on.show);
754
906
  }
755
907
  if (params?.on?.hide) {
756
908
  on.hide.push(params.on.hide);
757
909
  }
758
- return _tasks[id];
910
+ if (params?.on?.cancel) {
911
+ on.cancel.push(params.on.cancel);
912
+ }
913
+ return self;
759
914
  };
760
915
 
761
916
  // 显示弹层
@@ -763,23 +918,35 @@
763
918
  if (tryEnqueue('show', params, then)) return;
764
919
  if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
765
920
  bindOpenCallbacks(params, then);
921
+ showPending = true;
766
922
  state.openMode = resolveMode(params);
767
923
  applyIncomingRelation(state.openMode, params, false);
768
924
  state.parkAfterHide = false;
769
925
  state.view = 2;
926
+ // 立即同步全局状态,同一调用栈内随后的 queue 请求才能看到本层在显示
927
+ syncTaskView(2);
770
928
  closeReplacedLayers(state.openMode, params, false);
771
929
  syncGroupPresentation();
772
930
  };
773
931
 
774
932
  // 隐藏弹层
775
933
  const hide = (_params: any = {}, then = () => {}) => {
934
+ // 先撤销排队中的显示请求(触发其 cancel),无论当前是否在显示
935
+ if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
936
+ // 已关闭:目标状态已达成,直接回调
937
+ if (state.view === 0) {
938
+ if (isFunction(then)) then();
939
+ return;
940
+ }
941
+ cancelPendingShow();
776
942
  if (state.view === 4) {
777
943
  on.hide.push(then);
778
944
  state.parkAfterHide = false;
779
945
  state.immediate = true;
780
946
  state.view = 0;
781
- runCallbacks('hide');
947
+ syncTaskView(0);
782
948
  onGroupLayerClosed(id);
949
+ runCallbacks('hide');
783
950
  return;
784
951
  }
785
952
  if (state.view === 2 || state.view === 3) {
@@ -787,7 +954,12 @@
787
954
  syncTaskView(state.view);
788
955
  state.view = 1;
789
956
  on.hide.push(then);
957
+ return;
790
958
  }
959
+ // 正在关闭:回调并入本次关闭;若是 park 的退场过渡,改为真正关闭
960
+ state.parkAfterHide = false;
961
+ syncTaskView(state.view);
962
+ on.hide.push(then);
791
963
  };
792
964
 
793
965
  // 打开弹层(没有动画过程)
@@ -800,6 +972,7 @@
800
972
  state.parkAfterHide = false;
801
973
  state.immediate = true;
802
974
  state.view = 3;
975
+ syncTaskView(3);
803
976
  closeReplacedLayers(state.openMode, params, true);
804
977
  syncGroupPresentation();
805
978
  runCallbacks('show');
@@ -808,18 +981,34 @@
808
981
  const none = (_params: any = {}, then = () => {}) => {
809
982
  const wasOpen = state.view > 0;
810
983
  if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
984
+ cancelPendingShow();
811
985
  state.parkAfterHide = false;
812
986
  state.immediate = true;
813
987
  state.view = 0;
988
+ syncTaskView(0);
814
989
  on.none.push(then);
990
+ if (wasOpen) onGroupLayerClosed(id);
815
991
  // 立即执行回调,因为没有动画
816
992
  runCallbacks('none');
817
- if (wasOpen) onGroupLayerClosed(id);
818
993
  };
819
994
 
820
- // 点暗层关闭
821
- const _sys_hide = (ev: any) => {
822
- if (props.darkClickClose && ev.target.className === 'fekit-layer-wrap' && state.view === 3) {
995
+ // 点暗层关闭:按下与松开都必须落在暗层(wrap 本身)上。
996
+ // 按下和松开分处弹窗内外时(如拖选输入框文本拖出弹窗),click 会派发到共同祖先 wrap,不能据此关闭。
997
+ let pressedOnBackdrop = false;
998
+ let releasedOnBackdrop = false;
999
+ const _sys_press = (ev: PointerEvent) => {
1000
+ pressedOnBackdrop = ev.target === ev.currentTarget;
1001
+ releasedOnBackdrop = false;
1002
+ };
1003
+ const _sys_release = (ev: PointerEvent) => {
1004
+ releasedOnBackdrop = ev.target === ev.currentTarget;
1005
+ };
1006
+ const _sys_hide = (ev: MouseEvent) => {
1007
+ const fromBackdrop = pressedOnBackdrop && releasedOnBackdrop;
1008
+ pressedOnBackdrop = false;
1009
+ releasedOnBackdrop = false;
1010
+ if (!fromBackdrop || ev.target !== ev.currentTarget) return;
1011
+ if (props.darkClickClose && state.view === 3) {
823
1012
  hide();
824
1013
  }
825
1014
  };
@@ -827,6 +1016,7 @@
827
1016
  // 监听动画
828
1017
  const anim = (ev?: AnimationEvent) => {
829
1018
  if (ev && ev.target !== ev.currentTarget) return;
1019
+ clearAnimTimer();
830
1020
  if (state.view === 1) {
831
1021
  if (state.parkAfterHide) {
832
1022
  state.parkAfterHide = false;
@@ -834,11 +1024,14 @@
834
1024
  return;
835
1025
  }
836
1026
  state.view = 0;
837
- runCallbacks('hide');
1027
+ syncTaskView(0);
838
1028
  onGroupLayerClosed(id);
1029
+ runCallbacks('hide');
1030
+ return;
839
1031
  }
840
1032
  if (state.view === 2) {
841
1033
  state.view = 3;
1034
+ syncTaskView(3);
842
1035
  runCallbacks('show');
843
1036
  }
844
1037
  };
@@ -849,7 +1042,7 @@
849
1042
  console.error('Layer 有重复 id', id);
850
1043
  }
851
1044
  if (id) {
852
- _tasks[id] = {
1045
+ self = {
853
1046
  dark: props.dark,
854
1047
  darkClickClose: props.darkClickClose,
855
1048
  escClose: props.escClose,
@@ -868,6 +1061,7 @@
868
1061
  unpark,
869
1062
  view: state.view,
870
1063
  };
1064
+ _tasks[id] = self;
871
1065
  ensureEscapeListener();
872
1066
  } else {
873
1067
  console.error('Layer 必须填写全局统一的 id');
@@ -875,7 +1069,11 @@
875
1069
 
876
1070
  // 卸载弹层
877
1071
  onBeforeUnmount(() => {
878
- if (participatesInGroup(props.dark) && id) {
1072
+ clearAnimTimer();
1073
+ cancelPendingShow();
1074
+ // 同 id 已被新实例接管(路由过渡/HMR 时新实例先挂载):只释放本实例私有资源,不动全局注册与按 id 记录的栈/队列/暗层
1075
+ const owns = ownsRegistration();
1076
+ if (owns && participatesInGroup(props.dark) && id) {
879
1077
  const group = getAreaGroup(props.area);
880
1078
  removeFromQueue(group, id);
881
1079
  if (state.view > 0) {
@@ -885,10 +1083,14 @@
885
1083
  } else {
886
1084
  removeFromStack(group, id);
887
1085
  }
1086
+ // 卸载后 view watcher 可能不再执行,显式释放暗层占用
1087
+ releaseGlobalDark(props.area, group, id, true);
1088
+ }
1089
+ if (owns) {
1090
+ removeEscapeStack(id);
1091
+ delete _tasks[id];
888
1092
  }
889
- removeEscapeStack(id);
890
1093
  releaseScrollLock(scrollLockOwner);
891
- delete _tasks[id];
892
1094
  });
893
1095
  </script>
894
1096
 
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div ui-date="@a" class="fekit-date-view pr" :class="`${frame ? 'b-solid b-line b-xs r-sm' : ''}`" ui-flex="col xy" style="width: 22em">
2
+ <div ui-date="@a" class="mob:min-w-full fekit-date-view pr" :class="`${frame ? 'b-solid b-line b-xs r-sm' : ''}`" ui-flex="col xy" style="width: 22em">
3
3
  <div ui-date-head="" class="flex-fixed nx-ms mt-ss ny-sm b-solid b-line" ui-flex="row xm">
4
4
  <div ui-flex="row lm" class="o-lm">
5
5
  <div ui-btn="@a s none :square" @click="prev()">
@@ -60,6 +60,9 @@
60
60
  </div>
61
61
  </li>
62
62
  </ul>
63
+ <div v-if="$slots.footer" class="nx-sm ny-ss">
64
+ <slot name="footer" :close="closeDropdown"></slot>
65
+ </div>
63
66
  </template>
64
67
  </IFollowView>
65
68
  </div>
@@ -72,6 +75,7 @@
72
75
  import { matchOptionSearch } from './optionSearch';
73
76
  const ifollow: any = ref(null);
74
77
  const customInputRef: any = ref(null);
78
+ const closeDropdown = () => ifollow.value?.hide();
75
79
 
76
80
  const emits = defineEmits(['update:modelValue', 'change', 'onshow', 'onhide', 'update:other']);
77
81
 
@@ -187,6 +191,8 @@
187
191
 
188
192
  // 自定义输入值
189
193
  const customInputValue = ref('');
194
+ // 选择其他后立即显示输入框,不依赖已有自定义值。
195
+ const typing = ref(false);
190
196
 
191
197
  const state = reactive({
192
198
  show: 0,
@@ -194,9 +200,10 @@
194
200
  isInputting: computed(() => {
195
201
  // 如果配置了"其他"选项且当前选中的是"其他"
196
202
  if (props.other) {
203
+ if (typing.value) return true;
197
204
  if (props.other.field) {
198
- // 情况2:有独立字段,当主值匹配other.value且有otherValue时显示输入框
199
- return props.modelValue === props.other.value && props.otherValue;
205
+ // 情况2:独立字段可以尚未填写,主值选中其他即显示输入框。
206
+ return props.modelValue === props.other.value;
200
207
  } else {
201
208
  // 情况1:无独立字段,当值不在枚举中时显示输入框
202
209
  if (props.modelValue && props.modelValue !== '') {
@@ -323,33 +330,36 @@
323
330
  change('');
324
331
  }
325
332
  customInputValue.value = '';
333
+ typing.value = true;
326
334
  nextTick(() => {
327
- customInputRef.value?.focus();
335
+ // Input 未暴露 focus 方法,聚焦其原生输入框。
336
+ customInputRef.value?.$el?.querySelector?.('input')?.focus();
328
337
  });
329
338
  } else {
339
+ typing.value = false;
330
340
  change(item.value);
331
- ifollow.value.cancel();
341
+ closeDropdown();
332
342
  }
333
343
  };
334
344
 
335
- // 确认自定义输入
345
+ // 确认自定义输入(回车与失焦都会触发,需保持幂等,避免重复派发 change)
336
346
  const confirmCustomInput = () => {
337
- if (customInputValue.value.trim()) {
347
+ const text = customInputValue.value.trim();
348
+ if (text) {
338
349
  if (props.other?.field) {
339
350
  // 如果指定了 field,则主字段存储选项值,field 存储自定义输入
340
- change(props.other.value);
341
- emits('update:other', customInputValue.value.trim());
342
- } else {
351
+ if (props.modelValue !== props.other.value) change(props.other.value);
352
+ if (props.otherValue !== text) emits('update:other', text);
353
+ } else if (props.modelValue !== text) {
343
354
  // 如果没有指定 field,则直接在主字段存储自定义输入
344
- change(customInputValue.value.trim());
345
- }
346
- ifollow.value.cancel();
347
- } else {
348
- // 如果输入为空,清空值
349
- change(null);
350
- if (props.other?.field) {
351
- emits('update:other', '');
355
+ change(text);
352
356
  }
357
+ closeDropdown();
358
+ } else if (!props.other?.field) {
359
+ // 无独立字段时空输入即无值;有独立字段时保留已选的“其他”,等待补填。
360
+ if (props.modelValue != null && props.modelValue !== '') change(null);
361
+ } else if (props.otherValue) {
362
+ emits('update:other', '');
353
363
  }
354
364
  };
355
365
 
@@ -378,8 +388,9 @@
378
388
  };
379
389
  // 清空
380
390
  const remove = () => {
391
+ typing.value = false;
381
392
  change(null);
382
- ifollow.value.cancel();
393
+ closeDropdown();
383
394
  };
384
395
  // 取消
385
396
  const cancel = () => {
@@ -392,6 +403,7 @@
392
403
  // 折叠
393
404
  const onhide = () => {
394
405
  state.search = '';
406
+ typing.value = false;
395
407
  emits('onhide', state.selectd);
396
408
  };
397
409