Files
Nexus/frontend/src/composables/files/useFilesPage.ts
T
2026-06-02 05:05:26 +08:00

271 lines
8.1 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.
/**
* 文件管理页 — 组合各域 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<FileSortKey>('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,
}
}