@neatui/vue 2.12.0 → 2.15.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": "2.12.0",
3
+ "version": "2.15.0",
4
4
  "description": "basic component ui",
5
5
  "main": "./src/index.ts",
6
6
  "license": "MIT",
@@ -230,11 +230,11 @@
230
230
  e.target.value = props.modelValue;
231
231
  return false;
232
232
  }
233
- // 数字类型
234
- if (props.type === 'number' && !value && props.modelValue?.length !== 1) {
235
- e.target.value = props.modelValue;
236
- return false;
237
- }
233
+ // 数字类型 - 移除有问题的逻辑,允许用户正常删除数字
234
+ // if (props.type === 'number' && !value && props.modelValue?.length !== 1) {
235
+ // e.target.value = props.modelValue;
236
+ // return false;
237
+ // }
238
238
  // 输入事件
239
239
  emits('input', e, state);
240
240
  emits('update:modelValue', value || null);
@@ -24,13 +24,13 @@
24
24
  <template v-for="(item, idx) of state.filter" :key="idx">
25
25
  <li v-if="item.disabled" class="ux-hover nx-sm r-ss o-ms nowrap">
26
26
  <label style="pointer-events: none" ui-form="@a type:checkbox :block">
27
- <input type="checkbox" :name="state.name" :checked="modelValue?.some((i: any) => `${i}` === `${item.value}`)" />
27
+ <input type="checkbox" :name="state.name" :checked="state.processedValue?.some((i: any) => `${i}` === `${item.value}`)" />
28
28
  <del class="ml-sm">{{ item.label || item }}</del>
29
29
  </label>
30
30
  </li>
31
31
  <li v-else class="ux-hover nx-sm r-ss nowrap" @click.stop="select(item.value)">
32
32
  <label style="pointer-events: none" ui-form="@a type:checkbox :block">
33
- <input type="checkbox" :name="state.name" :checked="modelValue?.some((i: any) => `${i}` === `${item.value}`)" />
33
+ <input type="checkbox" :name="state.name" :checked="state.processedValue?.some((i: any) => `${i}` === `${item.value}`)" />
34
34
  <span class="ml-sm">{{ item.label || item }}</span>
35
35
  </label>
36
36
  </li>
@@ -43,7 +43,7 @@
43
43
  <div class="ux-hover nx-sm ny-ss r-ss nowrap co-main" @click.stop="allselect()">
44
44
  <label style="pointer-events: none" ui-form="@a type:checkbox :block">
45
45
  <input type="checkbox" :name="state.name" :checked="state.all" />
46
- <span class="ml-sm">全选({{ state.filter?.length || 0 }})</span>
46
+ <span class="ml-sm">全选({{ state.selectedCount }}/{{ state.availableCount }})</span>
47
47
  </label>
48
48
  </div>
49
49
  <div>
@@ -57,7 +57,7 @@
57
57
  </template>
58
58
 
59
59
  <script setup lang="ts">
60
- import { reactive, ref, computed, watch } from 'vue';
60
+ import { reactive, ref, computed, watch, nextTick } from 'vue';
61
61
  import { Input } from '.';
62
62
  import { IFollowView } from '../basic';
63
63
  const ifollow: any = ref(null);
@@ -96,7 +96,7 @@
96
96
 
97
97
  // 类型
