Ctrl K
ComunidadGuías…

App.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import React, { useEffect, useRef, useState } from 'react';
import { AppState, DeviceEventEmitter, View, StyleSheet, ActivityIndicator } from 'react-native';
import NetInfo from '@react-native-community/netinfo';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { COLORS, LEDGER_EVENTS, SETTING_KEYS, setCategoryConfig } from './src/constants';
import {
  initDatabase,
  setCustomCategoriesCache,
  getCategoryConfig,
  getSetting,
} from './src/database/ledgerDB';
import { runRecurringCheck } from './src/utils/recurring';
import { setHapticLevel } from './src/utils/haptics';
import { runSync, purgeOldTombstones } from './src/sync/syncEngine';
import TabBar, { type TabKey } from './src/components/TabBar';
import Toast from './src/components/Toast';
import HomeScreen from './src/screens/HomeScreen';
import LedgerScreen from './src/screens/LedgerScreen';
import StatsScreen from './src/screens/StatsScreen';
import ManageScreen from './src/screens/ManageScreen';

// 已登录时:数据变更后 debounce 自动同步(毫秒)
const AUTO_SYNC_DEBOUNCE = 5000;

// 静默后台同步(未配置/断网时内部自检跳过;完成或失败都不打扰 UI)
async function syncInBackground(): Promise<void> {
  try {
    await runSync();
  } catch {
    // 静默(断网等场景),下次事件/回前台/网络恢复再试
  }
}

export default function App() {
  const [dbReady, setDbReady] = useState(false);
  const [tab, setTab] = useState<TabKey>('home');
  const syncTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    (async () => {
      try {
        await initDatabase();
        await setCustomCategoriesCache();
        const cfg = await getCategoryConfig();
        setCategoryConfig(cfg);
        // 触感档位读进模块缓存:各页调用 hapticXxx 时不必再查库(默认 subtle)
        const haptic = await getSetting(SETTING_KEYS.HAPTIC_LEVEL);
        setHapticLevel(haptic === 'off' || haptic === 'full' ? haptic : 'subtle');
      } catch {
        // 初始化失败也放行,页面会自行兜底
      }
      // 周期记账:检查并生成到期记录
      try {
        await runRecurringCheck();
      } catch {
        // 静默
      }
      // 本地优先:先渲染 UI 再后台同步(runSync 内部自检配置,
      // 未配置同步则跳过;完成后广播 SYNC_DONE,各页面自行刷新)
      setDbReady(true);
      (async () => {
        try {
          await purgeOldTombstones();
        } catch {
          // 静默
        }
        syncInBackground();
      })();
    })();
  }, []);

  // 本地优先的补网策略:回前台 / 网络恢复时自动补一轮同步(已配置才生效)
  useEffect(() => {
    // 回前台(后台期间断网记的账,回来看一眼就能补上)
    const appStateSub = AppState.addEventListener('change', (state) => {
      if (state === 'active') syncInBackground();
    });
    // 网络恢复(前台等待断网恢复的场景,如地铁出站)
    // 只在「可达性」真的变化时补一轮同步:NetInfo 在任何连接属性变化时都会回调
    // (Wi-Fi↔蜂窝切换、IP 变化等),此前每次回调都打一串请求,信号不稳的机型会被放大
    let lastReachable: boolean | null = null;
    const netSub = NetInfo.addEventListener((state) => {
      const reachable = state.isInternetReachable ?? null;
      if (reachable === lastReachable) return;
      lastReachable = reachable;
      if (reachable === true) syncInBackground();
    });
    return () => {
      appStateSub.remove();
      netSub();
    };
  }, []);

  // 数据变更 → debounce 自动同步(仅已配置时;runSync 内部自检配置)
  useEffect(() => {
    const schedule = () => {
      if (syncTimer.current) clearTimeout(syncTimer.current);
      syncTimer.current = setTimeout(() => {
        // runSync 的正常路径不抛(内部 catch 后返回结果),但读取配置阶段仍可能 reject,
        // 不接住就是一次未处理拒绝、这一轮同步静默消失。
        // SYNC_DONE 由 runSync 的 finally 统一广播,这里不再补发(此前同一轮会触发两次刷新)。
        void runSync().catch(() => {});
      }, AUTO_SYNC_DEBOUNCE);
    };
    const subs = [
      DeviceEventEmitter.addListener(LEDGER_EVENTS.RECORDED, schedule),
      DeviceEventEmitter.addListener(LEDGER_EVENTS.DATA_IMPORTED, schedule),
      DeviceEventEmitter.addListener(LEDGER_EVENTS.AUTH_CHANGED, schedule),
    ];
    return () => {
      subs.forEach((s) => s.remove());
      if (syncTimer.current) clearTimeout(syncTimer.current);
    };
  }, []);

  if (!dbReady) {
    return (
      <View style={styles.loading}>
        <ActivityIndicator size="large" color={COLORS.accent} />
      </View>
    );
  }

  return (
    <SafeAreaProvider>
      <View style={styles.container}>
        <View style={styles.screen}>
          {/* 保持所有页面挂载,仅切换显隐,避免每次切 tab 重查库;激活时各自滚回顶部 */}
          <View style={[styles.page, tab !== 'home' && styles.pageHidden]}>
            <HomeScreen active={tab === 'home'} />
          </View>
          <View style={[styles.page, tab !== 'ledger' && styles.pageHidden]}>
            <LedgerScreen active={tab === 'ledger'} />
          </View>
          <View style={[styles.page, tab !== 'stats' && styles.pageHidden]}>
            <StatsScreen active={tab === 'stats'} />
          </View>
          <View style={[styles.page, tab !== 'manage' && styles.pageHidden]}>
            <ManageScreen active={tab === 'manage'} />
          </View>
        </View>
        <TabBar current={tab} onChange={setTab} />
        {/* 全局唯一提示层:非模态浮层,显示期间页面照常可点 */}
        <Toast />
      </View>
    </SafeAreaProvider>
  );
}

const styles = StyleSheet.create({
  loading: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: COLORS.background,
  },
  container: {
    flex: 1,
    backgroundColor: COLORS.background,
  },
  screen: {
    flex: 1,
  },
  page: {
    flex: 1,
  },
  pageHidden: {
    display: 'none',
  },
});