@neatui/vue 4.1.0 → 4.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neatui/vue",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "basic component ui",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@fekit/scrollto": "^3.0.3",
|
|
35
35
|
"@fekit/toast": "^2.7.2",
|
|
36
36
|
"@fekit/utils": "^6.0.0",
|
|
37
|
-
"@neatui/css": "^4.
|
|
37
|
+
"@neatui/css": "^4.2.0",
|
|
38
38
|
"@rollup/plugin-commonjs": "^26.0.1",
|
|
39
39
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
40
40
|
"@vue/compiler-sfc": "^3.4.30",
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const SCROLL_LOCK_STATE = Symbol.for('@neatui/vue.Layer.scrollLock');
|
|
61
|
+
const ESCAPE_STACK = Symbol.for('@neatui/vue.Layer.escapeStack');
|
|
62
|
+
const ESCAPE_LISTENER = Symbol.for('@neatui/vue.Layer.escapeListener');
|
|
61
63
|
|
|
62
64
|
const getScrollLockState = (): ScrollLockState => {
|
|
63
65
|
if (!_tasks[SCROLL_LOCK_STATE]) {
|
|
@@ -71,6 +73,51 @@
|
|
|
71
73
|
return _tasks[SCROLL_LOCK_STATE];
|
|
72
74
|
};
|
|
73
75
|
|
|
76
|
+
const getEscapeStack = (): string[] => {
|
|
77
|
+
if (!_tasks[ESCAPE_STACK]) {
|
|
78
|
+
Object.defineProperty(_tasks, ESCAPE_STACK, {
|
|
79
|
+
configurable: true,
|
|
80
|
+
value: [] as string[],
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return _tasks[ESCAPE_STACK];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const pushEscapeStack = (layerId: string) => {
|
|
87
|
+
if (!layerId) return;
|
|
88
|
+
const stack = getEscapeStack();
|
|
89
|
+
const index = stack.indexOf(layerId);
|
|
90
|
+
if (index !== -1) stack.splice(index, 1);
|
|
91
|
+
stack.push(layerId);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const removeEscapeStack = (layerId: string) => {
|
|
95
|
+
if (!layerId) return;
|
|
96
|
+
const stack = getEscapeStack();
|
|
97
|
+
const index = stack.indexOf(layerId);
|
|
98
|
+
if (index !== -1) stack.splice(index, 1);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// 全局只挂一次:ESC 仅关闭最后打开且已完全显示的一层
|
|
102
|
+
const ensureEscapeListener = () => {
|
|
103
|
+
if (!env.client || _tasks[ESCAPE_LISTENER]) return;
|
|
104
|
+
const onKeydown = (ev: KeyboardEvent) => {
|
|
105
|
+
if (ev.key !== 'Escape' || ev.defaultPrevented) return;
|
|
106
|
+
const stack = getEscapeStack();
|
|
107
|
+
const topId = stack[stack.length - 1];
|
|
108
|
+
if (!topId) return;
|
|
109
|
+
const layer = _tasks[topId];
|
|
110
|
+
if (!layer || layer.view !== 3 || layer.escClose === false) return;
|
|
111
|
+
layer.hide();
|
|
112
|
+
ev.preventDefault();
|
|
113
|
+
};
|
|
114
|
+
document.addEventListener('keydown', onKeydown);
|
|
115
|
+
Object.defineProperty(_tasks, ESCAPE_LISTENER, {
|
|
116
|
+
configurable: true,
|
|
117
|
+
value: onKeydown,
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
74
121
|
const hasInlineProperty = (style: CSSStyleDeclaration, property: string) => {
|
|
75
122
|
for (let index = 0; index < style.length; index += 1) {
|
|
76
123
|
if (style.item(index) === property) return true;
|
|
@@ -230,6 +277,7 @@
|
|
|
230
277
|
flex?: string;
|
|
231
278
|
dark?: string | number;
|
|
232
279
|
darkClickClose?: boolean;
|
|
280
|
+
escClose?: boolean;
|
|
233
281
|
wrapStyle?: string;
|
|
234
282
|
}
|
|
235
283
|
|
|
@@ -242,6 +290,7 @@
|
|
|
242
290
|
flex: 'cm',
|
|
243
291
|
dark: '',
|
|
244
292
|
darkClickClose: true,
|
|
293
|
+
escClose: true,
|
|
245
294
|
wrapStyle: '',
|
|
246
295
|
});
|
|
247
296
|
|
|
@@ -268,12 +317,22 @@
|
|
|
268
317
|
if (_tasks[props.id]) {
|
|
269
318
|
_tasks[props.id].view = view;
|
|
270
319
|
}
|
|
320
|
+
if (view === 2 || view === 3) pushEscapeStack(props.id);
|
|
321
|
+
else removeEscapeStack(props.id);
|
|
271
322
|
if (view > 0) acquireScrollLock(scrollLockOwner);
|
|
272
323
|
else releaseScrollLock(scrollLockOwner);
|
|
273
324
|
},
|
|
274
325
|
{ deep: true, immediate: true },
|
|
275
326
|
);
|
|
276
327
|
|
|
328
|
+
watch(
|
|
329
|
+
() => props.escClose,
|
|
330
|
+
(escClose) => {
|
|
331
|
+
if (_tasks[props.id]) _tasks[props.id].escClose = escClose;
|
|
332
|
+
},
|
|
333
|
+
{ immediate: true },
|
|
334
|
+
);
|
|
335
|
+
|
|
277
336
|
// 回调合集
|
|
278
337
|
const on: any = {
|
|
279
338
|
show: [],
|
|
@@ -367,13 +426,15 @@
|
|
|
367
426
|
console.error('Layer 有重复 id', id);
|
|
368
427
|
}
|
|
369
428
|
if (id) {
|
|
370
|
-
_tasks[id] = { init, show, hide, open, none };
|
|
429
|
+
_tasks[id] = { init, show, hide, open, none, view: state.view, escClose: props.escClose };
|
|
430
|
+
ensureEscapeListener();
|
|
371
431
|
} else {
|
|
372
432
|
console.error('Layer 必须填写全局统一的 id');
|
|
373
433
|
}
|
|
374
434
|
|
|
375
435
|
// 卸载弹层
|
|
376
436
|
onBeforeUnmount(() => {
|
|
437
|
+
removeEscapeStack(id);
|
|
377
438
|
releaseScrollLock(scrollLockOwner);
|
|
378
439
|
delete _tasks[id];
|
|
379
440
|
});
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
3
|
<div :class="`mc-img ${imgClass}`" :style="imgStyle">
|
|
4
|
-
<img
|
|
4
|
+
<img
|
|
5
|
+
ref="imgRef"
|
|
6
|
+
:src="displaySrc"
|
|
7
|
+
:alt="alt"
|
|
8
|
+
:loading="lazy ? 'lazy' : 'eager'"
|
|
9
|
+
:decoding="decoding"
|
|
10
|
+
:class="{ 'is-loaded': isLoaded }"
|
|
11
|
+
:style="`object-fit: ${fit}`"
|
|
12
|
+
@click.stop="showPreview"
|
|
13
|
+
/>
|
|
5
14
|
<div v-if="tools" class="mc-img-tools">
|
|
6
15
|
<slot name="tools">
|
|
7
16
|
<div class="mc-img-clean co-risk" @click="delImg"></div>
|
|
@@ -14,6 +23,7 @@
|
|
|
14
23
|
</div>
|
|
15
24
|
</template>
|
|
16
25
|
<script setup lang="ts">
|
|
26
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
17
27
|
import { Layer, LayerById } from '../basic';
|
|
18
28
|
import PhotoViewer from './PhotoViewer.vue';
|
|
19
29
|
|
|
@@ -46,10 +56,108 @@
|
|
|
46
56
|
type: Number,
|
|
47
57
|
default: 0,
|
|
48
58
|
},
|
|
59
|
+
// 是否开启懒加载
|
|
60
|
+
lazy: {
|
|
61
|
+
type: Boolean,
|
|
62
|
+
default: true,
|
|
63
|
+
},
|
|
64
|
+
// 懒加载占位图
|
|
65
|
+
placeholder: {
|
|
66
|
+
type: String,
|
|
67
|
+
default: '/assets/img/none.webp',
|
|
68
|
+
},
|
|
69
|
+
alt: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: '',
|
|
72
|
+
},
|
|
73
|
+
decoding: {
|
|
74
|
+
type: String,
|
|
75
|
+
default: 'async',
|
|
76
|
+
},
|
|
77
|
+
// 提前加载边距,接近视口即触发(同 IntersectionObserver.rootMargin)
|
|
78
|
+
rootMargin: {
|
|
79
|
+
type: String,
|
|
80
|
+
default: '200px',
|
|
81
|
+
},
|
|
49
82
|
});
|
|
50
83
|
|
|
51
84
|
const emit = defineEmits(['delImg']);
|
|
52
85
|
const layerId = Math.random().toString(36).slice(-6);
|
|
86
|
+
const imgRef = ref<HTMLImageElement | null>(null);
|
|
87
|
+
const isLoaded = ref(!props.lazy);
|
|
88
|
+
let observer: IntersectionObserver | null = null;
|
|
89
|
+
|
|
90
|
+
const realSrc = computed(() => props.src?.[props.srcName] || '');
|
|
91
|
+
const displaySrc = computed(() => {
|
|
92
|
+
if (!props.lazy || isLoaded.value) {
|
|
93
|
+
return realSrc.value || props.placeholder;
|
|
94
|
+
}
|
|
95
|
+
return props.placeholder;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const stopObserver = () => {
|
|
99
|
+
if (observer) {
|
|
100
|
+
observer.disconnect();
|
|
101
|
+
observer = null;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const startObserver = () => {
|
|
106
|
+
stopObserver();
|
|
107
|
+
if (!props.lazy || isLoaded.value) return;
|
|
108
|
+
const el = imgRef.value;
|
|
109
|
+
if (!el) {
|
|
110
|
+
isLoaded.value = true;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
114
|
+
isLoaded.value = true;
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
observer = new IntersectionObserver(
|
|
118
|
+
(entries) => {
|
|
119
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
120
|
+
isLoaded.value = true;
|
|
121
|
+
stopObserver();
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{ rootMargin: props.rootMargin },
|
|
125
|
+
);
|
|
126
|
+
observer.observe(el);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
onMounted(() => {
|
|
130
|
+
if (!props.lazy) {
|
|
131
|
+
isLoaded.value = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
startObserver();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
watch(
|
|
138
|
+
() => props.lazy,
|
|
139
|
+
(lazy) => {
|
|
140
|
+
if (!lazy) {
|
|
141
|
+
isLoaded.value = true;
|
|
142
|
+
stopObserver();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
isLoaded.value = false;
|
|
146
|
+
startObserver();
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
watch(
|
|
151
|
+
() => props.rootMargin,
|
|
152
|
+
() => {
|
|
153
|
+
if (!props.lazy || isLoaded.value) return;
|
|
154
|
+
startObserver();
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
onBeforeUnmount(() => {
|
|
159
|
+
stopObserver();
|
|
160
|
+
});
|
|
53
161
|
|
|
54
162
|
//预览
|
|
55
163
|
const showPreview = () => {
|
|
@@ -77,6 +185,12 @@
|
|
|
77
185
|
height: 100%;
|
|
78
186
|
transform: translate3d(0, 0, 0);
|
|
79
187
|
border-radius: 0.15em;
|
|
188
|
+
opacity: 0.55;
|
|
189
|
+
transition: opacity 0.25s ease;
|
|
190
|
+
|
|
191
|
+
&.is-loaded {
|
|
192
|
+
opacity: 1;
|
|
193
|
+
}
|
|
80
194
|
}
|
|
81
195
|
|
|
82
196
|
&-tools {
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
/>
|
|
29
29
|
</slot>
|
|
30
30
|
<div ui-form-suffix="">
|
|
31
|
-
<i
|
|
31
|
+
<i
|
|
32
|
+
v-if="clearable && !readonly && !preview && !disabled"
|
|
33
|
+
:ui-form-clean="hasClearValue ? '' : 'empty'"
|
|
34
|
+
@click.stop="hasClearValue && clear()"
|
|
35
|
+
></i>
|
|
32
36
|
<div
|
|
33
37
|
class="o-ls"
|
|
34
38
|
v-if="type === 'password' && cipher && !preview"
|
|
@@ -127,6 +131,8 @@
|
|
|
127
131
|
interacted: null,
|
|
128
132
|
});
|
|
129
133
|
|
|
134
|
+
const hasClearValue = computed(() => props.modelValue !== '' && props.modelValue !== null && props.modelValue !== undefined);
|
|
135
|
+
|
|
130
136
|
const state: any = reactive({
|
|
131
137
|
// 用户是否交互
|
|
132
138
|
interacted: null,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :ui-row="rows" class="
|
|
2
|
+
<div :ui-row="rows" :class="rootClass">
|
|
3
3
|
<template v-for="(col, idx) in state.form" :key="idx">
|
|
4
4
|
<div v-if="!col.clear" :id="col._fid" :ui-col="col.media" :ui-flex="`${vertical ? 'col xm' : 'row lt'}`">
|
|
5
5
|
<template v-if="col.label">
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
</div>
|
|
183
183
|
</template>
|
|
184
184
|
<script lang="ts" setup>
|
|
185
|
-
import { watch, reactive, onBeforeUnmount, nextTick } from 'vue';
|
|
185
|
+
import { watch, reactive, onBeforeUnmount, nextTick, useAttrs, computed } from 'vue';
|
|
186
186
|
import { useRoute, useRouter } from 'vue-router';
|
|
187
187
|
import { isString, isObject, isFunction, deepcopy, isArray } from '@fekit/utils';
|
|
188
188
|
import {
|
|
@@ -212,6 +212,9 @@
|
|
|
212
212
|
|
|
213
213
|
const route = useRoute();
|
|
214
214
|
const router = useRouter();
|
|
215
|
+
const attrs = useAttrs();
|
|
216
|
+
// 外部写了 class 时去掉默认根 class,由外部接管
|
|
217
|
+
const rootClass = computed(() => (attrs.class ? undefined : 'n-sl-sub'));
|
|
215
218
|
|
|
216
219
|
// 计算Label行高
|
|
217
220
|
const clh = ({ sz = '' }: any = {}) => {
|