feat(service): add foreground service for background message sync
Implement Android foreground service to keep app alive in background for real-time message sync: - Add withForegroundService expo config plugin for notification channel setup - Add BackgroundSyncManager with REALTIME, BATTERY_SAVER, and DISABLED modes - Add ForegroundServiceModule for native Android foreground service binding - Refactor backgroundService to integrate foreground service and expo-background-task Also refactor auth and profile screens to use dynamic theme colors: - Replace hardcoded THEME_COLORS with colors.primary.main, colors.background.paper, etc. - Add useResolvedColorScheme for dynamic StatusBar styling - Update NotificationSettingsScreen with sync mode selection UI BREAKING CHANGE: Switched from expo-background-fetch to expo-background-task, minimum background sync interval changed from 900s to 15min in config
This commit is contained in:
18
app.json
18
app.json
@@ -2,7 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "萝卜社区",
|
"name": "萝卜社区",
|
||||||
"slug": "qojo",
|
"slug": "qojo",
|
||||||
"version": "1.0.12",
|
"version": "1.0.13",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"predictiveBackGestureEnabled": false,
|
"predictiveBackGestureEnabled": false,
|
||||||
"package": "skin.carrot.bbs",
|
"package": "skin.carrot.bbs",
|
||||||
"versionCode": 6,
|
"versionCode": 7,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"VIBRATE",
|
"VIBRATE",
|
||||||
"RECORD_AUDIO",
|
"RECORD_AUDIO",
|
||||||
@@ -63,6 +63,16 @@
|
|||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"./plugins/withMainActivityConfigChange",
|
"./plugins/withMainActivityConfigChange",
|
||||||
|
[
|
||||||
|
"./plugins/withForegroundService",
|
||||||
|
{
|
||||||
|
"notificationTitle": "萝卜社区",
|
||||||
|
"notificationBody": "正在后台同步消息",
|
||||||
|
"notificationIcon": "ic_notification",
|
||||||
|
"channelId": "carrot_sync_foreground",
|
||||||
|
"channelName": "消息同步"
|
||||||
|
}
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"expo-router",
|
"expo-router",
|
||||||
{
|
{
|
||||||
@@ -86,9 +96,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"expo-background-fetch",
|
"expo-background-task",
|
||||||
{
|
{
|
||||||
"minimumInterval": 900
|
"minimumInterval": 15
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "carrot_bbs",
|
"name": "carrot_bbs",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"expo": "~55.0.4",
|
"expo": "~55.0.4",
|
||||||
"expo-background-fetch": "~55.0.10",
|
|
||||||
"expo-background-task": "~55.0.10",
|
"expo-background-task": "~55.0.10",
|
||||||
"expo-camera": "^55.0.10",
|
"expo-camera": "^55.0.10",
|
||||||
"expo-constants": "~55.0.7",
|
"expo-constants": "~55.0.7",
|
||||||
|
|||||||
467
plugins/withForegroundService.js
Normal file
467
plugins/withForegroundService.js
Normal file
@@ -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<NativeModule> {
|
||||||
|
return listOf(ForegroundServiceModule(reactContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成通知图标 XML
|
||||||
|
*/
|
||||||
|
function generateNotificationIcon() {
|
||||||
|
return `<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<!-- 背景圆 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#4CAF50"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
|
||||||
|
<!-- 对勾符号 -->
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
||||||
|
</vector>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = withForegroundService;
|
||||||
@@ -125,12 +125,12 @@ function createSystemMessageStyles(colors: AppColors) {
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.divider || '#E5E5EA',
|
borderBottomColor: colors.divider || '#E5E5EA',
|
||||||
},
|
},
|
||||||
unreadContainer: {
|
unreadContainer: {
|
||||||
backgroundColor: '#FFF8F6',
|
backgroundColor: colors.primary.main + '0A',
|
||||||
},
|
},
|
||||||
unreadIndicator: {
|
unreadIndicator: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@@ -140,7 +140,7 @@ function createSystemMessageStyles(colors: AppColors) {
|
|||||||
width: 8,
|
width: 8,
|
||||||
height: 8,
|
height: 8,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
backgroundColor: '#FF6B35',
|
backgroundColor: colors.primary.main,
|
||||||
},
|
},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
marginRight: 14,
|
marginRight: 14,
|
||||||
@@ -165,7 +165,7 @@ function createSystemMessageStyles(colors: AppColors) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: '#fff',
|
borderColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -220,7 +220,7 @@ function createSystemMessageStyles(colors: AppColors) {
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
@@ -318,13 +318,6 @@ const getSystemMessageTitle = (message: SystemMessageResponse): string => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 胡萝卜橙主题色
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
const SystemMessageItem: React.FC<SystemMessageItemProps> = ({
|
||||||
message,
|
message,
|
||||||
onPress,
|
onPress,
|
||||||
|
|||||||
@@ -26,19 +26,13 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { authService, resolveAuthApiError } from '../../services/authService';
|
||||||
import { showPrompt } from '../../services/promptService';
|
import { showPrompt } from '../../services/promptService';
|
||||||
|
|
||||||
// 胡萝卜橙主题色
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ForgotPasswordScreen: React.FC = () => {
|
export const ForgotPasswordScreen: React.FC = () => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
|
const resolved = useResolvedColorScheme();
|
||||||
const styles = useMemo(() => createForgotPasswordStyles(colors), [colors]);
|
const styles = useMemo(() => createForgotPasswordStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -156,7 +150,7 @@ export const ForgotPasswordScreen: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
style={styles.keyboardView}
|
style={styles.keyboardView}
|
||||||
@@ -198,7 +192,7 @@ export const ForgotPasswordScreen: React.FC = () => {
|
|||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
/>
|
/>
|
||||||
{email.length > 0 && validateEmail(email) && (
|
{email.length > 0 && validateEmail(email) && (
|
||||||
<MaterialCommunityIcons name="check-circle" size={20} color={THEME_COLORS.primary} style={styles.checkIcon} />
|
<MaterialCommunityIcons name="check-circle" size={20} color={colors.primary.main} style={styles.checkIcon} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -225,7 +219,7 @@ export const ForgotPasswordScreen: React.FC = () => {
|
|||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
{sendingCode ? (
|
{sendingCode ? (
|
||||||
<ActivityIndicator size="small" color={THEME_COLORS.primary} />
|
<ActivityIndicator size="small" color={colors.primary.main} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '获取验证码'}</Text>
|
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '获取验证码'}</Text>
|
||||||
)}
|
)}
|
||||||
@@ -295,7 +289,7 @@ export const ForgotPasswordScreen: React.FC = () => {
|
|||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.submitButtonText}>重置密码</Text>
|
<Text style={styles.submitButtonText}>重置密码</Text>
|
||||||
)}
|
)}
|
||||||
@@ -307,7 +301,7 @@ export const ForgotPasswordScreen: React.FC = () => {
|
|||||||
onPress={() => router.back()}
|
onPress={() => router.back()}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name="arrow-left" size={18} color={THEME_COLORS.primary} style={{ marginRight: 4 }} />
|
<MaterialCommunityIcons name="arrow-left" size={18} color={colors.primary.main} style={{ marginRight: 4 }} />
|
||||||
<Text style={styles.backButtonText}>返回登录</Text>
|
<Text style={styles.backButtonText}>返回登录</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -322,7 +316,7 @@ function createForgotPasswordStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
keyboardView: {
|
keyboardView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -354,7 +348,7 @@ function createForgotPasswordStyles(colors: AppColors) {
|
|||||||
underline: {
|
underline: {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 4,
|
height: 4,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
@@ -382,7 +376,7 @@ function createForgotPasswordStyles(colors: AppColors) {
|
|||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
paddingHorizontal: 18,
|
paddingHorizontal: 18,
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -412,7 +406,7 @@ function createForgotPasswordStyles(colors: AppColors) {
|
|||||||
sendCodeButton: {
|
sendCodeButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
backgroundColor: 'rgba(255, 107, 53, 0.1)',
|
backgroundColor: colors.primary.light + '20',
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -424,14 +418,14 @@ function createForgotPasswordStyles(colors: AppColors) {
|
|||||||
sendCodeButtonText: {
|
sendCodeButtonText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 提交按钮
|
// 提交按钮
|
||||||
submitButton: {
|
submitButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
@@ -440,7 +434,7 @@ function createForgotPasswordStyles(colors: AppColors) {
|
|||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
},
|
},
|
||||||
submitButtonText: {
|
submitButtonText: {
|
||||||
color: '#fff',
|
color: colors.text.inverse,
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
@@ -453,7 +447,7 @@ function createForgotPasswordStyles(colors: AppColors) {
|
|||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
},
|
},
|
||||||
backButtonText: {
|
backButtonText: {
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,21 +26,17 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { useAppColors, type AppColors } from '../../theme';
|
import { useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
|
||||||
import { useAuthStore } from '../../stores';
|
import { useAuthStore } from '../../stores';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { useResponsive } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
import { showPrompt } from '../../services/promptService';
|
import { showPrompt } from '../../services/promptService';
|
||||||
|
|
||||||
// 胡萝卜橙主题色
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LoginScreen: React.FC = () => {
|
export const LoginScreen: React.FC = () => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
|
const resolved = useResolvedColorScheme();
|
||||||
const styles = useMemo(() => createLoginStyles(colors), [colors]);
|
const styles = useMemo(() => createLoginStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const login = useAuthStore((state) => state.login);
|
const login = useAuthStore((state) => state.login);
|
||||||
@@ -129,7 +125,7 @@ export const LoginScreen: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
style={styles.keyboardView}
|
style={styles.keyboardView}
|
||||||
@@ -175,7 +171,7 @@ export const LoginScreen: React.FC = () => {
|
|||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
/>
|
/>
|
||||||
{username.length > 0 && (
|
{username.length > 0 && (
|
||||||
<MaterialCommunityIcons name="check-circle" size={20} color={THEME_COLORS.primary} style={styles.checkIcon} />
|
<MaterialCommunityIcons name="check-circle" size={20} color={colors.primary.main} style={styles.checkIcon} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -218,17 +214,17 @@ export const LoginScreen: React.FC = () => {
|
|||||||
|
|
||||||
{/* 服务条款勾选 */}
|
{/* 服务条款勾选 */}
|
||||||
<View style={styles.termsContainer}>
|
<View style={styles.termsContainer}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => setAgreedToTerms(!agreedToTerms)}
|
onPress={() => setAgreedToTerms(!agreedToTerms)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
style={styles.checkboxWrapper}
|
style={styles.checkboxWrapper}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={agreedToTerms ? 'checkbox-marked' : 'checkbox-blank-outline'}
|
name={agreedToTerms ? 'checkbox-marked' : 'checkbox-blank-outline'}
|
||||||
size={22}
|
size={22}
|
||||||
color={agreedToTerms ? THEME_COLORS.primary : colors.text.hint}
|
color={agreedToTerms ? colors.primary.main : colors.text.hint}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.termsText}>
|
<Text style={styles.termsText}>
|
||||||
我已阅读并同意
|
我已阅读并同意
|
||||||
<Text
|
<Text
|
||||||
@@ -255,7 +251,7 @@ export const LoginScreen: React.FC = () => {
|
|||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator size="small" color="#FFF" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.loginButtonText}>登 录</Text>
|
<Text style={styles.loginButtonText}>登 录</Text>
|
||||||
)}
|
)}
|
||||||
@@ -282,7 +278,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
keyboardView: {
|
keyboardView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -317,7 +313,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
underline: {
|
underline: {
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 4,
|
height: 4,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
marginTop: 12,
|
marginTop: 12,
|
||||||
marginBottom: 16,
|
marginBottom: 16,
|
||||||
@@ -345,7 +341,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
paddingHorizontal: 18,
|
paddingHorizontal: 18,
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -372,7 +368,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
forgotPasswordText: {
|
forgotPasswordText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -380,7 +376,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
loginButton: {
|
loginButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
@@ -390,7 +386,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
loginButtonText: {
|
loginButtonText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#FFFFFF',
|
color: colors.text.inverse,
|
||||||
},
|
},
|
||||||
|
|
||||||
// 底部
|
// 底部
|
||||||
@@ -406,7 +402,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
registerLink: {
|
registerLink: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
},
|
},
|
||||||
@@ -419,7 +415,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
color: colors.text.hint,
|
color: colors.text.hint,
|
||||||
},
|
},
|
||||||
policyLink: {
|
policyLink: {
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
termsContainer: {
|
termsContainer: {
|
||||||
@@ -439,7 +435,7 @@ function createLoginStyles(colors: AppColors) {
|
|||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
},
|
},
|
||||||
termsLink: {
|
termsLink: {
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
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 { authService, resolveAuthApiError } from '../../services/authService';
|
||||||
import { useAuthStore } from '../../stores';
|
import { useAuthStore } from '../../stores';
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +58,7 @@ const STEP_COMPONENTS = [
|
|||||||
|
|
||||||
export const RegisterScreen: React.FC = () => {
|
export const RegisterScreen: React.FC = () => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
|
const resolved = useResolvedColorScheme();
|
||||||
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
|
const styles = useMemo(() => createRegisterStyles(colors), [colors]);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const register = useAuthStore((state) => state.register);
|
const register = useAuthStore((state) => state.register);
|
||||||
@@ -232,7 +233,7 @@ export const RegisterScreen: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
style={styles.keyboardView}
|
style={styles.keyboardView}
|
||||||
@@ -295,7 +296,7 @@ function createRegisterStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
keyboardView: {
|
keyboardView: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -316,7 +317,7 @@ function createRegisterStyles(colors: AppColors) {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
@@ -346,7 +347,7 @@ function createRegisterStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
loginLink: {
|
loginLink: {
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: '#FF6B35',
|
color: colors.primary.main,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|||||||
import { useAppColors, type AppColors } from '../../theme';
|
import { useAppColors, type AppColors } from '../../theme';
|
||||||
import { RegisterStepProps } from './types';
|
import { RegisterStepProps } from './types';
|
||||||
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RegisterStep1Email: React.FC<RegisterStepProps> = ({
|
export const RegisterStep1Email: React.FC<RegisterStepProps> = ({
|
||||||
formData,
|
formData,
|
||||||
updateFormData,
|
updateFormData,
|
||||||
@@ -91,7 +85,7 @@ export const RegisterStep1Email: React.FC<RegisterStepProps> = ({
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="check-circle"
|
name="check-circle"
|
||||||
size={20}
|
size={20}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
style={styles.checkIcon}
|
style={styles.checkIcon}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -145,7 +139,7 @@ function createStyles(colors: AppColors) {
|
|||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
paddingHorizontal: 18,
|
paddingHorizontal: 18,
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -173,7 +167,7 @@ function createStyles(colors: AppColors) {
|
|||||||
actionButton: {
|
actionButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: '#FF6B35',
|
backgroundColor: colors.primary.main,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
@@ -184,7 +178,7 @@ function createStyles(colors: AppColors) {
|
|||||||
actionButtonText: {
|
actionButtonText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#FFFFFF',
|
color: colors.text.inverse,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* 注册步骤2:填写验证码
|
|
||||||
* 使用 VerificationCodeInput 组件
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
@@ -15,12 +10,6 @@ import { useAppColors, type AppColors } from '../../theme';
|
|||||||
import { VerificationCodeInput } from '../../components/common';
|
import { VerificationCodeInput } from '../../components/common';
|
||||||
import { RegisterStepProps } from './types';
|
import { RegisterStepProps } from './types';
|
||||||
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RegisterStep2Verification: React.FC<RegisterStepProps> = ({
|
export const RegisterStep2Verification: React.FC<RegisterStepProps> = ({
|
||||||
formData,
|
formData,
|
||||||
updateFormData,
|
updateFormData,
|
||||||
@@ -175,13 +164,13 @@ function createStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
resendText: {
|
resendText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
actionButton: {
|
actionButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: '#FF6B35',
|
backgroundColor: colors.primary.main,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -193,7 +182,7 @@ function createStyles(colors: AppColors) {
|
|||||||
actionButtonText: {
|
actionButtonText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#FFFFFF',
|
color: colors.text.inverse,
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* 注册步骤3:设置用户信息
|
|
||||||
* 包含用户名、昵称、密码等
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
@@ -18,12 +13,6 @@ import { useAppColors, type AppColors } from '../../theme';
|
|||||||
import { RegisterStepProps } from './types';
|
import { RegisterStepProps } from './types';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RegisterStep3Profile: React.FC<RegisterStepProps> = ({
|
export const RegisterStep3Profile: React.FC<RegisterStepProps> = ({
|
||||||
formData,
|
formData,
|
||||||
updateFormData,
|
updateFormData,
|
||||||
@@ -208,7 +197,7 @@ export const RegisterStep3Profile: React.FC<RegisterStepProps> = ({
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={agreedToTerms ? 'checkbox-marked' : 'checkbox-blank-outline'}
|
name={agreedToTerms ? 'checkbox-marked' : 'checkbox-blank-outline'}
|
||||||
size={22}
|
size={22}
|
||||||
color={agreedToTerms ? THEME_COLORS.primary : colors.text?.hint || '#999'}
|
color={agreedToTerms ? colors.primary.main : colors.text?.hint || '#999'}
|
||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.termsText}>
|
<Text style={styles.termsText}>
|
||||||
@@ -288,7 +277,7 @@ function createStyles(colors: AppColors) {
|
|||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
paddingHorizontal: 18,
|
paddingHorizontal: 18,
|
||||||
height: 52,
|
height: 52,
|
||||||
@@ -331,13 +320,13 @@ function createStyles(colors: AppColors) {
|
|||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
},
|
},
|
||||||
termsLink: {
|
termsLink: {
|
||||||
color: '#FF6B35',
|
color: colors.primary.main,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
actionButton: {
|
actionButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: '#FF6B35',
|
backgroundColor: colors.primary.main,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
@@ -347,7 +336,7 @@ function createStyles(colors: AppColors) {
|
|||||||
actionButtonText: {
|
actionButtonText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#FFFFFF',
|
color: colors.text.inverse,
|
||||||
},
|
},
|
||||||
backButton: {
|
backButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ import { showPrompt } from '../../services/promptService';
|
|||||||
import { uploadService } from '../../services/uploadService';
|
import { uploadService } from '../../services/uploadService';
|
||||||
import type { UserIdentity } from '../../types/dto';
|
import type { UserIdentity } from '../../types/dto';
|
||||||
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
const IDENTITY_CONFIG: Record<string, { title: string; idField: string; idLabel: string }> = {
|
const IDENTITY_CONFIG: Record<string, { title: string; idField: string; idLabel: string }> = {
|
||||||
student: { title: '在校学生', idField: 'student_id', idLabel: '学号' },
|
student: { title: '在校学生', idField: 'student_id', idLabel: '学号' },
|
||||||
teacher: { title: '教职工', idField: 'employee_id', idLabel: '工号' },
|
teacher: { title: '教职工', idField: 'employee_id', idLabel: '工号' },
|
||||||
@@ -230,7 +224,7 @@ export const VerificationFormScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="information-outline"
|
name="information-outline"
|
||||||
size={20}
|
size={20}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.infoText}>
|
<Text style={styles.infoText}>
|
||||||
请填写真实信息,上传清晰的证明材料照片,审核通过后将获得认证标识
|
请填写真实信息,上传清晰的证明材料照片,审核通过后将获得认证标识
|
||||||
@@ -303,7 +297,7 @@ export const VerificationFormScreen: React.FC = () => {
|
|||||||
disabled={isLoading || uploading}
|
disabled={isLoading || uploading}
|
||||||
>
|
>
|
||||||
{(isLoading || uploading) ? (
|
{(isLoading || uploading) ? (
|
||||||
<ActivityIndicator size="small" color="#FFF" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.submitButtonText}>提交认证申请</Text>
|
<Text style={styles.submitButtonText}>提交认证申请</Text>
|
||||||
)}
|
)}
|
||||||
@@ -317,7 +311,7 @@ function createStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -349,7 +343,7 @@ function createStyles(colors: AppColors) {
|
|||||||
infoCard: {
|
infoCard: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
backgroundColor: '#FFF5F0',
|
backgroundColor: colors.primary.main + '15',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
@@ -374,7 +368,7 @@ function createStyles(colors: AppColors) {
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
required: {
|
required: {
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
},
|
},
|
||||||
hintText: {
|
hintText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -382,7 +376,7 @@ function createStyles(colors: AppColors) {
|
|||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
height: 50,
|
height: 50,
|
||||||
@@ -404,7 +398,7 @@ function createStyles(colors: AppColors) {
|
|||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
},
|
},
|
||||||
uploadButton: {
|
uploadButton: {
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
minHeight: 200,
|
minHeight: 200,
|
||||||
@@ -433,7 +427,7 @@ function createStyles(colors: AppColors) {
|
|||||||
submitButton: {
|
submitButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
@@ -443,7 +437,7 @@ function createStyles(colors: AppColors) {
|
|||||||
submitButtonText: {
|
submitButtonText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#FFFFFF',
|
color: colors.text.inverse,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
/**
|
|
||||||
* 注册后身份认证引导页面
|
|
||||||
* 可跳过的认证界面,引导用户完成身份认证
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
@@ -10,7 +5,6 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
Image,
|
|
||||||
Alert,
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
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 { useAppColors, type AppColors } from '../../theme';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
// 身份类型选项
|
// 身份类型选项
|
||||||
const IDENTITY_OPTIONS = [
|
const IDENTITY_OPTIONS = [
|
||||||
{
|
{
|
||||||
@@ -98,7 +86,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name={option.icon as any}
|
name={option.icon as any}
|
||||||
size={32}
|
size={32}
|
||||||
color={isSelected ? THEME_COLORS.primary : colors.text?.secondary || '#666'}
|
color={isSelected ? colors.primary.main : colors.text?.secondary || '#666'}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.identityContent}>
|
<View style={styles.identityContent}>
|
||||||
@@ -112,7 +100,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="check-circle"
|
name="check-circle"
|
||||||
size={24}
|
size={24}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -132,7 +120,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="shield-check-outline"
|
name="shield-check-outline"
|
||||||
size={48}
|
size={48}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.title}>身份认证</Text>
|
<Text style={styles.title}>身份认证</Text>
|
||||||
@@ -147,7 +135,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="check-decagram"
|
name="check-decagram"
|
||||||
size={20}
|
size={20}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.benefitText}>获得认证标识</Text>
|
<Text style={styles.benefitText}>获得认证标识</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -155,7 +143,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="lock-open-outline"
|
name="lock-open-outline"
|
||||||
size={20}
|
size={20}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.benefitText}>解锁专属频道</Text>
|
<Text style={styles.benefitText}>解锁专属频道</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -163,7 +151,7 @@ export const VerificationGuideScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="account-group-outline"
|
name="account-group-outline"
|
||||||
size={20}
|
size={20}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.benefitText}>加入社群圈子</Text>
|
<Text style={styles.benefitText}>加入社群圈子</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -198,7 +186,7 @@ function createStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
@@ -214,7 +202,7 @@ function createStyles(colors: AppColors) {
|
|||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
borderRadius: 50,
|
borderRadius: 50,
|
||||||
backgroundColor: '#FFF5F0',
|
backgroundColor: colors.primary.main + '15',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
@@ -258,15 +246,15 @@ function createStyles(colors: AppColors) {
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 16,
|
padding: 16,
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
},
|
},
|
||||||
identityCardSelected: {
|
identityCardSelected: {
|
||||||
backgroundColor: '#FFF5F0',
|
backgroundColor: colors.primary.main + '15',
|
||||||
borderColor: THEME_COLORS.primary,
|
borderColor: colors.primary.main,
|
||||||
},
|
},
|
||||||
identityIconContainer: {
|
identityIconContainer: {
|
||||||
width: 48,
|
width: 48,
|
||||||
@@ -285,7 +273,7 @@ function createStyles(colors: AppColors) {
|
|||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
identityTitleSelected: {
|
identityTitleSelected: {
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
},
|
},
|
||||||
identityDescription: {
|
identityDescription: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -303,17 +291,17 @@ function createStyles(colors: AppColors) {
|
|||||||
continueButton: {
|
continueButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
continueButtonDisabled: {
|
continueButtonDisabled: {
|
||||||
backgroundColor: '#ccc',
|
backgroundColor: colors.background.disabled,
|
||||||
},
|
},
|
||||||
continueButtonText: {
|
continueButtonText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#fff',
|
color: colors.text.inverse,
|
||||||
},
|
},
|
||||||
skipButton: {
|
skipButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { useNavigation, useRouter } from 'expo-router';
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
|
import { Text, ImageGallery, ImageGridItem } from '../../components/common';
|
||||||
import { useAppColors } from '../../theme';
|
import { useAppColors, useResolvedColorScheme } from '../../theme';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
import { messageManager, callStore } from '../../stores';
|
import { messageManager, callStore } from '../../stores';
|
||||||
import { useBreakpointGTE } from '../../hooks/useResponsive';
|
import { useBreakpointGTE } from '../../hooks/useResponsive';
|
||||||
@@ -56,6 +56,7 @@ export const ChatScreen: React.FC = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
|
const resolved = useResolvedColorScheme();
|
||||||
const styles = useChatScreenStyles();
|
const styles = useChatScreenStyles();
|
||||||
|
|
||||||
// 响应式布局
|
// 响应式布局
|
||||||
@@ -406,7 +407,7 @@ export const ChatScreen: React.FC = () => {
|
|||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
||||||
>
|
>
|
||||||
<StatusBar style="dark" backgroundColor="#FFFFFF" />
|
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} backgroundColor={colors.background.paper} />
|
||||||
|
|
||||||
{/* 顶部栏 */}
|
{/* 顶部栏 */}
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
@@ -499,7 +500,7 @@ export const ChatScreen: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
backgroundColor: colors.background.paper + 'E6',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
|||||||
import { useIsFocused } from '@react-navigation/native';
|
import { useIsFocused } from '@react-navigation/native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
import { spacing, borderRadius, useAppColors, type AppColors } from '../../theme';
|
import { spacing, borderRadius, useAppColors, useResolvedColorScheme, type AppColors } from '../../theme';
|
||||||
import { SystemMessageResponse } from '../../types/dto';
|
import { SystemMessageResponse } from '../../types/dto';
|
||||||
import { messageService } from '../../services/messageService';
|
import { messageService } from '../../services/messageService';
|
||||||
import { commentService } from '../../services/commentService';
|
import { commentService } from '../../services/commentService';
|
||||||
@@ -49,18 +49,12 @@ const MESSAGE_TYPES = [
|
|||||||
{ key: 'announcement', title: '公告' },
|
{ key: 'announcement', title: '公告' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 胡萝卜橙主题色
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'favorite_post']);
|
const LIKE_SYSTEM_TYPES = new Set(['like_post', 'like_comment', 'like_reply', 'favorite_post']);
|
||||||
const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']);
|
const GROUP_SYSTEM_TYPES = new Set(['group_invite', 'group_join_apply', 'group_join_approved', 'group_join_rejected']);
|
||||||
|
|
||||||
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
|
export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack }) => {
|
||||||
const colors = useAppColors();
|
const colors = useAppColors();
|
||||||
|
const resolved = useResolvedColorScheme();
|
||||||
const styles = useMemo(() => createNotificationsStyles(colors), [colors]);
|
const styles = useMemo(() => createNotificationsStyles(colors), [colors]);
|
||||||
const isFocused = useIsFocused();
|
const isFocused = useIsFocused();
|
||||||
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
const { setSystemUnreadCount, decrementSystemUnreadCount } = useMessageManagerSystemUnreadCount();
|
||||||
@@ -424,9 +418,9 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
if (!isHydrated) {
|
if (!isHydrated) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
@@ -434,7 +428,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor="#fff" />
|
<StatusBar barStyle={resolved === 'dark' ? 'light-content' : 'dark-content'} backgroundColor={colors.background.paper} />
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.content,
|
styles.content,
|
||||||
@@ -499,7 +493,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
{/* 消息列表 */}
|
{/* 消息列表 */}
|
||||||
{isLoading && messages.length === 0 ? (
|
{isLoading && messages.length === 0 ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
@@ -519,8 +513,8 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
colors={[THEME_COLORS.primary]}
|
colors={[colors.primary.main]}
|
||||||
tintColor={THEME_COLORS.primary}
|
tintColor={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -580,7 +574,7 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
{/* 消息列表 */}
|
{/* 消息列表 */}
|
||||||
{isLoading && messages.length === 0 ? (
|
{isLoading && messages.length === 0 ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
@@ -600,8 +594,8 @@ export const NotificationsScreen: React.FC<{ onBack?: () => void }> = ({ onBack
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
colors={[THEME_COLORS.primary]}
|
colors={[colors.primary.main]}
|
||||||
tintColor={THEME_COLORS.primary}
|
tintColor={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -617,7 +611,7 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -629,7 +623,7 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingVertical: 16,
|
paddingVertical: 16,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
headerWide: {
|
headerWide: {
|
||||||
paddingHorizontal: 32,
|
paddingHorizontal: 32,
|
||||||
@@ -650,7 +644,7 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
},
|
},
|
||||||
unreadBadge: {
|
unreadBadge: {
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
@@ -660,7 +654,7 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
unreadBadgeText: {
|
unreadBadgeText: {
|
||||||
color: '#fff',
|
color: colors.text.inverse,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
@@ -674,7 +668,7 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
@@ -686,7 +680,7 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: colors.divider || '#E5E5EA',
|
borderBottomColor: colors.divider || '#E5E5EA',
|
||||||
},
|
},
|
||||||
@@ -702,7 +696,7 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
},
|
},
|
||||||
@@ -712,15 +706,15 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
filterTagActive: {
|
filterTagActive: {
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
borderColor: THEME_COLORS.primary,
|
borderColor: colors.primary.main,
|
||||||
},
|
},
|
||||||
filterTagTextActive: {
|
filterTagTextActive: {
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
filterCount: {
|
filterCount: {
|
||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
backgroundColor: THEME_COLORS.primaryLight + '40',
|
backgroundColor: colors.primary.light + '40',
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
paddingVertical: 1,
|
paddingVertical: 1,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
@@ -770,7 +764,7 @@ function createNotificationsStyles(colors: AppColors) {
|
|||||||
width: 120,
|
width: 120,
|
||||||
height: 120,
|
height: 120,
|
||||||
borderRadius: 60,
|
borderRadius: 60,
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
|
|||||||
@@ -21,13 +21,6 @@ import * as hrefs from '../../navigation/hrefs';
|
|||||||
|
|
||||||
const CONTENT_MAX_WIDTH = 720;
|
const CONTENT_MAX_WIDTH = 720;
|
||||||
|
|
||||||
// 胡萝卜橙主题色
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
function createAccountDeletionStyles(colors: AppColors) {
|
function createAccountDeletionStyles(colors: AppColors) {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
@@ -114,7 +107,7 @@ function createAccountDeletionStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
// 输入框 - 扁平化风格
|
// 输入框 - 扁平化风格
|
||||||
input: {
|
input: {
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
padding: spacing.md,
|
padding: spacing.md,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -133,12 +126,12 @@ function createAccountDeletionStyles(colors: AppColors) {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
primaryButtonText: {
|
primaryButtonText: {
|
||||||
color: '#fff',
|
color: colors.text.inverse,
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
@@ -166,7 +159,7 @@ function createAccountDeletionStyles(colors: AppColors) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
},
|
},
|
||||||
dangerButtonText: {
|
dangerButtonText: {
|
||||||
color: '#fff',
|
color: colors.text.inverse,
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
@@ -392,9 +385,9 @@ export const AccountDeletionScreen: React.FC = () => {
|
|||||||
onPress={handleRequestDeletion}
|
onPress={handleRequestDeletion}
|
||||||
disabled={submitting || !password.trim()}
|
disabled={submitting || !password.trim()}
|
||||||
>
|
>
|
||||||
{submitting ? (
|
{submitting ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.dangerButtonText}>确认注销</Text>
|
<Text style={styles.dangerButtonText}>确认注销</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -16,13 +16,6 @@ import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
|||||||
// 内容最大宽度
|
// 内容最大宽度
|
||||||
const CONTENT_MAX_WIDTH = 720;
|
const CONTENT_MAX_WIDTH = 720;
|
||||||
|
|
||||||
// 胡萝卜橙主题色
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
import { authService, resolveAuthApiError } from '../../services/authService';
|
import { authService, resolveAuthApiError } from '../../services/authService';
|
||||||
import { showPrompt } from '../../services/promptService';
|
import { showPrompt } from '../../services/promptService';
|
||||||
import { useAuthStore } from '../../stores';
|
import { useAuthStore } from '../../stores';
|
||||||
@@ -263,9 +256,9 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
onPress={handleSendCode}
|
onPress={handleSendCode}
|
||||||
disabled={sendingCode || countdown > 0}
|
disabled={sendingCode || countdown > 0}
|
||||||
>
|
>
|
||||||
{sendingCode ? (
|
{sendingCode ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
|
<Text style={styles.sendCodeButtonText}>{countdown > 0 ? `${countdown}s` : '发送验证码'}</Text>
|
||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@@ -278,7 +271,7 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
disabled={verifyingEmail}
|
disabled={verifyingEmail}
|
||||||
>
|
>
|
||||||
{verifyingEmail ? (
|
{verifyingEmail ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.primaryButtonText}>验证邮箱</Text>
|
<Text style={styles.primaryButtonText}>验证邮箱</Text>
|
||||||
)}
|
)}
|
||||||
@@ -352,7 +345,7 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
disabled={sendingChangePwdCode || changePwdCountdown > 0}
|
disabled={sendingChangePwdCode || changePwdCountdown > 0}
|
||||||
>
|
>
|
||||||
{sendingChangePwdCode ? (
|
{sendingChangePwdCode ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.sendCodeButtonText}>
|
<Text style={styles.sendCodeButtonText}>
|
||||||
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
|
{changePwdCountdown > 0 ? `${changePwdCountdown}s` : '发送验证码'}
|
||||||
@@ -368,7 +361,7 @@ export const AccountSecurityScreen: React.FC = () => {
|
|||||||
disabled={updatingPassword}
|
disabled={updatingPassword}
|
||||||
>
|
>
|
||||||
{updatingPassword ? (
|
{updatingPassword ? (
|
||||||
<ActivityIndicator size="small" color="#fff" />
|
<ActivityIndicator size="small" color={colors.text.inverse} />
|
||||||
) : (
|
) : (
|
||||||
<Text style={styles.primaryButtonText}>更新密码</Text>
|
<Text style={styles.primaryButtonText}>更新密码</Text>
|
||||||
)}
|
)}
|
||||||
@@ -432,26 +425,26 @@ function createAccountSecurityStyles(colors: AppColors) {
|
|||||||
borderRadius: borderRadius.sm,
|
borderRadius: borderRadius.sm,
|
||||||
},
|
},
|
||||||
statusVerified: {
|
statusVerified: {
|
||||||
backgroundColor: '#E8F5E9',
|
backgroundColor: colors.success.light + '30',
|
||||||
},
|
},
|
||||||
statusUnverified: {
|
statusUnverified: {
|
||||||
backgroundColor: '#FFF3E0',
|
backgroundColor: colors.warning.light + '30',
|
||||||
},
|
},
|
||||||
statusText: {
|
statusText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
statusVerifiedText: {
|
statusVerifiedText: {
|
||||||
color: '#2E7D32',
|
color: colors.success.dark,
|
||||||
},
|
},
|
||||||
statusUnverifiedText: {
|
statusUnverifiedText: {
|
||||||
color: '#E65100',
|
color: colors.warning.dark,
|
||||||
},
|
},
|
||||||
// 输入框样式 - 扁平化风格,与登录页一致
|
// 输入框样式 - 扁平化风格,与登录页一致
|
||||||
inputWrapper: {
|
inputWrapper: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
paddingHorizontal: spacing.lg,
|
paddingHorizontal: spacing.lg,
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -485,13 +478,13 @@ function createAccountSecurityStyles(colors: AppColors) {
|
|||||||
height: 56,
|
height: 56,
|
||||||
minWidth: 110,
|
minWidth: 110,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
},
|
},
|
||||||
sendCodeButtonText: {
|
sendCodeButtonText: {
|
||||||
color: '#fff',
|
color: colors.text.inverse,
|
||||||
fontSize: fontSizes.sm,
|
fontSize: fontSizes.sm,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
@@ -499,14 +492,14 @@ function createAccountSecurityStyles(colors: AppColors) {
|
|||||||
primaryButton: {
|
primaryButton: {
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: spacing.xs,
|
marginTop: spacing.xs,
|
||||||
marginHorizontal: spacing['2xl'],
|
marginHorizontal: spacing['2xl'],
|
||||||
},
|
},
|
||||||
primaryButtonText: {
|
primaryButtonText: {
|
||||||
color: '#fff',
|
color: colors.text.inverse,
|
||||||
fontSize: fontSizes.md,
|
fontSize: fontSizes.md,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
StyleSheet,
|
StyleSheet,
|
||||||
Switch,
|
Switch,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
|
Alert,
|
||||||
|
TouchableOpacity,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
@@ -22,6 +24,7 @@ import {
|
|||||||
setVibrationPreference,
|
setVibrationPreference,
|
||||||
} from '../../services/notificationPreferences';
|
} from '../../services/notificationPreferences';
|
||||||
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
import { useResponsive, useResponsiveSpacing } from '../../hooks';
|
||||||
|
import { backgroundSyncManager, BackgroundSyncMode } from '../../services/BackgroundSyncManager';
|
||||||
|
|
||||||
// 内容最大宽度
|
// 内容最大宽度
|
||||||
const CONTENT_MAX_WIDTH = 720;
|
const CONTENT_MAX_WIDTH = 720;
|
||||||
@@ -41,6 +44,7 @@ export const NotificationSettingsScreen: React.FC = () => {
|
|||||||
const [vibrationEnabled, setVibrationEnabledState] = useState(true);
|
const [vibrationEnabled, setVibrationEnabledState] = useState(true);
|
||||||
const [pushEnabled, setPushEnabled] = useState(true);
|
const [pushEnabled, setPushEnabled] = useState(true);
|
||||||
const [soundEnabled, setSoundEnabled] = useState(true);
|
const [soundEnabled, setSoundEnabled] = useState(true);
|
||||||
|
const [syncMode, setSyncMode] = useState<BackgroundSyncMode>(BackgroundSyncMode.BATTERY_SAVER);
|
||||||
const { isWideScreen, isMobile } = useResponsive();
|
const { isWideScreen, isMobile } = useResponsive();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
@@ -55,6 +59,7 @@ export const NotificationSettingsScreen: React.FC = () => {
|
|||||||
setVibrationEnabledState(prefs.vibrationEnabled);
|
setVibrationEnabledState(prefs.vibrationEnabled);
|
||||||
setPushEnabled(prefs.pushEnabled);
|
setPushEnabled(prefs.pushEnabled);
|
||||||
setSoundEnabled(prefs.soundEnabled);
|
setSoundEnabled(prefs.soundEnabled);
|
||||||
|
setSyncMode(backgroundSyncManager.getMode());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载通知设置失败:', 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[] = [
|
const settings: NotificationSettingItem[] = [
|
||||||
{
|
{
|
||||||
key: 'push',
|
key: 'push',
|
||||||
@@ -164,6 +214,62 @@ export const NotificationSettingsScreen: React.FC = () => {
|
|||||||
关闭推送通知后,您将不再收到任何消息提醒,但消息仍会在应用内显示。
|
关闭推送通知后,您将不再收到任何消息提醒,但消息仍会在应用内显示。
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 后台同步模式 */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<View style={styles.sectionHeader}>
|
||||||
|
<Text variant="caption" style={styles.sectionTitle}>
|
||||||
|
后台同步模式
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{syncModeOptions.map((option) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={option.mode}
|
||||||
|
style={[
|
||||||
|
styles.settingItem,
|
||||||
|
syncMode === option.mode && styles.settingItemActive,
|
||||||
|
]}
|
||||||
|
onPress={() => handleSyncModeChange(option.mode)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<View style={styles.iconContainer}>
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name={option.icon as any}
|
||||||
|
size={22}
|
||||||
|
color={syncMode === option.mode ? colors.primary.main : colors.text.secondary}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.settingContent}>
|
||||||
|
<Text
|
||||||
|
variant="body"
|
||||||
|
color={syncMode === option.mode ? colors.primary.main : colors.text.primary}
|
||||||
|
>
|
||||||
|
{option.title}
|
||||||
|
</Text>
|
||||||
|
<Text variant="caption" color={colors.text.secondary} style={styles.subtitle}>
|
||||||
|
{option.subtitle}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
{syncMode === option.mode && (
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="check"
|
||||||
|
size={20}
|
||||||
|
color={colors.primary.main}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 实时模式说明 */}
|
||||||
|
{syncMode === BackgroundSyncMode.REALTIME && (
|
||||||
|
<View style={styles.tipContainer}>
|
||||||
|
<MaterialCommunityIcons name="shield-check-outline" size={16} color={colors.text.hint} />
|
||||||
|
<Text variant="caption" color={colors.text.hint} style={styles.tipText}>
|
||||||
|
实时模式已开启,应用将在通知栏显示常驻通知以保持后台运行。如需更稳定的后台运行,请在系统设置中将本应用加入电池优化白名单。
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -210,6 +316,9 @@ function createNotificationSettingsStyles(colors: AppColors) {
|
|||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: colors.divider,
|
borderBottomColor: colors.divider,
|
||||||
},
|
},
|
||||||
|
settingItemActive: {
|
||||||
|
backgroundColor: colors.primary.main + '0D',
|
||||||
|
},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
width: 24,
|
width: 24,
|
||||||
height: 24,
|
height: 24,
|
||||||
|
|||||||
@@ -18,13 +18,6 @@ import type { PrivacySettingsDTO, VisibilityLevel } from '../../types/dto';
|
|||||||
|
|
||||||
const CONTENT_MAX_WIDTH = 720;
|
const CONTENT_MAX_WIDTH = 720;
|
||||||
|
|
||||||
// 胡萝卜橙主题色
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
const VISIBILITY_OPTIONS: { value: VisibilityLevel; label: string; description: string }[] = [
|
const VISIBILITY_OPTIONS: { value: VisibilityLevel; label: string; description: string }[] = [
|
||||||
{ value: 'everyone', label: '所有人可见', description: '任何人都可查看' },
|
{ value: 'everyone', label: '所有人可见', description: '任何人都可查看' },
|
||||||
{ value: 'following', label: '仅关注的人可见', description: '只有你关注的人可查看' },
|
{ value: 'following', label: '仅关注的人可见', description: '只有你关注的人可查看' },
|
||||||
@@ -74,7 +67,7 @@ function createPrivacySettingsStyles(colors: AppColors) {
|
|||||||
},
|
},
|
||||||
// 隐私设置项 - 扁平化卡片风格
|
// 隐私设置项 - 扁平化卡片风格
|
||||||
item: {
|
item: {
|
||||||
backgroundColor: '#F5F5F7',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
padding: spacing.lg,
|
padding: spacing.lg,
|
||||||
marginHorizontal: spacing['2xl'],
|
marginHorizontal: spacing['2xl'],
|
||||||
@@ -111,15 +104,15 @@ function createPrivacySettingsStyles(colors: AppColors) {
|
|||||||
backgroundColor: colors.background.paper,
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
optionButtonActive: {
|
optionButtonActive: {
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
borderColor: THEME_COLORS.primary,
|
borderColor: colors.primary.main,
|
||||||
},
|
},
|
||||||
optionText: {
|
optionText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: colors.text.primary,
|
color: colors.text.primary,
|
||||||
},
|
},
|
||||||
optionTextActive: {
|
optionTextActive: {
|
||||||
color: '#fff',
|
color: colors.text.inverse,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ import {
|
|||||||
import { showPrompt } from '../../services/promptService';
|
import { showPrompt } from '../../services/promptService';
|
||||||
import * as hrefs from '../../navigation/hrefs';
|
import * as hrefs from '../../navigation/hrefs';
|
||||||
|
|
||||||
const THEME_COLORS = {
|
|
||||||
primary: '#FF6B35',
|
|
||||||
primaryLight: '#FF8C5A',
|
|
||||||
primaryDark: '#E55A2B',
|
|
||||||
};
|
|
||||||
|
|
||||||
const STATUS_CONFIG = {
|
const STATUS_CONFIG = {
|
||||||
not_submitted: {
|
not_submitted: {
|
||||||
title: '未认证',
|
title: '未认证',
|
||||||
@@ -99,7 +93,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<ActivityIndicator size="large" color={THEME_COLORS.primary} />
|
<ActivityIndicator size="large" color={colors.primary.main} />
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
@@ -110,10 +104,11 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
const identityText = IDENTITY_TEXT[status?.identity || 'general'];
|
const identityText = IDENTITY_TEXT[status?.identity || 'general'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<SafeAreaView style={styles.container}>
|
||||||
contentContainerStyle={styles.scrollContent}
|
<ScrollView
|
||||||
showsVerticalScrollIndicator={false}
|
contentContainerStyle={styles.scrollContent}
|
||||||
>
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
{/* 状态卡片 */}
|
{/* 状态卡片 */}
|
||||||
<View style={styles.statusCard}>
|
<View style={styles.statusCard}>
|
||||||
<View style={[styles.statusIconContainer, { backgroundColor: statusConfig.color + '20' }]}>
|
<View style={[styles.statusIconContainer, { backgroundColor: statusConfig.color + '20' }]}>
|
||||||
@@ -128,7 +123,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
|
|
||||||
{status?.verification_status === 'approved' && (
|
{status?.verification_status === 'approved' && (
|
||||||
<View style={styles.identityBadge}>
|
<View style={styles.identityBadge}>
|
||||||
<MaterialCommunityIcons name="check-circle" size={16} color={THEME_COLORS.primary} />
|
<MaterialCommunityIcons name="check-circle" size={16} color={colors.primary.main} />
|
||||||
<Text style={styles.identityText}>{identityText}</Text>
|
<Text style={styles.identityText}>{identityText}</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -153,7 +148,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="check-decagram"
|
name="check-decagram"
|
||||||
size={24}
|
size={24}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.benefitContent}>
|
<View style={styles.benefitContent}>
|
||||||
@@ -169,7 +164,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="lock-open-outline"
|
name="lock-open-outline"
|
||||||
size={24}
|
size={24}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.benefitContent}>
|
<View style={styles.benefitContent}>
|
||||||
@@ -185,7 +180,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="account-group-outline"
|
name="account-group-outline"
|
||||||
size={24}
|
size={24}
|
||||||
color={THEME_COLORS.primary}
|
color={colors.primary.main}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.benefitContent}>
|
<View style={styles.benefitContent}>
|
||||||
@@ -208,7 +203,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
onPress={handleStartVerification}
|
onPress={handleStartVerification}
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name="shield-check-outline" size={20} color="#FFF" />
|
<MaterialCommunityIcons name="shield-check-outline" size={20} color={colors.primary.contrast} />
|
||||||
<Text style={styles.primaryButtonText}>
|
<Text style={styles.primaryButtonText}>
|
||||||
{status?.verification_status === 'rejected' ? '重新认证' : '开始认证'}
|
{status?.verification_status === 'rejected' ? '重新认证' : '开始认证'}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -221,7 +216,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
onPress={handleViewRecords}
|
onPress={handleViewRecords}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name="file-document-outline" size={20} color={THEME_COLORS.primary} />
|
<MaterialCommunityIcons name="file-document-outline" size={20} color={colors.primary.main} />
|
||||||
<Text style={styles.secondaryButtonText}>查看认证记录</Text>
|
<Text style={styles.secondaryButtonText}>查看认证记录</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
@@ -262,6 +257,7 @@ export const VerificationSettingsScreen: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,7 +265,7 @@ function createStyles(colors: AppColors) {
|
|||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.background.paper,
|
||||||
},
|
},
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -307,7 +303,7 @@ function createStyles(colors: AppColors) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 32,
|
paddingVertical: 32,
|
||||||
paddingHorizontal: 24,
|
paddingHorizontal: 24,
|
||||||
backgroundColor: '#F9F9F9',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
},
|
},
|
||||||
@@ -334,7 +330,7 @@ function createStyles(colors: AppColors) {
|
|||||||
identityBadge: {
|
identityBadge: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#FFF5F0',
|
backgroundColor: colors.primary.main + '15',
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
@@ -343,25 +339,25 @@ function createStyles(colors: AppColors) {
|
|||||||
identityText: {
|
identityText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
},
|
},
|
||||||
rejectReasonContainer: {
|
rejectReasonContainer: {
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
backgroundColor: '#FFF3F3',
|
backgroundColor: colors.error.light + '20',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
rejectReasonLabel: {
|
rejectReasonLabel: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#F44336',
|
color: colors.error.main,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
rejectReasonText: {
|
rejectReasonText: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#D32F2F',
|
color: colors.error.dark,
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
},
|
},
|
||||||
benefitsSection: {
|
benefitsSection: {
|
||||||
@@ -379,7 +375,7 @@ function createStyles(colors: AppColors) {
|
|||||||
benefitItem: {
|
benefitItem: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
backgroundColor: '#F9F9F9',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
@@ -387,7 +383,7 @@ function createStyles(colors: AppColors) {
|
|||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
backgroundColor: '#FFF5F0',
|
backgroundColor: colors.primary.main + '15',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
@@ -415,12 +411,12 @@ function createStyles(colors: AppColors) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: THEME_COLORS.primary,
|
backgroundColor: colors.primary.main,
|
||||||
},
|
},
|
||||||
primaryButtonText: {
|
primaryButtonText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: '#FFF',
|
color: colors.text.inverse,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
secondaryButton: {
|
secondaryButton: {
|
||||||
@@ -429,18 +425,18 @@ function createStyles(colors: AppColors) {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
height: 56,
|
height: 56,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: '#FFF5F0',
|
backgroundColor: colors.primary.main + '15',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: THEME_COLORS.primary,
|
borderColor: colors.primary.main,
|
||||||
},
|
},
|
||||||
secondaryButtonText: {
|
secondaryButtonText: {
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
color: THEME_COLORS.primary,
|
color: colors.primary.main,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
verifiedInfo: {
|
verifiedInfo: {
|
||||||
backgroundColor: '#F9F9F9',
|
backgroundColor: colors.background.default,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
|
|||||||
284
src/services/BackgroundSyncManager.ts
Normal file
284
src/services/BackgroundSyncManager.ts
Normal file
@@ -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<typeof AppState.addEventListener> | null = null;
|
||||||
|
private syncInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private isInitialized: boolean = false;
|
||||||
|
private lastSyncAt: number = 0;
|
||||||
|
private syncMessagesCallback: (() => Promise<void>) | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置消息同步回调函数
|
||||||
|
* 由外部注入,避免循环依赖
|
||||||
|
*/
|
||||||
|
setSyncMessagesCallback(callback: () => Promise<void>): void {
|
||||||
|
this.syncMessagesCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化后台同步
|
||||||
|
*/
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
// 如果当前在后台,先停止旧模式的保活
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
if (this.mode !== BackgroundSyncMode.REALTIME) return;
|
||||||
|
|
||||||
|
if (totalUnread > 0) {
|
||||||
|
await ForegroundServiceModule.update(
|
||||||
|
'萝卜社区',
|
||||||
|
`您有 ${totalUnread} 条未读消息`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await ForegroundServiceModule.update(
|
||||||
|
'萝卜社区',
|
||||||
|
'正在后台同步消息'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存模式到存储
|
||||||
|
*/
|
||||||
|
private async saveMode(mode: BackgroundSyncMode): Promise<void> {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.setItem(STORAGE_KEY, mode);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[BackgroundSyncManager] 保存模式失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从存储加载模式
|
||||||
|
*/
|
||||||
|
private async loadMode(): Promise<BackgroundSyncMode> {
|
||||||
|
try {
|
||||||
|
const saved = await AsyncStorage.getItem(STORAGE_KEY);
|
||||||
|
return (saved as BackgroundSyncMode) || BackgroundSyncMode.BATTERY_SAVER;
|
||||||
|
} catch (error) {
|
||||||
|
return BackgroundSyncMode.BATTERY_SAVER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源
|
||||||
|
*/
|
||||||
|
async cleanup(): Promise<void> {
|
||||||
|
this.stopPeriodicSync();
|
||||||
|
this.appStateSubscription?.remove();
|
||||||
|
this.appStateSubscription = null;
|
||||||
|
await ForegroundServiceModule.stop();
|
||||||
|
this.isInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const backgroundSyncManager = new BackgroundSyncManager();
|
||||||
102
src/services/ForegroundServiceModule.ts
Normal file
102
src/services/ForegroundServiceModule.ts
Normal file
@@ -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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,107 +1,92 @@
|
|||||||
/**
|
/**
|
||||||
* 后台保活服务
|
* 后台保活服务
|
||||||
* 使用 expo-background-fetch 和 expo-task-manager 实现 App 后台保活
|
|
||||||
*
|
*
|
||||||
* 功能:
|
* 整合前台服务和后台同步
|
||||||
* - 定期后台任务保持 App 在内存中
|
* - REALTIME 模式:使用 Android 前台服务保活(通知栏常驻)
|
||||||
* - 配合 SSE 服务保持长连接
|
* - BATTERY_SAVER 模式:使用 expo-background-task 定期同步(最小15分钟间隔)
|
||||||
* - 收到消息时触发震动提示
|
*
|
||||||
|
* 参考:Element Android 的 GuardAndroidService + BackgroundSyncStarter
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AppState, AppStateStatus, Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import * as BackgroundFetch from 'expo-background-fetch';
|
import * as BackgroundTask from 'expo-background-task';
|
||||||
import * as TaskManager from 'expo-task-manager';
|
import * as TaskManager from 'expo-task-manager';
|
||||||
import { wsService } from './wsService';
|
|
||||||
import {
|
import {
|
||||||
triggerVibration,
|
backgroundSyncManager,
|
||||||
vibrateOnMessage,
|
BackgroundSyncMode,
|
||||||
setVibrationConfig,
|
} from './BackgroundSyncManager';
|
||||||
getVibrationConfig,
|
import { messageService } from './messageService';
|
||||||
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 用于检查认证状态
|
|
||||||
import { api } from './api';
|
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 {
|
try {
|
||||||
// 检查是否已认证
|
// 检查是否已认证
|
||||||
const token = await api.getToken();
|
const token = await api.getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return BackgroundFetch.BackgroundFetchResult.NoData;
|
return BackgroundTask.BackgroundTaskResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查 SSE 连接状态
|
// 执行同步
|
||||||
if (!wsService.isConnected()) {
|
await syncMessages();
|
||||||
await wsService.connect();
|
|
||||||
}
|
return BackgroundTask.BackgroundTaskResult.Success;
|
||||||
|
|
||||||
// 返回收到新数据
|
|
||||||
return BackgroundFetch.BackgroundFetchResult.NewData;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BackgroundService] 后台任务执行失败:', error);
|
console.error('[BackgroundService] 后台任务执行失败:', error);
|
||||||
return BackgroundFetch.BackgroundFetchResult.Failed;
|
return BackgroundTask.BackgroundTaskResult.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;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册后台任务
|
* 执行消息同步
|
||||||
*/
|
*/
|
||||||
async function registerBackgroundTasks(): Promise<void> {
|
async function syncMessages(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
if (Platform.OS === 'web') {
|
if (Platform.OS === 'web') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查是否已注册
|
// 检查后台任务状态
|
||||||
const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
|
const status = await BackgroundTask.getStatusAsync();
|
||||||
if (!isRegistered) {
|
if (status !== BackgroundTask.BackgroundTaskStatus.Available) {
|
||||||
await BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
|
console.warn('[BackgroundService] 后台任务不可用,状态:', status);
|
||||||
minimumInterval: BACKGROUND_INTERVAL * 60, // 转换为秒
|
return;
|
||||||
stopOnTerminate: false, // App 终止后继续运行
|
|
||||||
startOnBoot: true, // 设备启动后自动运行
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 注册 SSE 保活任务
|
// 检查是否已注册
|
||||||
const isSseKeepaliveRegistered = await TaskManager.isTaskRegisteredAsync(REALTIME_KEEPALIVE_TASK);
|
const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_SYNC_TASK);
|
||||||
if (!isSseKeepaliveRegistered) {
|
if (!isRegistered) {
|
||||||
await BackgroundFetch.registerTaskAsync(REALTIME_KEEPALIVE_TASK, {
|
await BackgroundTask.registerTaskAsync(BACKGROUND_SYNC_TASK, {
|
||||||
minimumInterval: 60, // 1 分钟检查一次
|
minimumInterval: BACKGROUND_INTERVAL_MINUTES, // 以分钟为单位
|
||||||
stopOnTerminate: false,
|
|
||||||
startOnBoot: true,
|
|
||||||
});
|
});
|
||||||
|
console.log('[BackgroundService] 后台任务已注册');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BackgroundService] 注册后台任务失败:', error);
|
console.error('[BackgroundService] 注册后台任务失败:', error);
|
||||||
@@ -111,41 +96,22 @@ async function registerBackgroundTasks(): Promise<void> {
|
|||||||
/**
|
/**
|
||||||
* 取消后台任务
|
* 取消后台任务
|
||||||
*/
|
*/
|
||||||
async function unregisterBackgroundTasks(): Promise<void> {
|
async function unregisterBackgroundTask(): Promise<void> {
|
||||||
if (Platform.OS === 'web') {
|
if (Platform.OS === 'web') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await BackgroundFetch.unregisterTaskAsync(BACKGROUND_FETCH_TASK);
|
const isRegistered = await TaskManager.isTaskRegisteredAsync(BACKGROUND_SYNC_TASK);
|
||||||
await BackgroundFetch.unregisterTaskAsync(REALTIME_KEEPALIVE_TASK);
|
if (isRegistered) {
|
||||||
|
await BackgroundTask.unregisterTaskAsync(BACKGROUND_SYNC_TASK);
|
||||||
|
console.log('[BackgroundService] 后台任务已取消');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BackgroundService] 取消后台任务失败:', 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<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.OS === 'web') {
|
if (Platform.OS === 'web') {
|
||||||
setupAppStateListener();
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查后台任务状态
|
// 设置同步回调
|
||||||
const status = await BackgroundFetch.getStatusAsync();
|
backgroundSyncManager.setSyncMessagesCallback(syncMessages);
|
||||||
if (status !== BackgroundFetch.BackgroundFetchStatus.Available) {
|
|
||||||
console.warn('[BackgroundService] 后台任务不可用,状态:', status);
|
|
||||||
// 即使后台任务不可用,也继续初始化其他功能
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册后台任务
|
// 初始化后台同步管理器
|
||||||
await registerBackgroundTasks();
|
await backgroundSyncManager.initialize();
|
||||||
|
|
||||||
// 设置 App 状态监听
|
// 注册 expo-background-task 任务(用于 BATTERY_SAVER 模式)
|
||||||
setupAppStateListener();
|
await registerBackgroundTask();
|
||||||
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
|
console.log('[BackgroundService] 初始化完成');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BackgroundService] 初始化失败:', error);
|
console.error('[BackgroundService] 初始化失败:', error);
|
||||||
@@ -187,14 +149,10 @@ export async function initBackgroundService(): Promise<boolean> {
|
|||||||
*/
|
*/
|
||||||
export async function stopBackgroundService(): Promise<void> {
|
export async function stopBackgroundService(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await unregisterBackgroundTasks();
|
await unregisterBackgroundTask();
|
||||||
|
await backgroundSyncManager.cleanup();
|
||||||
if (appStateSubscription) {
|
|
||||||
appStateSubscription.remove();
|
|
||||||
appStateSubscription = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
|
console.log('[BackgroundService] 服务已停止');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[BackgroundService] 停止服务失败:', error);
|
console.error('[BackgroundService] 停止服务失败:', error);
|
||||||
}
|
}
|
||||||
@@ -207,34 +165,74 @@ export async function checkBackgroundStatus(): Promise<{
|
|||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
isInitialized: boolean;
|
isInitialized: boolean;
|
||||||
registeredTasks: string[];
|
registeredTasks: string[];
|
||||||
|
mode: BackgroundSyncMode;
|
||||||
}> {
|
}> {
|
||||||
if (Platform.OS === 'web') {
|
if (Platform.OS === 'web') {
|
||||||
return {
|
return {
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
isInitialized,
|
isInitialized,
|
||||||
registeredTasks: [],
|
registeredTasks: [],
|
||||||
|
mode: backgroundSyncManager.getMode(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const status = await BackgroundFetch.getStatusAsync();
|
|
||||||
|
const status = await BackgroundTask.getStatusAsync();
|
||||||
const registeredTasks = await TaskManager.getRegisteredTasksAsync();
|
const registeredTasks = await TaskManager.getRegisteredTasksAsync();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAvailable: status === BackgroundFetch.BackgroundFetchStatus.Available,
|
isAvailable: status === BackgroundTask.BackgroundTaskStatus.Available,
|
||||||
isInitialized,
|
isInitialized,
|
||||||
registeredTasks: registeredTasks.map(task => task.taskName),
|
registeredTasks: registeredTasks.map((task) => task.taskName),
|
||||||
|
mode: backgroundSyncManager.getMode(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出服务实例
|
/**
|
||||||
export const backgroundService = {
|
* 设置后台同步模式
|
||||||
init: initBackgroundService,
|
*/
|
||||||
stop: stopBackgroundService,
|
export async function setBackgroundSyncMode(mode: BackgroundSyncMode): Promise<void> {
|
||||||
vibrate: triggerVibration,
|
await backgroundSyncManager.setMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前后台同步模式
|
||||||
|
*/
|
||||||
|
export function getBackgroundSyncMode(): BackgroundSyncMode {
|
||||||
|
return backgroundSyncManager.getMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发同步
|
||||||
|
*/
|
||||||
|
export async function syncNow(): Promise<void> {
|
||||||
|
await syncMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出震动相关功能(保持向后兼容)
|
||||||
|
export {
|
||||||
|
triggerVibration,
|
||||||
vibrateOnMessage,
|
vibrateOnMessage,
|
||||||
setVibrationConfig,
|
setVibrationConfig,
|
||||||
getVibrationConfig,
|
getVibrationConfig,
|
||||||
setVibrationEnabled,
|
setVibrationEnabled,
|
||||||
|
} from './messageVibrationService';
|
||||||
|
|
||||||
|
// 重新导出类型
|
||||||
|
export { BackgroundSyncMode };
|
||||||
|
|
||||||
|
// 后台服务实例
|
||||||
|
export const backgroundService = {
|
||||||
|
init: initBackgroundService,
|
||||||
|
stop: stopBackgroundService,
|
||||||
|
setMode: setBackgroundSyncMode,
|
||||||
|
getMode: getBackgroundSyncMode,
|
||||||
|
syncNow,
|
||||||
checkStatus: checkBackgroundStatus,
|
checkStatus: checkBackgroundStatus,
|
||||||
|
// 震动相关
|
||||||
|
vibrate: async () => {
|
||||||
|
const { triggerVibration } = await import('./messageVibrationService');
|
||||||
|
triggerVibration();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default backgroundService;
|
export default backgroundService;
|
||||||
|
|||||||
Reference in New Issue
Block a user