web工单处理

This commit is contained in:
2025-12-28 13:18:28 +08:00
parent 1148e3368d
commit 7de30b1b36
12 changed files with 1209 additions and 95 deletions

View File

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

View File

@@ -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,
// 关闭弹窗