@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.
- package/package.json +2 -2
- package/src/components/basic/SideMenu.vue +480 -0
- package/src/components/basic/SideView.vue +488 -0
- package/src/components/basic/index.ts +4 -1
- package/src/components/form/Cascader.vue +2 -2
- package/src/components/form/MoreSelect.vue +3 -3
- package/src/components/form/PageMoreSelect.vue +2 -2
- package/src/components/form/PageSelect.vue +1 -1
- package/src/components/form/SearchMoreSelect.vue +2 -2
- package/src/components/form/SearchSelect.vue +1 -1
- package/src/components/form/Select.vue +2 -2
- package/src/components/form/Upload.vue +103 -35
- package/src/components/form/MoreSelectPanel.vue +0 -190
|
@@ -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="
|
|
5
|
-
<div class="pa full ot-no ol-no bg-fore
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
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-
|
|
166
|
-
:class="
|
|
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="
|
|
172
|
-
<div ui-flex="row cm" :class="
|
|
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:
|
|
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
|
-
|
|
456
|
+
const hit = state.files[fileIndex];
|
|
457
|
+
if (hit) {
|
|
389
458
|
if (file.status === 'yes') {
|
|
390
459
|
// yes 状态 3 秒后变为 end
|
|
391
|
-
|
|
392
|
-
clearFileTimer(
|
|
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 (
|
|
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
|
|
542
|
-
if (
|
|
609
|
+
const hit = state.files.find((f) => f.path === uploadFile.path);
|
|
610
|
+
if (hit) {
|
|
543
611
|
if (result) {
|
|
544
612
|
// 直接保存上传函数的返回值
|
|
545
|
-
|
|
546
|
-
|
|
613
|
+
hit.result = result;
|
|
614
|
+
hit.status = 'yes';
|
|
547
615
|
// 设置3秒后转换为end状态的定时器
|
|
548
|
-
setStatusTransitionTimer(
|
|
616
|
+
setStatusTransitionTimer(hit);
|
|
549
617
|
// 每个文件完成后更新外部数据源
|
|
550
618
|
emitUpdateEvents();
|
|
551
619
|
} else {
|
|
552
620
|
// 上传失败时先显示错误状态,3秒后移除文件
|
|
553
|
-
|
|
621
|
+
hit.status = 'err';
|
|
554
622
|
// 设置3秒后删除文件的定时器
|
|
555
|
-
setStatusTransitionTimer(
|
|
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
|
|
565
|
-
if (
|
|
566
|
-
|
|
632
|
+
const hit = state.files.find((f) => f.path === uploadFile.path);
|
|
633
|
+
if (hit) {
|
|
634
|
+
hit.status = 'err';
|
|
567
635
|
// 设置3秒后删除文件的定时器
|
|
568
|
-
setStatusTransitionTimer(
|
|
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: '
|
|
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 || '
|
|
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: '
|
|
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 || '
|
|
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
|
-
. .
|