更新ios

This commit is contained in:
2026-01-22 11:56:52 +08:00
parent 57c1dab17b
commit b4ee1a6ca3
75 changed files with 1525 additions and 551 deletions

16
MeAgent/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
# OSX
#
.DS_Store
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
# Bundle artifacts
*.jsbundle

View File

@@ -0,0 +1,201 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
static def versionToNumber(major, minor, patch) {
return patch * 100 + minor * 10000 + major * 1000000
}
def getRNVersion() {
def version = providers.exec {
workingDir(projectDir)
commandLine("node", "-e", "console.log(require('react-native/package.json').version);")
}.standardOutput.asText.get().trim()
def coreVersion = version.split("-")[0]
def (major, minor, patch) = coreVersion.tokenize('.').collect { it.toInteger() }
return versionToNumber(
major,
minor,
patch
)
}
def rnVersion = getRNVersion()
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
// Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects.
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '..'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
// codegenDir = file("../node_modules/@react-native/codegen")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
if (rnVersion >= versionToNumber(0, 75, 0)) {
/* Autolinking */
autolinkLibrariesWithApp()
}
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace 'com.valuefrontier.meagent'
defaultConfig {
applicationId 'com.valuefrontier.meagent'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
}
}
packagingOptions {
jniLibs {
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
}
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
// Split option: 'foo,bar' -> ['foo', 'bar']
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
// Trim all elements in place.
for (i in 0..<options.size()) options[i] = options[i].trim();
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
options -= ""
if (options.length > 0) {
println "android.packagingOptions.$prop += $options ($options.length)"
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
options.each {
android.packagingOptions[prop] += it
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}")
}
if (isWebpEnabled) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
if (isWebpAnimatedEnabled) {
// Animated webp support
implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}")
}
}
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
if (rnVersion < versionToNumber(0, 75, 0)) {
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesAppBuildGradle(project)
}

Binary file not shown.

14
MeAgent/android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,14 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -0,0 +1,37 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme">
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
<meta-data android:name="expo.modules.notifications.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="com.valuefrontier.meagent"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
</application>
</manifest>

View File

@@ -0,0 +1,61 @@
package com.valuefrontier.meagent
import android.os.Build
import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import expo.modules.ReactActivityDelegateWrapper
class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set the theme to AppTheme BEFORE onCreate to support
// coloring the background, status bar, and navigation bar.
// This is required for expo-splash-screen.
setTheme(R.style.AppTheme);
super.onCreate(null)
}
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "main"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate {
return ReactActivityDelegateWrapper(
this,
BuildConfig.IS_NEW_ARCHITECTURE_ENABLED,
object : DefaultReactActivityDelegate(
this,
mainComponentName,
fabricEnabled
){})
}
/**
* Align the back button behavior with Android S
* where moving root activities to background instead of finishing activities.
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
*/
override fun invokeDefaultOnBackPressed() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
if (!moveTaskToBack(false)) {
// For non-root activities, use the default implementation to finish them.
super.invokeDefaultOnBackPressed()
}
return
}
// Use the default back button implementation on Android S
// because it's doing more than [Activity.moveTaskToBack] in fact.
super.invokeDefaultOnBackPressed()
}
}

View File

@@ -0,0 +1,55 @@
package com.valuefrontier.meagent
import android.app.Application
import android.content.res.Configuration
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.soloader.SoLoader
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost = ReactNativeHostWrapper(
this,
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return PackageList(this).packages
}
override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
)
override val reactHost: ReactHost
get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
>
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

View File

@@ -0,0 +1,3 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
</layer-list>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/iconBackground"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1 @@
<resources/>

View File

@@ -0,0 +1,7 @@
<resources>
<color name="splashscreen_background">#000000</color>
<color name="iconBackground">#000000</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#000000</color>
<color name="notification_icon_color">#D4AF37</color>
</resources>

View File

@@ -0,0 +1,5 @@
<resources>
<string name="app_name">价值前沿</string>
<string name="expo_splash_screen_resize_mode" translatable="false">contain</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources>

View File

@@ -0,0 +1,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:textColor">@android:color/black</item>
<item name="android:editTextStyle">@style/ResetEditText</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="android:statusBarColor">#000000</item>
</style>
<style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item>
<item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/splashscreen</item>
</style>
</resources>

View File

@@ -0,0 +1,41 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '34.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '23')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '34')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.23'
ndkVersion = "26.1.10909125"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin')
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
}
}
apply plugin: "com.facebook.react.rootproject"
allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
}
maven {
// Android JSC is installed from npm
url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist'))
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}

View File

@@ -0,0 +1,59 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Enable AAPT2 PNG crunching
android.enablePngCrunchInReleaseBuilds=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
# Enable GIF support in React Native images (~200 B increase)
expo.gif.enabled=true
# Enable webp support in React Native images (~85 KB increase)
expo.webp.enabled=true
# Enable animated webp support (~3.4 MB increase)
# Disabled by default because iOS doesn't support animated webp
expo.webp.animated=false
# Enable network inspector
EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
MeAgent/android/gradlew vendored Executable file
View File

@@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
MeAgent/android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,19 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.9.24"
id("java-gradle-plugin")
}
repositories {
mavenCentral()
}
gradlePlugin {
plugins {
create("reactSettingsPlugin") {
id = "com.facebook.react.settings"
implementationClass = "expo.plugins.ReactSettingsPlugin"
}
}
}

View File

@@ -0,0 +1,10 @@
package expo.plugins
import org.gradle.api.Plugin
import org.gradle.api.initialization.Settings
class ReactSettingsPlugin : Plugin<Settings> {
override fun apply(settings: Settings) {
// Do nothing, just register the plugin.
}
}

View File

@@ -0,0 +1,66 @@
pluginManagement {
def version = providers.exec {
commandLine("node", "-e", "console.log(require('react-native/package.json').version);")
}.standardOutput.asText.get().trim()
def (_, reactNativeMinor, reactNativePatch) = version.split("-")[0].tokenize('.').collect { it.toInteger() }
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile().toString())
if(reactNativeMinor == 74 && reactNativePatch <= 3){
includeBuild("react-settings-plugin")
}
}
plugins { id("com.facebook.react.settings") }
def getRNMinorVersion() {
def version = providers.exec {
commandLine("node", "-e", "console.log(require('react-native/package.json').version);")
}.standardOutput.asText.get().trim()
def coreVersion = version.split("-")[0]
def (major, minor, patch) = coreVersion.tokenize('.').collect { it.toInteger() }
return minor
}
if (getRNMinorVersion() >= 75) {
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
if (System.getenv('EXPO_UNSTABLE_CORE_AUTOLINKING') == '1') {
println('\u001B[32mUsing expo-modules-autolinking as core autolinking source\u001B[0m')
def command = [
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'android'
].toList()
ex.autolinkLibrariesFromCommand(command)
} else {
ex.autolinkLibrariesFromCommand()
}
}
}
rootProject.name = '价值前沿'
dependencyResolutionManagement {
versionCatalogs {
reactAndroidLibs {
from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml")))
}
}
}
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
useExpoModules()
if (getRNMinorVersion() < 75) {
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesSettingsGradle(settings)
}
include ':app'
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile())

View File

