[特殊字符] uni-app App 端实现文件上传功能(基于 xe-upload 插件)

在 uni-app 开发中,文件上传是一个常见且重要的功能。尤其是在 App 端,如何实现一个既美观又实用的文件上传与展示界面,是很多开发者关心的问题。本文将介绍如何通过 xe-upload 插件,结合自定义 UI,实现一个完整的文件上传、展示与下载功能。

📸 效果预览

先来看一下最终实现的效果:

https://your-image-url.com/example.pnghttps://your-image-url.com/example.png

界面分为上下两部分:

  • 上方为固定位置的"上传"按钮

  • 下方为附件列表,支持文件图标、信息展示和下载功能

🛠️ 主要功能

  • ✅ 支持多类型文件上传(图片、文档、PDF 等)

  • ✅ 文件列表展示(图标、名称、上传者、上传时间)

  • ✅ 文件下载功能

  • ✅ 上传状态提示与错误处理

  • ✅ 空状态友好提示

📦 使用插件

我们使用 xe-upload 插件来实现文件选择与上传功能,它是一个功能丰富且兼容性好的 uni-app 上传组件。

安装与引入

bash

复制代码
npm install xe-upload

或者直接在uniapp插件市场下载

在页面中引入:

vue

html 复制代码
<xe-upload ref="XeUpload" :options="uploadOptions" @callback="handleUploadCallback"></xe-upload>

📝 核心代码实现

1. 模板结构

html 复制代码
<template>
	<view class="viewFileListWrapper">
		<!-- 固定上传按钮 -->
		<view class="upload-btn-fixed">
			<button class="upload-btn" @click="handleUploadClick">上传</button>
			<xe-upload ref="XeUpload" :options="uploadOptions" @callback="handleUploadCallback"></xe-upload>
		</view>
		
		<!-- 文件列表区域 -->
		<view class="content-wrapper">
			<view class="file-list" v-if="fileList.length > 0">
				<!-- 文件卡片循环 -->
				<view class="file-card" v-for="(file, index) in fileList" :key="index">
					<!-- 文件头部:图标和基本信息 -->
					<view class="file-header">
						<view class="file-icon" :class="getFileIconClass(file.fileType)">
							<text class="file-type-text">{{ getFileTypeText(file.fileType) }}</text>
						</view>
						<view class="file-info">
							<text class="file-name">{{ file.fileName }}</text>
							<text class="file-category">{{ file.type }}</text>
						</view>
					</view>
					
					<!-- 文件详情:上传者和时间 -->
					<view class="file-details">
						<view class="detail-item">
							<uni-icons type="person" size="14" color="#666"></uni-icons>
							<text class="detail-text">上传者:{{ file.itemCreateUser }}</text>
						</view>
						<view class="detail-item">
							<uni-icons type="calendar" size="14" color="#666"></uni-icons>
							<text class="detail-text">上传时间:{{ formatDate(file.itemCreateTime) }}</text>
						</view>
					</view>
					
					<!-- 下载按钮 -->
					<view class="file-actions">
						<button class="download-btn" @click="handleDownload(file)">
							<uni-icons type="download" size="16" color="#007AFF"></uni-icons>
							下载附件
						</button>
					</view>
				</view>
			</view>
			
			<!-- 空状态提示 -->
			<view class="empty-state" v-else>
				<uni-icons type="folder-open" size="60" color="#CCCCCC"></uni-icons>
				<text class="empty-text">暂无附件</text>
				<text class="empty-tip">点击上方按钮上传第一个附件</text>
			</view>
		</view>
	</view>
</template>

2. 上传功能实现

点击上传按钮

javascript

javascript 复制代码
handleUploadClick() {
    // 触发 xe-upload 的文件选择
    this.$refs.XeUpload.upload('file');
}
上传回调处理

javascript

javascript 复制代码
handleUploadCallback(e) {
    if (['success'].includes(e.type)) {
        const tmpFiles = (e.data || []).map(({ response, tempFilePath, name, fileType }) => {
            const resData = response?.data || {};
            return {
                url: resData.url,
                name: resData.name || name,
                fileRealName: e.data[0].name,
                fileExtension: resData.fileExtension,
                fileSize: resData.fileSize,
                originalResponse: response,
                tempFilePath: tempFilePath,
                fileType: fileType || resData.fileExtension
            };
        });
        
        if (tmpFiles && tmpFiles.length > 0) {
            // 调用上传完成后的接口
            this.handleFileUploadSuccess(tmpFiles);
        }
    }
}
上传成功后调用接口

