1616import { useCallback , useEffect , useMemo , useRef , useState } from "react" ;
1717import { invoke } from "@tauri-apps/api/core" ;
1818import type { Note , NoteGroup } from "../types" ;
19+ // REQ-316(批 7):移组返回契约(空组自动清理留痕数据源)
20+ import type { MoveNoteResult } from "../types/notes" ;
1921import { paletteHex } from "../utils/colorPalette" ;
2022import type { ThemeMode } from "../utils/colorPalette" ;
2123import { emptySelection , rangeSelection , toggleSelection } from "../utils/noteSelection" ;
@@ -61,6 +63,8 @@ interface Props {
6163 onNoteEdit ?: ( note : Note ) => void ;
6264 onNoteDelete ?: ( note : Note ) => void ;
6365 onNoteMoved ?: ( ) => void ;
66+ /** REQ-316(批 7):移组触发空组自动清理 → 上抛组标题(父层 toast 留痕) */
67+ onCleanNotice ?: ( groupNames : string [ ] ) => void ;
6468 onCollapse ?: ( ) => void ;
6569}
6670
@@ -85,7 +89,7 @@ export default function NoteListView({
8589 keyword, tagFilter, sortMode, allTags, selectedId, status,
8690 noteColors, tagColors,
8791 onKeywordChange, onTagFilterChange, onSortModeChange, onSelect, onCreate, onRefresh, onOpenSession, onBatchDelete,
88- onNotePinToggle, onNoteEdit, onNoteDelete, onNoteMoved, onCollapse,
92+ onNotePinToggle, onNoteEdit, onNoteDelete, onNoteMoved, onCleanNotice , onCollapse,
8993} : Props ) {
9094 const theme : ThemeMode = useMemo (
9195 ( ) => ( typeof window !== "undefined" && window . matchMedia ?.( "(prefers-color-scheme: dark)" ) . matches ? "dark" : "light" ) ,
@@ -305,9 +309,11 @@ export default function NoteListView({
305309 const groupNotes = new Set (
306310 notes . filter ( ( n ) => ( groupId == null ? n . group_id == null : n . group_id === groupId ) ) . map ( ( n ) => n . id ) ,
307311 ) ;
312+ const cleanedNames : string [ ] = [ ] ;
308313 for ( const id of ids ) {
309314 if ( groupNotes . has ( id ) ) continue ;
310- await invoke ( "move_note_to_group" , { noteId : id , groupId } ) ;
315+ const r = await invoke < MoveNoteResult > ( "move_note_to_group" , { noteId : id , groupId } ) ;
316+ cleanedNames . push ( ...r . autoCleanedGroups ) ;
311317 }
312318 // 目标手排:新入组未置顶笔记追加末尾(跨组 drop 的"加入该组"语义;
313319 // 置顶成员由置顶区表达,不写手排行;重写快照顺带清存量置顶残行)
@@ -318,13 +324,15 @@ export default function NoteListView({
318324 await saveOrder ( scope , [ ...cur , ...ids . filter ( ( id ) => ! groupNotes . has ( id ) && ! pinned . has ( id ) ) ] ) ;
319325 }
320326 onNoteMoved ?.( ) ;
327+ // REQ-316(批 7):批量移走后源空组清理留痕(跨条聚合去重)
328+ if ( cleanedNames . length > 0 ) onCleanNotice ?.( [ ...new Set ( cleanedNames ) ] ) ;
321329 setBatchMenu ( null ) ;
322330 } catch ( e ) {
323331 console . warn ( "[notes] 归组失败:" , e ) ;
324332 } finally {
325333 setBusyMove ( false ) ;
326334 }
327- } , [ notes , manualOrders , saveOrder , onNoteMoved ] ) ;
335+ } , [ notes , manualOrders , saveOrder , onNoteMoved , onCleanNotice ] ) ;
328336
329337 /**
330338 * REQ-315:右键「上移/下移」组内显式移动——补"必须拖一次才触发手排快照"的
@@ -367,9 +375,13 @@ export default function NoteListView({
367375 const n = notes . find ( ( x ) => x . id === id ) ;
368376 return n && ( n . group_id ?? null ) !== targetGroup ;
369377 } ) ;
378+ const cleanedNames : string [ ] = [ ] ;
370379 for ( const id of external ) {
371- await invoke ( "move_note_to_group" , { noteId : id , groupId : target . group_id } ) ;
380+ const r = await invoke < MoveNoteResult > ( "move_note_to_group" , { noteId : id , groupId : target . group_id } ) ;
381+ cleanedNames . push ( ...r . autoCleanedGroups ) ;
372382 }
383+ // REQ-316(批 7):跨组拖走使源组变空 → 清理留痕(零清理零变化)
384+ if ( cleanedNames . length > 0 ) onCleanNotice ?.( [ ...new Set ( cleanedNames ) ] ) ;
373385 // 落位(REQ-315:只写置顶区外子序列;落点在置顶行上 = 置顶区下沿即手动区首位)
374386 const pinned = new Set ( notes . filter ( ( n ) => n . pin === 1 ) . map ( ( n ) => n . id ) ) ;
375387 const moved = ids . filter ( ( id ) => ! pinned . has ( id ) ) ;
@@ -402,7 +414,7 @@ export default function NoteListView({
402414 } finally {
403415 dropBusyRef . current = false ;
404416 }
405- } , [ treeMode , notes , manualOrders , manualBaseOf , saveOrder , onNoteMoved ] ) ;
417+ } , [ treeMode , notes , manualOrders , manualBaseOf , saveOrder , onNoteMoved , onCleanNotice ] ) ;
406418
407419 /** 划选(组头空白起 → 组内首行至当前行带;走既有行命中的全局可见序)。
408420 * L9 审查修正:rAF 节流 + pointercancel/blur/松开(buttons=0)即清理——
@@ -597,6 +609,7 @@ export default function NoteListView({
597609 onEdit = { onNoteEdit }
598610 onDelete = { onNoteDelete }
599611 onMoved = { onNoteMoved }
612+ onCleanNotice = { onCleanNotice }
600613 // REQ-315:树视图才显 上移/下移(平铺态禁移动——orderActions 为 null)
601614 onMoveWithinScope = { orderActions ?. move }
602615 canMoveUp = { orderActions ?. canMoveUp ?? false }
0 commit comments