@@ -1289,6 +1289,8 @@ PODS:
- ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core - ReactCommon/turbomodule/core
- Yoga - Yoga
- RNIap (12.15.0):
- React-Core
- RNKLineView (1.0.0): - RNKLineView (1.0.0):
- lottie-ios (~> 4.5.0) - lottie-ios (~> 4.5.0)
- React - React
@@ -1418,6 +1420,7 @@ DEPENDENCIES:
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNIap (from `../node_modules/react-native-iap`)
- RNKLineView (from `../node_modules/react-native-kline-view`) - RNKLineView (from `../node_modules/react-native-kline-view`)
- RNReanimated (from `../node_modules/react-native-reanimated`) - RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`) - RNScreens (from `../node_modules/react-native-screens`)
@@ -1581,6 +1584,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-masked-view/masked-view" :path: "../node_modules/@react-native-masked-view/masked-view"
RNGestureHandler: RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler" :path: "../node_modules/react-native-gesture-handler"
RNIap:
:path: "../node_modules/react-native-iap"
RNKLineView: RNKLineView:
:path: "../node_modules/react-native-kline-view" :path: "../node_modules/react-native-kline-view"
RNReanimated: RNReanimated:
@@ -1669,6 +1674,7 @@ SPEC CHECKSUMS:
RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11 RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11
RNCMaskedView: de80352547bd4f0d607bf6bab363d826822bd126 RNCMaskedView: de80352547bd4f0d607bf6bab363d826822bd126
RNGestureHandler: 326e35460fb6c8c64a435d5d739bea90d7ed4e49 RNGestureHandler: 326e35460fb6c8c64a435d5d739bea90d7ed4e49
RNIap: cfac9771b7fc4e5012a1aea18f32c0ea1f03ac36
RNKLineView: bb63410106d30c7e3a7967c638ef682f9415b2a2 RNKLineView: bb63410106d30c7e3a7967c638ef682f9415b2a2
RNReanimated: def444e044c354f38bb0a5926a8583ba19d944c1 RNReanimated: def444e044c354f38bb0a5926a8583ba19d944c1
RNScreens: a2d8a2555b4653d7a19706eb172f855657ac30d7 RNScreens: a2d8a2555b4653d7a19706eb172f855657ac30d7

View File

@@ -42,6 +42,12 @@
<key>NSAllowsLocalNetworking</key> <key>NSAllowsLocalNetworking</key>
<true/> <true/>
</dict> </dict>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your photos</string>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>remote-notification</string> <string>remote-notification</string>

View File

@@ -166,7 +166,7 @@ function CustomDrawerContent({
{ title: "市场热点", navigateTo: "MarketDrawer", icon: "flame", gradient: ["#F59E0B", "#FBBF24"] }, { title: "市场热点", navigateTo: "MarketDrawer", icon: "flame", gradient: ["#F59E0B", "#FBBF24"] },
{ title: "概念中心", navigateTo: "ConceptsDrawer", icon: "bulb", gradient: ["#06B6D4", "#22D3EE"] }, { title: "概念中心", navigateTo: "ConceptsDrawer", icon: "bulb", gradient: ["#06B6D4", "#22D3EE"] },
{ title: "我的自选", navigateTo: "WatchlistDrawer", icon: "star", gradient: ["#EC4899", "#F472B6"] }, { title: "我的自选", navigateTo: "WatchlistDrawer", icon: "star", gradient: ["#EC4899", "#F472B6"] },
{ title: "社区论坛", navigateTo: "CommunityDrawer", icon: "chatbubbles", gradient: ["#10B981", "#34D399"] }, { title: "社区论坛", navigateTo: "SocialDrawer", icon: "chatbubbles", gradient: ["#10B981", "#34D399"] },
{ title: "AI 助手", navigateTo: "AgentDrawer", icon: "sparkles", gradient: ["#8B5CF6", "#EC4899"] }, { title: "AI 助手", navigateTo: "AgentDrawer", icon: "sparkles", gradient: ["#8B5CF6", "#EC4899"] },
{ title: "个人中心", navigateTo: "ProfileDrawerNew", icon: "person", gradient: ["#8B5CF6", "#A78BFA"] }, { title: "个人中心", navigateTo: "ProfileDrawerNew", icon: "person", gradient: ["#8B5CF6", "#A78BFA"] },
]; ];

View File

@@ -17,9 +17,7 @@ import Fashion from "../screens/Fashion";
import Gallery from "../screens/Gallery"; import Gallery from "../screens/Gallery";
// screens // screens
import Home from "../screens/Home"; import Home from "../screens/Home";
import NotificationsScreen from "../screens/Notifications"; // 旧版通知页面已删除,使用 src/screens/Social/Notifications.js
// Notifications
import PersonalNotifications from "../screens/PersonalNotifications";
import PrivacyScreen from "../screens/Privacy"; import PrivacyScreen from "../screens/Privacy";
// import Onboarding from "../screens/Onboarding"; // import Onboarding from "../screens/Onboarding";
import Pro from "../screens/Pro"; import Pro from "../screens/Pro";
@@ -30,7 +28,6 @@ import Register from "../screens/Register";
import Search from "../screens/Search"; import Search from "../screens/Search";
// settings // settings
import SettingsScreen from "../screens/Settings"; import SettingsScreen from "../screens/Settings";
import SystemNotifications from "../screens/SystemNotifications";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { createDrawerNavigator } from "@react-navigation/drawer"; import { createDrawerNavigator } from "@react-navigation/drawer";
import { createStackNavigator } from "@react-navigation/stack"; import { createStackNavigator } from "@react-navigation/stack";
@@ -50,14 +47,14 @@ import WatchlistScreen from "../src/screens/Watchlist/WatchlistScreen";
// 新股票详情页面 // 新股票详情页面
import { StockDetailScreen } from "../src/screens/StockDetail"; import { StockDetailScreen } from "../src/screens/StockDetail";
// 社页面 // 社页面
import CommunityHome from "../src/screens/Community"; import SocialHome from "../src/screens/Social";
import ChannelDetail from "../src/screens/Community/ChannelDetail"; import ChannelDetail from "../src/screens/Social/ChannelDetail";
import ForumChannel from "../src/screens/Community/ForumChannel"; import ForumChannel from "../src/screens/Social/ForumChannel";
import PostDetail from "../src/screens/Community/PostDetail"; import PostDetail from "../src/screens/Social/PostDetail";
import CreatePost from "../src/screens/Community/CreatePost"; import CreatePost from "../src/screens/Social/CreatePost";
import CreateChannel from "../src/screens/Community/CreateChannel"; import CreateChannel from "../src/screens/Social/CreateChannel";
import MemberList from "../src/screens/Community/MemberList"; import MemberList from "../src/screens/Social/MemberList";
// 新个人中心页面 // 新个人中心页面
import { ProfileScreen as NewProfileScreen } from "../src/screens/Profile"; import { ProfileScreen as NewProfileScreen } from "../src/screens/Profile";
@@ -80,42 +77,7 @@ const Stack = createStackNavigator();
const Drawer = createDrawerNavigator(); const Drawer = createDrawerNavigator();
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
function NotificationsStack(props) { // 旧版 NotificationsStack 已删除,使用社区通知页面
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color }) => {
let iconName;
if (route.name === "Personal") {
iconName = "user";
} else if (route.name === "System") {
iconName = "database";
}
// You can return any component that you like here!
return (
<Icon
name={iconName}
family="entypo"
size={22}
color={color}
style={{ marginTop: 10 }}
/>
);
},
})}
tabBarOptions={{
activeTintColor: argonTheme.COLORS.PRIMARY,
inactiveTintColor: "gray",
labelStyle: {
fontFamily: "open-sans-regular",
},
}}
>
<Tab.Screen name="Personal" component={PersonalNotifications} />
<Tab.Screen name="System" component={SystemNotifications} />
</Tab.Navigator>
);
}
function ElementsStack(props) { function ElementsStack(props) {
return ( return (
@@ -197,21 +159,6 @@ function SettingsStack(props) {
cardStyle: { backgroundColor: "#0F172A" }, cardStyle: { backgroundColor: "#0F172A" },
}} }}
/> />
<Stack.Screen
name="NotificationsSettings"
component={NotificationsScreen}
options={{
header: ({ navigation, scene }) => (
<Header
back
title="Notifications"
scene={scene}
navigation={navigation}
/>
),
cardStyle: { backgroundColor: "#F8F9FE" },
}}
/>
<Stack.Screen <Stack.Screen
name="Cart" name="Cart"
component={Cart} component={Cart}
@@ -227,21 +174,6 @@ function SettingsStack(props) {
cardStyle: { backgroundColor: "#F8F9FE" }, cardStyle: { backgroundColor: "#F8F9FE" },
}} }}
/> />
<Stack.Screen
name="Notifications"
component={NotificationsStack}
options={{
header: ({ navigation, scene }) => (
<Header
back
title="Notifications"
scene={scene}
navigation={navigation}
/>
),
cardStyle: { backgroundColor: "#F8F9FE" },
}}
/>
</Stack.Navigator> </Stack.Navigator>
); );
} }
@@ -435,8 +367,8 @@ function WatchlistStack(props) {
); );
} }
// 社导航栈 // 社导航栈
function CommunityStack(props) { function SocialStack(props) {
return ( return (
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{
@@ -445,8 +377,8 @@ function CommunityStack(props) {
}} }}
> >
<Stack.Screen <Stack.Screen
name="CommunityHome" name="SocialHome"
component={CommunityHome} component={SocialHome}
options={{ options={{
cardStyle: { backgroundColor: "#0F172A" }, cardStyle: { backgroundColor: "#0F172A" },
}} }}
@@ -585,21 +517,6 @@ function ProfileStack(props) {
cardStyle: { backgroundColor: "#FFFFFF" }, cardStyle: { backgroundColor: "#FFFFFF" },
}} }}
/> />
<Stack.Screen
name="Notifications"
component={NotificationsStack}
options={{
header: ({ navigation, scene }) => (
<Header
back
title="Notifications"
navigation={navigation}
scene={scene}
/>
),
cardStyle: { backgroundColor: "#FFFFFF" },
}}
/>
</Stack.Navigator> </Stack.Navigator>
); );
} }
@@ -753,21 +670,6 @@ function HomeStack(props) {
cardStyle: { backgroundColor: "#F8F9FE" }, cardStyle: { backgroundColor: "#F8F9FE" },
}} }}
/> />
<Stack.Screen
name="Notifications"
component={NotificationsStack}
options={{
header: ({ navigation, scene }) => (
<Header
title="Notifications"
back
navigation={navigation}
scene={scene}
/>
),
cardStyle: { backgroundColor: "#F8F9FE" },
}}
/>
</Stack.Navigator> </Stack.Navigator>
); );
} }
@@ -832,8 +734,8 @@ function AppStack(props) {
}} }}
/> />
<Drawer.Screen <Drawer.Screen
name="CommunityDrawer" name="SocialDrawer"
component={CommunityStack} component={SocialStack}
options={{ options={{
headerShown: false, headerShown: false,
}} }}

View File

@@ -42,6 +42,7 @@
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-native": "0.74.5", "react-native": "0.74.5",
"react-native-gesture-handler": "~2.16.1", "react-native-gesture-handler": "~2.16.1",
"react-native-iap": "^12.15.0",
"react-native-kline-view": "github:hellohublot/react-native-kline-view", "react-native-kline-view": "github:hellohublot/react-native-kline-view",
"react-native-modal-dropdown": "1.0.2", "react-native-modal-dropdown": "1.0.2",
"react-native-reanimated": "~3.10.1", "react-native-reanimated": "~3.10.1",

View File

@@ -1,66 +0,0 @@
import React from "react";
import { StyleSheet, FlatList } from "react-native";
import { Block, Text, theme } from "galio-framework";
import { Switch } from "../components";
import argonTheme from "../constants/Theme";
export default class Notifications extends React.Component {
state = {};
toggleSwitch = switchNumber =>
this.setState({ [switchNumber]: !this.state[switchNumber] });
renderItem = ({ item }) => (
<Block row middle space="between" style={styles.rows}>
<Text style={{ fontFamily: 'open-sans-regular' }} size={theme.SIZES.FONT} color="#525F7F" size={15}>{item.title}</Text>
<Switch
onValueChange={() => this.toggleSwitch(item.id)}
value={this.state[item.id]}
/>
</Block>
);
render() {
const notifications = [
{ title: "Someone mentions me", id: "mentions" },
{ title: "Anyone follows me", id: "follows" },
{ title: "Someone comments me", id: "comments" },
{ title: "A seller follows me", id: "seller" }
];
return (
<Block flex style={styles.notification}>
<FlatList
data={notifications}
keyExtractor={(item, index) => item.id}
renderItem={this.renderItem}
ListHeaderComponent={
<Block style={styles.title}>
<Text style={{ fontFamily: 'open-sans-bold', paddingBottom: 5 }} center size={16} color={argonTheme.COLORS.TEXT}>
Recommended Settings
</Text>
<Text style={{ fontFamily: 'open-sans-regular' }} center size={12} color={argonTheme.COLORS.TEXT}>
These are the most important settings
</Text>
</Block>
}
/>
</Block>
);
}
}
const styles = StyleSheet.create({
notification: {
paddingVertical: theme.SIZES.BASE / 3
},
title: {
paddingTop: theme.SIZES.BASE / 2,
paddingBottom: theme.SIZES.BASE * 1.5
},
rows: {
paddingHorizontal: theme.SIZES.BASE,
marginBottom: theme.SIZES.BASE * 1.25
}
});

View File

@@ -1,54 +0,0 @@
import React from "react";
import { ScrollView, Alert } from "react-native";
import { Block } from "galio-framework";
import { Notification } from "../components";
import { argonTheme } from "../constants";
export default class PersonalNotifications extends React.Component {
render() {
return (
<Block middle flex>
<Block flex style={{ width: "90%" }}>
<ScrollView showsVerticalScrollIndicator={false}>
<Notification
time="15:30"
body="About your order #45C23B Wifey made the best Father's Day meal ever. So thankful so happy."
iconName="ship"
iconFamily="font-awesome"
style={{ marginTop: 15 }}
onPress={() => Alert.alert('Yes, you can use the notifications as buttons so you could send your customers to anything you want.')}
/>
<Notification
time="12:10"
body="Customize our products. Now you can make the best and perfect clothes just for you."
iconName="ship"
iconFamily="font-awesome"
color={argonTheme.COLORS.INFO}
style={{ marginTop: 15 }}
onPress={() => Alert.alert('Yes, you can use the notifications as buttons so you could send your customers to anything you want.')}
/>
<Notification
time="11:30"
body="Breaking News! We have new methods to payment. Learn how to pay off debt fast using the stack method."
iconName="ship"
iconFamily="font-awesome"
color={argonTheme.COLORS.WARNING}
style={{ marginTop: 15 }}
onPress={() => Alert.alert('Yes, you can use the notifications as buttons so you could send your customers to anything you want.')}
/>
<Notification
time="04:23"
body="Congratulations! Someone just ordered a pair of Yamaha HS8 speakers through your app! Hurry up and ship them!"
iconName="ship"
iconFamily="font-awesome"
color={argonTheme.COLORS.SUCCESS}
style={{ marginTop: 15 }}
onPress={() => Alert.alert('Yes, you can use the notifications as buttons so you could send your customers to anything you want.')}
/>
<Block style={{ marginBottom: 20 }} />
</ScrollView>
</Block>
</Block>
);
}
}

View File

@@ -1,116 +0,0 @@
import React from "react";
import { StyleSheet, ScrollView, Alert } from "react-native";
import { Block, Text } from "galio-framework";
import { Notification } from "../components";
import { argonTheme } from "../constants";
export default class SystemNotifications extends React.Component {
render() {
return (
<Block flex>
<ScrollView showsVerticalScrollIndicator={false} style={{ flex: 1 }}>
<Block center style={{ width: "90%" }}>
<Block style={styles.card}>
<Block style={styles.cardHeader}>
<Text
size={18}
style={{ fontFamily: "open-sans-bold" }}
color={argonTheme.COLORS.TEXT}
>
Unread notifications
</Text>
</Block>
<Block>
<Notification
transparent
system
title="New message"
time="2 hrs ago"
body="The new message from the author."
iconName="bell"
iconFamily="font-awesome"
color={"#B0EED3"}
style={{ marginBottom: 10 }}
/>
<Notification
transparent
system
title="New order"
time="3 hrs ago"
body="A confirmed request by one party."
iconName="bell"
iconFamily="font-awesome"
color={"#B0EED3"}
style={{ marginBottom: 10 }}
/>
</Block>
</Block>
<Block style={styles.card}>
<Block style={styles.cardHeader}>
<Text
size={18}
style={{ fontFamily: "open-sans-bold" }}
color={argonTheme.COLORS.TEXT}
>
Read notifications
</Text>
</Block>
<Block>
<Notification
transparent
system
title="Last message"
time="1 day ago"
body="Let's meet at Starbucks at 11:30. Wdyt?"
iconName="like1"
iconFamily="antdesign"
color={"#AAEDF9"}
style={{ marginBottom: 10 }}
/>
<Notification
transparent
system
title="Product issue"
time="2 day ago"
body="A new issue has been reported for Argon."
iconName="html5"
iconFamily="font-awesome"
color={"#FDD1DA"}
style={{ marginBottom: 10 }}
/>
<Notification
transparent
system
title="New likes"
time="4 days ago"
body="Your posts have been liked a lot."
iconName="like1"
iconFamily="antdesign"
color={"#AAEDF9"}
style={{ marginBottom: 10 }}
/>
</Block>
</Block>
</Block>
<Block style={{ marginBottom: 20 }} />
</ScrollView>
</Block>
);
}
}
const styles = StyleSheet.create({
card: {
width: '100%',
backgroundColor: argonTheme.COLORS.WHITE,
marginTop: 25,
borderRadius: 6
},
cardHeader: {
paddingTop: 20,
paddingBottom: 20,
paddingLeft: 20,
borderColor: 'rgba(0,0,0,0.2)',
borderBottomWidth: StyleSheet.hairlineWidth
}
});

View File

@@ -1,6 +1,6 @@
/** /**
* WebSocket Hook * WebSocket Hook
* 管理社实时通信连接 * 管理社实时通信连接
* Web 端保持一致的事件名称和行为 * Web 端保持一致的事件名称和行为
*/ */
@@ -15,7 +15,7 @@ import {
addTypingUser, addTypingUser,
removeTypingUser, removeTypingUser,
incrementUnreadCount, incrementUnreadCount,
} from '../store/slices/communitySlice'; } from '../store/slices/socialSlice';
// 服务器事件类型 - 与 Web 端保持一致(大写) // 服务器事件类型 - 与 Web 端保持一致(大写)
export const SERVER_EVENTS = { export const SERVER_EVENTS = {
@@ -43,10 +43,10 @@ export const CLIENT_EVENTS = {
}; };
/** /**
* WebSocket Hook * WebSocket Hook
* @returns {object} WebSocket 操作方法 * @returns {object} WebSocket 操作方法
*/ */
export const useCommunitySocket = () => { export const useSocialSocket = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
// 安全获取 auth context避免在 AuthProvider 外部使用时报错 // 安全获取 auth context避免在 AuthProvider 外部使用时报错
const authContext = useContext(AuthContext); const authContext = useContext(AuthContext);
@@ -88,7 +88,7 @@ export const useCommunitySocket = () => {
// 连接成功 // 连接成功
socket.on('connect', () => { socket.on('connect', () => {
console.log('[CommunitySocket] 连接成功'); console.log('[SocialSocket] 连接成功');
setIsConnected(true); setIsConnected(true);
setConnectionError(null); setConnectionError(null);
@@ -100,13 +100,13 @@ export const useCommunitySocket = () => {
// 连接断开 // 连接断开
socket.on('disconnect', (reason) => { socket.on('disconnect', (reason) => {
console.log('[CommunitySocket] 连接断开:', reason); console.log('[SocialSocket] 连接断开:', reason);
setIsConnected(false); setIsConnected(false);
}); });
// 连接错误 - 只是警告,不阻断页面 // 连接错误 - 只是警告,不阻断页面
socket.on('connect_error', (error) => { socket.on('connect_error', (error) => {
console.warn('[CommunitySocket] WebSocket 暂不可用,使用离线模式'); console.warn('[SocialSocket] WebSocket 暂不可用,使用离线模式');
setConnectionError('WebSocket 暂不可用'); setConnectionError('WebSocket 暂不可用');
}); });
@@ -186,7 +186,7 @@ export const useCommunitySocket = () => {
socketRef.current = socket; socketRef.current = socket;
} catch (error) { } catch (error) {
console.warn('[CommunitySocket] Socket.IO 初始化失败,使用离线模式'); console.warn('[SocialSocket] Socket.IO 初始化失败,使用离线模式');
setConnectionError('初始化失败'); setConnectionError('初始化失败');
} }
}; };
@@ -212,7 +212,7 @@ export const useCommunitySocket = () => {
if (socketRef.current?.connected) { if (socketRef.current?.connected) {
socketRef.current.emit(CLIENT_EVENTS.SUBSCRIBE_CHANNEL, { channelId }); socketRef.current.emit(CLIENT_EVENTS.SUBSCRIBE_CHANNEL, { channelId });
console.log('[CommunitySocket] 已订阅频道:', channelId); console.log('[SocialSocket] 已订阅频道:', channelId);
} }
}, []); }, []);
@@ -224,7 +224,7 @@ export const useCommunitySocket = () => {
if (socketRef.current?.connected) { if (socketRef.current?.connected) {
socketRef.current.emit(CLIENT_EVENTS.UNSUBSCRIBE_CHANNEL, { channelId }); socketRef.current.emit(CLIENT_EVENTS.UNSUBSCRIBE_CHANNEL, { channelId });
console.log('[CommunitySocket] 已取消订阅频道:', channelId); console.log('[SocialSocket] 已取消订阅频道:', channelId);
} }
}, []); }, []);
@@ -298,4 +298,4 @@ export const useCommunitySocket = () => {
}; };
}; };
export default useCommunitySocket; export default useSocialSocket;

View File

@@ -36,9 +36,9 @@ import {
fetchMessages, fetchMessages,
sendMessage, sendMessage,
addMessage, addMessage,
} from '../../store/slices/communitySlice'; } from '../../store/slices/socialSlice';
import { useCommunitySocket } from '../../hooks/useCommunitySocket'; import { useSocialSocket } from '../../hooks/useSocialSocket';
import { uploadService } from '../../services/communityService'; import { uploadService } from '../../services/socialService';
// 消息分组:按日期 // 消息分组:按日期
const groupMessagesByDate = (messages) => { const groupMessagesByDate = (messages) => {
@@ -162,7 +162,7 @@ const ChannelDetail = ({ route, navigation }) => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const flatListRef = useRef(null); const flatListRef = useRef(null);
const communityState = useSelector((state) => state.community); const communityState = useSelector((state) => state.social);
const messages = communityState?.messages || {}; const messages = communityState?.messages || {};
const messagesHasMore = communityState?.messagesHasMore || {}; const messagesHasMore = communityState?.messagesHasMore || {};
const loading = communityState?.loading || {}; const loading = communityState?.loading || {};
@@ -184,7 +184,7 @@ const ChannelDetail = ({ route, navigation }) => {
unsubscribe, unsubscribe,
startTyping, startTyping,
stopTyping, stopTyping,
} = useCommunitySocket(); } = useSocialSocket();
// 订阅频道 WebSocket // 订阅频道 WebSocket
useEffect(() => { useEffect(() => {

View File

@@ -24,8 +24,8 @@ import {
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { fetchChannels, setCurrentChannel } from '../../store/slices/communitySlice'; import { fetchChannels, setCurrentChannel } from '../../store/slices/socialSlice';
import { CHANNEL_TYPES } from '../../services/communityService'; import { CHANNEL_TYPES } from '../../services/socialService';
// 频道图标映射 // 频道图标映射
const CHANNEL_ICONS = { const CHANNEL_ICONS = {
@@ -43,7 +43,7 @@ const CATEGORY_ICONS = {
const ChannelList = ({ navigation }) => { const ChannelList = ({ navigation }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const communityState = useSelector((state) => state.community); const communityState = useSelector((state) => state.social);
const categories = communityState?.categories || []; const categories = communityState?.categories || [];
const loading = communityState?.loading || {}; const loading = communityState?.loading || {};

View File

@@ -26,8 +26,8 @@ import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { channelService, CHANNEL_TYPES } from '../../services/communityService'; import { channelService, CHANNEL_TYPES } from '../../services/socialService';
import { fetchChannels } from '../../store/slices/communitySlice'; import { fetchChannels } from '../../store/slices/socialSlice';
// 频道类型选项 // 频道类型选项
const CHANNEL_TYPE_OPTIONS = [ const CHANNEL_TYPE_OPTIONS = [
@@ -50,7 +50,7 @@ const CreateChannel = ({ navigation }) => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
// 从 Redux 获取真实的 categories 列表 // 从 Redux 获取真实的 categories 列表
const communityState = useSelector((state) => state.community); const communityState = useSelector((state) => state.social);
const categories = communityState?.categories || []; const categories = communityState?.categories || [];
const loadingCategories = communityState?.loading?.channels || false; const loadingCategories = communityState?.loading?.channels || false;

View File

@@ -31,8 +31,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import { createPost } from '../../store/slices/communitySlice'; import { createPost } from '../../store/slices/socialSlice';
import { uploadService } from '../../services/communityService'; import { uploadService } from '../../services/socialService';
import { gradients } from '../../theme'; import { gradients } from '../../theme';
// 预设标签 // 预设标签

View File

@@ -27,7 +27,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { fetchPosts } from '../../store/slices/communitySlice'; import { fetchPosts } from '../../store/slices/socialSlice';
import { gradients } from '../../theme'; import { gradients } from '../../theme';
// 排序选项 // 排序选项
@@ -69,7 +69,7 @@ const ForumChannel = ({ route, navigation }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { posts, postsHasMore, loading } = useSelector((state) => state.community); const { posts, postsHasMore, loading } = useSelector((state) => state.social);
const channelPosts = posts[channel.id] || []; const channelPosts = posts[channel.id] || [];
const hasMore = postsHasMore[channel.id] ?? true; const hasMore = postsHasMore[channel.id] ?? true;

View File

@@ -25,7 +25,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { fetchMembers } from '../../store/slices/communitySlice'; import { fetchMembers } from '../../store/slices/socialSlice';
// 角色配置 // 角色配置
const ROLE_CONFIG = { const ROLE_CONFIG = {
@@ -39,7 +39,7 @@ const MemberList = ({ route, navigation }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { members, loading } = useSelector((state) => state.community); const { members, loading } = useSelector((state) => state.social);
const channelMembers = members[channel.id] || []; const channelMembers = members[channel.id] || [];
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');

View File

@@ -28,7 +28,7 @@ import {
fetchNotifications, fetchNotifications,
markNotificationRead, markNotificationRead,
markAllNotificationsRead, markAllNotificationsRead,
} from '../../store/slices/communitySlice'; } from '../../store/slices/socialSlice';
// 通知类型配置 // 通知类型配置
const NOTIFICATION_CONFIG = { const NOTIFICATION_CONFIG = {
@@ -83,7 +83,7 @@ const Notifications = ({ navigation }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const communityState = useSelector((state) => state.community); const communityState = useSelector((state) => state.social);
const notifications = communityState?.notifications || []; const notifications = communityState?.notifications || [];
const notificationsHasMore = communityState?.notificationsHasMore ?? true; const notificationsHasMore = communityState?.notificationsHasMore ?? true;
const loading = communityState?.loading || {}; const loading = communityState?.loading || {};

View File

@@ -35,8 +35,8 @@ import {
fetchReplies, fetchReplies,
createReply, createReply,
clearCurrentPost, clearCurrentPost,
} from '../../store/slices/communitySlice'; } from '../../store/slices/socialSlice';
import { postService } from '../../services/communityService'; import { postService } from '../../services/socialService';
// 格式化相对时间 // 格式化相对时间
const formatRelativeTime = (dateStr) => { const formatRelativeTime = (dateStr) => {
@@ -112,7 +112,7 @@ const PostDetail = ({ route, navigation }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const { currentPost, replies, loading } = useSelector((state) => state.community); const { currentPost, replies, loading } = useSelector((state) => state.social);
const postReplies = replies[initialPost.id] || []; const postReplies = replies[initialPost.id] || [];
const [replyText, setReplyText] = useState(''); const [replyText, setReplyText] = useState('');

View File

@@ -1,6 +1,6 @@
/** /**
* 主入口 * 主入口
* Discord 风格的社论坛 * Discord 风格的社论坛
*/ */
import React, { useState } from 'react'; import React, { useState } from 'react';
@@ -66,11 +66,11 @@ const CustomTabBar = ({ activeTab, onTabChange, unreadCount }) => {
); );
}; };
const CommunityHome = ({ navigation }) => { const SocialHome = ({ navigation }) => {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [activeTab, setActiveTab] = useState('channels'); const [activeTab, setActiveTab] = useState('channels');
const communityState = useSelector((state) => state.community); const communityState = useSelector((state) => state.social);
const unreadCount = communityState?.unreadCount || 0; const unreadCount = communityState?.unreadCount || 0;
return ( return (
@@ -96,9 +96,9 @@ const CommunityHome = ({ navigation }) => {
); );
}; };
export default CommunityHome; export default SocialHome;
// 导出所有社相关页面 // 导出所有社相关页面
export { default as ChannelList } from './ChannelList'; export { default as ChannelList } from './ChannelList';
export { default as ChannelDetail } from './ChannelDetail'; export { default as ChannelDetail } from './ChannelDetail';
export { default as ForumChannel } from './ForumChannel'; export { default as ForumChannel } from './ForumChannel';

View File

@@ -33,6 +33,7 @@ import {
fetchStockDetail, fetchStockDetail,
fetchMinuteData, fetchMinuteData,
fetchKlineData, fetchKlineData,
loadStockPage,
setChartType, setChartType,
clearCurrentStock, clearCurrentStock,
selectCurrentStock, selectCurrentStock,
@@ -134,26 +135,28 @@ const StockDetailScreen = () => {
} }
}, [stockCode]); }, [stockCode]);
// 加载股票数据 // 加载股票数据(并行加载,提升性能)
const loadStockData = useCallback(async () => { const loadStockData = useCallback(async () => {
if (!stockCode) { if (!stockCode) {
console.log('[StockDetailScreen] 无股票代码'); console.log('[StockDetailScreen] 无股票代码');
return; return;
} }
console.log('[StockDetailScreen] 开始加载数据:', { stockCode, stockName, chartType }); console.log('[StockDetailScreen] 开始并行加载数据:', { stockCode, stockName, chartType });
// 加载股票详情 // 根据当前图表类型决定加载策略
dispatch(fetchStockDetail(stockCode));
// 根据当前图表类型加载数据
if (chartType === 'minute') { if (chartType === 'minute') {
dispatch(fetchMinuteData(stockCode)); // 分时模式:并行加载股票详情和分时数据
dispatch(loadStockPage(stockCode));
} else { } else {
dispatch(fetchKlineData({ stockCode, type: chartType, eventTime })); // K线模式并行加载股票详情和K线数据
Promise.all([
dispatch(fetchStockDetail(stockCode)),
dispatch(fetchKlineData({ stockCode, type: chartType, eventTime })),
]);
} }
// 异步加载涨幅分析数据 // 异步加载涨幅分析数据(不阻塞主流程)
loadRiseAnalysis(); loadRiseAnalysis();
}, [dispatch, stockCode, stockName, chartType, eventTime, loadRiseAnalysis]); }, [dispatch, stockCode, stockName, chartType, eventTime, loadRiseAnalysis]);

View File

@@ -10,6 +10,9 @@ import {
Alert, Alert,
Platform, Platform,
Linking, Linking,
Text as RNText,
View as RNView,
ActivityIndicator,
} from 'react-native'; } from 'react-native';
import { import {
Box, Box,
@@ -19,8 +22,6 @@ import {
Icon, Icon,
Pressable, Pressable,
Spinner, Spinner,
Button,
Badge,
Divider, Divider,
} from 'native-base'; } from 'native-base';
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
@@ -92,7 +93,6 @@ const CycleSelector = ({ cycles, selectedCycle, onSelect }) => {
mb={2} mb={2}
> >
<Box <Box
position="relative"
px={4} px={4}
py={2} py={2}
bg={selectedCycle === cycle.cycleKey ? 'rgba(212, 175, 55, 0.2)' : 'rgba(255, 255, 255, 0.05)'} bg={selectedCycle === cycle.cycleKey ? 'rgba(212, 175, 55, 0.2)' : 'rgba(255, 255, 255, 0.05)'}
@@ -100,28 +100,20 @@ const CycleSelector = ({ cycles, selectedCycle, onSelect }) => {
borderColor={selectedCycle === cycle.cycleKey ? '#D4AF37' : 'rgba(255, 255, 255, 0.1)'} borderColor={selectedCycle === cycle.cycleKey ? '#D4AF37' : 'rgba(255, 255, 255, 0.1)'}
borderRadius={10} borderRadius={10}
> >
<Text <HStack alignItems="center" space={1}>
color={selectedCycle === cycle.cycleKey ? '#D4AF37' : 'white'} <Text
fontWeight={selectedCycle === cycle.cycleKey ? 'bold' : 'normal'} color={selectedCycle === cycle.cycleKey ? '#D4AF37' : 'white'}
fontSize={14} fontWeight={selectedCycle === cycle.cycleKey ? 'bold' : 'normal'}
> fontSize={14}
{cycle.label}
</Text>
{cycle.discount > 0 && (
<Badge
position="absolute"
top={-8}
right={-8}
bg="#EF4444"
borderRadius={8}
px={1}
py={0.5}
> >
<Text color="white" fontSize={9} fontWeight="bold"> {cycle.label}
{cycle.discount}% </Text>
{cycle.discount > 0 && (
<Text color="#EF4444" fontSize={11} fontWeight="bold">
-{cycle.discount}%
</Text> </Text>
</Badge> )}
)} </HStack>
</Box> </Box>
</Pressable> </Pressable>
))} ))}
@@ -141,7 +133,7 @@ const PriceDisplay = ({ plan, selectedCycle }) => {
<Text color="#D4AF37" fontSize={36} fontWeight="bold">{option.price}</Text> <Text color="#D4AF37" fontSize={36} fontWeight="bold">{option.price}</Text>
<Text color="gray.400" fontSize={14}>/{option.label}</Text> <Text color="gray.400" fontSize={14}>/{option.label}</Text>
</HStack> </HStack>
{option.originalPrice && ( {option.originalPrice > 0 && (
<HStack alignItems="center" space={2}> <HStack alignItems="center" space={2}>
<Text color="gray.500" fontSize={12} strikeThrough> <Text color="gray.500" fontSize={12} strikeThrough>
原价 ¥{option.originalPrice} 原价 ¥{option.originalPrice}
@@ -189,27 +181,18 @@ const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPl
> >
{/* 热门标签 */} {/* 热门标签 */}
{plan.popular && ( {plan.popular && (
<Box <RNView style={styles.popularBadgeContainer}>
position="absolute"
top={0}
right={0}
bg="linear-gradient(135deg, #D4AF37, #F5D85A)"
px={3}
py={1}
borderBottomLeftRadius={10}
zIndex={1}
>
<LinearGradient <LinearGradient
colors={['#D4AF37', '#F5D85A']} colors={['#D4AF37', '#F5D85A']}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }} end={{ x: 1, y: 0 }}
style={styles.popularBadge} style={styles.popularBadge}
> >
<Text color="#000" fontSize={11} fontWeight="bold"> <RNText style={styles.popularBadgeText}>
最受欢迎 最受欢迎
</Text> </RNText>
</LinearGradient> </LinearGradient>
</Box> </RNView>
)} )}
{/* 卡片标题区域 */} {/* 卡片标题区域 */}
@@ -219,17 +202,17 @@ const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPl
end={{ x: 1, y: 0 }} end={{ x: 1, y: 0 }}
style={styles.cardHeader} style={styles.cardHeader}
> >
<HStack alignItems="center" space={3}> <RNView style={styles.cardHeaderContent}>
<Icon as={MaterialCommunityIcons} name={plan.icon} size="lg" color="white" /> <MaterialCommunityIcons name={plan.icon} size={24} color="white" />
<VStack> <RNView style={styles.cardHeaderText}>
<Text color="white" fontSize={18} fontWeight="bold"> <RNText style={styles.planDisplayName}>
{plan.displayName} {plan.displayName}
</Text> </RNText>
<Text color="rgba(255, 255, 255, 0.8)" fontSize={12}> <RNText style={styles.planDescription}>
{plan.description} {plan.description}
</Text> </RNText>
</VStack> </RNView>
</HStack> </RNView>
</LinearGradient> </LinearGradient>
{/* 卡片内容 */} {/* 卡片内容 */}
@@ -259,11 +242,11 @@ const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPl
style={styles.subscribeButton} style={styles.subscribeButton}
> >
{loading ? ( {loading ? (
<Spinner color="white" size="sm" /> <ActivityIndicator color="white" size="small" />
) : ( ) : (
<Text color="white" fontSize={16} fontWeight="bold"> <RNText style={styles.subscribeButtonText}>
{isCurrentPlan ? '续费' : `订阅${plan.displayName}`} {isCurrentPlan ? '续费' : `订阅${plan.displayName}`}
</Text> </RNText>
)} )}
</LinearGradient> </LinearGradient>
</Pressable> </Pressable>
@@ -278,7 +261,7 @@ const PlanCard = ({ plan, selectedCycle, onSelectCycle, onSubscribe, isCurrentPl
}; };
// 当前订阅状态卡片 // 当前订阅状态卡片
const CurrentSubscriptionCard = ({ subscription, onManage }) => { const CurrentSubscriptionCard = ({ subscription }) => {
if (!subscription || !subscription.is_active) { if (!subscription || !subscription.is_active) {
return null; return null;
} }
@@ -305,34 +288,29 @@ const CurrentSubscriptionCard = ({ subscription, onManage }) => {
borderColor="rgba(212, 175, 55, 0.3)" borderColor="rgba(212, 175, 55, 0.3)"
borderRadius={16} borderRadius={16}
> >
<HStack alignItems="center" justifyContent="space-between"> <HStack alignItems="center" space={3}>
<HStack alignItems="center" space={3}> <Box
<Box bg={`${planInfo.color}20`}
bg={`${planInfo.color}20`} p={2}
p={2} borderRadius={10}
borderRadius={10} >
> <Icon as={MaterialCommunityIcons} name={planInfo.icon} size="md" color={planInfo.color} />
<Icon as={MaterialCommunityIcons} name={planInfo.icon} size="md" color={planInfo.color} /> </Box>
</Box> <VStack flex={1}>
<VStack> <HStack alignItems="center" space={2}>
<HStack alignItems="center" space={2}> <Text color="white" fontSize={16} fontWeight="bold">
<Text color="white" fontSize={16} fontWeight="bold"> {planInfo.name}
{planInfo.name} </Text>
</Text> <Box bg="rgba(76, 175, 80, 0.2)" borderRadius={6} px={1.5} py={0.5}>
<Badge bg="rgba(76, 175, 80, 0.2)" borderRadius={6}> <Text color="#4CAF50" fontSize={10}>使用中</Text>
<Text color="#4CAF50" fontSize={10}>使用中</Text> </Box>
</Badge> </HStack>
</HStack> {subscription.end_date ? (
{subscription.end_date && ( <Text color="gray.400" fontSize={12}>
<Text color="gray.400" fontSize={12}> 到期时间: {new Date(subscription.end_date).toLocaleDateString('zh-CN')}
到期时间: {new Date(subscription.end_date).toLocaleDateString('zh-CN')} </Text>
</Text> ) : null}
)} </VStack>
</VStack>
</HStack>
<Pressable onPress={onManage}>
<Text color="#D4AF37" fontSize={13}>管理</Text>
</Pressable>
</HStack> </HStack>
</Box> </Box>
); );
@@ -344,8 +322,8 @@ const SubscriptionScreen = () => {
const { user, subscription, refreshUser } = useAuth(); const { user, subscription, refreshUser } = useAuth();
const [selectedCycles, setSelectedCycles] = useState({ const [selectedCycles, setSelectedCycles] = useState({
pro: 'yearly', pro: 'quarterly',
max: 'yearly', max: 'quarterly',
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [loadingPlan, setLoadingPlan] = useState(null); const [loadingPlan, setLoadingPlan] = useState(null);
@@ -451,12 +429,6 @@ const SubscriptionScreen = () => {
} }
}, [user, navigation, refreshUser]); }, [user, navigation, refreshUser]);
// 管理订阅(打开苹果订阅管理页面)
const handleManageSubscription = useCallback(() => {
// iOS 订阅管理页面 URL
Linking.openURL('https://apps.apple.com/account/subscriptions');
}, []);
// 恢复购买 // 恢复购买
const handleRestorePurchases = useCallback(async () => { const handleRestorePurchases = useCallback(async () => {
if (Platform.OS !== 'ios') { if (Platform.OS !== 'ios') {
@@ -521,14 +493,11 @@ const SubscriptionScreen = () => {
<Text flex={1} color="white" fontSize={18} fontWeight="bold" textAlign="center"> <Text flex={1} color="white" fontSize={18} fontWeight="bold" textAlign="center">
订阅管理 订阅管理
</Text> </Text>
<Box w={6} /> {/* 占位,保持标题居中 */} <Box w={6} />
</HStack> </HStack>
{/* 当前订阅状态 */} {/* 当前订阅状态 */}
<CurrentSubscriptionCard <CurrentSubscriptionCard subscription={subscription} />
subscription={subscription}
onManage={handleManageSubscription}
/>
{/* 标题 */} {/* 标题 */}
<VStack alignItems="center" px={4} mb={6}> <VStack alignItems="center" px={4} mb={6}>
@@ -608,17 +577,49 @@ const styles = StyleSheet.create({
paddingVertical: 16, paddingVertical: 16,
paddingHorizontal: 20, paddingHorizontal: 20,
}, },
cardHeaderContent: {
flexDirection: 'row',
alignItems: 'center',
},
cardHeaderText: {
marginLeft: 12,
},
planDisplayName: {
color: 'white',
fontSize: 18,
fontWeight: 'bold',
},
planDescription: {
color: 'rgba(255, 255, 255, 0.8)',
fontSize: 12,
},
popularBadgeContainer: {
position: 'absolute',
top: 0,
right: 0,
zIndex: 1,
},
popularBadge: { popularBadge: {
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 4, paddingVertical: 4,
borderBottomLeftRadius: 10, borderBottomLeftRadius: 10,
}, },
popularBadgeText: {
color: '#000',
fontSize: 11,
fontWeight: 'bold',
},
subscribeButton: { subscribeButton: {
paddingVertical: 14, paddingVertical: 14,
borderRadius: 12, borderRadius: 12,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
}, },
subscribeButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
}); });
export default SubscriptionScreen; export default SubscriptionScreen;

View File

@@ -6,8 +6,9 @@
*/ */
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import {
initConnection, // 安全导入 react-native-iap模拟器或开发环境可能不可用
let initConnection,
endConnection, endConnection,
getProducts, getProducts,
getSubscriptions, getSubscriptions,
@@ -16,8 +17,38 @@ import {
finishTransaction, finishTransaction,
purchaseUpdatedListener, purchaseUpdatedListener,
purchaseErrorListener, purchaseErrorListener,
flushFailedPurchasesCachedAsPendingAndroid, flushFailedPurchasesCachedAsPendingAndroid;
} from 'react-native-iap';
let IAP_AVAILABLE = false;
try {
const RNIap = require('react-native-iap');
initConnection = RNIap.initConnection;
endConnection = RNIap.endConnection;
getProducts = RNIap.getProducts;
getSubscriptions = RNIap.getSubscriptions;
requestSubscription = RNIap.requestSubscription;
getAvailablePurchases = RNIap.getAvailablePurchases;
finishTransaction = RNIap.finishTransaction;
purchaseUpdatedListener = RNIap.purchaseUpdatedListener;
purchaseErrorListener = RNIap.purchaseErrorListener;
flushFailedPurchasesCachedAsPendingAndroid = RNIap.flushFailedPurchasesCachedAsPendingAndroid;
IAP_AVAILABLE = true;
console.log('[IAPService] react-native-iap 加载成功');
} catch (error) {
console.warn('[IAPService] react-native-iap 加载失败IAP 功能不可用:', error.message);
// 提供空函数作为降级
initConnection = async () => false;
endConnection = async () => {};
getProducts = async () => [];
getSubscriptions = async () => [];
requestSubscription = async () => { throw new Error('IAP 不可用'); };
getAvailablePurchases = async () => [];
finishTransaction = async () => {};
purchaseUpdatedListener = () => ({ remove: () => {} });
purchaseErrorListener = () => ({ remove: () => {} });
}
import { API_BASE_URL } from './api'; import { API_BASE_URL } from './api';
// 苹果内购产品 ID 配置 // 苹果内购产品 ID 配置
@@ -124,6 +155,11 @@ class IAPService {
return false; return false;
} }
if (!IAP_AVAILABLE) {
console.log('[IAPService] IAP 模块不可用,跳过初始化');
return false;
}
if (this.isInitialized) { if (this.isInitialized) {
console.log('[IAPService] 已经初始化,跳过'); console.log('[IAPService] 已经初始化,跳过');
return true; return true;

View File

@@ -1,13 +1,13 @@
/** /**
* 服务层 * 服务层
* 处理频道消息帖子成员等 API 调用 * 处理频道消息帖子成员通知 API 调用
* Web 端保持一致使用 ElasticSearch API 读取数据 * 频道/消息使用 ElasticSearch API通知使用后端 REST API
*/ */
import { apiRequest, API_BASE_URL } from './api'; import { apiRequest, API_BASE_URL } from './api';
// ES API 基础路径 // ES API 基础路径(用于频道消息等)
const ES_API_BASE = `${API_BASE_URL}/api/community/es`; const ES_API_BASE = `${API_BASE_URL}/api/social/es`;
// 频道类型 // 频道类型
export const CHANNEL_TYPES = { export const CHANNEL_TYPES = {
@@ -35,13 +35,13 @@ export const channelService = {
try { try {
const response = await fetch(`${API_BASE_URL}/api/community/channels`); const response = await fetch(`${API_BASE_URL}/api/community/channels`);
if (!response.ok) { if (!response.ok) {
console.warn('[CommunityService] 获取频道失败,使用默认数据'); console.warn('[SocialService] 获取频道失败,使用默认数据');
return getDefaultChannels(); return getDefaultChannels();
} }
const data = await response.json(); const data = await response.json();
return { categories: data.data || [] }; return { categories: data.data || [] };
} catch (error) { } catch (error) {
console.warn('[CommunityService] getChannels: 使用默认数据'); console.warn('[SocialService] getChannels: 使用默认数据');
return getDefaultChannels(); return getDefaultChannels();
} }
}, },
@@ -56,7 +56,7 @@ export const channelService = {
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.warn('[CommunityService] getChannelDetail 错误:', error); console.warn('[SocialService] getChannelDetail 错误:', error);
throw error; throw error;
} }
}, },
@@ -72,7 +72,7 @@ export const channelService = {
}); });
if (!response.ok) throw new Error('订阅频道失败'); if (!response.ok) throw new Error('订阅频道失败');
} catch (error) { } catch (error) {
console.warn('[CommunityService] subscribeChannel 错误:', error); console.warn('[SocialService] subscribeChannel 错误:', error);
throw error; throw error;
} }
}, },
@@ -88,7 +88,7 @@ export const channelService = {
}); });
if (!response.ok) throw new Error('取消订阅失败'); if (!response.ok) throw new Error('取消订阅失败');
} catch (error) { } catch (error) {
console.warn('[CommunityService] unsubscribeChannel 错误:', error); console.warn('[SocialService] unsubscribeChannel 错误:', error);
throw error; throw error;
} }
}, },
@@ -112,7 +112,7 @@ export const channelService = {
const result = await response.json(); const result = await response.json();
return result.data; return result.data;
} catch (error) { } catch (error) {
console.warn('[CommunityService] createChannel 错误:', error); console.warn('[SocialService] createChannel 错误:', error);
throw error; throw error;
} }
}, },
@@ -159,7 +159,7 @@ export const messageService = {
}); });
if (!response.ok) { if (!response.ok) {
console.warn('[CommunityService] getMessages 失败'); console.warn('[SocialService] getMessages 失败');
return { data: [], hasMore: false }; return { data: [], hasMore: false };
} }
@@ -193,7 +193,7 @@ export const messageService = {
hasMore: data.hits.total.value > items.length, hasMore: data.hits.total.value > items.length,
}; };
} catch (error) { } catch (error) {
console.warn('[CommunityService] getMessages 错误:', error); console.warn('[SocialService] getMessages 错误:', error);
return { data: [], hasMore: false }; return { data: [], hasMore: false };
} }
}, },
@@ -214,7 +214,7 @@ export const messageService = {
const result = await response.json(); const result = await response.json();
return { data: result.data }; return { data: result.data };
} catch (error) { } catch (error) {
console.warn('[CommunityService] sendMessage 错误:', error); console.warn('[SocialService] sendMessage 错误:', error);
throw error; throw error;
} }
}, },
@@ -230,7 +230,7 @@ export const messageService = {
}); });
if (!response.ok) throw new Error('删除消息失败'); if (!response.ok) throw new Error('删除消息失败');
} catch (error) { } catch (error) {
console.warn('[CommunityService] deleteMessage 错误:', error); console.warn('[SocialService] deleteMessage 错误:', error);
throw error; throw error;
} }
}, },
@@ -248,7 +248,7 @@ export const messageService = {
}); });
if (!response.ok) throw new Error('添加表情失败'); if (!response.ok) throw new Error('添加表情失败');
} catch (error) { } catch (error) {
console.warn('[CommunityService] addReaction 错误:', error); console.warn('[SocialService] addReaction 错误:', error);
throw error; throw error;
} }
}, },
@@ -296,7 +296,7 @@ export const postService = {
}); });
if (!response.ok) { if (!response.ok) {
console.warn('[CommunityService] getPosts 失败'); console.warn('[SocialService] getPosts 失败');
return { data: [], hasMore: false }; return { data: [], hasMore: false };
} }
@@ -329,7 +329,7 @@ export const postService = {
hasMore: page * limit < data.hits.total.value, hasMore: page * limit < data.hits.total.value,
}; };
} catch (error) { } catch (error) {
console.warn('[CommunityService] getPosts 错误:', error); console.warn('[SocialService] getPosts 错误:', error);
return { data: [], hasMore: false }; return { data: [], hasMore: false };
} }
}, },
@@ -344,7 +344,7 @@ export const postService = {
const result = await response.json(); const result = await response.json();
return { data: result.data }; return { data: result.data };
} catch (error) { } catch (error) {
console.warn('[CommunityService] getPostDetail 错误:', error); console.warn('[SocialService] getPostDetail 错误:', error);
throw error; throw error;
} }
}, },
@@ -365,7 +365,7 @@ export const postService = {
const result = await response.json(); const result = await response.json();
return { data: result.data }; return { data: result.data };
} catch (error) { } catch (error) {
console.warn('[CommunityService] createPost 错误:', error); console.warn('[SocialService] createPost 错误:', error);
throw error; throw error;
} }
}, },
@@ -398,7 +398,7 @@ export const postService = {
}); });
if (!response.ok) { if (!response.ok) {
console.warn('[CommunityService] getReplies 失败'); console.warn('[SocialService] getReplies 失败');
return { data: [], hasMore: false }; return { data: [], hasMore: false };
} }
@@ -427,7 +427,7 @@ export const postService = {
hasMore: page * limit < data.hits.total.value, hasMore: page * limit < data.hits.total.value,
}; };
} catch (error) { } catch (error) {
console.warn('[CommunityService] getReplies 错误:', error); console.warn('[SocialService] getReplies 错误:', error);
return { data: [], hasMore: false }; return { data: [], hasMore: false };
} }
}, },
@@ -448,7 +448,7 @@ export const postService = {
const result = await response.json(); const result = await response.json();
return { data: result.data }; return { data: result.data };
} catch (error) { } catch (error) {
console.warn('[CommunityService] createReply 错误:', error); console.warn('[SocialService] createReply 错误:', error);
throw error; throw error;
} }
}, },
@@ -464,7 +464,7 @@ export const postService = {
}); });
if (!response.ok) throw new Error('点赞失败'); if (!response.ok) throw new Error('点赞失败');
} catch (error) { } catch (error) {
console.warn('[CommunityService] likePost 错误:', error); console.warn('[SocialService] likePost 错误:', error);
throw error; throw error;
} }
}, },
@@ -501,7 +501,7 @@ export const memberService = {
const data = await response.json(); const data = await response.json();
return { data: data.data || [] }; return { data: data.data || [] };
} catch (error) { } catch (error) {
console.warn('[CommunityService] getMembers 错误:', error); console.warn('[SocialService] getMembers 错误:', error);
return { data: [] }; return { data: [] };
} }
}, },
@@ -545,43 +545,124 @@ export const uploadService = {
const result = await response.json(); const result = await response.json();
return result.data || result; return result.data || result;
} catch (error) { } catch (error) {
console.warn('[CommunityService] uploadImage 错误:', error); console.warn('[SocialService] uploadImage 错误:', error);
throw error; throw error;
} }
}, },
}; };
/** /**
* 通知服务 * 通知服务 - 使用后端 API
*/ */
export const notificationService = { export const notificationService = {
/** /**
* 获取通知列表 * 获取通知列表
* @param {object} options - 分页和筛选选项
* @param {number} options.page - 页码默认 1
* @param {number} options.limit - 每页数量默认 20
* @param {boolean} options.unreadOnly - 是否只显示未读默认 false
* @returns {Promise<{data: Array, hasMore: boolean, unreadCount: number}>}
*/ */
getNotifications: async (options = {}) => { getNotifications: async (options = {}) => {
// TODO: 实现通知 API try {
return { data: [], hasMore: false }; const { page = 1, limit = 20, unreadOnly = false } = options;
const params = new URLSearchParams({
page: String(page),
limit: String(limit),
unread_only: String(unreadOnly),
});
const response = await fetch(`${API_BASE_URL}/api/social/notifications?${params}`, {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
console.warn('[SocialService] getNotifications 失败,返回空列表');
return { data: [], hasMore: false, unreadCount: 0 };
}
const result = await response.json();
const data = result.data || {};
return {
data: data.notifications || [],
hasMore: data.hasMore || false,
unreadCount: data.unreadCount || 0,
};
} catch (error) {
console.warn('[SocialService] getNotifications 错误:', error);
return { data: [], hasMore: false, unreadCount: 0 };
}
}, },
/** /**
* 标记通知为已读 * 标记通知为已读
* @param {string} notificationId - 通知 ID
*/ */
markAsRead: async (notificationId) => { markAsRead: async (notificationId) => {
// TODO: 实现标记已读 API try {
const response = await fetch(
`${API_BASE_URL}/api/social/notifications/${notificationId}/read`,
{
method: 'POST',
credentials: 'include',
}
);
if (!response.ok) {
throw new Error('标记已读失败');
}
} catch (error) {
console.warn('[SocialService] markAsRead 错误:', error);
throw error;
}
}, },
/** /**
* 标记所有通知为已读 * 标记所有通知为已读
*/ */
markAllAsRead: async () => { markAllAsRead: async () => {
// TODO: 实现标记全部已读 API try {
const response = await fetch(
`${API_BASE_URL}/api/social/notifications/read-all`,
{
method: 'POST',
credentials: 'include',
}
);
if (!response.ok) {
throw new Error('标记全部已读失败');
}
} catch (error) {
console.warn('[SocialService] markAllAsRead 错误:', error);
throw error;
}
}, },
/** /**
* 获取未读通知数量 * 获取未读通知数量
* @returns {Promise<number>}
*/ */
getUnreadCount: async () => { getUnreadCount: async () => {
return 0; try {
const response = await fetch(`${API_BASE_URL}/api/social/notifications/unread-count`, {
method: 'GET',
credentials: 'include',
});
if (!response.ok) {
return 0;
}
const result = await response.json();
return result.data?.count || 0;
} catch (error) {
console.warn('[SocialService] getUnreadCount 错误:', error);
return 0;
}
}, },
}; };

View File

@@ -6,7 +6,7 @@ import { configureStore } from '@reduxjs/toolkit';
import eventsReducer from './slices/eventsSlice'; import eventsReducer from './slices/eventsSlice';
import watchlistReducer from './slices/watchlistSlice'; import watchlistReducer from './slices/watchlistSlice';
import stockReducer from './slices/stockSlice'; import stockReducer from './slices/stockSlice';
import communityReducer from './slices/communitySlice'; import socialReducer from './slices/socialSlice';
import agentReducer from './slices/agentSlice'; import agentReducer from './slices/agentSlice';
const store = configureStore({ const store = configureStore({
@@ -14,7 +14,7 @@ const store = configureStore({
events: eventsReducer, events: eventsReducer,
watchlist: watchlistReducer, watchlist: watchlistReducer,
stock: stockReducer, stock: stockReducer,
community: communityReducer, social: socialReducer,
agent: agentReducer, agent: agentReducer,
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>

View File

@@ -1,5 +1,5 @@
/** /**
* Redux Slice * Redux Slice
* 管理频道消息帖子成员通知状态 * 管理频道消息帖子成员通知状态
*/ */
@@ -10,13 +10,13 @@ import {
postService, postService,
memberService, memberService,
notificationService, notificationService,
} from '../../services/communityService'; } from '../../services/socialService';
// ============ Async Thunks ============ // ============ Async Thunks ============
// 获取频道列表 // 获取频道列表
export const fetchChannels = createAsyncThunk( export const fetchChannels = createAsyncThunk(
'community/fetchChannels', 'social/fetchChannels',
async (_, { rejectWithValue }) => { async (_, { rejectWithValue }) => {
try { try {
const data = await channelService.getChannels(); const data = await channelService.getChannels();
@@ -29,7 +29,7 @@ export const fetchChannels = createAsyncThunk(
// 获取频道消息 // 获取频道消息
export const fetchMessages = createAsyncThunk( export const fetchMessages = createAsyncThunk(
'community/fetchMessages', 'social/fetchMessages',
async ({ channelId, options = {} }, { rejectWithValue }) => { async ({ channelId, options = {} }, { rejectWithValue }) => {
try { try {
const response = await messageService.getMessages(channelId, options); const response = await messageService.getMessages(channelId, options);
@@ -42,7 +42,7 @@ export const fetchMessages = createAsyncThunk(
// 发送消息 // 发送消息
export const sendMessage = createAsyncThunk( export const sendMessage = createAsyncThunk(
'community/sendMessage', 'social/sendMessage',
async ({ channelId, data }, { rejectWithValue }) => { async ({ channelId, data }, { rejectWithValue }) => {
try { try {
const response = await messageService.sendMessage(channelId, data); const response = await messageService.sendMessage(channelId, data);
@@ -55,7 +55,7 @@ export const sendMessage = createAsyncThunk(
// 获取帖子列表 // 获取帖子列表
export const fetchPosts = createAsyncThunk( export const fetchPosts = createAsyncThunk(
'community/fetchPosts', 'social/fetchPosts',
async ({ channelId, options = {} }, { rejectWithValue }) => { async ({ channelId, options = {} }, { rejectWithValue }) => {
try { try {
const response = await postService.getPosts(channelId, options); const response = await postService.getPosts(channelId, options);
@@ -68,7 +68,7 @@ export const fetchPosts = createAsyncThunk(
// 获取帖子详情 // 获取帖子详情
export const fetchPostDetail = createAsyncThunk( export const fetchPostDetail = createAsyncThunk(
'community/fetchPostDetail', 'social/fetchPostDetail',
async (postId, { rejectWithValue }) => { async (postId, { rejectWithValue }) => {
try { try {
const response = await postService.getPostDetail(postId); const response = await postService.getPostDetail(postId);
@@ -81,7 +81,7 @@ export const fetchPostDetail = createAsyncThunk(
// 创建帖子 // 创建帖子
export const createPost = createAsyncThunk( export const createPost = createAsyncThunk(
'community/createPost', 'social/createPost',
async ({ channelId, data }, { rejectWithValue }) => { async ({ channelId, data }, { rejectWithValue }) => {
try { try {
const response = await postService.createPost(channelId, data); const response = await postService.createPost(channelId, data);
@@ -94,7 +94,7 @@ export const createPost = createAsyncThunk(
// 获取帖子回复 // 获取帖子回复
export const fetchReplies = createAsyncThunk( export const fetchReplies = createAsyncThunk(
'community/fetchReplies', 'social/fetchReplies',
async ({ postId, options = {} }, { rejectWithValue }) => { async ({ postId, options = {} }, { rejectWithValue }) => {
try { try {
const response = await postService.getReplies(postId, options); const response = await postService.getReplies(postId, options);
@@ -107,7 +107,7 @@ export const fetchReplies = createAsyncThunk(
// 创建回复 // 创建回复
export const createReply = createAsyncThunk( export const createReply = createAsyncThunk(
'community/createReply', 'social/createReply',
async ({ postId, data }, { rejectWithValue }) => { async ({ postId, data }, { rejectWithValue }) => {
try { try {
const response = await postService.createReply(postId, data); const response = await postService.createReply(postId, data);
@@ -120,7 +120,7 @@ export const createReply = createAsyncThunk(
// 获取成员列表 // 获取成员列表
export const fetchMembers = createAsyncThunk( export const fetchMembers = createAsyncThunk(
'community/fetchMembers', 'social/fetchMembers',
async ({ channelId, options = {} }, { rejectWithValue }) => { async ({ channelId, options = {} }, { rejectWithValue }) => {
try { try {
const response = await memberService.getMembers(channelId, options); const response = await memberService.getMembers(channelId, options);
@@ -133,11 +133,15 @@ export const fetchMembers = createAsyncThunk(
// 获取通知列表 // 获取通知列表
export const fetchNotifications = createAsyncThunk( export const fetchNotifications = createAsyncThunk(
'community/fetchNotifications', 'social/fetchNotifications',
async (options = {}, { rejectWithValue }) => { async (options = {}, { rejectWithValue }) => {
try { try {
const response = await notificationService.getNotifications(options); const response = await notificationService.getNotifications(options);
return { notifications: response.data || [], hasMore: response.hasMore }; return {
notifications: response.data || [],
hasMore: response.hasMore,
unreadCount: response.unreadCount || 0,
};
} catch (error) { } catch (error) {
return rejectWithValue(error.message); return rejectWithValue(error.message);
} }
@@ -146,7 +150,7 @@ export const fetchNotifications = createAsyncThunk(
// 标记通知已读 // 标记通知已读
export const markNotificationRead = createAsyncThunk( export const markNotificationRead = createAsyncThunk(
'community/markNotificationRead', 'social/markNotificationRead',
async (notificationId, { rejectWithValue }) => { async (notificationId, { rejectWithValue }) => {
try { try {
await notificationService.markAsRead(notificationId); await notificationService.markAsRead(notificationId);
@@ -159,7 +163,7 @@ export const markNotificationRead = createAsyncThunk(
// 标记所有通知已读 // 标记所有通知已读
export const markAllNotificationsRead = createAsyncThunk( export const markAllNotificationsRead = createAsyncThunk(
'community/markAllNotificationsRead', 'social/markAllNotificationsRead',
async (_, { rejectWithValue }) => { async (_, { rejectWithValue }) => {
try { try {
await notificationService.markAllAsRead(); await notificationService.markAllAsRead();
@@ -218,8 +222,8 @@ const initialState = {
// ============ Slice ============ // ============ Slice ============
const communitySlice = createSlice({ const socialSlice = createSlice({
name: 'community', name: 'social',
initialState, initialState,
reducers: { reducers: {
// 设置当前频道 // 设置当前频道
@@ -320,7 +324,7 @@ const communitySlice = createSlice({
}, },
// 重置状态 // 重置状态
resetCommunityState: () => initialState, resetSocialState: () => initialState,
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
@@ -462,9 +466,13 @@ const communitySlice = createSlice({
}) })
.addCase(fetchNotifications.fulfilled, (state, action) => { .addCase(fetchNotifications.fulfilled, (state, action) => {
state.loading.notifications = false; state.loading.notifications = false;
const { notifications, hasMore } = action.payload; const { notifications, hasMore, unreadCount } = action.payload;
state.notifications = notifications; state.notifications = notifications;
state.notificationsHasMore = hasMore; state.notificationsHasMore = hasMore;
// 更新未读数量(从服务器返回)
if (typeof unreadCount === 'number') {
state.unreadCount = unreadCount;
}
}) })
.addCase(fetchNotifications.rejected, (state, action) => { .addCase(fetchNotifications.rejected, (state, action) => {
state.loading.notifications = false; state.loading.notifications = false;
@@ -506,8 +514,8 @@ export const {
setUnreadCount, setUnreadCount,
incrementUnreadCount, incrementUnreadCount,
clearError, clearError,
resetCommunityState, resetSocialState,
} = communitySlice.actions; } = socialSlice.actions;
// 导出 reducer // 导出 reducer
export default communitySlice.reducer; export default socialSlice.reducer;

152
app.py
View File

@@ -1573,6 +1573,52 @@ class UserDeviceToken(db.Model):
user = db.relationship('User', backref='device_tokens') user = db.relationship('User', backref='device_tokens')
class SocialNotification(db.Model):
"""社交通知表 - 用于社区/社交功能的通知"""
__tablename__ = 'social_notifications'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, index=True)
# 通知类型: reply(回复), mention(提及), like(点赞), follow(关注), system(系统)
type = db.Column(db.String(20), nullable=False, default='system')
# 发送者信息
sender_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
sender_name = db.Column(db.String(100), nullable=True)
sender_avatar = db.Column(db.String(500), nullable=True)
# 通知内容
content = db.Column(db.Text, nullable=True)
# 关联目标: post(帖子), reply(回复), channel(频道), message(消息)
target_type = db.Column(db.String(20), nullable=True)
target_id = db.Column(db.String(50), nullable=True)
# 状态
is_read = db.Column(db.Boolean, default=False, index=True)
created_at = db.Column(db.DateTime, default=beijing_now, index=True)
# 关系
user = db.relationship('User', foreign_keys=[user_id], backref='social_notifications')
sender = db.relationship('User', foreign_keys=[sender_id])
def to_dict(self):
return {
'id': str(self.id),
'type': self.type,
'senderId': self.sender_id,
'senderName': self.sender_name,
'senderAvatar': self.sender_avatar,
'content': self.content,
'targetType': self.target_type,
'targetId': self.target_id,
'isRead': self.is_read,
'createdAt': self.created_at.isoformat() if self.created_at else None,
}
class SubscriptionPlan(db.Model): class SubscriptionPlan(db.Model):
"""订阅套餐表""" """订阅套餐表"""
__tablename__ = 'subscription_plans' __tablename__ = 'subscription_plans'
@@ -21201,6 +21247,112 @@ init_prediction_api(db, beijing_now)
app.register_blueprint(prediction_bp) app.register_blueprint(prediction_bp)
# ==================== 社交通知 API ====================
@app.route('/api/social/notifications', methods=['GET'])
@login_required
def get_social_notifications():
"""获取社交通知列表"""
try:
page = request.args.get('page', 1, type=int)
limit = request.args.get('limit', 20, type=int)
unread_only = request.args.get('unread_only', 'false').lower() == 'true'
query = SocialNotification.query.filter_by(user_id=current_user.id)
if unread_only:
query = query.filter_by(is_read=False)
# 获取总未读数
unread_count = SocialNotification.query.filter_by(
user_id=current_user.id,
is_read=False
).count()
# 分页查询
total = query.count()
notifications = query.order_by(SocialNotification.created_at.desc()) \
.offset((page - 1) * limit) \
.limit(limit) \
.all()
return jsonify({
'code': 200,
'message': 'success',
'data': {
'notifications': [n.to_dict() for n in notifications],
'hasMore': page * limit < total,
'unreadCount': unread_count,
'total': total,
}
})
except Exception as e:
app.logger.error(f"获取社交通知失败: {e}")
return jsonify({'code': 500, 'message': str(e)}), 500
@app.route('/api/social/notifications/<notification_id>/read', methods=['POST'])
@login_required
def mark_social_notification_read(notification_id):
"""标记单个通知为已读"""
try:
notification = SocialNotification.query.filter_by(
id=notification_id,
user_id=current_user.id
).first()
if not notification:
return jsonify({'code': 404, 'message': '通知不存在'}), 404
notification.is_read = True
db.session.commit()
return jsonify({'code': 200, 'message': 'success'})
except Exception as e:
db.session.rollback()
app.logger.error(f"标记通知已读失败: {e}")
return jsonify({'code': 500, 'message': str(e)}), 500
@app.route('/api/social/notifications/read-all', methods=['POST'])
@login_required
def mark_all_social_notifications_read():
"""标记所有通知为已读"""
try:
SocialNotification.query.filter_by(
user_id=current_user.id,
is_read=False
).update({'is_read': True})
db.session.commit()
return jsonify({'code': 200, 'message': 'success'})
except Exception as e:
db.session.rollback()
app.logger.error(f"标记所有通知已读失败: {e}")
return jsonify({'code': 500, 'message': str(e)}), 500
@app.route('/api/social/notifications/unread-count', methods=['GET'])
@login_required
def get_social_notifications_unread_count():
"""获取未读通知数量"""
try:
count = SocialNotification.query.filter_by(
user_id=current_user.id,
is_read=False
).count()
return jsonify({
'code': 200,
'message': 'success',
'data': {'count': count}
})
except Exception as e:
app.logger.error(f"获取未读通知数量失败: {e}")
return jsonify({'code': 500, 'message': str(e)}), 500
if __name__ == '__main__': if __name__ == '__main__':
# 创建数据库表 # 创建数据库表
with app.app_context(): with app.app_context():