@neatui/vue 4.5.0 → 4.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neatui/vue",
3
- "version": "4.5.0",
3
+ "version": "4.7.0",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@fekit/floor": "^3.0.1",
30
- "@fekit/follow": "^1.7.0",
30
+ "@fekit/follow": "^1.8.0",
31
31
  "@fekit/itable": "^1.1.6",
32
32
  "@fekit/pullload": "^1.3.5",
33
33
  "@fekit/scale": "^2.0.1",
@@ -56,6 +56,7 @@
56
56
  clickTipHide?: boolean;
57
57
  mobile?: boolean;
58
58
  title?: string;
59
+ zIndex?: string | number | null;
59
60
  }
60
61
 
61
62
  // 入参
@@ -73,6 +74,7 @@
73
74
  clickTipHide: false,
74
75
  mobile: true,
75
76
  title: '',
77
+ zIndex: undefined,
76
78
  });
77
79
 
78
80
  const dom: any = ref(null);
@@ -214,13 +216,20 @@
214
216
  }
215
217
  }
216
218
  },
217
- { hideOnScrollOut: false },
219
+ { hideOnScrollOut: false, zIndex: props.zIndex },
218
220
  );
219
221
  } else {
220
222
  ex.value = null;
221
223
  }
222
224
  });
223
225
 
