FileSelector 文件选择
文件选择器组件,用于选择文件。支持多选、拖拽上传等功能。
组件源码
vue
<script lang="ts" setup>
import { ref, watch } from 'vue';
interface IFileSelectorProps {
/**
* 是否启用上传目录功能,优先级最高,默认false(设置为true后,accept、limit、drag将不生效)
*/
webkitdirectory?: boolean;
/**
* @description 唯一文件类型说明符,请参考w3c。默认不限制文件类型。
*/
accept?: string;
/**
* @description 允许选择的文件个数,默认1.
*/
limit?: number;
/**
* @description 是否启用拖拽获取文件,默认false。
*/
drag?: boolean;
/**
* @description 超出限制的文件数时,是否允许继续选择文件并替换旧文件。默认false。
*/
excessReplace?: boolean;
/**
* @description 单个文件最大字节
*/
size?: number;
/**
* @description 是否禁用选择,默认false。
*/
disabled?: boolean;
}
const $props = withDefaults(defineProps<IFileSelectorProps>(), {
webkitdirectory: false,
accept: '',
limit: 1,
drag: false,
excessReplace: false,
disabled: false
});
const $emits = defineEmits<{
'change': [files: File[]];
'exceed-size': [files: File[]];
}>();
const disabled = ref(false || $props.disabled);
watch(
() => $props.disabled,
(value) => {
disabled.value = value;
}
);
const files = ref<File[]>([]);
const addFiles = (checkedFiles: File[]) => {
if ($props.size && $props.size > 0) {
const maxFiles = checkedFiles.filter((value) => {
return value.size > $props.size!;
});
if (maxFiles.length) {
$emits('exceed-size', maxFiles);
checkedFiles = checkedFiles.filter((value) => {
return value.size <= $props.size!;
});
}
}
if (!checkedFiles.length) {
return;
}
const strategies = {
singleFile: () => {
if (!$props.excessReplace) {
disabled.value = true;
}
files.value = checkedFiles;
$emits('change', files.value);
},
multipleFile: () => {
// 多选未限制个数
if ($props.limit === Infinity) {
files.value.push(...checkedFiles);
$emits('change', files.value);
return;
}
// 多选,限制了个数
const total = files.value.length + checkedFiles.length;
if (total >= $props.limit && !$props.excessReplace) {
disabled.value = true;
}
const allFiles = [...files.value, ...checkedFiles];
if ($props.excessReplace) {
allFiles.splice(0, total - $props.limit);
} else {
allFiles.splice($props.limit);
}
files.value = allFiles;
$emits('change', files.value);
},
folder: () => {
disabled.value = true;
files.value = checkedFiles;
$emits('change', files.value);
}
};
if ($props.webkitdirectory) {
strategies.folder();
return;
}
if (!$props.limit || $props.limit === 1) {
strategies.singleFile();
return;
}
strategies.multipleFile();
};
const inputElement = ref<HTMLInputElement | null>(null);
const onOpenFolder = () => {
if (!inputElement.value || disabled.value) {
return;
}
inputElement.value.click();
};
const clearFiles = () => {
disabled.value = false;
files.value = [];
// 解决删除文件后,再上传相同文件失败的错误
(inputElement.value as HTMLInputElement).value = '';
};
const deleteFile = (index: number) => {
if (!Number.isInteger(index) || index >= files.value.length) {
return;
}
if (files.value.length === 1) {
clearFiles();
return;
}
if (disabled.value) {
disabled.value = false;
}
files.value.splice(index, 1);
};
const onChangeFile = (e: Event) => {
const checkedFiles = (e.target as HTMLInputElement).files;
if (!checkedFiles) {
return;
}
addFiles(Array.from(checkedFiles));
};
const onDragFile = (e: DragEvent) => {
const checkedFiles = e.dataTransfer?.files;
if (!checkedFiles) {
return;
}
addFiles(Array.from(checkedFiles));
};
defineExpose({
onOpenFolder,
deleteFile,
clearFiles
});
</script>
<template>
<input
ref="inputElement"
type="file"
name="files"
title="upload"
:webkitdirectory="webkitdirectory"
:multiple="limit !== undefined && limit > 1"
:accept="accept"
class="file_input"
@change="onChangeFile" />
<div
class="file_btn"
:class="disabled ? 'file_btn--stop' : ''"
@click="onOpenFolder"
v-if="!drag || webkitdirectory">
<slot :disabled="disabled">
<div class="default_btn">选择文件</div>
</slot>
</div>
<div
class="file_btn"
:class="disabled ? 'file_btn--stop' : ''"
@click="onOpenFolder"
@dragenter.prevent
@dragover.prevent
@drop.prevent="onDragFile"
v-if="drag && !webkitdirectory">
<slot :disabled="disabled">
<div class="default_btn">选择文件</div>
</slot>
</div>
</template>
<style lang="scss" scoped>
.file_input {
display: none !important;
}
.file_btn {
display: inline-block;
cursor: pointer;
}
.file_btn--stop:hover {
cursor: not-allowed;
}
.default_btn {
width: 80px;
height: 32px;
text-align: center;
line-height: 32px;
border: 1px solid #999;
}
</style>基本用法
选择文件
选择图片
选择两张图片
选择 1M 以内的图片
示例代码
vue
<script lang="ts" setup>
import FileSelector from '@/FileSelector/FileSelector.vue';
</script>
<template>
<div class="flex">
<FileSelector />
<div class="ml-20">
<FileSelector accept="image/*">
<div class="upload">选择图片</div>
</FileSelector>
</div>
<div class="ml-20">
<FileSelector accept="image/*" :limit="2">
<div class="upload">选择两张图片</div>
</FileSelector>
</div>
<div class="ml-20">
<FileSelector accept="image/*" :size="1024 * 1024">
<div class="upload">选择 1M 以内的图片</div>
</FileSelector>
</div>
</div>
</template>
<style lang="scss" scoped>
.flex {
display: flex;
}
.ml-20 {
margin-left: 20px;
}
.upload {
padding: 5px 10px;
background-color: #409eff;
color: #fff;
border-radius: 4px;
}
</style>API
Props
| 参数名 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| limit | 文件数量限制 | number | 1 |
| accept | 接受上传的文件类型 | string | - |
| size | 文件大小限制,单位为字节 | number | - |
| drag | 是否允许拖拽上传 | boolean | false |
| excessReplace | 是否用新文件替换旧文件 | boolean | false |
| disabled | 是否禁用 | boolean | false |
| webkitdirectory | 是否支持文件夹上传 (设置为true后,accept、limit、drag将不生效) | boolean | false |
Events
| 事件名 | 说明 | 参数 |
|---|---|---|
| change | 文件选择变化时触发 | files: File[] |
| exceed-size | 文件大小超出限制时触发 | files: File[] |
Slots
| 插槽名 | 说明 | 参数 |
|---|---|---|
| default | 自定义内容 | { disabled: boolean } |