File size: 2,589 Bytes
793e953
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
 * useIdleTimer Hook
 *
 * 检测用户空闲状态(默认 5 分钟无操作)。
 * 监听鼠标移动、键盘按下、触摸等事件,空闲时触发 onIdle 回调。
 *
 * 用法:
 *   const { isIdle, resetTimer } = useIdleTimer({
 *     timeout: 5 * 60 * 1000, // 5 分钟
 *     onIdle: () => console.log('用户空闲'),
 *     onActive: () => console.log('用户恢复活动'),
 *   })
 */

import { useState, useEffect, useCallback, useRef } from 'react'

interface UseIdleTimerOptions {
  /** 空闲超时时间(ms),默认 5 分钟 */
  timeout?: number
  /** 空闲时触发 */
  onIdle?: () => void
  /** 从空闲恢复活动时触发 */
  onActive?: () => void
  /** 是否启用,默认 true */
  enabled?: boolean
}

interface UseIdleTimerReturn {
  /** 当前是否空闲 */
  isIdle: boolean
  /** 手动重置空闲计时器 */
  resetTimer: () => void
}

const EVENTS = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll', 'wheel'] as const

export function useIdleTimer(options: UseIdleTimerOptions = {}): UseIdleTimerReturn {
  const { timeout = 5 * 60 * 1000, onIdle, onActive, enabled = true } = options

  const [isIdle, setIsIdle] = useState(false)
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const isIdleRef = useRef(false)
  const onIdleRef = useRef(onIdle)
  const onActiveRef = useRef(onActive)
  onIdleRef.current = onIdle
  onActiveRef.current = onActive

  const clearTimer = useCallback(() => {
    if (timerRef.current) {
      clearTimeout(timerRef.current)
      timerRef.current = null
    }
  }, [])

  const startTimer = useCallback(() => {
    clearTimer()
    if (!enabled) return
    timerRef.current = setTimeout(() => {
      isIdleRef.current = true
      setIsIdle(true)
      onIdleRef.current?.()
    }, timeout)
  }, [timeout, enabled, clearTimer])

  const resetTimer = useCallback(() => {
    if (!enabled) return
    if (isIdleRef.current) {
      isIdleRef.current = false
      setIsIdle(false)
      onActiveRef.current?.()
    }
    startTimer()
  }, [enabled, startTimer])

  useEffect(() => {
    if (!enabled) {
      clearTimer()
      return
    }

    startTimer()

    const handleActivity = () => {
      resetTimer()
    }

    EVENTS.forEach((event) => {
      window.addEventListener(event, handleActivity, { passive: true })
    })

    return () => {
      clearTimer()
      EVENTS.forEach((event) => {
        window.removeEventListener(event, handleActivity)
      })
    }
  }, [enabled, startTimer, resetTimer, clearTimer])

  return { isIdle, resetTimer }
}