Документация/Обзор

Файловый менеджер

Облачный диск — сетка и список, рамка выделения, папки, которые принимают броски везде и открываются при наведении, Alt для копирования, корзина с Undo и файлы, брошенные с рабочего стола.

Редактировать страницу

Страница переведена ИИ и может содержать неточности. Читать английский оригинал

Live

Загружаем демо…

<script setup lang="ts">
import { DnDProvider, type AnnouncedPosition, type Announcements, type DnDEvent } from '@vue-dnd-kit/core';
import FileManager from './FileManager.vue';
import type { DropPlace } from './drive';
import type { Entry } from './files';

// Space picks a focused file up and drops it; Enter is left to open folders.
const keyboard = { keys: { start: ['Space'], drop: ['Space', 'Enter'] } };

/** "Budget 2026.xlsx", or "3 items". */
const what = ({ active }: DnDEvent) => (active.items.length === 1 ? (active.items[0]!.item as Entry).name : `${active.items.length} items`);
const at = (position?: AnnouncedPosition) => (position ? `, ${position.index + 1} of ${position.length}` : '');

// Screen readers hear names, not keys, and where a drop would go, from the folder's `data`.
const announcements: Partial<Announcements> = {
  onDragStart: (event, { position }) => `Picked up ${what(event)}${at(position)}.`,
  onDragOver: (event, { position }) => {
    const place = event.over?.data as DropPlace | undefined;
    if (place) return place.why ?? `Over ${place.name}. Drop to ${event.mode === 'copy' ? 'copy' : 'move'} ${what(event)} there.`;
    return position ? `${what(event)}${at(position)}.` : undefined;
  },
  // Into a folder, the toast says what happened.
  onDragEnd: (event, { position }) => (event.over?.data ? '' : `Dropped ${what(event)}${at(position)}.`),
  onDragCancel: (event) => `Cancelled: ${what(event)} went back.`,
};
</script>

<template>
  <!-- One provider: its selection is what the band and the clicks fill, and what a drag takes. -->
  <DnDProvider :keyboard="keyboard" :announcements="announcements">
    <FileManager />
  </DnDProvider>
</template>

На что обратить внимание

  • FileView.vue: открытая папка — один сортируемый список, показанный сеткой плиток или списком строк. Выделяют рамка, клики и клавиши; драг забирает выделение.
  • useFolderDrop.ts: каждая папка на экране — цель: плитки, строки дерева, избранное, крошки пути, корзина. Она говорит, почему отклоняет драг, и сглатывает, когда файлы внутри.
  • FilesOverlay.vue: то, что следует за указателем, собранное из пропсов, которые получает любой оверлей: стопка настоящих плиток со счётчиком, «+», пока Alt копирует, и что сделал бы бросок.
  • SidebarRow.vue: закрытая папка дерева открывается, когда на ней задерживается драг, чтобы бросок могли принять её подпапки. Favorites.vue — отдельный сортируемый список, строки которого тоже цели.
  • useDesktopDrop.ts: файлы, перетащенные с вашего компьютера. Это drag and drop браузера, а не библиотеки, и обрабатывается он рядом с ней.
  • drive.ts: состояние и каждое изменение, с Undo. index.vue: провайдер с переназначенными клавишами и его объявления.

Один список, два вида

Сетка и список — один и тот же сортируемый список. Его опции — геттеры, которые читаются, когда начинается драг: стратегия следует за видом, а папка, отсортированная по колонке (sort: false), не переставляется. Драги в её папки всё равно работают.

FileView.vue
const { items } = makeSortableList(list, {
  items: drive.entries,   // the open folder, sorted by a column or in your own order
  group: 'files',
  strategy: () => (grid.value ? 'grid' : 'vertical'),
  sort: () => drive.sort.value.key === 'manual',
  multiple: 'gaps',       // the other picked files wait in place, as in Finder
  clone: drive.duplicateOf, // Alt-drag leaves the files and drops "Budget copy.xlsx"
  dropzone: area,         // the empty space takes drops too
  animateChanges: true,   // a new sort, a new folder, files sent to the Trash slide and fade
});
  • drive.entries — записываемый computed: библиотека записывает каждый бросок обратно через его set, а onDrop списка сначала сохраняет диск для Undo.
  • Окно создаёт новый вид для каждой папки (:key на FileView), так что открытие папки — не изменение списка, которое animateChanges стал бы проигрывать.

