@neatui/vue 5.5.2 → 5.6.1

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": "5.5.2",
3
+ "version": "5.6.1",
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.5.2",
39
+ "@neatui/css": "^5.6.1",
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",
@@ -16,14 +16,23 @@
16
16
  @mouseout="tipHover(0)"
17
17
  @click="clickhide"
18
18
  >
19
- <div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="tipBoxStyle">
20
- <div ui-hide=">mob" class="mob-follow-tips w-full nx-sl nt-sl nb-ss" ui-flex="row xm">
19
+ <div
20
+ ref="box"
21
+ :ui-tips-box="state.pos"
22
+ :class="tipBoxClass"
23
+ :style="tipBoxStyle"
24
+ @touchstart="onSheetTouchStart"
25
+ @touchmove="onSheetTouchMove"
26
+ @touchend="onSheetTouchEnd"
27
+ @touchcancel="onSheetTouchCancel"
28
+ >
29
+ <div ui-hide=">mob" :class="`mob-follow-tips w-full nx-sl nt-sl ${closable ? 'nb-ss' : 'nb-sl'}`" ui-flex="row xm">
21
30
  <div class="w-xs*s"></div>
22
31
  <div>
23
32
  <h5 class="bold">{{ title }}</h5>
24
33
  </div>
25
34
  <div class="w-xs*s" ui-flex="row rm">
26
- <button ui-btn="@a none s :square" @click="toggle">
35
+ <button v-if="closable" ui-btn="@a none s :square" aria-label="关闭" @click="toggle">
27
36
  <i ui-icon="close"></i>
28
37
  </button>
29
38
  </div>
@@ -41,6 +50,12 @@
41
50
  // hover:Teleport 内容的鼠标进出不会冒泡到祖先,需要逐级通知。
42
51
  type FollowContext = { children: Set<FollowBoundary>; hover: (on: boolean) => void };
43
52
  const followContextKey = Symbol('follow-context');
53
+ // 按打开顺序记录已打开的浮层,Esc 只由最后打开的一层处理。
54
+ const openStack: FollowBoundary[] = [];
55
+ const removeOpen = (item: FollowBoundary) => {
56
+ const idx = openStack.indexOf(item);
57
+ if (idx > -1) openStack.splice(idx, 1);
58
+ };
44
59
  </script>
45
60
  <script setup lang="ts">
46
61
  import { reactive, ref, onMounted, onUnmounted, watch, inject, provide } from 'vue';
@@ -66,6 +81,8 @@
66
81
  dragTipHide?: boolean;
67
82
  mobile?: boolean;
68
83
  title?: string;
84
+ /** 移动端底部面板标题栏是否显示关闭按钮。 */
85
+ closable?: boolean;
69
86
  zIndex?: string | number | null;
70
87
  /** 桌面端是否放入浏览器 top layer(Popover API),盖住整页任意层级;移动端底部面板始终放入。 */
71
88
  topLayer?: boolean;
@@ -88,6 +105,7 @@
88
105
  dragTipHide: undefined,
89
106
  mobile: true,
90
107
  title: '',
108
+ closable: false,
91
109
  zIndex: undefined,
92
110
  topLayer: true,
93
111
  });
@@ -162,8 +180,13 @@
162
180
  () => state.show,
163
181
  (show) => {
164
182
  if (show) {
183
+ removeOpen(containsFollow);
184
+ openStack.push(containsFollow);
165
185
  emits('onshow');
166
186
  } else {
187
+ removeOpen(containsFollow);
188
+ // 拖动中被其他途径关闭时,清理拖动残留的内联样式。
189
+ if (sheetDrag) endSheetDrag(false);
167
190
  children.forEach(item => item.hide());
168
191
  emits('onhide');
169
192
  }
@@ -359,6 +382,156 @@
359
382
  };
360
383
  const onTouchCancel = () => { outsideTouch = null; };
361
384
 
