Files
YG-Datasets/frontend/src/page-logic/ProjectFilePage.ts

378 lines
11 KiB
TypeScript
Raw Normal View History

import { defineComponent } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { fileApi } from '@/core/api'
import DeleteDialog from '@/shared/components/common/DeleteDialog.vue'
export default defineComponent({
name: 'FileManage',
components: { DeleteDialog },
setup() {
const route = useRoute()
const projectId = computed(() => route.params.id)
const loading = ref(false)
const files = ref([])
const filterStatus = ref('')
const isInitialLoad = ref(true)
const filteredFiles = computed(() => {
if (!filterStatus.value) return files.value
return files.value.filter(f => f.status === filterStatus.value)
})
const uploadDialogVisible = ref(false)
const uploading = ref(false)
const uploadRef = ref(null)
const fileList = ref([])
const deleteDialogVisible = ref(false)
const pendingDeleteFile = ref(null)
const deletingFile = ref(false)
// Multi-select
const selectedFiles = ref([])
const isAllSelected = computed(() => filteredFiles.value.length > 0 && selectedFiles.value.length === filteredFiles.value.length)
const selectedCount = computed(() => selectedFiles.value.length)
const toggleSelectAll = () => {
if (isAllSelected.value) {
selectedFiles.value = []
} else {
selectedFiles.value = filteredFiles.value.map(f => f.id)
}
}
const toggleSelect = (fileId) => {
const index = selectedFiles.value.indexOf(fileId)
if (index === -1) {
selectedFiles.value.push(fileId)
} else {
selectedFiles.value.splice(index, 1)
}
}
const isSelected = (fileId) => selectedFiles.value.includes(fileId)
const clearSelection = () => {
selectedFiles.value = []
}
const batchDeleteDialogVisible = ref(false)
const batchDeleting = ref(false)
const batchDeleteFiles = ref([])
const batchDelete = async () => {
if (selectedFiles.value.length === 0) return
batchDeleteFiles.value = files.value.filter(f => selectedFiles.value.includes(f.id))
batchDeleteDialogVisible.value = true
}
const executeBatchDelete = async () => {
if (selectedFiles.value.length === 0) return
batchDeleting.value = true
try {
for (const fileId of selectedFiles.value) {
await fileApi.delete(projectId.value, fileId)
}
ElMessage.success(`已删除 ${selectedFiles.value.length} 个文件`)
selectedFiles.value = []
batchDeleteDialogVisible.value = false
fetchFiles()
} catch (error) {
ElMessage.error('删除失败')
} finally {
batchDeleting.value = false
}
}
// Preview
const previewVisible = ref(false)
const previewFile = ref(null)
const previewContent = ref('')
const previewLoading = ref(false)
const previewMode = ref('source') // 'source' | 'markdown'
const isPdfPreview = ref(false)
const pdfDataUrl = ref('')
const previewError = ref('')
const completedFiles = computed(() => files.value.filter(f => f.status === 'completed').length)
const processingFiles = computed(() => files.value.filter(f => f.status === 'processing' || f.status === 'pending'))
const failedFiles = computed(() => files.value.filter(f => f.status === 'failed').length)
const fetchFiles = async () => {
const wasInitial = isInitialLoad.value
loading.value = true
try {
const res = await fileApi.list(projectId.value)
files.value = res || []
} catch (error) {
files.value = []
} finally {
loading.value = false
if (wasInitial) {
isInitialLoad.value = false
}
}
}
const handleUpload = () => {
fileList.value = []
uploadDialogVisible.value = true
}
const handleChange = (file, files) => { fileList.value = files }
const handleRemove = (file, files) => { fileList.value = files }
const triggerUpload = () => {
const input = uploadRef.value?.$el?.querySelector('input')
if (input) {
input.click()
}
}
const submitUpload = async () => {
if (fileList.value.length === 0) {
ElMessage.warning('请先选择文件')
return
}
// 保存上传前的文件数量
const prevFileCount = files.value.length
// 先关闭对话框
uploadDialogVisible.value = false
ElMessage.success('已开始上传文件')
// 设置上传状态,防止显示空状态
uploading.value = true
// 在后台逐个上传(不等待上传完成)
const uploadPromises = fileList.value.map(async (item) => {
try {
const formData = new FormData()
formData.append('file', item.raw)
await fileApi.upload(projectId.value, formData)
} catch (error) {
console.error('上传失败:', error)
}
})
// 立即刷新文件列表,显示新增的文件(状态为 processing
await fetchFiles()
// 如果之前没有文件需要等待上传的Promise完成后再刷新一次
if (prevFileCount === 0) {
await Promise.all(uploadPromises)
await fetchFiles()
}
// 持续轮询文件列表,直到没有 processing 状态的文件
const pollInterval = setInterval(async () => {
await fetchFiles()
// 检查是否还有处理中的文件
const hasProcessing = files.value.some(f => f.status === 'processing')
if (!hasProcessing) {
clearInterval(pollInterval)
uploading.value = false
}
}, 2000)
// 最多轮询60秒
setTimeout(() => {
clearInterval(pollInterval)
uploading.value = false
}, 60000)
}
const handleDelete = async (file) => {
try {
deletingFile.value = true
await fileApi.delete(projectId.value, file.id)
ElMessage.success('删除成功')
fetchFiles()
} catch (error) {
ElMessage.error('删除失败')
} finally {
deletingFile.value = false
deleteDialogVisible.value = false
pendingDeleteFile.value = null
}
}
const openDeleteDialog = (file) => {
pendingDeleteFile.value = file
deleteDialogVisible.value = true
}
const confirmDeleteFile = async () => {
if (!pendingDeleteFile.value) return
await handleDelete(pendingDeleteFile.value)
}
const handlePreview = async (file) => {
previewFile.value = file
previewVisible.value = true
previewContent.value = ''
previewError.value = ''
previewLoading.value = true
previewMode.value = 'source'
try {
await loadPreviewContent()
} finally {
previewLoading.value = false
}
}
const loadPreviewContent = async () => {
if (!previewFile.value) return
previewLoading.value = true
previewContent.value = ''
previewError.value = ''
isPdfPreview.value = false
pdfDataUrl.value = ''
try {
const endpoint = previewMode.value === 'source' ? 'raw' : 'content'
const response = await fetch(`/api/v1/projects/${projectId.value}/files/${previewFile.value.id}/${endpoint}`)
if (response.ok) {
const text = await response.text()
if (text.startsWith('data:application/pdf;base64,')) {
isPdfPreview.value = true
pdfDataUrl.value = text
previewContent.value = ''
} else {
previewContent.value = text
}
} else if (response.status === 404) {
previewError.value = previewMode.value === 'source'
? '源文件不存在或已被删除'
: 'Markdown 内容不存在,请等待处理完成'
} else if (response.status === 500) {
previewError.value = '服务器内部错误,请稍后重试'
} else {
previewError.value = `加载失败 (${response.status})`
}
} catch (error) {
previewError.value = '网络错误,请检查网络连接'
} finally {
previewLoading.value = false
}
}
const switchPreviewMode = async (mode) => {
previewMode.value = mode
await loadPreviewContent()
}
const getFileIcon = (type) => {
const map = { pdf: 'Document', docx: 'Document', xlsx: 'Grid', csv: 'Document', epub: 'Notebook', md: 'Document', txt: 'Document' }
return map[type] || 'Document'
}
const getTypeColor = (type) => {
const map = {
pdf: '#ef4444',
docx: '#3b82f6',
xlsx: '#22c55e',
csv: '#22c55e',
epub: '#f59e0b',
md: '#8b5cf6',
txt: '#6b7280'
}
return map[type] || '#6b7280'
}
const getFileExt = (filename) => {
if (!filename) return ''
const ext = filename.split('.').pop()?.toLowerCase()
return ext ? '.' + ext : ''
}
const getStatusText = (status) => {
const map = {
processing: '处理中',
completed: '已完成',
failed: '失败',
pending: '待处理'
}
return map[status] || '未知'
}
const formatSize = (bytes) => {
if (!bytes) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}
const formatDate = (date) => {
if (!date) return ''
return new Date(date).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
}
onMounted(() => fetchFiles())
return {
route,
projectId,
loading,
files,
filterStatus,
isInitialLoad,
filteredFiles,
uploadDialogVisible,
uploading,
uploadRef,
fileList,
deleteDialogVisible,
pendingDeleteFile,
deletingFile,
selectedFiles,
isAllSelected,
selectedCount,
toggleSelectAll,
toggleSelect,
isSelected,
clearSelection,
batchDeleteDialogVisible,
batchDeleting,
batchDeleteFiles,
batchDelete,
executeBatchDelete,
previewVisible,
previewFile,
previewContent,
previewLoading,
previewMode,
isPdfPreview,
pdfDataUrl,
previewError,
completedFiles,
processingFiles,
failedFiles,
fetchFiles,
handleUpload,
handleChange,
handleRemove,
triggerUpload,
submitUpload,
handleDelete,
openDeleteDialog,
confirmDeleteFile,
handlePreview,
loadPreviewContent,
switchPreviewMode,
getFileIcon,
getTypeColor,
getFileExt,
getStatusText,
formatSize,
formatDate
}
}
})