Выделение

Выделение хранит провайдер; как его делают люди, решает вид. Нажатие на пустое место рисует рамку: сама по себе она заменяет выделение, а с зажатыми Shift, Ctrl или Cmd переключает плитки, которые пересекает. Стратегия — геттер, который читается, когда начинается рамка:

FileView.vue
let adding = false;
const { isSelecting, style } = makeSelectionArea(area, {
  strategy: () => (adding ? 'toggle' : 'select'),
});

// On the area, `@pointerdown.capture`: it runs before the band's own listener.
const onPress = (event: PointerEvent) => {
  adding = event.shiftKey || event.ctrlKey || event.metaKey;
};

Клики — как в Finder: клик выбирает один файл, Ctrl/Cmd-клик добавляет или убирает его, Shift-клик берёт ряд от последнего. Стрелки двигают единственную точку табуляции по файлам (roving tabindex, который библиотека не трогает), Shift+стрелки расширяют выделение, Enter открывает, F2 переименовывает, Delete отправляет в корзину, ввод букв прыгает к имени. Провайдер отдаёт Enter приложению: файл берёт только Space.

index.vue
<script setup lang="ts">
// Space picks a focused file up and drops it; Enter is left to open folders.
const keyboard = { keys: { start: ['Space'], drop: ['Space', 'Enter'] } };
</script>

<template>
  <DnDProvider :keyboard="keyboard" :announcements="announcements">
    <FileManager />
  </DnDProvider>
</template>

Папки принимают броски везде

useFolderDrop превращает любой элемент в папку, которая принимает файлы. Плитка к тому же — сортируемый элемент сетки: там цель учитывается только в середине плитки, а у её краёв сетка освобождает место. accept отклоняет папку, в которой файлы уже лежат, и папку в саму себя; отклонённая плитка позволяет сетке сортировать над ней, как над любой другой.

useFolderDrop.ts
const { id, isOver, isAccepting } = makeDroppable(target, {
  disabled: () => !folder(),
  data: () => drive.placeOf(folder()!),   // { name, trash, why }: the overlay reads it as `over.data`
  accept: ({ active, types }) => types.includes('files') && !drive.whyNot(active.ids, folder()!),
  onDrop: (event) => drive.dropInto(event, folder()!),
});

// The files are in once the overlay has landed in the folder: it gulps.
useDnDMonitor({
  onDragSettle: (event) => {
    if ('dropped' in event && event.dropped && event.over?.id === id.value) gulps.value++;
  },
});

dropInto перемещает файлы или копирует их, когда event.mode равен 'copy' (Alt зажат над списком с clone). Перемещение вызывает event.source.remove(): сетка отпускает файлы, и её onDrop выполняется с to.index === -1.

drive.ts
const dropInto = (event: DnDEvent, target: FolderEntry) => {
  const dragged = entries.value.filter((entry) => event.active.ids.includes(entry.id));
  const copying = event.mode === 'copy' && !isTrash(target) && !inTrash.value;
  record(`${copying ? 'Copied' : 'Moved'} ${describe(dragged)} to ${isTrash(target) ? 'the Trash' : target.name}`);
  if (copying) return put(target, dragged.map(copyEntry), current.value);
  put(target, dragged, current.value);
  // The grid lets them go: the library writes the open folder without them.
  event.source?.remove();
  selection.delete(...event.active.ids);
};

