Back to Astrbot

通用文件夹管理组件库

dashboard/src/components/folder/README.md

4.24.28.2 KB
Original Source

通用文件夹管理组件库

这是一个可复用的文件夹管理 UI 组件库,提供了完整的文件夹树、面包屑导航、拖放操作等功能。可用于管理各种类型的项目,如 Persona、模板、知识库等。

组件列表

组件说明
BaseFolderTree文件夹树组件,支持搜索、展开/折叠、右键菜单、拖放
BaseFolderTreeNode文件夹树节点组件(内部使用)
BaseFolderCard文件夹卡片组件,用于网格布局展示
BaseFolderBreadcrumb面包屑导航组件
BaseCreateFolderDialog创建文件夹对话框
BaseMoveToFolderDialog移动项目到文件夹对话框
BaseMoveTargetNode移动对话框中的目标文件夹节点(内部使用)

Composable

useFolderManager

提供文件夹管理的核心逻辑,包括状态管理、导航、CRUD 操作等。

typescript
import { useFolderManager } from '@/components/folder';

const {
  // 状态
  folderTree,
  currentFolderId,
  currentFolders,
  breadcrumbPath,
  expandedFolderIds,
  loading,
  treeLoading,
  
  // 计算属性
  currentFolderName,
  breadcrumbItems,
  
  // 方法
  loadFolderTree,
  navigateToFolder,
  refreshCurrentFolder,
  createFolder,
  updateFolder,
  deleteFolder,
  moveFolder,
  toggleFolderExpansion,
  setFolderExpansion,
  findFolderInTree,
  findPathToFolder,
  filterTreeBySearch,
} = useFolderManager({
  operations: {
    loadFolderTree: async () => {
      const response = await axios.get('/api/your-module/folder/tree');
      return response.data.data;
    },
    loadSubFolders: async (parentId) => {
      const response = await axios.get('/api/your-module/folder/list', {
        params: { parent_id: parentId ?? '' }
      });
      return response.data.data;
    },
    createFolder: async (data) => {
      const response = await axios.post('/api/your-module/folder/create', data);
      return response.data.data.folder;
    },
    updateFolder: async (data) => {
      await axios.post('/api/your-module/folder/update', data);
    },
    deleteFolder: async (folderId) => {
      await axios.post('/api/your-module/folder/delete', { folder_id: folderId });
    },
  },
  rootFolderName: '根目录',
  autoLoad: true,
});

使用示例

基础用法

vue
<template>
  <div class="folder-manager">
    <!-- 侧边栏 -->
    <div class="sidebar">
      <BaseFolderTree
        :folder-tree="folderTree"
        :current-folder-id="currentFolderId"
        :expanded-folder-ids="expandedFolderIds"
        :tree-loading="treeLoading"
        :accept-drop-types="['item']"
        :labels="treeLabels"
        @folder-click="navigateToFolder"
        @rename-folder="handleRenameFolder"
        @move-folder="handleMoveFolder"
        @delete-folder="handleDeleteFolder"
        @item-dropped="handleItemDropped"
        @toggle-expansion="toggleFolderExpansion"
      />
    </div>
    
    <!-- 主内容区 -->
    <div class="main-content">
      <!-- 面包屑 -->
      <BaseFolderBreadcrumb
        :breadcrumb-path="breadcrumbPath"
        :current-folder-id="currentFolderId"
        root-folder-name="根目录"
        @navigate="navigateToFolder"
      />
      
      <!-- 文件夹卡片 -->
      <v-row>
        <v-col v-for="folder in currentFolders" :key="folder.folder_id" cols="3">
          <BaseFolderCard
            :folder="folder"
            :accept-drop-types="['item']"
            :labels="cardLabels"
            @click="navigateToFolder(folder.folder_id)"
            @open="navigateToFolder(folder.folder_id)"
            @rename="handleRenameFolder(folder)"
            @move="handleMoveFolder(folder)"
            @delete="handleDeleteFolder(folder)"
            @item-dropped="handleItemDropped"
          />
        </v-col>
      </v-row>
    </div>
    
    <!-- 创建文件夹对话框 -->
    <BaseCreateFolderDialog
      v-model="showCreateDialog"
      :parent-folder-id="currentFolderId"
      :labels="createDialogLabels"
      @create="handleCreateFolder"
    />
    
    <!-- 移动对话框 -->
    <BaseMoveToFolderDialog
      v-model="showMoveDialog"
      :folder-tree="folderTree"
      :tree-loading="treeLoading"
      :current-folder-id="movingFolder?.folder_id"
      :item-current-folder-id="movingFolder?.parent_id"
      :is-moving-folder="true"
      :labels="moveDialogLabels"
      @move="handleMove"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import {
  BaseFolderTree,
  BaseFolderCard,
  BaseFolderBreadcrumb,
  BaseCreateFolderDialog,
  BaseMoveToFolderDialog,
  useFolderManager,
} from '@/components/folder';

