7cbddf5d48
Co-authored-by: Cursor <cursoragent@cursor.com>
271 lines
8.1 KiB
TypeScript
271 lines
8.1 KiB
TypeScript
/**
|
||
* 文件管理页 — 组合各域 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,
|
||
}
|
||
}
|