refactor(frontend): move views into app and pages structure
Reorganize the frontend around app-level routing and page modules so the runtime and feature screens share a clearer navigation and composition layout for future work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
344
frontend/src/pages/knowledge/composables/useKnowledgeView.ts
Normal file
344
frontend/src/pages/knowledge/composables/useKnowledgeView.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { documentApi, type Document } from '@/api/document'
|
||||
import { folderApi, type FolderTree } from '@/api/folder'
|
||||
|
||||
export function useKnowledgeView() {
|
||||
const folders = ref<FolderTree[]>([])
|
||||
const documents = ref<Document[]>([])
|
||||
const currentFolderId = ref<string | null>(null)
|
||||
const isUploading = ref(false)
|
||||
const isLoadingDocuments = ref(false)
|
||||
const uploadError = ref('')
|
||||
const uploadInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const showNewFolderDialog = ref(false)
|
||||
const newFolderName = ref('')
|
||||
const newFolderParentId = ref<string | null>(null)
|
||||
|
||||
const showRenameDialog = ref(false)
|
||||
const renameFolderName = ref('')
|
||||
const renamingFolder = ref<FolderTree | null>(null)
|
||||
|
||||
const showDeleteDialog = ref(false)
|
||||
const deletingFolder = ref<FolderTree | null>(null)
|
||||
|
||||
const showDocumentDialog = ref(false)
|
||||
const activeDocument = ref<Document | null>(null)
|
||||
const activeDocumentContent = ref('')
|
||||
const isLoadingDocumentContent = ref(false)
|
||||
|
||||
const folderMap = computed(() => {
|
||||
const map = new Map<string, FolderTree>()
|
||||
|
||||
function walk(nodes: FolderTree[]) {
|
||||
for (const node of nodes) {
|
||||
map.set(node.id, node)
|
||||
if (node.children?.length) {
|
||||
walk(node.children)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(folders.value)
|
||||
return map
|
||||
})
|
||||
|
||||
const currentFolder = computed(() => {
|
||||
if (!currentFolderId.value) return null
|
||||
return folderMap.value.get(currentFolderId.value) ?? null
|
||||
})
|
||||
|
||||
const isRoot = computed(() => currentFolderId.value === null)
|
||||
|
||||
const visibleFolders = computed(() => {
|
||||
if (isRoot.value) return folders.value
|
||||
return currentFolder.value?.children ?? []
|
||||
})
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
const items: Array<{ id: string | null; name: string }> = [{ id: null, name: '根目录' }]
|
||||
|
||||
if (!currentFolder.value) {
|
||||
return items
|
||||
}
|
||||
|
||||
const chain: FolderTree[] = []
|
||||
let cursor: FolderTree | null = currentFolder.value
|
||||
|
||||
while (cursor) {
|
||||
chain.unshift(cursor)
|
||||
cursor = cursor.parent_id ? folderMap.value.get(cursor.parent_id) ?? null : null
|
||||
}
|
||||
|
||||
for (const folder of chain) {
|
||||
items.push({ id: folder.id, name: folder.name })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
const explorerTitle = computed(() => {
|
||||
if (isRoot.value) {
|
||||
return `${visibleFolders.value.length} 个文件夹`
|
||||
}
|
||||
|
||||
return `${visibleFolders.value.length} 个文件夹 · ${documents.value.length} 个文件`
|
||||
})
|
||||
|
||||
async function loadFolders() {
|
||||
try {
|
||||
const response = await folderApi.getTree()
|
||||
folders.value = response.data
|
||||
} catch (error) {
|
||||
console.error('加载文件夹失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDocumentsByFolder(folderId: string | null) {
|
||||
if (!folderId) {
|
||||
documents.value = []
|
||||
return
|
||||
}
|
||||
|
||||
isLoadingDocuments.value = true
|
||||
try {
|
||||
const response = await documentApi.list(folderId)
|
||||
documents.value = response.data
|
||||
} catch (error) {
|
||||
console.error('加载文档失败:', error)
|
||||
} finally {
|
||||
isLoadingDocuments.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function enterFolder(folder: FolderTree) {
|
||||
currentFolderId.value = folder.id
|
||||
await loadDocumentsByFolder(folder.id)
|
||||
}
|
||||
|
||||
async function goToFolder(folderId: string | null) {
|
||||
currentFolderId.value = folderId
|
||||
await loadDocumentsByFolder(folderId)
|
||||
}
|
||||
|
||||
async function goBack() {
|
||||
if (!currentFolder.value) return
|
||||
await goToFolder(currentFolder.value.parent_id)
|
||||
}
|
||||
|
||||
function triggerUpload() {
|
||||
if (isRoot.value) return
|
||||
uploadInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleUpload(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
if (!currentFolderId.value) {
|
||||
uploadError.value = '请先进入目标文件夹后再上传文件'
|
||||
window.setTimeout(() => {
|
||||
uploadError.value = ''
|
||||
}, 3000)
|
||||
target.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
isUploading.value = true
|
||||
try {
|
||||
await documentApi.upload(file, currentFolderId.value)
|
||||
await loadDocumentsByFolder(currentFolderId.value)
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
target.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteDocument(id: string) {
|
||||
try {
|
||||
await documentApi.delete(id)
|
||||
documents.value = documents.value.filter((doc) => doc.id !== id)
|
||||
|
||||
if (activeDocument.value?.id === id) {
|
||||
closeDocumentDialog()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function openNewFolderDialog(parentId: string | null = null) {
|
||||
newFolderParentId.value = parentId
|
||||
newFolderName.value = ''
|
||||
showNewFolderDialog.value = true
|
||||
}
|
||||
|
||||
async function createFolder() {
|
||||
if (!newFolderName.value.trim()) return
|
||||
|
||||
try {
|
||||
await folderApi.create({
|
||||
name: newFolderName.value.trim(),
|
||||
parent_id: newFolderParentId.value,
|
||||
})
|
||||
await loadFolders()
|
||||
showNewFolderDialog.value = false
|
||||
} catch (error) {
|
||||
console.error('创建文件夹失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function openRenameDialog(folder: FolderTree) {
|
||||
renamingFolder.value = folder
|
||||
renameFolderName.value = folder.name
|
||||
showRenameDialog.value = true
|
||||
}
|
||||
|
||||
async function renameFolder() {
|
||||
if (!renamingFolder.value || !renameFolderName.value.trim()) return
|
||||
|
||||
try {
|
||||
await folderApi.rename(renamingFolder.value.id, { name: renameFolderName.value.trim() })
|
||||
await loadFolders()
|
||||
showRenameDialog.value = false
|
||||
renamingFolder.value = null
|
||||
} catch (error) {
|
||||
console.error('重命名文件夹失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteDialog(folder: FolderTree) {
|
||||
deletingFolder.value = folder
|
||||
showDeleteDialog.value = true
|
||||
}
|
||||
|
||||
function isFolderInTree(nodes: FolderTree[], targetId: string) {
|
||||
for (const node of nodes) {
|
||||
if (node.id === targetId) return true
|
||||
if (node.children?.length && isFolderInTree(node.children, targetId)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function deleteFolder() {
|
||||
if (!deletingFolder.value) return
|
||||
|
||||
const deletingId = deletingFolder.value.id
|
||||
const fallbackParentId = deletingFolder.value.parent_id
|
||||
|
||||
try {
|
||||
await folderApi.delete(deletingId)
|
||||
await loadFolders()
|
||||
|
||||
if (currentFolderId.value && !isFolderInTree(folders.value, currentFolderId.value)) {
|
||||
currentFolderId.value = fallbackParentId
|
||||
}
|
||||
|
||||
await loadDocumentsByFolder(currentFolderId.value)
|
||||
showDeleteDialog.value = false
|
||||
deletingFolder.value = null
|
||||
} catch (error) {
|
||||
console.error('删除文件夹失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function openDocument(doc: Document) {
|
||||
activeDocument.value = doc
|
||||
activeDocumentContent.value = ''
|
||||
showDocumentDialog.value = true
|
||||
isLoadingDocumentContent.value = true
|
||||
|
||||
try {
|
||||
const response = await documentApi.getContent(doc.id)
|
||||
const content = response.data as string | { content?: string }
|
||||
activeDocumentContent.value = typeof content === 'string' ? content : content.content ?? ''
|
||||
} catch (error) {
|
||||
console.error('加载文档内容失败:', error)
|
||||
activeDocumentContent.value = '暂时无法加载文档内容。'
|
||||
} finally {
|
||||
isLoadingDocumentContent.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeDocumentDialog() {
|
||||
showDocumentDialog.value = false
|
||||
activeDocument.value = null
|
||||
activeDocumentContent.value = ''
|
||||
}
|
||||
|
||||
function getFileTypeColor(type: string) {
|
||||
const colors: Record<string, string> = {
|
||||
pdf: '#f87171',
|
||||
md: '#60a5fa',
|
||||
txt: '#34d399',
|
||||
docx: '#a78bfa',
|
||||
}
|
||||
return colors[type] || '#9ca3af'
|
||||
}
|
||||
|
||||
function formatFileSize(fileSize: number) {
|
||||
if (fileSize < 1024) return `${fileSize} B`
|
||||
if (fileSize < 1024 * 1024) return `${(fileSize / 1024).toFixed(1)} KB`
|
||||
return `${(fileSize / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function formatDate(date: string) {
|
||||
return new Date(date).toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadFolders()
|
||||
await loadDocumentsByFolder(null)
|
||||
})
|
||||
|
||||
return {
|
||||
folders,
|
||||
documents,
|
||||
currentFolderId,
|
||||
isUploading,
|
||||
isLoadingDocuments,
|
||||
uploadError,
|
||||
uploadInput,
|
||||
showNewFolderDialog,
|
||||
newFolderName,
|
||||
newFolderParentId,
|
||||
showRenameDialog,
|
||||
renameFolderName,
|
||||
renamingFolder,
|
||||
showDeleteDialog,
|
||||
deletingFolder,
|
||||
showDocumentDialog,
|
||||
activeDocument,
|
||||
activeDocumentContent,
|
||||
isLoadingDocumentContent,
|
||||
currentFolder,
|
||||
isRoot,
|
||||
visibleFolders,
|
||||
breadcrumbs,
|
||||
explorerTitle,
|
||||
enterFolder,
|
||||
goToFolder,
|
||||
goBack,
|
||||
triggerUpload,
|
||||
handleUpload,
|
||||
handleDeleteDocument,
|
||||
openNewFolderDialog,
|
||||
createFolder,
|
||||
openRenameDialog,
|
||||
renameFolder,
|
||||
openDeleteDialog,
|
||||
deleteFolder,
|
||||
openDocument,
|
||||
closeDocumentDialog,
|
||||
getFileTypeColor,
|
||||
formatFileSize,
|
||||
formatDate,
|
||||
}
|
||||
}
|
||||
1336
frontend/src/pages/knowledge/index.vue
Normal file
1336
frontend/src/pages/knowledge/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user