OfficeSystem/components/task/AttachmentFileUploader.vue
2025-11-14 17:57:26 +08:00

457 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>