@neatui/vue 5.6.0 → 5.7.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.
@@ -1,10 +1,10 @@
1
1
  <template>
2
2
  <div ui-flex="row lm :wrap" class="neatui-upload mb-sm-sub mr-sm-sub" v-bind="{ ...attrs }">
3
3
  <div v-for="(file, idx) in state.files" :key="idx" ui-flex="col cm" class="flex-fixed pr" :style="`width: ${isNumber(width) ? width + 'em' : width}`" v-bind="itemAttrs">
4
- <div class="w-full pr" :style="`box-sizing: content-box; padding-top: ${ratio}%`">
5
- <div class="pa full ot-no ol-no bg-fore n-sm b-solid b-case b-xs r-sm" ui-flex="col cm">
4
+ <div class="w-full pr" :style="boxStyle">
5
+ <div :class="`${ratio ? 'pa full n-sm' : 'w-full n-ms min-h-ms*s'} ot-no ol-no bg-fore b-solid b-case b-xs r-sm ${canOpen(file) ? 'ux-none' : ''}`" ui-flex="col cm" @click="openFile(file)">
6
6
  <!-- 删除按钮 -->
7
- <div v-if="!disabled" class="pa w-sl h-sl ot-ss or-ss bg-fore co-risk lh-xs r-xl sub-hover z-ls" ui-flex="row cm" @click="remove(idx)">
7
+ <div v-if="!disabled" class="pa w-sl h-sl ot-ss or-ss bg-fore co-risk lh-xs r-xl sub-hover z-ls ux-none" ui-flex="row cm" @click.stop="remove(idx)">
8
8
  <svg class="block" style="width: 1.2em; height: 1.2em" viewBox="0 0 1024 1024">
9
9
  <path
10
10
  d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm165.4 618.2l-66 0.3L512 563.4l-99.3 118.4-66.1-0.3c-4.4 0-8-3.5-8-8 0-1.9 0.7-3.7 1.9-5.2l130.1-155L340.5 359c-1.2-1.5-1.9-3.3-1.9-5.2 0-4.4 3.6-8 8-8l66.1 0.3L512 464.6l99.3-118.4 66-0.3c4.4 0 8 3.5 8 8 0 1.9-0.7 3.7-1.9 5.2L553.5 514l130 155c1.2 1.5 1.9 3.3 1.9 5.2 0 4.4-3.6 8-8 8z"
@@ -17,7 +17,7 @@
17
17
  <img v-if="file.type === 'image'" class="r-ss" :src="getImageUrl(file)" alt="" />
18
18
  <video v-else-if="file.type === 'video'" class="full r-ss" :src="getImageUrl(file)" controls muted></video>
19
19
  <template v-else-if="file.type === 'pdf'">
20
- <svg t="1705645350800" class="w-ls h-ls" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1913">
20
+ <svg t="1705645350800" :class="iconClass" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1913">
21
21
  <path
22
22
  d="M205.5 64H665l232 232v639c0 13.807-11.193 25-25 25H205.5c-13.807 0-25-11.193-25-25V89c0-13.807 11.193-25 25-25z m449.145 25H205.5v846H872V306.355L654.645 89z"
23
23
  fill="#B7B7BD"
@@ -38,7 +38,7 @@
38
38
  </svg>
39
39
  </template>
40
40
  <template v-else-if="file.type === 'xlsx'">
41
- <svg t="1705645380770" class="w-ls h-ls" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2060">
41
+ <svg t="1705645380770" :class="iconClass" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2060">
42
42
  <path
43
43
  d="M205.799024 64.936585H664.850732l231.773658 231.773659v638.376585c0 13.79353-11.18208 24.97561-24.97561 24.97561H205.799024c-13.79353 0-24.97561-11.18208-24.975609-24.97561V89.912195c0-13.79353 11.18208-24.97561 24.975609-24.97561z m448.70681 24.97561H205.799024v845.174634H871.64878V307.055141L654.505834 89.912195z"
44
44
  fill="#B7B7BD"
@@ -87,7 +87,7 @@
87
87
  </svg>
88
88
  </template>
89
89
  <template v-else-if="file.type === 'docx'">
90
- <svg t="1705645401993" class="w-ls h-ls" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2207">
90
+ <svg t="1705645401993" :class="iconClass" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2207">
91
91
  <path
