@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.7.0",
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": "^4.4.1",
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,