<!-- * 上传器 --> <script setup lang="ts"> import { ref, reactive, onMounted, computed, nextTick, onBeforeUnmount, } from 'vue'; import { useI18n } from 'vue-i18n'; import { useMessage } from 'src/common/hooks'; import { getObjectURL } from './utils'; var XLSX = require('xlsx'); const { t } = useI18n(); const { warn } = useMessage(); interface IDetailProps { bgColor?: string; // 背景色 multiple?: boolean; // 多选 accept?: string; // 上传类型 'image/*' dark?: boolean; // 启用深色背景 noShadow?: boolean; // 阴影 } const props = withDefaults(defineProps<IDetailProps>(), { bgColor: '#96C0CE', multiple: false, accept: '', dark: false, noShadow: false, }); const emit = defineEmits<{ (e: 'onOk', value: any): void; }>(); const darkDeep = '#222831'; const darkLight = '#393E46'; const imageTypeList = ['png', 'jpg', 'gif', 'svg']; const excelTypeList = ['xlsx', 'xls']; const otherTypeList = ['pdf']; const dragContentRef = ref<any>(null); const viewModel = ref(false); const fileType = ref(''); const viewDialogTitle = ref(''); const isDragenter = ref(false); // 拖动元素是否进入有效放置目标 const state = reactive({ fileList: [] as any[], viewTableHtml: null as any, }); const canPreview = computed(() => (type: any) => { const viewTypeList = imageTypeList.concat(excelTypeList, otherTypeList); return viewTypeList.includes(type); }); const totalSize = computed(() => { const total = state.fileList.reduce((a, b) => a + b.size, 0); let unit: string; // 单位 let size: number | string = 0; if (total < 1000) { unit = 'B'; size = total.toFixed(3); } else { const KB_size = total * 0.0009765625; if (KB_size < 1000) { unit = 'KB'; size = KB_size.toFixed(3); } else { const MB_size = KB_size * 0.0009765625; if (MB_size < 1000) { unit = 'MB'; size = MB_size.toFixed(2); } else { const GB_size = MB_size * 0.0009765625; unit = 'GB'; size = GB_size.toFixed(2); } } } return size + unit; }); onMounted(() => { document.addEventListener('drop', windowDrop, false); document.addEventListener('dragover', windowDragover, false); if (dragContentRef.value) { dragContentRef.value.addEventListener('dragenter', onDragEnter, false); dragContentRef.value.addEventListener('dragover', onDragOver, false); dragContentRef.value.addEventListener('dragleave', onDragleave, false); dragContentRef.value.addEventListener('drop', onDrop, false); } }); onBeforeUnmount(() => { dragContentRef.value.removeEventListener('dragenter', onDragEnter, false); dragContentRef.value.removeEventListener('dragover', onDragOver, false); dragContentRef.value.removeEventListener('dragleave', onDragleave, false); dragContentRef.value.removeEventListener('drop', onDrop, false); document.removeEventListener('drop', windowDrop, false); document.removeEventListener('dragover', windowDragover, false); }); function windowDrop(e: any) { e.preventDefault(); } function windowDragover(e: any) { e.preventDefault(); isDragenter.value = false; } // 当拖动的元素或被选择的文本进入有效的放置目标时, dragenter 事件被触发。 function onDragEnter(e: any) { e.stopPropagation(); e.preventDefault(); isDragenter.value = true; } // 当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover 事件(每几百毫秒触发一次)。 function onDragOver(e: any) { e.stopPropagation(); e.preventDefault(); return false; } // dragleave 事件在拖动的元素或选中的文本离开一个有效的放置目标时被触发。 function onDragleave(e: any) { e.stopPropagation(); e.preventDefault(); } // drop 事件在元素或选中的文本被放置在有效的放置目标上时被触发。 function onDrop(e: any) { e.stopPropagation(); e.preventDefault(); console.log('放置'); // 当文件拖拽到dropBox区域时,可以在该事件取到files handleFile(e.dataTransfer.files); isDragenter.value = false; } function selectFile() { let _input: HTMLInputElement = document.createElement('input'); // 创建一个隐藏的input _input.setAttribute('type', 'file'); if (props.multiple) { _input.setAttribute('multiple', 'multiple'); // 多选 } // _input.setAttribute('accept', 'image/*'); // 上传类型 if (props.accept) { _input.setAttribute('accept', props.accept); } _input.onchange = function (e: any) { handleFile(e.target.files); }; _input.click(); } function handleFile(data: any) { let arr = [] as any[]; for (let i = 0; i < data.length; i++) { const _file = data[i]; const typeArr = _file.name.split('.'); const type = typeArr.length > 1 ? typeArr[typeArr.length - 1] : ''; let unit: string; // 单位 let size: number | string = 0; if (_file.size < 1000) { unit = 'B'; size = _file.size.toFixed(3); } else { const KB_size = _file.size * 0.0009765625; if (KB_size < 1000) { unit = 'KB'; size = KB_size.toFixed(3); } else { const MB_size = KB_size * 0.0009765625; if (MB_size < 1000) { unit = 'MB'; size = MB_size.toFixed(2); } else { const GB_size = MB_size * 0.0009765625; if (GB_size <= 2) { unit = 'GB'; size = GB_size.toFixed(2); } else { const msg = '文件过大,请选择不超过2GB的文件'; warn(msg); console.warn(msg); isDragenter.value = false; return; } } } } arr.push({ file: _file, name: _file.name, sizeFormat: size + unit, fileType: type, size: _file.size, }); } nextTick(() => { state.fileList = state.fileList.concat(arr); }); } function removeItem(index: number) { state.fileList.splice(index, 1); } function removeAll() { state.fileList = []; } function Ok() { let list = []; for (const item of state.fileList) { list.push(item); } emit('onOk', list); } // 预览 function onView(data: any) { if (excelTypeList.includes(data.fileType)) { fileType.value = data.fileType; viewDialogTitle.value = data.name; viewModel.value = true; void viewXlsx(data); } else { const url = getObjectURL(data.file); window.open(url); window.URL.revokeObjectURL(url); } } async function viewXlsx(data: any) { const file = data.file; const _data = await file.arrayBuffer(); const wb = XLSX.read(_data); const ws = wb.Sheets[wb.SheetNames[0]]; const ele = XLSX.utils.sheet_to_html(ws, { id: 'tabeller' }); state.viewTableHtml = ele; } function closeViewDialog() { viewModel.value = false; fileType.value = ''; state.viewTableHtml = null; } </script> <template> <div class="com-uploader" :class="{ 'shadow-4': !props.noShadow }" :style="{ borderColor: props.dark ? darkDeep : props.bgColor, backgroundColor: props.dark ? darkLight : '#ffffff', }" > <div class="uploader-title" :style="{ backgroundColor: props.dark ? darkDeep : props.bgColor }" > <div class="icon"> <q-btn v-if="state.fileList.length > 0" style="width: 28px" class="q-mr-xs" dense size="12px" label="OK" :title="t('Confirm upload')" :style="{ backgroundColor: props.dark ? props.bgColor : '#fff', color: props.dark ? darkLight : props.bgColor, }" @click="Ok" /> <q-btn v-if="state.fileList.length > 0" dense flat icon="bi-trash" size="12px" :title="t('clear all')" :style="{ color: props.dark ? props.bgColor : '#fff' }" @click="removeAll" /> <span v-if="state.fileList.length > 0" :style="{ color: props.dark ? props.bgColor : '#fff', }" class="total-file-size" >{{ totalSize }}</span > </div> <div class="btn"> <q-btn square dense push icon="add" size="12px" :title="t('Select file')" :label="t('Select file')" :style="{ backgroundColor: props.dark ? props.bgColor : '#fff', color: props.dark ? darkLight : props.bgColor, }" @click="selectFile" /> </div> </div> <div id="drag-content" class="uploader-content" :class="{ 'active-dragenter': isDragenter }" ref="dragContentRef" > <div class="uploader-main-content"> <div class="file-item" :style="{ borderColor: props.bgColor }" v-for="(item, index) in state.fileList" :key="index" > <div class="name" :title="item.name + ' ' + item.sizeFormat" :style="{ color: props.bgColor }" > <span class="file-name">{{ item.name }}</span> <span class="file-size"> {{ item.sizeFormat }}</span> </div> <div class="btns"> <div> <q-btn v-if="canPreview(item.fileType)" flat dense size="12px" :title="t('preview')" icon="bi-eye" :style="{ color: props.bgColor }" @click="onView(item)" /> </div> <div> <q-btn flat dense size="12px" :title="t('remove from list')" icon="bi-x-circle" :style="{ color: props.bgColor }" @click="removeItem(index)" /> </div> </div> </div> </div> </div> </div> <q-dialog v-model="viewModel" persistent> <q-card style="min-height: 600px; min-width: 600px; max-width: 1400px" class="column no-wrap" > <q-card-section class="row items-center q-pb-none"> <div class="text-h6">{{ viewDialogTitle }}</div> <q-space /> <q-btn icon="close" flat round dense @click="closeViewDialog" /> </q-card-section> <q-card-section class="q-pt-none overflow-auto" style="flex: 1"> <div v-if="excelTypeList.includes(fileType)"> <div v-html="state.viewTableHtml" class="table-view"></div> </div> </q-card-section> </q-card> </q-dialog> </template> <style lang="scss" scoped> // 上传器 .com-uploader { width: 100%; min-width: 400px; height: 100%; min-height: 200px; border-radius: 4px; box-sizing: border-box; border: 1px solid; display: flex; flex-flow: column nowrap; position: relative; // box-shadow: rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, // rgba(0, 0, 0, 0.3) 0px 3px 7px -3px; .total-file-size { display: inline-block; font-size: 12px; } .uploader-title { height: 46px; padding: 8px; display: flex; flex-flow: row nowrap; justify-content: space-between; align-items: center; } .uploader-content { flex: 1; overflow: auto; } .active-dragenter { box-shadow: rgb(204, 219, 232) 3px 3px 6px 0px inset, rgba(255, 255, 255, 0.5) -3px -3px 6px 1px inset; opacity: 0.5; background-color: rgba(128, 128, 128, 0.808); // border: 1px solid red; } .uploader-main-content { display: grid; grid-template-columns: 100%; row-gap: 4px; margin: 4px; .file-item { height: 40px; box-sizing: border-box; border-bottom: 2px solid; display: flex; flex-flow: row nowrap; justify-content: space-between; align-items: center; .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; .file-name { font-weight: bold; } .file-size { font-size: 12px; } } .btns { display: grid; grid-template-columns: repeat(2, 1fr); column-gap: 2px; > div { width: 28px; } } } } // .dialog-box { // // min-width: 600px; // min-height: 600px; // } } </style>