实现文件上传

This commit is contained in:
WindowBird 2025-11-06 18:01:41 +08:00
parent 3c2365660b
commit fc65bfedcb
3 changed files with 477 additions and 22 deletions

View File

@ -62,10 +62,14 @@
class="file-item"
v-for="(file, index) in formData.files"
:key="index"
@click="previewFile(file)"
>
<text class="file-icon">📄</text>
<text class="file-name">{{ file.name }}</text>
<view class="remove-btn" @click="removeFile(index)"></view>
<text class="file-icon">{{ getFileIcon(file.name) }}</text>
<view class="file-info">
<text class="file-name">{{ file.name }}</text>
<text class="file-size" v-if="file.size > 0">{{ formatFileSize(file.size) }}</text>
</view>
<view class="remove-btn" @click.stop="removeFile(index)"></view>
</view>
</view>
</view>
@ -121,7 +125,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { chooseAndUploadImages } from '@/utils/qiniu.js';
import { chooseAndUploadImages, uploadFileToQiniu, batchUploadFilesToQiniu } from '@/utils/qiniu.js';
import { submitTask } from '@/common/api.js';
//
@ -273,8 +277,8 @@ const removeImage = (index) => {
formData.value.images.splice(index, 1);
};
//
const chooseFiles = () => {
//
const chooseFiles = async () => {
const remainingCount = 5 - formData.value.files.length;
if (remainingCount <= 0) {
uni.showToast({
@ -284,6 +288,72 @@ const chooseFiles = () => {
return;
}
// 使 uni.chooseFileH5
// #ifdef H5 || MP-WEIXIN || APP-PLUS
try {
uni.chooseFile({
count: remainingCount,
extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.zip', '.rar'],
success: async (res) => {
try {
uni.showLoading({
title: '上传中...',
mask: true
});
//
const uploadResults = await batchUploadFilesToQiniu(
res.tempFiles.map(file => ({
path: file.path,
name: file.name
}))
);
//
const newFiles = uploadResults.map(result => ({
name: result.name,
path: result.url, // URL
size: result.size
}));
formData.value.files = [...formData.value.files, ...newFiles];
uni.hideLoading();
uni.showToast({
title: `成功添加${newFiles.length}个文件`,
icon: 'success'
});
} catch (error) {
uni.hideLoading();
console.error('上传文件失败:', error);
uni.showToast({
title: error.message || '上传文件失败',
icon: 'none'
});
}
},
fail: (err) => {
console.error('选择文件失败:', err);
// uni.chooseFile使
chooseFilesNative();
}
});
} catch (error) {
// uni.chooseFile使
chooseFilesNative();
}
// #endif
// #ifndef H5 || MP-WEIXIN || APP-PLUS
// 使
chooseFilesNative();
// #endif
};
//
const chooseFilesNative = async () => {
const remainingCount = 5 - formData.value.files.length;
// 使 plus API
if (typeof plus !== 'undefined') {
try {
@ -301,7 +371,7 @@ const chooseFiles = () => {
//
const originalOnActivityResult = main.onActivityResult;
main.onActivityResult = (requestCode, resultCode, data) => {
main.onActivityResult = async (requestCode, resultCode, data) => {
if (requestCode === 1001) {
if (resultCode === -1 && data) { // RESULT_OK = -1
try {
@ -360,7 +430,38 @@ const chooseFiles = () => {
}
if (files.length > 0) {
formData.value.files = [...formData.value.files, ...files];
//
uni.showLoading({
title: '上传中...',
mask: true
});
try {
//
const uploadResults = await batchUploadFilesToQiniu(files);
//
const newFiles = uploadResults.map(result => ({
name: result.name,
path: result.url, // URL
size: result.size
}));
formData.value.files = [...formData.value.files, ...newFiles];
uni.hideLoading();
uni.showToast({
title: `成功添加${newFiles.length}个文件`,
icon: 'success'
});
} catch (uploadError) {
uni.hideLoading();
console.error('上传文件失败:', uploadError);
uni.showToast({
title: uploadError.message || '上传文件失败',
icon: 'none'
});
}
}
// onActivityResult
@ -368,6 +469,7 @@ const chooseFiles = () => {
main.onActivityResult = originalOnActivityResult;
}
} catch (error) {
uni.hideLoading();
console.error('处理文件选择结果失败:', error);
uni.showToast({
title: '处理文件失败',
@ -402,6 +504,104 @@ const removeFile = (index) => {
formData.value.files.splice(index, 1);
};
//
const getFileIcon = (fileName) => {
if (!fileName) return '📄';
const ext = fileName.split('.').pop().toLowerCase();
const iconMap = {
'pdf': '📕',
'doc': '📘',
'docx': '📘',
'xls': '📗',
'xlsx': '📗',
'ppt': '📙',
'pptx': '📙',
'txt': '📄',
'zip': '📦',
'rar': '📦',
'jpg': '🖼️',
'jpeg': '🖼️',
'png': '🖼️',
'gif': '🖼️'
};
return iconMap[ext] || '📄';
};
//
const formatFileSize = (bytes) => {
if (!bytes || bytes === 0) return '';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
};
// /
const previewFile = (file) => {
if (!file.path) {
uni.showToast({
title: '文件路径不存在',
icon: 'none'
});
return;
}
// 使
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const ext = file.name.split('.').pop().toLowerCase();
if (imageExts.includes(ext)) {
uni.previewImage({
urls: [file.path],
current: file.path
});
} else {
//
// #ifdef H5
window.open(file.path, '_blank');
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(file.path);
// #endif
// #ifndef H5 || APP-PLUS
uni.showToast({
title: '点击下载文件',
icon: 'none'
});
// API
uni.downloadFile({
url: file.path,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => {
console.log('打开文档成功');
},
fail: (err) => {
console.error('打开文档失败:', err);
uni.showToast({
title: '无法打开此文件',
icon: 'none'
});
}
});
}
},
fail: (err) => {
console.error('下载文件失败:', err);
uni.showToast({
title: '下载文件失败',
icon: 'none'
});
}
});
// #endif
}
};
//
const formatTimeToChinese = (date) => {
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
@ -440,9 +640,10 @@ const handleSubmit = async () => {
try {
// URL
// 使URL
const allAttaches = [
...formData.value.images, // URL
...formData.value.files.map(file => file.path) // URL使
...formData.value.files.map(file => file.path) // URL
].filter(url => url && url.trim() !== ''); //
//
@ -656,9 +857,8 @@ const handleSubmit = async () => {
}
.remove-btn {
position: absolute;
top: 4px;
right: 4px;
position: relative;
width: 24px;
height: 24px;
background-color: rgba(0, 0, 0, 0.6);
@ -685,20 +885,38 @@ const handleSubmit = async () => {
border-radius: 8px;
margin-bottom: 8px;
gap: 12px;
cursor: pointer;
&:active {
background-color: #f5f5f5;
}
}
.file-icon {
font-size: 20px;
font-size: 24px;
flex-shrink: 0;
}
.file-name {
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.file-name {
font-size: 14px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.file-size {
font-size: 12px;
color: #999;
}
/* 进度选择弹窗 */

View File

@ -154,9 +154,13 @@
class="file-attachment-item"
v-for="(file, fileIndex) in record.fileAttachments"
:key="fileIndex"
@click="previewRecordFile(file)"
>
<text class="file-icon">📄</text>
<text class="file-name">{{ file.name }}</text>
<text class="file-icon">{{ getFileIcon(file.name) }}</text>
<view class="file-info">
<text class="file-name">{{ file.name }}</text>
<text class="file-size" v-if="file.size > 0">{{ formatFileSize(file.size) }}</text>
</view>
</view>
</view>
<view class="delay-btn-wrapper" v-if="record.showDelayBtn">
@ -396,7 +400,7 @@ const transformSubmitRecords = (submitList) => {
userName: item.userName || '',
userAvatar: item.userAvatar || '',
time: formatTimeToChinese(item.createTime) || '',
content: item.remark || item.description || item.taskDescription || '', //
content: item.remark || '', //
progress: null, // API
imageAttachments: imageAttachments,
fileAttachments: fileAttachments,
@ -492,6 +496,105 @@ const previewRecordImages = (imageUrls, index) => {
}
};
//
const getFileIcon = (fileName) => {
if (!fileName) return '📄';
const ext = fileName.split('.').pop().toLowerCase();
const iconMap = {
'pdf': '📕',
'doc': '📘',
'docx': '📘',
'xls': '📗',
'xlsx': '📗',
'ppt': '📙',
'pptx': '📙',
'txt': '📄',
'zip': '📦',
'rar': '📦',
'jpg': '🖼️',
'jpeg': '🖼️',
'png': '🖼️',
'gif': '🖼️'
};
return iconMap[ext] || '📄';
};
//
const formatFileSize = (bytes) => {
if (!bytes || bytes === 0) return '';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
};
// /
const previewRecordFile = (file) => {
if (!file.path) {
uni.showToast({
title: '文件路径不存在',
icon: 'none'
});
return;
}
// 使
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
const ext = file.name ? file.name.split('.').pop().toLowerCase() : '';
if (imageExts.includes(ext)) {
uni.previewImage({
urls: [file.path],
current: file.path
});
} else {
//
// #ifdef H5
window.open(file.path, '_blank');
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(file.path);
// #endif
// #ifndef H5 || APP-PLUS
uni.showToast({
title: '正在下载文件...',
icon: 'loading',
duration: 2000
});
//
uni.downloadFile({
url: file.path,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => {
console.log('打开文档成功');
},
fail: (err) => {
console.error('打开文档失败:', err);
uni.showToast({
title: '无法打开此文件',
icon: 'none'
});
}
});
}
},
fail: (err) => {
console.error('下载文件失败:', err);
uni.showToast({
title: '下载文件失败',
icon: 'none'
});
}
});
// #endif
}
};
// uv-tags
const getTagType = (tagText) => {
const status = getStatusFromTagText(tagText);
@ -1113,23 +1216,42 @@ onShow(() => {
.file-attachment-item {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
gap: 12px;
padding: 12px;
background-color: #f5f5f5;
border-radius: 4px;
border-radius: 8px;
cursor: pointer;
&:active {
background-color: #e0e0e0;
}
}
.file-icon {
font-size: 16px;
font-size: 24px;
flex-shrink: 0;
}
.file-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.file-name {
font-size: 14px;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.file-size {
font-size: 12px;
color: #999;
}
.no-record {

View File

@ -184,6 +184,121 @@ export const chooseAndUploadImages = (options = {}) => {
})
}
/**
* 上传单个文件到七牛云支持图片和文档
* @param {string} filePath - 文件临时路径或URI
* @param {string} fileName - 文件名可选用于文档文件
* @param {object} [options] - 配置选项
* @param {number} [options.maxSize=50] - 最大文件大小MB文档文件默认50MB
* @param {string} [options.prefix='uploads/'] - 文件前缀路径
* @param {boolean} [options.showToast=true] - 是否显示提示
* @param {string} [options.domain='https://api.ccttiot.com'] - 七牛云域名
* @returns {Promise<object>} 返回 { url: string, name: string, size: number }
*/
export const uploadFileToQiniu = async (filePath, fileName = '', options = {}) => {
const {
maxSize = 50,
prefix = 'uploads/',
showToast = true,
domain = 'https://api.ccttiot.com',
} = options
try {
// 1. 如果是URI格式content://),需要先转换为临时路径
let tempFilePath = filePath
if (filePath.startsWith('content://')) {
// 在uni-app中content:// URI需要特殊处理
// 对于文档文件uni.uploadFile可以直接使用URI
tempFilePath = filePath
}
// 2. 获取文件信息如果不是URI格式
let fileInfo = { size: 0 }
if (!filePath.startsWith('content://')) {
try {
fileInfo = await getFileInfo(tempFilePath)
} catch (error) {
console.warn('获取文件信息失败:', error)
}
}
const sizeInMB = fileInfo.size / (1024 * 1024)
if (sizeInMB > maxSize) {
throw new Error(`文件大小不能超过 ${maxSize}MB`)
}
// 3. 获取上传凭证
const token = await getQiniuToken()
if (!token) {
throw new Error('获取上传凭证失败')
}
// 4. 生成唯一文件名
let fileExt = 'file'
if (fileName) {
const parts = fileName.split('.')
if (parts.length > 1) {
fileExt = parts.pop().toLowerCase()
}
} else {
fileExt = getFileExtension(tempFilePath)
}
const key = generateUniqueKey(prefix, fileExt)
// 5. 上传到七牛云
const uploadResult = await uploadToQiniu(tempFilePath, token, key)
// 验证上传结果
if (!uploadResult || typeof uploadResult !== 'object') {
throw new Error('上传失败:返回数据格式错误')
}
if (!uploadResult.key) {
console.error('上传响应缺少key字段:', uploadResult)
throw new Error('上传失败未返回文件key')
}
// 6. 构建完整URL
const qiniuUrl = `${domain}/${uploadResult.key}`
console.log('文件上传成功:', { key: uploadResult.key, url: qiniuUrl })
// 7. 生成文件名(如果没有提供)
const finalFileName = fileName || `文件.${fileExt}`
if (showToast) {
// 上传成功不显示提示,避免打扰用户
}
return {
url: qiniuUrl,
name: finalFileName,
size: fileInfo.size || 0
}
} catch (error) {
console.error('文件上传失败:', error)
if (showToast) {
uni.showToast({
title: error.message || '上传失败',
icon: 'none',
})
}
throw error
}
}
/**
* 批量上传文件到七牛云支持图片和文档
* @param {Array<{path: string, name?: string}>} files - 文件数组每个文件包含path和可选的name
* @param {object} [options] - 配置选项
* @returns {Promise<Array<{url: string, name: string, size: number}>>} 上传结果数组
*/
export const batchUploadFilesToQiniu = async (files, options = {}) => {
return Promise.all(
files.map(file => uploadFileToQiniu(file.path, file.name || '', { ...options, showToast: false }))
)
}
/**
* 获取文件信息
* @private