diff --git a/app.json b/app.json index f04fe52..9e69d98 100644 --- a/app.json +++ b/app.json @@ -2,7 +2,7 @@ "expo": { "name": "萝卜社区", "slug": "qojo", - "version": "1.0.12", + "version": "1.0.13", "orientation": "default", "icon": "./assets/icon.png", "userInterfaceStyle": "automatic", @@ -36,7 +36,7 @@ }, "predictiveBackGestureEnabled": false, "package": "skin.carrot.bbs", - "versionCode": 6, + "versionCode": 7, "permissions": [ "VIBRATE", "RECORD_AUDIO", @@ -63,6 +63,16 @@ }, "plugins": [ "./plugins/withMainActivityConfigChange", + [ + "./plugins/withForegroundService", + { + "notificationTitle": "萝卜社区", + "notificationBody": "正在后台同步消息", + "notificationIcon": "ic_notification", + "channelId": "carrot_sync_foreground", + "channelName": "消息同步" + } + ], [ "expo-router", { @@ -86,9 +96,9 @@ } ], [ - "expo-background-fetch", + "expo-background-task", { - "minimumInterval": 900 + "minimumInterval": 15 } ], [ diff --git a/package.json b/package.json index 62dacb5..b021805 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "carrot_bbs", - "version": "1.0.2", + "version": "1.0.3", "main": "expo-router/entry", "scripts": { "start": "expo start", @@ -25,7 +25,6 @@ "axios": "^1.13.6", "date-fns": "^4.1.0", "expo": "~55.0.4", - "expo-background-fetch": "~55.0.10", "expo-background-task": "~55.0.10", "expo-camera": "^55.0.10", "expo-constants": "~55.0.7", diff --git a/plugins/withForegroundService.js b/plugins/withForegroundService.js new file mode 100644 index 0000000..bb8badb --- /dev/null +++ b/plugins/withForegroundService.js @@ -0,0 +1,467 @@ +const { withAndroidManifest, withDangerousMod } = require('@expo/config-plugins'); +const fs = require('fs'); +const path = require('path'); + +const FOREGROUND_SERVICE_PERMISSION = 'android.permission.FOREGROUND_SERVICE'; +const FOREGROUND_SERVICE_DATA_SYNC = 'android.permission.FOREGROUND_SERVICE_DATA_SYNC'; + +/** + * Expo Config Plugin:为 Android 添加前台服务保活 + * + * 功能: + * 1. 添加 FOREGROUND_SERVICE 相关权限 + * 2. 在 AndroidManifest 中声明 Service + * 3. 生成 Kotlin 原生服务代码 + * 4. 生成 React Native Bridge 模块 + * + * 参考:Element Android 的 GuardAndroidService + */ +const withForegroundService = (config, options = {}) => { + const { + notificationTitle = '萝卜社区', + notificationBody = '正在后台同步消息', + notificationIcon = 'ic_notification', + channelId = 'carrot_sync_foreground', + channelName = '消息同步', + } = options; + + // 1. 添加权限和 Service 声明到 AndroidManifest + config = withAndroidManifest(config, (config) => { + const manifest = config.modResults; + + // 添加权限声明 + const permissions = [ + FOREGROUND_SERVICE_PERMISSION, + FOREGROUND_SERVICE_DATA_SYNC, + ]; + + permissions.forEach((permission) => { + const existingPermission = manifest.manifest['uses-permission']?.find( + (p) => p.$['android:name'] === permission + ); + if (!existingPermission) { + if (!manifest.manifest['uses-permission']) { + manifest.manifest['uses-permission'] = []; + } + manifest.manifest['uses-permission'].push({ + $: { 'android:name': permission }, + }); + } + }); + + // 添加 Service 声明 + if (!manifest.manifest.application[0].service) { + manifest.manifest.application[0].service = []; + } + + const existingService = manifest.manifest.application[0].service.find( + (s) => s.$['android:name'] === '.SyncForegroundService' + ); + + if (!existingService) { + manifest.manifest.application[0].service.push({ + $: { + 'android:name': '.SyncForegroundService', + 'android:enabled': 'true', + 'android:exported': 'false', + 'android:foregroundServiceType': 'dataSync', + }, + }); + } + + return config; + }); + + // 2. 生成 Kotlin 原生代码 + config = withDangerousMod(config, [ + 'android', + async (config) => { + const projectRoot = config.modRequest.projectRoot; + const packageName = config.android.package; + const kotlinPath = path.join( + projectRoot, + 'android/app/src/main/java', + packageName.replace(/\./g, '/') + ); + + // 确保目录存在 + if (!fs.existsSync(kotlinPath)) { + fs.mkdirSync(kotlinPath, { recursive: true }); + } + + // 生成 SyncForegroundService.kt + const serviceCode = generateServiceCode(packageName, { + notificationTitle, + notificationBody, + notificationIcon, + channelId, + channelName, + }); + + fs.writeFileSync( + path.join(kotlinPath, 'SyncForegroundService.kt'), + serviceCode + ); + + // 生成 ForegroundServiceModule.kt (React Native Bridge) + const moduleCode = generateModuleCode(packageName); + fs.writeFileSync( + path.join(kotlinPath, 'ForegroundServiceModule.kt'), + moduleCode + ); + + // 生成 ForegroundServicePackage.kt + const packageCode = generatePackageCode(packageName); + fs.writeFileSync( + path.join(kotlinPath, 'ForegroundServicePackage.kt'), + packageCode + ); + + // 修改 MainApplication.kt 注册模块 + const mainAppPath = path.join(kotlinPath, 'MainApplication.kt'); + if (fs.existsSync(mainAppPath)) { + let content = fs.readFileSync(mainAppPath, 'utf-8'); + + // 添加 import + if (!content.includes('ForegroundServicePackage')) { + content = content.replace( + /package .*\n/, + `$&\nimport ${packageName}.ForegroundServicePackage\n` + ); + } + + // 在 packages 列表中添加 + if (!content.includes('ForegroundServicePackage()')) { + // 查找 PackageList(this).packages.apply 块 + content = content.replace( + /(PackageList\(this\)\.packages\.apply\s*\{)/, + `$1\n // 前台服务模块\n add(ForegroundServicePackage())` + ); + } + + fs.writeFileSync(mainAppPath, content); + } + + // 创建通知图标目录和文件 + const drawablePath = path.join(projectRoot, 'android/app/src/main/res/drawable'); + if (!fs.existsSync(drawablePath)) { + fs.mkdirSync(drawablePath, { recursive: true }); + } + + const iconPath = path.join(drawablePath, 'ic_notification.xml'); + if (!fs.existsSync(iconPath)) { + fs.writeFileSync(iconPath, generateNotificationIcon()); + } + + return config; + }, + ]); + + return config; +}; + +/** + * 生成前台服务 Kotlin 代码 + */ +function generateServiceCode(packageName, options) { + const { notificationTitle, notificationBody, notificationIcon, channelId, channelName } = options; + + return `package ${packageName} + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat + +/** + * 前台同步服务 + * + * 在通知栏显示常驻通知,防止应用进程被系统杀死 + * 类似 Element Android 的 GuardAndroidService 和 V2Ray 的保活机制 + */ +class SyncForegroundService : Service() { + + companion object { + const val NOTIFICATION_ID = 1001 + const val CHANNEL_ID = "${channelId}" + const val CHANNEL_NAME = "${channelName}" + + const val ACTION_START = "${packageName}.foreground.START" + const val ACTION_STOP = "${packageName}.foreground.STOP" + const val ACTION_UPDATE = "${packageName}.foreground.UPDATE" + + const val EXTRA_TITLE = "title" + const val EXTRA_BODY = "body" + + /** + * 启动前台服务 + */ + fun start(context: Context, title: String = "${notificationTitle}", body: String = "${notificationBody}") { + val intent = Intent(context, SyncForegroundService::class.java).apply { + action = ACTION_START + putExtra(EXTRA_TITLE, title) + putExtra(EXTRA_BODY, body) + } + try { + ContextCompat.startForegroundService(context, intent) + } catch (e: Exception) { + android.util.Log.e("SyncForegroundService", "启动前台服务失败", e) + } + } + + /** + * 停止前台服务 + */ + fun stop(context: Context) { + val intent = Intent(context, SyncForegroundService::class.java).apply { + action = ACTION_STOP + } + context.startService(intent) + } + + /** + * 更新通知内容 + */ + fun update(context: Context, title: String, body: String) { + val intent = Intent(context, SyncForegroundService::class.java).apply { + action = ACTION_UPDATE + putExtra(EXTRA_TITLE, title) + putExtra(EXTRA_BODY, body) + } + context.startService(intent) + } + } + + private var notificationTitle: String = "${notificationTitle}" + private var notificationBody: String = "${notificationBody}" + + override fun onCreate() { + super.onCreate() + createNotificationChannel() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + ACTION_START -> { + notificationTitle = intent.getStringExtra(EXTRA_TITLE) ?: "${notificationTitle}" + notificationBody = intent.getStringExtra(EXTRA_BODY) ?: "${notificationBody}" + startForegroundCompat() + } + ACTION_STOP -> { + stopForegroundCompat() + stopSelf() + } + ACTION_UPDATE -> { + notificationTitle = intent.getStringExtra(EXTRA_TITLE) ?: notificationTitle + notificationBody = intent.getStringExtra(EXTRA_BODY) ?: notificationBody + updateNotification() + } + } + // START_STICKY: 服务被杀后自动重启 + return START_STICKY + } + + override fun onBind(intent: Intent?): IBinder? = null + + /** + * 启动前台服务(兼容 Android Q+) + */ + private fun startForegroundCompat() { + val notification = buildNotification() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + startForeground( + NOTIFICATION_ID, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } else { + startForeground(NOTIFICATION_ID, notification) + } + } + + /** + * 停止前台服务(兼容 Android N+) + */ + private fun stopForegroundCompat() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + @Suppress("DEPRECATION") + stopForeground(true) + } + } + + /** + * 创建通知渠道 + */ + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + CHANNEL_NAME, + NotificationManager.IMPORTANCE_MIN // 最低重要性,不打扰用户 + ).apply { + description = "后台消息同步服务" + setSound(null, null) + setShowBadge(false) + enableLights(false) + enableVibration(false) + } + + val manager = getSystemService(NotificationManager::class.java) + manager.createNotificationChannel(channel) + } + } + + /** + * 构建通知 + */ + private fun buildNotification(): Notification { + // 点击通知打开应用 + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + val pendingIntent = PendingIntent.getActivity( + this, + 0, + launchIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(notificationTitle) + .setContentText(notificationBody) + .setSmallIcon(R.drawable.${notificationIcon}) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setOngoing(true) // 不可滑动清除 + .setShowWhen(false) + .setContentIntent(pendingIntent) + .build() + } + + /** + * 更新通知内容 + */ + private fun updateNotification() { + val manager = getSystemService(NotificationManager::class.java) + manager.notify(NOTIFICATION_ID, buildNotification()) + } +} +`; +} + +/** + * 生成 React Native Bridge 模块代码 + */ +function generateModuleCode(packageName) { + return `package ${packageName} + +import android.content.Context +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.Promise + +/** + * React Native Bridge: 前台服务控制模块 + */ +class ForegroundServiceModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String = "ForegroundService" + + /** + * 启动前台服务 + */ + @ReactMethod + fun start(title: String, body: String, promise: Promise) { + try { + SyncForegroundService.start(reactApplicationContext, title, body) + promise.resolve(true) + } catch (e: Exception) { + promise.reject("FOREGROUND_SERVICE_ERROR", e.message) + } + } + + /** + * 停止前台服务 + */ + @ReactMethod + fun stop(promise: Promise) { + try { + SyncForegroundService.stop(reactApplicationContext) + promise.resolve(true) + } catch (e: Exception) { + promise.reject("FOREGROUND_SERVICE_ERROR", e.message) + } + } + + /** + * 更新通知内容 + */ + @ReactMethod + fun update(title: String, body: String, promise: Promise) { + try { + SyncForegroundService.update(reactApplicationContext, title, body) + promise.resolve(true) + } catch (e: Exception) { + promise.reject("FOREGROUND_SERVICE_ERROR", e.message) + } + } +} +`; +} + +/** + * 生成 React Native Package 代码 + */ +function generatePackageCode(packageName) { + return `package ${packageName} + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +class ForegroundServicePackage : ReactPackage { + override fun createNativeModules(reactContext: ReactApplicationContext): List { + return listOf(ForegroundServiceModule(reactContext)) + } + + override fun createViewManagers(reactContext: ReactApplicationContext): List> { + return emptyList() + } +} +`; +} + +/** + * 生成通知图标 XML + */ +function generateNotificationIcon() { + return ` + + + + + + +`; +} + +module.exports = withForegroundService; \ No newline at end of file diff --git a/src/components/business/SystemMessageItem.tsx b/src/components/business/SystemMessageItem.tsx index b722eb7..094d4af 100644 --- a/src/components/business/SystemMessageItem.tsx +++ b/src/components/business/SystemMessageItem.tsx @@ -125,12 +125,12 @@ function createSystemMessageStyles(colors: AppColors) { flexDirection: 'row', alignItems: 'flex-start', padding: 16, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: colors.divider || '#E5E5EA', }, unreadContainer: { - backgroundColor: '#FFF8F6', + backgroundColor: colors.primary.main + '0A', }, unreadIndicator: { position: 'absolute', @@ -140,7 +140,7 @@ function createSystemMessageStyles(colors: AppColors) { width: 8, height: 8, borderRadius: 4, - backgroundColor: '#FF6B35', + backgroundColor: colors.primary.main, }, iconContainer: { marginRight: 14, @@ -165,7 +165,7 @@ function createSystemMessageStyles(colors: AppColors) { justifyContent: 'center', alignItems: 'center', borderWidth: 2, - borderColor: '#fff', + borderColor: colors.background.paper, }, content: { flex: 1, @@ -220,7 +220,7 @@ function createSystemMessageStyles(colors: AppColors) { flexDirection: 'row', alignItems: 'center', marginTop: 8, - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, paddingHorizontal: 10, paddingVertical: 6, borderRadius: 8, @@ -318,13 +318,6 @@ const getSystemMessageTitle = (message: SystemMessageResponse): string => { } }; -// 胡萝卜橙主题色 -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - const SystemMessageItem: React.FC = ({ message, onPress, diff --git a/src/screens/auth/ForgotPasswordScreen.tsx b/src/screens/auth/ForgotPasswordScreen.tsx index dafa722..3be3711 100644 --- a/src/screens/auth/ForgotPasswordScreen.tsx +++ b/src/screens/auth/ForgotPasswordScreen.tsx @@ -26,19 +26,13 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { useAppColors, type AppColors } from '../../theme'; +import { useAppColors, useResolvedColorScheme, type AppColors } from '../../theme'; import { authService, resolveAuthApiError } from '../../services/authService'; import { showPrompt } from '../../services/promptService'; -// 胡萝卜橙主题色 -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - export const ForgotPasswordScreen: React.FC = () => { const colors = useAppColors(); + const resolved = useResolvedColorScheme(); const styles = useMemo(() => createForgotPasswordStyles(colors), [colors]); const router = useRouter(); @@ -156,7 +150,7 @@ export const ForgotPasswordScreen: React.FC = () => { return ( - + { keyboardType="email-address" /> {email.length > 0 && validateEmail(email) && ( - + )} @@ -225,7 +219,7 @@ export const ForgotPasswordScreen: React.FC = () => { activeOpacity={0.8} > {sendingCode ? ( - + ) : ( {countdown > 0 ? `${countdown}s` : '获取验证码'} )} @@ -295,7 +289,7 @@ export const ForgotPasswordScreen: React.FC = () => { activeOpacity={0.9} > {loading ? ( - + ) : ( 重置密码 )} @@ -307,7 +301,7 @@ export const ForgotPasswordScreen: React.FC = () => { onPress={() => router.back()} activeOpacity={0.8} > - + 返回登录 @@ -322,7 +316,7 @@ function createForgotPasswordStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, }, keyboardView: { flex: 1, @@ -354,7 +348,7 @@ function createForgotPasswordStyles(colors: AppColors) { underline: { width: 40, height: 4, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, borderRadius: 2, marginTop: 12, marginBottom: 16, @@ -382,7 +376,7 @@ function createForgotPasswordStyles(colors: AppColors) { inputWrapper: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 14, paddingHorizontal: 18, height: 56, @@ -412,7 +406,7 @@ function createForgotPasswordStyles(colors: AppColors) { sendCodeButton: { height: 56, paddingHorizontal: 16, - backgroundColor: 'rgba(255, 107, 53, 0.1)', + backgroundColor: colors.primary.light + '20', borderRadius: 14, alignItems: 'center', justifyContent: 'center', @@ -424,14 +418,14 @@ function createForgotPasswordStyles(colors: AppColors) { sendCodeButtonText: { fontSize: 14, fontWeight: '600', - color: THEME_COLORS.primary, + color: colors.primary.main, }, // 提交按钮 submitButton: { height: 56, borderRadius: 14, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', marginTop: 8, @@ -440,7 +434,7 @@ function createForgotPasswordStyles(colors: AppColors) { opacity: 0.7, }, submitButtonText: { - color: '#fff', + color: colors.text.inverse, fontSize: 17, fontWeight: '600', }, @@ -453,7 +447,7 @@ function createForgotPasswordStyles(colors: AppColors) { marginTop: 24, }, backButtonText: { - color: THEME_COLORS.primary, + color: colors.primary.main, fontSize: 15, fontWeight: '500', }, diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx index 3abef85..fc4b792 100644 --- a/src/screens/auth/LoginScreen.tsx +++ b/src/screens/auth/LoginScreen.tsx @@ -26,21 +26,17 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { useAppColors, type AppColors } from '../../theme'; +import { useAppColors, useResolvedColorScheme, type AppColors } from '../../theme'; import { useAuthStore } from '../../stores'; import * as hrefs from '../../navigation/hrefs'; import { useResponsive } from '../../hooks'; import { showPrompt } from '../../services/promptService'; -// 胡萝卜橙主题色 -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; + export const LoginScreen: React.FC = () => { const colors = useAppColors(); + const resolved = useResolvedColorScheme(); const styles = useMemo(() => createLoginStyles(colors), [colors]); const router = useRouter(); const login = useAuthStore((state) => state.login); @@ -129,7 +125,7 @@ export const LoginScreen: React.FC = () => { return ( - + { returnKeyType="next" /> {username.length > 0 && ( - + )} @@ -218,17 +214,17 @@ export const LoginScreen: React.FC = () => { {/* 服务条款勾选 */} - setAgreedToTerms(!agreedToTerms)} - activeOpacity={0.8} - style={styles.checkboxWrapper} - > - - + setAgreedToTerms(!agreedToTerms)} + activeOpacity={0.8} + style={styles.checkboxWrapper} + > + + 我已阅读并同意 { activeOpacity={0.9} > {loading ? ( - + ) : ( 登 录 )} @@ -282,7 +278,7 @@ function createLoginStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, }, keyboardView: { flex: 1, @@ -317,7 +313,7 @@ function createLoginStyles(colors: AppColors) { underline: { width: 40, height: 4, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, borderRadius: 2, marginTop: 12, marginBottom: 16, @@ -345,7 +341,7 @@ function createLoginStyles(colors: AppColors) { inputWrapper: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 14, paddingHorizontal: 18, height: 56, @@ -372,7 +368,7 @@ function createLoginStyles(colors: AppColors) { }, forgotPasswordText: { fontSize: 14, - color: THEME_COLORS.primary, + color: colors.primary.main, fontWeight: '500', }, @@ -380,7 +376,7 @@ function createLoginStyles(colors: AppColors) { loginButton: { height: 56, borderRadius: 14, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', }, @@ -390,7 +386,7 @@ function createLoginStyles(colors: AppColors) { loginButtonText: { fontSize: 17, fontWeight: '600', - color: '#FFFFFF', + color: colors.text.inverse, }, // 底部 @@ -406,7 +402,7 @@ function createLoginStyles(colors: AppColors) { }, registerLink: { fontSize: 15, - color: THEME_COLORS.primary, + color: colors.primary.main, fontWeight: '600', marginLeft: 6, }, @@ -419,7 +415,7 @@ function createLoginStyles(colors: AppColors) { color: colors.text.hint, }, policyLink: { - color: THEME_COLORS.primary, + color: colors.primary.main, fontWeight: '500', }, termsContainer: { @@ -439,7 +435,7 @@ function createLoginStyles(colors: AppColors) { lineHeight: 22, }, termsLink: { - color: THEME_COLORS.primary, + color: colors.primary.main, fontWeight: '500', }, }); diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx index ff154e7..ab88991 100644 --- a/src/screens/auth/RegisterScreen.tsx +++ b/src/screens/auth/RegisterScreen.tsx @@ -25,7 +25,7 @@ import { import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { MaterialCommunityIcons } from '@expo/vector-icons'; -import { useAppColors, type AppColors } from '../../theme'; +import { useAppColors, useResolvedColorScheme, type AppColors } from '../../theme'; import { authService, resolveAuthApiError } from '../../services/authService'; import { useAuthStore } from '../../stores'; import { @@ -58,6 +58,7 @@ const STEP_COMPONENTS = [ export const RegisterScreen: React.FC = () => { const colors = useAppColors(); + const resolved = useResolvedColorScheme(); const styles = useMemo(() => createRegisterStyles(colors), [colors]); const router = useRouter(); const register = useAuthStore((state) => state.register); @@ -232,7 +233,7 @@ export const RegisterScreen: React.FC = () => { return ( - + = ({ formData, updateFormData, @@ -91,7 +85,7 @@ export const RegisterStep1Email: React.FC = ({ )} @@ -145,7 +139,7 @@ function createStyles(colors: AppColors) { inputWrapper: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 14, paddingHorizontal: 18, height: 56, @@ -173,7 +167,7 @@ function createStyles(colors: AppColors) { actionButton: { height: 56, borderRadius: 14, - backgroundColor: '#FF6B35', + backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', marginTop: 8, @@ -184,7 +178,7 @@ function createStyles(colors: AppColors) { actionButtonText: { fontSize: 17, fontWeight: '600', - color: '#FFFFFF', + color: colors.text.inverse, }, }); } diff --git a/src/screens/auth/RegisterStep2Verification.tsx b/src/screens/auth/RegisterStep2Verification.tsx index cb914f9..42eff63 100644 --- a/src/screens/auth/RegisterStep2Verification.tsx +++ b/src/screens/auth/RegisterStep2Verification.tsx @@ -1,8 +1,3 @@ -/** - * 注册步骤2:填写验证码 - * 使用 VerificationCodeInput 组件 - */ - import React, { useState } from 'react'; import { View, @@ -15,12 +10,6 @@ import { useAppColors, type AppColors } from '../../theme'; import { VerificationCodeInput } from '../../components/common'; import { RegisterStepProps } from './types'; -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - export const RegisterStep2Verification: React.FC = ({ formData, updateFormData, @@ -175,13 +164,13 @@ function createStyles(colors: AppColors) { }, resendText: { fontSize: 14, - color: THEME_COLORS.primary, + color: colors.primary.main, fontWeight: '500', }, actionButton: { height: 56, borderRadius: 14, - backgroundColor: '#FF6B35', + backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', width: '100%', @@ -193,7 +182,7 @@ function createStyles(colors: AppColors) { actionButtonText: { fontSize: 17, fontWeight: '600', - color: '#FFFFFF', + color: colors.text.inverse, }, backButton: { flexDirection: 'row', diff --git a/src/screens/auth/RegisterStep3Profile.tsx b/src/screens/auth/RegisterStep3Profile.tsx index 06db465..e8d8451 100644 --- a/src/screens/auth/RegisterStep3Profile.tsx +++ b/src/screens/auth/RegisterStep3Profile.tsx @@ -1,8 +1,3 @@ -/** - * 注册步骤3:设置用户信息 - * 包含用户名、昵称、密码等 - */ - import React, { useState } from 'react'; import { View, @@ -18,12 +13,6 @@ import { useAppColors, type AppColors } from '../../theme'; import { RegisterStepProps } from './types'; import * as hrefs from '../../navigation/hrefs'; -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - export const RegisterStep3Profile: React.FC = ({ formData, updateFormData, @@ -208,7 +197,7 @@ export const RegisterStep3Profile: React.FC = ({ @@ -288,7 +277,7 @@ function createStyles(colors: AppColors) { inputWrapper: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 14, paddingHorizontal: 18, height: 52, @@ -331,13 +320,13 @@ function createStyles(colors: AppColors) { lineHeight: 22, }, termsLink: { - color: '#FF6B35', + color: colors.primary.main, fontWeight: '500', }, actionButton: { height: 56, borderRadius: 14, - backgroundColor: '#FF6B35', + backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', }, @@ -347,7 +336,7 @@ function createStyles(colors: AppColors) { actionButtonText: { fontSize: 17, fontWeight: '600', - color: '#FFFFFF', + color: colors.text.inverse, }, backButton: { flexDirection: 'row', diff --git a/src/screens/auth/VerificationFormScreen.tsx b/src/screens/auth/VerificationFormScreen.tsx index 0140981..dd55aef 100644 --- a/src/screens/auth/VerificationFormScreen.tsx +++ b/src/screens/auth/VerificationFormScreen.tsx @@ -26,12 +26,6 @@ import { showPrompt } from '../../services/promptService'; import { uploadService } from '../../services/uploadService'; import type { UserIdentity } from '../../types/dto'; -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - const IDENTITY_CONFIG: Record = { student: { title: '在校学生', idField: 'student_id', idLabel: '学号' }, teacher: { title: '教职工', idField: 'employee_id', idLabel: '工号' }, @@ -230,7 +224,7 @@ export const VerificationFormScreen: React.FC = () => { 请填写真实信息,上传清晰的证明材料照片,审核通过后将获得认证标识 @@ -303,7 +297,7 @@ export const VerificationFormScreen: React.FC = () => { disabled={isLoading || uploading} > {(isLoading || uploading) ? ( - + ) : ( 提交认证申请 )} @@ -317,7 +311,7 @@ function createStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, }, header: { flexDirection: 'row', @@ -349,7 +343,7 @@ function createStyles(colors: AppColors) { infoCard: { flexDirection: 'row', alignItems: 'flex-start', - backgroundColor: '#FFF5F0', + backgroundColor: colors.primary.main + '15', borderRadius: 12, padding: 16, marginBottom: 24, @@ -374,7 +368,7 @@ function createStyles(colors: AppColors) { marginBottom: 8, }, required: { - color: THEME_COLORS.primary, + color: colors.primary.main, }, hintText: { fontSize: 13, @@ -382,7 +376,7 @@ function createStyles(colors: AppColors) { marginBottom: 8, }, inputWrapper: { - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 12, paddingHorizontal: 16, height: 50, @@ -404,7 +398,7 @@ function createStyles(colors: AppColors) { marginTop: 6, }, uploadButton: { - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 12, overflow: 'hidden', minHeight: 200, @@ -433,7 +427,7 @@ function createStyles(colors: AppColors) { submitButton: { height: 56, borderRadius: 14, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, justifyContent: 'center', alignItems: 'center', }, @@ -443,7 +437,7 @@ function createStyles(colors: AppColors) { submitButtonText: { fontSize: 17, fontWeight: '600', - color: '#FFFFFF', + color: colors.text.inverse, }, }); } diff --git a/src/screens/auth/VerificationGuideScreen.tsx b/src/screens/auth/VerificationGuideScreen.tsx index 1711f2f..0d3b70f 100644 --- a/src/screens/auth/VerificationGuideScreen.tsx +++ b/src/screens/auth/VerificationGuideScreen.tsx @@ -1,8 +1,3 @@ -/** - * 注册后身份认证引导页面 - * 可跳过的认证界面,引导用户完成身份认证 - */ - import React, { useState } from 'react'; import { View, @@ -10,7 +5,6 @@ import { StyleSheet, TouchableOpacity, ScrollView, - Image, Alert, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -19,12 +13,6 @@ import { MaterialCommunityIcons } from '@expo/vector-icons'; import { useAppColors, type AppColors } from '../../theme'; import * as hrefs from '../../navigation/hrefs'; -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - // 身份类型选项 const IDENTITY_OPTIONS = [ { @@ -98,7 +86,7 @@ export const VerificationGuideScreen: React.FC = () => { @@ -112,7 +100,7 @@ export const VerificationGuideScreen: React.FC = () => { )} @@ -132,7 +120,7 @@ export const VerificationGuideScreen: React.FC = () => { 身份认证 @@ -147,7 +135,7 @@ export const VerificationGuideScreen: React.FC = () => { 获得认证标识 @@ -155,7 +143,7 @@ export const VerificationGuideScreen: React.FC = () => { 解锁专属频道 @@ -163,7 +151,7 @@ export const VerificationGuideScreen: React.FC = () => { 加入社群圈子 @@ -198,7 +186,7 @@ function createStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, }, scrollContent: { flexGrow: 1, @@ -214,7 +202,7 @@ function createStyles(colors: AppColors) { width: 100, height: 100, borderRadius: 50, - backgroundColor: '#FFF5F0', + backgroundColor: colors.primary.main + '15', justifyContent: 'center', alignItems: 'center', marginBottom: 20, @@ -258,15 +246,15 @@ function createStyles(colors: AppColors) { flexDirection: 'row', alignItems: 'center', padding: 16, - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 14, marginBottom: 12, borderWidth: 2, borderColor: 'transparent', }, identityCardSelected: { - backgroundColor: '#FFF5F0', - borderColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main + '15', + borderColor: colors.primary.main, }, identityIconContainer: { width: 48, @@ -285,7 +273,7 @@ function createStyles(colors: AppColors) { marginBottom: 4, }, identityTitleSelected: { - color: THEME_COLORS.primary, + color: colors.primary.main, }, identityDescription: { fontSize: 13, @@ -303,17 +291,17 @@ function createStyles(colors: AppColors) { continueButton: { height: 56, borderRadius: 14, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, justifyContent: 'center', alignItems: 'center', }, continueButtonDisabled: { - backgroundColor: '#ccc', + backgroundColor: colors.background.disabled, }, continueButtonText: { fontSize: 17, fontWeight: '600', - color: '#fff', + color: colors.text.inverse, }, skipButton: { height: 56, diff --git a/src/screens/message/ChatScreen.tsx b/src/screens/message/ChatScreen.tsx index c9525dd..b737cc3 100644 --- a/src/screens/message/ChatScreen.tsx +++ b/src/screens/message/ChatScreen.tsx @@ -33,7 +33,7 @@ import { useNavigation, useRouter } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { Text, ImageGallery, ImageGridItem } from '../../components/common'; -import { useAppColors } from '../../theme'; +import { useAppColors, useResolvedColorScheme } from '../../theme'; import * as hrefs from '../../navigation/hrefs'; import { messageManager, callStore } from '../../stores'; import { useBreakpointGTE } from '../../hooks/useResponsive'; @@ -56,6 +56,7 @@ export const ChatScreen: React.FC = () => { const router = useRouter(); const insets = useSafeAreaInsets(); const colors = useAppColors(); + const resolved = useResolvedColorScheme(); const styles = useChatScreenStyles(); // 响应式布局 @@ -406,7 +407,7 @@ export const ChatScreen: React.FC = () => { behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0} > - + {/* 顶部栏 */} { > void }> = ({ onBack }) => { const colors = useAppColors(); + const resolved = useResolvedColorScheme(); const styles = useMemo(() => createNotificationsStyles(colors), [colors]); const isFocused = useIsFocused(); const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount(); @@ -424,9 +418,9 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack if (!isHydrated) { return ( - + - + ); @@ -434,7 +428,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack return ( - + void }> = ({ onBack {/* 消息列表 */} {isLoading && messages.length === 0 ? ( - + ) : ( void }> = ({ onBack } /> @@ -580,7 +574,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack {/* 消息列表 */} {isLoading && messages.length === 0 ? ( - + ) : ( void }> = ({ onBack } /> @@ -617,7 +611,7 @@ function createNotificationsStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, }, content: { flex: 1, @@ -629,7 +623,7 @@ function createNotificationsStyles(colors: AppColors) { justifyContent: 'space-between', paddingHorizontal: 20, paddingVertical: 16, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, }, headerWide: { paddingHorizontal: 32, @@ -650,7 +644,7 @@ function createNotificationsStyles(colors: AppColors) { fontSize: 20, }, unreadBadge: { - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, borderRadius: 10, paddingHorizontal: 6, paddingVertical: 2, @@ -660,7 +654,7 @@ function createNotificationsStyles(colors: AppColors) { justifyContent: 'center', }, unreadBadgeText: { - color: '#fff', + color: colors.text.inverse, fontSize: 11, fontWeight: '600', }, @@ -674,7 +668,7 @@ function createNotificationsStyles(colors: AppColors) { width: 40, height: 40, borderRadius: 20, - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, justifyContent: 'center', alignItems: 'center', }, @@ -686,7 +680,7 @@ function createNotificationsStyles(colors: AppColors) { flexDirection: 'row', paddingHorizontal: 16, paddingVertical: 12, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, borderBottomWidth: 1, borderBottomColor: colors.divider || '#E5E5EA', }, @@ -702,7 +696,7 @@ function createNotificationsStyles(colors: AppColors) { paddingVertical: 8, marginRight: 8, borderRadius: 14, - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderWidth: 1, borderColor: 'transparent', }, @@ -712,15 +706,15 @@ function createNotificationsStyles(colors: AppColors) { marginRight: 10, }, filterTagActive: { - backgroundColor: THEME_COLORS.primary, - borderColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, + borderColor: colors.primary.main, }, filterTagTextActive: { fontWeight: '600', }, filterCount: { marginLeft: 6, - backgroundColor: THEME_COLORS.primaryLight + '40', + backgroundColor: colors.primary.light + '40', paddingHorizontal: 6, paddingVertical: 1, borderRadius: 8, @@ -770,7 +764,7 @@ function createNotificationsStyles(colors: AppColors) { width: 120, height: 120, borderRadius: 60, - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, justifyContent: 'center', alignItems: 'center', marginBottom: 24, diff --git a/src/screens/profile/AccountDeletionScreen.tsx b/src/screens/profile/AccountDeletionScreen.tsx index ff8e35f..5069b1a 100644 --- a/src/screens/profile/AccountDeletionScreen.tsx +++ b/src/screens/profile/AccountDeletionScreen.tsx @@ -21,13 +21,6 @@ import * as hrefs from '../../navigation/hrefs'; const CONTENT_MAX_WIDTH = 720; -// 胡萝卜橙主题色 -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - function createAccountDeletionStyles(colors: AppColors) { return StyleSheet.create({ container: { @@ -114,7 +107,7 @@ function createAccountDeletionStyles(colors: AppColors) { }, // 输入框 - 扁平化风格 input: { - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 14, padding: spacing.md, fontSize: 16, @@ -133,12 +126,12 @@ function createAccountDeletionStyles(colors: AppColors) { flex: 1, height: 56, borderRadius: 14, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', }, primaryButtonText: { - color: '#fff', + color: colors.text.inverse, fontSize: fontSizes.md, fontWeight: '600', }, @@ -166,7 +159,7 @@ function createAccountDeletionStyles(colors: AppColors) { justifyContent: 'center', }, dangerButtonText: { - color: '#fff', + color: colors.text.inverse, fontSize: fontSizes.md, fontWeight: '600', }, @@ -392,9 +385,9 @@ export const AccountDeletionScreen: React.FC = () => { onPress={handleRequestDeletion} disabled={submitting || !password.trim()} > - {submitting ? ( - - ) : ( + {submitting ? ( + + ) : ( 确认注销 )} diff --git a/src/screens/profile/AccountSecurityScreen.tsx b/src/screens/profile/AccountSecurityScreen.tsx index 70262ba..907a924 100644 --- a/src/screens/profile/AccountSecurityScreen.tsx +++ b/src/screens/profile/AccountSecurityScreen.tsx @@ -16,13 +16,6 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks'; // 内容最大宽度 const CONTENT_MAX_WIDTH = 720; -// 胡萝卜橙主题色 -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - import { authService, resolveAuthApiError } from '../../services/authService'; import { showPrompt } from '../../services/promptService'; import { useAuthStore } from '../../stores'; @@ -263,9 +256,9 @@ export const AccountSecurityScreen: React.FC = () => { onPress={handleSendCode} disabled={sendingCode || countdown > 0} > - {sendingCode ? ( - - ) : ( + {sendingCode ? ( + + ) : ( {countdown > 0 ? `${countdown}s` : '发送验证码'} )} @@ -278,7 +271,7 @@ export const AccountSecurityScreen: React.FC = () => { disabled={verifyingEmail} > {verifyingEmail ? ( - + ) : ( 验证邮箱 )} @@ -352,7 +345,7 @@ export const AccountSecurityScreen: React.FC = () => { disabled={sendingChangePwdCode || changePwdCountdown > 0} > {sendingChangePwdCode ? ( - + ) : ( {changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'} @@ -368,7 +361,7 @@ export const AccountSecurityScreen: React.FC = () => { disabled={updatingPassword} > {updatingPassword ? ( - + ) : ( 更新密码 )} @@ -432,26 +425,26 @@ function createAccountSecurityStyles(colors: AppColors) { borderRadius: borderRadius.sm, }, statusVerified: { - backgroundColor: '#E8F5E9', + backgroundColor: colors.success.light + '30', }, statusUnverified: { - backgroundColor: '#FFF3E0', + backgroundColor: colors.warning.light + '30', }, statusText: { fontSize: 13, fontWeight: '600', }, statusVerifiedText: { - color: '#2E7D32', + color: colors.success.dark, }, statusUnverifiedText: { - color: '#E65100', + color: colors.warning.dark, }, // 输入框样式 - 扁平化风格,与登录页一致 inputWrapper: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 14, paddingHorizontal: spacing.lg, height: 56, @@ -485,13 +478,13 @@ function createAccountSecurityStyles(colors: AppColors) { height: 56, minWidth: 110, borderRadius: 14, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', paddingHorizontal: spacing.sm, }, sendCodeButtonText: { - color: '#fff', + color: colors.text.inverse, fontSize: fontSizes.sm, fontWeight: '600', }, @@ -499,14 +492,14 @@ function createAccountSecurityStyles(colors: AppColors) { primaryButton: { height: 56, borderRadius: 14, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, alignItems: 'center', justifyContent: 'center', marginTop: spacing.xs, marginHorizontal: spacing['2xl'], }, primaryButtonText: { - color: '#fff', + color: colors.text.inverse, fontSize: fontSizes.md, fontWeight: '600', }, diff --git a/src/screens/profile/NotificationSettingsScreen.tsx b/src/screens/profile/NotificationSettingsScreen.tsx index 68bcc5d..1aca70f 100644 --- a/src/screens/profile/NotificationSettingsScreen.tsx +++ b/src/screens/profile/NotificationSettingsScreen.tsx @@ -10,6 +10,8 @@ import { StyleSheet, Switch, ScrollView, + Alert, + TouchableOpacity, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; @@ -22,6 +24,7 @@ import { setVibrationPreference, } from '../../services/notificationPreferences'; import { useResponsive, useResponsiveSpacing } from '../../hooks'; +import { backgroundSyncManager, BackgroundSyncMode } from '../../services/BackgroundSyncManager'; // 内容最大宽度 const CONTENT_MAX_WIDTH = 720; @@ -41,6 +44,7 @@ export const NotificationSettingsScreen: React.FC = () => { const [vibrationEnabled, setVibrationEnabledState] = useState(true); const [pushEnabled, setPushEnabled] = useState(true); const [soundEnabled, setSoundEnabled] = useState(true); + const [syncMode, setSyncMode] = useState(BackgroundSyncMode.BATTERY_SAVER); const { isWideScreen, isMobile } = useResponsive(); const insets = useSafeAreaInsets(); @@ -55,6 +59,7 @@ export const NotificationSettingsScreen: React.FC = () => { setVibrationEnabledState(prefs.vibrationEnabled); setPushEnabled(prefs.pushEnabled); setSoundEnabled(prefs.soundEnabled); + setSyncMode(backgroundSyncManager.getMode()); } catch (error) { console.error('加载通知设置失败:', error); } @@ -89,6 +94,51 @@ export const NotificationSettingsScreen: React.FC = () => { } }; + const handleSyncModeChange = async (mode: BackgroundSyncMode) => { + if (mode === BackgroundSyncMode.REALTIME) { + Alert.alert( + '实时模式', + '实时模式会在通知栏显示常驻通知以保持应用后台运行,确保消息即时同步。\n\n' + + '建议在系统设置中将应用加入电池优化白名单以获得最佳效果。\n\n' + + '注意:此模式可能增加电池消耗。', + [ + { text: '取消', style: 'cancel' }, + { + text: '开启', + onPress: async () => { + await backgroundSyncManager.setMode(mode); + setSyncMode(mode); + }, + }, + ] + ); + } else { + await backgroundSyncManager.setMode(mode); + setSyncMode(mode); + } + }; + + const syncModeOptions: { mode: BackgroundSyncMode; title: string; subtitle: string; icon: string }[] = [ + { + mode: BackgroundSyncMode.BATTERY_SAVER, + title: '省电模式', + subtitle: '系统后台任务,每 15 分钟检查一次', + icon: 'leaf', + }, + { + mode: BackgroundSyncMode.REALTIME, + title: '实时模式', + subtitle: '通知栏常驻保活,即时同步消息', + icon: 'lightning-bolt', + }, + { + mode: BackgroundSyncMode.DISABLED, + title: '禁用', + subtitle: '仅在应用打开时接收消息', + icon: 'close-circle-outline', + }, + ]; + const settings: NotificationSettingItem[] = [ { key: 'push', @@ -164,6 +214,62 @@ export const NotificationSettingsScreen: React.FC = () => { 关闭推送通知后,您将不再收到任何消息提醒,但消息仍会在应用内显示。 + + {/* 后台同步模式 */} + + + + 后台同步模式 + + + {syncModeOptions.map((option) => ( + handleSyncModeChange(option.mode)} + activeOpacity={0.7} + > + + + + + + {option.title} + + + {option.subtitle} + + + {syncMode === option.mode && ( + + )} + + ))} + + + {/* 实时模式说明 */} + {syncMode === BackgroundSyncMode.REALTIME && ( + + + + 实时模式已开启,应用将在通知栏显示常驻通知以保持后台运行。如需更稳定的后台运行,请在系统设置中将本应用加入电池优化白名单。 + + + )} ); @@ -210,6 +316,9 @@ function createNotificationSettingsStyles(colors: AppColors) { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.divider, }, + settingItemActive: { + backgroundColor: colors.primary.main + '0D', + }, iconContainer: { width: 24, height: 24, diff --git a/src/screens/profile/PrivacySettingsScreen.tsx b/src/screens/profile/PrivacySettingsScreen.tsx index ab0c82d..0ae3a08 100644 --- a/src/screens/profile/PrivacySettingsScreen.tsx +++ b/src/screens/profile/PrivacySettingsScreen.tsx @@ -18,13 +18,6 @@ import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto'; const CONTENT_MAX_WIDTH = 720; -// 胡萝卜橙主题色 -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - const VISIBILITY_OPTIONS: { value: VisibilityLevel; label: string; description: string }[] = [ { value: 'everyone', label: '所有人可见', description: '任何人都可查看' }, { value: 'following', label: '仅关注的人可见', description: '只有你关注的人可查看' }, @@ -74,7 +67,7 @@ function createPrivacySettingsStyles(colors: AppColors) { }, // 隐私设置项 - 扁平化卡片风格 item: { - backgroundColor: '#F5F5F7', + backgroundColor: colors.background.default, borderRadius: 14, padding: spacing.lg, marginHorizontal: spacing['2xl'], @@ -111,15 +104,15 @@ function createPrivacySettingsStyles(colors: AppColors) { backgroundColor: colors.background.paper, }, optionButtonActive: { - backgroundColor: THEME_COLORS.primary, - borderColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, + borderColor: colors.primary.main, }, optionText: { fontSize: 13, color: colors.text.primary, }, optionTextActive: { - color: '#fff', + color: colors.text.inverse, fontWeight: '500', }, }); diff --git a/src/screens/profile/VerificationSettingsScreen.tsx b/src/screens/profile/VerificationSettingsScreen.tsx index 79b3aae..77cee0b 100644 --- a/src/screens/profile/VerificationSettingsScreen.tsx +++ b/src/screens/profile/VerificationSettingsScreen.tsx @@ -26,12 +26,6 @@ import { import { showPrompt } from '../../services/promptService'; import * as hrefs from '../../navigation/hrefs'; -const THEME_COLORS = { - primary: '#FF6B35', - primaryLight: '#FF8C5A', - primaryDark: '#E55A2B', -}; - const STATUS_CONFIG = { not_submitted: { title: '未认证', @@ -99,7 +93,7 @@ export const VerificationSettingsScreen: React.FC = () => { return ( - + ); @@ -110,10 +104,11 @@ export const VerificationSettingsScreen: React.FC = () => { const identityText = IDENTITY_TEXT[status?.identity || 'general']; return ( - + + {/* 状态卡片 */} @@ -128,7 +123,7 @@ export const VerificationSettingsScreen: React.FC = () => { {status?.verification_status === 'approved' && ( - + {identityText} )} @@ -153,7 +148,7 @@ export const VerificationSettingsScreen: React.FC = () => { @@ -169,7 +164,7 @@ export const VerificationSettingsScreen: React.FC = () => { @@ -185,7 +180,7 @@ export const VerificationSettingsScreen: React.FC = () => { @@ -208,7 +203,7 @@ export const VerificationSettingsScreen: React.FC = () => { onPress={handleStartVerification} activeOpacity={0.9} > - + {status?.verification_status === 'rejected' ? '重新认证' : '开始认证'} @@ -221,7 +216,7 @@ export const VerificationSettingsScreen: React.FC = () => { onPress={handleViewRecords} activeOpacity={0.8} > - + 查看认证记录 )} @@ -262,6 +257,7 @@ export const VerificationSettingsScreen: React.FC = () => { + ); }; @@ -269,7 +265,7 @@ function createStyles(colors: AppColors) { return StyleSheet.create({ container: { flex: 1, - backgroundColor: '#fff', + backgroundColor: colors.background.paper, }, loadingContainer: { flex: 1, @@ -307,7 +303,7 @@ function createStyles(colors: AppColors) { alignItems: 'center', paddingVertical: 32, paddingHorizontal: 24, - backgroundColor: '#F9F9F9', + backgroundColor: colors.background.default, borderRadius: 16, marginBottom: 24, }, @@ -334,7 +330,7 @@ function createStyles(colors: AppColors) { identityBadge: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#FFF5F0', + backgroundColor: colors.primary.main + '15', borderRadius: 20, paddingVertical: 8, paddingHorizontal: 16, @@ -343,25 +339,25 @@ function createStyles(colors: AppColors) { identityText: { fontSize: 14, fontWeight: '600', - color: THEME_COLORS.primary, + color: colors.primary.main, marginLeft: 6, }, rejectReasonContainer: { marginTop: 16, padding: 12, - backgroundColor: '#FFF3F3', + backgroundColor: colors.error.light + '20', borderRadius: 8, width: '100%', }, rejectReasonLabel: { fontSize: 13, fontWeight: '600', - color: '#F44336', + color: colors.error.main, marginBottom: 4, }, rejectReasonText: { fontSize: 13, - color: '#D32F2F', + color: colors.error.dark, lineHeight: 18, }, benefitsSection: { @@ -379,7 +375,7 @@ function createStyles(colors: AppColors) { benefitItem: { flexDirection: 'row', alignItems: 'flex-start', - backgroundColor: '#F9F9F9', + backgroundColor: colors.background.default, borderRadius: 12, padding: 16, }, @@ -387,7 +383,7 @@ function createStyles(colors: AppColors) { width: 48, height: 48, borderRadius: 12, - backgroundColor: '#FFF5F0', + backgroundColor: colors.primary.main + '15', justifyContent: 'center', alignItems: 'center', marginRight: 12, @@ -415,12 +411,12 @@ function createStyles(colors: AppColors) { justifyContent: 'center', height: 56, borderRadius: 14, - backgroundColor: THEME_COLORS.primary, + backgroundColor: colors.primary.main, }, primaryButtonText: { fontSize: 17, fontWeight: '600', - color: '#FFF', + color: colors.text.inverse, marginLeft: 8, }, secondaryButton: { @@ -429,18 +425,18 @@ function createStyles(colors: AppColors) { justifyContent: 'center', height: 56, borderRadius: 14, - backgroundColor: '#FFF5F0', + backgroundColor: colors.primary.main + '15', borderWidth: 1, - borderColor: THEME_COLORS.primary, + borderColor: colors.primary.main, }, secondaryButtonText: { fontSize: 17, fontWeight: '600', - color: THEME_COLORS.primary, + color: colors.primary.main, marginLeft: 8, }, verifiedInfo: { - backgroundColor: '#F9F9F9', + backgroundColor: colors.background.default, borderRadius: 12, padding: 16, }, diff --git a/src/services/BackgroundSyncManager.ts b/src/services/BackgroundSyncManager.ts new file mode 100644 index 0000000..c97823f --- /dev/null +++ b/src/services/BackgroundSyncManager.ts @@ -0,0 +1,284 @@ +/** + * 后台同步管理器 + * + * 整合前台服务、后台同步、消息状态管理 + * 参考 Element Android 的 BackgroundSyncStarter 设计 + */ + +import { AppState, AppStateStatus, Platform } from 'react-native'; +import { ForegroundServiceModule } from './ForegroundServiceModule'; +import { wsService } from './wsService'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +/** + * 后台同步模式 + */ +export enum BackgroundSyncMode { + /** 省电模式:使用 expo-background-task(最小15分钟间隔) */ + BATTERY_SAVER = 'battery_saver', + + /** 实时模式:前台服务保活 + 持续同步 */ + REALTIME = 'realtime', + + /** 禁用后台同步 */ + DISABLED = 'disabled', +} + +const STORAGE_KEY = 'background_sync_mode'; +const SYNC_INTERVAL_MS = 30 * 1000; // 30 秒同步间隔 + +class BackgroundSyncManager { + private mode: BackgroundSyncMode = BackgroundSyncMode.BATTERY_SAVER; + private appStateSubscription: ReturnType | null = null; + private syncInterval: ReturnType | null = null; + private isInitialized: boolean = false; + private lastSyncAt: number = 0; + private syncMessagesCallback: (() => Promise) | null = null; + + /** + * 设置消息同步回调函数 + * 由外部注入,避免循环依赖 + */ + setSyncMessagesCallback(callback: () => Promise): void { + this.syncMessagesCallback = callback; + } + + /** + * 初始化后台同步 + */ + async initialize(): Promise { + if (this.isInitialized) return; + + // 加载保存的模式 + const savedMode = await this.loadMode(); + this.mode = savedMode; + + // 监听 AppState 变化 + this.appStateSubscription = AppState.addEventListener( + 'change', + this.handleAppStateChange + ); + + // 监听 WebSocket 断开 + this.setupWSDisconnectListener(); + + this.isInitialized = true; + console.log('[BackgroundSyncManager] 初始化完成, 模式:', this.mode); + } + + /** + * 切换后台同步模式 + */ + async setMode(mode: BackgroundSyncMode): Promise { + // 如果当前在后台,先停止旧模式的保活 + if (AppState.currentState !== 'active') { + this.stopPeriodicSync(); + if (this.mode === BackgroundSyncMode.REALTIME) { + await ForegroundServiceModule.stop(); + } + } + + // 更新模式 + this.mode = mode; + await this.saveMode(mode); + + // 根据新模式立即执行相应操作 + if (mode === BackgroundSyncMode.REALTIME) { + // 切换到实时模式:立即启动前台服务(无论前台还是后台) + const started = await ForegroundServiceModule.start({ + title: '萝卜社区', + body: '正在后台同步消息', + }); + if (started) { + this.startPeriodicSync(); + } + } else { + // 切换到其他模式:立即停止前台服务和定时同步 + this.stopPeriodicSync(); + await ForegroundServiceModule.stop(); + } + + console.log('[BackgroundSyncManager] 模式已切换为:', mode); + } + + /** + * 获取当前模式 + */ + getMode(): BackgroundSyncMode { + return this.mode; + } + + /** + * AppState 变化处理 + */ + private handleAppStateChange = async (nextState: AppStateStatus) => { + console.log('[BackgroundSyncManager] AppState 变化:', nextState); + + if (nextState === 'background' || nextState === 'inactive') { + // 进入后台 + await this.startForBackground(); + } else if (nextState === 'active') { + // 回到前台 + await this.stopForForeground(); + } + }; + + /** + * 进入后台时的处理 + */ + private async startForBackground(): Promise { + if (this.mode === BackgroundSyncMode.DISABLED) return; + + console.log('[BackgroundSyncManager] 进入后台'); + + if (this.mode === BackgroundSyncMode.REALTIME) { + // 确保前台服务正在运行 + await ForegroundServiceModule.start({ + title: '萝卜社区', + body: '正在后台同步消息', + }); + + // 启动定时同步 + this.startPeriodicSync(); + } + } + + /** + * 回到前台时的处理 + */ + private async stopForForeground(): Promise { + console.log('[BackgroundSyncManager] 回到前台'); + + // 停止定时同步(前台通过 WebSocket 实时同步,不需要轮询) + this.stopPeriodicSync(); + + // 实时模式下:前台服务保持运行,只更新通知文字 + if (this.mode === BackgroundSyncMode.REALTIME) { + await ForegroundServiceModule.update('萝卜社区', '正在后台同步消息'); + } + + // 立即同步一次(获取离线消息) + await this.syncMessages(); + } + + /** + * 启动定时同步 + */ + private startPeriodicSync(): void { + this.stopPeriodicSync(); + + // 立即同步一次 + this.syncMessages(); + + // 启动定时器 + this.syncInterval = setInterval(() => { + this.syncMessages(); + }, SYNC_INTERVAL_MS); + + console.log('[BackgroundSyncManager] 定时同步已启动'); + } + + /** + * 停止定时同步 + */ + private stopPeriodicSync(): void { + if (this.syncInterval) { + clearInterval(this.syncInterval); + this.syncInterval = null; + console.log('[BackgroundSyncManager] 定时同步已停止'); + } + } + + /** + * 设置 WebSocket 断开监听 + */ + private setupWSDisconnectListener(): void { + // 监听 WebSocket 断开事件 + wsService.onDisconnect(() => { + if (AppState.currentState !== 'active' && this.mode === BackgroundSyncMode.REALTIME) { + console.log('[BackgroundSyncManager] WebSocket 断开,尝试重连'); + wsService.connect().catch((e) => { + console.error('[BackgroundSyncManager] WebSocket 重连失败:', e); + }); + } + }); + } + + /** + * 执行消息同步 + */ + async syncMessages(): Promise { + const now = Date.now(); + + // 防止短时间内重复同步(最小 10 秒间隔) + if (now - this.lastSyncAt < 10000) { + return; + } + + this.lastSyncAt = now; + + // 调用外部注入的同步回调 + if (this.syncMessagesCallback) { + try { + await this.syncMessagesCallback(); + } catch (error) { + console.error('[BackgroundSyncManager] 同步失败:', error); + } + } + } + + /** + * 更新前台服务通知内容 + */ + async updateNotification(totalUnread: number): Promise { + if (this.mode !== BackgroundSyncMode.REALTIME) return; + + if (totalUnread > 0) { + await ForegroundServiceModule.update( + '萝卜社区', + `您有 ${totalUnread} 条未读消息` + ); + } else { + await ForegroundServiceModule.update( + '萝卜社区', + '正在后台同步消息' + ); + } + } + + /** + * 保存模式到存储 + */ + private async saveMode(mode: BackgroundSyncMode): Promise { + try { + await AsyncStorage.setItem(STORAGE_KEY, mode); + } catch (error) { + console.error('[BackgroundSyncManager] 保存模式失败:', error); + } + } + + /** + * 从存储加载模式 + */ + private async loadMode(): Promise { + try { + const saved = await AsyncStorage.getItem(STORAGE_KEY); + return (saved as BackgroundSyncMode) || BackgroundSyncMode.BATTERY_SAVER; + } catch (error) { + return BackgroundSyncMode.BATTERY_SAVER; + } + } + + /** + * 清理资源 + */ + async cleanup(): Promise { + this.stopPeriodicSync(); + this.appStateSubscription?.remove(); + this.appStateSubscription = null; + await ForegroundServiceModule.stop(); + this.isInitialized = false; + } +} + +export const backgroundSyncManager = new BackgroundSyncManager(); diff --git a/src/services/ForegroundServiceModule.ts b/src/services/ForegroundServiceModule.ts new file mode 100644 index 0000000..3bb3a9f --- /dev/null +++ b/src/services/ForegroundServiceModule.ts @@ -0,0 +1,102 @@ +/** + * 前台服务模块 + * + * 提供 React Native 层对 Android 前台服务的控制接口 + * 仅 Android 平台有效 + */ + +import { NativeModules, Platform } from 'react-native'; + +const { ForegroundService } = NativeModules; + +export interface ForegroundServiceOptions { + title?: string; + body?: string; +} + +/** + * 前台服务管理器 + * + * 使用方式: + * - 前台服务启动后,会在通知栏显示一个常驻通知 + * - 服务会防止应用进程被系统杀死 + * - 类似 V2Ray 和 Element Android 的保活机制 + */ +export const ForegroundServiceModule = { + /** + * 启动前台服务 + * @param options 通知配置 + */ + async start(options: ForegroundServiceOptions = {}): Promise { + if (Platform.OS !== 'android') { + console.log('[ForegroundService] iOS 不支持前台服务保活'); + return false; + } + + if (!ForegroundService) { + console.error('[ForegroundService] 原生模块未注册'); + return false; + } + + try { + const result = await ForegroundService.start( + options.title || '萝卜社区', + options.body || '正在后台同步消息' + ); + console.log('[ForegroundService] 服务已启动'); + return result; + } catch (error) { + console.error('[ForegroundService] 启动失败:', error); + return false; + } + }, + + /** + * 停止前台服务 + */ + async stop(): Promise { + if (Platform.OS !== 'android') { + return false; + } + + if (!ForegroundService) { + return false; + } + + try { + const result = await ForegroundService.stop(); + console.log('[ForegroundService] 服务已停止'); + return result; + } catch (error) { + console.error('[ForegroundService] 停止失败:', error); + return false; + } + }, + + /** + * 更新通知内容 + */ + async update(title: string, body: string): Promise { + if (Platform.OS !== 'android') { + return false; + } + + if (!ForegroundService) { + return false; + } + + try { + return await ForegroundService.update(title, body); + } catch (error) { + console.error('[ForegroundService] 更新失败:', error); + return false; + } + }, + + /** + * 检查是否为 Android 平台 + */ + isSupported(): boolean { + return Platform.OS === 'android' && !!ForegroundService; + }, +}; diff --git a/src/services/backgroundService.ts b/src/services/backgroundService.ts index 53f9575..d7da22e 100644 --- a/src/services/backgroundService.ts +++ b/src/services/backgroundService.ts @@ -1,107 +1,92 @@ /** * 后台保活服务 - * 使用 expo-background-fetch 和 expo-task-manager 实现 App 后台保活 * - * 功能: - * - 定期后台任务保持 App 在内存中 - * - 配合 SSE 服务保持长连接 - * - 收到消息时触发震动提示 + * 整合前台服务和后台同步 + * - REALTIME 模式:使用 Android 前台服务保活(通知栏常驻) + * - BATTERY_SAVER 模式:使用 expo-background-task 定期同步(最小15分钟间隔) + * + * 参考:Element Android 的 GuardAndroidService + BackgroundSyncStarter */ -import { AppState, AppStateStatus, Platform } from 'react-native'; -import * as BackgroundFetch from 'expo-background-fetch'; +import { Platform } from 'react-native'; +import * as BackgroundTask from 'expo-background-task'; import * as TaskManager from 'expo-task-manager'; -import { wsService } from './wsService'; import { - triggerVibration, - vibrateOnMessage, - setVibrationConfig, - getVibrationConfig, - setVibrationEnabled, -} from './messageVibrationService'; -export { triggerVibration, vibrateOnMessage, setVibrationConfig, getVibrationConfig, setVibrationEnabled }; - -// 后台任务名称 -const BACKGROUND_FETCH_TASK = 'background-fetch-keepalive'; -const REALTIME_KEEPALIVE_TASK = 'realtime-keepalive'; - -// 后台任务间隔(Android 最小 15 分钟,iOS 最小 15 分钟) -const BACKGROUND_INTERVAL = 15; // 15 分钟 - -// 后台服务状态 -let isInitialized = false; -let appStateSubscription: any = null; - -// 导入 api 用于检查认证状态 + backgroundSyncManager, + BackgroundSyncMode, +} from './BackgroundSyncManager'; +import { messageService } from './messageService'; import { api } from './api'; -// 定义后台任务 -TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { +// 后台任务名称 +const BACKGROUND_SYNC_TASK = 'background-sync-task'; + +// 后台任务间隔(分钟) +const BACKGROUND_INTERVAL_MINUTES = 15; + +// 服务状态 +let isInitialized = false; + +// 定义后台任务(使用 expo-background-task) +TaskManager.defineTask(BACKGROUND_SYNC_TASK, async () => { try { // 检查是否已认证 const token = await api.getToken(); if (!token) { - return BackgroundFetch.BackgroundFetchResult.NoData; + return BackgroundTask.BackgroundTaskResult.Success; } - - // 检查 SSE 连接状态 - if (!wsService.isConnected()) { - await wsService.connect(); - } - - // 返回收到新数据 - return BackgroundFetch.BackgroundFetchResult.NewData; + + // 执行同步 + await syncMessages(); + + return BackgroundTask.BackgroundTaskResult.Success; } catch (error) { console.error('[BackgroundService] 后台任务执行失败:', error); - return BackgroundFetch.BackgroundFetchResult.Failed; - } -}); - -// SSE 保活任务 -TaskManager.defineTask(REALTIME_KEEPALIVE_TASK, async () => { - try { - // 检查是否已认证 - const token = await api.getToken(); - if (!token) { - return BackgroundFetch.BackgroundFetchResult.NoData; - } - - if (!wsService.isConnected()) { - await wsService.connect(); - } - return BackgroundFetch.BackgroundFetchResult.NewData; - } catch (error) { - console.error('[BackgroundService] SSE 保活失败:', error); - return BackgroundFetch.BackgroundFetchResult.Failed; + return BackgroundTask.BackgroundTaskResult.Failed; } }); /** - * 注册后台任务 + * 执行消息同步 */ -async function registerBackgroundTasks(): Promise { +async function syncMessages(): Promise { + try { + // 同步未读数 + const unreadData = await messageService.getUnreadCount(); + const totalUnread = unreadData?.total_unread_count ?? 0; + + // 更新前台服务通知 + await backgroundSyncManager.updateNotification(totalUnread); + + console.log('[BackgroundService] 同步完成, 未读数:', totalUnread); + } catch (error) { + console.error('[BackgroundService] 同步失败:', error); + } +} + +/** + * 注册后台任务(expo-background-task) + */ +async function registerBackgroundTask(): Promise { if (Platform.OS === 'web') { return; } + try { - // 检查是否已注册 - const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK); - if (!isRegistered) { - await BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, { - minimumInterval: BACKGROUND_INTERVAL * 60, // 转换为秒 - stopOnTerminate: false, // App 终止后继续运行 - startOnBoot: true, // 设备启动后自动运行 - }); + // 检查后台任务状态 + const status = await BackgroundTask.getStatusAsync(); + if (status !== BackgroundTask.BackgroundTaskStatus.Available) { + console.warn('[BackgroundService] 后台任务不可用,状态:', status); + return; } - // 注册 SSE 保活任务 - const isSseKeepaliveRegistered = await TaskManager.isTaskRegisteredAsync(REALTIME_KEEPALIVE_TASK); - if (!isSseKeepaliveRegistered) { - await BackgroundFetch.registerTaskAsync(REALTIME_KEEPALIVE_TASK, { - minimumInterval: 60, // 1 分钟检查一次 - stopOnTerminate: false, - startOnBoot: true, + // 检查是否已注册 + const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_SYNC_TASK); + if (!isRegistered) { + await BackgroundTask.registerTaskAsync(BACKGROUND_SYNC_TASK, { + minimumInterval: BACKGROUND_INTERVAL_MINUTES, // 以分钟为单位 }); + console.log('[BackgroundService] 后台任务已注册'); } } catch (error) { console.error('[BackgroundService] 注册后台任务失败:', error); @@ -111,41 +96,22 @@ async function registerBackgroundTasks(): Promise { /** * 取消后台任务 */ -async function unregisterBackgroundTasks(): Promise { +async function unregisterBackgroundTask(): Promise { if (Platform.OS === 'web') { return; } + try { - await BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK); - await BackgroundFetch.unregisterTaskAsync(REALTIME_KEEPALIVE_TASK); + const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_SYNC_TASK); + if (isRegistered) { + await BackgroundTask.unregisterTaskAsync(BACKGROUND_SYNC_TASK); + console.log('[BackgroundService] 后台任务已取消'); + } } catch (error) { console.error('[BackgroundService] 取消后台任务失败:', error); } } -/** - * 设置 App 状态监听 - */ -function setupAppStateListener(): void { - if (appStateSubscription) { - return; - } - - appStateSubscription = AppState.addEventListener('change', async (nextAppState: AppStateStatus) => { - if (nextAppState === 'active') { - // 检查是否已认证 - const token = await api.getToken(); - if (!token) { - return; - } - // App 回到前台,确保连接 - if (!wsService.isConnected()) { - wsService.connect(); - } - } - }); -} - /** * 初始化后台保活服务 */ @@ -155,26 +121,22 @@ export async function initBackgroundService(): Promise { } if (Platform.OS === 'web') { - setupAppStateListener(); isInitialized = true; return true; } try { - // 检查后台任务状态 - const status = await BackgroundFetch.getStatusAsync(); - if (status !== BackgroundFetch.BackgroundFetchStatus.Available) { - console.warn('[BackgroundService] 后台任务不可用,状态:', status); - // 即使后台任务不可用,也继续初始化其他功能 - } + // 设置同步回调 + backgroundSyncManager.setSyncMessagesCallback(syncMessages); - // 注册后台任务 - await registerBackgroundTasks(); + // 初始化后台同步管理器 + await backgroundSyncManager.initialize(); - // 设置 App 状态监听 - setupAppStateListener(); + // 注册 expo-background-task 任务(用于 BATTERY_SAVER 模式) + await registerBackgroundTask(); isInitialized = true; + console.log('[BackgroundService] 初始化完成'); return true; } catch (error) { console.error('[BackgroundService] 初始化失败:', error); @@ -187,14 +149,10 @@ export async function initBackgroundService(): Promise { */ export async function stopBackgroundService(): Promise { try { - await unregisterBackgroundTasks(); - - if (appStateSubscription) { - appStateSubscription.remove(); - appStateSubscription = null; - } - + await unregisterBackgroundTask(); + await backgroundSyncManager.cleanup(); isInitialized = false; + console.log('[BackgroundService] 服务已停止'); } catch (error) { console.error('[BackgroundService] 停止服务失败:', error); } @@ -207,34 +165,74 @@ export async function checkBackgroundStatus(): Promise<{ isAvailable: boolean; isInitialized: boolean; registeredTasks: string[]; + mode: BackgroundSyncMode; }> { if (Platform.OS === 'web') { return { isAvailable: false, isInitialized, registeredTasks: [], + mode: backgroundSyncManager.getMode(), }; } - const status = await BackgroundFetch.getStatusAsync(); + + const status = await BackgroundTask.getStatusAsync(); const registeredTasks = await TaskManager.getRegisteredTasksAsync(); return { - isAvailable: status === BackgroundFetch.BackgroundFetchStatus.Available, + isAvailable: status === BackgroundTask.BackgroundTaskStatus.Available, isInitialized, - registeredTasks: registeredTasks.map(task => task.taskName), + registeredTasks: registeredTasks.map((task) => task.taskName), + mode: backgroundSyncManager.getMode(), }; } -// 导出服务实例 -export const backgroundService = { - init: initBackgroundService, - stop: stopBackgroundService, - vibrate: triggerVibration, +/** + * 设置后台同步模式 + */ +export async function setBackgroundSyncMode(mode: BackgroundSyncMode): Promise { + await backgroundSyncManager.setMode(mode); +} + +/** + * 获取当前后台同步模式 + */ +export function getBackgroundSyncMode(): BackgroundSyncMode { + return backgroundSyncManager.getMode(); +} + +/** + * 手动触发同步 + */ +export async function syncNow(): Promise { + await syncMessages(); +} + +// 导出震动相关功能(保持向后兼容) +export { + triggerVibration, vibrateOnMessage, setVibrationConfig, getVibrationConfig, setVibrationEnabled, +} from './messageVibrationService'; + +// 重新导出类型 +export { BackgroundSyncMode }; + +// 后台服务实例 +export const backgroundService = { + init: initBackgroundService, + stop: stopBackgroundService, + setMode: setBackgroundSyncMode, + getMode: getBackgroundSyncMode, + syncNow, checkStatus: checkBackgroundStatus, + // 震动相关 + vibrate: async () => { + const { triggerVibration } = await import('./messageVibrationService'); + triggerVibration(); + }, }; export default backgroundService;