| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567 |
- <template>
- <div class="file-uploader-container">
- <el-upload
- ref="uploadRef"
- :action="uploadUrl"
- :headers="headers"
- :method="method"
- :multiple="multiple"
- :data="uploadData"
- :name="fileFieldName"
- :with-credentials="withCredentials"
- :show-file-list="showFileList"
- :accept="accept"
- :limit="limit"
- :file-list="fileList"
- :auto-upload="autoUpload"
- :drag="drag"
- :list-type="listType"
- :before-upload="handleBeforeUpload"
- :before-remove="handleBeforeRemove"
- :on-success="handleSuccess"
- :on-error="handleError"
- :on-progress="handleProgress"
- :on-change="handleChange"
- :on-remove="handleRemove"
- :on-exceed="handleExceed"
- :on-preview="handlePreview"
- :on-download="handleDownload"
- >
- <template v-if="drag">
- <el-icon class="el-icon--upload"><upload-filled /></el-icon>
- <div class="el-upload__text">
- 将文件拖到此处,或 <em>点击上传</em>
- </div>
- <template v-if="tip" class="el-upload__tip">{{ tip }}</template>
- </template>
- <template v-else-if="listType === 'picture-card'">
- <el-icon><Plus /></el-icon>
- </template>
-
-
- <template v-else>
- <slot name="trigger">
- <el-button type="primary" class="gradient" size="large">
- <el-icon><Plus /></el-icon>
- <span class="ml10">{{ buttonText }}</span>
- </el-button>
- </slot>
- <template v-if="tip" class="el-upload__tip">
- <span class="ml10 gray999">{{ tip }}</span>
- </template>
- </template>
-
- <template >
- <div v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item">
- <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
- <span class="el-upload-list__item-actions">
- <span
- class="el-upload-list__item-preview"
- @click="handlePreview(file,index)"
- >
- <el-icon><zoom-in /></el-icon>
- </span>
- <!-- <span
- v-if="!disabled"
- class="el-upload-list__item-delete"
- @click="handleDownload(file)"
- >
- <el-icon><Download /></el-icon>
- </span> -->
- <span
- v-if="!disabled"
- class="el-upload-list__item-delete"
- @click="handleRemove(file,index)"
- >
- <el-icon><Delete /></el-icon>
- </span>
- </span>
- </div>
- <!-- <slot name="file-list" :file-list="fileList">
- <el-upload-list :file-list="fileList" @remove="handleRemove">
- <template #default="{ file }">
- <el-upload-list-item
- :file="file"
- :name="fileFieldName"
- @remove="handleRemove"
- >
- <template #actions>
- <el-button
- type="text"
- size="small"
- @click="handlePreview(file)"
- >
- <el-icon><zoom-in /></el-icon>
- 预览
- </el-button>
- <el-button
- type="text"
- size="small"
- @click="handleDownload(file)"
- >
- <el-icon><download /></el-icon>
- 下载
- </el-button>
- <el-button
- type="text"
- size="small"
- @click="handleRemove(file)"
- >
- <el-icon><delete /></el-icon>
- 删除
- </el-button>
- </template>
- </el-upload-list-item>
- </template>
- </el-upload-list>
- </slot> -->
- </template>
-
- <!-- 上传进度条 -->
- <template v-if="showProgress" #progress="{ file }">
- <el-progress
- :percentage="file.percentage"
- :stroke-width="2"
- :show-text="false"
- />
- </template>
- </el-upload>
-
- <!-- 预览对话框 -->
- <el-dialog
- v-model="previewVisible"
- :title="previewTitle"
- width="50%"
- destroy-on-close
- >
- <div class="preview-container">
- <template v-if="isImageFile(currentPreviewFile)">
- <img :src="currentPreviewFile.url" alt="预览" class="preview-image" />
- </template>
-
- <template v-else-if="isTextFile(currentPreviewFile)">
- <pre class="preview-text">{{ currentPreviewContent }}</pre>
- </template>
-
- <template v-else>
- <div class="preview-other">
- <el-icon class="file-icon"><document /></el-icon>
- <div class="file-info">
- <div class="file-name">{{ currentPreviewFile.name }}</div>
- <div class="file-size">{{ formatFileSize(currentPreviewFile.size) }}</div>
- </div>
- </div>
- </template>
- </div>
- </el-dialog>
- </div>
- </template>
- <script setup>
- import { ref, computed, watch } from 'vue'
- import DGTMessage from '@/utils/message'
- import { downloadFile } from '@/utils/util'
- // import {
- // UploadFilled,
- // ZoomIn,
- // Download,
- // Delete,
- // Document,
- // Plus
- // } from '@element-plus/icons-vue'
- // 组件属性
- const props = defineProps({
- disabled: {
- type: Boolean,
- default: false
- },
- // 上传地址
- uploadUrl: {
- type: String,
- default: import.meta.env.VITE_API_BASE_URL + '/upload'
- },
- // 请求头
- headers: {
- type: Object,
- default: () => ({})
- },
- // 请求方法
- method: {
- type: String,
- default: 'post'
- },
- // 是否支持多文件上传
- multiple: {
- type: Boolean,
- default: false
- },
- // 上传时附带的额外参数
- data: {
- type: Object,
- default: () => ({
- //"note","工作流""workflow","工作流""video","视频""common","公共"
- "directory":"common"
- })
- },
- // 文件字段名
- fileFieldName: {
- type: String,
- default: 'file'
- },
- // 是否携带凭证信息
- withCredentials: {
- type: Boolean,
- default: false
- },
- // 是否显示已上传文件列表
- showFileList: {
- type: Boolean,
- default: true
- },
- // 接受的文件类型
- accept: {
- type: String,
- default: ''
- },
- // 最大允许上传文件数量
- limit: {
- type: Number,
- default: 0 // 0 表示不限制
- },
- // 已上传的文件列表
- modelValue: {
- type: Array,
- default: () => []
- },
- // 是否自动上传
- autoUpload: {
- type: Boolean,
- default: true
- },
- // 是否启用拖拽上传
- drag: {
- type: Boolean,
- default: false
- },
- // 文件列表的类型
- listType: {
- type: String,
- default: 'text', // text, picture, picture-card
- validator: (value) => ['text', 'picture', 'picture-card'].includes(value)
- },
- // 上传按钮文字
- buttonText: {
- type: String,
- default: '点击上传'
- },
- // 提示文字
- tip: {
- type: String,
- default: ''
- },
- // 最大文件大小 (MB)
- maxSize: {
- type: Number,
- default: 10 // 0 表示不限制
- },
- // 是否显示进度条
- showProgress: {
- type: Boolean,
- default: true
- }
- })
- // 组件事件
- const emit = defineEmits([
- 'update:modelValue', // 文件列表更新事件
- 'before-upload', // 上传前事件
- 'before-remove', // 删除前事件
- 'success', // 上传成功事件
- 'error', // 上传失败事件
- 'progress', // 上传进度事件
- 'change', // 文件状态改变事件
- 'remove', // 文件移除事件
- 'exceed', // 文件数量超出限制事件
- 'preview', // 文件预览事件
- 'download' // 文件下载事件
- ])
- // 内部状态
- const uploadRef = ref(null)
- const fileList = ref([...props.modelValue])
- const previewVisible = ref(false)
- const currentPreviewFile = ref(null)
- const currentPreviewContent = ref('')
- // 计算属性
- const uploadData = computed(() => props.data)
- const previewTitle = computed(() => currentPreviewFile.value ? `预览:${currentPreviewFile.value.name}` : '文件预览')
- // 添加一个标志位,防止循环更新
- const isInnerUpdate = ref(false)
- // 监听modelValue变化,同步到内部fileList
- watch(() => props.modelValue, (newVal) => {
- // 如果是内部更新触发的,跳过
- if (isInnerUpdate.value) {
- isInnerUpdate.value = false;
- return;
- }
- if (newVal && newVal.length) {
- fileList.value = newVal.map(item => ({
- ...item, // 完全保留接口返回的原始数据
- name: item.name || item.originalFileName || (item.url ? item.url.substring(item.url.lastIndexOf('/') + 1) : 'unknown'),
- url: item.url || item.fileUrl || item.path,
- status: item.status || 'success',
- uid: item.uid || Date.now() + Math.random()
- }));
- }
- // fileList.value = [...newVal]
- }, { immediate: true })
- // 监听fileList变化,同步到modelValue
- watch(() => fileList.value, (newVal) => {
- // 设置内部更新标志
- isInnerUpdate.value = true
- emit('update:modelValue', [...newVal])
- }, { deep: true })
- // 上传前的校验
- const handleBeforeUpload = (rawFile) => {
- // 文件大小校验
- if (props.maxSize > 0) {
- const maxSizeBytes = props.maxSize * 1024 * 1024
- if (rawFile.size > maxSizeBytes) {
- DGTMessage.error(`文件大小超出限制,最大允许 ${props.maxSize}MB`)
- emit('error', {
- message: `文件大小超出限制,最大允许 ${props.maxSize}MB`
- })
- return false
- }
- }
- // 调用外部before-upload钩子
- return emit('before-upload', rawFile) !== false
- }
- // 上传成功处理
- const handleSuccess = (response, rawFile, uploadedFiles) => {
- // 将上传成功的文件添加到fileList中
- // fileList.value.push({
- // ...response,
- // name: response.originalFileName || response.name,
- // url: response.url,
- // status: 'success',
- // uid: rawFile.uid
- // })
- emit('success', response, rawFile, uploadedFiles)
- }
- // 上传失败处理
- const handleError = (error, rawFile, uploadedFiles) => {
- DGTMessage.error(error.message || '文件上传失败')
- emit('error', error, rawFile, uploadedFiles)
- }
- // 上传进度处理
- const handleProgress = (event, file, uploadedFiles) => {
- emit('progress', event, file, uploadedFiles)
- }
- // 文件状态改变处理
- const handleChange = (file, newFileList) => {
- // 更新内部fileList
- fileList.value = newFileList.map(item => {
- // 如果文件已上传成功且有response,使用服务器返回的URL
- if (item.status === 'success' && item.response) {
- return {
- ...item.response,
- name: item.response.originalFileName || item.name,
- url: item.response.url,
- status: 'success',
- uid: item.uid
- }
- }
- return item
- })
- emit('change', file, fileList)
- }
- // 删除文件前的处理
- const handleBeforeRemove = (file, fileList) => {
- return emit('before-remove', file, fileList) !== false
- }
- // 文件移除处理
- const handleRemove = (file) => {
- console.log('移除文件:', file)
- // 从fileList中移除该文件
- // fileList.value = fileList.value.filter(f => f.url !== file.url)
- // 从fileList中移除该文件
- const index = fileList.value.findIndex(f =>
- f.uid === file.uid
- )
- console.log('index:', index,fileList.value)
- if (index > -1) {
- fileList.value.splice(index, 1)
- }
- emit('remove', file, fileList)
- }
- // 文件数量超出限制处理
- const handleExceed = (files, fileList) => {
- emit('exceed', files, fileList)
- }
- // 文件预览处理
- const handlePreview = (file,index) => {
- console.log('预览文件:', file,index)
- currentPreviewFile.value = file
-
- // 图片文件直接预览
- if (isImageFile(file)) {
- previewVisible.value = true
- emit('preview', file)
- return
- }
-
- // 文本文件读取内容后预览
- if (isTextFile(file)) {
- if (file.url) {
- //浏览器下载
- downloadFile(file.url, file.name)
- // fetch(file.url)
- // .then(response => response.text())
- // .then(text => {
- // currentPreviewContent.value = text
- // previewVisible.value = true
- // emit('preview', file)
- // })
- // .catch(error => {
- // console.error('读取文件内容失败:', error)
- // emit('error', error, file)
- // })
- } else {
- previewVisible.value = true
- emit('preview', file)
- }
- return
- }
-
- // 其他文件类型
- previewVisible.value = true
- emit('preview', file)
- }
- // 文件下载处理
- const handleDownload = (file) => {
- emit('download', file)
- }
- // 辅助方法:判断是否为图片文件
- const isImageFile = (file) => {
- return file.type && file.type.startsWith('image/') ||
- /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(file.name)
- }
- // 辅助方法:判断是否为文本文件
- const isTextFile = (file) => {
- return file.type && file.type.startsWith('text/') ||
- /\.(txt|json|yaml|yml|xml|html|htm|css|js|ts|md|markdown|zip|rar)$/i.test(file.name)
- }
- // 辅助方法:格式化文件大小
- const formatFileSize = (size) => {
- if (size < 1024) {
- return `${size} B`
- } else if (size < 1024 * 1024) {
- return `${(size / 1024).toFixed(2)} KB`
- } else if (size < 1024 * 1024 * 1024) {
- return `${(size / (1024 * 1024)).toFixed(2)} MB`
- } else {
- return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`
- }
- }
- // 暴露方法给父组件
- defineExpose({
- // 手动上传文件
- submitUpload: () => uploadRef.value?.submit(),
- // 清除文件列表
- clearFiles: () => uploadRef.value?.clearFiles(),
- // 撤销上传请求
- abort: (file) => uploadRef.value?.abort(file),
- // 获取当前文件列表
- getFileList: () => [...fileList.value],
- // 手动添加文件到列表
- addFile: (file) => {
- fileList.value.push(file)
- return fileList.value
- },
- // 手动移除文件
- removeFile: (file) => {
- const index = fileList.value.findIndex(item => item.uid === file.uid)
- if (index > -1) {
- fileList.value.splice(index, 1)
- }
- return fileList.value
- }
- })
- </script>
- <style scoped lang="scss">
- .file-uploader-container {
- width: 100%;
-
- .preview-container {
- width: 100%;
- min-height: 60vh;
- overflow: auto;
-
- .preview-image {
- width: 100%;
- height: auto;
- object-fit: contain;
- }
-
- .preview-text {
- width: 100%;
- padding: 10px;
- background-color: #f5f5f5;
- border-radius: 4px;
- font-family: monospace;
- white-space: pre-wrap;
- }
-
- .preview-other {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 40px 0;
-
- .file-icon {
- font-size: 64px;
- color: #409eff;
- margin-right: 20px;
- }
-
- .file-info {
- text-align: left;
-
- .file-name {
- font-size: 16px;
- font-weight: 500;
- margin-bottom: 8px;
- }
-
- .file-size {
- font-size: 14px;
- color: #666;
- }
- }
- }
- }
- }
- </style>
|