92
92
  d="M205.5 64H665l232 232v639c0 13.807-11.193 25-25 25H205.5c-13.807 0-25-11.193-25-25V89c0-13.807 11.193-25 25-25z m449.145 25H205.5v846H872V306.355L654.645 89z"
93
93
  fill="#B7B7BD"
@@ -108,7 +108,7 @@
108
108
  </svg>
109
109
  </template>
110
110
  <template v-else-if="file.type === 'pptx'">
111
- <svg t="1705645414803" class="w-ls h-ls" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2354">
111
+ <svg t="1705645414803" :class="iconClass" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2354">
112
112
  <path
113
113
  d="M205.5 64H665l232 232v639c0 13.807-11.193 25-25 25H205.5c-13.807 0-25-11.193-25-25V89c0-13.807 11.193-25 25-25z m449.145 25H205.5v846H872V306.355L654.645 89z"
114
114
  fill="#B7B7BD"
@@ -129,7 +129,7 @@
129
129
  </svg>
130
130
  </template>
131
131
  <template v-else>
132
- <svg t="1705645436666" class="w-ls h-ls" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2501">
132
+ <svg t="1705645436666" :class="iconClass" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2501">
133
133
  <path
134
134
  d="M205.5 64H665l232 232v639c0 13.807-11.193 25-25 25H205.5c-13.807 0-25-11.193-25-25V89c0-13.807 11.193-25 25-25z m449.145 25H205.5v846H872V306.355L654.645 89z"
135
135
  fill="#B7B7BD"
@@ -150,6 +150,9 @@
150
150
  </svg>
151
151
  </template>
152
152
 
153
+ <p v-if="file.name && file.type !== 'image' && file.type !== 'video'" class="w-full mt-ss ac" ui-omit="1" :title="file.name">{{ file.name }}</p>
154
+ <p v-if="viewText || text" class="w-full co-note fs-sm ac" ui-omit="1">{{ viewText || text }}</p>
155
+
153
156
  <!-- 上传状态 -->
154
157
  <div class="upload-status" :data-status="file.status">
155
158
  <div class="neatui-upload-icon"><i></i></div>
@@ -162,25 +165,26 @@
162
165
  <template v-if="showUploadButton">
163
166
  <slot name="btn" :disabled="disabled">
164
167
  <label
165
- class="pr flex-block"
166
- :class="{ 'ux-disable': disabled }"
168
+ class="pr flex-fixed"
169
+ :class="disabled ? 'ux-disable' : 'ux-none'"
167
170
  :ui-form="`${theme} type:upload tips:hover`"
168
171
  :style="`width: ${isNumber(width) ? width + 'em' : width}`"
169
172
  v-bind="itemAttrs"
170
173
  >
171
- <div class="w-full pr" :style="`box-sizing: content-box; padding-top: ${ratio}%`">
172
- <div ui-flex="row cm" :class="`pa ot-no ol-no full upload n-sl bg-fore b-solid b-case b-xs r-sm ${uploadClass}`" :style="uploadStyle">
174
+ <div class="w-full pr" :style="boxStyle">
175
+ <div ui-flex="row cm" :class="`${ratio ? 'pa full n-sl' : 'w-full n-ms min-h-ms*s'} ot-no ol-no upload bg-fore b-solid b-case b-xs r-sm ${uploadClass}`" :style="uploadStyle">
173
176
  <div v-if="tips" ui-form-tips>
174
177
  {{ tips }}
175
178
  </div>
176
179
  <input style="display: none" type="file" class="pa input" :multiple="!max || max > 1" :accept="accept" :disabled="disabled" @change="change" />
177
- <div class="co-note ac input-icon">
180
+ <div class="co-note ac input-icon" ui-flex="col cm">
178
181
  <svg t="1705642180833" style="width: 1em; height: 1em" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4215">
179
182
  <path
180
183
  d="M906.212134 565.732986 565.732986 565.732986 565.732986 906.212134C565.732986 926.013685 541.666486 959.972 511.97312 959.972 482.297674 959.972 458.213254 926.013685 458.213254 906.212134L458.213254 565.732986 117.734106 565.732986C97.950475 565.732986 63.97424 541.666486 63.97424 511.97312 63.97424 482.279754 97.950475 458.213254 117.734106 458.213254L458.213254 458.213254 458.213254 117.734106C458.213254 97.950475 482.297674 63.97424 511.97312 63.97424 541.666486 63.97424 565.732986 97.950475 565.732986 117.734106L565.732986 458.213254 906.212134 458.213254C925.995765 458.213254 959.972 482.279754 959.972 511.97312 959.972 541.666486 925.995765 565.732986 906.212134 565.732986Z"
