/** * 文件管理页 — 组合各域 composable,供 FilesPage.vue 使用 */ import { computed, ref } from 'vue' import { storeToRefs } from 'pinia' import { useFilesClipboard } from '@/composables/useFilesClipboard' import { useFilesHotkeys } from '@/composables/useFilesHotkeys' import { useFilesStore } from '@/stores/files' import type { FileEntry } from '@/types/api' import { FILE_EXT_FILTERS, isArchiveFile, matchesExtensionFilter, sortFileEntries, type FileSortKey, } from '@/utils/fileSort' import { formatOwnerChip, isEntryLikelyUnreadable, ownerChipColor, permissionTooltip, } from '@/utils/fileMode' import { isPreloadEligible } from '@/utils/filePreload' import { validatePathSegment } from '@/utils/remotePath' import { required } from '@/utils/validation' import { useFilesActions } from './useFilesActions' import { useFilesBrowse } from './useFilesBrowse' import { useFilesEditor } from './useFilesEditor' import { useFilesNavigation } from './useFilesNavigation' import { useFilesPermissions } from './useFilesPermissions' import { useFilesSelection } from './useFilesSelection' export function useFilesPage() { const fileFilter = ref('') const extFilter = ref('') const sortBy = ref('name') const sortOptions = [ { title: '名称', value: 'name' as const }, { title: '大小', value: 'size' as const }, { title: '修改时间', value: 'modified' as const }, ] const store = useFilesStore() const { selectedServer, currentPath, files, loading, browseError, pageReady, setupSudoLoading, setupSudoDone, } = storeToRefs(store) const selection = useFilesSelection() const { selectedFiles, selectedFileCount, clearSelection } = selection const { emptyHint, browse, browseCurrent, invalidateAfterMutation } = useFilesBrowse(fileFilter) const permissions = useFilesPermissions({ browse }) const { sshUserForFiles, filesAccessHint, dismissFilesAccessHint, setupFilesSudo, } = permissions const nav = useFilesNavigation({ browse, refreshFilesCapability: permissions.refreshFilesCapability, clearSelection, }) const actionLoading = ref(false) const editor = useFilesEditor({ browse, invalidateAfterMutation, actionLoading, }) const actions = useFilesActions({ browse, invalidateAfterMutation, selection, openEntry: nav.openEntry, editFile: editor.editFile, previewFile: editor.previewFile, openInTerminal: nav.openInTerminal, editorWorkbench: editor.editorWorkbench, actionLoading, }) const { hasClipboard, clipboardSummary, canPasteOnServer } = useFilesClipboard() const canPasteHere = computed(() => canPasteOnServer(selectedServer.value)) const displayedFiles = computed(() => { let list = sortFileEntries(files.value, sortBy.value, 'asc') const ext = extFilter.value if (ext) list = list.filter((f) => matchesExtensionFilter(f.name, ext)) const q = fileFilter.value.trim().toLowerCase() if (q) list = list.filter((f) => f.name.toLowerCase().includes(q)) return list }) const statusSummary = computed(() => { const dirs = files.value.filter((f) => f.type === 'directory').length const regular = files.value.filter((f) => f.type === 'file').length const links = files.value.filter((f) => f.type === 'symlink').length const preloadable = files.value.filter(isPreloadEligible).length const parts = [`${dirs} 个目录`, `${regular} 个文件`] if (links) parts.push(`${links} 个链接`) if (preloadable) parts.push(`${preloadable} 个 ≤300KB 可预加载`) return `${parts.join(',')} · ${currentPath.value}` }) function isArchiveName(name: string): boolean { return isArchiveFile(name) } function entryIcon(item: FileEntry): string { if (item.type === 'directory') return 'mdi-folder' if (item.type === 'symlink') return 'mdi-link-variant' return 'mdi-file-outline' } function entryIconColor(item: FileEntry): string { if (item.type === 'directory') return 'amber' if (item.type === 'symlink') return 'teal' return 'blue' } function isRegularFile(item: FileEntry): boolean { return item.type === 'file' } const fileHeaders = [ { title: '名称', key: 'name' }, { title: '归属', key: 'owner', width: 110, sortable: false }, { title: '大小', key: 'size', width: 120 }, { title: '修改时间', key: 'modified', width: 180 }, { title: '操作', key: 'actions', width: 200, align: 'end' as const }, ] useFilesHotkeys( { refresh: browseCurrent, goUp: nav.goUp, renameSelection: () => { if (selectedFiles.value.length === 1) { actions.startRename(selectedFiles.value[0]) } }, deleteSelection: () => { if (selectedFiles.value.length > 0) void actions.batchDelete() }, selectAll: () => { selectedFiles.value = [...displayedFiles.value] }, copySelection: () => actions.copyToClipboard(), cutSelection: () => actions.cutToClipboard(), pasteClipboard: () => { void actions.pasteClipboard() }, }, selectedFiles, canPasteHere, ) return { FILE_EXT_FILTERS, validatePathSegment, required, fileFilter, extFilter, sortBy, sortOptions, selectedServer, currentPath, loading, browseError, pageReady, setupSudoLoading, setupSudoDone, serverList: nav.serverList, onServerChange: nav.onServerChange, browseCurrent, filesAccessHint, dismissFilesAccessHint, setupFilesSudo, sshUserForFiles, statusSummary, hasClipboard, clipboardSummary, pasteDestLabel: actions.pasteDestLabel, canPasteHere, breadcrumbs: nav.breadcrumbs, onBreadcrumbClick: nav.onBreadcrumbClick, canGoUp: nav.canGoUp, goUp: nav.goUp, selectedFiles, selectedFileCount, displayedFiles, fileHeaders, fileRowProps: actions.fileRowProps, dropActive: actions.dropActive, onDropFiles: actions.onDropFiles, emptyHint, entryIcon, entryIconColor, isEntryLikelyUnreadable, formatOwnerChip, ownerChipColor, permissionTooltip, formatSize: editor.formatSize, isRegularFile, isArchiveName, showUpload: actions.showUpload, uploadFiles: actions.uploadFiles, uploading: actions.uploading, doUpload: actions.doUpload, showNewFile: actions.showNewFile, newFileName: actions.newFileName, doNewFile: actions.doNewFile, showMkdir: actions.showMkdir, mkdirName: actions.mkdirName, doMkdir: actions.doMkdir, showFileDelete: actions.showFileDelete, deletingFile: actions.deletingFile, doDeleteFile: actions.doDeleteFile, showRename: actions.showRename, newName: actions.newName, doRename: actions.doRename, showChmod: actions.showChmod, chmodFile: actions.chmodFile, chmodLoading: actions.chmodLoading, doChmod: actions.doChmod, showCompress: actions.showCompress, compressDest: actions.compressDest, compressFormat: actions.compressFormat, compressPaths: actions.compressPaths, doCompress: actions.doCompress, showDecompress: actions.showDecompress, decompressSource: actions.decompressSource, decompressDest: actions.decompressDest, doDecompress: actions.doDecompress, showContextMenu: actions.showContextMenu, contextX: actions.contextX, contextY: actions.contextY, contextFile: actions.contextFile, contextAction: actions.contextAction, actionLoading, batchDelete: actions.batchDelete, openCompressDialog: actions.openCompressDialog, showPreview: editor.showPreview, previewTitle: editor.previewTitle, previewContent: editor.previewContent, previewLoading: editor.previewLoading, editorWorkbench: editor.editorWorkbench, onEditorSaved: editor.onEditorSaved, navigateToPath: nav.navigateToPath, pasteClipboard: actions.pasteClipboard, editFile: editor.editFile, previewFile: editor.previewFile, downloadFile: actions.downloadFile, confirmDeleteFile: actions.confirmDeleteFile, openInTerminal: nav.openInTerminal, } }