385
+ // 底部面板下滑关闭(对齐 iOS/Android 原生嵌套滚动):
386
+ // 标题栏随时可拖;内容区在手指下所有可滚动祖先都位于顶部时,向下拖动由面板接管;
387
+ // 同一手势中内容先滚回顶部、继续下拉也会接力为拖动;拖回原位后交还内容滚动。
388
+ const DRAG_SLOP = 4;
389
+ const SHEET_MASK_ALPHA = 0.5; // 与样式中暗层透明度一致
390
+ const VELOCITY_WINDOW = 100; // ms
391
+ type SheetDrag = {
392
+ id: number;
393
+ x: number;
394
+ y: number;
395
+ lastY: number;
396
+ anchor: number;
397
+ offset: number;
398
+ mode: 'pending' | 'drag' | 'scroll' | 'none';
399
+ scrollers: HTMLElement[];
400
+ samples: { y: number; t: number }[];
401
+ };
402
+ let sheetDrag: SheetDrag | null = null;
403
+ const isScrollableY = (el: HTMLElement) => /(auto|scroll|overlay)/.test(getComputedStyle(el).overflowY) && el.scrollHeight > el.clientHeight + 1;
404
+ const scrollersOf = (target: EventTarget | null) => {
405
+ const list: HTMLElement[] = [];
406
+ for (let el = target instanceof Element ? target : null; el && el !== box.value; el = el.parentElement) {
407
+ if (el instanceof HTMLElement && isScrollableY(el)) list.push(el);
408
+ }
409
+ return list;
410
+ };
411
+ // 留 3px 容差:惯性回弹或小数像素未完全归零时也视为顶部,被遮住的内容可忽略。
412
+ const TOP_TOLERANCE = 3;
413
+ const atTop = (d: SheetDrag) => d.scrollers.every(el => el.scrollTop < TOP_TOLERANCE);
414
+ const setSheetOffset = (offset: number | null) => {
415
+ const panel: HTMLElement | null = box.value;
416
+ const mask: HTMLElement | null = tip.value;
417
+ if (!panel || !mask) return;
418
+ if (offset === null) {
419
+ // 移除内联样式,交还 CSS 过渡:从当前位置动画到打开或关闭状态。
420
+ panel.style.removeProperty('transform');
421
+ panel.style.removeProperty('transition');
422
+ mask.style.removeProperty('background-color');
423
+ mask.style.removeProperty('transition');
424
+ return;
425
+ }
426
+ const height = panel.offsetHeight || 1;
427
+ panel.style.setProperty('transition', 'none', 'important');
428
+ panel.style.setProperty('transform', `translate3d(0, ${offset}px, 0)`, 'important');
429
+ mask.style.setProperty('transition', 'none', 'important');
430
+ mask.style.setProperty('background-color', `rgba(0, 0, 0, ${SHEET_MASK_ALPHA * Math.max(0, 1 - offset / height)})`, 'important');
431
+ };
432
+ const findTouch = (list: TouchList, id: number) => [...list].find(item => item.identifier === id);
433
+ const endSheetDrag = (dismiss: boolean) => {
434
+ sheetDrag = null;
435
+ setSheetOffset(null);
436
+ if (dismiss) dismissOutside();
437
+ };
438
+ const onSheetTouchStart = (event: TouchEvent) => {
439
+ // 拖动中有新手指落下:放弃本次拖动并回弹,不开始新的拖动;
440
+ // 单指时视为上一手势的结束事件丢失,清理后正常开始。
441
+ if (sheetDrag) {
442
+ endSheetDrag(false);
443
+ if (event.touches.length > 1) return;
444
+ }
445
+ const touch = event.touches[0];
446
+ if (!state.show || !isSheet() || event.touches.length !== 1 || !touch) return;
447
+ // 滑块等自身需要拖动的控件不参与。
448
+ if (event.target instanceof Element && event.target.closest('input[type="range"], [ui-sheet-nodrag]')) return;
449
+ sheetDrag = {
450
+ id: touch.identifier,
451
+ x: touch.clientX,
452
+ y: touch.clientY,
453
+ lastY: touch.clientY,
454
+ anchor: touch.clientY,
455
+ offset: 0,
456
+ mode: 'pending',
457
+ scrollers: scrollersOf(event.target),
458
+ samples: [{ y: touch.clientY, t: event.timeStamp }],
459
+ };
460
+ };
461
+ const onSheetTouchMove = (event: TouchEvent) => {
462
+ const d = sheetDrag;
463
+ if (!d) return;
464
+ const touch = findTouch(event.touches, d.id);
465
+ if (!touch || event.touches.length !== 1) {
466
+ // 多指介入:放弃本次拖动并回弹。
467
+ if (d.mode === 'drag') endSheetDrag(false);
468
+ sheetDrag = null;
469
+ return;
470
+ }
471
+ const y = touch.clientY;
472
+ if (d.mode === 'pending') {
473
+ const dx = touch.clientX - d.x;
474
+ const dy = y - d.y;
475
+ if (Math.abs(dx) < DRAG_SLOP && Math.abs(dy) < DRAG_SLOP) return;
476
+ if (Math.abs(dx) > Math.abs(dy)) d.mode = 'none';
477
+ else if (dy > 0 && atTop(d)) d.mode = 'drag';
478
+ else d.mode = 'scroll';
479
+ d.anchor = y;
480
+ } else if (d.mode === 'scroll' && y > d.lastY && atTop(d)) {
481
+ // 内容已滚到顶部仍在下拉:接力为拖动面板。
482
+ d.mode = 'drag';
483
+ d.anchor = y;
484
+ }
485
+ d.lastY = y;
486
+ if (d.mode !== 'drag') return;
487
+ if (event.cancelable) event.preventDefault();
488
+ const offset = y - d.anchor;
489
+ // 接管瞬间锚点即当前位置(offset 为 0),只有真正推过原位才交还。
490
+ if (offset < 0 && d.scrollers.length) {
491
+ // 拖回原位且继续上推:交还内容滚动。
492
+ d.offset = 0;
493
+ setSheetOffset(null);
494
+ d.mode = 'scroll';
495
+ return;
496
+ }
497
+ d.offset = Math.max(0, offset);
498
+ setSheetOffset(d.offset);
499
+ d.samples.push({ y, t: event.timeStamp });
500
+ while (d.samples.length > 2 && event.timeStamp - d.samples[0]!.t > VELOCITY_WINDOW) d.samples.shift();
501
+ };
502
+ const onSheetTouchEnd = (event: TouchEvent) => {
503
+ const d = sheetDrag;
504
+ if (!d || !findTouch(event.changedTouches, d.id)) return;
505
+ sheetDrag = null;
506
+ if (d.mode !== 'drag' || d.offset <= 0) {
507
+ setSheetOffset(null);
508
+ return;
509
+ }
510
+ const first = d.samples[0]!;
511
+ const last = d.samples[d.samples.length - 1]!;
512
+ const elapsed = event.timeStamp - last.t > VELOCITY_WINDOW ? Infinity : last.t - first.t;
513
+ const velocity = elapsed > 0 && elapsed !== Infinity ? (last.y - first.y) / elapsed : 0; // px/ms,向下为正
514
+ const height = box.value?.offsetHeight || 1;
515
+ // 按速度投射落点(快速下甩即关闭),明显上推时回弹。
516
+ const projected = d.offset + velocity * 200;
517
+ endSheetDrag(velocity > -0.2 && (projected > height / 2 || velocity > 0.6));
518
+ };
519
+
520
+ // 系统中断手势(来电、手势被接管等):只回弹,不做关闭判定。
521
+ const onSheetTouchCancel = (event: TouchEvent) => {
522
+ const d = sheetDrag;
523
+ if (!d || !findTouch(event.changedTouches, d.id)) return;
524
+ endSheetDrag(false);
525
+ };
526
+
527
+ // Esc 关闭最后打开的浮层;捕获阶段先于 Layer 的 Esc 处理,并以 preventDefault 阻止其关闭外层弹窗。
528
+ const onKeydown = (event: KeyboardEvent) => {
529
+ if (event.key !== 'Escape' || event.defaultPrevented || event.isComposing || !state.show) return;
530
+ if (openStack[openStack.length - 1] !== containsFollow) return;
531
+ event.preventDefault();
532
+ dismissOutside();
533
+ };
534
+
362
535
  const ensureTeleportTarget = () => {
363
536
  let area = document.querySelector('#ifollow');
364
537
  if (!area) {
@@ -370,6 +543,8 @@
370
543
  };
371
544
 
372
545
  onMounted(() => {
546
+ // 初始即显示(modelValue=1)时 show 监听不会触发,补登记打开顺序。
547
+ if (state.show && !openStack.includes(containsFollow)) openStack.push(containsFollow);
373
548
  ensureTeleportTarget();
374
549
  document.addEventListener('pointerdown', onPressStart, true);
375
550
  document.addEventListener('pointerup', onPressEnd, true);
@@ -380,6 +555,7 @@
380
555
  document.addEventListener('touchmove', onTouchMove, { passive: false });
381
556
  document.addEventListener('touchend', onTouchEnd, { passive: false });
382
557
  document.addEventListener('touchcancel', onTouchCancel);
558
+ document.addEventListener('keydown', onKeydown, true);
383
559
  });
384
560
 
385
561
  const ex: any = ref(null);
@@ -433,6 +609,9 @@
433
609
  document.removeEventListener('touchmove', onTouchMove);
434
610
  document.removeEventListener('touchend', onTouchEnd);
435
611
  document.removeEventListener('touchcancel', onTouchCancel);
612
+ document.removeEventListener('keydown', onKeydown, true);
613
+ sheetDrag = null;
614
+ removeOpen(containsFollow);
436
615
  parentChildren?.delete(containsFollow);
437
616
  releaseSheet();
438
617
  outsideTouch = null;
@@ -542,6 +721,12 @@
542
721
  [ui-tips-arrow] {
543
722
  display: none;
544
723
  }
724
+
725
+ // 内容滚到顶部后的继续下拉交给面板拖动,禁止滚动链与回弹抢手势。
726
+ &,
727
+ * {
728
+ overscroll-behavior-y: none;
729
+ }
545
730
  }