181
184
  p-id="4216"
182
185
  ></path>
183
186
  </svg>
187
+ <p v-if="text" class="fs-sm mt-ss">{{ text }}</p>
184
188
  </div>
185
189
  </div>
186
190
  </div>
@@ -202,13 +206,22 @@
202
206
  uploadClass?: string;
203
207
  uploadStyle?: string;
204
208
  tips?: string;
209
+ /** 未上传时,+ 号下方的文字 */
210
+ text?: string;
211
+ /** 已上传时文件名下方的文字;不传则仍用 text */
212
+ viewText?: string;
213
+ /** 宽高比(高 / 宽 %);0 = 不定比例,高度随内容 */
214
+ ratio?: number;
215
+ /** 已上传时点整块打开文件(PDF、图片、视频新窗口看,其它下载);传 false 关掉 */
216
+ view?: boolean;
217
+ /** 接管「打开文件」:传了就不再新窗口打开 / 下载,交给它(如加展示水印预览) */
218
+ preview?: (file: { url: string; name: string; type: string }) => void;
205
219
  accept?: string | 'image/*' | 'video/*' | 'audio/*';
206
220
  upload?: (formData: FormData, fileExtension?: string) => Promise<any>;
207
221
  maxSize?: number;
208
222
  compress?: boolean;
209
223
  disabled?: boolean;
210
224
  width?: number | string;
211
- ratio?: number;
212
225
  theme?: string;
213
226
  }
214
227
 
@@ -219,9 +232,12 @@
219
232
  attrs: () => ({}),
220
233
  itemAttrs: () => ({}),
221
234
  tips: '',
235
+ text: '',
236
+ viewText: '',
237
+ view: true,
222
238
  accept: '*',
223
239
  upload: async () => null,
224
- maxSize: 10,
240
+ maxSize: 500,
225
241
  compress: true,
226
242
  max: 0,
227
243
  disabled: false,
@@ -230,6 +246,10 @@
230
246
  theme: '@a',
231
247
  });
232
248
 
249
+ /** 定比例:外层 padding-top 撑高、内层铺满;不定比例:高度随内容 */
250
+ const boxStyle = computed(() => (props.ratio ? `box-sizing: content-box; padding-top: ${props.ratio}%` : ''));
251
+ const iconClass = computed(() => (props.ratio ? 'w-ls h-ls' : 'w-lm h-lm'));
252
+
233
253
  interface UploadFile {
234
254
  name: string;
235
255
  type: string;
@@ -265,6 +285,51 @@
265
285
  return file.path;
266
286
  };
267
287
 
