update
This commit is contained in:
		
							
								
								
									
										172
									
								
								web/src/apps/muse/base/table/DragSelection.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								web/src/apps/muse/base/table/DragSelection.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,172 @@
 | 
				
			|||||||
 | 
					# 表格拖拽多选功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 功能概述
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					表格组件现在支持拖拽多选功能,用户可以通过鼠标拖拽来快速选择多行数据,提升批量操作的效率。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 功能特性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### ✅ 已实现的功能
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **基础拖拽选择**
 | 
				
			||||||
 | 
					   - 在表格行上拖拽可以选择多行
 | 
				
			||||||
 | 
					   - 实时显示选择框和选中状态
 | 
				
			||||||
 | 
					   - 拖拽结束后更新选中状态
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. **智能交互**
 | 
				
			||||||
 | 
					   - 避免在复选框和操作按钮区域启动拖拽
 | 
				
			||||||
 | 
					   - 设置最小拖拽距离阈值,避免意外触发
 | 
				
			||||||
 | 
					   - 拖拽过程中禁用文本选择
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. **键盘修饰键支持**
 | 
				
			||||||
 | 
					   - **Ctrl/Cmd + 拖拽**:追加选择模式(切换选中状态)
 | 
				
			||||||
 | 
					   - **Shift + 点击复选框**:范围选择模式
 | 
				
			||||||
 | 
					   - **Ctrl/Cmd + A**:全选
 | 
				
			||||||
 | 
					   - **Escape**:取消拖拽或清空选择
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. **虚拟滚动兼容**
 | 
				
			||||||
 | 
					   - 正确处理虚拟滚动的坐标转换
 | 
				
			||||||
 | 
					   - 考虑滚动位置的行索引计算
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					5. **视觉反馈**
 | 
				
			||||||
 | 
					   - 拖拽选择框的实时显示
 | 
				
			||||||
 | 
					   - 选中行的高亮效果
 | 
				
			||||||
 | 
					   - 拖拽过程中的样式变化
 | 
				
			||||||
 | 
					   - 用户操作提示
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 使用方法
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 基本配置
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					import { Table } from './base/table/Table';
 | 
				
			||||||
 | 
					import { RowSelection } from './base/table/types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rowSelection: RowSelection<Mark> = {
 | 
				
			||||||
 | 
					  type: 'checkbox',
 | 
				
			||||||
 | 
					  selectedRowKeys,
 | 
				
			||||||
 | 
					  onChange: (selectedRowKeys, selectedRows) => {
 | 
				
			||||||
 | 
					    // 处理选择变化
 | 
				
			||||||
 | 
					    setSelectedRowKeys(selectedRowKeys);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  dragSelection: {
 | 
				
			||||||
 | 
					    enabled: true, // 启用拖拽多选
 | 
				
			||||||
 | 
					    multi: true,   // 支持多选
 | 
				
			||||||
 | 
					    onDragStart: (startRowIndex) => {
 | 
				
			||||||
 | 
					      console.log('开始拖拽选择');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    onDragEnd: (selectedRows) => {
 | 
				
			||||||
 | 
					      console.log('拖拽选择结束');
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<Table
 | 
				
			||||||
 | 
					  data={data}
 | 
				
			||||||
 | 
					  columns={columns}
 | 
				
			||||||
 | 
					  rowSelection={rowSelection}
 | 
				
			||||||
 | 
					  // ... 其他属性
 | 
				
			||||||
 | 
					/>
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 配置选项
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### `DragSelectionConfig`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```typescript
 | 
				
			||||||
 | 
					interface DragSelectionConfig {
 | 
				
			||||||
 | 
					  enabled?: boolean; // 是否启用拖拽选择,默认为 true
 | 
				
			||||||
 | 
					  multi?: boolean; // 是否支持多选,默认为 true
 | 
				
			||||||
 | 
					  onDragStart?: (startRowIndex: number) => void; // 拖拽开始回调
 | 
				
			||||||
 | 
					  onDragEnd?: (selectedRows: Mark[]) => void; // 拖拽结束回调
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 用户操作指南
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **基础拖拽选择**
 | 
				
			||||||
 | 
					   - 在表格行(非复选框/操作按钮区域)按下鼠标左键
 | 
				
			||||||
 | 
					   - 拖拽至目标行
 | 
				
			||||||
 | 
					   - 释放鼠标完成选择
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. **追加选择**
 | 
				
			||||||
 | 
					   - 按住 `Ctrl`(Windows/Linux)或 `Cmd`(Mac)键
 | 
				
			||||||
 | 
					   - 进行拖拽选择
 | 
				
			||||||
 | 
					   - 已选中的行会切换状态(选中变未选中,未选中变选中)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. **范围选择**
 | 
				
			||||||
 | 
					   - 先点击一个复选框选中一行
 | 
				
			||||||
 | 
					   - 按住 `Shift` 键
 | 
				
			||||||
 | 
					   - 点击另一个复选框
 | 
				
			||||||
 | 
					   - 两行之间的所有行都会被选中
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					4. **键盘快捷键**
 | 
				
			||||||
 | 
					   - `Ctrl/Cmd + A`:全选所有行
 | 
				
			||||||
 | 
					   - `Escape`:取消当前拖拽操作或清空所有选择
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 技术实现
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 核心组件
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **状态管理**
 | 
				
			||||||
 | 
					   - `DragSelectionState`:管理拖拽状态
 | 
				
			||||||
 | 
					   - 鼠标位置跟踪
 | 
				
			||||||
 | 
					   - 选择范围计算
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. **事件处理**
 | 
				
			||||||
 | 
					   - `handleMouseDown`:开始拖拽检测
 | 
				
			||||||
 | 
					   - `handleMouseMove`:拖拽过程处理
 | 
				
			||||||
 | 
					   - `handleMouseUp`:完成选择操作
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. **坐标转换**
 | 
				
			||||||
 | 
					   - 虚拟滚动坐标映射
 | 
				
			||||||
 | 
					   - 行索引计算
 | 
				
			||||||
 | 
					   - 碰撞检测算法
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 样式类名
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `.row-selected`:选中行样式
 | 
				
			||||||
 | 
					- `.row-drag-selected`:拖拽选中行样式
 | 
				
			||||||
 | 
					- `.drag-selection-box`:拖拽选择框样式
 | 
				
			||||||
 | 
					- `.drag-hint`:操作提示样式
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 性能优化
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. **事件防抖**
 | 
				
			||||||
 | 
					   - 设置拖拽距离阈值
 | 
				
			||||||
 | 
					   - 避免频繁状态更新
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. **虚拟滚动适配**
 | 
				
			||||||
 | 
					   - 只处理可视区域内的行
 | 
				
			||||||
 | 
					   - 优化大数据量场景
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					3. **内存管理**
 | 
				
			||||||
 | 
					   - 及时清理事件监听器
 | 
				
			||||||
 | 
					   - 合理的状态重置
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 兼容性
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- ✅ 支持现有的复选框选择
 | 
				
			||||||
 | 
					- ✅ 兼容虚拟滚动
 | 
				
			||||||
 | 
					- ✅ 支持排序和筛选
 | 
				
			||||||
 | 
					- ✅ 响应式设计
 | 
				
			||||||
 | 
					- ✅ 键盘导航友好
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 示例代码
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					查看 `DragSelectionExample.tsx` 文件获取完整的使用示例。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 注意事项
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					1. 拖拽选择不会在复选框和操作按钮区域启动
 | 
				
			||||||
 | 
					2. 需要设置合适的行高以确保拖拽体验
 | 
				
			||||||
 | 
					3. 大数据量时建议启用虚拟滚动
 | 
				
			||||||
 | 
					4. 移动端设备可能需要额外的触摸事件处理
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 未来规划
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [ ] 触摸设备支持
 | 
				
			||||||
 | 
					- [ ] 自定义拖拽选择框样式
 | 
				
			||||||
 | 
					- [ ] 更多键盘快捷键
 | 
				
			||||||
 | 
					- [ ] 拖拽选择动画效果
 | 
				
			||||||
 | 
					- [ ] 选择统计和操作面板
 | 
				
			||||||
							
								
								
									
										115
									
								
								web/src/apps/muse/base/table/DragSelectionExample.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								web/src/apps/muse/base/table/DragSelectionExample.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,115 @@
 | 
				
			|||||||
 | 
					// 使用拖拽多选功能的示例
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import React, { useState } from 'react';
 | 
				
			||||||
 | 
					import { Table } from './Table';
 | 
				
			||||||
 | 
					import { Mark } from '../mock/collection';
 | 
				
			||||||
 | 
					import { TableColumn, RowSelection } from './types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const ExampleTable: React.FC = () => {
 | 
				
			||||||
 | 
					  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 示例数据
 | 
				
			||||||
 | 
					  const data: Mark[] = [
 | 
				
			||||||
 | 
					    { 
 | 
				
			||||||
 | 
					      id: '1', 
 | 
				
			||||||
 | 
					      title: '示例标记 1', 
 | 
				
			||||||
 | 
					      description: '这是第一个标记的描述',
 | 
				
			||||||
 | 
					      data: { content: '这是第一个标记' },
 | 
				
			||||||
 | 
					      createdAt: new Date(),
 | 
				
			||||||
 | 
					      updatedAt: new Date()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    { 
 | 
				
			||||||
 | 
					      id: '2', 
 | 
				
			||||||
 | 
					      title: '示例标记 2', 
 | 
				
			||||||
 | 
					      description: '这是第二个标记的描述',
 | 
				
			||||||
 | 
					      data: { content: '这是第二个标记' },
 | 
				
			||||||
 | 
					      createdAt: new Date(),
 | 
				
			||||||
 | 
					      updatedAt: new Date()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    { 
 | 
				
			||||||
 | 
					      id: '3', 
 | 
				
			||||||
 | 
					      title: '示例标记 3', 
 | 
				
			||||||
 | 
					      description: '这是第三个标记的描述',
 | 
				
			||||||
 | 
					      data: { content: '这是第三个标记' },
 | 
				
			||||||
 | 
					      createdAt: new Date(),
 | 
				
			||||||
 | 
					      updatedAt: new Date()
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    // ... 更多数据
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 表格列配置
 | 
				
			||||||
 | 
					  const columns: TableColumn<Mark>[] = [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: 'title',
 | 
				
			||||||
 | 
					      title: '标题',
 | 
				
			||||||
 | 
					      dataIndex: 'title',
 | 
				
			||||||
 | 
					      width: 200,
 | 
				
			||||||
 | 
					      sortable: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: 'description',
 | 
				
			||||||
 | 
					      title: '描述',
 | 
				
			||||||
 | 
					      dataIndex: 'description',
 | 
				
			||||||
 | 
					      width: 300,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      key: 'createdAt',
 | 
				
			||||||
 | 
					      title: '创建时间',
 | 
				
			||||||
 | 
					      dataIndex: 'createdAt',
 | 
				
			||||||
 | 
					      width: 180,
 | 
				
			||||||
 | 
					      render: (value: Date) => value.toLocaleString(),
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 行选择配置,启用拖拽多选
 | 
				
			||||||
 | 
					  const rowSelection: RowSelection<Mark> = {
 | 
				
			||||||
 | 
					    type: 'checkbox',
 | 
				
			||||||
 | 
					    selectedRowKeys,
 | 
				
			||||||
 | 
					    onChange: (selectedRowKeys: React.Key[], selectedRows: Mark[]) => {
 | 
				
			||||||
 | 
					      console.log('选中的行键:', selectedRowKeys);
 | 
				
			||||||
 | 
					      console.log('选中的行数据:', selectedRows);
 | 
				
			||||||
 | 
					      setSelectedRowKeys(selectedRowKeys);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    dragSelection: {
 | 
				
			||||||
 | 
					      enabled: true, // 启用拖拽多选
 | 
				
			||||||
 | 
					      multi: true,   // 支持多选
 | 
				
			||||||
 | 
					      onDragStart: (startRowIndex: number) => {
 | 
				
			||||||
 | 
					        console.log('开始拖拽选择,起始行索引:', startRowIndex);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      onDragEnd: (selectedRows: Mark[]) => {
 | 
				
			||||||
 | 
					        console.log('拖拽选择结束,选中的行:', selectedRows);
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div style={{ height: '600px', padding: '20px' }}>
 | 
				
			||||||
 | 
					      <h2>表格拖拽多选示例</h2>
 | 
				
			||||||
 | 
					      <p>
 | 
				
			||||||
 | 
					        <strong>使用说明:</strong>
 | 
				
			||||||
 | 
					        <br />
 | 
				
			||||||
 | 
					        • 单击复选框进行单个选择
 | 
				
			||||||
 | 
					        <br />
 | 
				
			||||||
 | 
					        • 在表格行上拖拽可以选择多行(避开复选框和操作按钮)
 | 
				
			||||||
 | 
					        <br />
 | 
				
			||||||
 | 
					        • 按住 Ctrl/Cmd 键 + 拖拽可以追加选择
 | 
				
			||||||
 | 
					        <br />
 | 
				
			||||||
 | 
					        • 按住 Shift 键 + 点击复选框可以进行范围选择
 | 
				
			||||||
 | 
					        <br />
 | 
				
			||||||
 | 
					        • 按 Ctrl/Cmd + A 可以全选
 | 
				
			||||||
 | 
					        <br />
 | 
				
			||||||
 | 
					        • 按 Escape 键可以取消选择或取消拖拽操作
 | 
				
			||||||
 | 
					      </p>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      <Table
 | 
				
			||||||
 | 
					        data={data}
 | 
				
			||||||
 | 
					        columns={columns}
 | 
				
			||||||
 | 
					        rowSelection={rowSelection}
 | 
				
			||||||
 | 
					        virtualScroll={{ rowHeight: 48 }}
 | 
				
			||||||
 | 
					        loading={false}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default ExampleTable;
 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import React, { useState, useMemo } from 'react';
 | 
					import React, { useState, useMemo, useRef, useCallback, useEffect } from 'react';
 | 
				
			||||||
import { AutoSizer, List } from 'react-virtualized';
 | 
					import { AutoSizer, List } from 'react-virtualized';
 | 
				
			||||||
import { Mark } from '../mock/collection';
 | 
					import { Mark } from '../mock/collection';
 | 
				
			||||||
import { TableProps, SortState } from './types';
 | 
					import { TableProps, SortState, DragSelectionState } from './types';
 | 
				
			||||||
import './table.css';
 | 
					import './table.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 虚拟滚动常量
 | 
					// 虚拟滚动常量
 | 
				
			||||||
@@ -19,6 +19,24 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const rowHeight = virtualScroll?.rowHeight || DEFAULT_ROW_HEIGHT;
 | 
					  const rowHeight = virtualScroll?.rowHeight || DEFAULT_ROW_HEIGHT;
 | 
				
			||||||
  const [sortState, setSortState] = useState<SortState>({ field: null, order: null });
 | 
					  const [sortState, setSortState] = useState<SortState>({ field: null, order: null });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 拖拽选择状态
 | 
				
			||||||
 | 
					  const [dragState, setDragState] = useState<DragSelectionState>({
 | 
				
			||||||
 | 
					    isDragging: false,
 | 
				
			||||||
 | 
					    startPosition: null,
 | 
				
			||||||
 | 
					    endPosition: null,
 | 
				
			||||||
 | 
					    startRowIndex: null,
 | 
				
			||||||
 | 
					    dragRect: null,
 | 
				
			||||||
 | 
					    selectedDuringDrag: new Set()
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // DOM 引用
 | 
				
			||||||
 | 
					  const tableBodyRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					  const selectionBoxRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					  const listRef = useRef<List>(null);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 拖拽选择配置
 | 
				
			||||||
 | 
					  const dragSelectionEnabled = rowSelection?.dragSelection?.enabled !== false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 处理排序
 | 
					  // 处理排序
 | 
				
			||||||
  const handleSort = (field: string) => {
 | 
					  const handleSort = (field: string) => {
 | 
				
			||||||
@@ -65,17 +83,40 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
    rowSelection.onChange?.(selectedKeys, selectedRows);
 | 
					    rowSelection.onChange?.(selectedKeys, selectedRows);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 单行选择
 | 
					  // 上次点击的行索引(用于Shift范围选择)
 | 
				
			||||||
  const handleRowSelect = (record: Mark, checked: boolean) => {
 | 
					  const lastClickedRowRef = useRef<number | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 单行选择 - 支持Shift范围选择
 | 
				
			||||||
 | 
					  const handleRowSelect = (record: Mark, checked: boolean, event?: React.ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
    if (!rowSelection) return;
 | 
					    if (!rowSelection) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const currentKeys = rowSelection.selectedRowKeys || [];
 | 
					    const currentKeys = rowSelection.selectedRowKeys || [];
 | 
				
			||||||
    const newKeys = checked
 | 
					    const recordIndex = displayData.findIndex(item => item.id === record.id);
 | 
				
			||||||
      ? [...currentKeys, record.id]
 | 
					    
 | 
				
			||||||
      : currentKeys.filter(key => key !== record.id);
 | 
					    // 处理Shift键范围选择
 | 
				
			||||||
 | 
					    if (event?.nativeEvent && (event.nativeEvent as any).shiftKey && lastClickedRowRef.current !== null) {
 | 
				
			||||||
 | 
					      const startIndex = Math.min(lastClickedRowRef.current, recordIndex);
 | 
				
			||||||
 | 
					      const endIndex = Math.max(lastClickedRowRef.current, recordIndex);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      const rangeKeys = displayData.slice(startIndex, endIndex + 1).map(item => item.id);
 | 
				
			||||||
 | 
					      const newKeys = checked 
 | 
				
			||||||
 | 
					        ? [...new Set([...currentKeys, ...rangeKeys])]
 | 
				
			||||||
 | 
					        : currentKeys.filter(key => !rangeKeys.includes(key as string));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					      const selectedRows = data.filter(item => newKeys.includes(item.id));
 | 
				
			||||||
 | 
					      rowSelection.onChange?.(newKeys, selectedRows);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // 普通单行选择
 | 
				
			||||||
 | 
					      const newKeys = checked
 | 
				
			||||||
 | 
					        ? [...currentKeys, record.id]
 | 
				
			||||||
 | 
					        : currentKeys.filter(key => key !== record.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const selectedRows = data.filter(item => newKeys.includes(item.id));
 | 
					      const selectedRows = data.filter(item => newKeys.includes(item.id));
 | 
				
			||||||
    rowSelection.onChange?.(newKeys, selectedRows);
 | 
					      rowSelection.onChange?.(newKeys, selectedRows);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 更新上次点击的行索引
 | 
				
			||||||
 | 
					    lastClickedRowRef.current = recordIndex;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 获取嵌套值
 | 
					  // 获取嵌套值
 | 
				
			||||||
@@ -83,18 +124,285 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
    return path.split('.').reduce((o, p) => o?.[p], obj);
 | 
					    return path.split('.').reduce((o, p) => o?.[p], obj);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 根据坐标获取行索引(考虑虚拟滚动的滚动位置)
 | 
				
			||||||
 | 
					  const getRowIndexFromPosition = useCallback((y: number): number => {
 | 
				
			||||||
 | 
					    if (!tableBodyRef.current || !listRef.current) return -1;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const rect = tableBodyRef.current.getBoundingClientRect();
 | 
				
			||||||
 | 
					    const relativeY = y - rect.top;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 获取当前滚动位置
 | 
				
			||||||
 | 
					    const scrollTop = (listRef.current as any).Grid?._scrollingContainer?.scrollTop || 0;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 计算实际的行索引,考虑滚动位置
 | 
				
			||||||
 | 
					    const actualY = relativeY + scrollTop;
 | 
				
			||||||
 | 
					    const rowIndex = Math.floor(actualY / rowHeight);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return Math.max(0, Math.min(rowIndex, displayData.length - 1));
 | 
				
			||||||
 | 
					  }, [rowHeight, displayData.length]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 计算拖拽选择范围内的行
 | 
				
			||||||
 | 
					  const getRowsInDragRange = useCallback((startY: number, endY: number): number[] => {
 | 
				
			||||||
 | 
					    if (!tableBodyRef.current) return [];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const minY = Math.min(startY, endY);
 | 
				
			||||||
 | 
					    const maxY = Math.max(startY, endY);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const startRowIndex = getRowIndexFromPosition(minY);
 | 
				
			||||||
 | 
					    const endRowIndex = getRowIndexFromPosition(maxY);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const rows: number[] = [];
 | 
				
			||||||
 | 
					    for (let i = startRowIndex; i <= endRowIndex; i++) {
 | 
				
			||||||
 | 
					      if (i >= 0 && i < displayData.length) {
 | 
				
			||||||
 | 
					        rows.push(i);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return rows;
 | 
				
			||||||
 | 
					  }, [getRowIndexFromPosition, displayData.length]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 检查拖拽选择框与行的碰撞检测
 | 
				
			||||||
 | 
					  const isRowInDragSelection = useCallback((rowIndex: number): boolean => {
 | 
				
			||||||
 | 
					    if (!dragState.dragRect || !tableBodyRef.current) return false;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    const rect = tableBodyRef.current.getBoundingClientRect();
 | 
				
			||||||
 | 
					    const rowTop = rowIndex * rowHeight;
 | 
				
			||||||
 | 
					    const rowBottom = rowTop + rowHeight;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 将拖拽选择框坐标转换为相对于表格的坐标
 | 
				
			||||||
 | 
					    const selectionTop = dragState.dragRect.top - rect.top;
 | 
				
			||||||
 | 
					    const selectionBottom = dragState.dragRect.bottom - rect.top;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 检查行和选择框的垂直重叠
 | 
				
			||||||
 | 
					    return !(rowBottom < selectionTop || rowTop > selectionBottom);
 | 
				
			||||||
 | 
					  }, [dragState.dragRect, rowHeight]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 更新拖拽选择状态
 | 
				
			||||||
 | 
					  const updateDragSelection = useCallback((currentPosition: { x: number; y: number }) => {
 | 
				
			||||||
 | 
					    if (!dragState.isDragging || !dragState.startPosition) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rowsInRange = getRowsInDragRange(dragState.startPosition.y, currentPosition.y);
 | 
				
			||||||
 | 
					    const newSelectedKeys = new Set(rowsInRange.map(index => displayData[index].id));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setDragState(prev => ({
 | 
				
			||||||
 | 
					      ...prev,
 | 
				
			||||||
 | 
					      endPosition: currentPosition,
 | 
				
			||||||
 | 
					      selectedDuringDrag: newSelectedKeys,
 | 
				
			||||||
 | 
					      dragRect: {
 | 
				
			||||||
 | 
					        left: Math.min(dragState.startPosition!.x, currentPosition.x),
 | 
				
			||||||
 | 
					        top: Math.min(dragState.startPosition!.y, currentPosition.y),
 | 
				
			||||||
 | 
					        right: Math.max(dragState.startPosition!.x, currentPosition.x),
 | 
				
			||||||
 | 
					        bottom: Math.max(dragState.startPosition!.y, currentPosition.y),
 | 
				
			||||||
 | 
					        width: Math.abs(currentPosition.x - dragState.startPosition!.x),
 | 
				
			||||||
 | 
					        height: Math.abs(currentPosition.y - dragState.startPosition!.y),
 | 
				
			||||||
 | 
					        x: Math.min(dragState.startPosition!.x, currentPosition.x),
 | 
				
			||||||
 | 
					        y: Math.min(dragState.startPosition!.y, currentPosition.y),
 | 
				
			||||||
 | 
					        toJSON: () => ({})
 | 
				
			||||||
 | 
					      } as DOMRect
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }, [dragState.isDragging, dragState.startPosition, getRowsInDragRange, displayData]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 拖拽最小距离阈值(像素)
 | 
				
			||||||
 | 
					  const DRAG_THRESHOLD = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 鼠标按下事件处理
 | 
				
			||||||
 | 
					  const handleMouseDown = useCallback((event: React.MouseEvent) => {
 | 
				
			||||||
 | 
					    if (!dragSelectionEnabled || !rowSelection || event.button !== 0) return;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 如果点击的是复选框或操作按钮,不启动拖拽选择
 | 
				
			||||||
 | 
					    const target = event.target as HTMLElement;
 | 
				
			||||||
 | 
					    if (target.tagName === 'INPUT' || target.closest('.selection-column') || target.closest('.actions-column')) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const rect = tableBodyRef.current?.getBoundingClientRect();
 | 
				
			||||||
 | 
					    if (!rect) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const startPosition = {
 | 
				
			||||||
 | 
					      x: event.clientX,
 | 
				
			||||||
 | 
					      y: event.clientY
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const startRowIndex = getRowIndexFromPosition(event.clientY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setDragState({
 | 
				
			||||||
 | 
					      isDragging: false, // 先不设为true,等达到阈值再设置
 | 
				
			||||||
 | 
					      startPosition,
 | 
				
			||||||
 | 
					      endPosition: startPosition,
 | 
				
			||||||
 | 
					      startRowIndex,
 | 
				
			||||||
 | 
					      dragRect: null,
 | 
				
			||||||
 | 
					      selectedDuringDrag: new Set()
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event.preventDefault();
 | 
				
			||||||
 | 
					  }, [dragSelectionEnabled, rowSelection, getRowIndexFromPosition]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 鼠标移动事件处理
 | 
				
			||||||
 | 
					  const handleMouseMove = useCallback((event: MouseEvent) => {
 | 
				
			||||||
 | 
					    if (!dragState.startPosition) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const currentPosition = {
 | 
				
			||||||
 | 
					      x: event.clientX,
 | 
				
			||||||
 | 
					      y: event.clientY
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 检查是否超过拖拽阈值
 | 
				
			||||||
 | 
					    const deltaX = Math.abs(currentPosition.x - dragState.startPosition.x);
 | 
				
			||||||
 | 
					    const deltaY = Math.abs(currentPosition.y - dragState.startPosition.y);
 | 
				
			||||||
 | 
					    const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!dragState.isDragging && distance > DRAG_THRESHOLD) {
 | 
				
			||||||
 | 
					      // 达到阈值,开始拖拽选择
 | 
				
			||||||
 | 
					      setDragState(prev => ({
 | 
				
			||||||
 | 
					        ...prev,
 | 
				
			||||||
 | 
					        isDragging: true
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // 调用拖拽开始回调
 | 
				
			||||||
 | 
					      rowSelection?.dragSelection?.onDragStart?.(dragState.startRowIndex!);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (dragState.isDragging) {
 | 
				
			||||||
 | 
					      updateDragSelection(currentPosition);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [dragState.startPosition, dragState.isDragging, dragState.startRowIndex, updateDragSelection, rowSelection]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 鼠标抬起事件处理
 | 
				
			||||||
 | 
					  const handleMouseUp = useCallback((event: MouseEvent) => {
 | 
				
			||||||
 | 
					    if (!dragState.startPosition) return;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 如果没有开始拖拽(未达到阈值),直接重置状态
 | 
				
			||||||
 | 
					    if (!dragState.isDragging) {
 | 
				
			||||||
 | 
					      setDragState({
 | 
				
			||||||
 | 
					        isDragging: false,
 | 
				
			||||||
 | 
					        startPosition: null,
 | 
				
			||||||
 | 
					        endPosition: null,
 | 
				
			||||||
 | 
					        startRowIndex: null,
 | 
				
			||||||
 | 
					        dragRect: null,
 | 
				
			||||||
 | 
					        selectedDuringDrag: new Set()
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const selectedRows = displayData.filter(item => dragState.selectedDuringDrag.has(item.id));
 | 
				
			||||||
 | 
					    const selectedKeys = Array.from(dragState.selectedDuringDrag);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 处理选择模式 - Ctrl/Cmd 键多选,Shift 键范围选择
 | 
				
			||||||
 | 
					    const isCtrlOrCmd = event.ctrlKey || event.metaKey;
 | 
				
			||||||
 | 
					    const currentSelectedKeys = rowSelection?.selectedRowKeys || [];
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let newSelectedKeys: React.Key[];
 | 
				
			||||||
 | 
					    if (isCtrlOrCmd) {
 | 
				
			||||||
 | 
					      // Ctrl/Cmd + 拖拽:切换选择状态
 | 
				
			||||||
 | 
					      const existingKeys = new Set(currentSelectedKeys);
 | 
				
			||||||
 | 
					      selectedKeys.forEach(key => {
 | 
				
			||||||
 | 
					        if (existingKeys.has(key)) {
 | 
				
			||||||
 | 
					          existingKeys.delete(key);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          existingKeys.add(key);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      newSelectedKeys = Array.from(existingKeys);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // 普通拖拽:替换选择
 | 
				
			||||||
 | 
					      newSelectedKeys = selectedKeys;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const finalSelectedRows = displayData.filter(item => newSelectedKeys.includes(item.id));
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 更新选择状态
 | 
				
			||||||
 | 
					    rowSelection?.onChange?.(newSelectedKeys, finalSelectedRows);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // 调用拖拽结束回调
 | 
				
			||||||
 | 
					    rowSelection?.dragSelection?.onDragEnd?.(finalSelectedRows);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // 重置拖拽状态
 | 
				
			||||||
 | 
					    setDragState({
 | 
				
			||||||
 | 
					      isDragging: false,
 | 
				
			||||||
 | 
					      startPosition: null,
 | 
				
			||||||
 | 
					      endPosition: null,
 | 
				
			||||||
 | 
					      startRowIndex: null,
 | 
				
			||||||
 | 
					      dragRect: null,
 | 
				
			||||||
 | 
					      selectedDuringDrag: new Set()
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }, [dragState.isDragging, dragState.selectedDuringDrag, displayData, rowSelection]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 键盘事件处理
 | 
				
			||||||
 | 
					  const handleKeyDown = useCallback((event: KeyboardEvent) => {
 | 
				
			||||||
 | 
					    if (!rowSelection) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Ctrl/Cmd + A 全选
 | 
				
			||||||
 | 
					    if ((event.ctrlKey || event.metaKey) && event.key === 'a') {
 | 
				
			||||||
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					      handleSelectAll(true);
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Escape 取消选择
 | 
				
			||||||
 | 
					    if (event.key === 'Escape') {
 | 
				
			||||||
 | 
					      if (dragState.isDragging) {
 | 
				
			||||||
 | 
					        // 取消拖拽
 | 
				
			||||||
 | 
					        setDragState({
 | 
				
			||||||
 | 
					          isDragging: false,
 | 
				
			||||||
 | 
					          startPosition: null,
 | 
				
			||||||
 | 
					          endPosition: null,
 | 
				
			||||||
 | 
					          startRowIndex: null,
 | 
				
			||||||
 | 
					          dragRect: null,
 | 
				
			||||||
 | 
					          selectedDuringDrag: new Set()
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // 清空选择
 | 
				
			||||||
 | 
					        handleSelectAll(false);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [rowSelection, dragState.isDragging, handleSelectAll]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 添加全局鼠标事件监听器
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (dragState.startPosition) {
 | 
				
			||||||
 | 
					      document.addEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
 | 
					      document.addEventListener('mouseup', handleMouseUp);
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      if (dragState.isDragging) {
 | 
				
			||||||
 | 
					        document.body.style.userSelect = 'none'; // 禁用文本选择
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      return () => {
 | 
				
			||||||
 | 
					        document.removeEventListener('mousemove', handleMouseMove);
 | 
				
			||||||
 | 
					        document.removeEventListener('mouseup', handleMouseUp);
 | 
				
			||||||
 | 
					        document.body.style.userSelect = '';
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [dragState.startPosition, dragState.isDragging, handleMouseMove, handleMouseUp]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 添加键盘事件监听器
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    document.addEventListener('keydown', handleKeyDown);
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      document.removeEventListener('keydown', handleKeyDown);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [handleKeyDown]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 渲染虚拟滚动行
 | 
					  // 渲染虚拟滚动行
 | 
				
			||||||
  const rowRenderer = ({ index, key, style }: any) => {
 | 
					  const rowRenderer = ({ index, key, style }: any) => {
 | 
				
			||||||
    const record = displayData[index];
 | 
					    const record = displayData[index];
 | 
				
			||||||
 | 
					    const isSelected = selectedKeys.includes(record.id);
 | 
				
			||||||
 | 
					    const isDragSelected = dragState.selectedDuringDrag.has(record.id);
 | 
				
			||||||
 | 
					    const isHighlighted = isSelected || isDragSelected;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div key={key} style={style} className="table-row virtual-row">
 | 
					      <div 
 | 
				
			||||||
 | 
					        key={key} 
 | 
				
			||||||
 | 
					        style={style} 
 | 
				
			||||||
 | 
					        className={`table-row virtual-row ${isHighlighted ? 'row-selected' : ''} ${isDragSelected ? 'row-drag-selected' : ''}`}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        {rowSelection && (
 | 
					        {rowSelection && (
 | 
				
			||||||
          <div className="selection-column">
 | 
					          <div className="selection-column">
 | 
				
			||||||
            <input
 | 
					            <input
 | 
				
			||||||
              type="checkbox"
 | 
					              type="checkbox"
 | 
				
			||||||
              checked={selectedKeys.includes(record.id)}
 | 
					              checked={isSelected}
 | 
				
			||||||
              onChange={(e) => handleRowSelect(record, e.target.checked)}
 | 
					              onChange={(e) => handleRowSelect(record, e.target.checked, e)}
 | 
				
			||||||
              disabled={rowSelection.getCheckboxProps?.(record)?.disabled}
 | 
					              disabled={rowSelection.getCheckboxProps?.(record)?.disabled}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
@@ -147,10 +455,21 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
      {/* 表格工具栏 */}
 | 
					      {/* 表格工具栏 */}
 | 
				
			||||||
      {rowSelection && selectedKeys.length > 0 && (
 | 
					      {rowSelection && selectedKeys.length > 0 && (
 | 
				
			||||||
        <div className="table-toolbar">
 | 
					        <div className="table-toolbar">
 | 
				
			||||||
          <span className="selected-info">
 | 
					          <div className="selected-info">
 | 
				
			||||||
            已选择 {selectedKeys.length} 项
 | 
					            已选择 {selectedKeys.length} 项
 | 
				
			||||||
          </span>
 | 
					            {dragSelectionEnabled && (
 | 
				
			||||||
 | 
					              <span className="drag-hint">
 | 
				
			||||||
 | 
					                (支持拖拽多选,按住Ctrl/Cmd键可追加选择,按住Shift键可范围选择)
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
          <div className="bulk-actions">
 | 
					          <div className="bulk-actions">
 | 
				
			||||||
 | 
					            <button className="btn btn-secondary" onClick={() => {
 | 
				
			||||||
 | 
					              // 取消选中所有项
 | 
				
			||||||
 | 
					              handleSelectAll(false);
 | 
				
			||||||
 | 
					            }}>
 | 
				
			||||||
 | 
					              取消选中
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
            <button className="btn btn-danger" onClick={() => {
 | 
					            <button className="btn btn-danger" onClick={() => {
 | 
				
			||||||
              // 批量删除逻辑
 | 
					              // 批量删除逻辑
 | 
				
			||||||
              console.log('批量删除:', selectedKeys);
 | 
					              console.log('批量删除:', selectedKeys);
 | 
				
			||||||
@@ -207,11 +526,17 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {/* 虚拟滚动内容区域 */}
 | 
					        {/* 虚拟滚动内容区域 */}
 | 
				
			||||||
        <div className="table-body-wrapper">
 | 
					        <div 
 | 
				
			||||||
 | 
					          className="table-body-wrapper"
 | 
				
			||||||
 | 
					          ref={tableBodyRef}
 | 
				
			||||||
 | 
					          onMouseDown={handleMouseDown}
 | 
				
			||||||
 | 
					          style={{ cursor: dragState.isDragging ? 'crosshair' : 'default' }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
          {displayData.length > 0 ? (
 | 
					          {displayData.length > 0 ? (
 | 
				
			||||||
            <AutoSizer>
 | 
					            <AutoSizer>
 | 
				
			||||||
              {({ height, width }) => (
 | 
					              {({ height, width }) => (
 | 
				
			||||||
                <List
 | 
					                <List
 | 
				
			||||||
 | 
					                  ref={listRef}
 | 
				
			||||||
                  height={height}
 | 
					                  height={height}
 | 
				
			||||||
                  width={width}
 | 
					                  width={width}
 | 
				
			||||||
                  rowCount={displayData.length}
 | 
					                  rowCount={displayData.length}
 | 
				
			||||||
@@ -226,6 +551,22 @@ export const Table: React.FC<TableProps> = ({
 | 
				
			|||||||
              <p>暂无数据</p>
 | 
					              <p>暂无数据</p>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          {/* 拖拽选择框 */}
 | 
				
			||||||
 | 
					          {dragState.isDragging && dragState.dragRect && (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					              ref={selectionBoxRef}
 | 
				
			||||||
 | 
					              className="drag-selection-box"
 | 
				
			||||||
 | 
					              style={{
 | 
				
			||||||
 | 
					                position: 'absolute',
 | 
				
			||||||
 | 
					                left: dragState.dragRect.x - (tableBodyRef.current?.getBoundingClientRect().left || 0),
 | 
				
			||||||
 | 
					                top: dragState.dragRect.y - (tableBodyRef.current?.getBoundingClientRect().top || 0),
 | 
				
			||||||
 | 
					                width: dragState.dragRect.width,
 | 
				
			||||||
 | 
					                height: dragState.dragRect.height,
 | 
				
			||||||
 | 
					                pointerEvents: 'none'
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -235,29 +235,6 @@ export const Base = (props: Props) => {
 | 
				
			|||||||
            支持多选、排序、虚拟滚动等功能的数据表格示例
 | 
					            支持多选、排序、虚拟滚动等功能的数据表格示例
 | 
				
			||||||
          </p>
 | 
					          </p>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div>
 | 
					 | 
				
			||||||
          {/* 批量操作条 */}
 | 
					 | 
				
			||||||
          {selectedRowKeys.length > 0 && (
 | 
					 | 
				
			||||||
            <div style={{
 | 
					 | 
				
			||||||
              marginBottom: '16px',
 | 
					 | 
				
			||||||
              padding: '12px',
 | 
					 | 
				
			||||||
              backgroundColor: '#e6f7ff',
 | 
					 | 
				
			||||||
              borderRadius: '4px',
 | 
					 | 
				
			||||||
              display: 'flex',
 | 
					 | 
				
			||||||
              justifyContent: 'space-between',
 | 
					 | 
				
			||||||
              alignItems: 'center',
 | 
					 | 
				
			||||||
              flexShrink: 0
 | 
					 | 
				
			||||||
            }}>
 | 
					 | 
				
			||||||
              <span>已选择 {selectedRowKeys.length} 项</span>
 | 
					 | 
				
			||||||
              <button
 | 
					 | 
				
			||||||
                className="btn btn-danger"
 | 
					 | 
				
			||||||
                onClick={handleBatchDelete}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                批量删除
 | 
					 | 
				
			||||||
              </button>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        {/* 表格容器 - 占据剩余空间并支持滚动 */}
 | 
					        {/* 表格容器 - 占据剩余空间并支持滚动 */}
 | 
				
			||||||
        <div style={{
 | 
					        <div style={{
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,15 @@
 | 
				
			|||||||
.selected-info {
 | 
					.selected-info {
 | 
				
			||||||
  color: #666;
 | 
					  color: #666;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.drag-hint {
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #999;
 | 
				
			||||||
 | 
					  font-style: italic;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.bulk-actions {
 | 
					.bulk-actions {
 | 
				
			||||||
@@ -85,6 +94,30 @@
 | 
				
			|||||||
  position: relative; /* 为AutoSizer提供相对定位上下文 */
 | 
					  position: relative; /* 为AutoSizer提供相对定位上下文 */
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 拖拽选择框 */
 | 
				
			||||||
 | 
					.drag-selection-box {
 | 
				
			||||||
 | 
					  background: rgba(24, 144, 255, 0.1);
 | 
				
			||||||
 | 
					  border: 2px dashed #1890ff;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					  animation: dragBoxPulse 0.3s ease-in-out;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@keyframes dragBoxPulse {
 | 
				
			||||||
 | 
					  0% {
 | 
				
			||||||
 | 
					    transform: scale(0.95);
 | 
				
			||||||
 | 
					    opacity: 0.8;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  50% {
 | 
				
			||||||
 | 
					    transform: scale(1.02);
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  100% {
 | 
				
			||||||
 | 
					    transform: scale(1);
 | 
				
			||||||
 | 
					    opacity: 1;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 虚拟行样式 */
 | 
					/* 虚拟行样式 */
 | 
				
			||||||
.virtual-row {
 | 
					.virtual-row {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
@@ -92,12 +125,30 @@
 | 
				
			|||||||
  border-bottom: 1px solid #e8e8e8;
 | 
					  border-bottom: 1px solid #e8e8e8;
 | 
				
			||||||
  background: #fff;
 | 
					  background: #fff;
 | 
				
			||||||
  transition: background-color 0.2s;
 | 
					  transition: background-color 0.2s;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.virtual-row:hover {
 | 
					.virtual-row:hover {
 | 
				
			||||||
  background: #f5f5f5;
 | 
					  background: #f5f5f5;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 选中行样式 */
 | 
				
			||||||
 | 
					.virtual-row.row-selected {
 | 
				
			||||||
 | 
					  background: #e6f7ff !important;
 | 
				
			||||||
 | 
					  border-color: #91d5ff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.virtual-row.row-selected:hover {
 | 
				
			||||||
 | 
					  background: #bae7ff !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 拖拽选中行样式 */
 | 
				
			||||||
 | 
					.virtual-row.row-drag-selected {
 | 
				
			||||||
 | 
					  background: #f0f9ff !important;
 | 
				
			||||||
 | 
					  border-color: #69c0ff;
 | 
				
			||||||
 | 
					  box-shadow: inset 0 0 0 1px #40a9ff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.table-cell {
 | 
					.table-cell {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
@@ -150,10 +201,38 @@
 | 
				
			|||||||
.selection-column {
 | 
					.selection-column {
 | 
				
			||||||
  width: 48px;
 | 
					  width: 48px;
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.selection-column input[type="checkbox"] {
 | 
					.selection-column input[type="checkbox"] {
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transform: scale(1.1);
 | 
				
			||||||
 | 
					  transition: all 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.selection-column input[type="checkbox"]:hover {
 | 
				
			||||||
 | 
					  transform: scale(1.2);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.selection-column input[type="checkbox"]:checked {
 | 
				
			||||||
 | 
					  accent-color: #1890ff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 拖拽选择时的表格样式 */
 | 
				
			||||||
 | 
					.table-body-wrapper[style*="cursor: crosshair"] {
 | 
				
			||||||
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					  -webkit-user-select: none;
 | 
				
			||||||
 | 
					  -moz-user-select: none;
 | 
				
			||||||
 | 
					  -ms-user-select: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table-body-wrapper[style*="cursor: crosshair"] .virtual-row {
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table-body-wrapper[style*="cursor: crosshair"] .selection-column,
 | 
				
			||||||
 | 
					.table-body-wrapper[style*="cursor: crosshair"] .actions-column {
 | 
				
			||||||
 | 
					  pointer-events: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 操作列 */
 | 
					/* 操作列 */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,12 +11,31 @@ export interface TableColumn<T = any> {
 | 
				
			|||||||
  fixed?: 'left' | 'right';
 | 
					  fixed?: 'left' | 'right';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 拖拽选择配置
 | 
				
			||||||
 | 
					export interface DragSelectionConfig {
 | 
				
			||||||
 | 
					  enabled?: boolean; // 是否启用拖拽选择,默认为 true
 | 
				
			||||||
 | 
					  multi?: boolean; // 是否支持多选,默认为 true
 | 
				
			||||||
 | 
					  onDragStart?: (startRowIndex: number) => void; // 拖拽开始回调
 | 
				
			||||||
 | 
					  onDragEnd?: (selectedRows: Mark[]) => void; // 拖拽结束回调
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 拖拽选择状态
 | 
				
			||||||
 | 
					export interface DragSelectionState {
 | 
				
			||||||
 | 
					  isDragging: boolean;
 | 
				
			||||||
 | 
					  startPosition: { x: number; y: number } | null;
 | 
				
			||||||
 | 
					  endPosition: { x: number; y: number } | null;
 | 
				
			||||||
 | 
					  startRowIndex: number | null;
 | 
				
			||||||
 | 
					  dragRect: DOMRect | null;
 | 
				
			||||||
 | 
					  selectedDuringDrag: Set<React.Key>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 表格行选择配置
 | 
					// 表格行选择配置
 | 
				
			||||||
export interface RowSelection<T = any> {
 | 
					export interface RowSelection<T = any> {
 | 
				
			||||||
  type?: 'checkbox' | 'radio';
 | 
					  type?: 'checkbox' | 'radio';
 | 
				
			||||||
  selectedRowKeys?: React.Key[];
 | 
					  selectedRowKeys?: React.Key[];
 | 
				
			||||||
  onChange?: (selectedRowKeys: React.Key[], selectedRows: T[]) => void;
 | 
					  onChange?: (selectedRowKeys: React.Key[], selectedRows: T[]) => void;
 | 
				
			||||||
  getCheckboxProps?: (record: T) => { disabled?: boolean };
 | 
					  getCheckboxProps?: (record: T) => { disabled?: boolean };
 | 
				
			||||||
 | 
					  dragSelection?: DragSelectionConfig; // 拖拽选择配置
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 虚拟滚动配置
 | 
					// 虚拟滚动配置
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										179
									
								
								web/src/apps/muse/components/MarkDetail.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								web/src/apps/muse/components/MarkDetail.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,179 @@
 | 
				
			|||||||
 | 
					/* MarkDetail 组件样式 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-detail-container {
 | 
				
			||||||
 | 
					  background: #ffffff;
 | 
				
			||||||
 | 
					  border-radius: 8px;
 | 
				
			||||||
 | 
					  padding: 16px;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					  transition: all 0.3s ease;
 | 
				
			||||||
 | 
					  max-height: 200px;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-detail-container.expanded {
 | 
				
			||||||
 | 
					  max-height: none;
 | 
				
			||||||
 | 
					  overflow: visible;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-detail-container h2 {
 | 
				
			||||||
 | 
					  margin: 0 0 8px 0;
 | 
				
			||||||
 | 
					  font-size: 18px;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					  line-height: 1.4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-detail-container p {
 | 
				
			||||||
 | 
					  margin: 0 0 12px 0;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					  line-height: 1.5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 封面图片样式 */
 | 
				
			||||||
 | 
					.mark-cover {
 | 
				
			||||||
 | 
					  margin-bottom: 12px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-cover img {
 | 
				
			||||||
 | 
					  max-width: 100%;
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 摘要样式 */
 | 
				
			||||||
 | 
					.mark-summary {
 | 
				
			||||||
 | 
					  margin-bottom: 12px;
 | 
				
			||||||
 | 
					  padding: 12px;
 | 
				
			||||||
 | 
					  background-color: #f8f9fa;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  border-left: 4px solid #007bff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-summary h3 {
 | 
				
			||||||
 | 
					  margin: 0 0 8px 0;
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  font-weight: 600;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-summary p {
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					  color: #555;
 | 
				
			||||||
 | 
					  line-height: 1.4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 链接样式 */
 | 
				
			||||||
 | 
					.mark-link {
 | 
				
			||||||
 | 
					  margin-bottom: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-link a {
 | 
				
			||||||
 | 
					  color: #007bff;
 | 
				
			||||||
 | 
					  text-decoration: none;
 | 
				
			||||||
 | 
					  padding: 6px 12px;
 | 
				
			||||||
 | 
					  border: 1px solid #007bff;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-link a:hover {
 | 
				
			||||||
 | 
					  background-color: #007bff;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 类型和日期样式 */
 | 
				
			||||||
 | 
					.mark-type,
 | 
				
			||||||
 | 
					.mark-date {
 | 
				
			||||||
 | 
					  margin-bottom: 8px;
 | 
				
			||||||
 | 
					  font-size: 12px;
 | 
				
			||||||
 | 
					  color: #666;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-type span,
 | 
				
			||||||
 | 
					.mark-date span {
 | 
				
			||||||
 | 
					  background-color: #f1f3f4;
 | 
				
			||||||
 | 
					  padding: 2px 6px;
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 标签样式 */
 | 
				
			||||||
 | 
					.mark-tags {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  gap: 6px;
 | 
				
			||||||
 | 
					  margin-bottom: 12px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-tag {
 | 
				
			||||||
 | 
					  background-color: #e3f2fd;
 | 
				
			||||||
 | 
					  color: #1976d2;
 | 
				
			||||||
 | 
					  padding: 4px 8px;
 | 
				
			||||||
 | 
					  border-radius: 12px;
 | 
				
			||||||
 | 
					  font-size: 11px;
 | 
				
			||||||
 | 
					  font-weight: 500;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 展开/收起按钮样式 */
 | 
				
			||||||
 | 
					.mark-expand-button {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  gap: 6px;
 | 
				
			||||||
 | 
					  margin-top: 12px;
 | 
				
			||||||
 | 
					  padding: 8px 12px;
 | 
				
			||||||
 | 
					  background-color: #f8f9fa;
 | 
				
			||||||
 | 
					  border: 1px solid #dee2e6;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  font-size: 13px;
 | 
				
			||||||
 | 
					  color: #495057;
 | 
				
			||||||
 | 
					  transition: all 0.2s ease;
 | 
				
			||||||
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-expand-button:hover {
 | 
				
			||||||
 | 
					  background-color: #e9ecef;
 | 
				
			||||||
 | 
					  border-color: #adb5bd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mark-expand-button:active {
 | 
				
			||||||
 | 
					  transform: translateY(1px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 收起状态下的渐变遮罩 */
 | 
				
			||||||
 | 
					.mark-detail-container.collapsed::after {
 | 
				
			||||||
 | 
					  content: '';
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  height: 40px;
 | 
				
			||||||
 | 
					  background: linear-gradient(transparent, white);
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* 响应式设计 */
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					  .mark-detail-container {
 | 
				
			||||||
 | 
					    padding: 12px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .mark-detail-container h2 {
 | 
				
			||||||
 | 
					    font-size: 16px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .mark-detail-container p {
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .mark-expand-button {
 | 
				
			||||||
 | 
					    font-size: 12px;
 | 
				
			||||||
 | 
					    padding: 6px 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										104
									
								
								web/src/apps/muse/components/MarkDetal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								web/src/apps/muse/components/MarkDetal.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					import React, { useState } from 'react';
 | 
				
			||||||
 | 
					import { Maximize2, Minimize2 } from 'lucide-react';
 | 
				
			||||||
 | 
					import './MarkDetail.css';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type MarkShow = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  title?: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					  markType?: string;
 | 
				
			||||||
 | 
					  cover?: string;
 | 
				
			||||||
 | 
					  link?: string;
 | 
				
			||||||
 | 
					  summary?: string;
 | 
				
			||||||
 | 
					  key?: string;
 | 
				
			||||||
 | 
					  data: any;
 | 
				
			||||||
 | 
					  createdAt?: string;
 | 
				
			||||||
 | 
					  updatedAt?: string;
 | 
				
			||||||
 | 
					  markedAt?: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type SimpleMarkShow = {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  title?: string;
 | 
				
			||||||
 | 
					  description?: string;
 | 
				
			||||||
 | 
					  tags?: string[];
 | 
				
			||||||
 | 
					  cover?: string;
 | 
				
			||||||
 | 
					  link?: string;
 | 
				
			||||||
 | 
					  summary?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MarkDetailProps {
 | 
				
			||||||
 | 
					  data: MarkShow;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const MarkDetail: React.FC<MarkDetailProps> = ({ data }) => {
 | 
				
			||||||
 | 
					  const [isExpanded, setIsExpanded] = useState(false);
 | 
				
			||||||
 | 
					  const toggleExpanded = () => {
 | 
				
			||||||
 | 
					    setIsExpanded(!isExpanded);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className={`mark-detail-container ${isExpanded ? 'expanded' : 'collapsed'}`}>
 | 
				
			||||||
 | 
					      <h2>{data.title}</h2>
 | 
				
			||||||
 | 
					      <p>{data.description}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {data.cover && (
 | 
				
			||||||
 | 
					        <div className="mark-cover">
 | 
				
			||||||
 | 
					          <img src={data.cover} alt={data.title} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {data.summary && isExpanded && (
 | 
				
			||||||
 | 
					        <div className="mark-summary">
 | 
				
			||||||
 | 
					          <h3>摘要</h3>
 | 
				
			||||||
 | 
					          <p>{data.summary}</p>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {data.link && isExpanded && (
 | 
				
			||||||
 | 
					        <div className="mark-link">
 | 
				
			||||||
 | 
					          <a href={data.link} target="_blank" rel="noopener noreferrer">
 | 
				
			||||||
 | 
					            访问链接
 | 
				
			||||||
 | 
					          </a>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {data.markType && isExpanded && (
 | 
				
			||||||
 | 
					        <div className="mark-type">
 | 
				
			||||||
 | 
					          <span>类型: {data.markType}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {data.createdAt && isExpanded && (
 | 
				
			||||||
 | 
					        <div className="mark-date">
 | 
				
			||||||
 | 
					          <span>创建时间: {new Date(data.createdAt).toLocaleString()}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {data.updatedAt && isExpanded && (
 | 
				
			||||||
 | 
					        <div className="mark-date">
 | 
				
			||||||
 | 
					          <span>更新时间: {new Date(data.updatedAt).toLocaleString()}</span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className="mark-tags">
 | 
				
			||||||
 | 
					        {data.tags?.map(tag => (
 | 
				
			||||||
 | 
					          <span key={tag} className="mark-tag">{tag}</span>
 | 
				
			||||||
 | 
					        ))}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className="mark-expand-button" onClick={toggleExpanded}>
 | 
				
			||||||
 | 
					        {isExpanded ? (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            <Minimize2 size={16} />
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            <Maximize2 size={16} />
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user