safety-eval-service-frontend/.cursor/skills/specs/05-页面布局与通用UI规范/03-分类树组件/SKILL.md

6.3 KiB
Raw Permalink Blame History

name description
jjb-category-tree 定义分类树组件规范。在使用左侧树形分类导航、分类搜索、右键菜单操作时使用。

页面布局与通用 UI 规范

分类树组件

分类树用于左侧导航区域,提供树形结构的浏览、搜索、右键菜单操作等功能。

布局与宽度

  • 分类树作为左侧面板,固定宽度,通常为 240px ~ 260px,高度撑满父容器。
  • 树内容区 flex: 1; overflow: auto,节点过多时可垂直滚动。
  • 结构分区(自上而下):
    1. 标题栏:左侧显示标题文字(如"文件夹"),右侧放置新建图标按钮。
    2. 搜索栏:输入框带搜索前缀图标,size="small",支持 allowClear
    3. 树主体Spin 包裹 Tree 组件,空数据时显示"暂无数据"占位提示。
.category-tree {
  display: flex;
  flex-direction: column;
  height: 100%;

  .category-tree-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 12px 8px;
  }

  .category-tree-search {
    padding: 0 12px 8px;
  }

  .category-tree-body {
    flex: 1;
    overflow: auto;
    padding: 0 8px;
  }
}

树节点渲染

  • 使用 titleRender 自定义节点内容,组合高亮文本悬停操作图标
  • 使用 fieldNames 将后端字段映射为 Tree 所需格式(title → 显示名、key → 唯一标识、children → 子节点)。
  • 设置 blockNode 使整行可点击,提升操作区域。
  • 节点标题文本需设置溢出省略:
.tree-node-title {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
<Tree
  treeData={filteredTree}
  blockNode
  fieldNames={{ title: 'folderName', key: 'id', children: 'children' }}
  titleRender={(nodeData) => (
    <div className="tree-node">
      <span className="tree-node-title">{highlight(nodeData.folderName, searchValue)}</span>
      <Space className="tree-node-actions" size={4}>
        <FolderAddOutlined onClick={(e) => { e.stopPropagation(); handleCreate(nodeData.id); }} />
        <EditOutlined onClick={(e) => { e.stopPropagation(); handleRename(nodeData); }} />
        <DeleteOutlined onClick={(e) => { e.stopPropagation(); handleDelete(nodeData); }} style={{ color: '#ff4d4f' }} />
      </Space>
    </div>
  )}
/>

注意:操作图标的 onClick 必须 e.stopPropagation() 阻止冒泡,避免触发树节点选中。

选中态与悬停交互

  • 选中节点背景色使用浅蓝 #e6f4ff
  • 悬停时显示行内操作图标,默认隐藏,通过 CSS 控制:
.tree-node-actions {
  display: none;
  flex-shrink: 0;
}

:global {
  .ant-tree-treenode:hover .tree-node-actions {
    display: inline-flex;
  }

  .ant-tree-treenode-selected .ant-tree-node-content-wrapper {
    background: #e6f4ff;
  }
}
  • 节点内边距不宜过大,保持紧凑:.ant-tree-node-content-wrapper { padding: 2px 4px; }

搜索匹配文字高亮

  • 在搜索框中输入关键词后,树节点中匹配的文字需高亮显示。
  • 高亮颜色使用主题蓝色 #0064C8
  • 实现方式:将节点标题文本按关键词拆分,匹配部分用 <span style={{ color: '#0064C8' }}> 包裹。
  • 关键词中的正则特殊字符需转义处理。
function highlight(text, keyword) {
  if (!keyword) return text;
  const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const regex = new RegExp(`(${escaped})`, 'gi');
  return text.split(regex).map((part, i) =>
    regex.test(part) ? <span key={i} style={{ color: '#0064C8' }}>{part}</span> : part
  );
}
  • 搜索结果需 过滤树:保留标题匹配的节点,以及其子孙中有匹配的父节点(保持树结构)。
  • 搜索有值时,应自动展开所有匹配节点,让用户直接看到结果。

右键菜单操作

  • 使用 Dropdown + menu + trigger={['contextMenu']} 实现右键弹出菜单。
  • Dropdown 包裹整个 Tree 组件,onRightClick 记录当前右键节点。
  • 菜单项通常包括:新建子文件夹重命名删除(删除项标记 danger: true)。
  • 删除操作需弹出二次确认弹窗(Modal.confirm),提示删除后不可恢复。
const contextMenuItems = [
  { key: 'create', label: '新建子文件夹' },
  { key: 'rename', label: '重命名' },
  { key: 'delete', label: '删除', danger: true },
];

<Dropdown menu={{ items: contextMenuItems, onClick: handleContextMenuClick }} trigger={['contextMenu']}>
  <Tree
    treeData={filteredTree}
    onRightClick={({ node }) => setRightClickNode(node)}
    // ...
  />
</Dropdown>
  • 右键新建子文件夹时,parentId 为右键节点的 id标题栏新建按钮点击时parentId 为当前选中节点 id无选中则传 null 表示根级新建)。

新增/重命名弹窗

  • 使用 Modal + Input 实现新增和重命名,两种操作复用同一弹窗,通过状态区分标题文案。
  • 输入框绑定 onPressEnter 支持回车确认。
  • 名称需 trim() 处理,空值不允许提交。
  • Modal 的 confirmLoading 绑定提交 loading 状态,防止重复点击。
<Modal
  title={modalType === 'create' ? (parentId != null ? '新建子文件夹' : '新建文件夹') : '重命名'}
  open={modalOpen}
  onOk={handleModalOk}
  onCancel={() => setModalOpen(false)}
  confirmLoading={submitLoading}
  destroyOnHidden
>
  <Input
    placeholder="请输入文件夹名称"
    value={modalName}
    onChange={e => setModalName(e.target.value)}
    onPressEnter={handleModalOk}
  />
</Modal>

destroyOnHidden 确保每次打开弹窗时输入框状态被重置。

Loading 状态

  • 树数据加载时:使用 Spin 组件包裹树主体,绑定树数据的 loading 状态。
  • 提交操作时Modal 的 confirmLoading 绑定提交 loading防止重复提交。
  • 操作完成后需重新请求树数据刷新列表。
<Spin spinning={treeLoading}>
  <div className="category-tree-body">
    {filteredTree.length > 0 ? <Tree ... /> : <div className="category-tree-empty">暂无文件夹</div>}
  </div>
</Spin>

空状态

  • 树数据为空或搜索无结果时,显示居中占位文字"暂无文件夹"。
  • 样式:text-align: center; color: #999; padding: 24px 0; font-size: 13px;