288
+ // 已上传的文件整块打开;上传中、失败不打开
289
+ const canOpen = (file: UploadFile) => props.view && (file.status === 'yes' || file.status === 'end') && !!getImageUrl(file);
290
+ // 浏览器能直接看的新窗口打开,Word、Excel 等下载
291
+ const VIEWABLE = ['pdf', 'image', 'video'];
292
+ const fileNameOf = (file: UploadFile, url: string) => file.name || decodeURIComponent(url.split(/[?#]/)[0]?.split('/').pop() || '') || 'file';
293
+ // data URL(mock)不能直接顶层打开,转成 blob URL
294
+ const dataToBlobUrl = (url: string) => {
295
+ const [head = '', body = ''] = url.split(',');
296
+ const mime = head.match(/^data:([^;,]+)/)?.[1] || 'application/octet-stream';
297
+ const bin = /;base64/i.test(head) ? atob(body) : decodeURIComponent(body);
298
+ const bytes = new Uint8Array(bin.length);
299
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
300
+ return URL.createObjectURL(new Blob([bytes], { type: mime }));
301
+ };
302
+ const download = async (url: string, name: string) => {
303
+ let href = url;
304
+ try {
305
+ // 跨域地址 a[download] 不生效,先取成 blob 才能按原文件名存
306
+ if (!/^(blob|data):/i.test(url)) {
307
+ const res = await fetch(url);
308
+ // 403/404/500 不会抛异常,不检查会把错误页按原文件名存下来
309
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
310
+ href = URL.createObjectURL(await res.blob());
311
+ }
312
+ } catch {
313
+ // 失败交给浏览器直接打开,由服务端显示真实错误
314
+ window.open(url, '_blank', 'noopener');
315
+ return;
316
+ }
317
+ const a = document.createElement('a');
318
+ a.href = href;
319
+ a.download = name;
320
+ document.body.appendChild(a);
321
+ a.click();
322
+ a.remove();
323
+ if (href !== url) setTimeout(() => URL.revokeObjectURL(href), 1000);
324
+ };
325
+ const openFile = (file: UploadFile) => {
326
+ if (!canOpen(file)) return;
327
+ const url = getImageUrl(file);
328
+ if (props.preview) return props.preview({ url, name: fileNameOf(file, url), type: file.type });
329
+ if (!VIEWABLE.includes(file.type)) return download(url, fileNameOf(file, url));
330
+ window.open(/^data:/i.test(url) ? dataToBlobUrl(url) : url, '_blank', 'noopener');
331
+ };
332
+
268
333
  // 计算属性
269
334
  const showUploadButton = computed(() => {
270
335
  const currentCount = state.files.length;
@@ -338,6 +403,9 @@
338
403
  if (props.accept && props.accept !== '*') {
339
404
  const acceptTypes = props.accept.split(',').map((type) => type.trim());
340
405
  const isValidType = acceptTypes.some((type) => {
406
+ if (type.startsWith('.')) {
407
+ return file.name.toLowerCase().endsWith(type.toLowerCase());
408
+ }
341
409
  if (type.endsWith('/*')) {
342
410
  return file.type.startsWith(type.replace('/*', ''));
343
411
  }
@@ -385,11 +453,12 @@
385
453
  clearFileTimer(file);
386
454
  file.timer = window.setTimeout(() => {
387
455
  const fileIndex = state.files.findIndex((f) => f.path === file.path);
388
- if (fileIndex !== -1) {
456
+ const hit = state.files[fileIndex];
457
+ if (hit) {
389
458
  if (file.status === 'yes') {
390
459
  // yes 状态 3 秒后变为 end
391
- state.files[fileIndex].status = 'end';
392
- clearFileTimer(state.files[fileIndex]);
460
+ hit.status = 'end';
461
+ clearFileTimer(hit);
393
462
  } else if (file.status === 'err') {
394
463
  // err 状态 3 秒后清除占位(删除文件)
395
464
  removeFile(fileIndex, true);
@@ -476,8 +545,7 @@
476
545
  // 步骤1:立即添加所有文件到UI,显示占位图
477
546
  const validFiles: { file: File; uploadFile: UploadFile }[] = [];
478
547
 
479
- for (let i = 0; i < selectedFiles.length; i++) {
480
- const file = selectedFiles[i];
548
+ for (const file of Array.from(selectedFiles)) {
481
549
 
482
550
  // 验证文件
483
551
  const validationError = validateFile(file);
@@ -538,21 +606,21 @@
538
606
  const result = await props.upload(formData, fileExtension);
539
607
 
540
608
  // 更新文件状态
541
- const fileIndex = state.files.findIndex((f) => f.path === uploadFile.path);
542
- if (fileIndex !== -1) {
609
+ const hit = state.files.find((f) => f.path === uploadFile.path);
610
+ if (hit) {
543
611
  if (result) {
544
612
  // 直接保存上传函数的返回值
545
- state.files[fileIndex].result = result;
546
- state.files[fileIndex].status = 'yes';
613
+ hit.result = result;
614
+ hit.status = 'yes';
547
615
  // 设置3秒后转换为end状态的定时器
548
- setStatusTransitionTimer(state.files[fileIndex]);
616
+ setStatusTransitionTimer(hit);
549
617
  // 每个文件完成后更新外部数据源
550
618
  emitUpdateEvents();
551
619
  } else {
552
620
  // 上传失败时先显示错误状态,3秒后移除文件
553
- state.files[fileIndex].status = 'err';
621
+ hit.status = 'err';
554
622
  // 设置3秒后删除文件的定时器
555
- setStatusTransitionTimer(state.files[fileIndex]);
623
+ setStatusTransitionTimer(hit);
556
624
  // 发射错误事件
557
625
  emit('error', `文件 ${file.name} 上传失败: 上传函数返回空值`);
558
626
  }
@@ -561,11 +629,11 @@
561
629
  console.error('文件处理失败:', error);
562
630
 
563
631
  // 上传失败时先显示错误状态,3秒后移除文件
564
- const fileIndex = state.files.findIndex((f) => f.path === uploadFile.path);
565
- if (fileIndex !== -1) {
566
- state.files[fileIndex].status = 'err';
632
+ const hit = state.files.find((f) => f.path === uploadFile.path);
633
+ if (hit) {
634
+ hit.status = 'err';
567
635
  // 设置3秒后删除文件的定时器
568
- setStatusTransitionTimer(state.files[fileIndex]);
636
+ setStatusTransitionTimer(hit);
569
637
  }
570
638
 
571
639
  emit('error', `文件 ${file.name} 上传失败: ${error}`);
@@ -586,7 +654,7 @@
586
654
  // 如果是字符串URL,尝试从URL判断类型
587
655
  const urlType = getFileTypeFromUrl(item);
588
656
  return {
589
- name: 'file',
657
+ name: '',
590
658
  type: urlType,
591
659
  path: item,
592
660
  status: 'end' as const,
@@ -595,7 +663,7 @@
595
663
  } else {
596
664
  // 如果是对象
597
665
  return {
598
- name: item.name || 'file',
666
+ name: item.name || '',
599
667
  type: item.type || 'file',
600
668
  path: item.url || item.path || '',
601
669
  status: 'end' as const,
@@ -607,7 +675,7 @@
607
675
  const urlType = getFileTypeFromUrl(newValue);
608
676
  state.files = [
609
677
  {
610
- name: 'file',
678
+ name: '',
611
679
  type: urlType,
612
680
  path: newValue,
613
681
  status: 'end',
@@ -620,7 +688,7 @@
620
688
  const objType = objValue.type || (objPath ? getFileTypeFromUrl(objPath) : 'file');
621
689
  state.files = [
622
690
  {
623
- name: objValue.name || 'file',
691
+ name: objValue.name || '',
624
692
  type: objType,
625
693
  path: objPath,
626
694
  status: 'end',
@@ -1,190 +0,0 @@
1
- <template>
2
- <div :ui-form="`${theme} type:select ${sz ? 'sz:' + sz : ''}`">
3
- <IPickerView ref="ipicker" tipBoxClass="ny-sm nx-no" tipBoxStyle="min-width: 100%">
4
- <slot>
5
- <Input
6
- v-bind="{ placeholder, rules, theme, sz, co, block, ready: true, type: 'text', tips, disabled, readonly, clearable: false, verify, prefix, suffix, interacted }"
7
- :clearable="false"
8
- readonly
9
- v-model="state.value"
10
- :interacted="interacted"
11
- >
12
- <template #suffix><i ui-form-select=""></i></template>
13
- </Input>
14
- </slot>
15
- <template #options>
16
- <ul v-if="props.options?.length" ui-scroll=":y s" class="nx-sm lh-ml" style="min-width: 100%; max-height: 15em">
17
- <template v-for="(item, idx) of props.options" :key="idx">
18
- <li v-if="item.disable" class="ux-hover nx-sm r-ss o-ms">
19
- <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
20
- <input type="checkbox" :name="state.name" :checked="modelValue?.some((i: any) => `${i}` === `${item.value}`)" />
21
- <del class="ml-sm">{{ item.label || item }}</del>
22
- </label>
23
- </li>
24
- <li v-else class="ux-hover nx-sm r-ss" @click.stop="select(item.value)">
25
- <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
26
- <input type="checkbox" :name="state.name" :checked="modelValue?.some((i: any) => `${i}` === `${item.value}`)" />
27
- <span class="ml-sm">{{ item.label || item }}</span>
28
- </label>
29
- </li>
30
- </template>
31
- </ul>
32
- <div v-else class="w-full ny-ms" ui-flex="col cm" v-html="spare"></div>
33
- <ul class="nx-sm lh-ml" v-if="tools">
34
- <li class="my-ss"><div class="o-mm" ui-line="@a"></div></li>
35
- <li ui-flex="row xm">
36
- <div class="ux-hover nx-sm r-ss co-main" ui-wrap="no" @click.stop="allselect()">
37
- <label style="pointer-events: none" :ui-form="`${theme} type:checkbox :block`">
38
- <input type="checkbox" :name="state.name" :checked="state.all" />
39
- <span class="ml-sm">全选({{ props.options?.length || 0 }})</span>
40
- </label>
41
- </div>
42
- <div>
43
- <button v-show="state.has" class="co-risk" ui-btn="@a none s" @click="remove">清空</button>
44
- </div>
45
- </li>
46
- </ul>
47
- </template>
48
- </IPickerView>
49
- </div>
50
- </template>
51
-
52
- <script setup lang="ts">
53
- import { reactive, ref, computed, watch } from 'vue';
54
- import Input from './Input.vue';
55
- import { IFollowView, IPickerView } from '../basic';
56
- const ipicker: any = ref(null);
57
-
58
- const emits = defineEmits(['update:modelValue']);
59
-
60
- /**
61
- * 入参说明
62
- *
63
- * modelValue 值
64
- * options 枚举列表
65
- * tools 是否显示工具栏
66
- * spare 空闲兜底内容
67
- * search 是否显示搜索框
68
- *
69
- * -----------------------------------------
70
- * theme 主题
71
- * sz 尺寸
72
- * co 颜色
73
- * block 撑满
74
- * ready 准备就绪
75
- * type 类型
76
- * tips 提示文案
77
- * placeholder 内容占位
78
- * maxlength 最大长度
79
- * minlength 最小长度
80
- * disabled 是否禁用
81
- * readonly 是否只读
82
- * clearable 是否显示清除按钮
83
- * rules 校验规则
84
- * verify 是否是示校验图标
85
- * prefix 前缀内容
86
- * suffix 后缀内容
87
- * interacted 用户是否交互
88
- */
89
-
90
- // 类型
91
- interface Props {
92
- modelValue?: Array<string | number>;
93
- options: Array<object>;
94
- tools?: boolean;
95
- spare?: string;
96
-
97
- // Input
98
- placeholder?: string;
99
- rules?: Array<object>;
100
- theme?: string;
101
- sz?: '' | 'xs' | 's' | 'm' | 'l' | 'xl';
102
- co?: string;
103
- block?: boolean;
104
- ready?: boolean;
105
- type?: string;
106
- tips?: string;
107
- maxlength?: null | number;
108
- minlength?: null | number;
109
- disabled?: boolean;
110
- readonly?: boolean;
111
- clearable?: boolean;
112
- verify?: boolean;
113
- prefix?: Array<any>;
114
- suffix?: Array<any>;
115
- interacted?: boolean;
116
- }
117
- // 入参
118
- const props: any = withDefaults(defineProps<Props>(), {
119
- modelValue: () => [],
120
- options: () => [],
121
- tools: true,
122
- spare: '当前没有可选项',
123
-
124
- // Input
125
- placeholder: '',
126
- rules: () => [],
127
- theme: '@a',
128
- sz: '',
129
- co: '',
130
- block: true,
131
- ready: false,
132
- type: 'text',
133
- tips: '',
134
- maxlength: null,
135
- minlength: null,
136
- disabled: false,
137
- readonly: false,
138
- clearable: true,
139
- verify: false,
140
- prefix: () => [],
141
- suffix: () => [],
142
- interacted: false,
143
- });
144
- // 内部数据
145
- const state: any = reactive({
146
- name: computed(() => {
147
- return Array.from({ length: 8 }, () => String.fromCharCode(97 + Math.floor(Math.random() * 26))).join('');
148
- }),
149
- show: 0,
150
- value: '',
151
- all: computed(() => {
152
- return props.options?.every((item: any) => props.modelValue?.some((i: any) => `${i}` === `${item.value}`));
153
- }),
154
- has: computed(() => {
155
- return props.options?.some((item: any) => props.modelValue?.some((i: any) => `${i}` === `${item.value}`));
156
- }),
157
- });
158
-
159
- // 监听外部数据
160
- watch(
161
- [() => props.modelValue, () => props.options],
162
- ([v = [], o = []]: any = []) => {
163
- const selectd: any = o?.filter((item: any) => v?.some((i: any) => `${i}` === `${item.value}`))?.map((item: any) => item.label) || [];
164
- state.value = selectd.join(' Ι ') || '';
165
- },
166
- { deep: true, immediate: true },
167
- );
168
-
169
- // 更新
170
- const change = (item: any) => {
171
- emits('update:modelValue', item);
172
- };
173
- // 选中
174
- const select = (item: any) => {
175
- const s = [...props.modelValue];
176
- s?.indexOf(item) !== -1 ? s?.splice(s?.indexOf(item), 1) : s?.push(item);
177
- change(s);
178
- };
179
- // 全选
180
- const allselect = () => {
181
- const s = state.all ? [] : props.options?.map((item: any) => item.value);
182
- change(s);
183
- };
184
- // 清除
185
- const remove = () => {
186
- change([]);
187
- ipicker.value.cancel();
188
- };
189
- </script>
190
- . .