Files
YG-Datasets/frontend/src/page-logic/ProjectFilePage.ts
Developer 6aa271c4f7 refactor: 前端架构重构 - 提取 CSS 和逻辑到独立模块
前端重构:
- 删除旧的大体积 Vue 组件(HomeView, FileManage, TextSplit 等)
- 删除旧的 composables(useFormatters, useModels, useProjects)
- 新增 core/, page-logic/, pages/, shared/ 模块化目录结构
- 提取 CSS 到 styles/pages/ 目录
- 添加全局样式 variables.css 和 common.css

后端 API 更新:
- chunks: 语义分割 API 增强
- files: 文件处理 API 更新
- models: 模型管理 API 更新
- questions: 问答管理 API 更新
- database: 数据库连接优化
- semantic_embedding: 语义嵌入服务优化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 14:23:34 +08:00

378 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}
})