@saberlayer/vue-file-uploader
Version:
A file uploader component for Vue 2.x and Vue 3.x
472 lines (460 loc) • 18.3 kB
JavaScript
import { install as install$1, isVue2 } from 'vue-demi';
import { defineComponent, ref, watch, openBlock, createElementBlock, createCommentVNode, createElementVNode, normalizeClass, withModifiers, renderSlot, Fragment, renderList, createTextVNode, toDisplayString, normalizeStyle } from 'vue';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
var script = defineComponent({
name: 'FileUploader',
// 组件属性定义
props: {
action: {
type: String,
required: true
},
multiple: {
type: Boolean,
default: false
},
accept: {
type: String,
default: ''
},
maxSize: {
type: Number,
default: 0
},
maxCount: {
type: Number,
default: 0
},
headers: {
type: Object,
default: () => ({})
},
data: {
type: Object,
default: () => ({})
},
autoUpload: {
type: Boolean,
default: true
},
fileList: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
drag: {
type: Boolean,
default: false
}
},
// 组件事件定义
emits: ['change', 'success', 'error', 'progress', 'exceed', 'update:fileList'],
setup(props, { emit, expose }) {
// 文件输入框引用
const fileInput = ref(null);
// 文件列表状态
const fileList = ref(props.fileList || []);
// 存储待上传的文件
const pendingFiles = ref(new Map());
// 拖拽状态
const isDragging = ref(false);
// 使用 watch 监听 props.fileList 的变化
watch(() => props.fileList, (newValue) => {
fileList.value = newValue || [];
});
/**
* 处理点击上传
* 触发文件选择框
*/
const handleClick = () => {
if (fileInput.value) {
fileInput.value.click();
}
};
/**
* 处理文件选择
* 当用户通过文件选择框选择文件时触发
*/
const handleChange = (event) => {
var _a;
const target = event.target;
if (!((_a = target.files) === null || _a === void 0 ? void 0 : _a.length))
return;
const files = Array.from(target.files);
handleFiles(files);
target.value = ''; // 重置input,允许选择相同文件
};
/**
* 处理文件拖拽
* 当用户拖拽文件到上传区域时触发
*/
const handleDrop = (event) => {
var _a;
const files = Array.from(((_a = event.dataTransfer) === null || _a === void 0 ? void 0 : _a.files) || []);
handleFiles(files);
};
/**
* 处理文件上传
* 验证文件并添加到上传列表
*/
const handleFiles = (files) => {
// 检查文件数量限制
if (props.maxCount && fileList.value.length + files.length > props.maxCount) {
emit('exceed', files);
return;
}
files.forEach(file => {
// 检查文件大小限制
if (props.maxSize && file.size > props.maxSize) {
const errorFile = {
uid: Date.now() + Math.random().toString(36).slice(2),
name: file.name,
size: file.size,
type: file.type,
status: 'error',
error: new Error('文件大小超出限制')
};
emit('error', errorFile.error, errorFile);
return;
}
// 创建上传文件对象
const uploadFile = {
uid: Date.now() + Math.random().toString(36).slice(2),
name: file.name,
size: file.size,
type: file.type,
status: 'ready',
percentage: 0
};
// 添加到文件列表
fileList.value.push(uploadFile);
// 存储原始文件对象,用于后续上传
pendingFiles.value.set(uploadFile.uid, file);
emit('update:fileList', fileList.value);
emit('change', { file: uploadFile, fileList: fileList.value });
// 自动上传
if (props.autoUpload) {
upload(file, uploadFile);
}
});
};
/**
* 处理拖拽进入
*/
const handleDragover = () => {
isDragging.value = true;
};
/**
* 处理拖拽离开
*/
const handleDragleave = () => {
isDragging.value = false;
};
/**
* 上传文件到服务器
* 使用 fetch API 发送文件,支持进度显示
*/
const upload = (file, uploadFile) => __awaiter(this, void 0, void 0, function* () {
const formData = new FormData();
formData.append('file', file);
// 添加额外参数
if (props.data) {
Object.entries(props.data || {}).forEach(([key, value]) => {
formData.append(key, value);
});
}
uploadFile.status = 'uploading';
uploadFile.percentage = 0;
try {
const xhr = new XMLHttpRequest();
// 监听上传进度
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
uploadFile.percentage = Math.round((e.loaded * 100) / e.total);
emit('update:fileList', fileList.value);
}
};
// 创建 Promise 包装 XHR
const response = yield new Promise((resolve, reject) => {
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
}
catch (e) {
reject(new Error('解析响应数据失败'));
}
}
else {
reject(new Error(`上传失败: ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('网络错误'));
// 设置请求头
const headers = {};
if (props.headers) {
Object.entries(props.headers || {}).forEach(([key, value]) => {
headers[key] = value;
});
}
// 发送请求
xhr.open('POST', props.action);
Object.entries(headers).forEach(([key, value]) => {
xhr.setRequestHeader(key, value);
});
xhr.send(formData);
});
uploadFile.status = 'success';
uploadFile.response = response;
uploadFile.url = response.url;
emit('success', uploadFile, fileList.value);
}
catch (error) {
uploadFile.status = 'error';
uploadFile.error = error;
emit('error', error, uploadFile);
}
emit('update:fileList', fileList.value);
});
/**
* 从列表中移除文件
*/
const removeFile = (file) => {
const index = fileList.value.indexOf(file);
if (index > -1) {
fileList.value.splice(index, 1);
emit('update:fileList', fileList.value);
}
};
/**
* 格式化文件大小
* 将字节数转换为人类可读的格式
*/
const formatSize = (bytes) => {
if (bytes === 0)
return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
};
/**
* 手动上传文件
* 上传所有状态为 ready 的文件
*/
const submit = () => {
fileList.value.forEach((uploadFile) => {
if (uploadFile.status === 'ready') {
const file = pendingFiles.value.get(uploadFile.uid);
if (file) {
upload(file, uploadFile);
}
}
});
};
// 暴露方法给父组件
expose({
submit,
handleClick,
fileInput
});
return {
fileInput,
fileList,
handleClick,
handleChange,
handleDrop,
handleDragover,
handleDragleave,
removeFile,
formatSize,
submit,
isDragging
};
}
});
const _hoisted_1 = { class: "vue-file-uploader" };
const _hoisted_2 = {
key: 0,
class: "uploader-list"
};
const _hoisted_3 = { class: "file-status-indicator" };
const _hoisted_4 = { class: "file-info" };
const _hoisted_5 = ["title"];
const _hoisted_6 = { class: "file-size" };
const _hoisted_7 = {
key: 0,
class: "file-status"
};
const _hoisted_8 = {
key: 1,
class: "file-status-text"
};
const _hoisted_9 = {
key: 0,
class: "success"
};
const _hoisted_10 = ["title"];
const _hoisted_11 = ["onClick", "title"];
const _hoisted_12 = ["accept", "multiple"];
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), createElementBlock("div", _hoisted_1, [
createCommentVNode(" 上传触发区域:点击或拖拽上传 "),
createElementVNode("div", {
class: normalizeClass(["uploader-trigger", { 'dragging': _ctx.isDragging, 'disabled': _ctx.disabled }]),
onClick: _cache[1] || (_cache[1] = $event => (!_ctx.disabled && _ctx.handleClick)),
onDrop: _cache[2] || (_cache[2] = withModifiers($event => (!_ctx.disabled && _ctx.handleDrop), ["prevent"])),
onDragover: _cache[3] || (_cache[3] = withModifiers($event => (!_ctx.disabled && _ctx.handleDragover), ["prevent"])),
onDragleave: _cache[4] || (_cache[4] = withModifiers($event => (!_ctx.disabled && _ctx.handleDragleave), ["prevent"]))
}, [
createCommentVNode(" 默认上传区域,可通过插槽自定义 "),
renderSlot(_ctx.$slots, "default", {}, () => [
createElementVNode("div", {
class: "uploader-placeholder",
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
}, _cache[6] || (_cache[6] = [
createElementVNode("i", { class: "uploader-icon" }, "+", -1 /* HOISTED */),
createElementVNode("div", { class: "uploader-text" }, "点击或拖拽文件上传", -1 /* HOISTED */)
]))
])
], 34 /* CLASS, NEED_HYDRATION */),
createCommentVNode(" 文件列表展示区域 "),
(_ctx.fileList.length)
? (openBlock(), createElementBlock("div", _hoisted_2, [
(openBlock(true), createElementBlock(Fragment, null, renderList(_ctx.fileList, (file) => {
return (openBlock(), createElementBlock("div", {
key: file.uid,
class: "uploader-file"
}, [
createCommentVNode(" 文件状态指示器 "),
createElementVNode("div", _hoisted_3, [
createElementVNode("span", {
class: normalizeClass(["icon", file.status])
}, [
(file.status === 'uploading')
? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
createTextVNode("↻")
], 64 /* STABLE_FRAGMENT */))
: (file.status === 'success')
? (openBlock(), createElementBlock(Fragment, { key: 1 }, [
createTextVNode("✓")
], 64 /* STABLE_FRAGMENT */))
: (file.status === 'error')
? (openBlock(), createElementBlock(Fragment, { key: 2 }, [
createTextVNode("✕")
], 64 /* STABLE_FRAGMENT */))
: (openBlock(), createElementBlock(Fragment, { key: 3 }, [
createTextVNode("•")
], 64 /* STABLE_FRAGMENT */))
], 2 /* CLASS */)
]),
createCommentVNode(" 文件信息展示 "),
createElementVNode("div", _hoisted_4, [
createElementVNode("span", {
class: "file-name",
title: file.name
}, toDisplayString(file.name), 9 /* TEXT, PROPS */, _hoisted_5),
createElementVNode("span", _hoisted_6, toDisplayString(_ctx.formatSize(file.size)), 1 /* TEXT */)
]),
createCommentVNode(" 上传进度/状态 "),
(file.status === 'uploading')
? (openBlock(), createElementBlock("div", _hoisted_7, [
createElementVNode("div", {
class: "progress-bar",
style: normalizeStyle({ width: `${file.percentage}%` })
}, null, 4 /* STYLE */)
]))
: (openBlock(), createElementBlock("div", _hoisted_8, [
(file.status === 'success')
? (openBlock(), createElementBlock("span", _hoisted_9, "完成"))
: createCommentVNode("v-if", true),
(file.status === 'error')
? (openBlock(), createElementBlock("span", {
key: 1,
class: "error",
title: file.error?.message
}, " 失败 ", 8 /* PROPS */, _hoisted_10))
: createCommentVNode("v-if", true)
])),
createCommentVNode(" 删除按钮 "),
createElementVNode("button", {
class: "delete-btn",
onClick: $event => (_ctx.removeFile(file)),
title: '删除 ' + file.name
}, " ✕ ", 8 /* PROPS */, _hoisted_11)
]))
}), 128 /* KEYED_FRAGMENT */))
]))
: createCommentVNode("v-if", true),
createCommentVNode(" 隐藏的文件输入框 "),
createElementVNode("input", {
ref: "fileInput",
type: "file",
accept: _ctx.accept,
multiple: _ctx.multiple,
style: {"display":"none"},
onChange: _cache[5] || (_cache[5] = (...args) => (_ctx.handleChange && _ctx.handleChange(...args)))
}, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_12)
]))
}
script.render = render;
script.__file = "src/components/FileUploader.vue";
/**
* Vue File Uploader 组件入口文件
* 支持 Vue 2.x 和 Vue 3.x
*/
// 确保 vue-demi 已安装
install$1();
/**
* Vue 插件安装函数
* 用于全局注册组件
* @param app Vue 应用实例
*/
const install = (app) => {
if (isVue2) {
// Vue 2.x
const Vue = app;
Vue.component('FileUploader', script);
}
else {
// Vue 3.x
app.component('FileUploader', script);
}
};
// 导出 Vue 插件
var index = {
install,
FileUploader: script
};
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue);
}
export { script as FileUploader, index as default };
//# sourceMappingURL=vue-file-uploader.esm.js.map