javascript

javascript 复制代码
handleFileUploadSuccess(files) {
    if (files.length === 0) return;
    const firstFile = files[0];
    
    let query = {
        attachmentList: [{
            businessIds: [this.config.row[0].fatId],
            extension: firstFile.fileExtension,
            fileId: firstFile.name,
            fileName: firstFile.fileRealName,
            fileSize: firstFile.fileSize,
            identifier: firstFile.name,
            isAccount: 0,
            moduleId: this.config.modelId,
            pathType: "defaultPath",
            tableName: "deliveryAcceptanceFile",
            type: "annex",
            url: this.define.comUploadUrl + firstFile.url
        }]
    };
    
    mergeAttachmentFile({ ...query }).then(res => {
        if (res.code === 200) {
            uni.showToast({ title: '文件上传成功', icon: 'none' });
            // 刷新文件列表
            setTimeout(() => this.getFileList(), 1000);
        }
    }).catch(err => {
        console.error('上传失败:', err);
        uni.showToast({ title: '文件上传失败', icon: 'none' });
    });
}

3. 文件下载功能

javascript

javascript 复制代码
handleDownload(file) {
    uni.showLoading({ title: '下载中...', mask: true });
    
    let token = uni.getStorageSync("token");
    uni.downloadFile({
        url: this.define.baseURL + `/api/file/downloadAttachmentApp?id=${file.id}`,
        header: {
            'Authorization': token,
            'Content-Type': 'application/vnd.ms-excel',
        },
        success: (res) => {
            if (res.statusCode === 200) {
                uni.openDocument({
                    filePath: res.tempFilePath,
                    fileType: 'xlsx',
                    success(res) { uni.hideLoading(); }
                });
            }
        }
    });
}

4. 获取文件列表

javascript

javascript 复制代码
getFileList() {
    uni.showLoading({ title: '加载中...', mask: true });
    
    let params = {
        businessType: "",
        businessId: this.config.row[0].fatId,
        moduleId: this.config.modelId,
        tableName: "deliveryAcceptanceFile"
    };
    
    getTaskExcuteUploadList(params).then((res) => {
        uni.hideLoading();
        if (res) {
            this.fileList = res.data;
        }
    }).catch((error) => {
        uni.hideLoading();
        console.error('获取文件列表失败:', error);
        uni.showToast({ title: '加载失败', icon: 'none' });
    });
}

🎨 样式设计要点

固定上传按钮

css

css 复制代码
.upload-btn-fixed {
    position: fixed;
    top: 7%;
    left: 0;
    right: 0;
    z-index: 999;
    background-color: #ffffff;
    padding: 20rpx;
    box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}

文件图标根据类型显示不同颜色

css

