@@ -22,9 +22,9 @@ import { paletteHex } from "../utils/colorPalette";
2222import type { ThemeMode } from "../utils/colorPalette" ;
2323import { emptySelection , rangeSelection , toggleSelection } from "../utils/noteSelection" ;
2424// REQ-315:scope 内 置顶→手排→自动 排序纯函数 + 显式移动(树视图 pin 生效)
25- import { dropNotesIntoOrder , orderScopeNotes , shiftNoteOrder } from "../utils/noteOrder" ;
26- // REQ-315:组展示序纯函数(树组头排序与组侧栏同规则 )
27- import { orderGroups } from "../utils/groupOrder " ;
25+ import { dropNotesIntoOrder , shiftNoteOrder } from "../utils/noteOrder" ;
26+ // 批 0-C2:展示节/可见序纯函数层(含 scope 键与裸折叠键派生——纯逻辑与副作用分离 )
27+ import { buildSections , buildVisibleOrder , isTreeMode , manualBaseIds , scopeKey } from "../utils/noteSectionModel " ;
2828import NoteListRow from "./NoteListRow" ;
2929import NoteTreeSection from "./NoteTreeSection" ;
3030import NoteRowContextMenu from "./NoteRowContextMenu" ;
@@ -71,22 +71,11 @@ interface Props {
7171 refreshToken ?: number ;
7272}
7373
74- /** scope 键(组/null=未分组) */
75- const scopeKey = ( groupId : number | null ) : string => ( groupId == null ? "none" : `g:${ groupId } ` ) ;
76-
7774/** 读组折叠记忆(localStorage 损坏/无 → 默认展开) */
7875function readGroupFold ( key : string ) : boolean {
7976 try { return window . localStorage . getItem ( `notes:group-fold:${ key } ` ) === "1" ; } catch { return false ; }
8077}
8178
82- interface SectionData {
83- scope : string ;
84- groupId : number | null ;
85- title : string ;
86- accent : string ;
87- items : Note [ ] ;
88- }
89-
9079export default function NoteListView ( {
9180 width = 320 , notes, groups = [ ] , groupFilter = null , onGroupFilterChange,
9281 keyword, tagFilter, sortMode, allTags, selectedId, status,
@@ -184,7 +173,7 @@ export default function NoteListView({
184173 // ── 分组树数据(同 v0.15 结构)——可见序统一从本结构生成 ──
185174 // Why 不再有分桶 memo:旧 `grouped` 是死载荷——仅被当作 treeMode 的第二真值,
186175 // 其 ungrouped/byGroup 从未被读取(真分桶一直在 sections 内重算);批 0-C2 去重
187- const treeMode = keyword . trim ( ) === "" && tagFilter === null && sortMode === "updated-desc" ;
176+ const treeMode = isTreeMode ( keyword , tagFilter , sortMode ) ;
188177
189178 // 折叠初始值(沿用 v0.15)
190179 useEffect ( ( ) => {
@@ -205,56 +194,15 @@ export default function NoteListView({
205194 } catch { /* 隐私模式 */ }
206195 } , [ groupFolds ] ) ;
207196
208- /**
209- * scope 展示序(REQ-315):置顶区(updated_at 降序)→ 手排 seq → 自动区;
210- * 平铺(scope=flat)保持后端排序原样(排序模式语义在后端 list_notes——
211- * 平铺是过滤结果非完整 scope,不做 scope 级重排)。
212- */
213- const orderSectionItems = useCallback (
214- ( items : Note [ ] , scope : string ) : Note [ ] =>
215- scope === "flat" ? items : orderScopeNotes ( items , manualOrders [ scope ] ) ,
216- [ manualOrders ] ,
197+ // 显示节(树/平铺)——节内展示序/分桶/accent 全在 utils/noteSectionModel.buildSections
198+ // deps 保留 groupFolds:函数体不读它,但移除会改变重算次数(本批只搬家不优化)
199+ const sections = useMemo (
200+ ( ) => buildSections ( { notes, groups, groupFilter, theme, treeMode, manualOrders, groupOrderRows } ) ,
201+ [ treeMode , groups , notes , groupFolds , manualOrders , theme , groupFilter , groupOrderRows ] ,
217202 ) ;
218203
219- // 显示节(树/平铺)→ sections(可见序)
220- const sections : SectionData [ ] = useMemo ( ( ) => {
221- const mk = ( scope : string , groupId : number | null , title : string , accent : string , items : Note [ ] ) : SectionData =>
222- ( { scope, groupId, title, accent, items : orderSectionItems ( items , scope ) } ) ;
223- const out : SectionData [ ] = [ ] ;
224- if ( treeMode ) {
225- const ungrouped : Note [ ] = [ ] ;
226- const byGroup = new Map < number , Note [ ] > ( ) ;
227- for ( const n of notes ) {
228- if ( n . group_id == null ) ungrouped . push ( n ) ;
229- else { const a = byGroup . get ( n . group_id ) ?? [ ] ; a . push ( n ) ; byGroup . set ( n . group_id , a ) ; }
230- }
231- // 折叠只影响 body(folded prop)——组头必须常驻(chevron 再点可展开)
232- if ( ungrouped . length > 0 || groupFilter === null ) {
233- out . push ( mk ( "none" , null , "未分组" , paletteHex ( null , theme ) , ungrouped ) ) ;
234- }
235- // REQ-315:组头序与组侧栏同规则(置顶→手排→自动)——组置顶在树面可见生效
236- const orderedGroups = orderGroups ( groups , groupOrderRows ) ;
237- for ( const g of orderedGroups ) {
238- const items = byGroup . get ( g . id ) ?? [ ] ;
239- if ( items . length === 0 ) continue ;
240- out . push ( mk ( scopeKey ( g . id ) , g . id , g . name , paletteHex ( g . color ?? null , theme ) , items ) ) ;
241- }
242- } else {
243- out . push ( mk ( "flat" , null , "" , paletteHex ( null , theme ) , notes ) ) ;
244- }
245- return out ;
246- } , [ treeMode , groups , notes , groupFolds , orderSectionItems , theme , groupFilter , groupOrderRows ] ) ;
247-
248204 // 可见序(L1 审查:折叠组行不参与区间/划选——与渲染可见一致;折叠组头仍在)
249- const visibleOrder = useMemo ( ( ) => {
250- const out : number [ ] = [ ] ;
251- for ( const sec of sections ) {
252- const key = sec . groupId == null ? "none" : String ( sec . groupId ) ;
253- if ( groupFolds [ key ] === true ) continue ; // 折叠=行不可见,排除出选择语义
254- for ( const n of sec . items ) out . push ( n . id ) ;
255- }
256- return out ;
257- } , [ sections , groupFolds ] ) ;
205+ const visibleOrder = useMemo ( ( ) => buildVisibleOrder ( sections , groupFolds ) , [ sections , groupFolds ] ) ;
258206 useEffect ( ( ) => { visibleIdsRef . current = visibleOrder ; } , [ visibleOrder ] ) ;
259207
260208 // ── 行交互 ──
@@ -287,16 +235,8 @@ export default function NoteListView({
287235 }
288236 } , [ anchor ] ) ;
289237
290- /**
291- * scope 手动底序(REQ-315):可见展示序去掉置顶区——置顶笔记由 pin 列置顶区
292- * 表达(按更新时间定序),不占手动位;快照只写本子序列。置顶区外的笔记
293- * (含新笔记/未置顶)都在底序内——显式移动即整序快照(自动组首移转手排)。
294- */
295- const manualBaseOf = useCallback ( ( scope : string ) : number [ ] | null => {
296- const section = sections . find ( ( s ) => s . scope === scope ) ;
297- if ( ! section ) return null ;
298- return section . items . filter ( ( n ) => n . pin !== 1 ) . map ( ( n ) => n . id ) ;
299- } , [ sections ] ) ;
238+ /** scope 手动底序(REQ-315):可见展示序去掉置顶区(快照只写本子序列)——纯函数在 noteSectionModel */
239+ const manualBaseOf = useCallback ( ( scope : string ) : number [ ] | null => manualBaseIds ( sections , scope ) , [ sections ] ) ;
300240
301241 /** 拖拽归组(组头/左侧组行复用单 id 兜底仍可用) */
302242 const moveToGroup = useCallback ( async ( ids : number [ ] , groupId : number | null ) => {
0 commit comments