1313 * @ai -context: 树序范围:全局可见序(树=未分组+各组顺次、折叠组行不参与)为
1414 * 区间/划选唯一基准;跨组语义=归组(目标手排时按落点插入)。
1515 */
16- import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
16+ import { useCallback , useEffect , useMemo , useState } from "react" ;
1717import type { Note , NoteGroup } from "../types" ;
1818import { paletteHex } from "../utils/colorPalette" ;
1919import type { ThemeMode } from "../utils/colorPalette" ;
20- import { emptySelection , rangeSelection , toggleSelection } from "../utils/noteSelection" ;
2120// 批 0-C2:展示节/可见序纯函数层(含 scope 键与裸折叠键派生——纯逻辑与副作用分离)
2221import { buildSections , buildVisibleOrder , isTreeMode , manualBaseIds , scopeKey } from "../utils/noteSectionModel" ;
2322// 批 0-C2:序行 store(笔记手排序行 + 组手排序行)——兑现既有登记拆分计划
2423import { useNoteOrders } from "../hooks/useNoteOrders" ;
2524// 批 0-C2:拖拽/移动三入口接线(共享一把行落点并发锁——硬性同文件约束)
2625import { useNoteMoves } from "../hooks/useNoteMoves" ;
26+ // 批 0-C2:多选三通道 + 菜单三态 + 唯一 Esc handler(可见序镜像同文件)
27+ import { useNoteListSelection } from "../hooks/useNoteListSelection" ;
2728import NoteListRow from "./NoteListRow" ;
2829import NoteTreeSection from "./NoteTreeSection" ;
2930import NoteRowContextMenu from "./NoteRowContextMenu" ;
@@ -88,53 +89,12 @@ export default function NoteListView({
8889 [ ] ,
8990 ) ;
9091
91- // ── 多选态(REQ-287):selectionMode=批量选择模式(单击=勾选);anchor=区间锚
92- const [ selection , setSelection ] = useState < Set < number > > ( emptySelection ( ) ) ;
93- const [ anchor , setAnchor ] = useState < number | null > ( null ) ;
94- const [ selectionMode , setSelectionMode ] = useState ( false ) ;
95- // 右键/批处理面板
96- const [ contextMenu , setContextMenu ] = useState < { note : Note ; x : number ; y : number } | null > ( null ) ;
97- const [ batchMenu , setBatchMenu ] = useState < { ids : number [ ] ; x : number ; y : number } | null > ( null ) ;
98- const [ batchMoveOpen , setBatchMoveOpen ] = useState ( false ) ;
99-
10092 // 组折叠态
10193 const [ groupFolds , setGroupFolds ] = useState < Record < string , boolean > > ( { } ) ;
10294
10395 // 序行 store(笔记手排序行 + 组手排序行)——装载/保存路径下沉 hooks/useNoteOrders
10496 const { manualOrders, groupOrderRows, saveOrder, resetOrder } = useNoteOrders ( refreshToken ) ;
10597
106- const visibleIdsRef = useRef < number [ ] > ( [ ] ) ;
107-
108- const clearSelection = useCallback ( ( ) => {
109- setSelection ( emptySelection ( ) ) ;
110- setAnchor ( null ) ;
111- } , [ ] ) ;
112- const exitBatch = useCallback ( ( ) => { setSelectionMode ( false ) ; clearSelection ( ) ; } , [ clearSelection ] ) ;
113-
114- // 列表数据变化裁剪(只留可见子集——既有安全边界)
115- useEffect ( ( ) => {
116- setSelection ( ( cur ) => {
117- if ( cur . size === 0 ) return cur ;
118- const visible = new Set ( notes . map ( ( n ) => n . id ) ) ;
119- let changed = false ;
120- const next = new Set < number > ( ) ;
121- for ( const id of cur ) if ( visible . has ( id ) ) next . add ( id ) ; else changed = true ;
122- return changed ? next : cur ;
123- } ) ;
124- } , [ notes ] ) ;
125-
126- // Esc:先退批处理面板 → 批量模式(清多选退出)
127- useEffect ( ( ) => {
128- const onKey = ( e : KeyboardEvent ) => {
129- if ( e . key !== "Escape" ) return ;
130- if ( batchMenu ) { setBatchMenu ( null ) ; setBatchMoveOpen ( false ) ; return ; }
131- if ( contextMenu ) { setContextMenu ( null ) ; return ; }
132- if ( selectionMode || selection . size > 0 ) exitBatch ( ) ;
133- } ;
134- window . addEventListener ( "keydown" , onKey ) ;
135- return ( ) => window . removeEventListener ( "keydown" , onKey ) ;
136- } , [ batchMenu , contextMenu , selectionMode , selection . size , exitBatch ] ) ;
137-
13898 // 手动序装载(REQ-287:notes 行)+ REQ-315:组序行(树组头排序)——见 hooks/useNoteOrders
13999 // (依赖 refreshToken 的理由随实现搬至该 hook 头注释:审查 P2-10 的令牌重拉)
140100
@@ -171,43 +131,19 @@ export default function NoteListView({
171131
172132 // 可见序(L1 审查:折叠组行不参与区间/划选——与渲染可见一致;折叠组头仍在)
173133 const visibleOrder = useMemo ( ( ) => buildVisibleOrder ( sections , groupFolds ) , [ sections , groupFolds ] ) ;
174- useEffect ( ( ) => { visibleIdsRef . current = visibleOrder ; } , [ visibleOrder ] ) ;
175134
176- // ── 行交互 ──
177- const handleOpen = useCallback ( ( note : Note ) => {
178- if ( selectionMode ) {
179- // 批量模式:单击=勾选(不换右栏)
180- setSelection ( ( cur ) => toggleSelection ( cur , note . id ) ) ;
181- setAnchor ( note . id ) ;
182- return ;
183- }
184- // 审查 L4:普通单击=单选并打开——先清既有选集,anchor 恒指向本次点击
185- if ( selection . size > 0 ) clearSelection ( ) ;
186- onSelect ( note ) ;
187- setAnchor ( note . id ) ;
188- } , [ selectionMode , onSelect , selection . size , clearSelection ] ) ;
189-
190- const handleModifierClick = useCallback ( ( note : Note , ctrl : boolean , shift : boolean ) => {
191- if ( ctrl ) {
192- setSelection ( ( cur ) => toggleSelection ( cur , note . id ) ) ;
193- // 审查 L4:Ctrl 后 anchor 指向本次点击行(即使该行被移除——Explorer 同款)
194- setAnchor ( note . id ) ;
195- } else if ( shift ) {
196- if ( anchor == null ) {
197- // 审查 L4:无锚的首次 Shift=单选该行并设为锚(连按两次不再各加单行)
198- setSelection ( new Set ( [ note . id ] ) ) ;
199- setAnchor ( note . id ) ;
200- } else {
201- setSelection ( ( cur ) => rangeSelection ( cur , visibleIdsRef . current , anchor , note . id ) ) ;
202- }
203- }
204- } , [ anchor ] ) ;
135+ // ── 行交互 / 多选 / 菜单(Esc 优先级链、可见序镜像、选集裁剪)——见 hooks/useNoteListSelection
136+ const {
137+ selection, setSelection, selectionMode, setSelectionMode,
138+ contextMenu, setContextMenu, batchMenu, setBatchMenu, batchMoveOpen, setBatchMoveOpen,
139+ clearSelection, exitBatch, handleOpen, handleModifierClick, openRowContextMenu,
140+ closeBatchMenu, batchDelete,
141+ } = useNoteListSelection ( { notes, visibleOrder, onSelect, onBatchDelete } ) ;
205142
206143 /** scope 手动底序(REQ-315):可见展示序去掉置顶区(快照只写本子序列)——纯函数在 noteSectionModel */
207144 const manualBaseOf = useCallback ( ( scope : string ) : number [ ] | null => manualBaseIds ( sections , scope ) , [ sections ] ) ;
208145
209146 // 拖拽/移动三入口(归组 / 组内上移下移 / 行间落点)——共享一把并发锁,见 hooks/useNoteMoves
210- const closeBatchMenu = useCallback ( ( ) => setBatchMenu ( null ) , [ ] ) ;
211147 const { busyMove, moveToGroup, moveWithinScope, handleDropOnRow } = useNoteMoves ( {
212148 notes, treeMode, manualOrders, manualBaseOf, saveOrder, onNoteMoved, onCleanNotice, closeBatchMenu,
213149 } ) ;
@@ -269,13 +205,7 @@ export default function NoteListView({
269205 dragIds = { selection . size > 0 && selection . has ( n . id ) ? [ ...selection ] : [ ] }
270206 onDropOnRow = { handleDropOnRow }
271207 onOpenSession = { onOpenSession }
272- onContextMenu = { ( e , note ) => {
273- if ( selection . size > 0 && selection . has ( note . id ) ) {
274- setBatchMenu ( { ids : [ ...selection ] , x : e . clientX , y : e . clientY } ) ;
275- } else {
276- setContextMenu ( { note, x : e . clientX , y : e . clientY } ) ;
277- }
278- } }
208+ onContextMenu = { openRowContextMenu }
279209 />
280210 ) ;
281211
@@ -299,13 +229,6 @@ export default function NoteListView({
299229 } ;
300230 } , [ contextMenu , treeMode , manualBaseOf , moveWithinScope ] ) ;
301231
302- // 批处理:删除(父层确认)与移动到组
303- const batchDelete = async ( ) => {
304- if ( ! batchMenu ) return ;
305- const ok = await onBatchDelete ( batchMenu . ids ) ;
306- if ( ok ) { setBatchMenu ( null ) ; setBatchMoveOpen ( false ) ; clearSelection ( ) ; }
307- } ;
308-
309232 return (
310233 < div style = { { width, flexShrink : 0 , borderRight : "1px solid #e5e7eb" , display : "flex" , flexDirection : "column" , minWidth : 0 } } >
311234 < div style = { { padding : "10px 14px" , borderBottom : "1px solid #e5e7eb" , fontWeight : 600 , display : "flex" , alignItems : "center" , gap : 8 } } >
0 commit comments