546
731
  }
547
732
 
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''" class="mc-cascader">
3
- <IFollowView :ui-tips="uiTips"
3
+ <IFollowView :ui-tips="uiTips" :closable="closable"
4
4
  ref="ifollow"
5
5
  :tipBoxStyle="`${state.flatOpts.length && state.flatOpts[0]?.length > 0 ? 'min-width: 10em;' : 'min-width: 100%'}`"
6
6
  pos="bl"
@@ -15,7 +15,7 @@
15
15
  <ul v-if="opt?.length" class="min-h-ms*s max-h-xl*s" :class="`nx-sm lh-ml ${optIndex === 0 ? '' : 'bl-xs b-back b-solid'}`" ui-scroll=":y s">
16
16
  <!-- 多选 -->
17
17
  <template v-if="multiple">
18
- <li v-for="(item, idx) of opt" :key="idx" class="ux-hover nx-sm r-ss" ui-wrap="no" @click="multiSelect(item, optIndex)">
18
+ <li v-for="(item, idx) of opt" :key="idx" class="ux-hover nx-sm mob:ny-ss r-ss" ui-wrap="no" @click="multiSelect(item, optIndex)">
19
19
  <div ui-flex="row xm">
20
20
  <div :ui-form="`${theme} type:checkbox :block`">