226
+ watch(
227
+ () => props.zIndex,
228
+ (zIndex) => {
229
+ ex.value?.setOptions({ zIndex });
230
+ },
231
+ );
232
+
224
233
  onUnmounted(() => {
225
234
  document.removeEventListener('click', cancel, true);
226
235
  // 移除触摸事件监听器
@@ -123,8 +123,9 @@
123
123
  border-radius: 0.5em 0.5em 0 0;
124
124
  }
125
125
 
126
- // 显示动画
127
- @keyframes am-show-am {
126
+ // Keep picker keyframes off the shared am-show-am / am-hide-am names
127
+ // so Layer's am="am" preset is not overwritten.
128
+ @keyframes am-picker-show {
128
129
  0% {
129
130
  opacity: 0;
130
131
  transform: translate3d(0, 100%, 0) scale3d(1, 1, 1);
@@ -135,8 +136,7 @@
135
136
  }
136
137
  }
137
138
 
138
- // 隐藏动画
139
- @keyframes am-hide-am {
139
+ @keyframes am-picker-hide {
140
140
  0% {
141
141
  opacity: 1;
142
142
  transform: translate3d(0, 0%, 0) scale3d(1, 1, 1);
@@ -147,39 +147,34 @@
147
147
  }
148
148
  }
149
149
 
150
- // 引用显示
151
- %am-show-am {
152
- transform: translate3d(0, -150%, 0);
150
+ %am-picker-show {
151
+ transform: translate3d(0, 100%, 0);
153
152
  opacity: 0;
154
153
  transform-style: preserve-3d;
155
154
  }
156
155
 
157
- // 引用隐藏
158
- %am-hide-am {
156
+ %am-picker-hide {
159
157
  transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
160
- opacity: 0;
158
+ opacity: 1;
161
159
  transform-style: preserve-3d;
162
160
  }
163
161
 
164
- // 传参调用
165
162
  @mixin am-view-am($time: 450ms, $delay: 100ms) {
166
163
  &.show,
167
164
  &[view='2'] {
168
165
  & > * {
169
- @extend %am-show-am;
170
- animation: am-show-am $time $delay both;
166
+ @extend %am-picker-show;
167
+ animation: am-picker-show $time $delay both;
171
168
  }
172
169
  }
173
170
 
174
171
  &.hide,
175
172
  &[view='1'] {
176
- opacity: 0;
177
- transition: ($time + $delay) * (4 * 0.1) ($time + $delay) * (6 * 0.1);
178
173
  pointer-events: none;
179
174
 
180
175
  & > * {
181
- @extend %am-hide-am;
182
- animation: am-hide-am $time $delay both;
176
+ @extend %am-picker-hide;
177
+ animation: am-picker-hide $time $delay both;
183
178
  }
184
179
  }
185
180
 
@@ -189,36 +184,11 @@
189
184
  }
190
185
  }
191
186
 
192
- // 动画时间
193
187
  $time: 400ms;
194
188
  $delay: 100ms;
195
189
 
196
- // 全套引用
197
- [am-view='am'] {
198
- &.show,
199
- &[view='2'] {
200
- & > * {
201
- @extend %am-show-am;
202
- animation: am-show-am $time $delay both;
203
- }
204
- }
205
-
206
- &.hide,
207
- &[view='1'] {
208
- opacity: 0;
209
- transition: ($time + $delay) * 0.4 ($time + $delay) * 0.6;
210
- pointer-events: none;
211
-
212
- & > * {
213
- @extend %am-hide-am;
214
- animation: am-hide-am $time $delay ease-in-out both;
215
- }
216
- }
217
-
218
- &.none,
219
- &[view='0'] {
220
- display: none;
221
- }
190
+ .fekit-picker[am-view='am'] {
191
+ @include am-view-am($time, $delay);
222
192
  }
223
193
 
224
194
  @keyframes am-dark-show {
@@ -6,11 +6,14 @@
6
6
  v-if="state.view"
7
7
  :am-view="am"
8
8
  :data-key="id"
9
- :style="`${`${dark}` ? 'background-color: rgba(0, 0, 0, ' + dark + ');' : ''}${zIndex ? 'z-index:' + zIndex + ';' : ''}`"
9
+ :data-recess="state.recess || undefined"
10
+ :data-cover-dark="state.coverDark !== '' ? '' : undefined"
11
+ :style="layerStyle"
10
12
  :view="state.view"
11
13
  class="fekit-layer"
12
14
  >
13
15
  <div :ui-flex="`col ${flex}`" :style="wrapStyle" class="fekit-layer-wrap" @animationend="anim" @click="_sys_hide">
16
+ <div v-if="state.coverDark !== ''" class="fekit-layer-cover-dark"></div>
14
17
  <slot :param="_tasks[id]?.params"></slot>
15
18
  </div>
16
19
  </div>
@@ -18,7 +21,7 @@
18
21
  </div>
19
22
  </template>
20
23
  <script lang="ts" setup>
21
- import { onBeforeUnmount, reactive, watch } from 'vue';
24
+ import { computed, nextTick, onBeforeUnmount, reactive, watch } from 'vue';
22
25
  import { env, isFunction } from '@fekit/utils';
23
26
 
24
27
  // 弹层队列
@@ -60,6 +63,21 @@
60
63
  const SCROLL_LOCK_STATE = Symbol.for('@neatui/vue.Layer.scrollLock');
61
64
  const ESCAPE_STACK = Symbol.for('@neatui/vue.Layer.escapeStack');
62
65
  const ESCAPE_LISTENER = Symbol.for('@neatui/vue.Layer.escapeListener');
66
+ const AREA_GROUPS = Symbol.for('@neatui/vue.Layer.areaGroups');
67
+
68
+ type LayerMode = 'cover' | 'cascade' | 'displace' | 'replace' | 'queue';
69
+
70
+ type AreaGroup = {
71
+ dark: {
72
+ afterHide?: () => void;
73
+ el: HTMLElement | null;
74
+ listening: boolean;
75
+ owners: Set<string>;
76
+ view: number;
77
+ };
78
+ queue: Array<{ id: string; kind: 'open' | 'show'; params: any; then: any }>;
79
+ stack: string[];
80
+ };
63
81
 
64
82
  const getScrollLockState = (): ScrollLockState => {
65
83
  if (!_tasks[SCROLL_LOCK_STATE]) {
@@ -119,6 +137,199 @@
119
137
  });
120
138
  };
121
139
 
140
+ const getAreaGroups = (): Record<string, AreaGroup> => {
141
+ if (!_tasks[AREA_GROUPS]) {
142
+ Object.defineProperty(_tasks, AREA_GROUPS, {
143
+ configurable: true,
144
+ value: {},
145
+ });
146
+ }
147
+ return _tasks[AREA_GROUPS];
148
+ };
149
+
150
+ const getAreaGroup = (area: string): AreaGroup => {
151
+ const groups = getAreaGroups();
152
+ if (!groups[area]) {
153
+ groups[area] = {
154
+ dark: {
155
+ el: null,
156
+ listening: false,
157
+ owners: new Set<string>(),
158
+ view: 0,
159
+ },
160
+ queue: [],
161
+ stack: [],
162
+ };
163
+ }
164
+ return groups[area];
165
+ };
166
+
167
+ const resolveDarkOpacity = (value: string | number | undefined) => {
168
+ return `${value ?? ''}` === '' ? '0.75' : `${value}`;
169
+ };
170
+
171
+ const participatesInGroup = (dark: string | number | undefined) => `${dark ?? ''}` !== '0';
172
+ const DEFAULT_COVER_DARK = 0.32;
173
+
174
+ const resolveCoverDark = (mode: LayerMode, hasPrev: boolean, coverDark: unknown) => {
175
+ if (mode !== 'cover' || !hasPrev) return '';
176
+ if (coverDark === false || `${coverDark}` === '0') return '';
177
+ if (coverDark === undefined || coverDark === true || `${coverDark}` === '') return DEFAULT_COVER_DARK;
178
+ return coverDark as string | number;
179
+ };
180
+
181
+ const isBlockingView = (view: number) => view === 1 || view === 2 || view === 3;
182
+
183
+ const occupiesGlobalDark = (task: any) => {
184
+ if (!task?.participates) return false;
185
+ if (task.view === 2 || task.view === 3 || task.view === 4) return true;
186
+ return task.view === 1 && !!task.parkAfterHide;
187
+ };
188
+
189
+ const setGlobalDarkView = (group: AreaGroup, view: number) => {
190
+ group.dark.view = view;
191
+ if (group.dark.el) group.dark.el.setAttribute('view', `${view}`);
192
+ };
193
+
194
+ const ensureGlobalDarkEl = (area: string, group: AreaGroup) => {
195
+ if (!env.client) return null;
196
+ if (group.dark.el?.isConnected) return group.dark.el;
197
+ group.dark.el = null;
198
+ group.dark.listening = false;
199
+ const container = document.querySelector(area);
200
+ if (!container) return null;
201
+ let el = container.querySelector(':scope > .fekit-layer-dark') as HTMLElement | null;
202
+ if (!el) {
203
+ el = document.createElement('div');
204
+ el.className = 'fekit-layer-dark';
205
+ el.setAttribute('view', '0');
206
+ container.insertBefore(el, container.firstChild);
207
+ }
208
+ group.dark.el = el;
209
+ el.addEventListener('animationend', (ev) => {
210
+ 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
+ }
218
+ });
219
+ group.dark.listening = true;
220
+ return el;
221
+ };
222
+
223
+ const syncGlobalDarkOpacity = (area: string, group: AreaGroup) => {
224
+ let opacity = '0.75';
225
+ for (let index = group.stack.length - 1; index >= 0; index -= 1) {
226
+ const task = _tasks[group.stack[index]];
227
+ if (occupiesGlobalDark(task)) {
228
+ opacity = resolveDarkOpacity(task.dark);
229
+ break;
230
+ }
231
+ }
232
+ const el = group.dark.owners.size ? ensureGlobalDarkEl(area, group) : group.dark.el;
233
+ if (el) el.style.setProperty('--layer-dark', opacity);
234
+ };
235
+
236
+ const acquireGlobalDark = (area: string, group: AreaGroup, layerId: string, immediate = false) => {
237
+ if (!layerId) return;
238
+ if (group.dark.owners.has(layerId)) {
239
+ syncGlobalDarkOpacity(area, group);
240
+ return;
241
+ }
242
+ group.dark.owners.add(layerId);
243
+ const el = ensureGlobalDarkEl(area, group);
244
+ if (!el) return;
245
+ syncGlobalDarkOpacity(area, group);
246
+ if (group.dark.owners.size > 1) {
247
+ if (group.dark.view === 1) setGlobalDarkView(group, 3);
248
+ else if (group.dark.view === 0) setGlobalDarkView(group, immediate ? 3 : 2);
249
+ return;
250
+ }
251
+ if (immediate) {
252
+ setGlobalDarkView(group, 3);
253
+ return;
254
+ }
255
+ if (group.dark.view === 0) setGlobalDarkView(group, 2);
256
+ else if (group.dark.view === 1) setGlobalDarkView(group, 3);
257
+ };
258
+
259
+ const releaseGlobalDark = (area: string, group: AreaGroup, layerId: string, immediate = false) => {
260
+ if (!layerId || !group.dark.owners.delete(layerId)) {
261
+ syncGlobalDarkOpacity(area, group);
262
+ return;
263
+ }
264
+ syncGlobalDarkOpacity(area, group);
265
+ if (group.dark.owners.size > 0) return;
266
+ if (immediate || !group.dark.el) {
267
+ setGlobalDarkView(group, 0);
268
+ const afterHide = group.dark.afterHide;
269
+ group.dark.afterHide = undefined;
270
+ afterHide?.();
271
+ return;
272
+ }
273
+ if (group.dark.view === 0) return;
274
+ setGlobalDarkView(group, 1);
275
+ };
276
+
277
+ const visibleGroupIds = (group: AreaGroup) =>
278
+ group.stack.filter((layerId) => {
279
+ const view = _tasks[layerId]?.view;
280
+ return view === 2 || view === 3;
281
+ });
282
+
283
+ const groupHasBlocking = (group: AreaGroup, exceptId = '') =>
284
+ group.stack.some((layerId) => layerId !== exceptId && isBlockingView(_tasks[layerId]?.view));
285
+
286
+ const syncStackIndex = (group: AreaGroup) => {
287
+ group.stack.forEach((layerId, index) => {
288
+ _tasks[layerId]?.setStackIndex?.(index);
289
+ });
290
+ };
291
+
292
+ const recomputeRecess = (group: AreaGroup) => {
293
+ const visible = visibleGroupIds(group);
294
+ visible.forEach((layerId, index) => {
295
+ let depth = 0;
296
+ for (let next = index + 1; next < visible.length; next += 1) {
297
+ if (_tasks[visible[next]]?.openMode === 'cascade') depth += 1;
298
+ }
299
+ _tasks[layerId]?.setRecess?.(depth > 2 ? 2 : depth);
300
+ });
301
+ group.stack.forEach((layerId) => {
302
+ if (!visible.includes(layerId)) _tasks[layerId]?.setRecess?.(0);
303
+ });
304
+ };
305
+
306
+ const ensureOnStack = (group: AreaGroup, layerId: string) => {
307
+ const index = group.stack.indexOf(layerId);
308
+ if (index !== -1) group.stack.splice(index, 1);
309
+ group.stack.push(layerId);
310
+ syncStackIndex(group);
311
+ };
312
+
313
+ const removeFromStack = (group: AreaGroup, layerId: string) => {
314
+ const index = group.stack.indexOf(layerId);
315
+ if (index !== -1) group.stack.splice(index, 1);
316
+ syncStackIndex(group);
317
+ };
318
+
319
+ const removeFromQueue = (group: AreaGroup, layerId: string) => {
320
+ group.queue = group.queue.filter((job) => job.id !== layerId);
321
+ };
322
+
323
+ const getVisibleTop = (group: AreaGroup, exceptId = '') => {
324
+ for (let index = group.stack.length - 1; index >= 0; index -= 1) {
325
+ const layerId = group.stack[index];
326
+ if (layerId === exceptId) continue;
327
+ const task = _tasks[layerId];
328
+ if (task && (task.view === 2 || task.view === 3)) return task;
329
+ }
330
+ return undefined;
331
+ };
332
+
122
333
  const hasInlineProperty = (style: CSSStyleDeclaration, property: string) => {
123
334
  for (let index = 0; index < style.length; index += 1) {
124
335
  if (style.item(index) === property) return true;
@@ -307,25 +518,78 @@
307
518
 
308
519
  // 状态管理
309
520
  const state = reactive({
310
- view: 0,
521
+ coverDark: '' as string | number,
522
+ immediate: false,
523
+ openMode: 'cover' as LayerMode,
311
524
  params: {},
525
+ parkAfterHide: false,
526
+ recess: 0,
527
+ stackIndex: 0,
528
+ view: 0,
312
529
  });
313
530
  const scrollLockOwner = {};
314
531
 
532
+ const layerStyle = computed(() => {
533
+ let style = '';
534
+ if (`${props.dark}` !== '') style += `--layer-dark:${props.dark};`;
535
+ if (`${state.coverDark}` !== '') style += `--layer-cover-dark:${state.coverDark};`;
536
+ if (props.zIndex) style += `z-index:${props.zIndex};`;
537
+ else if (state.stackIndex > 0) style += `z-index:${50 + state.stackIndex};`;
538
+ return style;
539
+ });
540
+
541
+ 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;
548
+ };
549
+
550
+ const syncOwnDarkOccupancy = () => {
551
+ if (!env.client || !participatesInGroup(props.dark) || !props.id) return;
552
+ const group = getAreaGroup(props.area);
553
+ const immediate = state.immediate;
554
+ if (
555
+ occupiesGlobalDark({
556
+ dark: props.dark,
557
+ parkAfterHide: state.parkAfterHide,
558
+ participates: true,
559
+ view: state.view,
560
+ })
561
+ ) {
562
+ acquireGlobalDark(props.area, group, props.id, immediate);
563
+ } else {
564
+ releaseGlobalDark(props.area, group, props.id, immediate);
565
+ }
566
+ };
567
+
315
568
  watch(
316
569
  () => state.view,
317
570
  (view: any) => {
318
- if (_tasks[props.id]) {
319
- _tasks[props.id].view = view;
320
- }
571
+ syncTaskView(view);
321
572
  if (view === 2 || view === 3) pushEscapeStack(props.id);
322
573
  else removeEscapeStack(props.id);
323
574
  if (view > 0) acquireScrollLock(scrollLockOwner);
324
575
  else releaseScrollLock(scrollLockOwner);
576
+ syncOwnDarkOccupancy();
577
+ state.immediate = false;
578
+ if (participatesInGroup(props.dark) && props.id) {
579
+ nextTick(() => recomputeRecess(getAreaGroup(props.area)));
580
+ }
325
581
  },
326
582
  { deep: true, immediate: true },
327
583
  );
328
584
 
585
+ watch(
586
+ () => props.dark,
587
+ () => {
588
+ syncTaskView(state.view);
589
+ syncOwnDarkOccupancy();
590
+ },
591
+ );
592
+
329
593
  watch(
330
594
  () => props.escClose,
331
595
  (escClose) => {
@@ -350,6 +614,138 @@
350
614
  none: [],
351
615
  };
352
616
 
617
+ const resolveMode = (params: any): LayerMode => {
618
+ const mode = params?.mode;
619
+ if (mode === 'cascade' || mode === 'displace' || mode === 'replace' || mode === 'queue') return mode;
620
+ return 'cover';
621
+ };
622
+
623
+ const bindOpenCallbacks = (params: any = {}, then = () => {}) => {
624
+ if (params?.on?.show) on.show.push(params.on.show);
625
+ if (params?.on?.hide) on.hide.push(params.on.hide);
626
+ on.show.push(then);
627
+ };
628
+
629
+ const runCallbacks = (key: 'show' | 'hide' | 'none') => {
630
+ on[key].forEach((fun: any) => {
631
+ if (isFunction(fun)) fun();
632
+ });
633
+ on[key].length = 0;
634
+ };
635
+
636
+ const flushQueue = (group: AreaGroup) => {
637
+ if (groupHasBlocking(group) || !group.queue.length) return;
638
+ const job = group.queue.shift();
639
+ if (!job) return;
640
+ const task = _tasks[job.id];
641
+ if (!task) {
642
+ flushQueue(group);
643
+ return;
644
+ }
645
+ const playParams = { ...job.params };
646
+ if (playParams.mode === 'queue') delete playParams.mode;
647
+ if (job.kind === 'open') task.open(playParams, job.then);
648
+ else task.show(playParams, job.then);
649
+ };
650
+
651
+ const scheduleFlushQueue = (group: AreaGroup) => {
652
+ const flush = () => flushQueue(group);
653
+ if (group.dark.view === 1) {
654
+ const prev = group.dark.afterHide;
655
+ group.dark.afterHide = () => {
656
+ prev?.();
657
+ flush();
658
+ };
659
+ return;
660
+ }
661
+ nextTick(flush);
662
+ };
663
+
664
+ const onGroupLayerClosed = (layerId: string) => {
665
+ if (!participatesInGroup(props.dark)) return;
666
+ const group = getAreaGroup(props.area);
667
+ removeFromStack(group, layerId);
668
+ const topId = group.stack[group.stack.length - 1];
669
+ if (topId && _tasks[topId]?.view === 4) _tasks[topId].unpark();
670
+ recomputeRecess(group);
671
+ scheduleFlushQueue(group);
672
+ };
673
+
674
+ const closeReplacedLayers = (mode: LayerMode, params: any, immediate: boolean) => {
675
+ if (mode !== 'replace' || !participatesInGroup(props.dark) || !props.id) return;
676
+ const group = getAreaGroup(props.area);
677
+ const prev = getVisibleTop(group, props.id);
678
+ const targets = params?.replace === 'all' ? group.stack.filter((layerId) => layerId !== props.id) : prev ? [prev.id] : [];
679
+ targets.forEach((layerId) => {
680
+ const task = _tasks[layerId];
681
+ if (!task) return;
682
+ if (immediate) task.none();
683
+ else task.hide();
684
+ });
685
+ };
686
+
687
+ const applyIncomingRelation = (mode: LayerMode, params: any, immediate: boolean) => {
688
+ if (!participatesInGroup(props.dark) || !props.id) {
689
+ state.coverDark = '';
690
+ state.recess = 0;
691
+ return;
692
+ }
693
+ const group = getAreaGroup(props.area);
694
+ const prev = getVisibleTop(group, props.id);
695
+ state.coverDark = resolveCoverDark(mode, !!prev, params?.coverDark);
696
+ state.recess = 0;
697
+
698
+ if (mode === 'displace' && prev) prev.park({ immediate });
699
+ ensureOnStack(group, props.id);
700
+ };
701
+
702
+ const syncGroupPresentation = () => {
703
+ if (!participatesInGroup(props.dark) || !props.id) return;
704
+ recomputeRecess(getAreaGroup(props.area));
705
+ };
706
+
707
+ const tryEnqueue = (kind: 'open' | 'show', params: any, then: any) => {
708
+ if (!participatesInGroup(props.dark) || resolveMode(params) !== 'queue' || !props.id) return false;
709
+ const group = getAreaGroup(props.area);
710
+ if (!groupHasBlocking(group, props.id)) return false;
711
+ removeFromQueue(group, props.id);
712
+ group.queue.push({ id: props.id, kind, params, then });
713
+ return true;
714
+ };
715
+
716
+ const setRecess = (depth: number) => {
717
+ state.recess = depth;
718
+ };
719
+
720
+ const setStackIndex = (index: number) => {
721
+ state.stackIndex = index;
722
+ };
723
+
724
+ const park = (options: any = {}) => {
725
+ if (options.immediate) {
726
+ state.parkAfterHide = false;
727
+ state.immediate = true;
728
+ state.view = 4;
729
+ return;
730
+ }
731
+ if (state.view === 2 || state.view === 3) {
732
+ state.parkAfterHide = true;
733
+ syncTaskView(state.view);
734
+ state.view = 1;
735
+ }
736
+ };
737
+
738
+ const unpark = (options: any = {}) => {
739
+ if (state.view !== 4) return;
740
+ state.parkAfterHide = false;
741
+ if (options.immediate) {
742
+ state.immediate = true;
743
+ state.view = 3;
744
+ return;
745
+ }
746
+ state.view = 2;
747
+ };
748
+
353
749
  // 注入参数
354
750
  const init = (params: any = {}) => {
355
751
  _tasks[id].params = params;
@@ -364,68 +760,86 @@
364
760
 
365
761
  // 显示弹层
366
762
  const show = (params: any = {}, then = () => {}) => {
763
+ if (tryEnqueue('show', params, then)) return;
764
+ if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
765
+ bindOpenCallbacks(params, then);
766
+ state.openMode = resolveMode(params);
767
+ applyIncomingRelation(state.openMode, params, false);
768
+ state.parkAfterHide = false;
367
769
  state.view = 2;
368
- if (params?.on?.show) {
369
- on.show.push(params.on.show);
370
- }
371
- if (params?.on?.hide) {
372
- on.hide.push(params.on.hide);
373
- }
374
- on.show.push(then);
770
+ closeReplacedLayers(state.openMode, params, false);
771
+ syncGroupPresentation();
375
772
  };
376
773
 
377
774
  // 隐藏弹层
378
775
  const hide = (_params: any = {}, then = () => {}) => {
379
- if (state.view === 3) {
776
+ if (state.view === 4) {
777
+ on.hide.push(then);
778
+ state.parkAfterHide = false;
779
+ state.immediate = true;
780
+ state.view = 0;
781
+ runCallbacks('hide');
782
+ onGroupLayerClosed(id);
783
+ return;
784
+ }
785
+ if (state.view === 2 || state.view === 3) {
786
+ state.parkAfterHide = false;
787
+ syncTaskView(state.view);
380
788
  state.view = 1;
381
789
  on.hide.push(then);
382
790
  }
383
791
  };
384
792
 
385
793
  // 打开弹层(没有动画过程)
386
- const open = (_params: any = {}, then = () => {}) => {
794
+ const open = (params: any = {}, then = () => {}) => {
795
+ if (tryEnqueue('open', params, then)) return;
796
+ if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
797
+ bindOpenCallbacks(params, then);
798
+ state.openMode = resolveMode(params);
799
+ applyIncomingRelation(state.openMode, params, true);
800
+ state.parkAfterHide = false;
801
+ state.immediate = true;
387
802
  state.view = 3;
388
- on.show.push(then);
803
+ closeReplacedLayers(state.openMode, params, true);
804
+ syncGroupPresentation();
805
+ runCallbacks('show');
389
806
  };
390
807
  // 关闭弹层(没有动画过程)
391
808
  const none = (_params: any = {}, then = () => {}) => {
809
+ const wasOpen = state.view > 0;
810
+ if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
811
+ state.parkAfterHide = false;
812
+ state.immediate = true;
392
813
  state.view = 0;
393
814
  on.none.push(then);
394
815
  // 立即执行回调,因为没有动画
395
- on.none.forEach((fun: any) => {
396
- if (isFunction(fun)) {
397
- fun();
398
- }
399
- });
400
- on.none.length = 0;
816
+ runCallbacks('none');
817
+ if (wasOpen) onGroupLayerClosed(id);
401
818
  };
402
819
 
403
820
  // 点暗层关闭
404
821
  const _sys_hide = (ev: any) => {
405
822
  if (props.darkClickClose && ev.target.className === 'fekit-layer-wrap' && state.view === 3) {
406
- state.view = 1;
823
+ hide();
407
824
  }
408
825
  };
409
826
 
410
827
  // 监听动画
411
- const anim = () => {
828
+ const anim = (ev?: AnimationEvent) => {
829
+ if (ev && ev.target !== ev.currentTarget) return;
412
830
  if (state.view === 1) {
831
+ if (state.parkAfterHide) {
832
+ state.parkAfterHide = false;
833
+ state.view = 4;
834
+ return;
835
+ }
413
836
  state.view = 0;
414
- on.hide.forEach((fun: any) => {
415
- if (isFunction(fun)) {
416
- fun();
417
- }
418
- });
419
- on.hide.length = 0; // 清空回调数组
837
+ runCallbacks('hide');
838
+ onGroupLayerClosed(id);
420
839
  }
421
840
  if (state.view === 2) {
422
841
  state.view = 3;
423
- on.show.forEach((fun: any) => {
424
- if (isFunction(fun)) {
425
- fun();
426
- }
427
- });
428
- on.show.length = 0; // 清空回调数组
842
+ runCallbacks('show');
429
843
  }
430
844
  };
431
845
 
@@ -435,7 +849,25 @@
435
849
  console.error('Layer 有重复 id', id);
436
850
  }
437
851
  if (id) {
438
- _tasks[id] = { init, show, hide, open, none, view: state.view, escClose: props.escClose, darkClickClose: props.darkClickClose };
852
+ _tasks[id] = {
853
+ dark: props.dark,
854
+ darkClickClose: props.darkClickClose,
855
+ escClose: props.escClose,
856
+ hide,
857
+ id,
858
+ init,
859
+ none,
860
+ open,
861
+ openMode: state.openMode,
862
+ park,
863
+ parkAfterHide: state.parkAfterHide,
864
+ participates: participatesInGroup(props.dark),
865
+ setRecess,
866
+ setStackIndex,
867
+ show,
868
+ unpark,
869
+ view: state.view,
870
+ };
439
871
  ensureEscapeListener();
440
872
  } else {
441
873
  console.error('Layer 必须填写全局统一的 id');
@@ -443,6 +875,17 @@
443
875
 
444
876
  // 卸载弹层
445
877
  onBeforeUnmount(() => {
878
+ if (participatesInGroup(props.dark) && id) {
879
+ const group = getAreaGroup(props.area);
880
+ removeFromQueue(group, id);
881
+ if (state.view > 0) {
882
+ state.immediate = true;
883
+ state.view = 0;
884
+ onGroupLayerClosed(id);
885
+ } else {
886
+ removeFromStack(group, id);
887
+ }
888
+ }
446
889
  removeEscapeStack(id);
447
890
  releaseScrollLock(scrollLockOwner);
448
891
  delete _tasks[id];
@@ -453,24 +896,122 @@
453
896
  @use '@neatui/css' as neatui;
454
897
 
455
898
  // 弹层
899
+ @keyframes am-layer-dark-show {
900
+ 0% {
901
+ opacity: 0;
902
+ }
903
+ 100% {
904
+ opacity: 1;
905
+ }
906
+ }
907
+
908
+ @keyframes am-layer-dark-hide {
909
+ 0% {
910
+ opacity: 1;
911
+ }
912
+ 100% {
913
+ opacity: 0;
914
+ }
915
+ }
916
+
917
+ .fekit-layer-dark {
918
+ --layer-dark: 0.75;
919
+ position: fixed;
920
+ inset: 0;
921
+ z-index: 50;
922
+ pointer-events: none;
923
+ background-color: rgba(0, 0, 0, var(--layer-dark));
924
+ opacity: 0;
925
+ transition: background-color 180ms linear;
926
+
927
+ &[view='2'] {
928
+ animation: am-layer-dark-show 320ms cubic-bezier(0.4, 0, 0.2, 1) both;
929
+ }
930
+
931
+ &[view='3'] {
932
+ opacity: 1;
933
+ }
934
+
935
+ &[view='1'] {
936
+ animation: am-layer-dark-hide 260ms cubic-bezier(0.4, 0, 0.2, 1) both;
937
+ }
938
+
939
+ &[view='0'] {
940
+ opacity: 0;
941
+ }
942
+ }
943
+
456
944
  .fekit-layer {
945
+ --layer-dark: 0.75;
946
+ --layer-cover-dark: 0.32;
457
947
  position: fixed;
458
948
  width: 100%;
459
949
  height: 100%;
460
950
  left: 0;
461
951
  top: 0;
462
952
  z-index: 50;
463
- background-color: rgba(0, 0, 0, 0.75);
953
+ background-color: transparent;
464
954
  pointer-events: none;
465
955
 
956
+ .fekit-layer-cover-dark {
957
+ position: absolute;
958
+ inset: 0;
959
+ z-index: 0;
960
+ background-color: rgba(0, 0, 0, var(--layer-cover-dark));
961
+ opacity: 0;
962
+ pointer-events: none;
963
+ }
964
+
965
+ &[view='2'] .fekit-layer-cover-dark {
966
+ animation: am-layer-dark-show 320ms cubic-bezier(0.4, 0, 0.2, 1) both;
967
+ }
968
+
969
+ &[view='3'] .fekit-layer-cover-dark {
970
+ opacity: 1;
971
+ }
972
+
973
+ &[view='1'] .fekit-layer-cover-dark {
974
+ animation: am-layer-dark-hide 260ms cubic-bezier(0.4, 0, 0.2, 1) both;
975
+ }
976
+
977
+ &[view='4'] {
978
+ pointer-events: none;
979
+
980
+ .fekit-layer-wrap {
981
+ pointer-events: none;
982
+
983
+ & > * {
984
+ opacity: 0;
985
+ visibility: hidden;
986
+ }
987
+ }
988
+ }
989
+
990
+ &[view='3'][data-recess='1'] .fekit-layer-wrap > *:not(.fekit-layer-cover-dark) {
991
+ transform: scale(0.94);
992
+ opacity: 0.88;
993
+ transition:
994
+ transform 280ms cubic-bezier(0.4, 0, 0.2, 1),
995
+ opacity 280ms cubic-bezier(0.4, 0, 0.2, 1);
996
+ }
997
+
998
+ &[view='3'][data-recess='2'] .fekit-layer-wrap > *:not(.fekit-layer-cover-dark) {
999
+ transform: scale(0.88);
1000
+ opacity: 0.76;
1001
+ transition:
1002
+ transform 280ms cubic-bezier(0.4, 0, 0.2, 1),
1003
+ opacity 280ms cubic-bezier(0.4, 0, 0.2, 1);
1004
+ }
1005
+
466
1006
  &-wrap {
467
1007
  position: relative;
468
1008
  width: 100%;
469
1009
  height: 100%;
470
1010
  pointer-events: auto;
471
1011
 
472
- & > * {
1012
+ & > *:not(.fekit-layer-cover-dark) {
473
1013
  position: relative !important;
1014
+ z-index: 1;
474
1015
 
475
1016
  &.com-dialog {
476
1017
  max-width: 80%;
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- :ui-form="`${theme} sz:${sz} ${state.error.co} type:textarea ${block ? ' :block' : ''} ${disabled ? 'disabled' : ''} ${readonly ? 'readonly' : ''}`"
3
+ :ui-form="`${theme} sz:${sz} ${state.error.co} type:textarea ${isAutoHeight ? 'auto-height' : ''} ${block ? ' :block' : ''} ${disabled ? 'disabled' : ''} ${readonly ? 'readonly' : ''}`"
4
4
  @click="emits('click', $event)"
5
5
  >
6
6
  <div :ui-form-tips="state.error.ontips">{{ state.error.tips }}</div>
@@ -21,8 +21,8 @@
21
21
  :placeholder="placeholder"
22
22
  :disabled="disabled"
23
23
  :readonly="readonly"
24
- :rows="rows"
25
- ui-scroll=":y"
24
+ :rows="displayRows"
25
+ :ui-scroll="isAutoHeight && !isOverflowing ? undefined : ':y'"
26
26
  dir="auto"
27
27
  @focus="emits('focus', $event, state)"
28
28
  @input="input($event)"
@@ -45,7 +45,7 @@
45
45
  </template>
46
46
 
47
47
  <script setup lang="ts">
48
- import { reactive, watch, ref, nextTick, onMounted } from 'vue';
48
+ import { reactive, watch, ref, nextTick, onMounted, onBeforeUnmount, computed } from 'vue';
49
49
  import { isObject, isFunction } from '@fekit/utils';
50
50
 
51
51
  const emits = defineEmits(['update:modelValue', 'click', 'blur', 'focus', 'change', 'input', 'clear']);
@@ -71,8 +71,10 @@
71
71
  * prefix 前缀内容
72
72
  * suffix 后缀内容
73
73
  * interacted 用户是否交互
74
- * autoHeight 是否自动调整高度
75
- * rows 初始行数
74
+ * autoHeight 是否自动调整高度;同时传入 minRows 与 maxRows 时默认开启;单独开启时无限增高
75
+ * rows 初始行数(未开启自动高度,或开启但未同时设 min/max 时)
76
+ * minRows 与 maxRows 同时传入时生效的最小行数
77
+ * maxRows 与 minRows 同时传入时生效的最大行数,超出后出现滚动条
76
78
  */
77
79
 
78
80
  // 类型
@@ -95,8 +97,10 @@
95
97
  prefix?: Array<any>;
96
98
  suffix?: Array<any>;
97
99
  interacted?: boolean | number | null;
98
- autoHeight?: boolean;
100
+ autoHeight?: boolean | null;
99
101
  rows?: number;
102
+ minRows?: number;
103
+ maxRows?: number;
100
104
  }
101
105
  const props: any = withDefaults(defineProps<Props>(), {
102
106
  modelValue: '',
@@ -117,8 +121,9 @@
117
121
  prefix: () => [],
118
122
  suffix: () => [],
119
123
  interacted: null,
124
+ // Boolean 未传时 Vue 会落到 false,无法与「显式关闭」区分
125
+ autoHeight: null,
120
126
  // textarea 特有属性默认值
121
- autoHeight: false,
122
127
  rows: 3,
123
128
  });
124
129
 
@@ -132,18 +137,81 @@
132
137
  });
133
138
 
134
139
  // textarea 元素引用
135
- const el: any = ref(null);
140
+ const el = ref<HTMLTextAreaElement | null>(null);
141
+ const isOverflowing = ref(false);
142
+
143
+ const fRowLimit = (value: unknown, fallback: number) => {
144
+ const n = Number(value);
145
+ return Number.isFinite(n) && n > 0 ? Math.max(1, Math.floor(n)) : fallback;
146
+ };
147
+
148
+ const hasRowRange = computed(() => props.minRows != null && props.maxRows != null);
149
+ const isAutoHeight = computed(() => (props.autoHeight === false ? false : props.autoHeight === true ? true : hasRowRange.value));
150
+ const resolvedMinRows = computed(() => fRowLimit(props.minRows, 1));
151
+ const resolvedMaxRows = computed(() => Math.max(resolvedMinRows.value, fRowLimit(props.maxRows, 10)));
152
+ // 有 min/max 时从最小行起算;仅 autoHeight 时保留 rows,并无限增高
153
+ const displayRows = computed(() => (isAutoHeight.value && hasRowRange.value ? resolvedMinRows.value : props.rows));
154
+
155
+ let measurementQueued = false;
156
+ let resizeObserver: ResizeObserver | undefined;
157
+ let observedWidth = 0;
158
+
159
+ const fMeasureHeight = () => {
160
+ measurementQueued = false;
161
+ const node = el.value;
162
+ if (!node) return;
163
+
164
+ if (!isAutoHeight.value) {
165
+ node.style.height = '';
166
+ node.style.overflowY = '';
167
+ isOverflowing.value = false;
168
+ return;
169
+ }
170
+
171
+ // 未同时设 min/max:跟内容无限增高,不封顶
172
+ if (!hasRowRange.value) {
173
+ node.style.height = 'auto';
174
+ node.style.overflowY = 'hidden';
175
+ node.style.height = `${node.scrollHeight + 1}px`;
176
+ isOverflowing.value = false;
177
+ return;
178
+ }
179
+
180
+ node.style.height = '0px';
181
+ node.style.overflowY = 'hidden';
182
+
183
+ const style = getComputedStyle(node);
184
+ let lineHeight = parseFloat(style.lineHeight);
185
+ if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
186
+ lineHeight = (parseFloat(style.fontSize) || 14) * 1.5;
187
+ }
188
+
189
+ const paddingBlock = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
190
+ const minHeight = resolvedMinRows.value * lineHeight + paddingBlock;
191
+ const maxHeight = resolvedMaxRows.value * lineHeight + paddingBlock;
192
+ const contentHeight = node.scrollHeight;
193
+ const overflowing = contentHeight > maxHeight + 1;
194
+ const measuredHeight = contentHeight <= minHeight + 1 ? minHeight : contentHeight;
195
+ const targetHeight = Math.min(Math.max(measuredHeight, minHeight), maxHeight);
196
+
197
+ node.style.height = `${Math.ceil(targetHeight)}px`;
198
+ node.style.overflowY = overflowing ? 'auto' : 'hidden';
199
+ isOverflowing.value = overflowing;
200
+ };
136
201
 
137
- // 自动高度功能
138
202
  const fAutoHeight = () => {
139
- nextTick(() => {
140
- if (props.autoHeight && el.value) {
141
- el.value.style.height = 'auto';
142
- el.value.style.height = el.value.scrollHeight + 1 + 'px';
143
- }
144
- });
203
+ if (measurementQueued) return;
204
+ measurementQueued = true;
205
+ nextTick(fMeasureHeight);
145
206
  };
146
207
 
208
+ watch(
209
+ [() => isAutoHeight.value, () => resolvedMinRows.value, () => resolvedMaxRows.value, () => props.modelValue, () => props.rows, () => props.sz, () => props.theme],
210
+ () => {
211
+ fAutoHeight();
212
+ },
213
+ );
214
+
147
215
  // 外部传入交互标识,主要用于外部没有输入但点击了提交
148
216
  watch(
149
217
  () => props.interacted,
@@ -267,7 +335,23 @@
267
335
 
268
336
  // 组件挂载后初始化自动高度
269
337
  onMounted(() => {
270
- // 自动高度
338
+ const node = el.value;
339
+ if (node && typeof ResizeObserver !== 'undefined') {
340
+ observedWidth = node.getBoundingClientRect().width;
341
+ resizeObserver = new ResizeObserver(() => {
342
+ const width = node.getBoundingClientRect().width;
343
+ if (Math.abs(width - observedWidth) > 0.5) {
344
+ observedWidth = width;
345
+ fAutoHeight();
346
+ }
347
+ });
348
+ resizeObserver.observe(node);
349
+ }
271
350
  fAutoHeight();
272
351
  });
352
+
353
+ onBeforeUnmount(() => {
354
+ resizeObserver?.disconnect();
355
+ resizeObserver = undefined;
356
+ });
273
357
  </script>