@@ -27,6 +27,15 @@ function kindBadge(kind: string): { label: string; color: string } {
2727 return { label : "📄 独立" , color : "#6b7280" } ;
2828}
2929
30+ /** 结算计划(v0.11.3;仪式第一步——呈现沼泽全貌) */
31+ interface SettlementPlan {
32+ itemCount : number ;
33+ due : boolean ;
34+ lastSettledAt : number | null ;
35+ mergePairs : { keepId : number ; dropId : number ; keepText : string ; dropText : string } [ ] ;
36+ archiveCandidates : { id : number ; text : string } [ ] ;
37+ }
38+
3039/** 解析路由理由 JSON(损坏防御性回退空对象)。 */
3140export function parseRouteReason ( raw : string | null ) : GroupRouteReason {
3241 if ( ! raw ) return { } ;
@@ -62,6 +71,8 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
6271 // 改判表单态(展开区消费)
6372 const [ overrideKind , setOverrideKind ] = useState ( "standalone" ) ;
6473 const [ overrideDomain , setOverrideDomain ] = useState ( "" ) ;
74+ // v0.11.3:结算计划态(展开区仪式)
75+ const [ settlePlan , setSettlePlan ] = useState < SettlementPlan | null > ( null ) ;
6576
6677 const load = useCallback ( async ( ) => {
6778 try {
@@ -155,6 +166,32 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
155166 }
156167 } ;
157168
169+ // ── v0.11.3:组结算仪式(先呈现计划,用户确认后执行)──
170+ const runSettlementPlan = async ( g : NoteGroup ) => {
171+ try {
172+ const plan = await invoke < SettlementPlan > ( "settlement_plan" , { groupId : g . id } ) ;
173+ setSettlePlan ( plan ) ;
174+ setStatus ( "" ) ;
175+ } catch ( e ) {
176+ setStatus ( `结算计划失败: ${ e } ` ) ;
177+ }
178+ } ;
179+
180+ const runExecuteSettlement = async ( g : NoteGroup ) => {
181+ try {
182+ const r = await invoke < { merged : number ; archived : number ; coreNoteId : number | null } > (
183+ "execute_settlement" ,
184+ { groupId : g . id , applyMerges : true , applyArchives : true } ,
185+ ) ;
186+ setSettlePlan ( null ) ;
187+ setStatus ( `结算完成:合并 ${ r . merged } 条重复、归档 ${ r . archived } 条低价值,已生成核心提炼笔记` ) ;
188+ await load ( ) ;
189+ onChanged ( ) ;
190+ } catch ( e ) {
191+ setStatus ( `结算执行失败: ${ e } ` ) ;
192+ }
193+ } ;
194+
158195 return (
159196 < div style = { { width : 230 , flexShrink : 0 , borderRight : "1px solid #e5e7eb" , display : "flex" , flexDirection : "column" } } >
160197 < div style = { { padding : "10px 12px" , borderBottom : "1px solid #e5e7eb" , fontWeight : 600 , display : "flex" , alignItems : "center" , gap : 8 } } >
@@ -231,7 +268,7 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
231268 </ span >
232269 < span style = { { fontSize : 11 , color : "#9ca3af" } } > { g . noteCount } </ span >
233270 < span
234- onClick = { ( e ) => { e . stopPropagation ( ) ; setExpandedId ( expanded ? null : g . id ) ; setOverrideKind ( g . kind ) ; setOverrideDomain ( g . domainTag ?? "" ) ; } }
271+ onClick = { ( e ) => { e . stopPropagation ( ) ; setExpandedId ( expanded ? null : g . id ) ; setOverrideKind ( g . kind ) ; setOverrideDomain ( g . domainTag ?? "" ) ; setSettlePlan ( null ) ; } }
235272 style = { { fontSize : 11 , color : "#6b7280" , cursor : "pointer" , padding : "0 3px" } }
236273 title = "路由详情/改判"
237274 >
@@ -292,6 +329,12 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
292329 style = { { fontSize : 11 , cursor : "pointer" , padding : "2px 8px" , borderRadius : 4 , border : "1px solid #d1d5db" , background : "#fff" } } >
293330 🎴 复习本组
294331 </ button >
332+ { /* v0.11.3:组结算入口(防沼泽仪式) */ }
333+ < button onClick = { ( ) => void runSettlementPlan ( g ) }
334+ style = { { fontSize : 11 , cursor : "pointer" , padding : "2px 8px" , borderRadius : 4 , border : "1px solid #d1d5db" , background : "#fff" } }
335+ title = "对账本组:提炼核心/合并重复/归档低价值" >
336+ 🧹 结算
337+ </ button >
295338 { selectedNoteId != null && (
296339 < >
297340 < button onClick = { ( ) => void runMove ( g . id ) }
@@ -305,6 +348,32 @@ export default function NoteGroupPanel({ groupFilter, onGroupFilterChange, selec
305348 </ >
306349 ) }
307350 </ div >
351+ { /* v0.11.3:结算计划呈现(仪式第一步:看见沼泽全貌再动手) */ }
352+ { settlePlan && (
353+ < div style = { { marginTop : 8 , padding : 8 , background : "#fff" , borderRadius : 6 , border : "1px solid #e5e7eb" } } >
354+ < div style = { { fontWeight : 600 , color : "#374151" , marginBottom : 4 } } >
355+ 结算计划 · 共 { settlePlan . itemCount } 条目
356+ { settlePlan . due && < span style = { { color : "#b45309" , marginLeft : 6 } } > ⚠ 建议结算</ span > }
357+ </ div >
358+ < div style = { { color : "#6b7280" } } > 重复合并:{ settlePlan . mergePairs . length } 对(保留长文本,归档短重复)</ div >
359+ { settlePlan . mergePairs . slice ( 0 , 3 ) . map ( ( p ) => (
360+ < div key = { p . dropId } style = { { color : "#9ca3af" , paddingLeft : 6 , overflow : "hidden" , textOverflow : "ellipsis" , whiteSpace : "nowrap" } } >
361+ 「{ p . keepText . slice ( 0 , 18 ) } …」⇐「{ p . dropText . slice ( 0 , 18 ) } …」
362+ </ div >
363+ ) ) }
364+ < div style = { { color : "#6b7280" , marginTop : 2 } } > 低价值归档:{ settlePlan . archiveCandidates . length } 条(老化且无卡绑定,可恢复)</ div >
365+ < div style = { { display : "flex" , gap : 4 , marginTop : 6 } } >
366+ < button onClick = { ( ) => void runExecuteSettlement ( g ) }
367+ style = { { fontSize : 11 , cursor : "pointer" , padding : "2px 10px" , borderRadius : 4 , border : "1px solid #0f766e" , background : "#f0fdfa" , color : "#0f766e" } } >
368+ ✓ 执行结算(含核心提炼)
369+ </ button >
370+ < button onClick = { ( ) => setSettlePlan ( null ) }
371+ style = { { fontSize : 11 , cursor : "pointer" , padding : "2px 8px" , borderRadius : 4 , border : "1px solid #d1d5db" , background : "#fff" } } >
372+ 取消
373+ </ button >
374+ </ div >
375+ </ div >
376+ ) }
308377 </ div >
309378 ) }
310379 </ div >
0 commit comments