@@ -20,7 +20,7 @@ import { invoke } from "@tauri-apps/api/core";
2020import type { CourseGroup , OcrBlockHit , SegmentHit , Session , SessionListItem } from "../types" ;
2121import { fmtMs } from "../utils/fmt" ;
2222import { isSessionConvertible } from "../utils/sessionEligibility" ;
23- import { emptySelection , rangeSelection , toggleSelection } from "../utils/noteSelection " ;
23+ import { useSessionSelection } from "../hooks/useSessionSelection " ;
2424import SessionListRow from "./SessionListRow" ;
2525import type { SessionRenameRequest } from "./SessionListRow" ;
2626import SessionRowContextMenu from "./SessionRowContextMenu" ;
@@ -82,10 +82,6 @@ export default function SessionListPanel({
8282 const [ filterConverted , setFilterConverted ] = useState < ConvertedFilter > ( "all" ) ;
8383 const [ sortBy , setSortBy ] = useState < SortBy > ( "time-desc" ) ;
8484 const [ collapsed , setCollapsed ] = useState < Record < string , boolean > > ( { } ) ;
85- // ── 多选态(批 4):selectionMode=选择模式(单击=勾选);anchor=区间锚 ──
86- const [ selected , setSelected ] = useState < Set < number > > ( emptySelection ( ) ) ;
87- const [ anchor , setAnchor ] = useState < number | null > ( null ) ;
88- const [ selectionMode , setSelectionMode ] = useState ( false ) ;
8985 const [ contextMenu , setContextMenu ] = useState < { item : SessionListItem ; x : number ; y : number } | null > ( null ) ;
9086 // 行内重命名请求(nonce:同一行连续两次「重命名」也能重启编辑态)
9187 const [ renameReq , setRenameReq ] = useState < SessionRenameRequest | null > ( null ) ;
@@ -97,9 +93,6 @@ export default function SessionListPanel({
9793 const [ batchBusy , setBatchBusy ] = useState < "convert" | "delete" | null > ( null ) ;
9894 const batchBusyRef = useRef ( false ) ;
9995
100- const clearSelection = useCallback ( ( ) => { setSelected ( emptySelection ( ) ) ; setAnchor ( null ) ; } , [ ] ) ;
101- const exitBatch = useCallback ( ( ) => { setSelectionMode ( false ) ; clearSelection ( ) ; } , [ clearSelection ] ) ;
102-
10396 /** REQ-079:段搜索(片段上下文 + 点击跳详情) */
10497 const searchSegments = async ( ) => {
10598 const kw = searchKw . trim ( ) ;
@@ -195,23 +188,17 @@ export default function SessionListPanel({
195188 return filtered . map ( ( i ) => i . session . id ) ;
196189 } , [ grouped , groupedView , filtered , collapsed ] ) ;
197190
198- const visibleOrderRef = useRef < number [ ] > ( [ ] ) ;
199- useEffect ( ( ) => { visibleOrderRef . current = visibleOrder ; } , [ visibleOrder ] ) ;
200-
201- // 列表数据变化裁剪:只留当前可见行(筛选/折叠/删除后不残留幽灵勾选)
202- useEffect ( ( ) => {
203- setSelected ( ( cur ) => {
204- if ( cur . size === 0 ) return cur ;
205- const visible = new Set ( visibleOrder ) ;
206- let changed = false ;
207- const next = new Set < number > ( ) ;
208- for ( const id of cur ) if ( visible . has ( id ) ) next . add ( id ) ; else changed = true ;
209- return changed ? next : cur ;
210- } ) ;
211- } , [ visibleOrder ] ) ;
191+ // ── 多选/选择模式状态机(批 0-C2 拆至 hooks/useSessionSelection)──
192+ const {
193+ selected, selectionMode, clearSelection, exitBatch, enterSelectionMode,
194+ rowOpen, rowModifier, toggleAllVisible,
195+ } = useSessionSelection ( { visibleOrder, onOpenDetail } ) ;
212196
213197 // Esc 退出链:右键菜单 → 选择模式/多选态(清选集退出)。
214198 // (行内重命名输入内已 stopPropagation 自吞 Esc——不在此列)
199+ // ★ 全面板唯一 window keydown:优先级=右键菜单先 return(菜单开着时按 Esc 只关菜单,
200+ // 选集保留)。**不得**把 Esc 挪进 useSessionSelection 另建监听——两个监听会让一次
201+ // Esc 同时关菜单 + 清选集(行为不等价)。
215202 useEffect ( ( ) => {
216203 const onKey = ( e : KeyboardEvent ) => {
217204 if ( e . key !== "Escape" ) return ;
@@ -254,37 +241,6 @@ export default function SessionListPanel({
254241 }
255242 } ;
256243
257- // ── 行交互(批 4):单击语义按模式分派;修饰键不换右栏 ──
258- const rowOpen = useCallback ( ( item : SessionListItem ) => {
259- if ( selectionMode ) {
260- // 选择模式:单击=勾选(不打开详情)
261- setSelected ( ( cur ) => toggleSelection ( cur , item . session . id ) ) ;
262- setAnchor ( item . session . id ) ;
263- return ;
264- }
265- // 普通单击=单选语义并打开——先清既有选集(防误以为仍处多选态),锚恒指向本次点击
266- if ( selected . size > 0 ) clearSelection ( ) ;
267- onOpenDetail ( item . session . id ) ;
268- setAnchor ( item . session . id ) ;
269- } , [ selectionMode , selected . size , onOpenDetail , clearSelection ] ) ;
270-
271- const rowModifier = useCallback ( ( item : SessionListItem , ctrl : boolean , shift : boolean ) => {
272- const id = item . session . id ;
273- if ( ctrl ) {
274- // Ctrl/⌘:加/减单行;锚指向本次点击行
275- setSelected ( ( cur ) => toggleSelection ( cur , id ) ) ;
276- setAnchor ( id ) ;
277- } else if ( shift ) {
278- if ( anchor == null ) {
279- // 无锚的首次 Shift=单选该行并设为锚(连按两次不再各加单行)
280- setSelected ( new Set ( [ id ] ) ) ;
281- setAnchor ( id ) ;
282- } else {
283- setSelected ( ( cur ) => rangeSelection ( cur , visibleOrderRef . current , anchor , id ) ) ;
284- }
285- }
286- } , [ anchor ] ) ;
287-
288244 /** 行渲染(平铺/分组共用);行内编辑与多选视觉在 SessionListRow */
289245 const renderRow = ( item : SessionListItem ) => (
290246 < SessionListRow
@@ -349,7 +305,7 @@ export default function SessionListPanel({
349305 < button
350306 data-testid = "session-select-mode-btn"
351307 style = { selectModeBtn ( selectionMode ) }
352- onClick = { ( ) => ( selectionMode ? exitBatch ( ) : setSelectionMode ( true ) ) }
308+ onClick = { ( ) => ( selectionMode ? exitBatch ( ) : enterSelectionMode ( ) ) }
353309 title = { selectionMode ? "退出选择模式(Esc)" : "进入选择模式:单击会话=勾选(Ctrl/Shift 多选)" }
354310 >
355311 选择
@@ -533,14 +489,7 @@ export default function SessionListPanel({
533489 if ( el ) el . indeterminate = selected . size > 0 && selected . size < visibleOrder . length ;
534490 } }
535491 checked = { selected . size === visibleOrder . length && visibleOrder . length > 0 }
536- onChange = { ( ) => {
537- if ( selected . size === visibleOrder . length && visibleOrder . length > 0 ) {
538- clearSelection ( ) ;
539- } else {
540- setSelected ( new Set ( visibleOrder ) ) ;
541- setAnchor ( null ) ; // 全选后无区间锚(下次 Shift 需新锚)
542- }
543- } }
492+ onChange = { toggleAllVisible }
544493 style = { { cursor : "pointer" , flexShrink : 0 } }
545494 title = "全选当前可见的会话(折叠组行不含在内)"
546495 />
0 commit comments