@@ -884,6 +884,7 @@ export default function Home() {
884884 const [ isListsOpen , setIsListsOpen ] = useState ( true ) ;
885885 const [ expandedQuadrants , setExpandedQuadrants ] = useState < Record < string , boolean > > ( { } ) ;
886886 const [ showAppMenu , setShowAppMenu ] = useState ( false ) ;
887+ const [ lastRemovedTask , setLastRemovedTask ] = useState < Task | null > ( null ) ;
887888 const [ showAbout , setShowAbout ] = useState ( false ) ;
888889 const [ calendarView , setCalendarView ] = useState < 'month' | 'week' | 'day' | 'agenda' > ( 'month' ) ;
889890 const [ showCompletedInCalendar , setShowCompletedInCalendar ] = useState ( false ) ;
@@ -926,6 +927,8 @@ export default function Home() {
926927 const [ notificationPermission , setNotificationPermission ] = useState < NotificationPermission > ( 'default' ) ;
927928 const [ isSecureContext , setIsSecureContext ] = useState ( true ) ;
928929 const [ showLogs , setShowLogs ] = useState ( false ) ;
930+ const [ taskUndoSnapshot , setTaskUndoSnapshot ] = useState < Task [ ] | null > ( null ) ;
931+ const [ taskUndoLabel , setTaskUndoLabel ] = useState ( '' ) ;
929932 const [ importMode , setImportMode ] = useState < 'merge' | 'overwrite' > ( 'merge' ) ;
930933 const importInputRef = useRef < HTMLInputElement | null > ( null ) ;
931934 const [ isFetchingModels , setIsFetchingModels ] = useState ( false ) ;
@@ -1994,6 +1997,74 @@ export default function Home() {
19941997 return ( ) => window . removeEventListener ( 'click' , handleClose ) ;
19951998 } , [ showAppMenu ] ) ;
19961999
2000+ useEffect ( ( ) => {
2001+ const handleGlobalKeydown = async ( event : KeyboardEvent ) => {
2002+ const modifier = event . metaKey || event . ctrlKey ;
2003+ const editable = isEditableShortcutTarget ( event . target ) ;
2004+
2005+ if ( event . key === 'Escape' ) {
2006+ if ( editingTaskId ) {
2007+ setEditingTaskId ( null ) ;
2008+ setEditingTaskTitle ( '' ) ;
2009+ return ;
2010+ }
2011+ if ( showSettings ) {
2012+ setShowSettings ( false ) ;
2013+ return ;
2014+ }
2015+ if ( showLogs ) {
2016+ setShowLogs ( false ) ;
2017+ return ;
2018+ }
2019+ if ( selectedTask ) {
2020+ setSelectedTask ( null ) ;
2021+ return ;
2022+ }
2023+ }
2024+
2025+ if ( ! modifier ) return ;
2026+
2027+ const key = event . key . toLowerCase ( ) ;
2028+
2029+ if ( key === 'c' && selectedTask && ! editable ) {
2030+ event . preventDefault ( ) ;
2031+ await copyTaskContent ( selectedTask ) ;
2032+ return ;
2033+ }
2034+
2035+ if ( key === 'v' && ! editable ) {
2036+ event . preventDefault ( ) ;
2037+ try {
2038+ const text = await navigator . clipboard . readText ( ) ;
2039+ if ( text . trim ( ) ) {
2040+ await createLocalTaskFromInput ( text . trim ( ) , activeFilter === 'category' ? activeCategory : null ) ;
2041+ }
2042+ } catch ( error ) {
2043+ console . error ( 'Failed to paste task from clipboard' , error ) ;
2044+ }
2045+ return ;
2046+ }
2047+
2048+ if ( key === 'z' && ! editable ) {
2049+ event . preventDefault ( ) ;
2050+ if ( lastRemovedTask ) {
2051+ restoreLastRemovedTask ( ) ;
2052+ }
2053+ }
2054+ } ;
2055+
2056+ window . addEventListener ( 'keydown' , handleGlobalKeydown ) ;
2057+ return ( ) => window . removeEventListener ( 'keydown' , handleGlobalKeydown ) ;
2058+ } , [
2059+ activeCategory ,
2060+ activeFilter ,
2061+ editingTaskId ,
2062+ lastRemovedTask ,
2063+ selectedTask ,
2064+ showLogs ,
2065+ showSettings ,
2066+ ] ) ;
2067+
19972068 const refreshTasks = ( ) => {
19982069 const all = taskStore . getAll ( ) ;
19992070 const deletedMap = readDeletedMap ( DELETED_TASKS_KEY ) ;
@@ -2004,6 +2075,29 @@ export default function Home() {
20042075 setTasks ( filtered ) ;
20052076 } ;
20062077
2078+ const snapshotTasksForUndo = ( label : string ) => {
2079+ setTaskUndoSnapshot ( taskStore . getAll ( ) . map ( ( task ) => ( {
2080+ ...task ,
2081+ tags : task . tags ? [ ...task . tags ] : [ ] ,
2082+ subtasks : task . subtasks ? task . subtasks . map ( ( subtask ) => ( { ...subtask } ) ) : [ ] ,
2083+ attachments : task . attachments ? task . attachments . map ( ( attachment ) => ( { ...attachment } ) ) : [ ] ,
2084+ repeat : task . repeat ? { ...task . repeat , weekdays : task . repeat . weekdays ? [ ...task . repeat . weekdays ] : undefined } : undefined ,
2085+ } ) ) ) ;
2086+ setTaskUndoLabel ( label ) ;
2087+ } ;
2088+
2089+ const restoreTaskSnapshot = ( ) => {
2090+ if ( ! taskUndoSnapshot ) return ;
2091+ taskStore . replaceAll ( taskUndoSnapshot ) ;
2092+ refreshTasks ( ) ;
2093+ if ( selectedTask ) {
2094+ const restoredSelected = taskUndoSnapshot . find ( ( task ) => task . id === selectedTask . id ) ?? null ;
2095+ setSelectedTask ( restoredSelected ) ;
2096+ }
2097+ setTaskUndoSnapshot ( null ) ;
2098+ setTaskUndoLabel ( '' ) ;
2099+ } ;
2100+
20072101 const refreshHabits = ( ) => {
20082102 const all = habitStore . getAll ( ) ;
20092103 const deletedMap = readDeletedMap ( DELETED_HABITS_KEY ) ;
@@ -3426,6 +3520,12 @@ export default function Home() {
34263520 }
34273521 } ;
34283522
3523+ const isEditableShortcutTarget = ( target : EventTarget | null ) => {
3524+ if ( ! ( target instanceof HTMLElement ) ) return false ;
3525+ const tagName = target . tagName ;
3526+ return target . isContentEditable || tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' ;
3527+ } ;
3528+
34293529 const toggleTaskSelected = ( taskId : string ) => {
34303530 setSelectedTaskIds ( ( prev ) => {
34313531 const next = new Set ( prev as Set < string > ) ;
@@ -3529,9 +3629,11 @@ export default function Home() {
35293629 } ;
35303630
35313631 const removeTask = ( taskId : string ) => {
3632+ const removedTask = taskStore . getAll ( ) . find ( ( task ) => task . id === taskId ) ;
35323633 taskStore . remove ( taskId ) ;
35333634 markDeleted ( DELETED_TASKS_KEY , taskId ) ;
35343635 refreshTasks ( ) ;
3636+ setLastRemovedTask ( removedTask ?? null ) ;
35353637
35363638 // 异步同步到 PG
35373639 syncToPg ( 'tasks' , 'DELETE' , { id : taskId } ) ;
@@ -3541,7 +3643,24 @@ export default function Home() {
35413643 }
35423644 } ;
35433645
3646+ const restoreLastRemovedTask = ( ) => {
3647+ if ( ! lastRemovedTask ) return ;
3648+ const restoredTask = { ...lastRemovedTask , updatedAt : new Date ( ) . toISOString ( ) } ;
3649+ const deletedMap = readDeletedMap ( DELETED_TASKS_KEY ) ;
3650+ if ( deletedMap [ restoredTask . id ] ) {
3651+ const nextDeletedMap = { ...deletedMap } ;
3652+ delete nextDeletedMap [ restoredTask . id ] ;
3653+ persistDeletedMap ( DELETED_TASKS_KEY , nextDeletedMap ) ;
3654+ }
3655+ taskStore . add ( restoredTask ) ;
3656+ refreshTasks ( ) ;
3657+ syncToPg ( 'tasks' , 'POST' , restoredTask ) ;
3658+ setSelectedTask ( restoredTask ) ;
3659+ setLastRemovedTask ( null ) ;
3660+ } ;
3661+
35443662 const clearCompletedTasks = ( ) => {
3663+ snapshotTasksForUndo ( '恢复已清除的已完成任务' ) ;
35453664 const completedIds = taskStore . getAll ( ) . filter ( ( task ) => task . status === 'completed' ) . map ( ( task ) => task . id ) ;
35463665 completedIds . forEach ( ( taskId ) => markDeleted ( DELETED_TASKS_KEY , taskId ) ) ;
35473666 const remaining = taskStore . getAll ( ) . filter ( ( task ) => task . status !== 'completed' ) ;
@@ -3705,6 +3824,14 @@ export default function Home() {
37053824 } ) ;
37063825 } ;
37073826
3827+ const isTypingTarget = ( target : EventTarget | null ) => {
3828+ const element = target as HTMLElement | null ;
3829+ if ( ! element ) return false ;
3830+ const tagName = element . tagName ;
3831+ return element . isContentEditable || tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' ;
3832+ } ;
3833+
3834+
37083835 const addTagToTask = ( ) => {
37093836 if ( ! selectedTask ) return ;
37103837 const tag = newTagInput . trim ( ) ;
0 commit comments