21
21
  <input
@@ -38,7 +38,7 @@
38
38
  v-for="(item, idx) of opt"
39
39
  :key="idx"
40
40
  :class="item.value === state.transSelected?.value ? 'co-main' : ''"
41
- class="ux-hover nx-sm r-ss" ui-wrap="no"
41
+ class="ux-hover nx-sm mob:ny-ss r-ss" ui-wrap="no"
42
42
  @click="select(item, optIndex)"
43
43
  >
44
44
  <IFollowView :ui-tips="uiTips" v-if="item.optTips" event="hover">
@@ -96,6 +96,7 @@
96
96
  const props = defineProps({
97
97
  theme: { type: String, default: '@a' },
98
98
  uiTips: { type: String, default: '@a' },
99
+ closable: { type: Boolean, default: false },
99
100
  sz: { type: String, default: 'm' },
100
101
  co: { type: String, default: '' },
101
102
  block: { type: Boolean, default: true },
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" ui-tips="@a co:well" :ui-tips-view="state.show">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type, tips, maxlength, minlength, disabled, readonly, clearable, verify, prefix, suffix, interacted }"
6
6
  :clearable="false"
@@ -47,6 +47,7 @@
47
47
  rules?: Array<object>;
48
48
  theme?: string;
49
49
  uiTips?: string;
50
+ closable?: boolean;
50
51
  sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
51
52
  co?: string;
52
53
  block?: boolean;
@@ -78,6 +79,7 @@
78
79
  rules: () => [],
79
80
  theme: '@a',
80
81
  uiTips: '@a',
82
+ closable: false,
81
83
  sz: '',
82
84
  co: '',
83
85
  block: true,
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" ui-tips="@a co:well" :ui-tips-view="state.show">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow">
4
4
  <InputRange
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type, tips, maxlength, minlength, disabled, readonly, clearable, verify, prefix, suffix, interacted }"
6
6
  :clearable="false"
@@ -40,6 +40,7 @@
40
40
  rules?: Array<object>;
41
41
  theme?: string;
42
42
  uiTips?: string;
43
+ closable?: boolean;
43
44
  sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
44
45
  co?: string;
45
46
  block?: boolean;
@@ -71,6 +72,7 @@
71
72
  rules: () => [],
72
73
  theme: '@a',
73
74
  uiTips: '@a',
75
+ closable: false,
74
76
  sz: '',
75
77
  co: '',
76
78
  block: true,
@@ -173,8 +173,8 @@
173
173
  <div>&nbsp;</div>
174
174
  </div>
175
175
  <div class="ml-ss-sub" ui-flex="row rm">
176
- <button v-if="state.panel" ui-btn="@a xs none" class="nx-ss co-risk" @click="remove">清除</button>
177
- <button v-if="state.panel && !state.isNoTime" ui-btn="@a xs none" class="nx-ss co-main" @click="update">确定</button>
176
+ <button v-if="state.panel" ui-btn="@a s none" class="nx-ss mob:nx-ms co-risk" @click="remove">清除</button>
177
+ <button v-if="state.panel && !state.isNoTime" ui-btn="@a s none" class="nx-ss mob:nx-ms co-main" @click="update">确定</button>
178
178
  </div>
179
179
  </div>
180
180
  </div>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select ${sz ? 'sz:' + sz : ''}`">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
4
4
  <slot>
5
5
  <Input
6
6
  v-bind="{ placeholder, rules, theme, sz, co, block, ready: true, type: 'text', tips, disabled, readonly, clearable: false, verify, prefix, suffix, interacted }"
@@ -22,13 +22,13 @@
22
22
  </div>
23
23
  <ul v-if="state.filter?.length" ui-scroll=":y s" class="nx-sm lh-ml" style="min-width: 100%; max-height: 15em">
24
24
  <template v-for="(item, idx) of state.filter" :key="idx">
25
- <li v-if="item.disabled" class="ux-hover nx-sm r-ss o-ms" ui-wrap="no">
25
+ <li v-if="item.disabled" class="ux-hover nx-sm mob:ny-ss r-ss o-ms" ui-wrap="no">
26
26
  <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
27
27
  <input type="checkbox" :name="state.name" :checked="state.processedValue?.some((i: any) => `${i}` === `${item.value}`)" />
28
28
  <del class="ml-sm">{{ item.label || item }}</del>
29
29
  </label>
30
30
  </li>
31
- <li v-else class="ux-hover nx-sm r-ss" ui-wrap="no" @click.stop="select(item.value)">
31
+ <li v-else class="ux-hover nx-sm mob:ny-ss r-ss" ui-wrap="no" @click.stop="select(item.value)">
32
32
  <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
33
33
  <input type="checkbox" :name="state.name" :checked="state.processedValue?.some((i: any) => `${i}` === `${item.value}`)" />
34
34
  <span class="ml-sm">{{ item.label || item }}</span>
@@ -40,7 +40,7 @@
40
40
  <ul class="nx-sm lh-sl" v-if="tools">
41
41
  <li class="my-ss"><div class="o-mm" ui-line="@a"></div></li>
42
42
  <li ui-flex="row xm">
43
- <div class="ux-hover nx-sm ny-ss r-ss co-main" ui-wrap="no" @click.stop="allselect()">
43
+ <div class="ux-hover nx-sm ny-ss mob:ny-sm r-ss co-main" ui-wrap="no" @click.stop="allselect()">
44
44
  <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
45
45
  <input type="checkbox" :name="state.name" :checked="state.all" />
46
46
  <span class="ml-sm">全选({{ state.selectedCount }}/{{ state.availableCount }})</span>
@@ -108,6 +108,7 @@
108
108
  rules?: Array<object>;
109
109
  theme?: string;
110
110
  uiTips?: string;
111
+ closable?: boolean;
111
112
  sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
112
113
  co?: string;
113
114
  block?: boolean;
@@ -137,6 +138,7 @@
137
138
  rules: () => [],
138
139
  theme: '@a',
139
140
  uiTips: '@a',
141
+ closable: false,
140
142
  sz: '',
141
143
  co: '',
142
144
  block: true,
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : 'hide'">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type: 'text', tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
6
6
  :modelValue="state.label"
@@ -103,6 +103,7 @@
103
103
  rules?: Array<object>;
104
104
  theme?: string;
105
105
  uiTips?: string;
106
+ closable?: boolean;
106
107
  sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
107
108
  co?: string;
108
109
  block?: boolean;
@@ -133,6 +134,7 @@
133
134
  rules: () => [],
134
135
  theme: '@a',
135
136
  uiTips: '@a',
137
+ closable: false,
136
138
  sz: '',
137
139
  co: '',
138
140
  block: true,
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select ${sz ? 'sz:' + sz : ''}`" :ui-form-open="state.show ? 'show' : ''" ref="dom">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
4
4
  <slot>
5
5
  <Input v-bind="$props" :clearable="false" readonly v-model="state.selectd">
6
6
  <template #suffix><i ui-form-select=""></i></template>
@@ -17,7 +17,7 @@
17
17
  </div>
18
18
  </div>
19
19
  <ul v-if="props.options?.length" ui-form-dropdown="" ui-scroll=":y s" class="nx-sm lh-ml" style="min-width: 100%; max-height: 15em">
20
- <li class="ux-hover nx-sm r-ss" ui-wrap="no" v-for="(item, idx) of props.options" :key="idx" @click.stop="select(item.value)">
20
+ <li class="ux-hover nx-sm mob:ny-ss r-ss" ui-wrap="no" v-for="(item, idx) of props.options" :key="idx" @click.stop="select(item.value)">
21
21
  <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
22
22
  <input type="checkbox" :name="formName" :checked="modelValue?.some((i: any) => `${i}` === `${item.value}`)" />
23
23
  <span class="ml-sm">{{ item.label || item }}</span>
@@ -28,7 +28,7 @@
28
28
  <ul class="nx-sm lh-ml" v-if="tools">
29
29
  <li class="my-ss"><div class="o-mm" ui-line="@a"></div></li>
30
30
  <li ui-flex="row xm">
31
- <div class="ux-hover nx-sm r-ss co-main" ui-wrap="no" @click.stop="allselect()">
31
+ <div class="ux-hover nx-sm mob:ny-ss r-ss co-main" ui-wrap="no" @click.stop="allselect()">
32
32
  <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
33
33
  <input type="checkbox" :name="formName" :checked="state.all" />
34
34
  <!-- <span class="ml-sm">全选({{ props.options?.length || 0 }})</span> -->
@@ -61,6 +61,7 @@
61
61
  const props: any = defineProps({
62
62
  theme: { type: String, default: '@a' },
63
63
  uiTips: { type: String, default: '@a' },
64
+ closable: { type: Boolean, default: false },
64
65
  sz: { type: String, default: '' },
65
66
  co: { type: String, default: '' },
66
67
  block: { type: Boolean, default: true },
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''" ref="dom">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%">
4
4
  <Input v-bind="$props" :clearable="false" readonly v-model="state.selectd.label" :interacted="interacted">
5
5
  <template #suffix><i ui-form-select=""></i></template>
6
6
  </Input>
@@ -15,7 +15,7 @@
15
15
  </div>
16
16
  </div>
17
17
  <ul v-if="props.options?.length" ui-scroll=":y s" class="nx-sm lh-ml" style="min-width: 100%; max-height: 16em">
18
- <li class="ux-hover nx-sm r-ss" ui-wrap="no" :class="item.value === state.selectd?.value ? 'co-main' : ''" v-for="(item, idx) of options" :key="idx" @click="select(item)">
18
+ <li class="ux-hover nx-sm mob:ny-ss r-ss" ui-wrap="no" :class="item.value === state.selectd?.value ? 'co-main' : ''" v-for="(item, idx) of options" :key="idx" @click="select(item)">
19
19
  {{ item.label || item }}
20
20
  </li>
21
21
  </ul>
@@ -56,6 +56,7 @@
56
56
  },
57
57
  theme: { type: String, default: '@a' },
58
58
  uiTips: { type: String, default: '@a' },
59
+ closable: { type: Boolean, default: false },
59
60
  sz: { type: String as PropType<'' | 'xs' | 's' | 'm' | 'l' | 'xl'>, default: 'm' },
60
61
  co: { type: String, default: '' },
61
62
  block: { type: Boolean, default: true },
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input v-bind="$props" :clearable="false" readonly v-model="state.selectd" @click="toggle" :interacted="interacted">
5
5
  <template #suffix><i ui-form-select=""></i></template>
6
6
  </Input>
@@ -15,7 +15,7 @@
15
15
  </div>
16
16
  </div>
17
17
  <ul v-if="props.options?.length" ui-form-dropdown="" ui-scroll=":y s" class="nx-sm lh-ml" style="min-width: 100%; max-height: 15em">
18
- <li class="ux-hover nx-sm r-ss" ui-wrap="no" v-for="(item, idx) of state.filter" :key="idx" @click.stop="select(item.value)">
18
+ <li class="ux-hover nx-sm mob:ny-ss r-ss" ui-wrap="no" v-for="(item, idx) of state.filter" :key="idx" @click.stop="select(item.value)">
19
19
  <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
20
20
  <input type="checkbox" :name="formName" :checked="modelValue?.some((i: any) => `${i}` === `${item.value}`)" />
21
21
  <span class="ml-sm">{{ item.label || item }}</span>
@@ -26,7 +26,7 @@
26
26
  <ul class="nx-sm lh-ml" v-if="tools">
27
27
  <li class="my-ss"><div class="o-mm" ui-line="@a"></div></li>
28
28
  <li ui-flex="row xm">
29
- <div class="ux-hover nx-sm r-ss co-main" ui-wrap="no" @click.stop="allselect()">
29
+ <div class="ux-hover nx-sm mob:ny-ss r-ss co-main" ui-wrap="no" @click.stop="allselect()">
30
30
  <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
31
31
  <input type="checkbox" :name="formName" :checked="state.all" />
32
32
  <span class="ml-sm">全选({{ state.filter?.length || 0 }})</span>
@@ -60,6 +60,7 @@
60
60
  },
61
61
  theme: { type: String, default: '@a' },
62
62
  uiTips: { type: String, default: '@a' },
63
+ closable: { type: Boolean, default: false },
63
64
  sz: { type: String as PropType<'' | 'xs' | 's' | 'm' | 'l' | 'xl'>, default: 'm' },
64
65
  co: { type: String, default: '' },
65
66
  block: { type: Boolean, default: true },
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type, tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
6
6
  v-model="state.selectd.label"
@@ -18,7 +18,7 @@
18
18
  </div>
19
19
  <ul v-if="props.options?.length" ui-scroll=":y s" class="nx-sm lh-sl" style="min-width: 100%; max-height: 16em">
20
20
  <li
21
- class="ux-hover nx-sm ny-ss r-ss" ui-wrap="no"
21
+ class="ux-hover nx-sm ny-ss mob:ny-sm r-ss" ui-wrap="no"
22
22
  :class="item.value === state.selectd?.value ? 'co-main' : ''"
23
23
  v-for="(item, idx) of state.filter"
24
24
  :key="idx"
@@ -101,6 +101,7 @@
101
101
  rules?: Array<object>;
102
102
  theme?: string;
103
103
  uiTips?: string;
104
+ closable?: boolean;
104
105
  sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
105
106
  co?: string;
106
107
  block?: boolean;
@@ -133,6 +134,7 @@
133
134
  rules: () => [],
134
135
  theme: '@a',
135
136
  uiTips: '@a',
137
+ closable: false,
136
138
  sz: '',
137
139
  co: '',
138
140
  block: true,
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : ''">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type, tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
6
6
  v-model="state.displayLabel"
@@ -21,10 +21,10 @@
21
21
  </div>
22
22
  <ul v-if="state.filter?.length" ui-scroll=":y s" class="nx-sm lh-sl" style="min-width: 100%; max-height: 16em">
23
23
  <template v-for="(item, idx) of state.filter" :key="idx">
24
- <li v-if="item.disabled" class="ux-hover nx-sm ny-ss r-ss o-ms" ui-wrap="no" :class="isItemSelected(item) ? 'co-main' : ''">
24
+ <li v-if="item.disabled" class="ux-hover nx-sm ny-ss mob:ny-sm r-ss o-ms" ui-wrap="no" :class="isItemSelected(item) ? 'co-main' : ''">
25
25
  <del>{{ item.label || item }}</del>
26
26
  </li>
27
- <li v-else class="ux-hover nx-sm ny-ss r-ss" ui-wrap="no" :class="isItemSelected(item) ? 'co-main' : ''" @click="select(item)">
27
+ <li v-else class="ux-hover nx-sm ny-ss mob:ny-sm r-ss" ui-wrap="no" :class="isItemSelected(item) ? 'co-main' : ''" @click="select(item)">
28
28
  <span>{{ item.label || item }}</span>
29
29
  </li>
30
30
  </template>
@@ -132,6 +132,7 @@
132
132
  rules?: Array<object>;
133
133
  theme?: string;
134
134
  uiTips?: string;
135
+ closable?: boolean;
135
136
  sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
136
137
  co?: string;
137
138
  block?: boolean;
@@ -167,6 +168,7 @@
167
168
  rules: () => [],
168
169
  theme: '@a',
169
170
  uiTips: '@a',
171
+ closable: false,
170
172
  sz: 'm',
171
173
  co: '',
172
174
  block: true,
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : 'hide'">
3
- <IFollowView :ui-tips="uiTips" ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
3
+ <IFollowView :ui-tips="uiTips" :closable="closable" ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-ss" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
4
4
  <Input
5
5
  v-bind="{ placeholder, rules, theme, sz, co, block, ready, type, tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
6
6
  v-model="state.label"
@@ -97,6 +97,7 @@
97
97
  rules?: Array<object>;
98
98
  theme?: string;
99
99
  uiTips?: string;
100
+ closable?: boolean;
100
101
  sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
101
102
  co?: string;
102
103
  block?: boolean;
@@ -132,6 +133,7 @@
132
133
  rules: () => [],
133
134
  theme: '@a',
134
135
  uiTips: '@a',
136
+ closable: false,
135
137
  sz: '',
136
138
  co: '',
137
139
  block: true,
@@ -1,190 +0,0 @@
1
- <template>
2
- <div :ui-form="`${theme} type:select ${sz ? 'sz:' + sz : ''}`">
3
- <IPickerView ref="ipicker" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%">
4
- <slot>
5
- <Input
6
- v-bind="{ placeholder, rules, theme, sz, co, block, ready: true, type: 'text', tips, disabled, readonly, clearable: false, verify, prefix, suffix, interacted }"
7
- :clearable="false"
8
- readonly
9
- v-model="state.value"
10
- :interacted="interacted"
11
- >
12
- <template #suffix><i ui-form-select=""></i></template>
13
- </Input>
14
- </slot>
15
- <template #options>
16
- <ul v-if="props.options?.length" ui-scroll=":y s" class="nx-sm lh-ml" style="min-width: 100%; max-height: 15em">
17
- <template v-for="(item, idx) of props.options" :key="idx">
18
- <li v-if="item.disable" class="ux-hover nx-sm r-ss o-ms">
19
- <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
20
- <input type="checkbox" :name="state.name" :checked="modelValue?.some((i: any) => `${i}` === `${item.value}`)" />
21
- <del class="ml-sm">{{ item.label || item }}</del>
22
- </label>
23
- </li>
24
- <li v-else class="ux-hover nx-sm r-ss" @click.stop="select(item.value)">
25
- <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
26
- <input type="checkbox" :name="state.name" :checked="modelValue?.some((i: any) => `${i}` === `${item.value}`)" />
27
- <span class="ml-sm">{{ item.label || item }}</span>
28
- </label>
29
- </li>
30
- </template>
31
- </ul>
32
- <div v-else class="w-full ny-ms" ui-flex="col cm" v-html="spare"></div>
33
- <ul class="nx-sm lh-ml" v-if="tools">
34
- <li class="my-ss"><div class="o-mm" ui-line="@a"></div></li>
35
- <li ui-flex="row xm">
36
- <div class="ux-hover nx-sm r-ss co-main" ui-wrap="no" @click.stop="allselect()">
37
- <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
38
- <input type="checkbox" :name="state.name" :checked="state.all" />
39
- <span class="ml-sm">全选({{ props.options?.length || 0 }})</span>
40
- </label>
41
- </div>
42
- <div>
43
- <button v-show="state.has" class="co-risk" ui-btn="@a none s" @click="remove">清空</button>
44
- </div>
45
- </li>
46
- </ul>
47
- </template>
48
- </IPickerView>
49
- </div>
50
- </template>
51
-
52
- <script setup lang="ts">
53
- import { reactive, ref, computed, watch } from 'vue';
54
- import Input from './Input.vue';
55
- import { IFollowView, IPickerView } from '../basic';
56
- const ipicker: any = ref(null);
57
-
58
- const emits = defineEmits(['update:modelValue']);
59
-
60
- /**
61
- * 入参说明
62
- *
63
- * modelValue 值
64
- * options 枚举列表
65
- * tools 是否显示工具栏
66
- * spare 空闲兜底内容
67
- * search 是否显示搜索框
68
- *
69
- * -----------------------------------------
70
- * theme 主题
71
- * sz 尺寸
72
- * co 颜色
73
- * block 撑满
74
- * ready 准备就绪
75
- * type 类型
76
- * tips 提示文案
77
- * placeholder 内容占位
78
- * maxlength 最大长度
79
- * minlength 最小长度
80
- * disabled 是否禁用
81
- * readonly 是否只读
82
- * clearable 是否显示清除按钮
83
- * rules 校验规则
84
- * verify 是否是示校验图标
85
- * prefix 前缀内容
86
- * suffix 后缀内容
87
- * interacted 用户是否交互
88
- */
89
-
90
- // 类型
91
- interface Props {
92
- modelValue?: Array<string | number>;
93
- options: Array<object>;
94
- tools?: boolean;
95
- spare?: string;
96
-
97
- // Input
98
- placeholder?: string;
99
- rules?: Array<object>;
100
- theme?: string;
101
- sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
102
- co?: string;
103
- block?: boolean;
104
- ready?: boolean;
105
- type?: string;
106
- tips?: string;
107
- maxlength?: null | number;
108
- minlength?: null | number;
109
- disabled?: boolean;
110
- readonly?: boolean;
111
- clearable?: boolean;
112
- verify?: boolean;
113
- prefix?: Array<any>;
114
- suffix?: Array<any>;
115
- interacted?: boolean;
116
- }
117
- // 入参
118
- const props: any = withDefaults(defineProps<Props>(), {
119
- modelValue: () => [],
120
- options: () => [],
121
- tools: true,
122
- spare: '当前没有可选项',
123
-
124
- // Input
125
- placeholder: '',
126
- rules: () => [],
127
- theme: '@a',
128
- sz: '',
129
- co: '',
130
- block: true,
131
- ready: false,
132
- type: 'text',
133
- tips: '',
134
- maxlength: null,
135
- minlength: null,
136
- disabled: false,
137
- readonly: false,
138
- clearable: true,
139
- verify: false,
140
- prefix: () => [],
141
- suffix: () => [],
142
- interacted: false,
143
- });
144
- // 内部数据
145
- const state: any = reactive({
146
- name: computed(() => {
147
- return Array.from({ length: 8 }, () => String.fromCharCode(97 + Math.floor(Math.random() * 26))).join('');
148
- }),
149
- show: 0,
150
- value: '',
151
- all: computed(() => {
152
- return props.options?.every((item: any) => props.modelValue?.some((i: any) => `${i}` === `${item.value}`));
153
- }),
154
- has: computed(() => {
155
- return props.options?.some((item: any) => props.modelValue?.some((i: any) => `${i}` === `${item.value}`));
156
- }),
157
- });
158
-
159
- // 监听外部数据
160
- watch(
161
- [() => props.modelValue, () => props.options],
162
- ([v = [], o = []]: any = []) => {
163
- const selectd: any = o?.filter((item: any) => v?.some((i: any) => `${i}` === `${item.value}`))?.map((item: any) => item.label) || [];
164
- state.value = selectd.join(' Ι ') || '';
165
- },
166
- { deep: true, immediate: true },
167
- );
168
-
169
- // 更新
170
- const change = (item: any) => {
171
- emits('update:modelValue', item);
172
- };
173
- // 选中
174
- const select = (item: any) => {
175
- const s = [...props.modelValue];
176
- s?.indexOf(item) !== -1 ? s?.splice(s?.indexOf(item), 1) : s?.push(item);
177
- change(s);
178
- };
179
- // 全选
180
- const allselect = () => {
181
- const s = state.all ? [] : props.options?.map((item: any) => item.value);
182
- change(s);
183
- };
184
- // 清除
185
- const remove = () => {
186
- change([]);
187
- ipicker.value.cancel();
188
- };
189
- </script>
190
- . .