98
98
  interface Props {
99
- modelValue?: Array<string | number>;
99
+ modelValue?: Array<string | number> | string;
100
100
  options: Array<object>;
101
101
  tools?: boolean;
102
102
  spare?: string;
@@ -158,11 +158,21 @@
158
158
  show: 0,
159
159
  value: '',
160
160
  search: '',
161
+ originalType: 'array',
162
+ processedValue: computed(() => processInput(props.modelValue)),
161
163
  all: computed(() => {
162
- return state.filter?.every((item: any) => props.modelValue?.some((i: any) => `${i}` === `${item.value}`));
164
+ const availableItems = state.filter?.filter((item: any) => !item.disabled);
165
+ return availableItems?.length > 0 && availableItems?.every((item: any) => state.processedValue?.some((i: any) => `${i}` === `${item.value}`));
163
166
  }),
164
167
  has: computed(() => {
165
- return state.filter?.some((item: any) => props.modelValue?.some((i: any) => `${i}` === `${item.value}`));
168
+ return state.filter?.some((item: any) => state.processedValue?.some((i: any) => `${i}` === `${item.value}`));
169
+ }),
170
+ availableCount: computed(() => {
171
+ return state.filter?.filter((item: any) => !item.disabled)?.length || 0;
172
+ }),
173
+ selectedCount: computed(() => {
174
+ const availableItems = state.filter?.filter((item: any) => !item.disabled);
175
+ return availableItems?.filter((item: any) => state.processedValue?.some((i: any) => `${i}` === `${item.value}`))?.length || 0;
166
176
  }),
167
177
  filter: computed(() => {
168
178
  if (state.search) {
@@ -175,23 +185,62 @@
175
185
  }),
176
186
  });
177
187
 
188
+ // 处理输入数据类型转换和过滤
189
+ const processInput = (value: any) => {
190
+ let processedArray: any[] = [];
191
+
192
+ if (typeof value === 'string') {
193
+ state.originalType = 'string';
194
+ processedArray = value.trim() ? (value.includes(',') ? value.split(',').map((s: string) => s.trim()) : [value]) : [];
195
+ } else {
196
+ state.originalType = 'array';
197
+ processedArray = value || [];
198
+ }
199
+
200
+ // 过滤掉在options中不存在的值
201
+ const validValues = processedArray.filter((val: any) =>
202
+ props.options?.some((option: any) => `${option.value}` === `${val}`)
203
+ );
204
+
205
+ return validValues;
206
+ };
207
+
178
208
  // 监听外部数据
179
209
  watch(
180
210
  [() => props.modelValue, () => props.options],
181
211
  ([v = [], o = []]: any = []) => {
182
- const selectd: any = o?.filter((item: any) => v?.some((i: any) => `${i}` === `${item.value}`))?.map((item: any) => item.label) || [];
212
+ const processedValue = processInput(v);
213
+
214
+ // 如果过滤后的值与原始值不同,需要更新modelValue
215
+ const originalProcessedValue = v ? (typeof v === 'string' ?
216
+ (v.trim() ? (v.includes(',') ? v.split(',').map((s: string) => s.trim()) : [v]) : []) :
217
+ (v || [])) : [];
218
+
219
+ if (originalProcessedValue.length !== processedValue.length) {
220
+ // 异步更新以避免递归
221
+ nextTick(() => {
222
+ emits('update:modelValue', convertOutput(processedValue));
223
+ });
224
+ }
225
+
226
+ const selectd: any = o?.filter((item: any) => processedValue?.some((i: any) => `${i}` === `${item.value}`))?.map((item: any) => item.label) || [];
183
227
  state.value = selectd?.join(' Ι ') || '';
184
228
  },
185
229
  { deep: true, immediate: true },
186
230
  );
187
231
 
232
+ // 输出时类型转换
233
+ const convertOutput = (arrayValue: any[]) => {
234
+ return state.originalType === 'string' ? arrayValue.join(',') : arrayValue;
235
+ };
236
+
188
237
  // 更新
189
238
  const change = (item: any) => {
190
- emits('update:modelValue', item);
239
+ emits('update:modelValue', convertOutput(item));
191
240
  };
192
241
  // 选中
