@neatui/vue 2.17.2 → 2.17.5

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": "2.17.2",
3
+ "version": "2.17.5",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -3,24 +3,33 @@
3
3
  <slot>
4
4
  <button ui-btn="@a s case :border"><i class="icon icon-dropdown"></i></button>
5
5
  </slot>
6
- <Teleport to="#ifollow" v-if="$slots.tips">
7
- <div
8
- ref="tip"
9
- :class="tipClass"
10
- :style="`position: fixed; pointer-events: none; ${tipStyle}`"
11
- ui-tips="@a"
12
- :ui-tips-view="state.show ? 1 : 0"
13
- @mouseover="hovershow"
14
- @mouseout="hoverhide"
15
- @click="clickhide"
16
- >
17
- <div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="tipBoxStyle" @mousedown="downdom">
18
- <slot name="tips"></slot>
19
- <div v-if="arrow" ui-tips-arrow="" v-bind="arrow"></div>
6
+ </div>
7
+ <Teleport v-if="$slots.tips" to="#ifollow">
8
+ <div
9
+ ref="tip"
10
+ :class="`${tipClass}${mobile ? ' fekit-follow-mobile' : ''}`"
11
+ :style="`position: fixed; pointer-events: none; ${tipStyle}`"
12
+ ui-tips="@a"
13
+ :ui-tips-view="state.show ? 1 : 0"
14
+ @mouseover="hovershow"
15
+ @mouseout="hoverhide"
16
+ @click="clickhide"
17
+ >
18
+ <div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="tipBoxStyle">
19
+ <div ui-hide=">mob" class="mob-follow-tips w-full n-ss" ui-flex="row xm">
20
+ <div class="w-sm"></div>
21
+ <div>
22
+ <h5 class="bold">{{ title }}</h5>
23
+ </div>
24
+ <div class="w-sm" ui-flex="row rm">
25
+ <button ui-btn="@a none s :square" @click="toggle"><i class="icon icon-close"></i></button>
26
+ </div>
20
27
  </div>
28
+ <slot name="tips"></slot>
29
+ <div v-if="arrow" ui-tips-arrow="" v-bind="arrow"></div>
21
30
  </div>
22
- </Teleport>
23
- </div>
31
+ </div>
32
+ </Teleport>
24
33
  </template>
25
34
  <script setup lang="ts">
26
35
  import { reactive, ref, onMounted, onUnmounted, watch } from 'vue';
@@ -48,6 +57,8 @@
48
57
  tipBoxStyle?: string;
49
58
  arrow?: any;
50
59
  clickTipHide?: boolean;
60
+ mobile?: boolean;
61
+ title?: string;
51
62
  }
52
63
 
53
64
  // 入参
@@ -63,6 +74,8 @@
63
74
  tipBoxStyle: '',
64
75
  arrow: {},
65
76
  clickTipHide: false,
77
+ mobile: true,
78
+ title: '',
66
79
  });
67
80
 
68
81
  const dom: any = ref(null);
@@ -74,6 +87,7 @@
74
87
  pos: props.pos,
75
88
  hover: 0,
76
89
  tipdom: false,
90
+ isMobile: false,
77
91
  });
78
92
 
79
93
  const showTimer: any = ref(null);
@@ -148,42 +162,47 @@
148
162
  }
149
163
  };
150
164
 
