JS实现文件点击或者拖拽上传

B站看到了渡一大师课的切片,自己实现了一下,做下记录

效果展示

分为上传前、上传中和上传后

实现

分为两步

  1. 界面交互
  2. 网络请求

源码如下

upload.html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>图片上传 Demo</title>
		<link rel="stylesheet" href="upload.css" />
	</head>
	<body>
		<h1>图片上传 Demo</h1>
		<div class="upload select">
			<div class="upload-select"><input type="file" accept="image/*" / ></div>
			<div class="upload-progress">
				<div class="upload-progress-bar"></div>
				<div class="upload-progress-text">文件上传中...</div>
				<button>取消</button>
			</div>
			<div class="upload-result">
				<button>删除</button>
				<img src="" alt="" class="preview" />
			</div>
		</div>

		<script src="upload.js"></script>
	</body>
</html>

upload.js

js 复制代码
document.addEventListener('DOMContentLoaded', function () {
	const $ = document.querySelector.bind(document);
	const doms = {
		img: $('.preview'),
		container: $('.upload'),
		select: $('.upload-select'),
		selectFile: $('.upload-select input'),
		progress: $('.upload-progress'),
		cancelBtn: $('.upload-progress button'),
		delBtn: $('.upload-result button'),
	};
	// 备用方案,不利用input拖拽,将input设置为none
	// doms.select.ondragenter = function (e) {
	// 	e.preventDefault();
	// };

	// doms.select.ondragover = function (e) {
	// 	e.preventDefault();
	// };

	// doms.select.ondrop = function (e) {
	// 	e.preventDefault();
	// 	const file = e.dataTransfer.files[0];
	// 	if (!validateFile(file)) {
	// 		return;
	// 	}
	// 	doms.selectFile.files = e.dataTransfer.files;
	// 	doms.selectFile.onchange();
	// };

	// 切换三个子界面
	function showArea(areaName) {
		doms.container.className = `upload ${areaName}`;
	}
	// 设置进度
	function setProgress(value) {
		doms.progress.style.setProperty('--progress', value + '%');
	}
	// 取消上传
	let cancelUpload = null;
	function cancel() {
		cancelUpload && cancelUpload(); // 取消网络传输
		showArea('select');
		doms.selectFile.value = '';
	}

	// 上传文件的文件变化
	doms.selectFile.onchange = function () {
		if (this.files.length === 0) {
			return;
		}

		const file = this.files[0];
		console.log(file);
		if (!validateFile(file)) {
			return;
		}
		// 切换界面
		showArea('progress');
		// 显示预览图
		const reader = new FileReader();
		reader.onload = function (e) {
			doms.img.src = e.target.result;
		};
		reader.readAsDataURL(file); // 异步的,结果需要上边的监控拿到
		upload(
			file,
			function (val) {
				setProgress(val);
			},
			function (res) {
				showArea('result');
			},
		);
	};

	// 上传文件
	function upload(file, onProgress, onFinish) {
		const xhr = new XMLHttpRequest();
		xhr.onload = function () {
			const resp = JSON.parse(xhr.responseText);
			onFinish(resp);
		};
		xhr.upload.onprogress = function (e) {
			if (e.lengthComputable) {
				const percent = Math.round((e.loaded / e.total) * 100);
				onProgress(percent);
			}
		};
		xhr.open('POST', '/upload');
		const form = new FormData();
		form.append('avatar', file);
		xhr.send(form);
	}

	// 校验
	function validateFile(file) {
		const maxSize = 1024 * 1024 * 2;
		if (file.size > maxSize) {
			alert('文件大小不能超过2M');
			return false;
		}

		const allowTypes = ['image/jpeg', 'image/png', 'image/gif'];
		if (!allowTypes.includes(file.type)) {
			alert('文件类型只能是jpg、png、gif');
			return false;
		}
		return true;
	}

	doms.cancelBtn.onclick = doms.delBtn.onclick = cancel;
});

upload.css

css 复制代码
body {
	font-family: Arial, sans-serif;
	margin: 0;
	padding: 0;
}
.upload {
	width: 400px;
	height: 400px;
	background-color: azure;
}

/* 通过属性控制子组件显示 */
/* 当父元素有 select 类时,只显示上传选择区域 */
.upload.select .upload-select {
	display: flex;
}

.upload.select .upload-progress,
.upload.select .upload-result {
	display: none;
}

/* 当父元素有 progress 类时,只显示进度条 */
.upload.progress .upload-progress {
	display: flex;
}

.upload.progress .upload-select,
.upload.progress .upload-result {
	display: none;
}

/* 当父元素有 result 类时,只显示结果区域 */
.upload.result .upload-result {
	display: flex;
}

.upload.result .upload-select,
.upload.result .upload-progress {
	display: none;
}

.upload-select {
	height: 100%;
	width: 100%;
	display: flex;
	justify-content: center;
	align-items: center;
	position: relative;
	background-image: url(./fileUplaod.svg);
	background-position: center;
	background-repeat: no-repeat;
}

/*  本身就支持拖拽 */
.upload-select input {
	display: block;
	width: 100%;
	height: 100%;
	opacity: 0;
	cursor: pointer;
}

.upload-progress {
	--progress: 0%;
	height: 100%;
	width: 100%;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
}

.upload-progress-bar {
	width: var(--progress);
	height: 10px;
	background-color: #4caf50;
	transition: width 0.3s ease;
}

.upload-result {
	height: 100%;
	width: 100%;
	display: flex;
	flex-direction: column;
	justify-content: center;
}

.preview {
	max-width: 90%;
	max-height: 90%;
	width: auto;
	height: auto;
	/* 以下属性确保图片居中显示 */
	display: block;
	margin: 0 auto;
	/* 保持宽高比 */
	object-fit: contain;
}
相关推荐
用户69371750013847 小时前
Google 正在“收紧侧加载”:陌生 APK 安装或需等待 24 小时
android·前端
蓝帆傲亦7 小时前
Web 前端搜索文字高亮实现方法汇总
前端
用户69371750013847 小时前
Room 3.0:这次不是升级,是重来
android·前端·google
漫随流水8 小时前
旅游推荐系统(view.py)
前端·数据库·python·旅游
踩着两条虫9 小时前
VTJ.PRO 核心架构全公开!从设计稿到代码,揭秘AI智能体如何“听懂人话”
前端·vue.js·ai编程
jzlhll12310 小时前
kotlin Flow first() last()总结
开发语言·前端·kotlin
用头发抵命11 小时前
Vue 3 中优雅地集成 Video.js 播放器:从组件封装到功能定制
开发语言·javascript·ecmascript
蓝冰凌11 小时前
Vue 3 中 defineExpose 的行为【defineExpose暴露ref变量】详解:自动解包、响应性与实际使用
前端·javascript·vue.js
奔跑的呱呱牛11 小时前
generate-route-vue基于文件系统的 Vue Router 动态路由生成工具
前端·javascript·vue.js