概述
本文深入分析Coze Studio中用户删除插件功能的前端实现。该功能允许用户在资源库中安全地删除不需要的插件资源,为开发者提供了完善的资源管理能力。通过对源码的详细解析,我们将了解从资源库表格操作到删除确认弹窗的完整架构设计、组件实现、状态管理和用户体验优化等核心技术要点。删除功能涉及权限验证、用户确认、API调用和状态更新等多个环节,确保数据安全和操作的可靠性。
功能特性
核心功能
- 安全删除:支持插件资源的安全删除操作
- 权限控制:基于用户权限动态显示删除按钮状态
- 确认机制:提供删除确认弹窗防止误操作
- 批量操作:支持通过TableAction组件进行批量管理
- 状态同步:删除后自动刷新资源列表
用户体验特性
- 即时反馈:删除操作结果实时展示和Toast提示
- 权限提示:无权限时按钮禁用并提供视觉反馈
- 操作便捷:通过表格行操作菜单快速访问删除功能
- 国际化支持:删除相关文案支持多语言适配
技术架构
整体架构设计
┌─────────────────────────────────────────────────────────────┐
│ 插件删除管理模块 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ LibraryPage │ │BaseLibrary │ │ TableAction │ │
│ │ (资源库页面) │ │ Page │ │ (操作菜单) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Table │ │UITableAction│ │ Modal.confirm │ │
│ │ (资源列表) │ │ (操作组件) │ │ (删除确认弹窗) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 状态管理层 │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │usePluginConfig │ │ useRequest Hook │ │
│ │ (删除配置) │ │ (删除API调用) │ │
│ └─────────────────┘ └─────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ API服务层 │
│ ┌─────────────────────────────────────────────────────────┐
│ │ PluginDevelop API │
│ │ DelPlugin │
│ └─────────────────────────────────────────────────────────┘
└────────────────────────────────────────────────────────────┘
核心模块结构
frontend/
├── packages/studio/workspace/
│ ├── entry-adapter/src/pages/library/
│ │ └── index.tsx # LibraryPage适配器组件
│ └── entry-base/src/pages/library/
│ ├── index.tsx # BaseLibraryPage核心组件
│ └── hooks/use-entity-configs/
│ └── use-plugin-config.tsx # 插件配置Hook
├── packages/arch/idl/src/auto-generated/plugin_develop/
│ ├── namespaces/
│ │ ├── plugin_develop.ts # DelPluginRequest/Response接口定义
│ │ └── plugin_develop_common.ts # 通用类型定义
│ └── index.ts # PluginDevelopApi导出
└── packages/arch/coze-design/
└── Table/
└── TableAction # 表格操作组件
用户删除插件流程概述
用户登录Coze Studio
↓
点击"资源库"菜单
↓
LibraryPage 组件加载
↓
BaseLibraryPage 渲染资源列表
↓
用户找到要删除的插件行
↓
点击表格行最右边的"..."操作按钮
↓
TableAction 下拉菜单显示
↓
点击"删除"菜单项
↓
权限验证(检查ActionKey.Delete权限)
↓
Modal.confirm 删除确认弹窗显示
↓
用户确认删除操作
↓
delPlugin() 函数触发
↓
PluginDevelopApi.DelPlugin() 调用
↓
后端执行删除操作
↓
删除成功回调处理
↓
reloadList() 刷新资源列表
↓
Toast.success() 显示删除成功提示
该流程包含多层安全验证和处理:
- 权限验证:通过ActionKey.Delete检查用户是否有删除权限
- 用户确认:使用Modal.confirm防止误删除操作
- API调用:使用DelPlugin API安全删除资源
- 状态同步:删除成功后自动刷新列表保持数据一致性
- 用户反馈:通过Toast提示用户操作结果
- 错误处理:API调用失败时提供相应的错误提示
整个流程确保了插件删除的安全性和用户体验的友好性。
核心组件实现
组件层次结构
插件删除功能涉及多个层次的组件:
- LibraryPage组件:资源库主页面,整合各种资源配置
- BaseLibraryPage组件:资源库核心逻辑,渲染资源列表
- Table组件:资源列表表格,包含操作列
- TableAction组件:表格行操作菜单,包含删除选项
- usePluginConfig Hook:插件配置逻辑,包含删除功能
1. 资源库入口组件(LibraryPage)
文件位置:frontend/packages/studio/workspace/entry-adapter/src/pages/library/index.tsx
作为资源库的适配器组件,整合各种资源配置,包括插件的删除功能:
typescript
import { type FC, useRef } from 'react';
import {
BaseLibraryPage,
useDatabaseConfig,
usePluginConfig,
useWorkflowConfig,
usePromptConfig,
useKnowledgeConfig,
} from '@coze-studio/workspace-base/library';
export const LibraryPage: FC<{ spaceId: string }> = ({ spaceId }) => {
const basePageRef = useRef<{ reloadList: () => void }>(null);
const configCommonParams = {
spaceId,
reloadList: () => {
basePageRef.current?.reloadList();
},
};
// 各种资源配置,包括插件删除配置
const { config: pluginConfig, modals: pluginModals } =
usePluginConfig(configCommonParams);
// 其他资源配置...
return (
<>
<BaseLibraryPage
spaceId={spaceId}
ref={basePageRef}
entityConfigs={[
pluginConfig, // 包含删除配置
// 其他配置...
]}
/>
{pluginModals}
{/* 其他模态框... */}
</>
);
};
设计亮点:
- 配置统一管理 :通过
usePluginConfig
统一管理插件的删除配置 - 组件解耦:删除功能通过配置传递,组件职责明确
- 状态同步 :删除操作后通过
reloadList
自动刷新列表
2. 资源库核心组件(BaseLibraryPage)
文件位置:frontend/packages/studio/workspace/entry-base/src/pages/library/index.tsx
负责资源库的核心展示逻辑,包含资源列表表格和删除操作:
typescript
import { forwardRef, useImperativeHandle } from 'react';
import classNames from 'classnames';
import { useInfiniteScroll } from 'ahooks';
import { I18n } from '@coze-arch/i18n';
import {
Table,
Select,
Search,
Layout,
Cascader,
Space,
} from '@coze-arch/coze-design';
import { renderHtmlTitle } from '@coze-arch/bot-utils';
import { EVENT_NAMES, sendTeaEvent } from '@coze-arch/bot-tea';
import {
type ResType,
type LibraryResourceListRequest,
type ResourceInfo,
} from '@coze-arch/bot-api/plugin_develop';
import { PluginDevelopApi } from '@coze-arch/bot-api';
import { type ListData, type BaseLibraryPageProps } from './types';
import { LibraryHeader } from './components/library-header';
export const BaseLibraryPage = forwardRef<
{ reloadList: () => void },
BaseLibraryPageProps
>(
({ spaceId, isPersonalSpace = true, entityConfigs }, ref) => {
const { params, setParams, resetParams, hasFilter, ready } =
useCachedQueryParams({
spaceId,
});
const listResp = useInfiniteScroll<ListData>(
async prev => {
if (!ready) {
return {
list: [],
nextCursorId: undefined,
hasMore: false,
};
}
const resp = await PluginDevelopApi.LibraryResourceList(
entityConfigs.reduce<LibraryResourceListRequest>(
(res, config) => config.parseParams?.(res) ?? res,
{
...params,
cursor: prev?.nextCursorId,
space_id: spaceId,
size: LIBRARY_PAGE_SIZE,
},
),
);
return {
list: resp?.resource_list || [],
nextCursorId: resp?.cursor,
hasMore: !!resp?.has_more,
};
},
{
reloadDeps: [params, spaceId],
},
);
useImperativeHandle(ref, () => ({
reloadList: listResp.reload,
}));
return (
<Layout
className={s['layout-content']}
title={renderHtmlTitle(I18n.t('navigation_workspace_library'))}
>
<Layout.Header className={classNames(s['layout-header'], 'pb-0')}>
<div className="w-full">
<LibraryHeader entityConfigs={entityConfigs} />
{/* 过滤器组件 */}
</div>
</Layout.Header>
<Layout.Content>
{/* 表格和列表内容 */}
</Layout.Content>
</Layout>
);
}
);
3. 表格操作组件(TableAction)
文件位置:@coze-arch/coze-design
包中的 Table.TableAction
组件
提供表格行的操作菜单,包含删除功能:
typescript
import { Table } from '@coze-arch/coze-design';
const { TableAction } = Table;
// 在 usePluginConfig 中使用
renderActions: (item: ResourceInfo) => {
const deleteDisabled = !item.actions?.find(
action => action.key === ActionKey.Delete,
)?.enable;
const deleteProps = {
disabled: deleteDisabled,
deleteDesc: I18n.t('library_delete_desc'),
handler: async () => {
await PluginDevelopApi.DelPlugin({ plugin_id: item.res_id });
reloadList();
Toast.success(I18n.t('Delete_success'));
},
};
return (
<TableAction
deleteProps={deleteProps}
actionList={getCommonActions?.(item)}
/>
);
}