@@ -23,6 +23,8 @@ export default function TabsArea() {
2323 const { state, dispatch } = useAppContext ( )
2424 const chatWindowRefs = useRef < Map < string , ChatWindowRef > > ( new Map ( ) )
2525 const [ settingsOpen , setSettingsOpen ] = React . useState ( false )
26+ const [ draggedTabId , setDraggedTabId ] = React . useState < string | null > ( null )
27+ const [ dragOverTabId , setDragOverTabId ] = React . useState < string | null > ( null )
2628
2729 // 设置ChatWindow引用的回调函数
2830 const setChatWindowRef = useCallback ( ( chatId : string , ref : ChatWindowRef | null ) => {
@@ -62,6 +64,88 @@ export default function TabsArea() {
6264 setSettingsOpen ( true )
6365 } , [ ] )
6466
67+ // 拖拽排序处理函数
68+ const handleDragStart = useCallback ( ( event : React . DragEvent , chatId : string ) => {
69+ setDraggedTabId ( chatId )
70+ event . dataTransfer . setData ( 'text/plain' , chatId )
71+ event . dataTransfer . effectAllowed = 'move'
72+ } , [ ] )
73+
74+ const handleDragOver = useCallback ( ( event : React . DragEvent , chatId : string ) => {
75+ event . preventDefault ( )
76+
77+ if ( draggedTabId ) {
78+ const sourceChat = state . pages . find ( p => p . id === draggedTabId )
79+ const targetChat = state . pages . find ( p => p . id === chatId )
80+
81+ const sourcePinned = sourceChat ?. pinned || false
82+ const targetPinned = targetChat ?. pinned || false
83+
84+ // 检查是否可以拖拽
85+ if ( sourcePinned !== targetPinned ) {
86+ event . dataTransfer . dropEffect = 'none'
87+ return
88+ }
89+ }
90+
91+ event . dataTransfer . dropEffect = 'move'
92+ setDragOverTabId ( chatId )
93+ } , [ draggedTabId , state . pages ] )
94+
95+ const handleDragLeave = useCallback ( ( event : React . DragEvent ) => {
96+ // 只在离开整个标签区域时清除
97+ if ( ! event . currentTarget . contains ( event . relatedTarget as Node ) ) {
98+ setDragOverTabId ( null )
99+ }
100+ } , [ ] )
101+
102+ const handleDrop = useCallback ( ( event : React . DragEvent , targetChatId : string ) => {
103+ event . preventDefault ( )
104+ const sourceChatId = event . dataTransfer . getData ( 'text/plain' )
105+
106+ if ( sourceChatId && sourceChatId !== targetChatId ) {
107+ const sourceChat = state . pages . find ( p => p . id === sourceChatId )
108+ const targetChat = state . pages . find ( p => p . id === targetChatId )
109+
110+ // 检查是否可以进行拖拽排序
111+ const sourcePinned = sourceChat ?. pinned || false
112+ const targetPinned = targetChat ?. pinned || false
113+
114+ // 固定标签页和非固定标签页不能互相拖拽
115+ if ( sourcePinned !== targetPinned ) {
116+ setDraggedTabId ( null )
117+ setDragOverTabId ( null )
118+ return
119+ }
120+
121+ const currentTabs = [ ...state . openTabs ]
122+ const sourceIndex = currentTabs . indexOf ( sourceChatId )
123+ const targetIndex = currentTabs . indexOf ( targetChatId )
124+
125+ if ( sourceIndex !== - 1 && targetIndex !== - 1 ) {
126+ // 移除源标签
127+ currentTabs . splice ( sourceIndex , 1 )
128+ // 在目标位置插入
129+ const newTargetIndex = sourceIndex < targetIndex ? targetIndex - 1 : targetIndex
130+ currentTabs . splice ( newTargetIndex , 0 , sourceChatId )
131+
132+ // 通过reducer进行排序,确保固定标签页在前
133+ dispatch ( {
134+ type : 'REORDER_TABS' ,
135+ payload : { newOrder : currentTabs }
136+ } )
137+ }
138+ }
139+
140+ setDraggedTabId ( null )
141+ setDragOverTabId ( null )
142+ } , [ state . openTabs , state . pages , dispatch ] )
143+
144+ const handleDragEnd = useCallback ( ( ) => {
145+ setDraggedTabId ( null )
146+ setDragOverTabId ( null )
147+ } , [ ] )
148+
65149 // 键盘快捷键处理
66150 useEffect ( ( ) => {
67151 const handleKeyDown = ( event : KeyboardEvent ) => {
@@ -173,11 +257,9 @@ export default function TabsArea() {
173257 </ span >
174258 ) ,
175259 icon : < CloseOutlined /> ,
176- disabled : isPinned && hasOtherTabs ,
260+ disabled : false ,
177261 onClick : ( ) => {
178- if ( ! isPinned || ! hasOtherTabs ) {
179- handleTabClose ( chatId )
180- }
262+ handleTabClose ( chatId )
181263 }
182264 } ,
183265 {
@@ -331,7 +413,7 @@ export default function TabsArea() {
331413 description = {
332414 < div style = { { textAlign : 'center' } } >
333415 < h3 style = { { color : '#262626' , marginBottom : 8 } } > 暂无打开的聊天</ h3 >
334- < p style = { { color : '#8c8c8c' , marginBottom : 24 } } >
416+ < p style = { { color : '#8c8c8c' , marginBottom : 24 } } >
335417 创建一个新聊天开始对话,或者尝试新的交叉视图分析
336418 </ p >
337419 < div style = { { display : 'flex' , gap : 12 , justifyContent : 'center' } } >
@@ -377,9 +459,25 @@ export default function TabsArea() {
377459 const chatStatus = getChatStatus ( chat )
378460 const isPinned = chat . pinned || false
379461
462+ // 检查是否可以与当前拖拽的标签页进行拖拽
463+ const canDragToThis = ! draggedTabId || ( ( ) => {
464+ const sourceChat = state . pages . find ( p => p . id === draggedTabId )
465+ const sourcePinned = sourceChat ?. pinned || false
466+ const targetPinned = chat . pinned || false
467+ return sourcePinned === targetPinned
468+ } ) ( )
469+
380470 const tabLabel = (
381471 < Dropdown menu = { { items : getContextMenuItems ( chatId ) } } trigger = { [ 'contextMenu' ] } >
382- < span className = "tab-label-content" >
472+ < span
473+ className = { `tab-label-content ${ draggedTabId === chatId ? 'dragging' : '' } ${ dragOverTabId === chatId ? ( canDragToThis ? 'drag-over' : 'drag-over-forbidden' ) : '' } ` }
474+ draggable
475+ onDragStart = { ( e ) => handleDragStart ( e , chatId ) }
476+ onDragOver = { ( e ) => handleDragOver ( e , chatId ) }
477+ onDragLeave = { handleDragLeave }
478+ onDrop = { ( e ) => handleDrop ( e , chatId ) }
479+ onDragEnd = { handleDragEnd }
480+ >
383481 { chat . type === 'crosstab' ? (
384482 < TableOutlined className = "message-icon" />
385483 ) : chat . type === 'object' ? (
@@ -407,7 +505,8 @@ export default function TabsArea() {
407505 ) : (
408506 < ChatWindow chatId = { chatId } ref = { ( ref ) => setChatWindowRef ( chatId , ref ) } />
409507 ) ,
410- closable : ! isPinned || state . openTabs . length === 1
508+ closable : true ,
509+ className : `${ draggedTabId === chatId ? 'dragging' : '' } ${ dragOverTabId === chatId ? 'drag-over' : '' } `
411510 }
412511 } )
413512 . filter ( ( item ) : item is NonNullable < typeof item > => item !== null )
@@ -434,7 +533,11 @@ export default function TabsArea() {
434533 return chat ?. pinned
435534 } )
436535 . map ( ( id ) => `[data-node-key="${ id } "]` )
437- . join ( ',' )
536+ . join ( ',' ) ,
537+ '--pinned-count' : state . openTabs . filter ( ( id ) => {
538+ const chat = state . pages . find ( ( c ) => c . id === id )
539+ return chat ?. pinned
540+ } ) . length
438541 } as React . CSSProperties
439542 }
440543 />
@@ -445,7 +548,7 @@ export default function TabsArea() {
445548 return chat ?. pinned
446549 } )
447550 . map (
448- ( id ) => `
551+ ( id , index , pinnedTabs ) => `
449552 .tabs-area .ant-tabs-tab[data-node-key="${ id } "] {
450553 background: linear-gradient(135deg, rgba(24, 144, 255, 0.1), rgba(24, 144, 255, 0.05)) !important;
451554 border-color: rgba(24, 144, 255, 0.3) !important;
@@ -461,10 +564,19 @@ export default function TabsArea() {
461564 background: #1890ff;
462565 border-radius: 0 2px 2px 0;
463566 }
464- .tabs-area .ant-tabs-tab[data-node-key="${ id } "] .ant-tabs-tab-remove {
465- opacity: 0.5 !important;
466- pointer-events: none !important;
567+ ${ index === pinnedTabs . length - 1 ? `
568+ .tabs-area .ant-tabs-tab[data-node-key="${ id } "]::after {
569+ content: '';
570+ position: absolute;
571+ right: -8px;
572+ top: 50%;
573+ transform: translateY(-50%);
574+ width: 1px;
575+ height: 20px;
576+ background: rgba(24, 144, 255, 0.3);
577+ border-radius: 1px;
467578 }
579+ ` : '' }
468580 `
469581 )
470582 . join ( '' ) }
0 commit comments