@@ -107,6 +107,50 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
107107 const [ sopStart , setSopStart ] = useState ( "0" ) ;
108108 const [ sopEnd , setSopEnd ] = useState ( "0" ) ;
109109 const [ suggestions , setSuggestions ] = useState < Record < number , string [ ] > > ( { } ) ;
110+ // v0.20.3(REQ-294):批量周回顾(选中执行/放弃——batch_weekly_resolve 单事务)
111+ const [ batchMode , setBatchMode ] = useState ( false ) ;
112+ const [ checked , setChecked ] = useState < Set < number > > ( new Set ( ) ) ;
113+ const [ batchReason , setBatchReason ] = useState ( "" ) ;
114+
115+ const toggleChecked = ( id : number ) => {
116+ setChecked ( ( prev ) => {
117+ const next = new Set ( prev ) ;
118+ if ( next . has ( id ) ) next . delete ( id ) ; else next . add ( id ) ;
119+ return next ;
120+ } ) ;
121+ } ;
122+
123+ const runBatch = async ( action : "done" | "abandon" ) => {
124+ const candidates = [ ...overdue , ...planned , ...rows ] ;
125+ const decisions = candidates
126+ . filter ( ( r ) => checked . has ( r . id ) && ! r . unrefined )
127+ . map ( ( r ) => ( { rowId : r . id , action, reason : action === "abandon" ? batchReason . trim ( ) || null : null } ) ) ;
128+ if ( decisions . length === 0 ) {
129+ setErr ( "请先勾选要批决议的任务行" ) ;
130+ return ;
131+ }
132+ try {
133+ const view = await invoke < { done : number ; abandoned : number ; failed : string [ ] } > (
134+ "batch_weekly_resolve" ,
135+ { decisions } ,
136+ ) ;
137+ setMsg ( `⚖ 周回顾批提交完成:执行 ${ view . done } · 放弃 ${ view . abandoned } ${ view . failed . length > 0 ? ` · 失败 ${ view . failed . length } 条(${ view . failed [ 0 ] } )` : "" } ` ) ;
138+ setChecked ( new Set ( ) ) ;
139+ setBatchReason ( "" ) ;
140+ setBatchMode ( false ) ;
141+ onChanged ?.( ) ;
142+ const [ o , p , s , u , h ] = await Promise . all ( [
143+ fetchTab ( "overdue" ) ,
144+ fetchTab ( "planned" ) ,
145+ fetchTab ( "someday" ) ,
146+ fetchTab ( "unrefined" ) ,
147+ invoke < HistoryRow [ ] > ( "completion_history_list" , { eventType : null , limit : 150 } ) ,
148+ ] ) ;
149+ setOverdue ( o ) ; setPlanned ( p ) ; setRows ( s ) ; setUnrefined ( u ) ; setHistory ( h ) ;
150+ } catch ( e ) {
151+ setErr ( String ( e ) ) ;
152+ }
153+ } ;
110154 // 裁决队列(四分区同拉全量行,前端按 tab 分区展示——逾期标红/计划/搁置/提炼)
111155 const [ rows , setRows ] = useState < ActionRow [ ] > ( [ ] ) ;
112156 const [ history , setHistory ] = useState < HistoryRow [ ] > ( [ ] ) ;
@@ -366,6 +410,39 @@ export default function ActionCenterOverlay({ onClose, onChanged }: Props) {
366410
367411 { tab === "queue" ? (
368412 < >
413+ { /* v0.20.3(REQ-294):周回顾批裁决(不留死尸——裁决机制批量面) */ }
414+ < div style = { { display : "flex" , gap : 6 , alignItems : "center" , marginBottom : 8 , flexWrap : "wrap" } } >
415+ < button style = { ghostBtn } onClick = { ( ) => setBatchMode ( ( m ) => ! m ) } data-testid = "weekly-batch-toggle" >
416+ { batchMode ? "退出批量(⚖ 周回顾)" : "⚖ 批量裁决(周回顾)" }
417+ </ button >
418+ { batchMode && (
419+ < >
420+ < input value = { batchReason } onChange = { ( e ) => setBatchReason ( e . target . value ) } placeholder = "放弃原因(批量放弃共用,可空)" style = { { fontSize : 12 , width : 200 , border : "1px solid #e5e7eb" , borderRadius : 4 , padding : "2px 6px" } } />
421+ < button style = { okBtn } disabled = { checked . size === 0 } onClick = { ( ) => void runBatch ( "done" ) } >
422+ ⚖ 执行选中({ checked . size } )
423+ </ button >
424+ < button style = { { ...ghostBtn , color : "#dc2626" } } disabled = { checked . size === 0 } onClick = { ( ) => void runBatch ( "abandon" ) } >
425+ ✗ 放弃选中(留因)
426+ </ button >
427+ </ >
428+ ) }
429+ </ div >
430+ { batchMode && (
431+ < div style = { { border : "1px dashed #c4b5fd" , borderRadius : 8 , padding : 6 , marginBottom : 8 , display : "flex" , flexDirection : "column" , gap : 4 } } >
432+ { [ ...overdue , ...planned , ...rows ] . filter ( ( r ) => ! r . unrefined ) . length === 0 && (
433+ < p style = { { margin : 0 , fontSize : 12 , color : "#9ca3af" } } > 当前无可批决议的 todo 行</ p >
434+ ) }
435+ { [ ...overdue , ...planned , ...rows ]
436+ . filter ( ( r ) => ! r . unrefined )
437+ . map ( ( r ) => (
438+ < label key = { r . id } style = { { display : "flex" , gap : 6 , alignItems : "center" , fontSize : 12 , cursor : "pointer" , padding : "2px 4px" , borderRadius : 4 , background : checked . has ( r . id ) ? "#f5f3ff" : "transparent" } } >
439+ < input type = "checkbox" checked = { checked . has ( r . id ) } onChange = { ( ) => toggleChecked ( r . id ) } />
440+ < span style = { { color : "#111827" } } > { r . text } </ span >
441+ < span style = { { fontSize : 10.5 , color : "#9ca3af" , marginLeft : "auto" } } > @{ r . note_title } </ span >
442+ </ label >
443+ ) ) }
444+ </ div >
445+ ) }
369446 < h4 style = { { fontSize : 12.5 , margin : "6px 0 4px" , color : "#dc2626" } } > 逾期({ overdue . length } )</ h4 >
370447 < div style = { { display : "flex" , flexDirection : "column" , gap : 6 } } >
371448 { overdue . length === 0 ? empty ( "无逾期——裁决是机制不是自动清理" ) : overdue . map ( ( r ) => renderRow ( r , true ) ) }
0 commit comments