OfficeSystem/components/task/AttachmentFileUploader.vue

457 lines
9.9 KiB
Vue
Raw Normal View History

2025-11-14 17:57:26 +08:00
<template>
<view class="attachment-block">
<view class="form-item clickable-item" @click="handleChooseFiles">
<view class="form-icon">{{ icon }}</view>
<text class="form-label">{{ title }}</text>
<text class="arrow"></text>
</view>
<view class="files-list" v-if="files.length">
<view
class="file-item"
v-for="(file, index) in files"
:key="file.path + index"
@click="previewFile(file)"
>
<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>
</template>
<script setup>
import { computed } from 'vue';
import { batchUploadFilesToQiniu } from '@/utils/qiniu.js';
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
maxCount: {
type: Number,
default: 5
},
title: {
type: String,
default: '添加文件'
},
icon: {
type: String,
default: '📄'
},
extensions: {
type: Array,
default: () => ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.zip', '.rar', '.jpg', '.png']
}
});
const emit = defineEmits(['update:modelValue', 'change']);
const files = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val);
emit('change', val);
}
});
const handleChooseFiles = async () => {
const remainingCount = props.maxCount - files.value.length;
if (remainingCount <= 0) {
uni.showToast({
title: `最多只能添加${props.maxCount}个文件`,
icon: 'none'
});
return;
}
try {
// #ifdef H5 || MP-WEIXIN || APP-PLUS
await chooseFilesWithUni(remainingCount);
// #endif
// #ifndef H5 || MP-WEIXIN || APP-PLUS
await chooseFilesNative(remainingCount);
// #endif
} catch (error) {
console.error('选择文件失败:', error);
uni.showToast({
title: error?.message || '选择文件失败',
icon: 'none'
});
}
};
const chooseFilesWithUni = (remainingCount) => {
return new Promise((resolve, reject) => {
uni.chooseFile({
count: remainingCount,
extension: props.extensions,
success: async (res) => {
try {
await uploadFiles(res.tempFiles.map(file => ({
path: file.path,
name: file.name
})));
resolve();
} catch (error) {
reject(error);
}
},
fail: async () => {
try {
await chooseFilesNative(remainingCount);
resolve();
} catch (nativeError) {
reject(nativeError);
}
}
});
});
};
const chooseFilesNative = async (remainingCount) => {
if (typeof plus === 'undefined') {
uni.showToast({
title: '当前环境不支持文件选择',
icon: 'none'
});
throw new Error('native file picker not available');
}
return new Promise((resolve, reject) => {
try {
const Intent = plus.android.importClass('android.content.Intent');
const main = plus.android.runtimeMainActivity();
const intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType('*/*');
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
const originalOnActivityResult = main.onActivityResult;
main.startActivityForResult(intent, 1001);
main.onActivityResult = async (requestCode, resultCode, data) => {
if (requestCode === 1001) {
if (resultCode === -1 && data) {
try {
const filesToUpload = extractFilesFromIntent(data, main, remainingCount);
if (filesToUpload.length) {
await uploadFiles(filesToUpload);
}
cleanup();
resolve();
} catch (error) {
cleanup();
reject(error);
}
} else {
cleanup();
reject(new Error('用户取消选择'));
}
} else {
cleanup();
reject(new Error('无效的请求'));
}
};
const cleanup = () => {
if (originalOnActivityResult) {
main.onActivityResult = originalOnActivityResult;
}
};
} catch (error) {
reject(error);
}
});
};
const extractFilesFromIntent = (data, main, remainingCount) => {
const clipData = data.getClipData();
const filesToUpload = [];
const getFileName = (uri) => {
try {
const cursor = main.getContentResolver().query(uri, null, null, null, null);
if (cursor && cursor.moveToFirst()) {
const nameIndex = cursor.getColumnIndex('_display_name');
if (nameIndex !== -1) {
const fileName = cursor.getString(nameIndex);
cursor.close();
return fileName;
}
cursor.close();
}
} catch (e) {
console.error('获取文件名失败:', e);
}
return `file_${Date.now()}`;
};
if (clipData) {
const count = clipData.getItemCount();
for (let i = 0; i < count && filesToUpload.length < remainingCount; i++) {
const item = clipData.getItemAt(i);
const uri = item.getUri();
const uriString = uri.toString();
const fileName = getFileName(uri);
filesToUpload.push({
name: fileName,
path: uriString,
size: 0
});
}
} else {
const uri = data.getData();
if (uri) {
const uriString = uri.toString();
const fileName = getFileName(uri);
filesToUpload.push({
name: fileName,
path: uriString,
size: 0
});
}
}
return filesToUpload;
};
const uploadFiles = async (fileList) => {
if (!fileList.length) return;
try {
uni.showLoading({
title: '上传中...',
mask: true
});
const uploadResults = await batchUploadFilesToQiniu(fileList);
const newFiles = uploadResults.map(result => ({
name: result.name,
path: result.url,
size: result.size || 0
}));
files.value = [...files.value, ...newFiles];
uni.showToast({
title: `成功添加${newFiles.length}个文件`,
icon: 'success'
});
} catch (error) {
console.error('上传文件失败:', error);
uni.showToast({
title: error?.message || '上传文件失败',
icon: 'none'
});
throw error;
} finally {
uni.hideLoading();
}
};
const removeFile = (index) => {
const next = [...files.value];
next.splice(index, 1);
files.value = next;
};
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
});
return;
}
// #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'
});
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
};
</script>
<style scoped lang="scss">
.attachment-block {
margin-bottom: 12px;
}
.form-item {
display: flex;
align-items: center;
padding: 16px;
background-color: #fff;
border-radius: 8px;
gap: 12px;
}
.clickable-item {
cursor: pointer;
&:active {
background-color: #f5f5f5;
}
}
.form-icon {
font-size: 20px;
flex-shrink: 0;
}
.form-label {
flex: 1;
font-size: 15px;
color: #333;
}
.arrow {
font-size: 20px;
color: #999;
flex-shrink: 0;
}
.files-list {
margin-top: 12px;
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
background-color: #fff;
border-radius: 8px;
margin-bottom: 8px;
gap: 12px;
&:active {
background-color: #f5f5f5;
}
}
.file-icon {
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;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 500;
}
.file-size {
font-size: 12px;
color: #999;
}
.remove-btn {
width: 24px;
height: 24px;
background-color: rgba(0, 0, 0, 0.6);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
cursor: pointer;
}
</style>