@@ -31,6 +31,23 @@ import { useState, useEffect, useRef, useCallback } from "react";
3131import { useNavigate } from "react-router-dom" ;
3232import { Icon } from "@iconify/react" ;
3333import { addToast } from "@heroui/toast" ;
34+ import {
35+ DndContext ,
36+ closestCenter ,
37+ KeyboardSensor ,
38+ PointerSensor ,
39+ useSensor ,
40+ useSensors ,
41+ DragEndEvent ,
42+ } from "@dnd-kit/core" ;
43+ import {
44+ arrayMove ,
45+ SortableContext ,
46+ sortableKeyboardCoordinates ,
47+ useSortable ,
48+ verticalListSortingStrategy ,
49+ } from "@dnd-kit/sortable" ;
50+ import { CSS } from "@dnd-kit/utilities" ;
3451import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" ;
3552import {
3653 faPlus ,
@@ -102,6 +119,86 @@ interface EndpointFormData {
102119 apiKey : string ;
103120}
104121
122+ // 可排序的表格行组件
123+ function SortableTableRow ( {
124+ id,
125+ result,
126+ index,
127+ } : {
128+ id : string ;
129+ result : any ;
130+ index : number ;
131+ } ) {
132+ const {
133+ attributes,
134+ listeners,
135+ setNodeRef,
136+ transform,
137+ transition,
138+ isDragging,
139+ } = useSortable ( { id } ) ;
140+
141+ const style = {
142+ transform : CSS . Transform . toString ( transform ) ,
143+ transition,
144+ opacity : isDragging ? 0.5 : 1 ,
145+ } ;
146+
147+ return (
148+ < tr
149+ ref = { setNodeRef }
150+ style = { style }
151+ className = "border-b border-divider hover:bg-default-50"
152+ >
153+ < td className = "px-3 py-3" >
154+ < div className = "flex items-center gap-2" >
155+ < button
156+ { ...attributes }
157+ { ...listeners }
158+ className = "cursor-grab active:cursor-grabbing text-default-400 hover:text-default-600"
159+ >
160+ < FontAwesomeIcon icon = { faGrip } />
161+ </ button >
162+ < span className = "text-small" > { result . name } </ span >
163+ </ div >
164+ </ td >
165+ < td className = "px-3 py-3 text-small font-mono text-xs" >
166+ { result . url }
167+ { result . apiPath }
168+ </ td >
169+ < td className = "px-3 py-3 text-small" >
170+ < span
171+ className = { `font-mono ${
172+ result . status === "success"
173+ ? "text-success"
174+ : result . status === "low_version"
175+ ? "text-warning"
176+ : "text-danger"
177+ } `}
178+ >
179+ { result . version }
180+ </ span >
181+ </ td >
182+ < td className = "px-3 py-3" >
183+ < div className = "flex flex-col gap-1" >
184+ < span
185+ className = { `text-xs ${
186+ result . status === "success"
187+ ? "text-success"
188+ : result . status === "low_version"
189+ ? "text-warning"
190+ : "text-danger"
191+ } `}
192+ >
193+ { result . canImport ? "✓ 可导入" : "✗ 不可导入" }
194+ </ span >
195+ < span className = "text-xs text-default-400" > { result . message } </ span >
196+ </ div >
197+ </ td >
198+ </ tr >
199+ ) ;
200+ }
201+
105202export default function EndpointsPage ( ) {
106203 const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
107204 const [ selectedFile , setSelectedFile ] = useState < File | null > ( null ) ;
@@ -138,8 +235,17 @@ export default function EndpointsPage() {
138235 } = useDisclosure ( ) ;
139236
140237 const [ importValidateResults , setImportValidateResults ] = useState < any [ ] > ( [ ] ) ;
238+ const [ sortedValidateResults , setSortedValidateResults ] = useState < any [ ] > ( [ ] ) ;
141239 const [ importFileData , setImportFileData ] = useState < any > ( null ) ;
142240
241+ // 拖拽传感器配置
242+ const sensors = useSensors (
243+ useSensor ( PointerSensor ) ,
244+ useSensor ( KeyboardSensor , {
245+ coordinateGetter : sortableKeyboardCoordinates ,
246+ } )
247+ ) ;
248+
143249 const {
144250 isOpen : isAddOpen ,
145251 onOpen : onAddOpen ,
@@ -559,7 +665,9 @@ export default function EndpointsPage() {
559665 }
560666
561667 // 关闭导入窗口,显示验证结果窗口
562- setImportValidateResults ( validateResult . results || [ ] ) ;
668+ const results = validateResult . results || [ ] ;
669+ setImportValidateResults ( results ) ;
670+ setSortedValidateResults ( results ) ; // 初始化排序结果
563671 onImportOpenChange ( ) ;
564672 onImportValidateOpen ( ) ;
565673 } catch ( error ) {
@@ -575,10 +683,24 @@ export default function EndpointsPage() {
575683 }
576684 } ;
577685
686+ // 处理拖拽结束事件
687+ const handleDragEnd = ( event : DragEndEvent ) => {
688+ const { active, over } = event ;
689+
690+ if ( over && active . id !== over . id ) {
691+ setSortedValidateResults ( ( items ) => {
692+ const oldIndex = items . findIndex ( ( item , idx ) => `item-${ idx } ` === active . id ) ;
693+ const newIndex = items . findIndex ( ( item , idx ) => `item-${ idx } ` === over . id ) ;
694+
695+ return arrayMove ( items , oldIndex , newIndex ) ;
696+ } ) ;
697+ }
698+ } ;
699+
578700 // 确认导入 - 只导入可导入的主控
579701 const handleConfirmImport = async ( ) => {
580- // 筛选出可导入的主控
581- const importableEndpoints = importValidateResults
702+ // 筛选出可导入的主控,使用排序后的结果
703+ const importableEndpoints = sortedValidateResults
582704 . filter ( ( result ) => result . canImport )
583705 . map ( ( result ) => ( {
584706 name : result . name ,
@@ -621,6 +743,7 @@ export default function EndpointsPage() {
621743 setSelectedFile ( null ) ;
622744 setImportFileData ( null ) ;
623745 setImportValidateResults ( [ ] ) ;
746+ setSortedValidateResults ( [ ] ) ;
624747 if ( fileInputRef . current ) {
625748 fileInputRef . current . value = "" ;
626749 }
@@ -1885,78 +2008,52 @@ export default function EndpointsPage() {
18852008 < ModalBody >
18862009 < div className = "flex flex-col gap-4" >
18872010 < p className = "text-small text-default-500" >
1888- 共检测到 { importValidateResults . length } 个主控,请查看验证结果:
2011+ 共检测到 { importValidateResults . length } 个主控,请查看验证结果(可拖动排序) :
18892012 </ p >
18902013 < div className = "max-h-[400px] overflow-y-auto" >
1891- < table className = "w-full" >
1892- < thead className = "sticky top-0 bg-default-100 z-10" >
1893- < tr >
1894- < th className = "text-left px-3 py-2 text-small font-semibold" >
1895- 名称
1896- </ th >
1897- < th className = "text-left px-3 py-2 text-small font-semibold" >
1898- URL
1899- </ th >
1900- < th className = "text-left px-3 py-2 text-small font-semibold" >
1901- 版本
1902- </ th >
1903- < th className = "text-left px-3 py-2 text-small font-semibold" >
1904- 状态
1905- </ th >
1906- </ tr >
1907- </ thead >
1908- < tbody >
1909- { importValidateResults . map ( ( result , index ) => (
1910- < tr
1911- key = { index }
1912- className = "border-b border-divider hover:bg-default-50"
1913- >
1914- < td className = "px-3 py-3 text-small" >
1915- { result . name }
1916- </ td >
1917- < td className = "px-3 py-3 text-small font-mono text-xs" >
1918- { result . url }
1919- { result . apiPath }
1920- </ td >
1921- < td className = "px-3 py-3 text-small" >
1922- < span
1923- className = { `font-mono ${
1924- result . status === "success"
1925- ? "text-success"
1926- : result . status === "low_version"
1927- ? "text-warning"
1928- : "text-danger"
1929- } `}
1930- >
1931- { result . version }
1932- </ span >
1933- </ td >
1934- < td className = "px-3 py-3" >
1935- < div className = "flex flex-col gap-1" >
1936- < span
1937- className = { `text-xs ${
1938- result . status === "success"
1939- ? "text-success"
1940- : result . status === "low_version"
1941- ? "text-warning"
1942- : "text-danger"
1943- } `}
1944- >
1945- { result . canImport ? "✓ 可导入" : "✗ 不可导入" }
1946- </ span >
1947- < span className = "text-xs text-default-400" >
1948- { result . message }
1949- </ span >
1950- </ div >
1951- </ td >
2014+ < DndContext
2015+ sensors = { sensors }
2016+ collisionDetection = { closestCenter }
2017+ onDragEnd = { handleDragEnd }
2018+ >
2019+ < table className = "w-full" >
2020+ < thead className = "sticky top-0 bg-default-100 z-10" >
2021+ < tr >
2022+ < th className = "text-left px-3 py-2 text-small font-semibold" >
2023+ 名称
2024+ </ th >
2025+ < th className = "text-left px-3 py-2 text-small font-semibold" >
2026+ URL
2027+ </ th >
2028+ < th className = "text-left px-3 py-2 text-small font-semibold" >
2029+ 版本
2030+ </ th >
2031+ < th className = "text-left px-3 py-2 text-small font-semibold" >
2032+ 状态
2033+ </ th >
19522034 </ tr >
1953- ) ) }
1954- </ tbody >
1955- </ table >
2035+ </ thead >
2036+ < tbody >
2037+ < SortableContext
2038+ items = { sortedValidateResults . map ( ( _ , idx ) => `item-${ idx } ` ) }
2039+ strategy = { verticalListSortingStrategy }
2040+ >
2041+ { sortedValidateResults . map ( ( result , index ) => (
2042+ < SortableTableRow
2043+ key = { `item-${ index } ` }
2044+ id = { `item-${ index } ` }
2045+ result = { result }
2046+ index = { index }
2047+ />
2048+ ) ) }
2049+ </ SortableContext >
2050+ </ tbody >
2051+ </ table >
2052+ </ DndContext >
19562053 </ div >
19572054 < div className = "rounded-lg bg-default-100 p-3" >
19582055 < p className = "text-xs text-default-600" >
1959- 注意:只有版本 ≥ 1.10.0 的主控才会被导入,低版本或连接失败的主控将被自动跳过。
2056+ 注意:只有版本 ≥ 1.10.0 的主控才会被导入,低版本或连接失败的主控将被自动跳过。拖动行可调整导入顺序。
19602057 </ p >
19612058 </ div >
19622059 </ div >
@@ -1969,6 +2066,7 @@ export default function EndpointsPage() {
19692066 onPress = { ( ) => {
19702067 onClose ( ) ;
19712068 setImportValidateResults ( [ ] ) ;
2069+ setSortedValidateResults ( [ ] ) ;
19722070 setImportFileData ( null ) ;
19732071 } }
19742072 >
0 commit comments