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 } } })