css 复制代码
.file-icon-excel { background-color: #1d6f42; }
.file-icon-word { background-color: #2b579a; }
.file-icon-pdf { background-color: #e74c3c; }
.file-icon-image { background-color: #9b59b6; }
.file-icon-ppt { background-color: #d24726; }

🔧 配置说明

xe-upload 配置

javascript

css 复制代码
uploadOptions: {
    url: this.define.comUploadUrl + 'annexpic', // 上传地址
}

文件类型映射

javascript

javascript 复制代码
getFileIconClass(fileType) {
    const typeMap = {
        'xlsx': 'excel', 'xls': 'excel',
        'docx': 'word', 'doc': 'word',
        'pdf': 'pdf', 'txt': 'txt',
        'png': 'image', 'jpg': 'image', 'jpeg': 'image',
        'ppt': 'ppt', 'pptx': 'ppt', 'rtf': 'rtf'
    };
    return `file-icon-${typeMap[fileType] || 'default'}`;
}
整体代码:
javascript 复制代码
<template>
	<view class="viewFileListWrapper">
		<!-- 上传按钮 - 固定定位 -->
		<view class="upload-btn-fixed">
			<button class="upload-btn" @click="handleUploadClick">上传</button>
			<xe-upload ref="XeUpload" :options="uploadOptions" @callback="handleUploadCallback"></xe-upload>
		</view>
		<!-- 内容区域,给上传按钮留出空间 -->
		<view class="content-wrapper">
			<!-- 附件列表 -->
			<view class="file-list" v-if="fileList.length > 0">
				<view class="file-card" v-for="(file, index) in fileList" :key="index">
					<view class="file-header">
						<!-- 文件类型图标 -->
						<view class="file-icon" :class="getFileIconClass(file.fileName)">
							<text class="file-type-text">{{ getFileTypeText(file.fileName) }}</text>
						</view>
						<view class="file-info">
							<text class="file-name">{{ file.fileName }}</text>
							<text class="file-category">{{ file.type }}</text>
						</view>
					</view>

					<view class="file-details">
						<view class="detail-item">
							<uni-icons type="person" size="14" color="#666"></uni-icons>
							<text class="detail-text">上传者:{{ file.itemCreateUser }}</text>
						</view>
						<view class="detail-item">
							<uni-icons type="calendar" size="14" color="#666"></uni-icons>
							<text class="detail-text">上传时间:{{ formatDate(file.itemCreateTime) }}</text>
						</view>
					</view>

					<!-- 下载按钮 -->
					<view class="file-actions">
						<button class="download-btn" @click="handleDownload(file)">
							<uni-icons type="download" size="16" color="#007AFF"></uni-icons>
							下载附件
						</button>
					</view>
				</view>
			</view>

			<!-- 空状态 -->
			<view class="empty-state" v-else>
				<uni-icons type="folder-open" size="60" color="#CCCCCC"></uni-icons>
				<text class="empty-text">暂无附件</text>
				<text class="empty-tip">点击上方按钮上传第一个附件</text>
			</view>
		</view>
	</view>
</template>

<script>
	import {
		getTaskExcuteUploadList
	} from '@/api/apply/coustomComponent.js'
	import {
		uploadFile
	} from '@/api/apply/file.js'
	import {
		mergeAttachmentFile
	} from '@/api/common.js'
	export default {
		name: "viewFileListVue",
		props: ['config'],
		data() {
			return {
				// 模拟数据 - 添加更多数据以便测试滚动
				fileList: [],
				uploadProgress: 0,
				uploadingFileName: '',
				uploadTask: null, // 上传任务对象
				currentUploadFile: null, // 当前上传的文件
				uploadOptions: {
					url: this.define.comUploadUrl + 'annexpicApp', // 不传入上传地址则返回本地链接
				},
			}
		},
		mounted() {
			this.getFileList()
		},
		methods: {
			handleUploadClick() {
				// 使用默认配置则不需要传入第二个参数
				// type: ['image', 'video', 'file'];
				this.$refs.XeUpload.upload('file');
			},
			handleUploadCallback(e) {
				console.log(e, "3333333")
				if (['success'].includes(e.type)) {
					// 根据接口返回修改对应的response相关的逻辑
					const tmpFiles = (e.data || []).map(({
						response,
						tempFilePath,
						name,
						fileType
					}) => {
						console.log(response, "responseresponseresponse")
						// 提取响应数据
						const resData = response?.data || {}

						// 返回处理后的文件对象
						return {
							url: resData.url,
							name: resData.name || name,
							fileRealName: e.data[0].name,
							fileExtension: resData.fileExtension,
							fileSize: resData.fileSize,
							originalResponse: response,
							tempFilePath: tempFilePath,
							fileType: fileType || resData.fileExtension
						}
					});
					console.log(tmpFiles, "处理后的文件数组")
					// 如果需要,可以在这里调用上传完成后的接口
					if (tmpFiles && tmpFiles.length > 0) {
						// 调用 mergeAttachmentFile 接口
						this.handleFileUploadSuccess(tmpFiles)
					}
				}
			},

			// 新增方法:处理上传成功的文件
			handleFileUploadSuccess(files) {
				console.log(files, "999999999999")
				if (files.length === 0) return
				const firstFile = files[0]
				let query = {
					attachmentList: [{
						businessIds: [this.config.row[0].fatId],
						extension: firstFile.fileExtension,
						fileId: firstFile.name,
						fileName: firstFile.fileRealName,
						fileSize: firstFile.fileSize,
						identifier: firstFile.name,
						isAccount: 0,
						moduleId: this.config.modelId,
						pathType: "defaultPath",
						tableName: "deliveryAcceptanceFile",
						type: "annex",
						url: this.define.comUploadUrl + firstFile.url // 使用上传返回的url
					}]
				}
				mergeAttachmentFile({
					...query
				}).then(res => {
					if (res.code === 200) {
						uni.showToast({
							title: '文件上传成功',
							icon: 'none'
						})
						setTimeout(() => {
							// 上传成功后刷新文件列表
							this.getFileList()
						}, 1000)

					}
				}).catch(err => {
					console.error('上传失败:', err)
					uni.showToast({
						title: '文件上传失败',
						icon: 'none'
					})
				})
			},
			getFileList() {
				// 显示加载中
				uni.showLoading({
					title: '加载中...',
					mask: true
				});
				let params = {
					businessType: "",
					businessId: this.config.row[0].fatId,
					moduleId: this.config.modelId,
					tableName: "deliveryAcceptanceFile"
				}
				getTaskExcuteUploadList(params).then((res) => {
					console.log("resresres",res)
					// 隐藏加载提示
					uni.hideLoading();
					if (res) {
						this.fileList = res.data
					}
				}).catch((error) => {
					// 隐藏加载提示
					uni.hideLoading();

					console.error('获取文件列表失败:', error);
					uni.showToast({
						title: '加载失败',
						icon: 'none'
					});
				})
			},
			// 修改 handleDownload 方法
			handleDownload(file) {
				let fileId = file.id
				// 显示加载提示
				uni.showLoading({
					title: '下载中...',
					mask: true
				});

				let token = uni.getStorageSync("token")
				uni.downloadFile({
					url: this.define.baseURL + `/api/file/downloadAttachmentApp?id=${fileId}`,
					header: {
						'Authorization': token,
						'Content-Type': 'application/vnd.ms-excel',
					},
					success: (res) => {
						if (res.statusCode === 200) {
							uni.openDocument({
								filePath: res.tempFilePath,
								fileType: 'xlsx',
								success(res) {
									uni.hideLoading();
								}
							})
						}
					}
				});
			},
			// 获取文件类型图标样式
			getFileIconClass(fileType) {
				let fileExtension = fileType.split('.').pop();
				const typeMap = {
					'xlsx': 'excel',
					'xls': 'excel',
					'docx': 'word',
					'doc': 'word',
					'pdf': 'pdf',
					'txt': 'txt',
					'png': 'image',
					'jpg': 'image',
					'jpeg': 'image',
					'ppt': 'ppt',
					'pptx': 'ppt',
					'rtf': 'rtf'
				};
				return `file-icon-${typeMap[fileExtension] || 'default'}`;
			},
			// 获取文件类型显示文本
			getFileTypeText(fileType) {
				if(fileType) {
					return fileType.split('.').pop()
				} else {
					return ""
				}
			},
			// 格式化日期
			formatDate(dateString) {
				return dateString;
			}
		}
	}
</script>

<style lang="scss" scoped>
	.viewFileListWrapper {
		width: 100%;
		height: 100vh;
		background-color: #f5f5f5;
		position: relative;
	}

	/* 固定上传按钮 */
	.upload-btn-fixed {
		position: fixed;
		top: 7%;
		left: 0;
		right: 0;
		z-index: 999;
		background-color: #ffffff;
		padding: 20rpx;
		box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);

		.upload-btn {
			display: flex;
			align-items: center;
			justify-content: center;
			width: 100%;
			height: 80rpx;
			background-color: #007AFF;
			color: #ffffff;
			border-radius: 8rpx;
			font-size: 28rpx;
			font-weight: 500;
			border: none;

			uni-icons {
				margin-right: 10rpx;
			}
		}
	}

	/* 内容区域 - 为固定按钮留出空间 */
	.content-wrapper {
		padding-top: 120rpx;
		/* 按钮高度 + 上下padding */
		padding-left: 20rpx;
		padding-right: 20rpx;
		padding-bottom: 40rpx;
		box-sizing: border-box;
		min-height: 100vh;
	}

	.file-list {
		.file-card {
			background-color: #ffffff;
			border-radius: 12rpx;
			padding: 30rpx;
			margin-bottom: 20rpx;
			box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);

			.file-header {
				display: flex;
				align-items: center;
				margin-bottom: 20rpx;

				.file-icon {
					width: 80rpx;
					height: 80rpx;
					border-radius: 8rpx;
					display: flex;
					align-items: center;
					justify-content: center;
					margin-right: 20rpx;
					flex-shrink: 0;

					.file-type-text {
						color: #ffffff;
						font-size: 20rpx;
						font-weight: bold;
					}
				}

				// 文件类型颜色
				.file-icon-excel {
					background-color: #1d6f42;
				}

				.file-icon-word {
					background-color: #2b579a;
				}

				.file-icon-pdf {
					background-color: #e74c3c;
				}

				.file-icon-image {
					background-color: #9b59b6;
				}

				.file-icon-ppt {
					background-color: #d24726;
				}

				.file-icon-txt {
					background-color: #7f8c8d;
				}

				.file-icon-rtf {
					background-color: #3498db;
				}

				.file-icon-default {
					background-color: #95a5a6;
				}

				.file-info {
					flex: 1;
					overflow: hidden;

					.file-name {
						display: block;
						font-size: 32rpx;
						font-weight: 500;
						color: #333333;
						margin-bottom: 8rpx;
						white-space: nowrap;
						overflow: hidden;
						text-overflow: ellipsis;
					}

					.file-category {
						display: inline-block;
						font-size: 24rpx;
						color: #007AFF;
						background-color: #e6f3ff;
						padding: 4rpx 12rpx;
						border-radius: 20rpx;
					}
				}
			}

			.file-details {
				display: flex;
				flex-direction: column;
				gap: 12rpx;
				margin-bottom: 24rpx;

				.detail-item {
					display: flex;
					align-items: center;

					uni-icons {
						margin-right: 10rpx;
						flex-shrink: 0;
					}

					.detail-text {
						font-size: 26rpx;
						color: #666666;
					}
				}
			}

			.file-actions {
				.download-btn {
					display: flex;
					align-items: center;
					justify-content: center;
					width: 100%;
					height: 70rpx;
					background-color: #f0f8ff;
					color: #007AFF;
					border: 2rpx solid #007AFF;
					border-radius: 8rpx;
					font-size: 28rpx;

					uni-icons {
						margin-right: 10rpx;
					}
				}
			}
		}
	}

	.empty-state {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		padding: 100rpx 0;

		.empty-text {
			font-size: 32rpx;
			color: #999999;
			margin-top: 20rpx;
		}

		.empty-tip {
			font-size: 26rpx;
			color: #cccccc;
			margin-top: 10rpx;
		}
	}
</style>

📱 适配说明

  • 使用 rpx 单位确保在不同设备上的适配

  • 固定按钮使用 position: fixed 确保始终可见

  • 为内容区域设置 padding-top 避免被固定按钮遮挡

💡 使用建议

  1. 权限控制:可根据业务需求添加上传权限控制

  2. 文件大小限制:可在上传前添加文件大小校验

  3. 上传进度:可扩展显示上传进度条

  4. 批量上传:支持多文件同时上传

  5. 上传失败重试:添加上传失败后的重试机制

🎯 总结

通过 xe-upload 插件结合自定义 UI,我们实现了一个功能完整、用户体验良好的文件上传系统。这个方案不仅适用于验收任务场景,也可轻松适配到其他需要文件管理的业务模块中。

相关推荐
焚 城7 小时前
uniapp 各种文件预览实现
vue.js·uni-app·html
weixin79893765432...7 小时前
uni-app 全面深入的解读
uni-app
2501_915918417 小时前
提升 iOS 应用安全审核通过率的一种思路,把容易被拒的点先处理
android·安全·ios·小程序·uni-app·iphone·webview
San30.7 小时前
现代前端工程化实战:从 Vite 到 Vue Router 的构建之旅
前端·javascript·vue.js
sg_knight7 小时前
模块热替换 (HMR):前端开发的“魔法”与提速秘籍
前端·javascript·vue·浏览器·web·模块化·hmr
A24207349307 小时前
js常用事件
开发语言·前端·javascript
LV技术派7 小时前
适合很多公司和团队的 AI Coding 落地范式(一)
前端·aigc·ai编程
Fighting_p7 小时前
【导出】前端 js 导出下载文件时,文件名前后带下划线问题
开发语言·前端·javascript
WYiQIU7 小时前
从今天开始备战1月中旬的前端寒假实习需要准备什么?(飞书+github+源码+题库含答案)
前端·javascript·面试·职场和发展·前端框架·github·飞书