@neatui/vue 5.2.0 → 5.3.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 +13 -0
- package/package.json +2 -2
- package/src/components/basic/IFollowView.vue +131 -11
- package/src/components/form/Cascader.vue +1 -1
- package/src/components/form/MoreSelect.vue +1 -1
- package/src/components/form/MoreSelectTree.vue +1 -1
- package/src/components/form/PageMoreSelect.vue +1 -1
- package/src/components/form/PageSelect.vue +1 -1
- package/src/components/form/SearchMoreSelect.vue +1 -1
- package/src/components/form/SearchSelect.vue +1 -1
- package/src/components/form/Select.vue +1 -1
- package/src/components/form/SelectTree/SelectTree.vue +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @neatui/vue 更新记录
|
|
2
2
|
|
|
3
|
+
## 5.2.1
|
|
4
|
+
|
|
5
|
+
### IFollowView(移动端底部面板)
|
|
6
|
+
- 面板改为从底部滑入滑出,暗层同步淡入淡出,使用 `cubic-bezier(0.32, 0.72, 0, 1)` 减速曲线;支持“减少动态效果”。
|
|
7
|
+
- 面板通过 Popover API 进入浏览器 top layer,不再依赖 z-index;不支持时回退为普通 fixed。
|
|
8
|
+
- 插槽内容撑满面板宽度,覆盖桌面端写死的宽度。
|
|
9
|
+
- 面板贴底,Home 指示条安全区以面板底色留白(需页面 viewport 设置 `viewport-fit=cover`)。
|
|
10
|
+
- 顶部圆角改为 `0.75em`;上方间距统一由标题栏提供;关闭按钮改用 CSS 图标 `ui-icon="close"`。
|
|
11
|
+
- 移动端样式只作用于本层面板盒子,不再影响插槽内其他 `ui-tips` 结构。
|
|
12
|
+
|
|
13
|
+
### 依赖
|
|
14
|
+
- CSS 依赖更新为 `@neatui/css@^5.2.1`。
|
|
15
|
+
|
|
3
16
|
## 5.2.0
|
|
4
17
|
|
|
5
18
|
### Layer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neatui/vue",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.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.
|
|
39
|
+
"@neatui/css": "^5.3.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",
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
@click="clickhide"
|
|
18
18
|
>
|
|
19
19
|
<div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="tipBoxStyle">
|
|
20
|
-
<div ui-hide=">mob" class="mob-follow-tips w-full
|
|
20
|
+
<div ui-hide=">mob" class="mob-follow-tips w-full nx-sl nt-sl nb-ss" ui-flex="row xm">
|
|
21
21
|
<div class="w-xs*s"></div>
|
|
22
22
|
<div>
|
|
23
23
|
<h5 class="bold">{{ title }}</h5>
|
|
24
24
|
</div>
|
|
25
25
|
<div class="w-xs*s" ui-flex="row rm">
|
|
26
26
|
<button ui-btn="@a none s :square" @click="toggle">
|
|
27
|
-
<i
|
|
27
|
+
<i ui-icon="close"></i>
|
|
28
28
|
</button>
|
|
29
29
|
</div>
|
|
30
30
|
</div>
|
|
@@ -67,6 +67,8 @@
|
|
|
67
67
|
mobile?: boolean;
|
|
68
68
|
title?: string;
|
|
69
69
|
zIndex?: string | number | null;
|
|
70
|
+
/** 桌面端是否放入浏览器 top layer(Popover API),盖住整页任意层级;移动端底部面板始终放入。 */
|
|
71
|
+
topLayer?: boolean;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
// 入参
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
mobile: true,
|
|
88
90
|
title: '',
|
|
89
91
|
zIndex: undefined,
|
|
92
|
+
topLayer: true,
|
|
90
93
|
});
|
|
91
94
|
|
|
92
95
|
const dom: any = ref(null);
|
|
@@ -168,6 +171,73 @@
|
|
|
168
171
|
},
|
|
169
172
|
);
|
|
170
173
|
|
|
174
|
+
// 弹层放入浏览器 top layer(Popover API),无需比较 z-index 即可盖住整页:
|
|
175
|
+
// 移动端底部面板始终放入;桌面端由 topLayer 控制(默认开启),关闭时按 Follow 的祖先层级定位。
|
|
176
|
+
// 不支持的浏览器退回普通 fixed。
|
|
177
|
+
const SHEET_LEAVE_MS = 500;
|
|
178
|
+
let sheetTimer: ReturnType<typeof setTimeout> | undefined;
|
|
179
|
+
const supportsPopover = () => typeof HTMLElement !== 'undefined' && 'showPopover' in HTMLElement.prototype;
|
|
180
|
+
const isSheet = () => props.mobile && window.matchMedia('(max-width: 799px)').matches;
|
|
181
|
+
const useTopLayer = () => props.topLayer || isSheet();
|
|
182
|
+
const releaseSheet = () => {
|
|
183
|
+
clearTimeout(sheetTimer);
|
|
184
|
+
const el = tip.value;
|
|
185
|
+
if (!el?.hasAttribute('popover')) return;
|
|
186
|
+
try {
|
|
187
|
+
if (el.matches(':popover-open')) el.hidePopover();
|
|
188
|
+
} catch {}
|
|
189
|
+
el.removeAttribute('popover');
|
|
190
|
+
};
|
|
191
|
+
const openSheet = () => {
|
|
192
|
+
const el = tip.value;
|
|
193
|
+
if (!el || !supportsPopover()) return;
|
|
194
|
+
clearTimeout(sheetTimer);
|
|
195
|
+
// 不需要 top layer 时主动清理(如关闭动画期间禁用 topLayer 后快速重开)。
|
|
196
|
+
if (!useTopLayer()) return releaseSheet();
|
|
197
|
+
el.setAttribute('popover', 'manual');
|
|
198
|
+
try {
|
|
199
|
+
// 已打开时先关再开,使其移到 top layer 最上方。
|
|
200
|
+
if (el.matches(':popover-open')) el.hidePopover();
|
|
201
|
+
el.showPopover();
|
|
202
|
+
} catch {
|
|
203
|
+
el.removeAttribute('popover');
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
// 打开状态下跨越断点(如桌面端拖窄窗口)时,同步进入/离开 top layer。
|
|
207
|
+
let sheetMql: MediaQueryList | null = null;
|
|
208
|
+
const onSheetMediaChange = () => {
|
|
209
|
+
if (!state.show) return;
|
|
210
|
+
if (!useTopLayer()) return releaseSheet();
|
|
211
|
+
// 已在 top layer 时保持原顺序,避免父面板被重排到子面板之上。
|
|
212
|
+
if (!tip.value?.matches(':popover-open')) openSheet();
|
|
213
|
+
};
|
|
214
|
+
onMounted(() => {
|
|
215
|
+
sheetMql = window.matchMedia('(max-width: 799px)');
|
|
216
|
+
sheetMql.addEventListener('change', onSheetMediaChange);
|
|
217
|
+
});
|
|
218
|
+
onUnmounted(() => {
|
|
219
|
+
sheetMql?.removeEventListener('change', onSheetMediaChange);
|
|
220
|
+
});
|
|
221
|
+
// 展开期间动态切换 topLayer 时同步。
|
|
222
|
+
watch(() => props.topLayer, onSheetMediaChange);
|
|
223
|
+
watch(
|
|
224
|
+
() => state.show,
|
|
225
|
+
(show) => {
|
|
226
|
+
const el = tip.value;
|
|
227
|
+
if (!el || !supportsPopover()) return;
|
|
228
|
+
clearTimeout(sheetTimer);
|
|
229
|
+
if (show) {
|
|
230
|
+
openSheet();
|
|
231
|
+
} else if (el.hasAttribute('popover')) {
|
|
232
|
+
// 等滑出动画结束再离开 top layer。
|
|
233
|
+
sheetTimer = setTimeout(() => {
|
|
234
|
+
if (!state.show) releaseSheet();
|
|
235
|
+
}, SHEET_LEAVE_MS);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
{ flush: 'post' },
|
|
239
|
+
);
|
|
240
|
+
|
|
171
241
|
const hovershow = () => {
|
|
172
242
|
if (props.event === 'hover') {
|
|
173
243
|
state.hover = 1;
|
|
@@ -337,8 +407,11 @@
|
|
|
337
407
|
},
|
|
338
408
|
{ hideOnScrollOut: false, zIndex: props.zIndex },
|
|
339
409
|
);
|
|
340
|
-
// 初始即显示(如 modelValue=1)时 state.show
|
|
341
|
-
if (state.show)
|
|
410
|
+
// 初始即显示(如 modelValue=1)时 state.show 不会再变化,需要立即启动定位并同步 top layer。
|
|
411
|
+
if (state.show) {
|
|
412
|
+
ex.value.listen();
|
|
413
|
+
openSheet();
|
|
414
|
+
}
|
|
342
415
|
} else {
|
|
343
416
|
ex.value = null;
|
|
344
417
|
}
|
|
@@ -361,6 +434,7 @@
|
|
|
361
434
|
document.removeEventListener('touchend', onTouchEnd);
|
|
362
435
|
document.removeEventListener('touchcancel', onTouchCancel);
|
|
363
436
|
parentChildren?.delete(containsFollow);
|
|
437
|
+
releaseSheet();
|
|
364
438
|
outsideTouch = null;
|
|
365
439
|
state.show = 0;
|
|
366
440
|
|
|
@@ -391,6 +465,20 @@
|
|
|
391
465
|
</script>
|
|
392
466
|
<style lang="scss">
|
|
393
467
|
@use '@neatui/css/query' as neatui;
|
|
468
|
+
// 抵消 [popover] 的 UA 默认样式(桌面与移动端共用);放在媒体查询外,跨断点时不会走样。
|
|
469
|
+
[ui-tips-view][popover] {
|
|
470
|
+
inset: auto;
|
|
471
|
+
width: auto;
|
|
472
|
+
height: auto;
|
|
473
|
+
margin: 0;
|
|
474
|
+
padding: 0;
|
|
475
|
+
border: 0;
|
|
476
|
+
overflow: visible;
|
|
477
|
+
color: inherit;
|
|
478
|
+
background: transparent;
|
|
479
|
+
max-width: none;
|
|
480
|
+
max-height: none;
|
|
481
|
+
}
|
|
394
482
|
@include neatui.mob {
|
|
395
483
|
.fekit-follow-mobile {
|
|
396
484
|
position: fixed !important;
|
|
@@ -404,26 +492,52 @@
|
|
|
404
492
|
align-content: center !important;
|
|
405
493
|
inset: 0 0 0 0 !important;
|
|
406
494
|
|
|
495
|
+
// 底部面板:暗层淡入淡出,面板从底部滑入滑出(iOS 风格减速曲线)。
|
|
496
|
+
--follow-sheet-ease: cubic-bezier(0.32, 0.72, 0, 1);
|
|
497
|
+
background-color: rgba(0, 0, 0, 0);
|
|
498
|
+
transition: background-color 0.4s var(--follow-sheet-ease);
|
|
499
|
+
|
|
407
500
|
&[ui-tips-view='1'] {
|
|
408
501
|
background-color: rgba(0, 0, 0, 0.5);
|
|
409
502
|
pointer-events: auto !important;
|
|
410
503
|
}
|
|
411
504
|
|
|
412
|
-
&[ui-tips] [ui-tips-box] {
|
|
505
|
+
&[ui-tips][ui-tips-view='1'] > [ui-tips-box] {
|
|
506
|
+
transform: translate3d(0, 0, 0) !important;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
&[ui-tips] > [ui-tips-box] {
|
|
413
510
|
position: relative !important;
|
|
414
511
|
|
|
415
512
|
width: 100% !important;
|
|
416
|
-
bottom:
|
|
513
|
+
bottom: 0 !important;
|
|
514
|
+
// iPhone Home 指示条安全区:面板背景铺到屏幕底边,内容让出安全区。
|
|
515
|
+
// 用透明下边框而非 padding,避免覆盖各组件 tipBoxClass 的内边距(背景默认绘制到边框下)。
|
|
516
|
+
border-bottom: env(safe-area-inset-bottom, 0px) solid transparent !important;
|
|
417
517
|
right: unset !important;
|
|
418
518
|
top: unset !important;
|
|
419
519
|
left: unset !important;
|
|
420
|
-
|
|
520
|
+
border-radius: 0.75em 0.75em 0 0 !important;
|
|
521
|
+
// 上方间距统一由标题栏提供,忽略各组件 tipBoxClass 的上内边距(如 ny-sm)。
|
|
522
|
+
padding-top: 0 !important;
|
|
523
|
+
// 全程不透明,只做位移。
|
|
524
|
+
opacity: 1 !important;
|
|
525
|
+
transform: translate3d(0, 100%, 0) !important;
|
|
526
|
+
transition: transform 0.45s var(--follow-sheet-ease) !important;
|
|
527
|
+
will-change: transform;
|
|
421
528
|
|
|
422
529
|
display: flex !important;
|
|
423
530
|
flex-direction: column !important;
|
|
424
531
|
justify-content: flex-end !important;
|
|
425
|
-
align-items:
|
|
426
|
-
align-content:
|
|
532
|
+
align-items: stretch !important;
|
|
533
|
+
align-content: stretch !important;
|
|
534
|
+
|
|
535
|
+
// 插槽内容撑满面板宽度,覆盖桌面端写死的宽度(如 w-ml*s)。
|
|
536
|
+
> * {
|
|
537
|
+
width: 100% !important;
|
|
538
|
+
max-width: 100%;
|
|
539
|
+
box-sizing: border-box;
|
|
540
|
+
}
|
|
427
541
|
|
|
428
542
|
[ui-tips-arrow] {
|
|
429
543
|
display: none;
|
|
@@ -431,9 +545,15 @@
|
|
|
431
545
|
}
|
|
432
546
|
}
|
|
433
547
|
|
|
548
|
+
@media (prefers-reduced-motion: reduce) {
|
|
549
|
+
.fekit-follow-mobile,
|
|
550
|
+
.fekit-follow-mobile[ui-tips] > [ui-tips-box] {
|
|
551
|
+
transition-duration: 0.01s !important;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
434
555
|
.mob-follow-tips {
|
|
435
556
|
position: relative;
|
|
436
|
-
padding-bottom: env(safe-area-inset-bottom, 16px);
|
|
437
557
|
&::before {
|
|
438
558
|
position: absolute;
|
|
439
559
|
content: '';
|
|
@@ -442,7 +562,7 @@
|
|
|
442
562
|
height: 3px;
|
|
443
563
|
border-radius: 1em;
|
|
444
564
|
background-color: var(--co-text-o-ss);
|
|
445
|
-
top:
|
|
565
|
+
top: 6px;
|
|
446
566
|
left: 50%;
|
|
447
567
|
transform: translateX(-50%);
|
|
448
568
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
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"
|
|
7
|
-
tipBoxClass="ny-sm nx-
|
|
7
|
+
tipBoxClass="ny-sm nx-ss"
|
|
8
8
|
>
|
|
9
9
|
<Input v-model="state.selectd.label" :clearable="false" :disabled="disabled" readonly v-bind="bindProps">
|
|
10
10
|
<template #suffix><i ui-form-select=""></i></template>
|
|
@@ -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-
|
|
3
|
+
<IFollowView :ui-tips="uiTips" 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 }"
|
|
@@ -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-
|
|
3
|
+
<IFollowView :ui-tips="uiTips" 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"
|
|
@@ -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-
|
|
3
|
+
<IFollowView :ui-tips="uiTips" 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>
|
|
@@ -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-
|
|
3
|
+
<IFollowView :ui-tips="uiTips" 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>
|
|
@@ -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-
|
|
3
|
+
<IFollowView :ui-tips="uiTips" 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>
|
|
@@ -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-
|
|
3
|
+
<IFollowView :ui-tips="uiTips" 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"
|
|
@@ -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-
|
|
3
|
+
<IFollowView :ui-tips="uiTips" 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"
|
|
@@ -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-
|
|
3
|
+
<IFollowView :ui-tips="uiTips" 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"
|