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>
|
|
|
|
|
|
|
2025-11-18 14:46:09 +08:00
|
|
|
|
<view class="images-preview" v-if="imageFiles.length">
|
|
|
|
|
|
<view
|
|
|
|
|
|
class="image-item"
|
|
|
|
|
|
v-for="(file, index) in imageFiles"
|
|
|
|
|
|
:key="(file.uid || file.path) + index"
|
|
|
|
|
|
>
|
|
|
|
|
|
<image
|
|
|
|
|
|
:src="file.path"
|
|
|
|
|
|
mode="aspectFill"
|
|
|
|
|
|
class="preview-image"
|
|
|
|
|
|
@click="previewImage(index)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<view class="remove-btn" @click.stop="removeFile(file)">✕</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="files-list" v-if="otherFiles.length">
|
2025-11-14 17:57:26 +08:00
|
|
|
|
<view
|
|
|
|
|
|
class="file-item"
|
2025-11-18 14:46:09 +08:00
|
|
|
|
v-for="(file, index) in otherFiles"
|
|
|
|
|
|
:key="(file.uid || file.path) + index"
|
2025-11-14 17:57:26 +08:00
|
|
|
|
@click="previewFile(file)"
|
|
|
|
|
|
>
|
2025-11-18 14:46:09 +08:00
|
|
|
|
<text class="file-type-badge" :class="getFileTypeClass(file.name)">
|
|
|
|
|
|
{{ getFileTypeLabel(file.name) }}
|
|
|
|
|
|
</text>
|
2025-11-14 17:57:26 +08:00
|
|
|
|
<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>
|
2025-11-18 14:46:09 +08:00
|
|
|
|
<view class="file-icon">{{ getFileIcon(file.name) }}</view>
|
|
|
|
|
|
<view class="remove-btn" @click.stop="removeFile(file)">✕</view>
|
2025-11-14 17:57:26 +08:00
|
|
|
|
</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);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-11-18 14:46:09 +08:00
|
|
|
|
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'heic', 'heif', 'svg'];
|
|
|
|
|
|
|
|
|
|
|
|
const isImageFile = (file) => {
|
|
|
|
|
|
if (!file) return false;
|
|
|
|
|
|
const ext = getFileExtension(file.name);
|
|
|
|
|
|
return imageExtensions.includes(ext);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const imageFiles = computed(() => files.value.filter((file) => isImageFile(file)));
|
|
|
|
|
|
const otherFiles = computed(() => files.value.filter((file) => !isImageFile(file)));
|
|
|
|
|
|
|
2025-11-14 17:57:26 +08:00
|
|
|
|
const handleChooseFiles = async () => {
|
|
|
|
|
|
const remainingCount = props.maxCount - files.value.length;
|
|
|
|
|
|
if (remainingCount <= 0) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: `最多只能添加${props.maxCount}个文件`,
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-11-17 17:22:57 +08:00
|
|
|
|
// #ifdef APP-PLUS
|
|
|
|
|
|
await chooseFilesNative(remainingCount);
|
2025-11-14 17:57:26 +08:00
|
|
|
|
// #endif
|
|
|
|
|
|
|
2025-11-17 17:22:57 +08:00
|
|
|
|
// #ifndef APP-PLUS
|
|
|
|
|
|
await chooseFilesWithUni(remainingCount);
|
2025-11-14 17:57:26 +08:00
|
|
|
|
// #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 {
|
2025-11-18 14:46:09 +08:00
|
|
|
|
await uploadFiles(res.tempFiles.map((file) => ({
|
2025-11-14 17:57:26 +08:00
|
|
|
|
path: file.path,
|
2025-11-18 14:46:09 +08:00
|
|
|
|
name: file.name,
|
|
|
|
|
|
size: file.size || 0
|
2025-11-14 17:57:26 +08:00
|
|
|
|
})));
|
|
|
|
|
|
resolve();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
reject(error);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-11-17 17:22:57 +08:00
|
|
|
|
fail: () => {
|
|
|
|
|
|
reject(new Error('文件选择被取消或失败'));
|
2025-11-14 17:57:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-18 14:46:09 +08:00
|
|
|
|
const removeFile = (file) => {
|
|
|
|
|
|
const next = files.value.filter((item) => item !== file);
|
2025-11-14 17:57:26 +08:00
|
|
|
|
files.value = next;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-18 14:46:09 +08:00
|
|
|
|
const getFileExtension = (fileName = '') => {
|
|
|
|
|
|
if (!fileName) return '';
|
|
|
|
|
|
return fileName.split('.').pop().toLowerCase();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-14 17:57:26 +08:00
|
|
|
|
const getFileIcon = (fileName) => {
|
2025-11-18 14:46:09 +08:00
|
|
|
|
const ext = getFileExtension(fileName);
|
2025-11-14 17:57:26 +08:00
|
|
|
|
const iconMap = {
|
|
|
|
|
|
pdf: '📕',
|
|
|
|
|
|
doc: '📘',
|
|
|
|
|
|
docx: '📘',
|
|
|
|
|
|
xls: '📗',
|
|
|
|
|
|
xlsx: '📗',
|
|
|
|
|
|
ppt: '📙',
|
|
|
|
|
|
pptx: '📙',
|
|
|
|
|
|
txt: '📄',
|
|
|
|
|
|
zip: '📦',
|
|
|
|
|
|
rar: '📦',
|
|
|
|
|
|
jpg: '🖼️',
|
|
|
|
|
|
jpeg: '🖼️',
|
|
|
|
|
|
png: '🖼️',
|
|
|
|
|
|
gif: '🖼️'
|
|
|
|
|
|
};
|
|
|
|
|
|
return iconMap[ext] || '📄';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-18 14:46:09 +08:00
|
|
|
|
const getFileTypeKey = (fileName) => {
|
|
|
|
|
|
const ext = getFileExtension(fileName);
|
|
|
|
|
|
if (!ext) return 'other';
|
|
|
|
|
|
if (imageExtensions.includes(ext)) return 'image';
|
|
|
|
|
|
if (['pdf'].includes(ext)) return 'pdf';
|
|
|
|
|
|
if (['doc', 'docx', 'wps'].includes(ext)) return 'doc';
|
|
|
|
|
|
if (['xls', 'xlsx', 'csv'].includes(ext)) return 'xls';
|
|
|
|
|
|
if (['ppt', 'pptx'].includes(ext)) return 'ppt';
|
|
|
|
|
|
if (['zip', 'rar', '7z'].includes(ext)) return 'zip';
|
|
|
|
|
|
return 'other';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getFileTypeLabel = (fileName) => {
|
|
|
|
|
|
const type = getFileTypeKey(fileName);
|
|
|
|
|
|
const labelMap = {
|
|
|
|
|
|
image: 'IMG',
|
|
|
|
|
|
pdf: 'PDF',
|
|
|
|
|
|
doc: 'DOC',
|
|
|
|
|
|
xls: 'XLS',
|
|
|
|
|
|
ppt: 'PPT',
|
|
|
|
|
|
zip: 'ZIP',
|
|
|
|
|
|
other: 'FILE'
|
|
|
|
|
|
};
|
|
|
|
|
|
return labelMap[type] || 'FILE';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getFileTypeClass = (fileName) => {
|
|
|
|
|
|
return `badge-${getFileTypeKey(fileName)}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-14 17:57:26 +08:00
|
|
|
|
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];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-18 14:46:09 +08:00
|
|
|
|
const previewImage = (index) => {
|
|
|
|
|
|
const urls = imageFiles.value.map((file) => file.path);
|
|
|
|
|
|
if (!urls.length) return;
|
|
|
|
|
|
uni.previewImage({
|
|
|
|
|
|
urls,
|
|
|
|
|
|
current: urls[index] || urls[0]
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-14 17:57:26 +08:00
|
|
|
|
const previewFile = (file) => {
|
|
|
|
|
|
if (!file?.path) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: '文件路径不存在',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 14:46:09 +08:00
|
|
|
|
const ext = getFileExtension(file.name);
|
|
|
|
|
|
if (imageExtensions.includes(ext)) {
|
|
|
|
|
|
previewImage(imageFiles.value.findIndex((item) => item === file));
|
2025-11-14 17:57:26 +08:00
|
|
|
|
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;
|
2025-11-18 14:46:09 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 8px;
|
2025-11-14 17:57:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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-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;
|
|
|
|
|
|
}
|
2025-11-18 14:46:09 +08:00
|
|
|
|
|
|
|
|
|
|
.images-preview {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-item {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: calc((100% - 16px) / 3);
|
|
|
|
|
|
aspect-ratio: 1;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background-color: #f4f4f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.preview-image {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-item .remove-btn {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 6px;
|
|
|
|
|
|
right: 6px;
|
|
|
|
|
|
width: 22px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-type-badge {
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-pdf {
|
|
|
|
|
|
background-color: #fff1f0;
|
|
|
|
|
|
color: #f5222d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-doc {
|
|
|
|
|
|
background-color: #f0f5ff;
|
|
|
|
|
|
color: #2f54eb;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-xls {
|
|
|
|
|
|
background-color: #f6ffed;
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-ppt {
|
|
|
|
|
|
background-color: #fff7e6;
|
|
|
|
|
|
color: #fa8c16;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-zip {
|
|
|
|
|
|
background-color: #f9f0ff;
|
|
|
|
|
|
color: #722ed1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-image {
|
|
|
|
|
|
background-color: #fff7e6;
|
|
|
|
|
|
color: #d48806;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-other {
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
color: #888;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-icon {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
2025-11-14 17:57:26 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
|