Files
AstrBot/dashboard/src/components/folder/README.md
Weilong Liao 0d8e8682db refactor(core): migrate backend backbone from Quart to FastAPI and introduce more OpenAPI (#8688)
* refactor: migrate to fastapi

* structure refactor

* fix: pyright fix

* refactor: improve error handling and public messages in plugin services

* feat(api): refactor API client integration and enhance request handling

- Updated API client configuration to use a dedicated HTTP client.
- Introduced utility functions for generating options, queries, and form data for API requests.
- Refactored multiple API methods to utilize the new utility functions for improved consistency and readability.
- Renamed types for clarity and updated import statements accordingly.

feat(docs): add script to update OpenAPI JSON from YAML spec

- Created a Python script to convert OpenAPI YAML specification to JSON format.
- The script supports customizable input and output paths.
- Ensured the script handles directory creation for output paths and validates the YAML structure.

* fix

* feat(auth): implement rate limiting for v1 login endpoint and enhance request handling

* Refactor dashboard API routers to use legacy_router for backward compatibility

- Changed all instances of dashboard_router to legacy_router across multiple API modules including platform, plugins, providers, sessions, skills, stats, subagents, t2i, tools, updates, and asgi_runtime.
- Updated route definitions to ensure existing endpoints remain functional under the new router structure.
- Introduced support for Quart request context in asgi_runtime to enhance compatibility with existing Quart-based plugins.
- Added a test case to validate the functionality of the new Quart request context handling in plugin extensions.

* chore: remove cli test

* fix: update dashboard tests for fastapi migration

* chore: satisfy ruff checks

* fix: update openapi api key scopes

* fix: sync config scope chip selection

* fix: restore quart dependency

* docs: clarify quart plugin api compatibility

* docs: update openapi scope documentation

* fix: use singular skill openapi scope

* fix: hide update service exception details

* fix: address fastapi review comments

* fix: address dashboard review findings

* docs: revert unrelated package deployment changes

* docs: update agent api generation guidance

* feat: add plugin page web api helpers

* docs: add plugin page bridge demo

* fix: type plugin upload files

* fix: stabilize plugin page uploads

* fix: type plugin web request proxy

* docs: remove plugin page docs example

* fix: authenticate plugin page SSE bridge
2026-06-14 15:03:26 +08:00

8.3 KiB
Raw Blame History

通用文件夹管理组件库

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

组件列表

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

Composable

useFolderManager

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

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 pluginExtensionApi.get('your-module/folder/tree');
      return response.data.data;
    },
    loadSubFolders: async (parentId) => {
      const response = await pluginExtensionApi.get('your-module/folder/list', {
        params: { parent_id: parentId ?? '' }
      });
      return response.data.data;
    },
    createFolder: async (data) => {
      const response = await pluginExtensionApi.post('your-module/folder/create', data);
      return response.data.data.folder;
    },
    updateFolder: async (data) => {
      await pluginExtensionApi.post('your-module/folder/update', data);
    },
    deleteFolder: async (folderId) => {
      await pluginExtensionApi.post('your-module/folder/delete', { folder_id: folderId });
    },
  },
  rootFolderName: '根目录',
  autoLoad: true,
});

使用示例

基础用法

<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>

类型定义

// 文件夹基础接口
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 自定义文本,方便集成到不同的国际化方案中:

<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 指定接受的拖放类型:

<!-- 只接受 '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 实现:

// 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() {
      // ...
    },
    // ...
  },
});