151
- const downdom = (ev: any) => {
152
- // 不记得了这个当时为什么要加上这个,这个会影响单选框的选中后不自动关闭下拉框啊~
153
- // state.tipdom = true;
154
- // document.addEventListener(
155
- // 'mouseup',
156
- // () => {
157
- // setTimeout(() => {
158
- // state.tipdom = false;
159
- // }, 10);
160
- // },
161
- // true
162
- // );
165
+ // 阻止触摸事件冒泡和默认行为
166
+ const preventTouchScroll = (e: TouchEvent) => {
167
+ // 只有当触摸点在遮罩层本身时才阻止,不影响子元素
168
+ if (e.target === tip.value && tip.value?.classList.contains('fekit-follow-mobile')) {
169
+ e.preventDefault();
170
+ e.stopPropagation();
171
+ }
163
172
  };
164
173
 
165
174
  onMounted(() => {
166
175
  document.addEventListener('click', cancel, true);
176
+ // 添加触摸事件监听器以防止页面滚动
177
+ document.addEventListener('touchstart', preventTouchScroll, { passive: false });
178
+ document.addEventListener('touchmove', preventTouchScroll, { passive: false });
179
+ // 判断是否为移动端
180
+ state.isMobile = window.innerWidth < 800;
167
181
  });
168
182
 
169
183
  const ex: any = ref(null);
170
184
  watch([() => dom.value, () => tip.value], () => {
171
185
  if (dom.value && tip.value) {
172
- ex.value = new Follow(dom.value, tip.value, (status) => {
173
- if (!props?.pos?.includes(':fixed')) {
174
- if (status === 'b') {
175
- const _pos = props?.pos?.replace(/b/g, 't');
176
- if (_pos) {
177
- state.pos = _pos;
178
- }
179
- } else {
180
- const _pos = props?.pos?.replace(/t/g, 'b');
181
- if (_pos) {
182
- state.pos = _pos;
186
+ ex.value = new Follow(
187
+ dom.value,
188
+ tip.value,
189
+ (status) => {
190
+ if (!props?.pos?.includes(':fixed')) {
191
+ if (status === 'b') {
192
+ const _pos = props?.pos?.replace(/b/g, 't');
193
+ if (_pos) {
194
+ state.pos = _pos;
195
+ }
196
+ } else {
197
+ const _pos = props?.pos?.replace(/t/g, 'b');
198
+ if (_pos) {
199
+ state.pos = _pos;
200
+ }
183
201
  }
184
202
  }
185
- }
186
- });
203
+ },
204
+ { hideOnScrollOut: false },
205
+ );
187
206
  } else {
188
207
  ex.value = null;
189
208
  }
@@ -191,6 +210,9 @@
191
210
 
192
211
  onUnmounted(() => {
193
212
  document.removeEventListener('click', cancel, true);
213
+ // 移除触摸事件监听器
214
+ document.removeEventListener('touchstart', preventTouchScroll);
215
+ document.removeEventListener('touchmove', preventTouchScroll);
194
216
  state.show = 0;
195
217
 
196
218
  if (ex.value) {
@@ -218,3 +240,64 @@
218
240
  cancel,
219
241
  });
220
242
  </script>
243
+ <style lang="scss">
244
+ @use '@neatui/css' as neatui;
245
+ @include neatui.mob {
246
+ .fekit-follow-mobile {
247
+ position: fixed !important;
248
+ width: 100% !important;
249
+ height: 100% !important;
250
+
251
+ display: flex !important;
252
+ flex-direction: column !important;
253
+ justify-content: flex-end !important;
254
+ align-items: center !important;
255
+ align-content: center !important;
256
+ inset: 0 0 0 0 !important;
257
+
258
+ &[ui-tips-view='1'] {
259
+ background-color: rgba(0, 0, 0, 0.5);
260
+ pointer-events: auto !important;
261
+ touch-action: none !important;
262
+ }
263
+
264
+ &[ui-tips~='@a'] [ui-tips-box] {
265
+ position: relative !important;
266
+
267
+ width: 100% !important;
268
+ bottom: env(safe-area-inset-bottom) !important;
269
+ right: unset !important;
270
+ top: unset !important;
271
+ left: unset !important;
272
+ transform: translate3d(0, 0.5em, 0);
273
+
274
+ display: flex !important;
275
+ flex-direction: column !important;
276
+ justify-content: flex-end !important;
277
+ align-items: center !important;
278
+ align-content: center !important;
279
+
280
+ [ui-tips-arrow] {
281
+ display: none;
282
+ }
283
+ }
284
+ }
285
+
286
+ .mob-follow-tips {
287
+ position: relative;
288
+ padding-bottom: env(safe-area-inset-bottom, 16px);
289
+ &::before {
290
+ position: absolute;
291
+ content: '';
292
+ display: block;
293
+ width: 4rem;
294
+ height: 3px;
295
+ border-radius: 1em;
296
+ background-color: var(--co-text-o-ss);
297
+ top: 5px;
298
+ left: 50%;
299
+ transform: translateX(-50%);
300
+ }
301
+ }
302
+ }
303
+ </style>
@@ -1,131 +1,136 @@
1
1
  <template>
2
- <div class="fekit-ipull-scroll" :ui-scroll="`:y ${stable ? 'stable' : ''}`" ref="root">
3
- <div>
2
+ <div
3
+ class="fekit-ipull-scroll"
4
+ :ui-scroll="`:y ${stable ? 'stable' : ''}`"
5
+ ref="root"
6
+ >
7
+ <div class="full">
4
8
  <div class="fs-xs co-text o-mm" fekit-pullload-head="" v-if="pull">
5
- <div v-show="demo.store.pull < 2" ui-flex="row cm">
6
- <i class="pulldown icon icon-pulldown" :class="`${demo.store.pull === 1 ? 'toup' : ''}`"></i>
7
- <span v-show="demo.store.pull === 0">下拉刷新</span>
8
- <span v-show="demo.store.pull === 1">松开加载</span>
9
- </div>
10
- <div v-show="demo.store.pull === 2" ui-flex="row cm">
11
- <span ui-load="@d"></span>
12
- <!-- <span>正在刷新</span> -->
13
- </div>
14
- <div v-show="demo.store.pull === 3" ui-flex="row cm">
15
- <span>刷新完成</span>
16
- </div>
17
- </div>
18
- <slot></slot>
19
- <div class="fs-xs co-text o-mm mb-sm" fekit-pullload-foot="" v-if="load">
20
- <transition name="fekit-pullload-fade" mode="out-in">
21
- <div v-if="demo.store.load === 2" ui-flex="row cm">
9
+ <slot name="pull-head" :state="demo.store.pull">
10
+ <div v-show="demo.store.pull < 2" ui-flex="row cm">
11
+ <i class="pulldown icon icon-pulldown" :class="`${demo.store.pull === 1 ? 'toup' : ''}`"></i>
12
+ <span v-show="demo.store.pull === 0">下拉刷新</span>
13
+ <span v-show="demo.store.pull === 1">松开加载</span>
14
+ </div>
15
+ <div v-show="demo.store.pull === 2" ui-flex="row cm">
22
16
  <span ui-load="@d"></span>
23
- <!-- <span>正在加载</span> -->
24
17
  </div>
25
- <div v-else-if="demo.store.load === 3">没有更多了</div>
26
- <div v-else>&nbsp;</div>
27
- </transition>
18
+ <div v-show="demo.store.pull === 3" ui-flex="row cm">
19
+ <span>刷新完成</span>
20
+ </div>
21
+ </slot>
22
+ </div>
23
+ <slot></slot>
24
+ <div class="fs-xs co-text o-mm my-sm" fekit-pullload-foot="" v-if="load">
25
+ <slot name="load-foot" :state="demo.store.load">
26
+ <transition name="fekit-pullload-fade" mode="out-in">
27
+ <div v-if="demo.store.load === 2" ui-flex="row cm">
28
+ <span ui-load="@d"></span>
29
+ </div>
30
+ <div v-else-if="demo.store.load === 3">没有更多了</div>
31
+ <div v-else>&nbsp;</div>
32
+ </transition>
33
+ </slot>
28
34
  </div>
29
35
  </div>
30
36
  </div>
31
37
  </template>
32
38
  <script setup lang="ts">
33
- import { onMounted, onBeforeUnmount, ref } from 'vue';
34
- import PullLoad, { Easing } from '@fekit/pullload';
35
- const emits = defineEmits(['onpull', 'onload']);
36
- const props: any = defineProps({
37
- pull: {
38
- type: Boolean,
39
- default: false,
40
- },
41
- load: {
42
- type: Boolean,
43
- default: false,
44
- },
45
- stable: {
46
- type: Boolean,
47
- default: false,
48
- },
49
- });
50
- const root: any = ref(null);
51
- const demo: any = ref(null);
52
- demo.value = new PullLoad({
53
- easing: Easing.easeOut,
54
- friction: 0.3,
55
- triggerDistance: 60,
56
- loadingDistance: 50,
57
- ...(props.pull
58
- ? {
59
- onpull: async () => {
60
- emits('onpull', demo.value);
61
- },
62
- }
63
- : {}),
64
- ...(props.pull
65
- ? {
66
- onload: async () => {
67
- emits('onload', demo.value);
68
- },
69
- }
70
- : {}),
71
- });
72
-
73
- onMounted(() => {
74
- if (root.value) {
75
- demo.value?.listen(root.value);
76
- if (props.pull) {
77
- demo.value?.ispull();
39
+ import { onMounted, onBeforeUnmount, ref } from "vue";
40
+ import PullLoad, { Easing } from "@fekit/pullload";
41
+ const emits = defineEmits(["onpull", "onload"]);
42
+ const props: any = defineProps({
43
+ pull: {
44
+ type: Boolean,
45
+ default: false,
46
+ },
47
+ load: {
48
+ type: Boolean,
49
+ default: false,
50
+ },
51
+ stable: {
52
+ type: Boolean,
53
+ default: false,
54
+ },
55
+ });
56
+ const root: any = ref(null);
57
+ const demo: any = ref(null);
58
+ demo.value = new PullLoad({
59
+ easing: Easing.easeOut,
60
+ friction: 0.3,
61
+ triggerDistance: 60,
62
+ loadingDistance: 50,
63
+ ...(props.pull
64
+ ? {
65
+ onpull: async () => {
66
+ emits("onpull", demo.value);
67
+ },
68
+ }
69
+ : {}),
70
+ ...(props.load
71
+ ? {
72
+ onload: async () => {
73
+ emits("onload", demo.value);
74
+ },
78
75
  }
76
+ : {}),
77
+ });
78
+
79
+ onMounted(() => {
80
+ if (root.value) {
81
+ demo.value?.listen(root.value);
82
+ if (props.pull) {
83
+ demo.value?.ispull();
79
84
  }
80
- });
81
- onBeforeUnmount(() => {
82
- demo.value?.remove();
83
- });
85
+ }
86
+ });
87
+ onBeforeUnmount(() => {
88
+ demo.value?.remove();
89
+ });
84
90
 
85
- const pullload = () => {
86
- demo.value?.ispull();
87
- };
91
+ const pullload = () => {
92
+ demo.value?.ispull();
93
+ };
88
94
 
89
- defineExpose({ pullload, ex: () => demo.value });
95
+ defineExpose({ pullload, ex: () => demo.value });
90
96
  </script>
91
97
 
92
98
  <style lang="scss">
93
- .fekit-ipull-scroll {
94
- [fekit-pullload-head],
95
- [fekit-pullload-foot] {
96
- position: absolute;
97
- width: 100%;
98
- height: 2em;
99
- left: 0;
100
- display: flex;
101
- justify-content: center;
102
- align-items: center;
103
- align-content: center;
104
- }
105
- [fekit-pullload-head] {
106
- bottom: 100%;
107
- margin-bottom: 1em;
108
- }
109
- [fekit-pullload-foot] {
110
- position: relative;
111
- top: 100%;
112
- }
99
+ .fekit-ipull-scroll {
100
+ [fekit-pullload-head],
101
+ [fekit-pullload-foot] {
102
+ position: absolute;
103
+ width: 100%;
104
+ height: 2em;
105
+ left: 0;
106
+ display: flex;
107
+ justify-content: center;
108
+ align-items: center;
109
+ align-content: center;
110
+ }
111
+ [fekit-pullload-head] {
112
+ bottom: 100%;
113
+ margin-bottom: 1em;
114
+ }
115
+ [fekit-pullload-foot] {
116
+ position: relative;
117
+ }
113
118
 
114
- .fekit-pullload-fade-enter-active,
115
- .fekit-pullload-fade-leave-active {
116
- transition: opacity 0.2s;
117
- }
118
- .fekit-pullload-fade-enter,
119
- .fekit-pullload-fade-leave-to {
120
- opacity: 0;
121
- }
119
+ .fekit-pullload-fade-enter-active,
120
+ .fekit-pullload-fade-leave-active {
121
+ transition: opacity 0.2s;
122
+ }
123
+ .fekit-pullload-fade-enter,
124
+ .fekit-pullload-fade-leave-to {
125
+ opacity: 0;
126
+ }
122
127
 
123
- .pulldown {
124
- transition: all 0.3s;
125
- display: inline-block;
126
- &.toup {
127
- transform: rotate(180deg);
128
- }
128
+ .pulldown {
129
+ transition: all 0.3s;
130
+ display: inline-block;
131
+ &.toup {
132
+ transform: rotate(180deg);
129
133
  }
130
134
  }
135
+ }
131
136
  </style>
@@ -1,64 +1,79 @@
1
1
  <template>
2
- <form class="m-sm-sub">
3
- <label v-for="(item, idx) in state.lists" :key="idx" :ui-form="`${theme} type:checkbox${item.disabled ? ' disabled' : ''}`">
4
- <input v-model="item.__checked" type="checkbox" :disabled="item.disabled" />
5
- <slot>
6
- <span>{{ item.label }}</span>
7
- </slot>
8
- </label>
9
- </form>
2
+ <CheckboxGroup v-if="isMultiple" v-bind="groupBindings" :modelValue="arrayValue" @update:modelValue="emitValue">
3
+ <template #default="slotProps">
4
+ <slot v-bind="slotProps" />
5
+ </template>
6
+ </CheckboxGroup>
7
+ <Checkbox v-else v-bind="singleBindings" :modelValue="modelValue" @update:modelValue="emitValue">
8
+ <template #default="slotProps">
9
+ <slot v-bind="slotProps" />
10
+ </template>
11
+ </Checkbox>
10
12
  </template>
13
+
11
14
  <script setup lang="ts">
12
- import { reactive, watch } from 'vue';
15
+ import { computed } from 'vue';
16
+ import Checkbox from './CheckboxItem.vue';
17
+ import CheckboxGroup from './CheckboxGroup.vue';
18
+
19
+ type ValueType = any | { label?: string; value: any };
13
20
 
14
- // 定义类型
15
- interface OptionsTypes {
21
+ interface OptionType {
16
22
  label: string;
17
- value: any;
23
+ value?: any;
18
24
  disabled?: boolean;
19
25
  }
26
+
20
27
  interface Props {
21
- modelValue: any[];
22
- options: OptionsTypes[];
28
+ modelValue?: any | any[];
29
+ label?: string;
30
+ disabled?: boolean;
23
31
  theme?: string;
32
+ attrs?: any;
33
+ checkedValue?: ValueType;
34
+ defaultValue?: ValueType;
35
+ options?: (OptionType | string)[] | [ValueType, ValueType];
36
+ required?: boolean;
24
37
  }
25
- // 外部入参
38
+
26
39
  const props = withDefaults(defineProps<Props>(), {
40
+ modelValue: () => false,
27
41
  theme: '@a',
28
- modelValue: () => [],
42
+ disabled: false,
43
+ label: '',
44
+ attrs: () => ({}),
45
+ checkedValue: () => true,
46
+ defaultValue: () => false,
47
+ required: false,
29
48
  options: () => [],
30
49
  });
31
50
 
32
- // 定义事件
33
- const emits = defineEmits(['update:modelValue']);
51
+ const emit = defineEmits<{
52
+ (e: 'update:modelValue', value: any | any[]): void;
53
+ }>();
34
54
 
35
- // 内部数据
36
- const state: any = reactive({
37
- lists: [],
38
- });
55
+ const isMultiple = computed(() => Array.isArray(props.modelValue));
56
+ const arrayValue = computed(() => (Array.isArray(props.modelValue) ? props.modelValue : []));
57
+
58
+ const singleOptions = computed(() => (Array.isArray(props.options) && props.options.length === 2 ? (props.options as [ValueType, ValueType]) : undefined));
59
+
60
+ const singleBindings = computed(() => ({
61
+ label: props.label,
62
+ disabled: props.disabled,
63
+ theme: props.theme,
64
+ attrs: props.attrs,
65
+ checkedValue: props.checkedValue,
66
+ defaultValue: props.defaultValue,
67
+ options: singleOptions.value,
68
+ required: props.required,
69
+ }));
70
+
71
+ const groupBindings = computed(() => ({
72
+ options: props.options as (OptionType | string)[],
73
+ disabled: props.disabled,
74
+ theme: props.theme,
75
+ attrs: props.attrs,
76
+ }));
39
77
 
40
- // 监听数据
41
- watch(
42
- [() => props.options, () => props.modelValue],
43
- ([options, value]) => {
44
- const lists = options?.map((item) => ({ ...item, __checked: value?.includes(item.value) }));
45
- // 只有在内容变化时才更新
46
- if (JSON.stringify(state.lists) !== JSON.stringify(lists)) {
47
- state.lists = lists;
48
- }
49
- },
50
- {
51
- immediate: true,
52
- },
53
- );
54
- watch(
55
- () => state.lists,
56
- (val) => {
57
- const value = val.filter((item: any) => item.__checked).map((item: any) => item.value);
58
- emits('update:modelValue', value);
59
- },
60
- {
61
- deep: true,
62
- },
63
- );
78
+ const emitValue = (value: any | any[]) => emit('update:modelValue', value);
64
79
  </script>
@@ -0,0 +1,74 @@
1
+ <template>
2
+ <form class="m-sm-sub">
3
+ <label v-for="(item, idx) in normalizedOptions" :key="idx" :ui-form="`${theme} type:checkbox${item.disabled ? ' disabled' : ''}`" v-bind="attrs">
4
+ <input type="checkbox" :checked="isChecked(item.value)" :disabled="item.disabled" @change="handleChange(item.value, $event)" />
5
+ <slot :item="item" :index="idx">
6
+ <span>{{ item.label }}</span>
7
+ </slot>
8
+ </label>
9
+ </form>
10
+ </template>
11
+
12
+ <script setup lang="ts">
13
+ import { computed } from 'vue';
14
+
15
+ interface OptionType {
16
+ label: string;
17
+ value?: any;
18
+ disabled?: boolean;
19
+ }
20
+
21
+ interface Props {
22
+ modelValue?: any[];
23
+ options?: (OptionType | string)[];
24
+ disabled?: boolean;
25
+ theme?: string;
26
+ attrs?: any;
27
+ }
28
+
29
+ const props = withDefaults(defineProps<Props>(), {
30
+ modelValue: () => [],
31
+ options: () => [],
32
+ disabled: false,
33
+ theme: '@a',
34
+ attrs: () => ({}),
35
+ });
36
+
37
+ const emits = defineEmits<{
38
+ (e: 'update:modelValue', value: any[]): void;
39
+ }>();
40
+
41
+ // 规范化 options
42
+ const normalizedOptions = computed(() => {
43
+ return props.options.map((item) =>
44
+ typeof item === 'string'
45
+ ? { label: item, value: item, disabled: props.disabled }
46
+ : {
47
+ label: item?.label ?? '',
48
+ value: 'value' in item ? item.value : item?.label,
49
+ disabled: item?.disabled ?? props.disabled,
50
+ },
51
+ );
52
+ });
53
+
54
+ // 判断某个值是否被选中
55
+ const isChecked = (value: any) => {
56
+ return props.modelValue.includes(value);
57
+ };
58
+
59
+ // 处理选中状态变化
60
+ const handleChange = (value: any, e: Event) => {
61
+ const checked = (e.target as HTMLInputElement).checked;
62
+ let newValue: any[];
63
+
64
+ if (checked) {
65
+ // 添加到选中列表
66
+ newValue = [...props.modelValue, value];
67
+ } else {
68
+ // 从选中列表移除
69
+ newValue = props.modelValue.filter((v) => v !== value);
70
+ }
71
+
72
+ emits('update:modelValue', newValue);
73
+ };
74
+ </script>