const folderManager = useFolderManager({
  operations: {
    // ... 实现你的 API 调用
  },
});

const {
  folderTree,
  currentFolderId,
  currentFolders,
  breadcrumbPath,
  expandedFolderIds,
  treeLoading,
  navigateToFolder,
  toggleFolderExpansion,
  createFolder,
} = folderManager;

const showCreateDialog = ref(false);
const showMoveDialog = ref(false);
const movingFolder = ref(null);

// 自定义标签
const treeLabels = {
  searchPlaceholder: '搜索文件夹...',
  rootFolder: '根目录',
  noFolders: '暂无文件夹',
  contextMenu: {
    open: '打开',
    rename: '重命名',
    moveTo: '移动到...',
    delete: '删除',
  },
};

const cardLabels = {
  open: '打开',
  rename: '重命名',
  moveTo: '移动到...',
  delete: '删除',
};

const createDialogLabels = {
  title: '创建文件夹',
  nameLabel: '名称',
  descriptionLabel: '描述',
  nameRequired: '请输入名称',
  cancelButton: '取消',
  createButton: '创建',
};

// 处理函数
async function handleCreateFolder(data) {
  await createFolder(data);
  showCreateDialog.value = false;
}

function handleRenameFolder(folder) {
  // 打开重命名对话框
}

function handleMoveFolder(folder) {
  movingFolder.value = folder;
  showMoveDialog.value = true;
}

function handleDeleteFolder(folder) {
  // 确认并删除
}

function handleItemDropped({ item_id, item_type, target_folder_id }) {
  // 处理拖放
}

async function handleMove(targetFolderId) {
  // 执行移动
  showMoveDialog.value = false;
}
</script>

类型定义

typescript
// 文件夹基础接口
interface Folder {
  folder_id: string;
  name: string;
  parent_id: string | null;
  description?: string | null;
  sort_order?: number;
  created_at?: string;
  updated_at?: string;
}

// 文件夹树节点接口
interface FolderTreeNode extends Folder {
  children: FolderTreeNode[];
}

// 拖放事件数据
interface DropEventData {
  item_id: string;
  item_type: string;
  target_folder_id: string | null;
  source_data?: any;
}

// 创建文件夹数据
interface CreateFolderData {
  name: string;
  parent_id?: string | null;
  description?: string;
}

国际化支持

所有组件都支持通过 labels prop 自定义文本,方便集成到不同的国际化方案中:

vue
<BaseFolderTree
  :labels="{
    searchPlaceholder: t('folder.search'),
    rootFolder: t('folder.root'),
    noFolders: t('folder.empty'),
    contextMenu: {
      open: t('folder.menu.open'),
      rename: t('folder.menu.rename'),
      moveTo: t('folder.menu.move'),
      delete: t('folder.menu.delete'),
    },
  }"
/>

拖放支持

组件内置了拖放支持,可以通过 acceptDropTypes 指定接受的拖放类型:

vue
<!-- 只接受 'persona' 类型的拖放 -->
<BaseFolderTree
  :accept-drop-types="['persona']"
  @item-dropped="handleDrop"
/>

<!-- 拖放事件处理 -->
<script setup>
function handleDrop({ item_id, item_type, target_folder_id, source_data }) {
  if (item_type === 'persona') {
    // 移动 persona 到目标文件夹
    movePersonaToFolder(item_id, target_folder_id);
  }
}
</script>

与 Pinia Store 集成

如果你更喜欢使用 Pinia Store 管理状态,可以参考现有的 personaStore.ts 实现:

typescript
// stores/myFolderStore.ts
import { defineStore } from 'pinia';
import type { FolderTreeNode, Folder } from '@/components/folder';

export const useMyFolderStore = defineStore('myFolder', {
  state: () => ({
    folderTree: [] as FolderTreeNode[],
    currentFolderId: null as string | null,
    currentFolders: [] as Folder[],
    // ...
  }),
  
  actions: {
    async loadFolderTree() {
      // ...
    },
    // ...
  },
});