news 2025/12/31 20:16:53

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
[特殊字符] 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

<xe-upload ref="XeUpload" :options="uploadOptions" @callback="handleUploadCallback"></xe-upload>

📝 核心代码实现

1. 模板结构

<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

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

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

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

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

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

.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

.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

uploadOptions: { url: this.define.comUploadUrl + 'annexpic', // 上传地址 }

文件类型映射

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'}`; }
整体代码:
<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,我们实现了一个功能完整、用户体验良好的文件上传系统。这个方案不仅适用于验收任务场景,也可轻松适配到其他需要文件管理的业务模块中。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/17 22:27:35

二、Visual Studio 2026如何创建C语言项目

1.打开软件2.创建新项目3.下一步4.创建5.添加源文件&#xff08;1&#xff09;新建项&#xff08;2&#xff09;显示所有模板&#xff08;3&#xff09;添加C语言源文件后缀名为.c6.添加头文件&#xff08;1&#xff09;新建项&#xff08;2&#xff09;显示所有模板&#xff0…

作者头像 李华
网站建设 2025/12/30 14:47:28

Java二叉树:原理、实现与实战

深入浅出Java二叉树&#xff1a;原理、实现与实战 一、二叉树核心概念深度解析 1. 二叉树的定义与分类 二叉树是一种每个节点最多有2个子节点的树状结构&#xff0c;子节点分为左子节点&#xff08;lChild&#xff09;和右子节点&#xff08;rChild&#xff09;。根据节点分布规…

作者头像 李华
网站建设 2025/12/30 22:44:53

动态规划(四)算法设计与分析 国科大

0-1背包问题输入&#xff1a;给定物品集合 &#xff0c;每个物品 i 对应重量 和价值&#xff1b;同时给定背包的总重量限制 W。输出&#xff1a;选择物品的一个子集&#xff0c;满足 “子集总重量不超过 W” 的约束&#xff0c;同时最大化子集的总价值。这是一个二元决策问题&a…

作者头像 李华
网站建设 2025/12/30 16:36:15

为什么90%的团队搞不定云原生Agent部署?Docker批量方案深度拆解

第一章&#xff1a;云原生Agent部署的现状与挑战随着云原生技术的快速发展&#xff0c;Agent作为实现可观测性、自动化运维和安全监控的核心组件&#xff0c;被广泛部署于Kubernetes集群、边缘节点及混合云环境中。这些轻量级代理程序负责采集指标、日志和追踪数据&#xff0c;…

作者头像 李华
网站建设 2025/12/30 18:39:00

2025年为何越来越多的程序员都转行网络安全?难道发展前景更好?

2025年为何越来越多的程序员都转行网络安全&#xff1f;难道发展前景更好&#xff1f; 为何越来越多的程序员纷纷转行网络安全&#xff1f; 其实黑客都是程序员&#xff0c;但是并不是所有的程序员都是黑客. 从企业和社会需求来看&#xff0c;现在真不缺程序猿 &#xff0c;反…

作者头像 李华
网站建设 2025/12/30 16:36:13

统信域管域用户在加域计算机中的组

统信域管域用户在加域计算机中会自动创建与用户名相同的组&#xff0c;并且域用户会同时在dialout、disk、sambashare、vboxusers、netdev、scanner、lpadmin、users、sudo、udcp、lp组中test2:x:10093:test2 dialout:x:20:test,test2 disk:x:6:test,test2 sambashare:x:998:te…

作者头像 李华