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:
2026-03-21 22:13:12 +08:00
parent a27736a832
commit b024a2bcb5
25 changed files with 2628 additions and 1656 deletions

View 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,
}
}

File diff suppressed because it is too large Load Diff