@neatui/vue 4.7.0 → 4.8.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.8.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": "
|
|
37
|
+
"@neatui/css": "4.7.1",
|
|
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",
|
|
@@ -46,5 +46,8 @@
|
|
|
46
46
|
"sortablejs": "^1.15.6",
|
|
47
47
|
"swiper": "^11.0.4",
|
|
48
48
|
"vue-cropper": "^0.6.4"
|
|
49
|
+
},
|
|
50
|
+
"resolutions": {
|
|
51
|
+
"@neatui/css": "4.7.1"
|
|
49
52
|
}
|
|
50
53
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
@mouseout="hoverhide"
|
|
17
17
|
@click="clickhide"
|
|
18
18
|
>
|
|
19
|
-
<div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="tipBoxStyle">
|
|
19
|
+
<div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="[tipBoxStyle, dragPassing ? 'pointer-events: none;' : '']">
|
|
20
20
|
<div ui-hide=">mob" class="mob-follow-tips w-full n-ss" ui-flex="row xm">
|
|
21
21
|
<div class="w-xs*s"></div>
|
|
22
22
|
<div>
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
tipBoxStyle?: string;
|
|
55
55
|
arrow?: any;
|
|
56
56
|
clickTipHide?: boolean;
|
|
57
|
+
dragTipHide?: boolean;
|
|
57
58
|
mobile?: boolean;
|
|
58
59
|
title?: string;
|
|
59
60
|
zIndex?: string | number | null;
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
tipBoxStyle: '',
|
|
73
74
|
arrow: {},
|
|
74
75
|
clickTipHide: false,
|
|
76
|
+
dragTipHide: false,
|
|
75
77
|
mobile: true,
|
|
76
78
|
title: '',
|
|
77
79
|
zIndex: undefined,
|
|
@@ -92,6 +94,78 @@
|
|
|
92
94
|
|
|
93
95
|
const showTimer: any = ref(null);
|
|
94
96
|
const hideTimer: any = ref(null);
|
|
97
|
+
const dragPointerId = ref<number | null>(null);
|
|
98
|
+
const dragFromInternal = ref(false);
|
|
99
|
+
const dragStartX = ref(0);
|
|
100
|
+
const dragStartY = ref(0);
|
|
101
|
+
const dragPassing = ref(false);
|
|
102
|
+
const DRAG_HIDE_PX = 6;
|
|
103
|
+
|
|
104
|
+
const containsPoint = (el: HTMLElement | null, x: number, y: number) => {
|
|
105
|
+
if (!el) return false;
|
|
106
|
+
const rect = el.getBoundingClientRect();
|
|
107
|
+
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const isOutsideFollow = (x: number, y: number) => {
|
|
111
|
+
return !containsPoint(box.value, x, y) && !containsPoint(dom.value, x, y);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const isInternalDragSource = (target: EventTarget | null) => {
|
|
115
|
+
if (!(target instanceof Element)) return false;
|
|
116
|
+
if (target.closest('input, textarea, select, [contenteditable="true"], [role="slider"]')) return true;
|
|
117
|
+
const stop = box.value?.parentElement || null;
|
|
118
|
+
let el: Element | null = target;
|
|
119
|
+
while (el && el !== stop) {
|
|
120
|
+
const style = window.getComputedStyle(el);
|
|
121
|
+
const canScroll =
|
|
122
|
+
((style.overflowY === 'auto' || style.overflowY === 'scroll') && el.scrollHeight > el.clientHeight) ||
|
|
123
|
+
((style.overflowX === 'auto' || style.overflowX === 'scroll') && el.scrollWidth > el.clientWidth);
|
|
124
|
+
if (canScroll) return true;
|
|
125
|
+
el = el.parentElement;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const clearDragSession = () => {
|
|
131
|
+
dragPointerId.value = null;
|
|
132
|
+
dragFromInternal.value = false;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const hideFromDrag = () => {
|
|
136
|
+
dragPassing.value = true;
|
|
137
|
+
state.show = 0;
|
|
138
|
+
state.hover = 0;
|
|
139
|
+
clearDragSession();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const onDragPointerDown = (ev: PointerEvent) => {
|
|
143
|
+
if (!props.dragTipHide || !state.show || ev.button !== 0) return;
|
|
144
|
+
if (!box.value?.contains(ev.target as Node)) return;
|
|
145
|
+
dragPointerId.value = ev.pointerId;
|
|
146
|
+
dragFromInternal.value = isInternalDragSource(ev.target);
|
|
147
|
+
dragStartX.value = ev.clientX;
|
|
148
|
+
dragStartY.value = ev.clientY;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const onDragPointerMove = (ev: PointerEvent) => {
|
|
152
|
+
if (!props.dragTipHide || dragPointerId.value !== ev.pointerId || ev.buttons === 0) return;
|
|
153
|
+
if (isOutsideFollow(ev.clientX, ev.clientY)) {
|
|
154
|
+
hideFromDrag();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (dragFromInternal.value) return;
|
|
158
|
+
const dx = ev.clientX - dragStartX.value;
|
|
159
|
+
const dy = ev.clientY - dragStartY.value;
|
|
160
|
+
if (dx * dx + dy * dy >= DRAG_HIDE_PX * DRAG_HIDE_PX) {
|
|
161
|
+
hideFromDrag();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const onDragPointerUp = (ev: PointerEvent) => {
|
|
166
|
+
if (dragPointerId.value === ev.pointerId) clearDragSession();
|
|
167
|
+
};
|
|
168
|
+
|
|
95
169
|
watch(
|
|
96
170
|
() => state.hover,
|
|
97
171
|
() => {
|
|
@@ -148,11 +222,13 @@
|
|
|
148
222
|
};
|
|
149
223
|
|
|
150
224
|
const show = () => {
|
|
225
|
+
dragPassing.value = false;
|
|
151
226
|
state.show = 1;
|
|
152
227
|
};
|
|
153
228
|
|
|
154
229
|
const hide = () => {
|
|
155
230
|
state.show = 0;
|
|
231
|
+
clearDragSession();
|
|
156
232
|
};
|
|
157
233
|
|
|
158
234
|
// 取消
|
|
@@ -184,6 +260,10 @@
|
|
|
184
260
|
onMounted(() => {
|
|
185
261
|
ensureTeleportTarget();
|
|
186
262
|
document.addEventListener('click', cancel, true);
|
|
263
|
+
document.addEventListener('pointerdown', onDragPointerDown, true);
|
|
264
|
+
window.addEventListener('pointermove', onDragPointerMove, true);
|
|
265
|
+
window.addEventListener('pointerup', onDragPointerUp, true);
|
|
266
|
+
window.addEventListener('pointercancel', onDragPointerUp, true);
|
|
187
267
|
// 添加触摸事件监听器以防止页面滚动
|
|
188
268
|
document.addEventListener('touchstart', preventTouchScroll, {
|
|
189
269
|
passive: false,
|
|
@@ -232,6 +312,10 @@
|
|
|
232
312
|
|
|
233
313
|
onUnmounted(() => {
|
|
234
314
|
document.removeEventListener('click', cancel, true);
|
|
315
|
+
document.removeEventListener('pointerdown', onDragPointerDown, true);
|
|
316
|
+
window.removeEventListener('pointermove', onDragPointerMove, true);
|
|
317
|
+
window.removeEventListener('pointerup', onDragPointerUp, true);
|
|
318
|
+
window.removeEventListener('pointercancel', onDragPointerUp, true);
|
|
235
319
|
// 移除触摸事件监听器
|
|
236
320
|
document.removeEventListener('touchstart', preventTouchScroll);
|
|
237
321
|
document.removeEventListener('touchmove', preventTouchScroll);
|
|
@@ -245,6 +329,11 @@
|
|
|
245
329
|
watch(
|
|
246
330
|
() => state.show,
|
|
247
331
|
(show: any) => {
|
|
332
|
+
if (show) {
|
|
333
|
+
dragPassing.value = false;
|
|
334
|
+
} else {
|
|
335
|
+
clearDragSession();
|
|
336
|
+
}
|
|
248
337
|
if (ex.value) {
|
|
249
338
|
if (show) {
|
|
250
339
|
ex.value.listen();
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :ui-form="`${theme} type:select sz:${sz}`" :ui-form-open="state.show ? 'show' : 'hide'">
|
|
3
|
+
<IFollowView ref="ifollow" v-model="state.show" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%" @onshow="onshow" @onhide="onhide">
|
|
4
|
+
<Input
|
|
5
|
+
v-bind="{ placeholder, rules, theme, sz, co, block, ready, type: 'text', tips, disabled, readonly: true, clearable: false, verify, prefix, suffix, interacted }"
|
|
6
|
+
:modelValue="state.label"
|
|
7
|
+
:interacted="interacted"
|
|
8
|
+
>
|
|
9
|
+
<template #suffix><i ui-form-select=""></i></template>
|
|
10
|
+
</Input>
|
|
11
|
+
<template #tips v-if="!disabled && !readonly">
|
|
12
|
+
<div v-if="search" class="nb-sm nx-sl">
|
|
13
|
+
<Input v-model="state.search" :theme="theme" sz="s">
|
|
14
|
+
<template #suffix>
|
|
15
|
+
<i ui-form-search="" class="co-note"></i>
|
|
16
|
+
</template>
|
|
17
|
+
</Input>
|
|
18
|
+
</div>
|
|
19
|
+
<div v-if="state.filter?.length" ui-scroll=":y s" class="nx-sm lh-ml" style="min-width: 100%; max-height: 12em">
|
|
20
|
+
<Tree
|
|
21
|
+
v-model="state.value"
|
|
22
|
+
:treeData="state.filter"
|
|
23
|
+
:options="fields"
|
|
24
|
+
:multiple="true"
|
|
25
|
+
:checkStrictly="checkStrictly"
|
|
26
|
+
:defaultExpandAll="defaultExpandAll"
|
|
27
|
+
:expandedKeys="expandedKeys"
|
|
28
|
+
block
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
<div v-else class="w-full ny-ms" ui-flex="col cm" v-html="spare"></div>
|
|
32
|
+
<ul class="nx-sm lh-ml" v-if="tools">
|
|
33
|
+
<li class="my-ss"><div class="o-mm" ui-line="@a"></div></li>
|
|
34
|
+
<li ui-flex="row xm">
|
|
35
|
+
<div class="co-main">
|
|
36
|
+
<p class="nx-sm r-ss nowrap">
|
|
37
|
+
<span>{{ state.count }}个可选项</span>
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
<button v-show="state.value?.length" class="co-risk" ui-btn="@a none s" type="button" @click="remove">清空</button>
|
|
42
|
+
</div>
|
|
43
|
+
</li>
|
|
44
|
+
</ul>
|
|
45
|
+
</template>
|
|
46
|
+
</IFollowView>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<script setup lang="ts">
|
|
51
|
+
import { reactive, ref, computed, watch } from 'vue';
|
|
52
|
+
import Input from './Input.vue';
|
|
53
|
+
import { IFollowView } from '../basic';
|
|
54
|
+
import { Tree } from '../display';
|
|
55
|
+
import { isArray } from '@fekit/utils';
|
|
56
|
+
|
|
57
|
+
const ifollow: any = ref(null);
|
|
58
|
+
const emits = defineEmits(['update:modelValue', 'change', 'onshow', 'onhide']);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 入参说明
|
|
62
|
+
* modelValue 值(多选,数组)
|
|
63
|
+
* options 枚举列表(label / value / child)
|
|
64
|
+
* tools 是否显示工具栏
|
|
65
|
+
* spare 空闲兜底内容
|
|
66
|
+
* search 是否启用搜索框
|
|
67
|
+
* separator 回显文案的分隔符
|
|
68
|
+
* checkStrictly 父子节点选中状态不再关联
|
|
69
|
+
* defaultExpandAll 是否默认展开所有子节点
|
|
70
|
+
* expandedKeys 默认展开的节点
|
|
71
|
+
*
|
|
72
|
+
* ---------------------------
|
|
73
|
+
* theme 主题
|
|
74
|
+
* sz 尺寸
|
|
75
|
+
* co 颜色
|
|
76
|
+
* block 撑满
|
|
77
|
+
* ready 准备就绪
|
|
78
|
+
* tips 提示文案
|
|
79
|
+
* placeholder 内容占位
|
|
80
|
+
* disabled 是否禁用
|
|
81
|
+
* readonly 是否只读
|
|
82
|
+
* rules 校验规则
|
|
83
|
+
* verify 是否显示校验图标
|
|
84
|
+
* prefix 前缀内容
|
|
85
|
+
* suffix 后缀内容
|
|
86
|
+
* interacted 用户是否交互
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
// 类型
|
|
90
|
+
interface Props {
|
|
91
|
+
modelValue?: Array<string | number>;
|
|
92
|
+
options?: Array<object>;
|
|
93
|
+
tools?: boolean;
|
|
94
|
+
spare?: string;
|
|
95
|
+
search?: boolean;
|
|
96
|
+
separator?: string;
|
|
97
|
+
checkStrictly?: boolean;
|
|
98
|
+
defaultExpandAll?: boolean;
|
|
99
|
+
expandedKeys?: Array<string | number>;
|
|
100
|
+
|
|
101
|
+
// Input
|
|
102
|
+
placeholder?: string;
|
|
103
|
+
rules?: Array<object>;
|
|
104
|
+
theme?: string;
|
|
105
|
+
sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
|
|
106
|
+
co?: string;
|
|
107
|
+
block?: boolean;
|
|
108
|
+
ready?: boolean;
|
|
109
|
+
tips?: string;
|
|
110
|
+
disabled?: boolean;
|
|
111
|
+
readonly?: boolean;
|
|
112
|
+
verify?: boolean;
|
|
113
|
+
prefix?: Array<any>;
|
|
114
|
+
suffix?: Array<any>;
|
|
115
|
+
interacted?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 入参
|
|
119
|
+
const props: any = withDefaults(defineProps<Props>(), {
|
|
120
|
+
modelValue: () => [],
|
|
121
|
+
options: () => [],
|
|
122
|
+
tools: true,
|
|
123
|
+
spare: '当前没有可选项',
|
|
124
|
+
search: true,
|
|
125
|
+
separator: ' | ',
|
|
126
|
+
checkStrictly: false,
|
|
127
|
+
defaultExpandAll: true,
|
|
128
|
+
expandedKeys: () => [],
|
|
129
|
+
|
|
130
|
+
// INPUT
|
|
131
|
+
placeholder: '',
|
|
132
|
+
rules: () => [],
|
|
133
|
+
theme: '@a',
|
|
134
|
+
sz: '',
|
|
135
|
+
co: '',
|
|
136
|
+
block: true,
|
|
137
|
+
ready: false,
|
|
138
|
+
tips: '',
|
|
139
|
+
disabled: false,
|
|
140
|
+
readonly: false,
|
|
141
|
+
verify: false,
|
|
142
|
+
prefix: () => [],
|
|
143
|
+
suffix: () => [],
|
|
144
|
+
interacted: false,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Tree 的字段映射:枚举用 value / label / child,Tree 内部用 id / label / children
|
|
148
|
+
const fields = { id: 'value', label: 'label', children: 'child' };
|
|
149
|
+
|
|
150
|
+
// 过滤:子节点命中则只留命中的子节点,自身命中则保留整棵子树(与 filterJson 行为一致,但忽略大小写)
|
|
151
|
+
const filterTree = (nodes: Array<any> = [], keyword: string): Array<any> => {
|
|
152
|
+
if (!keyword) return nodes;
|
|
153
|
+
const out: Array<any> = [];
|
|
154
|
+
for (const node of nodes) {
|
|
155
|
+
const child = node?.child?.length ? filterTree(node.child, keyword) : [];
|
|
156
|
+
if (child.length) {
|
|
157
|
+
out.push({ ...node, child });
|
|
158
|
+
} else if (String(node?.label ?? '').toLowerCase().includes(keyword)) {
|
|
159
|
+
out.push({ ...node });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// 回显文案:始终在完整枚举里找,避免搜索时已选项的文案消失
|
|
166
|
+
const collectLabels = (nodes: Array<any> = [], values: Array<string | number> = []) => {
|
|
167
|
+
const set = new Set(values.map(String));
|
|
168
|
+
const out: Array<string> = [];
|
|
169
|
+
const walk = (list: Array<any>) => {
|
|
170
|
+
for (const node of list) {
|
|
171
|
+
if (set.has(String(node?.value))) out.push(node?.label);
|
|
172
|
+
if (node?.child?.length) walk(node.child);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
walk(nodes);
|
|
176
|
+
return out;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// 可选项数量:关联选择时只算叶子节点,独立选择时算全部节点
|
|
180
|
+
const countNodes = (nodes: Array<any> = [], all: boolean): number => {
|
|
181
|
+
let n = 0;
|
|
182
|
+
const walk = (list: Array<any>) => {
|
|
183
|
+
for (const node of list) {
|
|
184
|
+
if (node?.child?.length) {
|
|
185
|
+
if (all) n += 1;
|
|
186
|
+
walk(node.child);
|
|
187
|
+
} else {
|
|
188
|
+
n += 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
walk(nodes);
|
|
193
|
+
return n;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const state: any = reactive({
|
|
197
|
+
show: 0,
|
|
198
|
+
search: '',
|
|
199
|
+
value: computed({
|
|
200
|
+
get: () => (isArray(props.modelValue) ? props.modelValue : []),
|
|
201
|
+
set: (value: Array<string | number>) => {
|
|
202
|
+
const next = isArray(value) ? value : [];
|
|
203
|
+
// Tree 挂载时会回吐一次初始值,值没变就不向外抛事件
|
|
204
|
+
if (next.join(',') === state.value.join(',')) return;
|
|
205
|
+
change(next);
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
label: computed(() => collectLabels(props.options, isArray(props.modelValue) ? props.modelValue : []).join(props.separator)),
|
|
209
|
+
filter: computed(() => filterTree(props.options, state.search.trim().toLowerCase())),
|
|
210
|
+
count: computed(() => countNodes(state.filter, props.checkStrictly)),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// 更新
|
|
214
|
+
const change = (value: Array<string | number> = []) => {
|
|
215
|
+
emits('update:modelValue', value);
|
|
216
|
+
emits('change', value);
|
|
217
|
+
};
|
|
218
|
+
// 选择
|
|
219
|
+
const select = (value: Array<string | number> = []) => {
|
|
220
|
+
change(value);
|
|
221
|
+
};
|
|
222
|
+
// 清空
|
|
223
|
+
const remove = () => {
|
|
224
|
+
change([]);
|
|
225
|
+
cancel();
|
|
226
|
+
};
|
|
227
|
+
// 取消
|
|
228
|
+
const cancel = () => {
|
|
229
|
+
ifollow.value?.cancel();
|
|
230
|
+
};
|
|
231
|
+
// 显示
|
|
232
|
+
const onshow = () => {
|
|
233
|
+
emits('onshow', state.value);
|
|
234
|
+
};
|
|
235
|
+
// 折叠
|
|
236
|
+
const timer: any = ref(null);
|
|
237
|
+
const onhide = () => {
|
|
238
|
+
clearTimeout(timer.value);
|
|
239
|
+
timer.value = setTimeout(() => {
|
|
240
|
+
state.search = '';
|
|
241
|
+
}, 500);
|
|
242
|
+
emits('onhide', state.value);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
watch(
|
|
246
|
+
() => props.disabled,
|
|
247
|
+
(disabled: boolean) => {
|
|
248
|
+
if (disabled) cancel();
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
defineExpose({
|
|
253
|
+
select,
|
|
254
|
+
remove,
|
|
255
|
+
change,
|
|
256
|
+
cancel,
|
|
257
|
+
});
|
|
258
|
+
</script>
|
|
@@ -15,6 +15,7 @@ import MoreSelect from './MoreSelect.vue';
|
|
|
15
15
|
import MoreSelectList from './MoreSelectList.vue';
|
|
16
16
|
import MoreSelectTags from './MoreSelectTags.vue';
|
|
17
17
|
import MoreSelectPicker from './MoreSelectPicker.vue';
|
|
18
|
+
import MoreSelectTree from './MoreSelectTree.vue';
|
|
18
19
|
import SearchMoreSelect from './SearchMoreSelect.vue';
|
|
19
20
|
import PageSelect from './PageSelect.vue';
|
|
20
21
|
import SelectTree from './SelectTree/SelectTree.vue';
|
|
@@ -47,6 +48,7 @@ export {
|
|
|
47
48
|
MoreSelectList,
|
|
48
49
|
MoreSelectTags,
|
|
49
50
|
MoreSelectPicker,
|
|
51
|
+
MoreSelectTree,
|
|
50
52
|
SearchMoreSelect,
|
|
51
53
|
// 日期&时间
|
|
52
54
|
DateView,
|