193
242
  const select = (item: any) => {
194
- const s = [...props.modelValue];
243
+ const s = [...state.processedValue];
195
244
  // s?.indexOf(item) !== -1 ? s?.splice(s?.indexOf(item), 1) : s?.push(item);
196
245
  if (s?.indexOf(item) !== -1) {
197
246
  s?.splice(s?.indexOf(item), 1);
@@ -202,7 +251,9 @@
202
251
  };
203
252
  // 全选
204
253
  const allselect = () => {
205
- const s = state.all ? [] : state.filter?.map((item: any) => item.value);
254
+ // 只选择可用的选项,排除禁用项
255
+ const availableItems = state.filter?.filter((item: any) => !item.disabled);
256
+ const s = state.all ? [] : availableItems?.map((item: any) => item.value);
206
257
  change(s);
207
258
  };
208
259
  // 清除
@@ -3,7 +3,7 @@
3
3
  <IFollowView ref="ifollow" tipBoxClass="ny-sm nx-no" 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
- v-model="state.selectd.label"
6
+ v-model="state.displayLabel"
7
7
  :interacted="interacted"
8
8
  >
9
9
  <template #suffix>
@@ -21,21 +21,37 @@
21
21
  </div>
22
22
  <ul v-if="state.filter?.length" ui-scroll=":y s" class="nx-sm lh-sl" style="min-width: 100%; max-height: 16em">
23
23
  <template v-for="(item, idx) of state.filter" :key="idx">
24
- <li v-if="item.disabled" class="ux-hover nx-sm ny-ss r-ss o-ms nowrap" :class="item.value === state.selectd?.value ? 'co-main' : ''">
24
+ <li v-if="item.disabled" class="ux-hover nx-sm ny-ss r-ss o-ms nowrap" :class="isItemSelected(item) ? 'co-main' : ''">
25
25
  <del>{{ item.label || item }}</del>
26
26
  </li>
27
- <li v-else class="ux-hover nx-sm ny-ss r-ss nowrap" :class="item.value === state.selectd?.value ? 'co-main' : ''" @click="select(item)">
27
+ <li v-else class="ux-hover nx-sm ny-ss r-ss nowrap" :class="isItemSelected(item) ? 'co-main' : ''" @click="select(item)">
28
28
  <span>{{ item.label || item }}</span>
29
29
  </li>
30
30
  </template>
31
31
  </ul>
32
32
  <div v-else class="w-full ny-ms" ui-flex="col cm" v-html="spare"></div>
33
+ <!-- 其他选项输入框 -->
34
+ <div v-if="other && state.isInputting" class="nx-sm ny-sm">
35
+ <div class="o-mm" ui-line="@a"></div>
36
+ <div class="ny-sm">
37
+ <Input
38
+ v-model="customInputValue"
39
+ v-bind="other.config || {}"
40
+ :placeholder="other.config?.placeholder || '请输入自定义内容'"
41
+ sz="s"
42
+ @keyup.enter="confirmCustomInput"
43
+ @blur="confirmCustomInput"
44
+ ref="customInputRef"
45
+ />
46
+ </div>
47
+ </div>
48
+
33
49
  <ul class="nx-sm lh-ml" v-if="tools">
34
50
  <li class="my-ss"><div class="o-mm" ui-line="@a"></div></li>
35
51
  <li ui-flex="row xm">
36
52
  <div class="co-main">
37
53
  <p class="nx-sm r-ss nowrap">
38
- <span>{{ state.filter?.length || 0 }}个可选项</span>
54
+ <span>{{ state.availableCount }}个可选项</span>
39
55
  </p>
40
56
  </div>
41
57
  <div>
@@ -49,12 +65,14 @@
49
65
  </template>
50
66
 
51
67
  <script setup lang="ts">
52
- import { reactive, ref, computed } from 'vue';
68
+ import { reactive, ref, computed, watch, nextTick } from 'vue';
53
69
  import { Input } from '.';
54
70
  import { IFollowView } from '../basic';
55
71
  const ifollow: any = ref(null);
72
+ const customInputRef: any = ref(null);
73
+
74
+ const emits = defineEmits(['update:modelValue', 'change', 'onshow', 'onhide', 'update:other']);
56
75
 
57
- const emits = defineEmits(['update:modelValue', 'change', 'onshow', 'onhide']);
58
76
 
59
77
  /**
60
78
  * 入参说明
@@ -89,8 +107,15 @@
89
107
 
90
108
  // 类型
91
109
  interface Props {
92
- modelValue?: string | number;
110
+ modelValue?: string | number | null;
93
111
  options: Array<object>;
112
+ all?: { label: string; value: null | '*' };
113
+ other?: {
114
+ value: string;
115
+ field?: string;
116
+ config?: object;
117
+ };
118
+ otherValue?: string;
94
119
  tools?: boolean;
95
120
  spare?: string;
96
121
  search?: boolean;
@@ -122,6 +147,9 @@
122
147
  const props: any = withDefaults(defineProps<Props>(), {
123
148
  modelValue: '',
124
149
  options: () => [],
150
+ all: undefined,
151
+ other: undefined,
152
+ otherValue: '',
125
153
  tools: false,
126
154
  spare: '当前没有可选项',
127
155
  search: false,
@@ -149,35 +177,211 @@
149
177
  interacted: false,
150
178
  });
151
179
 
152
- const change = ({ value = null }: any = {}) => {
180
+ const change = (value: any = null) => {
153
181
  emits('update:modelValue', value);
154
182
  emits('change', value);
155
183
  };
156
184
 
185
+ // 自定义输入值
186
+ const customInputValue = ref('');
187
+
157
188
  const state = reactive({
158
189
  show: 0,
159
190
  search: '',
191
+ isInputting: computed(() => {
192
+ // 如果配置了"其他"选项且当前选中的是"其他"
193
+ if (props.other) {
194
+ if (props.other.field) {
195
+ // 情况2:有独立字段,当主值匹配other.value且有otherValue时显示输入框
196
+ return props.modelValue === props.other.value && props.otherValue;
197
+ } else {
198
+ // 情况1:无独立字段,当值不在枚举中时显示输入框
199
+ if (props.modelValue && props.modelValue !== '') {
200
+ const isInEnum = props.options?.some((option: any) => `${option.value}` === `${props.modelValue}`);
201
+ return !isInEnum;
202
+ }
203
+ }
204
+ }
205
+ return false;
206
+ }),
160
207
  selectd: computed(() => {
161
- return props?.options?.find((option: any) => `${option.value}` === `${props.modelValue}`) || {};
208
+ // 优先查找 all 选项
209
+ if (props.all && props.modelValue === props.all.value) {
210
+ return props.all;
211
+ }
212
+ // 查找普通选项
213
+ const found = props?.options?.find((option: any) => `${option.value}` === `${props.modelValue}`);
214
+ if (found) {
215
+ // 如果是分字段存储模式且选中的是other选项,需要显示自定义值
216
+ if (props.other?.field && found.value === props.other.value && props.otherValue) {
217
+ const otherLabel = found.label || '其他';
218
+ return {
219
+ label: `${otherLabel}-${props.otherValue}`,
220
+ value: found.value
221
+ };
222
+ }
223
+ return found;
224
+ }
225
+
226
+ // 如果配置了 other 且当前值不在枚举中,可能是自定义输入值(直接存储模式)
227
+ if (props.other && props.modelValue && !props.other.field) {
228
+ // 找到其他选项的label
229
+ const otherOption = props?.options?.find((option: any) => `${option.value}` === `${props.other.value}`);
230
+ const otherLabel = otherOption?.label || '其他';
231
+ return {
232
+ label: `${otherLabel}-${props.modelValue}`,
233
+ value: props.modelValue
234
+ };
235
+ }
236
+
237
+ return {};
238
+ }),
239
+ displayLabel: computed(() => {
240
+ return state.selectd.label || '';
241
+ }),
242
+ availableCount: computed(() => {
243
+ return state.filter?.filter((item: any) => !item.disabled)?.length || 0;
162
244
  }),
163
245
  filter: computed(() => {
246
+ let filteredOptions = props.options || [];
247
+
164
248
  if (state.search) {
165
- return props.options?.filter((item: any) => {
249
+ filteredOptions = filteredOptions.filter((item: any) => {
166
250
  return item.label?.includes(state.search);
167
251
  });
168
- } else {
169
- return props.options;
170
252
  }
253
+
254
+ // 在顶部添加全部选项
255
+ if (props.all) {
256
+ filteredOptions = [props.all, ...filteredOptions];
257
+ }
258
+
259
+ return filteredOptions;
171
260
  }),
172
261
  });
262
+
263
+ // 同步自定义输入值
264
+ watch(
265
+ [() => props.modelValue, () => props.otherValue, () => props.other, () => props.options],
266
+ () => {
267
+ if (props.other) {
268
+ if (props.other.field) {
269
+ // 情况2:有独立字段,显示otherValue
270
+ customInputValue.value = props.otherValue || '';
271
+ } else {
272
+ // 情况1:无独立字段,显示当前modelValue(如果不在枚举中)
273
+ if (props.modelValue && props.modelValue !== '') {
274
+ const isInEnum = props.options?.some((option: any) => `${option.value}` === `${props.modelValue}`);
275
+ customInputValue.value = !isInEnum ? props.modelValue : '';
276
+ } else {
277
+ customInputValue.value = '';
278
+ }
279
+ }
280
+ } else {
281
+ customInputValue.value = '';
282
+ }
283
+ },
284
+ { immediate: true }
285
+ );
286
+
287
+ // 监听并验证modelValue的有效性
288
+ watch(
289
+ [() => props.modelValue, () => props.options, () => props.all, () => props.other],
290
+ ([value, options, allOption, otherConfig]) => {
291
+ // 如果配置了 other,则允许任意值(可能是用户自定义输入)
292
+ if (otherConfig) {
293
+ return;
294
+ }
295
+
296
+ // 如果有值但不在有效选项中,清空该值
297
+ if (value != null && value !== '' && options?.length > 0) {
298
+ let isValid = false;
299
+
300
+ // 检查是否是 all 选项
301
+ if (allOption && value === allOption.value) {
302
+ isValid = true;
303
+ } else {
304
+ // 检查是否在普通选项中
305
+ isValid = options.some((option: any) => `${option.value}` === `${value}`);
306
+ }
307
+
308
+ if (!isValid) {
309
+ nextTick(() => {
310
+ change(null);
311
+ });
312
+ }
313
+ }
314
+ },
315
+ { immediate: true }
316
+ );
317
+
173
318
  // 选择
174
319
  const select = (item: any = {}) => {
175
- change(item);
176
- ifollow.value.cancel();
320
+ if (props.other && item.value === props.other.value) {
321
+ // 选中"其他"选项时,设置值并聚焦输入框
322
+ if (props.other.field) {
323
+ change(props.other.value);
324
+ emits('update:other', '');
325
+ } else {
326
+ change('');
327
+ }
328
+ customInputValue.value = '';
329
+ nextTick(() => {
330
+ customInputRef.value?.focus();
331
+ });
332
+ } else {
333
+ change(item.value);
334
+ ifollow.value.cancel();
335
+ }
336
+ };
337
+
338
+ // 确认自定义输入
339
+ const confirmCustomInput = () => {
340
+ if (customInputValue.value.trim()) {
341
+ if (props.other?.field) {
342
+ // 如果指定了 field,则主字段存储选项值,field 存储自定义输入
343
+ change(props.other.value);
344
+ emits('update:other', customInputValue.value.trim());
345
+ } else {
346
+ // 如果没有指定 field,则直接在主字段存储自定义输入
347
+ change(customInputValue.value.trim());
348
+ }
349
+ ifollow.value.cancel();
350
+ } else {
351
+ // 如果输入为空,清空值
352
+ change(null);
353
+ if (props.other?.field) {
354
+ emits('update:other', '');
355
+ }
356
+ }
357
+ };
358
+
359
+ // 判断选项是否被选中(包括"其他"选项的特殊逻辑)
360
+ const isItemSelected = (item: any) => {
361
+ // 普通选项的判断
362
+ if (item.value === state.selectd?.value) {
363
+ return true;
364
+ }
365
+
366
+ // "其他"选项的特殊判断
367
+ if (props.other && item.value === props.other.value) {
368
+ if (props.other.field) {
369
+ // 情况2:有独立字段,当主值匹配other.value时高亮
370
+ return props.modelValue === props.other.value;
371
+ } else {
372
+ // 情况1:无独立字段,当值不在枚举中时高亮
373
+ if (props.modelValue && props.modelValue !== '') {
374
+ const isInEnum = props.options?.some((option: any) => `${option.value}` === `${props.modelValue}`);
375
+ return !isInEnum;
376
+ }
377
+ }
378
+ }
379
+
380
+ return false;
177
381
  };
178
382
  // 清空
179
383
  const remove = () => {
180
- change({});
384
+ change(null);
181
385
  ifollow.value.cancel();
182
386
  };
183
387
  // 取消