web工单处理
This commit is contained in:
@@ -48,6 +48,26 @@ export const fileAPI = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据ID获取文件信息
|
||||
* @param fileId 文件ID
|
||||
* @returns Promise<ResultDomain<TbSysFileDTO>>
|
||||
*/
|
||||
async getFileById(fileId: string): Promise<ResultDomain<TbSysFileDTO>> {
|
||||
const response = await api.get<TbSysFileDTO>(`${this.baseUrl}/${fileId}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量获取文件信息
|
||||
* @param fileIds 文件ID列表
|
||||
* @returns Promise<ResultDomain<TbSysFileDTO>>
|
||||
*/
|
||||
async getFilesByIds(fileIds: string[]): Promise<ResultDomain<TbSysFileDTO>> {
|
||||
const response = await api.post<TbSysFileDTO>(`${this.baseUrl}/list`, { fileIds });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
* @param fileId 文件ID
|
||||
|
||||
@@ -98,7 +98,8 @@
|
||||
<!-- 内容模式 -->
|
||||
<div v-else class="file-upload content">
|
||||
<div class="container">
|
||||
<div class="area" :class="{ dragover: isDragover, disabled: uploading }" @click="triggerFileInput"
|
||||
<!-- 上传区域 -->
|
||||
<div v-if="!uploading && currentFileList.length < maxCount" class="area" :class="{ dragover: isDragover }" @click="triggerFileInput"
|
||||
@drop.prevent="handleDrop" @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave">
|
||||
<div class="icon">
|
||||
<div class="plus"></div>
|
||||
@@ -109,38 +110,37 @@
|
||||
<div class="tip">{{ getUploadTip() }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传中状态 -->
|
||||
<div v-if="uploading" class="area disabled">
|
||||
<div class="loading">
|
||||
<div class="spinner">⟳</div>
|
||||
<div>上传中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input ref="fileInputRef" type="file" :accept="accept" :multiple="maxCount > 1" @change="handleFileSelect"
|
||||
hidden />
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div v-if="selectedFiles.length > 0" class="files">
|
||||
<h4>待上传文件:</h4>
|
||||
<div v-for="(file, index) in selectedFiles" :key="index" class="file">
|
||||
<!-- 已上传文件列表 -->
|
||||
<div v-if="currentFileList.length > 0" class="files">
|
||||
<div v-for="(file, index) in currentFileList" :key="file.fileId || index" class="file">
|
||||
<div class="preview">
|
||||
<img v-if="isImageFile(file)" :src="getFilePreviewUrl(file)" class="thumb" />
|
||||
<span v-else class="type-icon">{{ getFileTypeIcon(file) }}</span>
|
||||
<img v-if="isUploadedImageFile(file)" :src="getUploadedFileUrl(file)" class="thumb" />
|
||||
<span v-else class="type-icon">{{ getUploadedFileTypeIcon(file) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="name">{{ file.name }}</div>
|
||||
<div class="size">{{ formatFileSize(file.size) }}</div>
|
||||
<div class="name">{{ file.name || file.fileName || '未知文件' }}</div>
|
||||
<div class="size">{{ file.size ? formatFileSize(file.size) : '' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<ElButton type="danger" size="small" @click="removeFile(index)" :disabled="uploading">
|
||||
<ElButton type="danger" size="small" @click="removeUploadedFile(index)">
|
||||
删除
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传按钮 -->
|
||||
<div v-if="selectedFiles.length > 0" class="actions">
|
||||
<ElButton type="primary" @click="uploadFiles" :loading="uploading"
|
||||
:disabled="selectedFiles.length === 0">
|
||||
{{ uploading ? '上传中...' : '确定上传' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -203,6 +203,12 @@ const selectedFiles = ref<File[]>([])
|
||||
const isDragover = ref(false)
|
||||
const coverImageClass = ref('')
|
||||
|
||||
// 内部文件列表(包含本地预览URL)
|
||||
interface InternalFile extends TbSysFileDTO {
|
||||
localPreviewUrl?: string // 本地预览URL,避免重新下载
|
||||
}
|
||||
const internalFileList = ref<InternalFile[]>([])
|
||||
|
||||
// 文件输入引用
|
||||
const fileInputRef = ref<HTMLInputElement>()
|
||||
|
||||
@@ -212,6 +218,56 @@ const currentCoverImg = computed({
|
||||
set: (value: string) => emit('update:coverImg', value)
|
||||
})
|
||||
|
||||
// 当前文件列表(优先使用内部列表,否则使用props)
|
||||
const currentFileList = computed(() => {
|
||||
return internalFileList.value.length > 0 ? internalFileList.value : props.fileList
|
||||
})
|
||||
|
||||
// 判断已上传文件是否为图片
|
||||
const isUploadedImageFile = (file: InternalFile): boolean => {
|
||||
if (file.localPreviewUrl) return true // 有本地预览说明是图片
|
||||
const mimeType = file.mimeType || file.extension || ''
|
||||
return mimeType.includes('image') || /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name || '')
|
||||
}
|
||||
|
||||
// 获取已上传文件的显示URL(优先使用本地预览)
|
||||
const getUploadedFileUrl = (file: InternalFile): string => {
|
||||
if (file.localPreviewUrl) return file.localPreviewUrl
|
||||
if (file.url) return file.url
|
||||
if (file.fileId) return `${FILE_DOWNLOAD_URL}${file.fileId}`
|
||||
return ''
|
||||
}
|
||||
|
||||
// 获取已上传文件的类型图标
|
||||
const getUploadedFileTypeIcon = (file: InternalFile): string => {
|
||||
const ext = file.extension || file.name?.split('.').pop() || ''
|
||||
const iconMap: Record<string, string> = {
|
||||
pdf: '📄',
|
||||
doc: '📝',
|
||||
docx: '📝',
|
||||
xls: '📊',
|
||||
xlsx: '📊',
|
||||
ppt: '📽️',
|
||||
pptx: '📽️',
|
||||
zip: '📦',
|
||||
rar: '📦',
|
||||
txt: '📃'
|
||||
}
|
||||
return iconMap[ext.toLowerCase()] || '📎'
|
||||
}
|
||||
|
||||
// 删除已上传的文件
|
||||
const removeUploadedFile = (index: number) => {
|
||||
const newList = [...internalFileList.value]
|
||||
const removed = newList.splice(index, 1)[0]
|
||||
// 释放本地预览URL
|
||||
if (removed?.localPreviewUrl) {
|
||||
URL.revokeObjectURL(removed.localPreviewUrl)
|
||||
}
|
||||
internalFileList.value = newList
|
||||
emit('update:fileList', newList)
|
||||
}
|
||||
|
||||
// 触发文件输入
|
||||
const triggerFileInput = () => {
|
||||
fileInputRef.value?.click()
|
||||
@@ -292,18 +348,23 @@ const addFiles = (files: File[]) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否重复
|
||||
// 检查是否重复(包括已上传的文件)
|
||||
if (selectedFiles.value.some(f => f.name === file.name && f.size === file.size)) {
|
||||
emit('upload-error', `文件 ${file.name} 已添加`)
|
||||
return
|
||||
}
|
||||
if (internalFileList.value.some(f => f.name === file.name)) {
|
||||
emit('upload-error', `文件 ${file.name} 已上传`)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果不允许多选或封面模式,清空之前的文件
|
||||
if (props.maxCount === 1 || props.mode === 'cover') {
|
||||
selectedFiles.value = [file]
|
||||
} else {
|
||||
// 检查数量限制
|
||||
if (selectedFiles.value.length >= props.maxCount) {
|
||||
// 检查数量限制(已上传 + 待上传)
|
||||
const totalCount = internalFileList.value.length + selectedFiles.value.length
|
||||
if (totalCount >= props.maxCount) {
|
||||
emit('upload-error', `最多只能上传 ${props.maxCount} 个文件`)
|
||||
return
|
||||
}
|
||||
@@ -391,7 +452,15 @@ const uploadFiles = async () => {
|
||||
|
||||
// 默认上传逻辑
|
||||
uploading.value = true
|
||||
const uploadedFilesList: TbSysFileDTO[] = []
|
||||
const uploadedFilesList: InternalFile[] = []
|
||||
|
||||
// 为图片文件创建本地预览URL的映射
|
||||
const localPreviewMap = new Map<string, string>()
|
||||
selectedFiles.value.forEach(file => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
localPreviewMap.set(file.name, URL.createObjectURL(file))
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
|
||||
@@ -404,12 +473,19 @@ const uploadFiles = async () => {
|
||||
})
|
||||
|
||||
if (result.success && result.data) {
|
||||
uploadedFilesList.push(result.data)
|
||||
// 添加本地预览URL
|
||||
const uploadedFile: InternalFile = {
|
||||
...result.data,
|
||||
localPreviewUrl: localPreviewMap.get(file.name)
|
||||
}
|
||||
uploadedFilesList.push(uploadedFile)
|
||||
|
||||
if (props.mode === 'cover' && result.data.url) {
|
||||
emit('update:coverImg', result.data.url)
|
||||
}
|
||||
} else {
|
||||
// 释放预览URL
|
||||
localPreviewMap.forEach(url => URL.revokeObjectURL(url))
|
||||
emit('upload-error', result.message || '上传失败,请重试')
|
||||
return
|
||||
}
|
||||
@@ -422,20 +498,33 @@ const uploadFiles = async () => {
|
||||
|
||||
if (result.success) {
|
||||
const files = result.dataList || []
|
||||
uploadedFilesList.push(...files)
|
||||
// 为每个上传成功的文件添加本地预览URL
|
||||
files.forEach(f => {
|
||||
const uploadedFile: InternalFile = {
|
||||
...f,
|
||||
localPreviewUrl: localPreviewMap.get(f.name || '')
|
||||
}
|
||||
uploadedFilesList.push(uploadedFile)
|
||||
})
|
||||
} else {
|
||||
// 释放预览URL
|
||||
localPreviewMap.forEach(url => URL.revokeObjectURL(url))
|
||||
emit('upload-error', result.message || '上传失败,请重试')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 上传成功
|
||||
// 上传成功 - 更新内部文件列表
|
||||
if (uploadedFilesList.length > 0) {
|
||||
// content模式:追加到内部列表
|
||||
if (props.mode === 'content') {
|
||||
internalFileList.value = [...internalFileList.value, ...uploadedFilesList]
|
||||
}
|
||||
emit('upload-success', uploadedFilesList)
|
||||
emit('update:fileList', [...props.fileList, ...uploadedFilesList])
|
||||
}
|
||||
|
||||
// 清空文件列表
|
||||
// 清空待上传文件列表
|
||||
selectedFiles.value = []
|
||||
if (props.mode === 'dialog') {
|
||||
closeDialog()
|
||||
@@ -453,6 +542,8 @@ const uploadFiles = async () => {
|
||||
defineExpose({
|
||||
// 当前选中的文件列表
|
||||
selectedFiles,
|
||||
// 已上传的文件列表
|
||||
internalFileList,
|
||||
// 上传状态
|
||||
uploading,
|
||||
// 弹窗显示状态
|
||||
@@ -463,8 +554,17 @@ defineExpose({
|
||||
addFiles,
|
||||
// 移除指定文件
|
||||
removeFile,
|
||||
// 清空所有文件
|
||||
clearFiles: () => { selectedFiles.value = [] },
|
||||
// 清空所有文件(包括已上传的)
|
||||
clearFiles: () => {
|
||||
// 释放本地预览URL
|
||||
internalFileList.value.forEach(f => {
|
||||
if (f.localPreviewUrl) {
|
||||
URL.revokeObjectURL(f.localPreviewUrl)
|
||||
}
|
||||
})
|
||||
selectedFiles.value = []
|
||||
internalFileList.value = []
|
||||
},
|
||||
// 手动触发上传
|
||||
uploadFiles,
|
||||
// 关闭弹窗
|
||||
|
||||
Reference in New Issue
Block a user