Элемент избранного — одновременно сортируемый элемент (в Favorites.vue, списке своей группы) и цель: FavoriteRow.vue передаёт компонент строки в makeSortableItem, который читает его корневой элемент, а SidebarRow.vue делает тот же элемент папкой. У его краёв перетаскиваемый элемент избранного сортируется; файлы, брошенные на него, попадают в его папку.

Папки, открывающиеся при наведении

Закрытая папка дерева открывается, когда на ней задерживается драг, как в Finder: isOver держится какое-то время, и строка раскрывается. Её подпапки монтируются посреди драга и сразу принимают броски.

SidebarRow.vue
const springing = computed(() => isOver.value && props.expandable && !props.expanded);
watch(springing, (now) => {
  clearTimeout(timer);
  if (now) timer = setTimeout(() => emit('toggle'), 700);
});

Оверлей

FilesOverlay.vue — overlay списка. Из сетки он складывает в стопку DragPreview — настоящие клоны выбранных плиток: при броске в слот каждый приземляется на свою плитку; при броске в папку они уходят в неё один за другим. Строка шириной со список, поэтому список центрирует блок под указателем (snapCenterToCursor), а оверлей подвешивает к нему маленький чип. Оверлей — функция, которую Vue принимает как функциональный компонент: она передаёт дальше пропсы, которые читает, и говорит, какой из двух видов рисовать.

FileView.vue
overlay: ({ items, over, accepted, status, mode }: OverlayProps) =>
  h(FilesOverlay, { items, over, accepted, status, mode, rows: !grid.value }),
modifiers: () => (grid.value ? [] : [snapCenterToCursor]),

Под стопкой плашка говорит, что сделает отпускание, по data папки: «Move to Photos», «Copy to Photos», пока зажат Alt (mode), или красным — почему нельзя, когда accepted равен false.

FilesOverlay.vue
const place = computed(() => (props.status === 'dragging' ? (props.over?.data as DropPlace | undefined) : undefined));
const pill = computed(() => {
  if (!place.value) return null;
  if (!props.accepted) return { refused: true, text: place.value.why ?? 'Can’t go here' };
  if (place.value.trash) return { refused: false, text: 'Move to Trash' };
  return { refused: false, text: `${props.mode === 'copy' ? 'Copy' : 'Move'} to ${place.value.name}` };
});

announcements провайдера читают те же data, так что скринридер слышит «Over Photos. Drop to move 3 items there» или «Already in Documents». Они называют файлы там, где встроенные тексты сказали бы их ключи.

Файлы с рабочего стола

Перетащите файлы с компьютера на окно: они загружаются в папку под указателем (плитку, строку боковой панели, крошку), а иначе — в открытую, и фото показывают себя. Библиотека перетаскивает элементы страницы; файлы снаружи приходят через собственный drag and drop браузера, dragover и drop, который обрабатывается рядом с ней:

useDesktopDrop.ts
const dragover = (event: DragEvent) => {
  if (!carriesFiles(event)) return;
  // Cancelled, a `dragover` says the drop is welcome here.
  event.preventDefault();
  const folder = folderAt(event); // under the pointer: an element with a folder's `data-id`
  // …nothing goes into the Trash…
  drive.uploadTarget.value = folder.id; // that folder opens, as for the library's drags
};

Undo, корзина и остальное

  • Каждое изменение сначала сохраняет копию диска; Undo в уведомлении (или Ctrl/Cmd+Z) возвращается к ней. Корзина — папка вне дерева: то, что в ней лежит, помнит, откуда пришло, и Put Back возвращает это туда.
  • ContextMenu.vue (правый клик, Shift+F10 или кнопка «⋯») действует на выделение или на открытую папку: Rename на месте, Duplicate, Add to Favorites, Move to Trash, Empty Trash.
  • Развёрнутое окно заполняет сцену, а панель сведений открывается рядом с файлами. На телефоне боковая панель — выдвижная; касание открывает папку, а долгое нажатие берёт файл.

Подробнее: мульти-драг и выделение · бросок в сортируемый элемент · DragPreview · изменения данных: animateChanges · доступность