@@ -26,6 +26,11 @@ export default function GoalDetail({ goalId, onChanged, onDeleted }: Props) {
2626 const [ bindGroupId , setBindGroupId ] = useState ( 0 ) ;
2727 const [ groups , setGroups ] = useState < { id : number ; name : string } [ ] > ( [ ] ) ;
2828 const [ editing , setEditing ] = useState ( false ) ;
29+ // 改名(update_goal 接线)与里程碑标题行内编辑(update_goal_milestone 接线)
30+ const [ renameMode , setRenameMode ] = useState ( false ) ;
31+ const [ renameValue , setRenameValue ] = useState ( "" ) ;
32+ const [ editingMileId , setEditingMileId ] = useState < number | null > ( null ) ;
33+ const [ editingMileValue , setEditingMileValue ] = useState ( "" ) ;
2934
3035 const refresh = useCallback ( async ( ) => {
3136 try {
@@ -57,6 +62,20 @@ export default function GoalDetail({ goalId, onChanged, onDeleted }: Props) {
5762 await invoke ( "set_goal_milestone_status" , { id : m . id , status : m . status === "done" ? "pending" : "done" } ) ;
5863 onChanged ( ) ; void refresh ( ) ;
5964 } ;
65+ const saveRename = async ( ) => {
66+ if ( ! renameValue . trim ( ) ) { setRenameMode ( false ) ; return ; }
67+ // horizon 不传 → 后端保持原锚点(update_goal_inner None=不变语义)
68+ await invoke ( "update_goal" , { id : goalId , name : renameValue . trim ( ) , domainTag : detail ! . goal . domainTag , horizon : null } ) ;
69+ setRenameMode ( false ) ;
70+ onChanged ( ) ; await refresh ( ) ;
71+ } ;
72+ const saveMileTitle = async ( id : number ) => {
73+ const title = editingMileValue . trim ( ) ;
74+ if ( ! title ) { setEditingMileId ( null ) ; return ; }
75+ await invoke ( "update_goal_milestone" , { id, title, dueAt : null } ) ;
76+ setEditingMileId ( null ) ;
77+ onChanged ( ) ; await refresh ( ) ;
78+ } ;
6079 const addMilestone = async ( ) => {
6180 if ( ! newMile . trim ( ) ) return ;
6281 await invoke ( "add_goal_milestone" , { goalId, title : newMile . trim ( ) , dueAt : null , criteriaType : null , refGroupId : null } ) ;
@@ -82,10 +101,36 @@ export default function GoalDetail({ goalId, onChanged, onDeleted }: Props) {
82101 return (
83102 < div data-testid = "goal-detail" style = { { padding : 16 , overflow : "auto" , height : "100%" , boxSizing : "border-box" } } >
84103 < div style = { { display : "flex" , alignItems : "center" , gap : 8 } } >
85- < span style = { { fontWeight : 700 , fontSize : 15 } } > 🎯 { goal . name } </ span >
104+ { renameMode ? (
105+ < input
106+ data-testid = "rename-input"
107+ value = { renameValue }
108+ onChange = { ( e ) => setRenameValue ( e . target . value ) }
109+ autoFocus
110+ style = { { fontSize : 14 , padding : "3px 8px" , border : "1px solid #0f766e" , borderRadius : 6 , flex : 1 } }
111+ onKeyDown = { ( e ) => { if ( e . key === "Enter" ) void saveRename ( ) ; } }
112+ />
113+ ) : (
114+ < span style = { { fontWeight : 700 , fontSize : 15 } } > 🎯 { goal . name } </ span >
115+ ) }
86116 < span style = { { fontSize : 11 , color : "#6b7280" } } > { GOAL_STATUS_LABELS [ goal . status ] } </ span >
87117 { ready && < span style = { { fontSize : 11 , color : "#b45309" , background : "#fffbeb" , borderRadius : 8 , padding : "1px 8px" } } > 🎓 可毕业(毕业仪式 M2)</ span > }
88- < span style = { { marginLeft : "auto" , fontSize : 11 , color : "#9ca3af" } } > 始于 { new Date ( goal . createdAt * 1000 ) . toISOString ( ) . slice ( 0 , 10 ) } </ span >
118+ { renameMode ? (
119+ < >
120+ < button data-testid = "rename-save" onClick = { ( ) => void saveRename ( ) } style = { miniPrimary } > ✓</ button >
121+ < button onClick = { ( ) => setRenameMode ( false ) } style = { miniDanger } > 取消</ button >
122+ </ >
123+ ) : (
124+ < button
125+ data-testid = "rename-start"
126+ onClick = { ( ) => { setRenameValue ( goal . name ) ; setRenameMode ( true ) ; } }
127+ style = { { marginLeft : "auto" , ...miniPrimary } }
128+ title = "改名(不改判据/绑组)"
129+ >
130+ ✎ 改名
131+ </ button >
132+ ) }
133+ < span style = { { fontSize : 11 , color : "#9ca3af" } } > 始于 { new Date ( goal . createdAt * 1000 ) . toISOString ( ) . slice ( 0 , 10 ) } </ span >
89134 </ div >
90135 < p style = { { fontSize : 12 , color : "#4b5563" , background : "#fafaf9" , padding : 8 , borderRadius : 6 , margin : "10px 0" } } > { detail . declaration } </ p >
91136
@@ -111,9 +156,26 @@ export default function GoalDetail({ goalId, onChanged, onDeleted }: Props) {
111156 { milestones . map ( ( m ) => (
112157 < div key = { m . id } style = { { display : "flex" , alignItems : "center" , gap : 6 , padding : "4px 0" } } >
113158 < input type = "checkbox" checked = { m . status === "done" } onChange = { ( ) => void toggleMilestone ( m ) } />
114- < span style = { { fontSize : 12 , color : m . status === "done" ? "#9ca3af" : "#1f2937" , textDecoration : m . status === "done" ? "line-through" : "none" } } > { m . title } </ span >
115- { m . criteriaType === "group_settled" && < span style = { { fontSize : 10 , color : "#0f766e" , background : "#f0fdfa" , borderRadius : 8 , padding : "0 6px" } } > 随组结算</ span > }
116- { m . status === "skipped" && < span style = { { fontSize : 10 , color : "#9ca3af" } } > 已跳过</ span > }
159+ { editingMileId === m . id ? (
160+ < >
161+ < input
162+ data-testid = "mile-title-input"
163+ value = { editingMileValue }
164+ onChange = { ( e ) => setEditingMileValue ( e . target . value ) }
165+ autoFocus
166+ style = { { flex : 1 , fontSize : 12 , padding : "2px 6px" , border : "1px solid #0f766e" , borderRadius : 4 } }
167+ onKeyDown = { ( e ) => { if ( e . key === "Enter" ) void saveMileTitle ( m . id ) ; } }
168+ />
169+ < button onClick = { ( ) => void saveMileTitle ( m . id ) } style = { miniPrimary } > ✓</ button >
170+ </ >
171+ ) : (
172+ < >
173+ < span style = { { fontSize : 12 , color : m . status === "done" ? "#9ca3af" : "#1f2937" , textDecoration : m . status === "done" ? "line-through" : "none" } } > { m . title } </ span >
174+ { m . criteriaType === "group_settled" && < span style = { { fontSize : 10 , color : "#0f766e" , background : "#f0fdfa" , borderRadius : 8 , padding : "0 6px" } } > 随组结算</ span > }
175+ { m . status === "skipped" && < span style = { { fontSize : 10 , color : "#9ca3af" } } > 已跳过</ span > }
176+ < button onClick = { ( ) => { setEditingMileId ( m . id ) ; setEditingMileValue ( m . title ) ; } } style = { smallGhost } > 改</ button >
177+ </ >
178+ ) }
117179 < button onClick = { ( ) => void removeMilestone ( m . id ) } style = { smallDanger } > 删</ button >
118180 </ div >
119181 ) ) }
@@ -190,3 +252,4 @@ const miniInput: React.CSSProperties = { fontSize: 11, padding: "4px 8px", borde
190252const miniPrimary : React . CSSProperties = { fontSize : 11 , padding : "4px 10px" , borderRadius : 4 , border : "1px solid #0f766e" , background : "#f0fdfa" , color : "#0f766e" , cursor : "pointer" } ;
191253const miniDanger : React . CSSProperties = { fontSize : 11 , padding : "4px 10px" , borderRadius : 4 , border : "1px solid #fecaca" , background : "#fff" , color : "#b91c1c" , cursor : "pointer" } ;
192254const smallDanger : React . CSSProperties = { fontSize : 10 , padding : "1px 6px" , border : "none" , background : "none" , color : "#b91c1c" , cursor : "pointer" } ;
255+ const smallGhost : React . CSSProperties = { fontSize : 10 , padding : "1px 6px" , border : "none" , background : "none" , color : "#6b7280" , cursor : "pointer" } ;
0 commit comments