|
@@ -0,0 +1,381 @@
|
|
|
|
+<template>
|
|
|
|
+ <div class="file-upload-container">
|
|
|
|
+ <div class="header">
|
|
|
|
+ <h2>文件上传</h2>
|
|
|
|
+ <p>选择文件后提交给小程序</p>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div class="upload-area" @click="triggerFileInput">
|
|
|
|
+ <div class="upload-icon">
|
|
|
|
+ <i class="el-icon-upload"></i>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="upload-text">
|
|
|
|
+ <p>点击选择文件</p>
|
|
|
|
+ <p class="upload-hint">支持图片、文档、视频等格式</p>
|
|
|
|
+ </div>
|
|
|
|
+ <input
|
|
|
|
+ ref="fileInput"
|
|
|
|
+ type="file"
|
|
|
|
+ multiple
|
|
|
|
+ accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.txt,.mp4,.mp3"
|
|
|
|
+ @change="handleFileSelect"
|
|
|
|
+ style="display: none"
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div v-if="selectedFiles.length > 0" class="file-list">
|
|
|
|
+ <h3>已选择的文件</h3>
|
|
|
|
+ <div class="file-item" v-for="(file, index) in selectedFiles" :key="index">
|
|
|
|
+ <div class="file-info">
|
|
|
|
+ <div class="file-icon">
|
|
|
|
+ <i :class="getFileIcon(file.type)"></i>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="file-details">
|
|
|
|
+ <div class="file-name">{{ file.name }}</div>
|
|
|
|
+ <div class="file-size">{{ formatFileSize(file.size) }}</div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="file-actions">
|
|
|
|
+ <el-button
|
|
|
|
+ type="danger"
|
|
|
|
+ size="mini"
|
|
|
|
+ @click="removeFile(index)"
|
|
|
|
+ icon="el-icon-delete"
|
|
|
|
+ >
|
|
|
|
+ 删除
|
|
|
|
+ </el-button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <div class="submit-area">
|
|
|
|
+ <el-button
|
|
|
|
+ type="primary"
|
|
|
|
+ size="large"
|
|
|
|
+ @click="submitToMiniProgram"
|
|
|
|
+ :disabled="selectedFiles.length === 0"
|
|
|
|
+ :loading="submitting"
|
|
|
|
+ >
|
|
|
|
+ {{ submitting ? '提交中...' : '提交给小程序' }}
|
|
|
|
+ </el-button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script>
|
|
|
|
+export default {
|
|
|
|
+ name: 'FileUpload',
|
|
|
|
+ data() {
|
|
|
|
+ return {
|
|
|
|
+ selectedFiles: [],
|
|
|
|
+ submitting: false
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ mounted() {
|
|
|
|
+ this.checkWechatEnvironment()
|
|
|
|
+ },
|
|
|
|
+ methods: {
|
|
|
|
+ // 检查微信环境
|
|
|
|
+ checkWechatEnvironment() {
|
|
|
|
+ const isWechat = /micromessenger/i.test(navigator.userAgent)
|
|
|
|
+ if (!isWechat) {
|
|
|
|
+ this.$message.warning('请在微信中打开此页面')
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 触发文件选择
|
|
|
|
+ triggerFileInput() {
|
|
|
|
+ this.$refs.fileInput.click()
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 处理文件选择
|
|
|
|
+ handleFileSelect(event) {
|
|
|
|
+ const files = Array.from(event.target.files)
|
|
|
|
+
|
|
|
|
+ files.forEach(file => {
|
|
|
|
+ // 检查文件大小限制 (50MB)
|
|
|
|
+ if (file.size > 50 * 1024 * 1024) {
|
|
|
|
+ this.$message.error(`文件 ${file.name} 超过50MB限制`)
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 检查是否已存在同名文件
|
|
|
|
+ const exists = this.selectedFiles.find(f => f.name === file.name)
|
|
|
|
+ if (exists) {
|
|
|
|
+ this.$message.warning(`文件 ${file.name} 已存在`)
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.selectedFiles.push(file)
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // 清空input值,允许重复选择同一文件
|
|
|
|
+ event.target.value = ''
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 删除文件
|
|
|
|
+ removeFile(index) {
|
|
|
|
+ this.selectedFiles.splice(index, 1)
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 获取文件图标
|
|
|
|
+ getFileIcon(type) {
|
|
|
|
+ if (type.startsWith('image/')) {
|
|
|
|
+ return 'el-icon-picture'
|
|
|
|
+ } else if (type.includes('pdf')) {
|
|
|
|
+ return 'el-icon-document'
|
|
|
|
+ } else if (type.includes('word') || type.includes('document')) {
|
|
|
|
+ return 'el-icon-document'
|
|
|
|
+ } else if (type.includes('excel') || type.includes('spreadsheet')) {
|
|
|
|
+ return 'el-icon-document'
|
|
|
|
+ } else if (type.includes('video')) {
|
|
|
|
+ return 'el-icon-video-camera'
|
|
|
|
+ } else if (type.includes('audio')) {
|
|
|
|
+ return 'el-icon-headset'
|
|
|
|
+ } else {
|
|
|
|
+ return 'el-icon-document'
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 格式化文件大小
|
|
|
|
+ formatFileSize(bytes) {
|
|
|
|
+ if (bytes === 0) return '0 B'
|
|
|
|
+ const k = 1024
|
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB']
|
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 提交给小程序
|
|
|
|
+ async submitToMiniProgram() {
|
|
|
|
+ if (this.selectedFiles.length === 0) {
|
|
|
|
+ this.$message.warning('请先选择文件')
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.submitting = true
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ // 检查微信环境
|
|
|
|
+ if (typeof WeixinJSBridge === 'undefined') {
|
|
|
|
+ this.$message.error('请在微信中打开此页面')
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 准备文件数据
|
|
|
|
+ const filesData = await this.prepareFilesData()
|
|
|
|
+
|
|
|
|
+ // 通过微信JS-SDK发送消息给小程序
|
|
|
|
+ if (window.wx && window.wx.miniProgram) {
|
|
|
|
+ // 在微信小程序web-view中
|
|
|
|
+ window.wx.miniProgram.postMessage({
|
|
|
|
+ data: {
|
|
|
|
+ type: 'file_upload',
|
|
|
|
+ files: filesData
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ this.$message.success('文件已提交给小程序')
|
|
|
|
+ this.selectedFiles = []
|
|
|
|
+ } else {
|
|
|
|
+ // 尝试通过WeixinJSBridge
|
|
|
|
+ WeixinJSBridge.invoke('sendAppMessage', {
|
|
|
|
+ title: '文件上传',
|
|
|
|
+ desc: `已选择 ${this.selectedFiles.length} 个文件`,
|
|
|
|
+ link: window.location.href,
|
|
|
|
+ imgUrl: '',
|
|
|
|
+ data: {
|
|
|
|
+ type: 'file_upload',
|
|
|
|
+ files: filesData
|
|
|
|
+ }
|
|
|
|
+ }, (res) => {
|
|
|
|
+ if (res.err_msg === 'send_app_msg:ok') {
|
|
|
|
+ this.$message.success('文件已提交给小程序')
|
|
|
|
+ this.selectedFiles = []
|
|
|
|
+ } else {
|
|
|
|
+ this.$message.error('提交失败,请重试')
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('提交文件失败:', error)
|
|
|
|
+ this.$message.error('提交失败,请重试')
|
|
|
|
+ } finally {
|
|
|
|
+ this.submitting = false
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 准备文件数据
|
|
|
|
+ async prepareFilesData() {
|
|
|
|
+ const filesData = []
|
|
|
|
+
|
|
|
|
+ for (const file of this.selectedFiles) {
|
|
|
|
+ try {
|
|
|
|
+ const base64 = await this.fileToBase64(file)
|
|
|
|
+ filesData.push({
|
|
|
|
+ name: file.name,
|
|
|
|
+ type: file.type,
|
|
|
|
+ size: file.size,
|
|
|
|
+ data: base64
|
|
|
|
+ })
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('转换文件失败:', error)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return filesData
|
|
|
|
+ },
|
|
|
|
+
|
|
|
|
+ // 文件转Base64
|
|
|
|
+ fileToBase64(file) {
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
+ const reader = new FileReader()
|
|
|
|
+ reader.onload = () => resolve(reader.result)
|
|
|
|
+ reader.onerror = reject
|
|
|
|
+ reader.readAsDataURL(file)
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style scoped>
|
|
|
|
+.file-upload-container {
|
|
|
|
+ max-width: 800px;
|
|
|
|
+ margin: 0 auto;
|
|
|
|
+ padding: 20px;
|
|
|
|
+ background: #fff;
|
|
|
|
+ min-height: 100vh;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.header {
|
|
|
|
+ text-align: center;
|
|
|
|
+ margin-bottom: 30px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.header h2 {
|
|
|
|
+ color: #303133;
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.header p {
|
|
|
|
+ color: #909399;
|
|
|
|
+ font-size: 14px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.upload-area {
|
|
|
|
+ border: 2px dashed #d9d9d9;
|
|
|
|
+ border-radius: 8px;
|
|
|
|
+ padding: 40px;
|
|
|
|
+ text-align: center;
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
+ background: #fafafa;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.upload-area:hover {
|
|
|
|
+ border-color: #409eff;
|
|
|
|
+ background: #f0f9ff;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.upload-icon {
|
|
|
|
+ font-size: 48px;
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.upload-text p {
|
|
|
|
+ margin: 5px 0;
|
|
|
|
+ color: #606266;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.upload-hint {
|
|
|
|
+ font-size: 12px;
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-list {
|
|
|
|
+ margin-top: 30px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-list h3 {
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
+ color: #303133;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-item {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ justify-content: space-between;
|
|
|
|
+ padding: 15px;
|
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
|
+ border-radius: 6px;
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
+ background: #fff;
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-item:hover {
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-info {
|
|
|
|
+ display: flex;
|
|
|
|
+ align-items: center;
|
|
|
|
+ flex: 1;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-icon {
|
|
|
|
+ font-size: 24px;
|
|
|
|
+ color: #409eff;
|
|
|
|
+ margin-right: 15px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-details {
|
|
|
|
+ flex: 1;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-name {
|
|
|
|
+ font-weight: 500;
|
|
|
|
+ color: #303133;
|
|
|
|
+ margin-bottom: 5px;
|
|
|
|
+ word-break: break-all;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-size {
|
|
|
|
+ font-size: 12px;
|
|
|
|
+ color: #909399;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.file-actions {
|
|
|
|
+ margin-left: 15px;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.submit-area {
|
|
|
|
+ margin-top: 30px;
|
|
|
|
+ text-align: center;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/* 响应式设计 */
|
|
|
|
+@media (max-width: 768px) {
|
|
|
|
+ .file-upload-container {
|
|
|
|
+ padding: 15px;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .upload-area {
|
|
|
|
+ padding: 30px 20px;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .file-item {
|
|
|
|
+ flex-direction: column;
|
|
|
|
+ align-items: flex-start;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .file-actions {
|
|
|
|
+ margin-left: 0;
|
|
|
|
+ margin-top: 10px;
|
|
|
|
+ align-self: flex-end;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+</style>
|