| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- import { STORAGE } from '@/core/constants/storage-keys'
- import { isTeenModeEnabled, verifyTeenPin } from '@/features/account/teen-mode-settings'
- export const SESSION_LIMIT_MS = 40 * 60 * 1000
- export const DAILY_LIMIT_MS = 40 * 60 * 1000
- export const CURFEW_START_HOUR = 22
- export const CURFEW_END_HOUR = 6
- export const TICK_MS = 15000
- const LOCK_PAGE = 'pages/user/settings/teen-mode-lock'
- let i18nRef = null
- let started = false
- let foreground = false
- let tickTimer = null
- let navigatingLock = false
- const lockState = {
- locked: false,
- reason: ''
- }
- const listeners = new Set()
- function t(key, fallback) {
- if (!i18nRef) return fallback
- try {
- const v = i18nRef.t(key)
- return v && v !== key ? v : fallback
- } catch (_) {
- return fallback
- }
- }
- function pad2(n) {
- return n < 10 ? `0${n}` : String(n)
- }
- function todayKey(date = new Date()) {
- return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`
- }
- function defaultUsage(now = Date.now()) {
- return {
- date: todayKey(new Date(now)),
- dailyMs: 0,
- sessionMs: 0,
- lastTickAt: now
- }
- }
- function normalizeUsage(raw) {
- if (!raw || typeof raw !== 'object') return defaultUsage()
- const now = Date.now()
- const usage = {
- date: String(raw.date || todayKey(new Date(now))),
- dailyMs: Math.max(0, Number(raw.dailyMs) || 0),
- sessionMs: Math.max(0, Number(raw.sessionMs) || 0),
- lastTickAt: Number(raw.lastTickAt) || now
- }
- if (usage.date !== todayKey()) {
- usage.date = todayKey()
- usage.dailyMs = 0
- usage.sessionMs = 0
- usage.lastTickAt = now
- }
- return usage
- }
- function readUsage() {
- try {
- return normalizeUsage(uni.getStorageSync(STORAGE.teenModeUsage))
- } catch (_) {
- return defaultUsage()
- }
- }
- function writeUsage(usage) {
- uni.setStorageSync(STORAGE.teenModeUsage, normalizeUsage(usage))
- }
- export function isCurfewHour(date = new Date()) {
- const hour = date.getHours()
- return hour >= CURFEW_START_HOUR || hour < CURFEW_END_HOUR
- }
- function notifyLock() {
- listeners.forEach((fn) => {
- try {
- fn({ ...lockState })
- } catch (_) {}
- })
- }
- function setLock(reason) {
- const nextReason = reason || ''
- if (lockState.locked === !!nextReason && lockState.reason === nextReason) return
- lockState.locked = !!nextReason
- lockState.reason = nextReason
- notifyLock()
- syncNativeMask()
- if (lockState.locked) {
- openLockPage(nextReason)
- }
- }
- function clearLock() {
- setLock('')
- closeLockPageIfNeeded()
- }
- export function refreshTeenModeGuard() {
- return evaluateLock()
- }
- export function getTeenLockState() {
- return { ...lockState }
- }
- export function subscribeTeenLock(listener) {
- if (typeof listener !== 'function') return () => {}
- listeners.add(listener)
- listener({ ...lockState })
- return () => listeners.delete(listener)
- }
- function flushUsage(now = Date.now()) {
- if (!foreground || isCurfewHour(new Date(now))) return readUsage()
- const usage = readUsage()
- const delta = Math.max(0, now - (usage.lastTickAt || now))
- if (delta <= 0) return usage
- usage.dailyMs += delta
- usage.sessionMs += delta
- usage.lastTickAt = now
- writeUsage(usage)
- return usage
- }
- function evaluateLock(now = Date.now()) {
- if (!isTeenModeEnabled()) {
- clearLock()
- return null
- }
- const date = new Date(now)
- if (isCurfewHour(date)) {
- setLock('curfew')
- return 'curfew'
- }
- if (lockState.locked && lockState.reason === 'curfew') {
- // 宵禁刚结束,继续检查时长
- } else if (lockState.locked && (lockState.reason === 'session' || lockState.reason === 'daily')) {
- return lockState.reason
- }
- const usage = flushUsage(now)
- if (usage.sessionMs >= SESSION_LIMIT_MS) {
- setLock('session')
- return 'session'
- }
- if (usage.dailyMs >= DAILY_LIMIT_MS) {
- setLock('daily')
- return 'daily'
- }
- clearLock()
- return null
- }
- function isOnLockPage() {
- const pages = getCurrentPages()
- const top = pages[pages.length - 1]
- const route = top && (top.route || (top.$page && top.$page.fullPath))
- return String(route || '').indexOf(LOCK_PAGE) !== -1
- }
- function openLockPage(reason) {
- if (navigatingLock || isOnLockPage()) return
- navigatingLock = true
- const url = `/pages/user/settings/teen-mode-lock?reason=${encodeURIComponent(reason || 'session')}`
- uni.navigateTo({
- url,
- fail: () => {
- uni.reLaunch({ url })
- },
- complete: () => {
- setTimeout(() => {
- navigatingLock = false
- syncNativeMask()
- }, 400)
- }
- })
- }
- function closeLockPageIfNeeded() {
- if (!isOnLockPage()) return
- uni.navigateBack({
- delta: 1,
- fail: () => {}
- })
- }
- export function tryUnlockTeenLock(pin) {
- if (!lockState.locked) return true
- if (lockState.reason === 'curfew') return false
- if (!verifyTeenPin(pin)) return false
- const usage = readUsage()
- const now = Date.now()
- if (lockState.reason === 'session') {
- usage.sessionMs = 0
- } else if (lockState.reason === 'daily') {
- usage.dailyMs = 0
- usage.sessionMs = 0
- }
- usage.lastTickAt = now
- writeUsage(usage)
- clearLock()
- return true
- }
- export function getTeenLockCopy(reason) {
- if (reason === 'curfew') {
- return {
- title: t('user.teenModeLockCurfewTitle', '宵禁时段'),
- desc: t(
- 'user.teenModeLockCurfewDesc',
- '每日 22:00 至次日 6:00 为宵禁时段,期间暂停使用。请明日 6:00 后再试。'
- ),
- showPin: false
- }
- }
- if (reason === 'daily') {
- return {
- title: t('user.teenModeLockDailyTitle', '今日使用时长已达上限'),
- desc: t(
- 'user.teenModeLockDailyDesc',
- '今日累计使用已超过 40 分钟,请输入监护密码后继续。'
- ),
- showPin: true
- }
- }
- return {
- title: t('user.teenModeLockSessionTitle', '该次使用时长已达上限'),
- desc: t(
- 'user.teenModeLockSessionDesc',
- '本次连续使用已超过 40 分钟,请输入监护密码后继续。'
- ),
- showPin: true
- }
- }
- function startTick() {
- stopTick()
- if (!foreground || !isTeenModeEnabled()) return
- tickTimer = setInterval(() => {
- evaluateLock()
- }, TICK_MS)
- }
- function stopTick() {
- if (tickTimer) {
- clearInterval(tickTimer)
- tickTimer = null
- }
- }
- export function notifyTeenModeChanged() {
- if (!isTeenModeEnabled()) {
- stopTick()
- clearLock()
- return
- }
- const usage = defaultUsage()
- writeUsage(usage)
- if (foreground) {
- evaluateLock()
- startTick()
- }
- }
- export function onTeenModeForeground() {
- foreground = true
- if (!isTeenModeEnabled()) {
- stopTick()
- return
- }
- const usage = readUsage()
- const now = Date.now()
- usage.lastTickAt = now
- writeUsage(usage)
- evaluateLock(now)
- startTick()
- }
- export function onTeenModeBackground() {
- if (!isTeenModeEnabled()) {
- foreground = false
- stopTick()
- return
- }
- flushUsage()
- foreground = false
- stopTick()
- }
- export function startTeenModeGuard(i18n) {
- if (started) return
- started = true
- i18nRef = i18n
- subscribeTeenLock(syncNativeMask)
- }
- // #ifdef APP-PLUS
- let nativeMask = null
- function ensureNativeMask() {
- if (nativeMask) return nativeMask
- nativeMask = new plus.nativeObj.View(
- 'hl88-teen-mode-blocker',
- {
- top: '0px',
- left: '0px',
- width: '100%',
- height: '100%',
- backgroundColor: 'rgba(0,0,0,0.01)'
- },
- []
- )
- nativeMask.interceptTouchEvent(true)
- nativeMask.addEventListener('click', () => {
- if (lockState.locked) {
- openLockPage(lockState.reason)
- }
- })
- return nativeMask
- }
- function syncNativeMask() {
- try {
- if (!lockState.locked || isOnLockPage()) {
- if (nativeMask) nativeMask.hide()
- return
- }
- ensureNativeMask().show()
- } catch (e) {
- console.warn('[teen-mode-guard] native mask', e)
- }
- }
- export function syncTeenLockOverlay() {
- syncNativeMask()
- }
- // #endif
- // #ifndef APP-PLUS
- function syncNativeMask() {}
- export function syncTeenLockOverlay() {}
- // #endif
|