diff --git a/argon-pro-react-native/.gitignore b/MeAgent/.gitignore similarity index 100% rename from argon-pro-react-native/.gitignore rename to MeAgent/.gitignore diff --git a/argon-pro-react-native/.watchmanconfig b/MeAgent/.watchmanconfig similarity index 100% rename from argon-pro-react-native/.watchmanconfig rename to MeAgent/.watchmanconfig diff --git a/argon-pro-react-native/App.js b/MeAgent/App.js similarity index 100% rename from argon-pro-react-native/App.js rename to MeAgent/App.js diff --git a/argon-pro-react-native/AuthKey_HSF578B626.p8 b/MeAgent/AuthKey_HSF578B626.p8 similarity index 100% rename from argon-pro-react-native/AuthKey_HSF578B626.p8 rename to MeAgent/AuthKey_HSF578B626.p8 diff --git a/argon-pro-react-native/README.md b/MeAgent/README.md similarity index 100% rename from argon-pro-react-native/README.md rename to MeAgent/README.md diff --git a/argon-pro-react-native/app.json b/MeAgent/app.json similarity index 96% rename from argon-pro-react-native/app.json rename to MeAgent/app.json index 0b90eb7d..9da3017a 100644 --- a/argon-pro-react-native/app.json +++ b/MeAgent/app.json @@ -24,6 +24,7 @@ "ios": { "supportsTablet": true, "bundleIdentifier": "com.valuefrontier.meagent", + "deploymentTarget": "15.1", "infoPlist": { "UIBackgroundModes": ["remote-notification"] } diff --git a/argon-pro-react-native/assets/config/argon.json b/MeAgent/assets/config/argon.json similarity index 100% rename from argon-pro-react-native/assets/config/argon.json rename to MeAgent/assets/config/argon.json diff --git a/argon-pro-react-native/assets/font/OpenSans-Bold.ttf b/MeAgent/assets/font/OpenSans-Bold.ttf similarity index 100% rename from argon-pro-react-native/assets/font/OpenSans-Bold.ttf rename to MeAgent/assets/font/OpenSans-Bold.ttf diff --git a/argon-pro-react-native/assets/font/OpenSans-Light.ttf b/MeAgent/assets/font/OpenSans-Light.ttf similarity index 100% rename from argon-pro-react-native/assets/font/OpenSans-Light.ttf rename to MeAgent/assets/font/OpenSans-Light.ttf diff --git a/argon-pro-react-native/assets/font/OpenSans-Regular.ttf b/MeAgent/assets/font/OpenSans-Regular.ttf similarity index 100% rename from argon-pro-react-native/assets/font/OpenSans-Regular.ttf rename to MeAgent/assets/font/OpenSans-Regular.ttf diff --git a/argon-pro-react-native/assets/font/argon.ttf b/MeAgent/assets/font/argon.ttf similarity index 100% rename from argon-pro-react-native/assets/font/argon.ttf rename to MeAgent/assets/font/argon.ttf diff --git a/argon-pro-react-native/assets/icon.png b/MeAgent/assets/icon.png similarity index 100% rename from argon-pro-react-native/assets/icon.png rename to MeAgent/assets/icon.png diff --git a/argon-pro-react-native/assets/imgs/android.png b/MeAgent/assets/imgs/android.png similarity index 100% rename from argon-pro-react-native/assets/imgs/android.png rename to MeAgent/assets/imgs/android.png diff --git a/argon-pro-react-native/assets/imgs/argon-logo-onboarding.png b/MeAgent/assets/imgs/argon-logo-onboarding.png similarity index 100% rename from argon-pro-react-native/assets/imgs/argon-logo-onboarding.png rename to MeAgent/assets/imgs/argon-logo-onboarding.png diff --git a/argon-pro-react-native/assets/imgs/argon-logo-onboarding@2x.png b/MeAgent/assets/imgs/argon-logo-onboarding@2x.png similarity index 100% rename from argon-pro-react-native/assets/imgs/argon-logo-onboarding@2x.png rename to MeAgent/assets/imgs/argon-logo-onboarding@2x.png diff --git a/argon-pro-react-native/assets/imgs/argon-logo.png b/MeAgent/assets/imgs/argon-logo.png similarity index 100% rename from argon-pro-react-native/assets/imgs/argon-logo.png rename to MeAgent/assets/imgs/argon-logo.png diff --git a/argon-pro-react-native/assets/imgs/argon-logo@2x.png b/MeAgent/assets/imgs/argon-logo@2x.png similarity index 100% rename from argon-pro-react-native/assets/imgs/argon-logo@2x.png rename to MeAgent/assets/imgs/argon-logo@2x.png diff --git a/argon-pro-react-native/assets/imgs/argonlogo.png b/MeAgent/assets/imgs/argonlogo.png similarity index 100% rename from argon-pro-react-native/assets/imgs/argonlogo.png rename to MeAgent/assets/imgs/argonlogo.png diff --git a/argon-pro-react-native/assets/imgs/bg.png b/MeAgent/assets/imgs/bg.png similarity index 100% rename from argon-pro-react-native/assets/imgs/bg.png rename to MeAgent/assets/imgs/bg.png diff --git a/argon-pro-react-native/assets/imgs/getPro-bg.png b/MeAgent/assets/imgs/getPro-bg.png similarity index 100% rename from argon-pro-react-native/assets/imgs/getPro-bg.png rename to MeAgent/assets/imgs/getPro-bg.png diff --git a/argon-pro-react-native/assets/imgs/getPro-bg@2x.png b/MeAgent/assets/imgs/getPro-bg@2x.png similarity index 100% rename from argon-pro-react-native/assets/imgs/getPro-bg@2x.png rename to MeAgent/assets/imgs/getPro-bg@2x.png diff --git a/argon-pro-react-native/assets/imgs/icon.png b/MeAgent/assets/imgs/icon.png similarity index 100% rename from argon-pro-react-native/assets/imgs/icon.png rename to MeAgent/assets/imgs/icon.png diff --git a/argon-pro-react-native/assets/imgs/ios.png b/MeAgent/assets/imgs/ios.png similarity index 100% rename from argon-pro-react-native/assets/imgs/ios.png rename to MeAgent/assets/imgs/ios.png diff --git a/argon-pro-react-native/assets/imgs/profile-img.jpg b/MeAgent/assets/imgs/profile-img.jpg similarity index 100% rename from argon-pro-react-native/assets/imgs/profile-img.jpg rename to MeAgent/assets/imgs/profile-img.jpg diff --git a/argon-pro-react-native/assets/imgs/profile-screen-bg.png b/MeAgent/assets/imgs/profile-screen-bg.png similarity index 100% rename from argon-pro-react-native/assets/imgs/profile-screen-bg.png rename to MeAgent/assets/imgs/profile-screen-bg.png diff --git a/argon-pro-react-native/assets/imgs/register-bg.png b/MeAgent/assets/imgs/register-bg.png similarity index 100% rename from argon-pro-react-native/assets/imgs/register-bg.png rename to MeAgent/assets/imgs/register-bg.png diff --git a/argon-pro-react-native/assets/imgs/splash.png b/MeAgent/assets/imgs/splash.png similarity index 100% rename from argon-pro-react-native/assets/imgs/splash.png rename to MeAgent/assets/imgs/splash.png diff --git a/argon-pro-react-native/assets/logo.jpg b/MeAgent/assets/logo.jpg similarity index 100% rename from argon-pro-react-native/assets/logo.jpg rename to MeAgent/assets/logo.jpg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/bag-17.svg b/MeAgent/assets/nucleo icons/svg/bag-17.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/bag-17.svg rename to MeAgent/assets/nucleo icons/svg/bag-17.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/basket.svg b/MeAgent/assets/nucleo icons/svg/basket.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/basket.svg rename to MeAgent/assets/nucleo icons/svg/basket.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/bell.svg b/MeAgent/assets/nucleo icons/svg/bell.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/bell.svg rename to MeAgent/assets/nucleo icons/svg/bell.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/calendar-date.svg b/MeAgent/assets/nucleo icons/svg/calendar-date.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/calendar-date.svg rename to MeAgent/assets/nucleo icons/svg/calendar-date.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/chart-pie-35.svg b/MeAgent/assets/nucleo icons/svg/chart-pie-35.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/chart-pie-35.svg rename to MeAgent/assets/nucleo icons/svg/chart-pie-35.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/diamond.svg b/MeAgent/assets/nucleo icons/svg/diamond.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/diamond.svg rename to MeAgent/assets/nucleo icons/svg/diamond.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/engine-start.svg b/MeAgent/assets/nucleo icons/svg/engine-start.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/engine-start.svg rename to MeAgent/assets/nucleo icons/svg/engine-start.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/g-check.svg b/MeAgent/assets/nucleo icons/svg/g-check.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/g-check.svg rename to MeAgent/assets/nucleo icons/svg/g-check.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/hat-3.svg b/MeAgent/assets/nucleo icons/svg/hat-3.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/hat-3.svg rename to MeAgent/assets/nucleo icons/svg/hat-3.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/ic_grain_48px.svg b/MeAgent/assets/nucleo icons/svg/ic_grain_48px.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/ic_grain_48px.svg rename to MeAgent/assets/nucleo icons/svg/ic_grain_48px.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/ic_mail_24px.svg b/MeAgent/assets/nucleo icons/svg/ic_mail_24px.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/ic_mail_24px.svg rename to MeAgent/assets/nucleo icons/svg/ic_mail_24px.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/map-big.svg b/MeAgent/assets/nucleo icons/svg/map-big.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/map-big.svg rename to MeAgent/assets/nucleo icons/svg/map-big.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/menu-8.svg b/MeAgent/assets/nucleo icons/svg/menu-8.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/menu-8.svg rename to MeAgent/assets/nucleo icons/svg/menu-8.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/nav-down.svg b/MeAgent/assets/nucleo icons/svg/nav-down.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/nav-down.svg rename to MeAgent/assets/nucleo icons/svg/nav-down.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/nav-left.svg b/MeAgent/assets/nucleo icons/svg/nav-left.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/nav-left.svg rename to MeAgent/assets/nucleo icons/svg/nav-left.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/nav-right.svg b/MeAgent/assets/nucleo icons/svg/nav-right.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/nav-right.svg rename to MeAgent/assets/nucleo icons/svg/nav-right.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/padlock-unlocked.svg b/MeAgent/assets/nucleo icons/svg/padlock-unlocked.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/padlock-unlocked.svg rename to MeAgent/assets/nucleo icons/svg/padlock-unlocked.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/palette.svg b/MeAgent/assets/nucleo icons/svg/palette.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/palette.svg rename to MeAgent/assets/nucleo icons/svg/palette.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/search-zoom-in.svg b/MeAgent/assets/nucleo icons/svg/search-zoom-in.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/search-zoom-in.svg rename to MeAgent/assets/nucleo icons/svg/search-zoom-in.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/shop.svg b/MeAgent/assets/nucleo icons/svg/shop.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/shop.svg rename to MeAgent/assets/nucleo icons/svg/shop.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/spaceship.svg b/MeAgent/assets/nucleo icons/svg/spaceship.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/spaceship.svg rename to MeAgent/assets/nucleo icons/svg/spaceship.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/support.svg b/MeAgent/assets/nucleo icons/svg/support.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/support.svg rename to MeAgent/assets/nucleo icons/svg/support.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/switches.svg b/MeAgent/assets/nucleo icons/svg/switches.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/switches.svg rename to MeAgent/assets/nucleo icons/svg/switches.svg diff --git a/argon-pro-react-native/assets/nucleo icons/svg/ungroup.svg b/MeAgent/assets/nucleo icons/svg/ungroup.svg similarity index 100% rename from argon-pro-react-native/assets/nucleo icons/svg/ungroup.svg rename to MeAgent/assets/nucleo icons/svg/ungroup.svg diff --git a/argon-pro-react-native/assets/splash.png b/MeAgent/assets/splash.png similarity index 100% rename from argon-pro-react-native/assets/splash.png rename to MeAgent/assets/splash.png diff --git a/argon-pro-react-native/babel.config.js b/MeAgent/babel.config.js similarity index 100% rename from argon-pro-react-native/babel.config.js rename to MeAgent/babel.config.js diff --git a/argon-pro-react-native/components/Button.js b/MeAgent/components/Button.js similarity index 100% rename from argon-pro-react-native/components/Button.js rename to MeAgent/components/Button.js diff --git a/argon-pro-react-native/components/Card.js b/MeAgent/components/Card.js similarity index 100% rename from argon-pro-react-native/components/Card.js rename to MeAgent/components/Card.js diff --git a/argon-pro-react-native/components/DrawerItem.js b/MeAgent/components/DrawerItem.js similarity index 100% rename from argon-pro-react-native/components/DrawerItem.js rename to MeAgent/components/DrawerItem.js diff --git a/argon-pro-react-native/components/Header.js b/MeAgent/components/Header.js similarity index 100% rename from argon-pro-react-native/components/Header.js rename to MeAgent/components/Header.js diff --git a/argon-pro-react-native/components/Icon.js b/MeAgent/components/Icon.js similarity index 100% rename from argon-pro-react-native/components/Icon.js rename to MeAgent/components/Icon.js diff --git a/argon-pro-react-native/components/Input.js b/MeAgent/components/Input.js similarity index 100% rename from argon-pro-react-native/components/Input.js rename to MeAgent/components/Input.js diff --git a/argon-pro-react-native/components/Notification.js b/MeAgent/components/Notification.js similarity index 100% rename from argon-pro-react-native/components/Notification.js rename to MeAgent/components/Notification.js diff --git a/argon-pro-react-native/components/Select.js b/MeAgent/components/Select.js similarity index 100% rename from argon-pro-react-native/components/Select.js rename to MeAgent/components/Select.js diff --git a/argon-pro-react-native/components/Switch.js b/MeAgent/components/Switch.js similarity index 100% rename from argon-pro-react-native/components/Switch.js rename to MeAgent/components/Switch.js diff --git a/argon-pro-react-native/components/Tabs.js b/MeAgent/components/Tabs.js similarity index 100% rename from argon-pro-react-native/components/Tabs.js rename to MeAgent/components/Tabs.js diff --git a/argon-pro-react-native/components/index.js b/MeAgent/components/index.js similarity index 100% rename from argon-pro-react-native/components/index.js rename to MeAgent/components/index.js diff --git a/argon-pro-react-native/constants/Images.js b/MeAgent/constants/Images.js similarity index 100% rename from argon-pro-react-native/constants/Images.js rename to MeAgent/constants/Images.js diff --git a/argon-pro-react-native/constants/Theme.js b/MeAgent/constants/Theme.js similarity index 100% rename from argon-pro-react-native/constants/Theme.js rename to MeAgent/constants/Theme.js diff --git a/argon-pro-react-native/constants/articles.js b/MeAgent/constants/articles.js similarity index 100% rename from argon-pro-react-native/constants/articles.js rename to MeAgent/constants/articles.js diff --git a/argon-pro-react-native/constants/cart.js b/MeAgent/constants/cart.js similarity index 100% rename from argon-pro-react-native/constants/cart.js rename to MeAgent/constants/cart.js diff --git a/argon-pro-react-native/constants/categories.js b/MeAgent/constants/categories.js similarity index 100% rename from argon-pro-react-native/constants/categories.js rename to MeAgent/constants/categories.js diff --git a/argon-pro-react-native/constants/deals.js b/MeAgent/constants/deals.js similarity index 100% rename from argon-pro-react-native/constants/deals.js rename to MeAgent/constants/deals.js diff --git a/argon-pro-react-native/constants/index.js b/MeAgent/constants/index.js similarity index 100% rename from argon-pro-react-native/constants/index.js rename to MeAgent/constants/index.js diff --git a/argon-pro-react-native/constants/tabs.js b/MeAgent/constants/tabs.js similarity index 100% rename from argon-pro-react-native/constants/tabs.js rename to MeAgent/constants/tabs.js diff --git a/argon-pro-react-native/constants/utils.js b/MeAgent/constants/utils.js similarity index 100% rename from argon-pro-react-native/constants/utils.js rename to MeAgent/constants/utils.js diff --git a/MeAgent/eas.json b/MeAgent/eas.json new file mode 100644 index 00000000..51284cc2 --- /dev/null +++ b/MeAgent/eas.json @@ -0,0 +1,28 @@ +{ + "cli": { + "version": ">= 12.0.0" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal", + "ios": { + "simulator": false + } + }, + "preview": { + "distribution": "internal", + "ios": { + "buildConfiguration": "Release" + } + }, + "production": { + "ios": { + "buildConfiguration": "Release" + } + } + }, + "submit": { + "production": {} + } +} diff --git a/argon-pro-react-native/index.js b/MeAgent/index.js similarity index 100% rename from argon-pro-react-native/index.js rename to MeAgent/index.js diff --git a/MeAgent/ios/.gitignore b/MeAgent/ios/.gitignore new file mode 100644 index 00000000..8beb3443 --- /dev/null +++ b/MeAgent/ios/.gitignore @@ -0,0 +1,30 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace +.xcode.env.local + +# Bundle artifacts +*.jsbundle + +# CocoaPods +/Pods/ diff --git a/MeAgent/ios/.xcode.env b/MeAgent/ios/.xcode.env new file mode 100644 index 00000000..3d5782c7 --- /dev/null +++ b/MeAgent/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/MeAgent/ios/Podfile b/MeAgent/ios/Podfile new file mode 100644 index 00000000..87b0637d --- /dev/null +++ b/MeAgent/ios/Podfile @@ -0,0 +1,87 @@ +require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") +require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods") + +require 'json' +podfile_properties = JSON.parse(File.read(File.join(__dir__, 'Podfile.properties.json'))) rescue {} + +ENV['RCT_NEW_ARCH_ENABLED'] = podfile_properties['newArchEnabled'] == 'true' ? '1' : '0' +ENV['EX_DEV_CLIENT_NETWORK_INSPECTOR'] = podfile_properties['EX_DEV_CLIENT_NETWORK_INSPECTOR'] + +use_autolinking_method_symbol = ('use' + '_native' + '_modules!').to_sym +origin_autolinking_method = self.method(use_autolinking_method_symbol) +self.define_singleton_method(use_autolinking_method_symbol) do |*args| + if ENV['EXPO_UNSTABLE_CORE_AUTOLINKING'] == '1' + Pod::UI.puts('Using expo-modules-autolinking as core autolinking source'.green) + config_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', + 'ios' + ] + origin_autolinking_method.call(config_command) + else + origin_autolinking_method.call() + end +end + +platform :ios, podfile_properties['ios.deploymentTarget'] || '13.4' +install! 'cocoapods', + :deterministic_uuids => false + +prepare_react_native_project! + +target 'app' do + use_expo_modules! + config = use_native_modules! + + use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks'] + use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS'] + + use_react_native!( + :path => config[:reactNativePath], + :hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes', + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/..", + :privacy_file_aggregation_enabled => podfile_properties['apple.privacyManifestAggregationEnabled'] != 'false', + ) + + post_install do |installer| + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + :ccache_enabled => podfile_properties['apple.ccacheEnabled'] == 'true', + ) + + # This is necessary for Xcode 14, because it signs resource bundles by default + # when building for devices. + installer.target_installation_results.pod_target_installation_results + .each do |pod_name, target_installation_result| + target_installation_result.resource_bundle_targets.each do |resource_bundle_target| + resource_bundle_target.build_configurations.each do |config| + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + end + end + end + + # 抑制第三方库的警告并统一部署目标 + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['GCC_WARN_INHIBIT_ALL_WARNINGS'] = 'YES' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.1' + end + end + end + + post_integrate do |installer| + begin + expo_patch_react_imports!(installer) + rescue => e + Pod::UI.warn e + end + end +end diff --git a/MeAgent/ios/Podfile.lock b/MeAgent/ios/Podfile.lock new file mode 100644 index 00000000..cc030802 --- /dev/null +++ b/MeAgent/ios/Podfile.lock @@ -0,0 +1,1658 @@ +PODS: + - boost (1.83.0) + - DoubleConversion (1.1.6) + - EXApplication (5.9.1): + - ExpoModulesCore + - EXConstants (16.0.2): + - ExpoModulesCore + - EXNotifications (0.28.19): + - ExpoModulesCore + - Expo (51.0.39): + - ExpoModulesCore + - ExpoAsset (10.0.10): + - ExpoModulesCore + - ExpoBlur (13.0.3): + - ExpoModulesCore + - ExpoClipboard (6.0.3): + - ExpoModulesCore + - ExpoDevice (6.0.2): + - ExpoModulesCore + - ExpoFileSystem (17.0.1): + - ExpoModulesCore + - ExpoFont (12.0.10): + - ExpoModulesCore + - ExpoKeepAwake (13.0.2): + - ExpoModulesCore + - ExpoLinearGradient (13.0.2): + - ExpoModulesCore + - ExpoModulesCore (1.12.26): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoWebBrowser (13.0.3): + - ExpoModulesCore + - EXSplashScreen (0.27.7): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - FBLazyVector (0.74.5) + - fmt (9.1.0) + - glog (0.3.5) + - hermes-engine (0.74.5): + - hermes-engine/Pre-built (= 0.74.5) + - hermes-engine/Pre-built (0.74.5) + - RCT-Folly (2024.01.01.00): + - boost + - DoubleConversion + - fmt (= 9.1.0) + - glog + - RCT-Folly/Default (= 2024.01.01.00) + - RCT-Folly/Default (2024.01.01.00): + - boost + - DoubleConversion + - fmt (= 9.1.0) + - glog + - RCT-Folly/Fabric (2024.01.01.00): + - boost + - DoubleConversion + - fmt (= 9.1.0) + - glog + - RCTDeprecation (0.74.5) + - RCTRequired (0.74.5) + - RCTTypeSafety (0.74.5): + - FBLazyVector (= 0.74.5) + - RCTRequired (= 0.74.5) + - React-Core (= 0.74.5) + - React (0.74.5): + - React-Core (= 0.74.5) + - React-Core/DevSupport (= 0.74.5) + - React-Core/RCTWebSocket (= 0.74.5) + - React-RCTActionSheet (= 0.74.5) + - React-RCTAnimation (= 0.74.5) + - React-RCTBlob (= 0.74.5) + - React-RCTImage (= 0.74.5) + - React-RCTLinking (= 0.74.5) + - React-RCTNetwork (= 0.74.5) + - React-RCTSettings (= 0.74.5) + - React-RCTText (= 0.74.5) + - React-RCTVibration (= 0.74.5) + - React-callinvoker (0.74.5) + - React-Codegen (0.74.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-jsi + - React-jsiexecutor + - React-NativeModulesApple + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-Core (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default (= 0.74.5) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/CoreModulesHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/Default (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/DevSupport (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default (= 0.74.5) + - React-Core/RCTWebSocket (= 0.74.5) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTActionSheetHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTAnimationHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTBlobHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTImageHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTLinkingHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTNetworkHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTSettingsHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTTextHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTVibrationHeaders (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-Core/RCTWebSocket (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTDeprecation + - React-Core/Default (= 0.74.5) + - React-cxxreact + - React-featureflags + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-perflogger + - React-runtimescheduler + - React-utils + - SocketRocket (= 0.7.0) + - Yoga + - React-CoreModules (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety (= 0.74.5) + - React-Codegen + - React-Core/CoreModulesHeaders (= 0.74.5) + - React-jsi (= 0.74.5) + - React-jsinspector + - React-NativeModulesApple + - React-RCTBlob + - React-RCTImage (= 0.74.5) + - ReactCommon + - SocketRocket (= 0.7.0) + - React-cxxreact (0.74.5): + - boost (= 1.83.0) + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker (= 0.74.5) + - React-debug (= 0.74.5) + - React-jsi (= 0.74.5) + - React-jsinspector + - React-logger (= 0.74.5) + - React-perflogger (= 0.74.5) + - React-runtimeexecutor (= 0.74.5) + - React-debug (0.74.5) + - React-Fabric (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/animations (= 0.74.5) + - React-Fabric/attributedstring (= 0.74.5) + - React-Fabric/componentregistry (= 0.74.5) + - React-Fabric/componentregistrynative (= 0.74.5) + - React-Fabric/components (= 0.74.5) + - React-Fabric/core (= 0.74.5) + - React-Fabric/imagemanager (= 0.74.5) + - React-Fabric/leakchecker (= 0.74.5) + - React-Fabric/mounting (= 0.74.5) + - React-Fabric/scheduler (= 0.74.5) + - React-Fabric/telemetry (= 0.74.5) + - React-Fabric/templateprocessor (= 0.74.5) + - React-Fabric/textlayoutmanager (= 0.74.5) + - React-Fabric/uimanager (= 0.74.5) + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/animations (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/attributedstring (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistry (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/componentregistrynative (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/components/inputaccessory (= 0.74.5) + - React-Fabric/components/legacyviewmanagerinterop (= 0.74.5) + - React-Fabric/components/modal (= 0.74.5) + - React-Fabric/components/rncore (= 0.74.5) + - React-Fabric/components/root (= 0.74.5) + - React-Fabric/components/safeareaview (= 0.74.5) + - React-Fabric/components/scrollview (= 0.74.5) + - React-Fabric/components/text (= 0.74.5) + - React-Fabric/components/textinput (= 0.74.5) + - React-Fabric/components/unimplementedview (= 0.74.5) + - React-Fabric/components/view (= 0.74.5) + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/inputaccessory (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/legacyviewmanagerinterop (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/modal (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/rncore (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/root (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/safeareaview (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/scrollview (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/text (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/textinput (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/unimplementedview (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/view (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - Yoga + - React-Fabric/core (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/imagemanager (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/leakchecker (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/mounting (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/scheduler (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/telemetry (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/templateprocessor (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/textlayoutmanager (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-Fabric/uimanager + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/uimanager (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-graphics + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-FabricImage (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - RCTRequired (= 0.74.5) + - RCTTypeSafety (= 0.74.5) + - React-Fabric + - React-graphics + - React-ImageManager + - React-jsi + - React-jsiexecutor (= 0.74.5) + - React-logger + - React-rendererdebug + - React-utils + - ReactCommon + - Yoga + - React-featureflags (0.74.5) + - React-graphics (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-Core/Default (= 0.74.5) + - React-utils + - React-hermes (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-cxxreact (= 0.74.5) + - React-jsi + - React-jsiexecutor (= 0.74.5) + - React-jsinspector + - React-perflogger (= 0.74.5) + - React-runtimeexecutor + - React-ImageManager (0.74.5): + - glog + - RCT-Folly/Fabric + - React-Core/Default + - React-debug + - React-Fabric + - React-graphics + - React-rendererdebug + - React-utils + - React-jserrorhandler (0.74.5): + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-debug + - React-jsi + - React-Mapbuffer + - React-jsi (0.74.5): + - boost (= 1.83.0) + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-jsiexecutor (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-cxxreact (= 0.74.5) + - React-jsi (= 0.74.5) + - React-jsinspector + - React-perflogger (= 0.74.5) + - React-jsinspector (0.74.5): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-featureflags + - React-jsi + - React-runtimeexecutor (= 0.74.5) + - React-jsitracing (0.74.5): + - React-jsi + - React-logger (0.74.5): + - glog + - React-Mapbuffer (0.74.5): + - glog + - React-debug + - react-native-safe-area-context (4.10.5): + - React-Core + - react-native-webview (13.8.6): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - React-nativeconfig (0.74.5) + - React-NativeModulesApple (0.74.5): + - glog + - hermes-engine + - React-callinvoker + - React-Core + - React-cxxreact + - React-jsi + - React-jsinspector + - React-runtimeexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-perflogger (0.74.5) + - React-RCTActionSheet (0.74.5): + - React-Core/RCTActionSheetHeaders (= 0.74.5) + - React-RCTAnimation (0.74.5): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTAnimationHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon + - React-RCTAppDelegate (0.74.5): + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-CoreModules + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-nativeconfig + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-RCTNetwork + - React-rendererdebug + - React-RuntimeApple + - React-RuntimeCore + - React-RuntimeHermes + - React-runtimescheduler + - React-utils + - ReactCommon + - React-RCTBlob (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-Codegen + - React-Core/RCTBlobHeaders + - React-Core/RCTWebSocket + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTNetwork + - ReactCommon + - React-RCTFabric (0.74.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-Core + - React-debug + - React-Fabric + - React-FabricImage + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-jsinspector + - React-nativeconfig + - React-RCTImage + - React-RCTText + - React-rendererdebug + - React-runtimescheduler + - React-utils + - Yoga + - React-RCTImage (0.74.5): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTImageHeaders + - React-jsi + - React-NativeModulesApple + - React-RCTNetwork + - ReactCommon + - React-RCTLinking (0.74.5): + - React-Codegen + - React-Core/RCTLinkingHeaders (= 0.74.5) + - React-jsi (= 0.74.5) + - React-NativeModulesApple + - ReactCommon + - ReactCommon/turbomodule/core (= 0.74.5) + - React-RCTNetwork (0.74.5): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTNetworkHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon + - React-RCTSettings (0.74.5): + - RCT-Folly (= 2024.01.01.00) + - RCTTypeSafety + - React-Codegen + - React-Core/RCTSettingsHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon + - React-RCTText (0.74.5): + - React-Core/RCTTextHeaders (= 0.74.5) + - Yoga + - React-RCTVibration (0.74.5): + - RCT-Folly (= 2024.01.01.00) + - React-Codegen + - React-Core/RCTVibrationHeaders + - React-jsi + - React-NativeModulesApple + - ReactCommon + - React-rendererdebug (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - RCT-Folly (= 2024.01.01.00) + - React-debug + - React-rncore (0.74.5) + - React-RuntimeApple (0.74.5): + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-callinvoker + - React-Core/Default + - React-CoreModules + - React-cxxreact + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-Mapbuffer + - React-NativeModulesApple + - React-RCTFabric + - React-RuntimeCore + - React-runtimeexecutor + - React-RuntimeHermes + - React-utils + - React-RuntimeCore (0.74.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-cxxreact + - React-featureflags + - React-jserrorhandler + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-runtimeexecutor + - React-runtimescheduler + - React-utils + - React-runtimeexecutor (0.74.5): + - React-jsi (= 0.74.5) + - React-RuntimeHermes (0.74.5): + - hermes-engine + - RCT-Folly/Fabric (= 2024.01.01.00) + - React-featureflags + - React-hermes + - React-jsi + - React-jsinspector + - React-jsitracing + - React-nativeconfig + - React-RuntimeCore + - React-utils + - React-runtimescheduler (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker + - React-cxxreact + - React-debug + - React-featureflags + - React-jsi + - React-rendererdebug + - React-runtimeexecutor + - React-utils + - React-utils (0.74.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-debug + - React-jsi (= 0.74.5) + - ReactCommon (0.74.5): + - ReactCommon/turbomodule (= 0.74.5) + - ReactCommon/turbomodule (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker (= 0.74.5) + - React-cxxreact (= 0.74.5) + - React-jsi (= 0.74.5) + - React-logger (= 0.74.5) + - React-perflogger (= 0.74.5) + - ReactCommon/turbomodule/bridging (= 0.74.5) + - ReactCommon/turbomodule/core (= 0.74.5) + - ReactCommon/turbomodule/bridging (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker (= 0.74.5) + - React-cxxreact (= 0.74.5) + - React-jsi (= 0.74.5) + - React-logger (= 0.74.5) + - React-perflogger (= 0.74.5) + - ReactCommon/turbomodule/core (0.74.5): + - DoubleConversion + - fmt (= 9.1.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - React-callinvoker (= 0.74.5) + - React-cxxreact (= 0.74.5) + - React-debug (= 0.74.5) + - React-jsi (= 0.74.5) + - React-logger (= 0.74.5) + - React-perflogger (= 0.74.5) + - React-utils (= 0.74.5) + - RNCAsyncStorage (1.23.1): + - React-Core + - RNCMaskedView (0.3.1): + - React-Core + - RNGestureHandler (2.16.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNReanimated (3.10.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNScreens (3.31.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-RCTImage + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNSVG (15.2.0): + - React-Core + - SocketRocket (0.7.0) + - Yoga (0.0.0) + +DEPENDENCIES: + - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - EXApplication (from `../node_modules/expo-application/ios`) + - EXConstants (from `../node_modules/expo-constants/ios`) + - EXNotifications (from `../node_modules/expo-notifications/ios`) + - Expo (from `../node_modules/expo`) + - ExpoAsset (from `../node_modules/expo-asset/ios`) + - ExpoBlur (from `../node_modules/expo-blur/ios`) + - ExpoClipboard (from `../node_modules/expo-clipboard/ios`) + - ExpoDevice (from `../node_modules/expo-device/ios`) + - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) + - ExpoFont (from `../node_modules/expo-font/ios`) + - ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`) + - ExpoLinearGradient (from `../node_modules/expo-linear-gradient/ios`) + - ExpoModulesCore (from `../node_modules/expo-modules-core`) + - ExpoWebBrowser (from `../node_modules/expo-web-browser/ios`) + - EXSplashScreen (from `../node_modules/expo-splash-screen/ios`) + - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) + - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) + - RCTRequired (from `../node_modules/react-native/Libraries/Required`) + - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../node_modules/react-native/`) + - React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`) + - React-Codegen (from `build/generated/ios`) + - React-Core (from `../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../node_modules/react-native/`) + - React-CoreModules (from `../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../node_modules/react-native/ReactCommon/cxxreact`) + - React-debug (from `../node_modules/react-native/ReactCommon/react/debug`) + - React-Fabric (from `../node_modules/react-native/ReactCommon`) + - React-FabricImage (from `../node_modules/react-native/ReactCommon`) + - React-featureflags (from `../node_modules/react-native/ReactCommon/react/featureflags`) + - React-graphics (from `../node_modules/react-native/ReactCommon/react/renderer/graphics`) + - React-hermes (from `../node_modules/react-native/ReactCommon/hermes`) + - React-ImageManager (from `../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios`) + - React-jserrorhandler (from `../node_modules/react-native/ReactCommon/jserrorhandler`) + - React-jsi (from `../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsitracing (from `../node_modules/react-native/ReactCommon/hermes/executor/`) + - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) + - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) + - react-native-webview (from `../node_modules/react-native-webview`) + - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) + - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) + - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../node_modules/react-native/Libraries/Blob`) + - React-RCTFabric (from `../node_modules/react-native/React`) + - React-RCTImage (from `../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../node_modules/react-native/Libraries/Network`) + - React-RCTSettings (from `../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) + - React-rendererdebug (from `../node_modules/react-native/ReactCommon/react/renderer/debug`) + - React-rncore (from `../node_modules/react-native/ReactCommon`) + - React-RuntimeApple (from `../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) + - React-RuntimeCore (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) + - React-RuntimeHermes (from `../node_modules/react-native/ReactCommon/react/runtime`) + - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) + - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) + - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" + - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNReanimated (from `../node_modules/react-native-reanimated`) + - RNScreens (from `../node_modules/react-native-screens`) + - RNSVG (from `../node_modules/react-native-svg`) + - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - SocketRocket + +EXTERNAL SOURCES: + boost: + :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + DoubleConversion: + :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EXApplication: + :path: "../node_modules/expo-application/ios" + EXConstants: + :path: "../node_modules/expo-constants/ios" + EXNotifications: + :path: "../node_modules/expo-notifications/ios" + Expo: + :path: "../node_modules/expo" + ExpoAsset: + :path: "../node_modules/expo-asset/ios" + ExpoBlur: + :path: "../node_modules/expo-blur/ios" + ExpoClipboard: + :path: "../node_modules/expo-clipboard/ios" + ExpoDevice: + :path: "../node_modules/expo-device/ios" + ExpoFileSystem: + :path: "../node_modules/expo-file-system/ios" + ExpoFont: + :path: "../node_modules/expo-font/ios" + ExpoKeepAwake: + :path: "../node_modules/expo-keep-awake/ios" + ExpoLinearGradient: + :path: "../node_modules/expo-linear-gradient/ios" + ExpoModulesCore: + :path: "../node_modules/expo-modules-core" + ExpoWebBrowser: + :path: "../node_modules/expo-web-browser/ios" + EXSplashScreen: + :path: "../node_modules/expo-splash-screen/ios" + FBLazyVector: + :path: "../node_modules/react-native/Libraries/FBLazyVector" + fmt: + :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec" + glog: + :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + hermes-engine: + :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + :tag: hermes-2024-06-28-RNv0.74.3-7bda0c267e76d11b68a585f84cfdd65000babf85 + RCT-Folly: + :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + RCTDeprecation: + :path: "../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation" + RCTRequired: + :path: "../node_modules/react-native/Libraries/Required" + RCTTypeSafety: + :path: "../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../node_modules/react-native/" + React-callinvoker: + :path: "../node_modules/react-native/ReactCommon/callinvoker" + React-Codegen: + :path: build/generated/ios + React-Core: + :path: "../node_modules/react-native/" + React-CoreModules: + :path: "../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../node_modules/react-native/ReactCommon/cxxreact" + React-debug: + :path: "../node_modules/react-native/ReactCommon/react/debug" + React-Fabric: + :path: "../node_modules/react-native/ReactCommon" + React-FabricImage: + :path: "../node_modules/react-native/ReactCommon" + React-featureflags: + :path: "../node_modules/react-native/ReactCommon/react/featureflags" + React-graphics: + :path: "../node_modules/react-native/ReactCommon/react/renderer/graphics" + React-hermes: + :path: "../node_modules/react-native/ReactCommon/hermes" + React-ImageManager: + :path: "../node_modules/react-native/ReactCommon/react/renderer/imagemanager/platform/ios" + React-jserrorhandler: + :path: "../node_modules/react-native/ReactCommon/jserrorhandler" + React-jsi: + :path: "../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsitracing: + :path: "../node_modules/react-native/ReactCommon/hermes/executor/" + React-logger: + :path: "../node_modules/react-native/ReactCommon/logger" + React-Mapbuffer: + :path: "../node_modules/react-native/ReactCommon" + react-native-safe-area-context: + :path: "../node_modules/react-native-safe-area-context" + react-native-webview: + :path: "../node_modules/react-native-webview" + React-nativeconfig: + :path: "../node_modules/react-native/ReactCommon" + React-NativeModulesApple: + :path: "../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-perflogger: + :path: "../node_modules/react-native/ReactCommon/reactperflogger" + React-RCTActionSheet: + :path: "../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../node_modules/react-native/Libraries/Blob" + React-RCTFabric: + :path: "../node_modules/react-native/React" + React-RCTImage: + :path: "../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../node_modules/react-native/Libraries/Network" + React-RCTSettings: + :path: "../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../node_modules/react-native/Libraries/Vibration" + React-rendererdebug: + :path: "../node_modules/react-native/ReactCommon/react/renderer/debug" + React-rncore: + :path: "../node_modules/react-native/ReactCommon" + React-RuntimeApple: + :path: "../node_modules/react-native/ReactCommon/react/runtime/platform/ios" + React-RuntimeCore: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimeexecutor: + :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" + React-RuntimeHermes: + :path: "../node_modules/react-native/ReactCommon/react/runtime" + React-runtimescheduler: + :path: "../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler" + React-utils: + :path: "../node_modules/react-native/ReactCommon/react/utils" + ReactCommon: + :path: "../node_modules/react-native/ReactCommon" + RNCAsyncStorage: + :path: "../node_modules/@react-native-async-storage/async-storage" + RNCMaskedView: + :path: "../node_modules/@react-native-masked-view/masked-view" + RNGestureHandler: + :path: "../node_modules/react-native-gesture-handler" + RNReanimated: + :path: "../node_modules/react-native-reanimated" + RNScreens: + :path: "../node_modules/react-native-screens" + RNSVG: + :path: "../node_modules/react-native-svg" + Yoga: + :path: "../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + boost: d3f49c53809116a5d38da093a8aa78bf551aed09 + DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 + EXApplication: ec862905fdab3a15bf6bd8ca1a99df7fc02d7762 + EXConstants: 89d35611505a8ce02550e64e43cd05565da35f9a + EXNotifications: 6ce128c0d3d3d161cd68bfd07d593db40e140396 + Expo: ed0a748eb6be0efd2c3df7f6de3f3158a14464c9 + ExpoAsset: 286fee7ba711ce66bf20b315e68106b13b8629fc + ExpoBlur: 99901a4531f5d3ac4a19b362907b8f75da4ed9c8 + ExpoClipboard: 243e22ff4161bbffcd3d2db469ae860ddc1156be + ExpoDevice: 84b3ed79df1234c17edfbf335f6ecf3c636f74de + ExpoFileSystem: 2988caaf68b7cb706e36d382829d99811d9d76a5 + ExpoFont: 38dddf823e32740c2a9f37c926a33aeca736b5c4 + ExpoKeepAwake: dd02e65d49f1cfd9194640028ae2857e536eb1c9 + ExpoLinearGradient: 4c44b3803b441724874b232e6520b51ca6a50db1 + ExpoModulesCore: 9ac73e2f60e0ea1d30137ca96cfc8c2aa34ef2b2 + ExpoWebBrowser: cf10afe886891ab495877dada977fe6c269614a4 + EXSplashScreen: a4ce3dd5d28d48e8b9132bcd9b58ee8e340db78c + FBLazyVector: ac12dc084d1c8ec4cc4d7b3cf1b0ebda6dab85af + fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 + glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f + hermes-engine: 8c1577f3fdb849cbe7729c2e7b5abc4b845e88f8 + RCT-Folly: 5dc73daec3476616d19e8a53f0156176f7b55461 + RCTDeprecation: 3afceddffa65aee666dafd6f0116f1d975db1584 + RCTRequired: ec1239bc9d8bf63e10fb92bd8b26171a9258e0c1 + RCTTypeSafety: f5ecbc86c5c5fa163c05acb7a1c5012e15b5f994 + React: fc9fa7258eff606f44d58c5b233a82dc9cf09018 + React-callinvoker: e3fab14d69607fb7e8e3a57e5a415aed863d3599 + React-Codegen: 3963186cb6a4ef21b5e67dcf7badf359867ff6df + React-Core: c3f589f104983dec3c3eeec5e70d61aa811bc236 + React-CoreModules: 864932ddae3ead5af5bfb05f9bbc2cedcb958b39 + React-cxxreact: bd9146108c44e6dbb99bba4568ce7af0304a2419 + React-debug: d30893c49ae1bce4037ea5cd8bb2511d2a38d057 + React-Fabric: a171830e52baf8ec2b175c6a3791e01bbb92f1fb + React-FabricImage: ad154af0067f4b5dc5a41f607e48ee343641e903 + React-featureflags: 4ae83e72d9a92452793601ac9ac7d2280e486089 + React-graphics: ed7d57965140168de86835946e8f1210c72c65dc + React-hermes: 177b1efdf3b8f10f4ca12b624b83fb4d4ccb2884 + React-ImageManager: 3a50d0ee0bf81b1a6f23a0c5b30388293bcd6004 + React-jserrorhandler: dcd62f5ca1c724c19637595ef7f45b78018e758f + React-jsi: 0abe1b0881b67caf8d8df6a57778dd0d3bb9d9a5 + React-jsiexecutor: f6ca8c04f19f6a3acaa9610f7fb728f39d6e3248 + React-jsinspector: db98771eae84e6f86f0ca5d9dcc572baadbfefc0 + React-jsitracing: f8367edacc50bb3f9f056a5aeafb8cee5849fafb + React-logger: 780b9ee9cec7d44eabc4093de90107c379078cb6 + React-Mapbuffer: f544f00b98dbdd8cbae96dd2bdb8b47f719976e0 + react-native-safe-area-context: df9763c5de6fa38883028e243a0b60123acb8858 + react-native-webview: a4483a25c71098e407df1c1d9056ab907647d7c7 + React-nativeconfig: ba9a2e54e2f0882cf7882698825052793ed4c851 + React-NativeModulesApple: 84aaad2b0e546d7b839837ca537f6e72804a4cad + React-perflogger: ed4e0c65781521e0424f2e5e40b40cc7879d737e + React-RCTActionSheet: 49d53ff03bb5688ca4606c55859053a0cd129ea5 + React-RCTAnimation: 3075449f26cb98a52bcbf51cccd0c7954e2a71db + React-RCTAppDelegate: 9a419c4dda9dd039ad851411546dd297b930c454 + React-RCTBlob: e81ab773a8fc1e9dceed953e889f936a7b7b3aa6 + React-RCTFabric: 47a87a3e3fa751674f7e64d0bcd58976b8c57db9 + React-RCTImage: d570531201c6dce7b5b63878fa8ecec0cc311c4c + React-RCTLinking: af888972b925d2811633d47853c479e88c35eb4d + React-RCTNetwork: 5728a06ff595003eca628f43f112a804f4a9a970 + React-RCTSettings: ba3665b0569714a8aaceee5c7d23b943e333fa55 + React-RCTText: b733fa984f0336b072e47512898ba91214f66ddb + React-RCTVibration: 0cbcbbd8781b6f6123671bae9ee5dd20d621af6c + React-rendererdebug: 9fc8f7d0bd19f2a3fe3791982af550b5e1535ff7 + React-rncore: 4013508a2f3fcf46c961919bbbd4bfdda198977e + React-RuntimeApple: a852a6e06ab20711658873f39cb10b0033bea19d + React-RuntimeCore: 12e5e176c0cb09926f3e6f37403a84d2e0f203a7 + React-runtimeexecutor: 0e688aefc14c6bc8601f4968d8d01c3fb6446844 + React-RuntimeHermes: 80c03a5215520c9733764ba11cbe535053c9746d + React-runtimescheduler: 2cbd0f3625b30bba08e8768776107f6f0203159b + React-utils: 9fa4e5d0b5e6c6c85c958f19d6ef854337886417 + ReactCommon: 9f285823dbe955099978d9bff65a7653ca029256 + RNCAsyncStorage: aa75595c1aefa18f868452091fa0c411a516ce11 + RNCMaskedView: de80352547bd4f0d607bf6bab363d826822bd126 + RNGestureHandler: 326e35460fb6c8c64a435d5d739bea90d7ed4e49 + RNReanimated: def444e044c354f38bb0a5926a8583ba19d944c1 + RNScreens: a2d8a2555b4653d7a19706eb172f855657ac30d7 + RNSVG: 0e7deccab0678200815104223aadd5ca734dd41d + SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d + Yoga: 950bbfd7e6f04790fdb51149ed51df41f329fcc8 + +PODFILE CHECKSUM: b710501c35a8aa0e7f8bc9bef05d8871882e1580 + +COCOAPODS: 1.16.2 diff --git a/MeAgent/ios/Podfile.properties.json b/MeAgent/ios/Podfile.properties.json new file mode 100644 index 00000000..3540391c --- /dev/null +++ b/MeAgent/ios/Podfile.properties.json @@ -0,0 +1,5 @@ +{ + "expo.jsEngine": "hermes", + "EX_DEV_CLIENT_NETWORK_INSPECTOR": "true", + "ios.deploymentTarget": "15.1" +} diff --git a/MeAgent/ios/app.xcodeproj/project.pbxproj b/MeAgent/ios/app.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3322cf0e --- /dev/null +++ b/MeAgent/ios/app.xcodeproj/project.pbxproj @@ -0,0 +1,553 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; + 5EF73AEC3ABB8432EA5E20D1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 50B8B0092BBC618FDE9C0B04 /* PrivacyInfo.xcprivacy */; }; + 96905EF65AED1B983A6B3ABC /* libPods-app.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-app.a */; }; + B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; + E7343D6D90954730BB45D8E1 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2985DBCB70452992045F05 /* noop-file.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 13B07F961A680F5B00A75B9A /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = app/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = app/AppDelegate.mm; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = app/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = app/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = app/main.m; sourceTree = ""; }; + 50B8B0092BBC618FDE9C0B04 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = app/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-app.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-app.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C2E3173556A471DD304B334 /* Pods-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.debug.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.debug.xcconfig"; sourceTree = ""; }; + 7A4D352CD337FB3A3BF06240 /* Pods-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.release.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.release.xcconfig"; sourceTree = ""; }; + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = app/SplashScreen.storyboard; sourceTree = ""; }; + BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; + E24A9282120D4E608D7E34DA /* app-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "app-Bridging-Header.h"; path = "app/app-Bridging-Header.h"; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-app/ExpoModulesProvider.swift"; sourceTree = ""; }; + FE2985DBCB70452992045F05 /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "app/noop-file.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 96905EF65AED1B983A6B3ABC /* libPods-app.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* app */ = { + isa = PBXGroup; + children = ( + BB2F792B24A3F905000567C9 /* Supporting */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.mm */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB71A68108700A75B9A /* main.m */, + AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, + FE2985DBCB70452992045F05 /* noop-file.swift */, + E24A9282120D4E608D7E34DA /* app-Bridging-Header.h */, + 50B8B0092BBC618FDE9C0B04 /* PrivacyInfo.xcprivacy */, + ); + name = app; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-app.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* app */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + D65327D7A22EEC0BE12398D9 /* Pods */, + D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* app.app */, + ); + name = Products; + sourceTree = ""; + }; + 92DBD88DE9BF7D494EA9DA96 /* app */ = { + isa = PBXGroup; + children = ( + FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */, + ); + name = app; + sourceTree = ""; + }; + BB2F792B24A3F905000567C9 /* Supporting */ = { + isa = PBXGroup; + children = ( + BB2F792C24A3F905000567C9 /* Expo.plist */, + ); + name = Supporting; + path = app/Supporting; + sourceTree = ""; + }; + D65327D7A22EEC0BE12398D9 /* Pods */ = { + isa = PBXGroup; + children = ( + 6C2E3173556A471DD304B334 /* Pods-app.debug.xcconfig */, + 7A4D352CD337FB3A3BF06240 /* Pods-app.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D7E4C46ADA2E9064B798F356 /* ExpoModulesProviders */ = { + isa = PBXGroup; + children = ( + 92DBD88DE9BF7D494EA9DA96 /* app */, + ); + name = ExpoModulesProviders; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* app */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */; + buildPhases = ( + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, + F50DEC99269F1C170908EB3D /* [Expo] Configure project */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, + 4E9C7677DD0DF9724695D156 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = app; + productName = app; + productReference = 13B07F961A680F5B00A75B9A /* app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1130; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1250; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* app */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, + 5EF73AEC3ABB8432EA5E20D1 /* PrivacyInfo.xcprivacy in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n# Source .xcode.env.updates if it exists to allow\n# SKIP_BUNDLING to be unset if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.updates\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.updates\"\nfi\n# Source local changes to allow overrides\n# if needed\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n\n"; + }; + 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-app-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 4E9C7677DD0DF9724695D156 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/EXApplication/ExpoApplication_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/ExpoConstants_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/EXNotifications/ExpoNotifications_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoDevice/ExpoDevice_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/ExpoFileSystem/ExpoFileSystem_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/RNCAsyncStorage/RNCAsyncStorage_resources.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoApplication_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoConstants_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoNotifications_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoDevice_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/ExpoFileSystem_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RNCAsyncStorage_resources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + F50DEC99269F1C170908EB3D /* [Expo] Configure project */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "[Expo] Configure project"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-app/expo-configure-project.sh\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */, + E7343D6D90954730BB45D8E1 /* noop-file.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6C2E3173556A471DD304B334 /* Pods-app.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = app/app.entitlements; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6XML2LHR2J; + ENABLE_BITCODE = NO; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "FB_SONARKIT_ENABLED=1", + ); + INFOPLIST_FILE = app/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.valuefrontier.meagent; + PRODUCT_NAME = app; + SWIFT_OBJC_BRIDGING_HEADER = "app/app-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7A4D352CD337FB3A3BF06240 /* Pods-app.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = app/app.entitlements; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6XML2LHR2J; + INFOPLIST_FILE = app/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.valuefrontier.meagent; + PRODUCT_NAME = app; + SWIFT_OBJC_BRIDGING_HEADER = "app/app-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CC = ""; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CXX = ""; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD = ""; + LDPLUSPLUS = ""; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = "$(inherited) "; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + USE_HERMES = true; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CC = ""; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + CXX = ""; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD = ""; + LDPLUSPLUS = ""; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = "$(inherited) "; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; + SDKROOT = iphoneos; + USE_HERMES = true; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/MeAgent/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme b/MeAgent/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme new file mode 100644 index 00000000..07f5cb69 --- /dev/null +++ b/MeAgent/ios/app.xcodeproj/xcshareddata/xcschemes/app.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MeAgent/ios/app.xcworkspace/contents.xcworkspacedata b/MeAgent/ios/app.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..b83e63c3 --- /dev/null +++ b/MeAgent/ios/app.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/MeAgent/ios/app/AppDelegate.h b/MeAgent/ios/app/AppDelegate.h new file mode 100644 index 00000000..1658a437 --- /dev/null +++ b/MeAgent/ios/app/AppDelegate.h @@ -0,0 +1,7 @@ +#import +#import +#import + +@interface AppDelegate : EXAppDelegateWrapper + +@end diff --git a/MeAgent/ios/app/AppDelegate.mm b/MeAgent/ios/app/AppDelegate.mm new file mode 100644 index 00000000..b27f8328 --- /dev/null +++ b/MeAgent/ios/app/AppDelegate.mm @@ -0,0 +1,62 @@ +#import "AppDelegate.h" + +#import +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + self.moduleName = @"main"; + + // You can add your custom initial props in the dictionary below. + // They will be passed down to the ViewController used by React Native. + self.initialProps = @{}; + + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ + return [self bundleURL]; +} + +- (NSURL *)bundleURL +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@".expo/.virtual-metro-entry"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +// Linking API +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + return [super application:application openURL:url options:options] || [RCTLinkingManager application:application openURL:url options:options]; +} + +// Universal Links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler { + BOOL result = [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; + return [super application:application continueUserActivity:userActivity restorationHandler:restorationHandler] || result; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken +{ + return [super application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + return [super application:application didFailToRegisterForRemoteNotificationsWithError:error]; +} + +// Explicitly define remote notification delegates to ensure compatibility with some third-party libraries +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler +{ + return [super application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler]; +} + +@end diff --git a/MeAgent/ios/app/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png b/MeAgent/ios/app/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png new file mode 100644 index 00000000..bb5119fb Binary files /dev/null and b/MeAgent/ios/app/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png differ diff --git a/MeAgent/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json b/MeAgent/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..90d8d4c2 --- /dev/null +++ b/MeAgent/ios/app/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images": [ + { + "filename": "App-Icon-1024x1024@1x.png", + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/MeAgent/ios/app/Images.xcassets/Contents.json b/MeAgent/ios/app/Images.xcassets/Contents.json new file mode 100644 index 00000000..ed285c2e --- /dev/null +++ b/MeAgent/ios/app/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "expo" + } +} diff --git a/MeAgent/ios/app/Images.xcassets/SplashScreen.imageset/Contents.json b/MeAgent/ios/app/Images.xcassets/SplashScreen.imageset/Contents.json new file mode 100644 index 00000000..3cf84897 --- /dev/null +++ b/MeAgent/ios/app/Images.xcassets/SplashScreen.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "scale": "2x" + }, + { + "idiom": "universal", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/MeAgent/ios/app/Images.xcassets/SplashScreen.imageset/image.png b/MeAgent/ios/app/Images.xcassets/SplashScreen.imageset/image.png new file mode 100644 index 00000000..e803295a Binary files /dev/null and b/MeAgent/ios/app/Images.xcassets/SplashScreen.imageset/image.png differ diff --git a/MeAgent/ios/app/Images.xcassets/SplashScreenBackground.imageset/Contents.json b/MeAgent/ios/app/Images.xcassets/SplashScreenBackground.imageset/Contents.json new file mode 100644 index 00000000..3cf84897 --- /dev/null +++ b/MeAgent/ios/app/Images.xcassets/SplashScreenBackground.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images": [ + { + "idiom": "universal", + "filename": "image.png", + "scale": "1x" + }, + { + "idiom": "universal", + "scale": "2x" + }, + { + "idiom": "universal", + "scale": "3x" + } + ], + "info": { + "version": 1, + "author": "expo" + } +} \ No newline at end of file diff --git a/MeAgent/ios/app/Images.xcassets/SplashScreenBackground.imageset/image.png b/MeAgent/ios/app/Images.xcassets/SplashScreenBackground.imageset/image.png new file mode 100644 index 00000000..837b3d57 Binary files /dev/null and b/MeAgent/ios/app/Images.xcassets/SplashScreenBackground.imageset/image.png differ diff --git a/MeAgent/ios/app/Info.plist b/MeAgent/ios/app/Info.plist new file mode 100644 index 00000000..4bef78a4 --- /dev/null +++ b/MeAgent/ios/app/Info.plist @@ -0,0 +1,76 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + 价值前沿 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + com.valuefrontier.meagent + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + SplashScreen + UIRequiredDeviceCapabilities + + arm64 + + UIRequiresFullScreen + + UIStatusBarStyle + UIStatusBarStyleDefault + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIUserInterfaceStyle + Light + UIViewControllerBasedStatusBarAppearance + + + \ No newline at end of file diff --git a/MeAgent/ios/app/PrivacyInfo.xcprivacy b/MeAgent/ios/app/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..c6b452ea --- /dev/null +++ b/MeAgent/ios/app/PrivacyInfo.xcprivacy @@ -0,0 +1,48 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + 0A2A.1 + 3B52.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + E174.1 + 85F4.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/MeAgent/ios/app/SplashScreen.storyboard b/MeAgent/ios/app/SplashScreen.storyboard new file mode 100644 index 00000000..ed03a529 --- /dev/null +++ b/MeAgent/ios/app/SplashScreen.storyboard @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MeAgent/ios/app/Supporting/Expo.plist b/MeAgent/ios/app/Supporting/Expo.plist new file mode 100644 index 00000000..750be020 --- /dev/null +++ b/MeAgent/ios/app/Supporting/Expo.plist @@ -0,0 +1,12 @@ + + + + + EXUpdatesCheckOnLaunch + ALWAYS + EXUpdatesEnabled + + EXUpdatesLaunchWaitMs + 0 + + \ No newline at end of file diff --git a/MeAgent/ios/app/app-Bridging-Header.h b/MeAgent/ios/app/app-Bridging-Header.h new file mode 100644 index 00000000..e11d920b --- /dev/null +++ b/MeAgent/ios/app/app-Bridging-Header.h @@ -0,0 +1,3 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// diff --git a/MeAgent/ios/app/app.entitlements b/MeAgent/ios/app/app.entitlements new file mode 100644 index 00000000..018a6e20 --- /dev/null +++ b/MeAgent/ios/app/app.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + \ No newline at end of file diff --git a/MeAgent/ios/app/main.m b/MeAgent/ios/app/main.m new file mode 100644 index 00000000..25181b6c --- /dev/null +++ b/MeAgent/ios/app/main.m @@ -0,0 +1,10 @@ +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} + diff --git a/MeAgent/ios/app/noop-file.swift b/MeAgent/ios/app/noop-file.swift new file mode 100644 index 00000000..b2ffafbf --- /dev/null +++ b/MeAgent/ios/app/noop-file.swift @@ -0,0 +1,4 @@ +// +// @generated +// A blank Swift file must be created for native modules with Swift files to work correctly. +// diff --git a/argon-pro-react-native/logo.jpg b/MeAgent/logo.jpg similarity index 100% rename from argon-pro-react-native/logo.jpg rename to MeAgent/logo.jpg diff --git a/MeAgent/navigation/Menu.js b/MeAgent/navigation/Menu.js new file mode 100644 index 00000000..83297f54 --- /dev/null +++ b/MeAgent/navigation/Menu.js @@ -0,0 +1,413 @@ +import React from "react"; +import { + ScrollView, + StyleSheet, + Dimensions, + Image, + TouchableOpacity, + Linking, +} from "react-native"; +import { Block, Text, theme } from "galio-framework"; +import { useSafeArea } from "react-native-safe-area-context"; +import { Box, HStack, VStack, Icon, Pressable, Spinner } from "native-base"; +import { Ionicons } from "@expo/vector-icons"; +import { LinearGradient } from "expo-linear-gradient"; +import Images from "../constants/Images"; +import { DrawerItem as DrawerCustomItem } from "../components/index"; +import { useAuth } from "../src/contexts/AuthContext"; + +const { width } = Dimensions.get("screen"); + +// 金色主题色 +const GOLD_PRIMARY = '#D4AF37'; + +// 用户卡片组件 +const UserCard = ({ navigation }) => { + const { user, isLoggedIn, isLoading, subscription, logout } = useAuth(); + + const handleLoginPress = () => { + navigation.closeDrawer(); + // 使用 getParent 获取根导航器来导航到 Login + navigation.getParent()?.navigate("Login"); + }; + + const handleLogoutPress = async () => { + await logout(); + }; + + // 获取订阅显示文本 + const getSubscriptionText = () => { + if (!subscription || !subscription.is_active) { + return "免费用户"; + } + const typeMap = { pro: "Pro 会员", max: "Max 会员" }; + return typeMap[subscription.type] || "免费用户"; + }; + + if (isLoading) { + return ( + + + + + 加载中... + + + + ); + } + + if (!isLoggedIn) { + return ( + + + + + + + + + 登录/注册 + + + 登录解锁更多功能 + + + + + + + ); + } + + // 已登录状态 + return ( + + + + + {(user?.username || user?.nickname || "U").charAt(0).toUpperCase()} + + + + + {user?.nickname || user?.username || "用户"} + + + + + {getSubscriptionText()} + + + + + + + + + + ); +}; + +function CustomDrawerContent({ + drawerPosition, + navigation, + profile, + focused, + state, + ...rest +}) { + const insets = useSafeArea(); + // 菜单项配置 + const screens = [ + { title: "事件中心", navigateTo: "EventsDrawer", icon: "flash", gradient: ["#7C3AED", "#A78BFA"] }, + { title: "市场热点", navigateTo: "MarketDrawer", icon: "flame", gradient: ["#F59E0B", "#FBBF24"] }, + { title: "概念中心", navigateTo: "ConceptsDrawer", icon: "bulb", gradient: ["#06B6D4", "#22D3EE"] }, + { title: "我的自选", navigateTo: "WatchlistDrawer", icon: "star", gradient: ["#EC4899", "#F472B6"] }, + { title: "个人中心", navigateTo: "ProfileDrawerNew", icon: "person", gradient: ["#8B5CF6", "#A78BFA"] }, + ]; + return ( + + {/* 品牌头部 - 黑金主题 */} + + + + + + 价值前沿 + + + VALUE FRONTIER + + + + + + + {/* 用户卡片 - 深色版本 */} + + + + + {/* 导航菜单 */} + + {screens.map((item, index) => { + const isFocused = state.index === index; + return ( + navigation.navigate(item.navigateTo)} + mb={2} + > + {isFocused ? ( + + + + + + + {item.title} + + + + ) : ( + + + + + + + {item.title} + + + + )} + + ); + })} + + + {/* 底部版本信息 */} + + + + 价值前沿 v1.0.0 + + + + + + ); +} + +// 深色主题用户卡片 +const UserCardDark = ({ navigation }) => { + const { user, isLoggedIn, isLoading, subscription, logout } = useAuth(); + + const handleLoginPress = () => { + navigation.closeDrawer(); + navigation.getParent()?.navigate("Login"); + }; + + const handleLogoutPress = async () => { + await logout(); + }; + + const getSubscriptionText = () => { + if (!subscription || !subscription.is_active) return "免费用户"; + const typeMap = { pro: "Pro 会员", max: "Max 会员" }; + return typeMap[subscription.type] || "免费用户"; + }; + + if (isLoading) { + return ( + + + + 加载中... + + + ); + } + + if (!isLoggedIn) { + return ( + + + + + + + + + 登录/注册 + + + 登录解锁更多功能 + + + + + + + ); + } + + return ( + + + + + {(user?.username || user?.nickname || "U").charAt(0).toUpperCase()} + + + + + {user?.nickname || user?.username || "用户"} + + + + {getSubscriptionText()} + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + paddingHorizontal: 28, + paddingBottom: theme.SIZES.BASE, + paddingTop: theme.SIZES.BASE * 3, + justifyContent: "center", + }, +}); + +export default CustomDrawerContent; diff --git a/argon-pro-react-native/navigation/Screens.js b/MeAgent/navigation/Screens.js similarity index 86% rename from argon-pro-react-native/navigation/Screens.js rename to MeAgent/navigation/Screens.js index 9a266804..811f8f89 100644 --- a/argon-pro-react-native/navigation/Screens.js +++ b/MeAgent/navigation/Screens.js @@ -39,7 +39,19 @@ import { createStackNavigator } from "@react-navigation/stack"; import { EventList, EventDetail } from "../src/screens/Events"; // 市场热点页面 -import { MarketHot, SectorDetail, EventCalendar, StockDetail, TodayStats } from "../src/screens/Market"; +import { MarketHot, SectorDetail, EventCalendar, TodayStats } from "../src/screens/Market"; + +// 概念中心页面 +import { ConceptList } from "../src/screens/Concepts"; + +// 自选股页面 +import WatchlistScreen from "../src/screens/Watchlist/WatchlistScreen"; + +// 新股票详情页面 +import { StockDetailScreen } from "../src/screens/StockDetail"; + +// 新个人中心页面 +import { ProfileScreen as NewProfileScreen } from "../src/screens/Profile"; // 认证页面 import { LoginScreen } from "../src/screens/Auth"; @@ -264,6 +276,13 @@ function EventsStack(props) { cardStyle: { backgroundColor: "#0F172A" }, }} /> + ); } @@ -309,9 +328,9 @@ function MarketStack(props) { /> + + + ); +} + +// 自选股导航栈 +function WatchlistStack(props) { + return ( + + + + + + ); +} + +// 新个人中心导航栈 +function NewProfileStack(props) { + return ( + + + + ); +} + function ProfileStack(props) { return ( } drawerStyle={{ - backgroundColor: "white", + backgroundColor: "#0F172A", width: width * 0.8, }} screenOptions={{ @@ -598,6 +691,27 @@ function AppStack(props) { headerShown: false, }} /> + + + { + const [isLoading, setIsLoading] = useState(false); + const { isInWatchlist, toggleStock } = useWatchlist({ autoLoad: false }); + + const inWatchlist = isInWatchlist(stockCode); + + const handlePress = useCallback(async () => { + if (isLoading) return; + + setIsLoading(true); + try { + const result = await toggleStock(stockCode, stockName); + if (result.success) { + onSuccess?.(inWatchlist ? 'removed' : 'added'); + } else { + onError?.(result.error); + } + } catch (error) { + onError?.(error.message); + } finally { + setIsLoading(false); + } + }, [stockCode, stockName, inWatchlist, toggleStock, isLoading, onSuccess, onError]); + + // 尺寸配置 + const sizeConfig = { + sm: { px: 2, py: 1, fontSize: 10, iconSize: 'xs' }, + md: { px: 3, py: 1.5, fontSize: 11, iconSize: 'sm' }, + lg: { px: 4, py: 2, fontSize: 12, iconSize: 'sm' }, + }; + + const config = sizeConfig[size] || sizeConfig.sm; + + // 只显示图标 + if (variant === 'icon') { + return ( + + + {isLoading ? ( + + ) : ( + + )} + + + ); + } + + // 带文字的按钮 + const bgColor = inWatchlist + ? 'rgba(239, 68, 68, 0.15)' + : 'rgba(59, 130, 246, 0.15)'; + const borderColor = inWatchlist + ? 'rgba(239, 68, 68, 0.3)' + : 'rgba(59, 130, 246, 0.3)'; + const textColor = inWatchlist ? '#EF4444' : '#3B82F6'; + + return ( + + {({ pressed }) => ( + + + {isLoading ? ( + + ) : ( + <> + + + {inWatchlist ? '已自选' : '加自选'} + + + )} + + + )} + + ); +}); + +StockWatchlistButton.displayName = 'StockWatchlistButton'; + +/** + * 事件关注按钮 + * @param {object} props + * @param {number|string} props.eventId - 事件ID + * @param {string} props.size - 按钮大小 'sm' | 'md' | 'lg' + * @param {string} props.variant - 样式变体 'solid' | 'outline' | 'icon' + * @param {function} props.onSuccess - 成功回调 + * @param {function} props.onError - 失败回调 + */ +export const EventFollowButton = memo(({ + eventId, + size = 'sm', + variant = 'outline', + onSuccess, + onError, +}) => { + const [isLoading, setIsLoading] = useState(false); + const { isEventFollowed, toggleEventFollow } = useWatchlist({ autoLoad: false }); + + const isFollowed = isEventFollowed(eventId); + + const handlePress = useCallback(async () => { + if (isLoading) return; + + setIsLoading(true); + try { + const result = await toggleEventFollow(eventId); + if (result.success) { + onSuccess?.(isFollowed ? 'unfollowed' : 'followed'); + } else { + onError?.(result.error); + } + } catch (error) { + onError?.(error.message); + } finally { + setIsLoading(false); + } + }, [eventId, isFollowed, toggleEventFollow, isLoading, onSuccess, onError]); + + // 尺寸配置 + const sizeConfig = { + sm: { px: 2, py: 1, fontSize: 10, iconSize: 'xs' }, + md: { px: 3, py: 1.5, fontSize: 11, iconSize: 'sm' }, + lg: { px: 4, py: 2, fontSize: 12, iconSize: 'sm' }, + }; + + const config = sizeConfig[size] || sizeConfig.sm; + + // 只显示图标 + if (variant === 'icon') { + return ( + + + {isLoading ? ( + + ) : ( + + )} + + + ); + } + + // 带文字的按钮 + const bgColor = isFollowed + ? 'rgba(236, 72, 153, 0.15)' + : 'rgba(124, 58, 237, 0.15)'; + const borderColor = isFollowed + ? 'rgba(236, 72, 153, 0.3)' + : 'rgba(124, 58, 237, 0.3)'; + const textColor = isFollowed ? '#EC4899' : '#7C3AED'; + + return ( + + {({ pressed }) => ( + + + {isLoading ? ( + + ) : ( + <> + + + {isFollowed ? '已关注' : '关注'} + + + )} + + + )} + + ); +}); + +EventFollowButton.displayName = 'EventFollowButton'; + +export default { + StockWatchlistButton, + EventFollowButton, +}; diff --git a/argon-pro-react-native/src/components/PushNotificationHandler.js b/MeAgent/src/components/PushNotificationHandler.js similarity index 100% rename from argon-pro-react-native/src/components/PushNotificationHandler.js rename to MeAgent/src/components/PushNotificationHandler.js diff --git a/argon-pro-react-native/src/components/index.js b/MeAgent/src/components/index.js similarity index 100% rename from argon-pro-react-native/src/components/index.js rename to MeAgent/src/components/index.js diff --git a/argon-pro-react-native/src/constants/index.js b/MeAgent/src/constants/index.js similarity index 100% rename from argon-pro-react-native/src/constants/index.js rename to MeAgent/src/constants/index.js diff --git a/argon-pro-react-native/src/contexts/AuthContext.js b/MeAgent/src/contexts/AuthContext.js similarity index 100% rename from argon-pro-react-native/src/contexts/AuthContext.js rename to MeAgent/src/contexts/AuthContext.js diff --git a/argon-pro-react-native/src/hooks/usePushNotifications.js b/MeAgent/src/hooks/usePushNotifications.js similarity index 100% rename from argon-pro-react-native/src/hooks/usePushNotifications.js rename to MeAgent/src/hooks/usePushNotifications.js diff --git a/MeAgent/src/hooks/useRealtimeQuote.js b/MeAgent/src/hooks/useRealtimeQuote.js new file mode 100644 index 00000000..8a37fe14 --- /dev/null +++ b/MeAgent/src/hooks/useRealtimeQuote.js @@ -0,0 +1,270 @@ +/** + * 实时行情 Hook + * 通过 WebSocket 订阅股票实时行情 + */ + +import { useState, useEffect, useCallback, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { realtimeQuoteService } from '../services/websocketService'; +import { updateRealtimeQuotes } from '../store/slices/watchlistSlice'; + +/** + * 实时行情 Hook + * @param {object} options - 配置选项 + * @param {string[]} options.codes - 要订阅的股票代码列表 + * @param {boolean} options.autoConnect - 是否自动连接(默认 true) + * @param {boolean} options.updateRedux - 是否更新 Redux 状态(默认 true) + */ +export const useRealtimeQuote = (options = {}) => { + const { + codes = [], + autoConnect = true, + updateRedux = true, + } = options; + + const dispatch = useDispatch(); + const [quotes, setQuotes] = useState({}); + const [connectionState, setConnectionState] = useState('disconnected'); + const [isConnected, setIsConnected] = useState(false); + const subscribedCodesRef = useRef(new Set()); + + // 处理行情更新 + const handleQuoteUpdate = useCallback((newQuotes) => { + setQuotes(prev => ({ + ...prev, + ...newQuotes, + })); + + // 同步到 Redux + if (updateRedux) { + dispatch(updateRealtimeQuotes(newQuotes)); + } + }, [dispatch, updateRedux]); + + // 处理连接状态变化 + const handleStateChange = useCallback((state) => { + setConnectionState(state); + setIsConnected(state === 'connected'); + }, []); + + // 连接 WebSocket + const connect = useCallback(() => { + realtimeQuoteService.connect(); + }, []); + + // 断开连接 + const disconnect = useCallback(() => { + realtimeQuoteService.disconnect(); + }, []); + + // 订阅股票 + const subscribe = useCallback((stockCodes) => { + if (!Array.isArray(stockCodes)) { + stockCodes = [stockCodes]; + } + + const newCodes = stockCodes.filter(code => !subscribedCodesRef.current.has(code)); + if (newCodes.length > 0) { + newCodes.forEach(code => subscribedCodesRef.current.add(code)); + realtimeQuoteService.subscribe(newCodes); + } + }, []); + + // 取消订阅 + const unsubscribe = useCallback((stockCodes) => { + if (!Array.isArray(stockCodes)) { + stockCodes = [stockCodes]; + } + + const existingCodes = stockCodes.filter(code => subscribedCodesRef.current.has(code)); + if (existingCodes.length > 0) { + existingCodes.forEach(code => subscribedCodesRef.current.delete(code)); + realtimeQuoteService.unsubscribe(existingCodes); + } + }, []); + + // 获取单个股票行情 + const getQuote = useCallback((code) => { + return quotes[code] || null; + }, [quotes]); + + // 初始化连接和事件监听 + useEffect(() => { + // 添加事件处理器 + const removeQuoteHandler = realtimeQuoteService.addQuoteHandler(handleQuoteUpdate); + const removeStateHandler = realtimeQuoteService.addStateHandler(handleStateChange); + + // 自动连接 + if (autoConnect) { + connect(); + } + + // 获取初始状态 + setConnectionState(realtimeQuoteService.getConnectionState()); + setIsConnected(realtimeQuoteService.isConnected()); + + return () => { + removeQuoteHandler(); + removeStateHandler(); + }; + }, [autoConnect, connect, handleQuoteUpdate, handleStateChange]); + + // 处理股票代码变化 + useEffect(() => { + if (codes.length > 0) { + // 计算需要新订阅的 + const codesToSubscribe = codes.filter(code => !subscribedCodesRef.current.has(code)); + + // 计算需要取消订阅的 + const currentCodes = new Set(codes); + const codesToUnsubscribe = Array.from(subscribedCodesRef.current).filter( + code => !currentCodes.has(code) + ); + + if (codesToUnsubscribe.length > 0) { + unsubscribe(codesToUnsubscribe); + } + + if (codesToSubscribe.length > 0) { + subscribe(codesToSubscribe); + } + } + }, [codes, subscribe, unsubscribe]); + + // 组件卸载时取消所有订阅 + useEffect(() => { + return () => { + const allCodes = Array.from(subscribedCodesRef.current); + if (allCodes.length > 0) { + realtimeQuoteService.unsubscribe(allCodes); + subscribedCodesRef.current.clear(); + } + }; + }, []); + + return { + // 状态 + quotes, + connectionState, + isConnected, + + // 操作 + connect, + disconnect, + subscribe, + unsubscribe, + getQuote, + }; +}; + +/** + * 单个股票实时行情 Hook + * @param {string} code - 股票代码 + */ +export const useSingleQuote = (code) => { + const { quotes, isConnected, subscribe, unsubscribe } = useRealtimeQuote({ + codes: code ? [code] : [], + updateRedux: false, + }); + + return { + quote: code ? quotes[code] : null, + isConnected, + }; +}; + +/** + * 自选股列表实时行情 Hook + * 自动从 Redux 获取自选股列表并订阅 + */ +export const useWatchlistRealtimeQuotes = () => { + const dispatch = useDispatch(); + const [connectionState, setConnectionState] = useState('disconnected'); + const subscribedCodesRef = useRef(new Set()); + + // 处理行情更新 + const handleQuoteUpdate = useCallback((newQuotes) => { + dispatch(updateRealtimeQuotes(newQuotes)); + }, [dispatch]); + + // 处理连接状态变化 + const handleStateChange = useCallback((state) => { + setConnectionState(state); + }, []); + + // 订阅股票列表 + const subscribeStocks = useCallback((stocks) => { + if (!Array.isArray(stocks) || stocks.length === 0) return; + + const codes = stocks.map(s => s.stock_code).filter(Boolean); + const newCodes = codes.filter(code => !subscribedCodesRef.current.has(code)); + + if (newCodes.length > 0) { + newCodes.forEach(code => subscribedCodesRef.current.add(code)); + realtimeQuoteService.subscribe(newCodes); + } + }, []); + + // 更新订阅列表 + const updateSubscription = useCallback((stocks) => { + if (!Array.isArray(stocks)) return; + + const newCodes = new Set(stocks.map(s => s.stock_code).filter(Boolean)); + + // 取消不再需要的订阅 + const codesToUnsubscribe = []; + subscribedCodesRef.current.forEach(code => { + if (!newCodes.has(code)) { + codesToUnsubscribe.push(code); + } + }); + + if (codesToUnsubscribe.length > 0) { + codesToUnsubscribe.forEach(code => subscribedCodesRef.current.delete(code)); + realtimeQuoteService.unsubscribe(codesToUnsubscribe); + } + + // 添加新订阅 + const codesToSubscribe = []; + newCodes.forEach(code => { + if (!subscribedCodesRef.current.has(code)) { + codesToSubscribe.push(code); + subscribedCodesRef.current.add(code); + } + }); + + if (codesToSubscribe.length > 0) { + realtimeQuoteService.subscribe(codesToSubscribe); + } + }, []); + + // 初始化 + useEffect(() => { + const removeQuoteHandler = realtimeQuoteService.addQuoteHandler(handleQuoteUpdate); + const removeStateHandler = realtimeQuoteService.addStateHandler(handleStateChange); + + realtimeQuoteService.connect(); + setConnectionState(realtimeQuoteService.getConnectionState()); + + return () => { + removeQuoteHandler(); + removeStateHandler(); + + // 取消所有订阅 + const allCodes = Array.from(subscribedCodesRef.current); + if (allCodes.length > 0) { + realtimeQuoteService.unsubscribe(allCodes); + subscribedCodesRef.current.clear(); + } + }; + }, [handleQuoteUpdate, handleStateChange]); + + return { + connectionState, + isConnected: connectionState === 'connected', + subscribeStocks, + updateSubscription, + }; +}; + +export default useRealtimeQuote; diff --git a/MeAgent/src/hooks/useWatchlist.js b/MeAgent/src/hooks/useWatchlist.js new file mode 100644 index 00000000..4a3b3603 --- /dev/null +++ b/MeAgent/src/hooks/useWatchlist.js @@ -0,0 +1,234 @@ +/** + * 自选股管理 Hook + * 提供自选股和自选事件的操作方法 + */ + +import { useCallback, useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + fetchWatchlist, + fetchWatchlistRealtime, + fetchFollowingEvents, + addToWatchlist, + removeFromWatchlist, + toggleEventFollow, + selectWatchlistStocks, + selectWatchlistEvents, + selectRealtimeQuotes, + selectWatchlistLoading, + selectWatchlistError, + selectIsInWatchlist, + selectIsEventFollowed, + optimisticAddStock, + optimisticRemoveStock, +} from '../store/slices/watchlistSlice'; + +/** + * 自选股管理 Hook + * @param {object} options - 配置选项 + * @param {boolean} options.autoLoad - 是否自动加载数据 + * @param {number} options.refreshInterval - 实时行情刷新间隔(毫秒),0 表示不自动刷新 + */ +export const useWatchlist = (options = {}) => { + const { + autoLoad = true, + refreshInterval = 30000, // 默认 30 秒刷新一次 + } = options; + + const dispatch = useDispatch(); + const refreshTimerRef = useRef(null); + + // Redux 状态 + const stocks = useSelector(selectWatchlistStocks); + const events = useSelector(selectWatchlistEvents); + const realtimeQuotes = useSelector(selectRealtimeQuotes); + const loading = useSelector(selectWatchlistLoading); + const error = useSelector(selectWatchlistError); + + // 加载自选股列表 + const loadWatchlist = useCallback(() => { + return dispatch(fetchWatchlist()); + }, [dispatch]); + + // 加载自选股实时行情 + const loadRealtimeQuotes = useCallback(() => { + return dispatch(fetchWatchlistRealtime()); + }, [dispatch]); + + // 加载关注的事件 + const loadFollowingEvents = useCallback(() => { + return dispatch(fetchFollowingEvents()); + }, [dispatch]); + + // 添加股票到自选 + const handleAddStock = useCallback(async (stockCode, stockName = '') => { + // 乐观更新 + dispatch(optimisticAddStock({ stockCode, stockName })); + + try { + const result = await dispatch(addToWatchlist({ stockCode, stockName })).unwrap(); + return { success: true, data: result }; + } catch (error) { + // 回滚乐观更新 + dispatch(optimisticRemoveStock(stockCode)); + return { success: false, error }; + } + }, [dispatch]); + + // 从自选移除股票 + const handleRemoveStock = useCallback(async (stockCode) => { + // 先保存原始数据用于回滚 + const originalStocks = stocks; + + // 乐观更新 + dispatch(optimisticRemoveStock(stockCode)); + + try { + await dispatch(removeFromWatchlist(stockCode)).unwrap(); + return { success: true }; + } catch (error) { + // 回滚 - 重新加载列表 + dispatch(fetchWatchlist()); + return { success: false, error }; + } + }, [dispatch, stocks]); + + // 切换股票自选状态 + const toggleStock = useCallback(async (stockCode, stockName = '') => { + const isInList = isInWatchlist(stockCode); + + if (isInList) { + return handleRemoveStock(stockCode); + } else { + return handleAddStock(stockCode, stockName); + } + }, [handleAddStock, handleRemoveStock]); + + // 切换事件关注状态 + const handleToggleEventFollow = useCallback(async (eventId) => { + try { + const result = await dispatch(toggleEventFollow(eventId)).unwrap(); + return { success: true, data: result }; + } catch (error) { + return { success: false, error }; + } + }, [dispatch]); + + // 检查股票是否在自选中 + const isInWatchlist = useCallback((stockCode) => { + const normalizeCode = (code) => String(code).match(/\d{6}/)?.[0] || code; + const normalizedCode = normalizeCode(stockCode); + return stocks.some(item => normalizeCode(item.stock_code) === normalizedCode); + }, [stocks]); + + // 检查事件是否被关注 + const isEventFollowed = useCallback((eventId) => { + return events.some(event => event.id === eventId); + }, [events]); + + // 获取股票的实时行情 + const getStockQuote = useCallback((stockCode) => { + const normalizeCode = (code) => String(code).match(/\d{6}/)?.[0] || code; + const normalizedCode = normalizeCode(stockCode); + + // 尝试多种格式匹配 + return realtimeQuotes[stockCode] || + realtimeQuotes[normalizedCode] || + realtimeQuotes[`${normalizedCode}.SH`] || + realtimeQuotes[`${normalizedCode}.SZ`] || + null; + }, [realtimeQuotes]); + + // 合并股票列表和实时行情 + const stocksWithQuotes = stocks.map(stock => { + const quote = getStockQuote(stock.stock_code); + return { + ...stock, + quote, + }; + }); + + // 刷新全部数据 + const refreshAll = useCallback(async () => { + await Promise.all([ + loadWatchlist(), + loadRealtimeQuotes(), + loadFollowingEvents(), + ]); + }, [loadWatchlist, loadRealtimeQuotes, loadFollowingEvents]); + + // 启动定时刷新 + const startRefreshTimer = useCallback(() => { + if (refreshInterval > 0 && !refreshTimerRef.current) { + refreshTimerRef.current = setInterval(() => { + loadRealtimeQuotes(); + }, refreshInterval); + } + }, [refreshInterval, loadRealtimeQuotes]); + + // 停止定时刷新 + const stopRefreshTimer = useCallback(() => { + if (refreshTimerRef.current) { + clearInterval(refreshTimerRef.current); + refreshTimerRef.current = null; + } + }, []); + + // 自动加载数据 + useEffect(() => { + if (autoLoad) { + loadWatchlist(); + loadRealtimeQuotes(); + loadFollowingEvents(); + } + }, [autoLoad, loadWatchlist, loadRealtimeQuotes, loadFollowingEvents]); + + // 启动/停止定时刷新 + useEffect(() => { + if (autoLoad && refreshInterval > 0) { + startRefreshTimer(); + } + + return () => { + stopRefreshTimer(); + }; + }, [autoLoad, refreshInterval, startRefreshTimer, stopRefreshTimer]); + + return { + // 数据 + stocks, + events, + realtimeQuotes, + stocksWithQuotes, + + // 加载状态 + loading, + error, + isLoadingStocks: loading.stocks, + isLoadingEvents: loading.events, + isLoadingRealtime: loading.realtime, + + // 操作方法 + loadWatchlist, + loadRealtimeQuotes, + loadFollowingEvents, + refreshAll, + + // 自选股操作 + addStock: handleAddStock, + removeStock: handleRemoveStock, + toggleStock, + isInWatchlist, + getStockQuote, + + // 事件操作 + toggleEventFollow: handleToggleEventFollow, + isEventFollowed, + + // 定时刷新控制 + startRefreshTimer, + stopRefreshTimer, + }; +}; + +export default useWatchlist; diff --git a/argon-pro-react-native/src/screens/Auth/LoginScreen.js b/MeAgent/src/screens/Auth/LoginScreen.js similarity index 100% rename from argon-pro-react-native/src/screens/Auth/LoginScreen.js rename to MeAgent/src/screens/Auth/LoginScreen.js diff --git a/argon-pro-react-native/src/screens/Auth/index.js b/MeAgent/src/screens/Auth/index.js similarity index 100% rename from argon-pro-react-native/src/screens/Auth/index.js rename to MeAgent/src/screens/Auth/index.js diff --git a/MeAgent/src/screens/Concepts/ConceptDetail.js b/MeAgent/src/screens/Concepts/ConceptDetail.js new file mode 100644 index 00000000..6c36f908 --- /dev/null +++ b/MeAgent/src/screens/Concepts/ConceptDetail.js @@ -0,0 +1,322 @@ +/** + * 概念详情页面 - 内嵌 WebView + * 展示概念板块的详细信息 + */ + +import React, { useState, useCallback, memo } from 'react'; +import { StyleSheet, ActivityIndicator, StatusBar } from 'react-native'; +import { + Box, + HStack, + Text, + Icon, + Pressable, + Center, +} from 'native-base'; +import { WebView } from 'react-native-webview'; +import { Ionicons } from '@expo/vector-icons'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useNavigation, useRoute } from '@react-navigation/native'; + +// 简单的 MD5 实现 +const md5 = (string) => { + function md5cycle(x, k) { + var a = x[0], b = x[1], c = x[2], d = x[3]; + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); + } + + function cmn(q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); + } + + function ff(a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); + } + + function gg(a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); + } + + function hh(a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); + } + + function ii(a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); + } + + function md51(s) { + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], + i; + for (i = 64; i <= s.length; i += 64) { + md5cycle(state, md5blk(s.substring(i - 64, i))); + } + s = s.substring(i - 64); + var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < s.length; i++) + tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { + md5cycle(state, tail); + for (i = 0; i < 16; i++) tail[i] = 0; + } + tail[14] = n * 8; + md5cycle(state, tail); + return state; + } + + function md5blk(s) { + var md5blks = [], + i; + for (i = 0; i < 64; i += 4) { + md5blks[i >> 2] = + s.charCodeAt(i) + + (s.charCodeAt(i + 1) << 8) + + (s.charCodeAt(i + 2) << 16) + + (s.charCodeAt(i + 3) << 24); + } + return md5blks; + } + + var hex_chr = '0123456789abcdef'.split(''); + + function rhex(n) { + var s = '', + j = 0; + for (; j < 4; j++) + s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; + return s; + } + + function hex(x) { + for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); + return x.join(''); + } + + function add32(a, b) { + return (a + b) & 0xffffffff; + } + + // 处理 UTF-8 编码 + function utf8Encode(str) { + let utf8 = ''; + for (let i = 0; i < str.length; i++) { + let charCode = str.charCodeAt(i); + if (charCode < 128) { + utf8 += String.fromCharCode(charCode); + } else if (charCode < 2048) { + utf8 += String.fromCharCode((charCode >> 6) | 192); + utf8 += String.fromCharCode((charCode & 63) | 128); + } else { + utf8 += String.fromCharCode((charCode >> 12) | 224); + utf8 += String.fromCharCode(((charCode >> 6) & 63) | 128); + utf8 += String.fromCharCode((charCode & 63) | 128); + } + } + return utf8; + } + + return hex(md51(utf8Encode(string))); +}; + +// 生成概念详情页 URL +const getConceptUrl = (conceptName) => { + const hash = md5(conceptName); + return `https://valuefrontier.cn/htmls/concept/${hash}/`; +}; + +const ConceptDetail = () => { + const navigation = useNavigation(); + const route = useRoute(); + const insets = useSafeAreaInsets(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + const { conceptName } = route.params || {}; + const url = conceptName ? getConceptUrl(conceptName) : ''; + + const handleGoBack = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + const handleRefresh = useCallback(() => { + setLoading(true); + setError(false); + }, []); + + if (!conceptName) { + return ( + +
+ + 概念名称缺失 +
+
+ ); + } + + return ( + + + + {/* 顶部导航栏 */} + + + + + + + + {conceptName} + + + + + + + + + {/* WebView */} + + setLoading(true)} + onLoadEnd={() => setLoading(false)} + onError={() => { + setLoading(false); + setError(true); + }} + startInLoadingState={true} + renderLoading={() => ( +
+ + 加载中... +
+ )} + javaScriptEnabled={true} + domStorageEnabled={true} + scalesPageToFit={true} + allowsBackForwardNavigationGestures={true} + /> + + {/* 加载遮罩 */} + {loading && ( +
+ + 加载中... +
+ )} + + {/* 错误状态 */} + {error && !loading && ( +
+ + 加载失败 + + 重新加载 + +
+ )} +
+
+ ); +}; + +const styles = StyleSheet.create({ + webview: { + flex: 1, + backgroundColor: '#0F172A', + }, +}); + +export default memo(ConceptDetail); diff --git a/MeAgent/src/screens/Concepts/ConceptList.js b/MeAgent/src/screens/Concepts/ConceptList.js new file mode 100644 index 00000000..1efa692e --- /dev/null +++ b/MeAgent/src/screens/Concepts/ConceptList.js @@ -0,0 +1,1246 @@ +/** + * 概念中心页面 - iOS 17 毛玻璃风格 Treemap + * 嵌套显示当前层级和子层级 + */ + +import React, { useState, useEffect, useCallback, useMemo, memo, useRef } from 'react'; +import { + StyleSheet, + RefreshControl, + Dimensions, + StatusBar, + View, + TextInput, + Keyboard, + ActivityIndicator, +} from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Icon, + Pressable, + Spinner, + Center, + ScrollView, + Input, + FlatList, +} from 'native-base'; +import { BlurView } from 'expo-blur'; +import { LinearGradient } from 'expo-linear-gradient'; +import { Ionicons } from '@expo/vector-icons'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useNavigation } from '@react-navigation/native'; +import * as WebBrowser from 'expo-web-browser'; +import { API_BASE_URL } from '../../services/api'; +import ConceptTimeline from './ConceptTimeline'; + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const PADDING = 12; +const GAP = 8; + +// MD5 实现 +const md5 = (string) => { + function md5cycle(x, k) { + var a = x[0], b = x[1], c = x[2], d = x[3]; + a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); + a = gg(a, b, c, d, k[1], 5, -165796510); d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); + a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); + a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); + x[0] = add32(a, x[0]); x[1] = add32(b, x[1]); x[2] = add32(c, x[2]); x[3] = add32(d, x[3]); + } + function cmn(q, a, b, x, s, t) { a = add32(add32(a, q), add32(x, t)); return add32((a << s) | (a >>> (32 - s)), b); } + function ff(a, b, c, d, x, s, t) { return cmn((b & c) | ((~b) & d), a, b, x, s, t); } + function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & (~d)), a, b, x, s, t); } + function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); } + function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | (~d)), a, b, x, s, t); } + function md51(s) { + var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; + for (i = 64; i <= s.length; i += 64) { md5cycle(state, md5blk(s.substring(i - 64, i))); } + s = s.substring(i - 64); + var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { md5cycle(state, tail); for (i = 0; i < 16; i++) tail[i] = 0; } + tail[14] = n * 8; md5cycle(state, tail); return state; + } + function md5blk(s) { + var md5blks = [], i; + for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); } + return md5blks; + } + var hex_chr = '0123456789abcdef'.split(''); + function rhex(n) { var s = '', j = 0; for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; return s; } + function hex(x) { for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); return x.join(''); } + function add32(a, b) { return (a + b) & 0xffffffff; } + function utf8Encode(str) { + let utf8 = ''; + for (let i = 0; i < str.length; i++) { + let charCode = str.charCodeAt(i); + if (charCode < 128) { utf8 += String.fromCharCode(charCode); } + else if (charCode < 2048) { utf8 += String.fromCharCode((charCode >> 6) | 192); utf8 += String.fromCharCode((charCode & 63) | 128); } + else { utf8 += String.fromCharCode((charCode >> 12) | 224); utf8 += String.fromCharCode(((charCode >> 6) & 63) | 128); utf8 += String.fromCharCode((charCode & 63) | 128); } + } + return utf8; + } + return hex(md51(utf8Encode(string))); +}; + +const getConceptUrl = (conceptName) => `https://valuefrontier.cn/htmls/concept/${md5(conceptName)}/`; +const CONCEPT_API_BASE = `${API_BASE_URL}/concept-api`; + +// ============ 颜色工具 ============ +const getChangeColor = (value, opacity = 1) => { + if (value === null || value === undefined) return `rgba(100, 116, 139, ${opacity})`; + if (value > 7) return `rgba(185, 28, 28, ${opacity})`; + if (value > 5) return `rgba(220, 38, 38, ${opacity})`; + if (value > 3) return `rgba(239, 68, 68, ${opacity})`; + if (value > 1) return `rgba(248, 113, 113, ${opacity})`; + if (value > 0) return `rgba(252, 165, 165, ${opacity})`; + if (value < -7) return `rgba(20, 83, 45, ${opacity})`; + if (value < -5) return `rgba(22, 101, 52, ${opacity})`; + if (value < -3) return `rgba(21, 128, 61, ${opacity})`; + if (value < -1) return `rgba(22, 163, 74, ${opacity})`; + if (value < 0) return `rgba(34, 197, 94, ${opacity})`; + return `rgba(100, 116, 139, ${opacity})`; +}; + +const formatChange = (value) => { + if (value === null || value === undefined) return ''; + const sign = value > 0 ? '+' : ''; + return `${sign}${value.toFixed(2)}%`; +}; + +const extractPureName = (apiName) => { + if (!apiName) return ''; + return apiName.replace(/^\[(一级|二级|三级)\]\s*/, ''); +}; + +// ============ 列表项组件(卡片模式用,简单版) ============ +const ListItem = memo(({ item, onPress, showArrow = true }) => { + const changeColor = item.avg_change_pct > 0 ? '#F87171' : item.avg_change_pct < 0 ? '#4ADE80' : '#94A3B8'; + const bgColor = getChangeColor(item.avg_change_pct, 0.1); + + return ( + onPress?.(item)}> + {({ pressed }) => ( + + {/* 涨跌指示条 */} + + + {/* 名称和子分类数 */} + + + {item.name} + + + {item.concept_count > 0 && ( + + {item.concept_count} 个概念 + + )} + {item.stock_count > 0 && ( + + {item.stock_count} 只股票 + + )} + + + + {/* 涨跌幅 */} + + + + {formatChange(item.avg_change_pct)} + + + {showArrow && ( + + )} + + + )} + + ); +}); + +ListItem.displayName = 'ListItem'; + +// ============ 叶子概念列表项组件(列表模式用,详细版) ============ +const LeafListItem = memo(({ item, onPress, onTimelinePress, sortBy }) => { + const changeColor = item.avg_change_pct > 0 ? '#F87171' : item.avg_change_pct < 0 ? '#4ADE80' : '#94A3B8'; + const bgColor = getChangeColor(item.avg_change_pct, 0.08); + + // 获取最近的启动日期 + const latestOutbreak = item.outbreak_dates && item.outbreak_dates.length > 0 + ? item.outbreak_dates[0] + : null; + + // 格式化启动日期显示 + const formatOutbreakDate = (dateStr) => { + if (!dateStr) return ''; + // 假设格式是 YYYY-MM-DD + const parts = dateStr.split('-'); + if (parts.length === 3) { + return `${parts[1]}/${parts[2]}`; + } + return dateStr; + }; + + // 处理时间轴按钮点击(阻止事件冒泡) + const handleTimelinePress = (e) => { + e?.stopPropagation?.(); + onTimelinePress?.(item); + }; + + return ( + onPress?.(item)}> + {({ pressed }) => ( + + + {/* 涨跌指示条 */} + + + {/* 主要内容 */} + + {/* 概念名称 */} + + {item.name} + + + {/* 层级路径 */} + {item.hierarchy && ( + + {item.hierarchy.lv1 && ( + + {item.hierarchy.lv1} + + )} + {item.hierarchy.lv2 && ( + <> + + + {item.hierarchy.lv2} + + + )} + {item.hierarchy.lv3 && ( + <> + + + {item.hierarchy.lv3} + + + )} + + )} + + {/* 标签和统计 */} + + {item.stock_count > 0 && ( + + + + {item.stock_count} 只 + + + )} + {latestOutbreak && ( + + + + {formatOutbreakDate(latestOutbreak)} + + + )} + {item.tags && item.tags.length > 0 && ( + + {item.tags.slice(0, 2).join(' · ')} + + )} + + + + {/* 涨跌幅 */} + + + {formatChange(item.avg_change_pct)} + + {sortBy === 'outbreak' && latestOutbreak && ( + + 启动 {latestOutbreak} + + )} + + + {/* 时间轴按钮 */} + + + + + {/* 箭头 */} + + + + )} + + ); +}); + +LeafListItem.displayName = 'LeafListItem'; + +// ============ 子分类小块组件 ============ +const ChildBlock = memo(({ item, onPress, width }) => { + const bgColor = getChangeColor(item.avg_change_pct, 0.85); + + return ( + onPress?.(item)} style={{ width, marginBottom: 6 }}> + {({ pressed }) => ( + + + {item.name} + + {item.avg_change_pct !== undefined && ( + + {formatChange(item.avg_change_pct)} + + )} + + )} + + ); +}); + +ChildBlock.displayName = 'ChildBlock'; + +// ============ 父分类卡片组件(毛玻璃风格)============ +const ParentCard = memo(({ item, children, priceMap, onPress, onChildPress }) => { + const borderColor = getChangeColor(item.avg_change_pct, 0.4); + const accentColor = getChangeColor(item.avg_change_pct, 0.8); + + // 计算子分类数据 + const childData = useMemo(() => { + if (!children || children.length === 0) return []; + return children.slice(0, 8).map(child => { + const price = priceMap[child.name] || {}; + return { + ...child, + avg_change_pct: price.avg_change_pct, + stock_count: price.stock_count, + }; + }); + }, [children, priceMap]); + + const hasMore = children && children.length > 8; + const childWidth = (SCREEN_WIDTH - PADDING * 2 - 24 - GAP) / 2; + + return ( + onPress?.(item)} style={styles.parentCard}> + {({ pressed }) => ( + + {/* 背景渐变 */} + + + {/* 毛玻璃效果 */} + + + {/* 边框和内容 */} + + {/* 标题栏 */} + + + + + + {item.name} + + + {item.concept_count || children?.length || 0} 个子分类 + + + + + + {item.avg_change_pct !== undefined && item.avg_change_pct !== 0 && ( + 0 ? 'trending-up' : 'trending-down'} + size="xs" + color={item.avg_change_pct > 0 ? '#F87171' : '#4ADE80'} + /> + )} + 0 ? '#F87171' : item.avg_change_pct < 0 ? '#4ADE80' : 'gray.400'} + fontSize={16} + fontWeight="bold" + > + {formatChange(item.avg_change_pct)} + + + + + {/* 子分类网格 */} + {childData.length > 0 ? ( + + {childData.map((child, index) => ( + + ))} + + ) : ( +
+ 暂无子分类 +
+ )} + + {/* 更多提示 */} + {hasMore && ( + + + 还有 {children.length - 8} 个子分类 + + + + )} +
+
+ )} +
+ ); +}); + +ParentCard.displayName = 'ParentCard'; + +// ============ 叶子概念卡片 ============ +const LeafCard = memo(({ item, onPress }) => { + const bgColor = getChangeColor(item.avg_change_pct, 0.7); + + return ( + onPress?.(item)} style={styles.leafCard}> + {({ pressed }) => ( + + + + + + {item.name} + + + {item.stock_count && ( + + {item.stock_count} 只 + + )} + + {formatChange(item.avg_change_pct)} + + + + + )} + + ); +}); + +LeafCard.displayName = 'LeafCard'; + +// ============ 面包屑组件 ============ +const Breadcrumbs = memo(({ path, onNavigate }) => { + const items = [{ label: '全部分类', level: -1 }, ...path]; + + return ( + + + {items.map((item, index) => ( + + onNavigate(index - 1)} _pressed={{ opacity: 0.6 }}> + + {item.label} + + + {index < items.length - 1 && ( + + )} + + ))} + + + ); +}); + +Breadcrumbs.displayName = 'Breadcrumbs'; + +// ============ 搜索框组件 ============ +const SearchBar = memo(({ value, onChangeText, onClear, onSubmit, placeholder }) => { + const inputRef = useRef(null); + + return ( + + + + + {value ? ( + + + + ) : null} + + + ); +}); + +SearchBar.displayName = 'SearchBar'; + +// ============ 主组件 ============ +const ConceptList = () => { + const navigation = useNavigation(); + const insets = useSafeAreaInsets(); + const flatListRef = useRef(null); + const searchTimeoutRef = useRef(null); + + const [hierarchy, setHierarchy] = useState([]); + const [priceData, setPriceData] = useState({ lv1Map: {}, lv2Map: {}, lv3Map: {}, leafMap: {} }); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + const [drillPath, setDrillPath] = useState([]); + const [viewMode, setViewMode] = useState('card'); // 'card' | 'list' + const [sortBy, setSortBy] = useState('change'); // 'change' | 'outbreak' + const [leafConcepts, setLeafConcepts] = useState([]); // 列表模式下的叶子概念 + const [leafLoading, setLeafLoading] = useState(false); + + // 搜索和分页状态 + const [searchQuery, setSearchQuery] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalCount, setTotalCount] = useState(0); + const [hasMore, setHasMore] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const PAGE_SIZE = 30; + + // 时间轴模态框状态 + const [timelineVisible, setTimelineVisible] = useState(false); + const [timelineConcept, setTimelineConcept] = useState(null); + + // 获取数据 + const fetchHierarchy = useCallback(async () => { + const response = await fetch(`${CONCEPT_API_BASE}/hierarchy`); + if (!response.ok) throw new Error('获取数据失败'); + const data = await response.json(); + setHierarchy(data.hierarchy || []); + }, []); + + const fetchPriceData = useCallback(async () => { + try { + const response = await fetch(`${CONCEPT_API_BASE}/hierarchy/price`); + if (!response.ok) return; + const data = await response.json(); + + const lv1Map = {}, lv2Map = {}, lv3Map = {}, leafMap = {}; + (data.lv1_concepts || []).forEach(item => { lv1Map[extractPureName(item.concept_name)] = item; }); + (data.lv2_concepts || []).forEach(item => { lv2Map[extractPureName(item.concept_name)] = item; }); + (data.lv3_concepts || []).forEach(item => { lv3Map[extractPureName(item.concept_name)] = item; }); + (data.leaf_concepts || []).forEach(item => { leafMap[item.concept_name] = item; }); + + setPriceData({ lv1Map, lv2Map, lv3Map, leafMap }); + + // 保存叶子概念列表供列表模式使用 + return data.leaf_concepts || []; + } catch (err) { + console.error('[ConceptList] fetchPriceData error:', err); + return []; + } + }, []); + + // 搜索概念(支持分页) + const searchConcepts = useCallback(async (query, page = 1, append = false) => { + if (page === 1) { + setLeafLoading(true); + if (!append) setLeafConcepts([]); + } else { + setLoadingMore(true); + } + + try { + // 有搜索词时按相关度排序,没有搜索词时按用户选择的排序方式 + const actualSortBy = query?.trim() + ? '_score' + : (sortBy === 'outbreak' ? 'outbreak_date' : 'change_pct'); + + const response = await fetch(`${CONCEPT_API_BASE}/search`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query: query || '', + size: PAGE_SIZE, + page: page, + sort_by: actualSortBy, + search_size: 1000, + }), + }); + + if (response.ok) { + const data = await response.json(); + const results = data.results || []; + + // 转换为组件需要的格式 + const formatted = results.map(item => ({ + concept_id: item.concept_id, + name: item.concept, + avg_change_pct: item.price_info?.avg_change_pct, + stock_count: item.stock_count, + outbreak_dates: item.outbreak_dates || [], + hierarchy: item.hierarchy, + tags: item.tags || [], + })); + + if (append && page > 1) { + setLeafConcepts(prev => [...prev, ...formatted]); + } else { + setLeafConcepts(formatted); + } + + setCurrentPage(data.page || page); + setTotalPages(data.total_pages || 1); + setTotalCount(data.total || 0); + setHasMore(page < (data.total_pages || 1)); + } + } catch (err) { + console.error('[ConceptList] searchConcepts error:', err); + } finally { + setLeafLoading(false); + setLoadingMore(false); + } + }, [sortBy, PAGE_SIZE]); + + // 加载更多 + const loadMore = useCallback(() => { + if (loadingMore || !hasMore || leafLoading) return; + searchConcepts(searchQuery, currentPage + 1, true); + }, [loadingMore, hasMore, leafLoading, searchQuery, currentPage, searchConcepts]); + + // 防抖搜索 + const handleSearchChange = useCallback((text) => { + setSearchQuery(text); + + // 清除之前的定时器 + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + + // 设置新的防抖定时器 + searchTimeoutRef.current = setTimeout(() => { + setCurrentPage(1); + setHasMore(true); + searchConcepts(text, 1, false); + }, 300); + }, [searchConcepts]); + + // 清除搜索 + const handleClearSearch = useCallback(() => { + setSearchQuery(''); + setCurrentPage(1); + setHasMore(true); + searchConcepts('', 1, false); + Keyboard.dismiss(); + }, [searchConcepts]); + + // 提交搜索 + const handleSubmitSearch = useCallback(() => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + setCurrentPage(1); + setHasMore(true); + searchConcepts(searchQuery, 1, false); + Keyboard.dismiss(); + }, [searchQuery, searchConcepts]); + + const loadData = useCallback(async (isRefresh = false) => { + if (isRefresh) setRefreshing(true); + else setLoading(true); + setError(null); + + try { + await fetchHierarchy(); + await fetchPriceData(); + // 如果当前是列表模式,加载概念列表 + if (viewMode === 'list') { + await searchConcepts(searchQuery, 1, false); + } + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + setRefreshing(false); + } + }, [fetchHierarchy, fetchPriceData, searchConcepts, viewMode, searchQuery]); + + useEffect(() => { loadData(); }, []); + + // 切换到列表模式时,加载概念列表 + useEffect(() => { + if (viewMode === 'list' && leafConcepts.length === 0) { + searchConcepts(searchQuery, 1, false); + } + }, [viewMode]); + + // 排序方式变化时,重新获取数据 + useEffect(() => { + if (viewMode === 'list') { + setCurrentPage(1); + setHasMore(true); + searchConcepts(searchQuery, 1, false); + } + }, [sortBy]); + + // 当前显示数据 + const { currentItems, childPriceMap, isLeafLevel } = useMemo(() => { + const { lv1Map, lv2Map, lv3Map, leafMap } = priceData; + + if (drillPath.length === 0) { + // 一级分类,子分类是二级 + const items = hierarchy.map(lv1 => ({ + ...lv1, + avg_change_pct: lv1Map[lv1.name]?.avg_change_pct, + stock_count: lv1Map[lv1.name]?.stock_count, + })); + return { currentItems: items, childPriceMap: lv2Map, isLeafLevel: false }; + } + + if (drillPath.length === 1) { + // 二级分类,子分类是三级 + const lv1 = drillPath[0].data; + const items = (lv1.children || []).map(lv2 => ({ + ...lv2, + avg_change_pct: lv2Map[lv2.name]?.avg_change_pct, + stock_count: lv2Map[lv2.name]?.stock_count, + })); + return { currentItems: items, childPriceMap: lv3Map, isLeafLevel: false }; + } + + if (drillPath.length === 2) { + // 三级分类,子分类是叶子概念 + const lv2 = drillPath[1].data; + const items = (lv2.children || []).map(lv3 => ({ + ...lv3, + avg_change_pct: lv3Map[lv3.name]?.avg_change_pct, + stock_count: lv3Map[lv3.name]?.stock_count, + // 把 concepts 转换为 children 格式 + children: (lv3.concepts || []).map(c => ({ name: c })), + })); + return { currentItems: items, childPriceMap: leafMap, isLeafLevel: false }; + } + + if (drillPath.length === 3) { + // 叶子概念列表 + const lv3 = drillPath[2].data; + const items = (lv3.concepts || []).map(concept => ({ + name: concept, + avg_change_pct: leafMap[concept]?.avg_change_pct, + stock_count: leafMap[concept]?.stock_count, + })); + return { currentItems: items, childPriceMap: {}, isLeafLevel: true }; + } + + return { currentItems: [], childPriceMap: {}, isLeafLevel: false }; + }, [hierarchy, priceData, drillPath]); + + // 点击父分类 + const handleParentPress = useCallback((item) => { + if (item.children?.length > 0 || item.concepts?.length > 0) { + setDrillPath(prev => [...prev, { label: item.name, data: item }]); + } + }, []); + + // 点击子分类 + const handleChildPress = useCallback((item) => { + // 找到完整的子分类数据并钻取 + const currentLevel = drillPath.length; + if (currentLevel === 0) { + const lv1 = hierarchy.find(h => h.name === drillPath[0]?.data?.name); + const lv2 = lv1?.children?.find(c => c.name === item.name) || item; + setDrillPath(prev => [...prev, { label: item.name, data: lv2 }]); + } else { + setDrillPath(prev => [...prev, { label: item.name, data: item }]); + } + }, [drillPath, hierarchy]); + + // 点击叶子概念 - 打开详情页 + const handleLeafPress = useCallback(async (item) => { + const url = getConceptUrl(item.name); + try { + await WebBrowser.openBrowserAsync(url, { + toolbarColor: '#0F172A', + controlsColor: '#06B6D4', + }); + } catch (error) { + console.error('打开浏览器失败:', error); + } + }, []); + + // 点击时间轴按钮 - 打开日历时间轴模态框 + const handleTimelinePress = useCallback((item) => { + setTimelineConcept(item); + setTimelineVisible(true); + }, []); + + // 关闭时间轴模态框 + const handleCloseTimeline = useCallback(() => { + setTimelineVisible(false); + setTimelineConcept(null); + }, []); + + const handleBreadcrumbNavigate = useCallback((index) => { + if (index < 0) setDrillPath([]); + else setDrillPath(prev => prev.slice(0, index + 1)); + }, []); + + const openDrawer = useCallback(() => navigation.openDrawer(), [navigation]); + const goBack = useCallback(() => setDrillPath(prev => prev.slice(0, -1)), []); + const toggleViewMode = useCallback(() => { + const newMode = viewMode === 'card' ? 'list' : 'card'; + setViewMode(newMode); + // 切换到列表模式时重置下钻路径 + if (newMode === 'list') { + setDrillPath([]); + } + }, [viewMode]); + const levelTitles = ['一级分类', '二级分类', '三级分类', '概念板块']; + const currentLevelTitle = viewMode === 'list' ? '全部概念' : levelTitles[Math.min(drillPath.length, 3)]; + + return ( + + + + {/* 顶部导航 */} + + + + + {drillPath.length > 0 ? ( + + + + ) : ( + + + + )} + + + + 概念中心 + + + {currentLevelTitle} · {viewMode === 'list' ? totalCount : currentItems.length} 项 + + + + + {/* 视图模式切换按钮 - 显示将要切换到的模式图标 */} + + + + loadData(true)} hitSlop={10}> + + + + + + + + {/* 列表模式:搜索框 + 排序选项 */} + {viewMode === 'list' && ( + + {/* 搜索框 */} + + + {/* 排序栏 */} + + + {searchQuery ? `搜索结果:${totalCount} 个` : `共 ${totalCount} 个概念`} + {leafConcepts.length < totalCount && ` · 已加载 ${leafConcepts.length}`} + + {searchQuery ? ( + // 搜索时显示相关度排序提示 + + + 按相关度排序 + + ) : ( + // 无搜索时显示排序选项 + + 排序: + setSortBy('change')} + bg={sortBy === 'change' ? 'rgba(248, 113, 113, 0.2)' : 'transparent'} + px={3} + py={1} + borderRadius={12} + borderWidth={1} + borderColor={sortBy === 'change' ? 'rgba(248, 113, 113, 0.4)' : 'rgba(255,255,255,0.1)'} + > + + + + 涨幅 + + + + setSortBy('outbreak')} + bg={sortBy === 'outbreak' ? 'rgba(251, 191, 36, 0.2)' : 'transparent'} + px={3} + py={1} + borderRadius={12} + borderWidth={1} + borderColor={sortBy === 'outbreak' ? 'rgba(251, 191, 36, 0.4)' : 'rgba(255,255,255,0.1)'} + > + + + + 启动日期 + + + + + )} + + + )} + + {/* 卡片模式:面包屑 */} + {viewMode === 'card' && drillPath.length > 0 && ( + + + + )} + + {/* 内容 */} + {loading ? ( +
+ + 加载概念数据... +
+ ) : error ? ( +
+ + {error} + loadData()} bg="rgba(6, 182, 212, 0.2)" px={6} py={2} rounded="full"> + 重新加载 + +
+ ) : viewMode === 'list' ? ( + // 列表模式 - 使用 FlatList 支持无限滚动 + item.concept_id || item.name || String(index)} + renderItem={({ item }) => ( + + )} + contentContainerStyle={{ padding: PADDING, paddingBottom: 30 }} + showsVerticalScrollIndicator={false} + refreshControl={ + { + setCurrentPage(1); + setHasMore(true); + loadData(true); + }} + tintColor="#06B6D4" + /> + } + onEndReached={loadMore} + onEndReachedThreshold={0.3} + ListEmptyComponent={ + leafLoading ? ( +
+ + 加载概念列表... +
+ ) : ( +
+ + {searchQuery ? '未找到匹配的概念' : '暂无概念数据'} +
+ ) + } + ListFooterComponent={ + loadingMore ? ( +
+ + + 加载更多... + +
+ ) : !hasMore && leafConcepts.length > 0 ? ( +
+ 已加载全部 {totalCount} 个概念 +
+ ) : null + } + /> + ) : ( + // 卡片模式 - 使用 ScrollView + loadData(true)} tintColor="#06B6D4" /> + } + contentContainerStyle={{ padding: PADDING, paddingBottom: 30 }} + > + {isLeafLevel ? ( + // 叶子概念网格 + + {currentItems.map((item, index) => ( + + ))} + + ) : ( + // 父分类卡片列表 + + {currentItems.map((item, index) => ( + + ))} + + )} + + {currentItems.length === 0 && ( +
+ + 暂无数据 +
+ )} +
+ )} + + {/* 时间轴模态框 */} + +
+ ); +}; + +const styles = StyleSheet.create({ + searchInput: { + flex: 1, + color: 'white', + fontSize: 14, + paddingVertical: 4, + }, + parentCard: { + borderRadius: 16, + overflow: 'hidden', + }, + cardContainer: { + borderRadius: 16, + overflow: 'hidden', + }, + cardContent: { + padding: 12, + borderWidth: 1, + borderRadius: 16, + }, + childGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + childBlockShadow: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 3, + elevation: 3, + }, + leafCard: { + width: (SCREEN_WIDTH - PADDING * 2 - GAP * 2) / 3, + height: 80, + marginBottom: GAP, + borderRadius: 10, + overflow: 'hidden', + }, + leafContainer: { + flex: 1, + borderRadius: 10, + overflow: 'hidden', + }, + leafContent: { + flex: 1, + padding: 8, + justifyContent: 'space-between', + }, + leafGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, +}); + +export default memo(ConceptList); diff --git a/MeAgent/src/screens/Concepts/ConceptTimeline.js b/MeAgent/src/screens/Concepts/ConceptTimeline.js new file mode 100644 index 00000000..06cb8c4c --- /dev/null +++ b/MeAgent/src/screens/Concepts/ConceptTimeline.js @@ -0,0 +1,818 @@ +/** + * 概念时间轴模态框 - 日历形式展示历史事件和涨跌幅 + */ + +import React, { useState, useEffect, useCallback, useMemo, memo } from 'react'; +import { + Modal, + StyleSheet, + Dimensions, + TouchableOpacity, + ActivityIndicator, +} from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Icon, + Pressable, + ScrollView, + Center, + Spinner, + Badge, +} from 'native-base'; +import { BlurView } from 'expo-blur'; +import { LinearGradient } from 'expo-linear-gradient'; +import { Ionicons } from '@expo/vector-icons'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useNavigation } from '@react-navigation/native'; + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const CELL_SIZE = (SCREEN_WIDTH - 32) / 7; + +// API 基础地址 - 生产环境使用代理 +const API_BASE = 'https://api.valuefrontier.cn'; +const CONCEPT_API_BASE = `${API_BASE}/concept-api`; +const REPORT_API_BASE = `${API_BASE}/report-api`; +const MAIN_API_URL = API_BASE; + +// 日期工具函数 +const formatDate = (date) => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +}; + +const getMonthDays = (year, month) => { + return new Date(year, month + 1, 0).getDate(); +}; + +const getFirstDayOfMonth = (year, month) => { + return new Date(year, month, 1).getDay(); +}; + +// 涨跌幅颜色 +const getChangeColor = (change, alpha = 1) => { + if (change > 3) return `rgba(239, 68, 68, ${alpha})`; // 大涨 - 深红 + if (change > 0) return `rgba(248, 113, 113, ${alpha})`; // 小涨 - 红 + if (change < -3) return `rgba(34, 197, 94, ${alpha})`; // 大跌 - 深绿 + if (change < 0) return `rgba(74, 222, 128, ${alpha})`; // 小跌 - 绿 + return `rgba(148, 163, 184, ${alpha})`; // 平盘 - 灰 +}; + +// ============ 日历单元格组件 ============ +const CalendarCell = memo(({ day, isCurrentMonth, data, isToday, onPress }) => { + if (!day) { + return ; + } + + const hasEvents = data?.events?.length > 0; + const hasNews = data?.events?.some(e => e.type === 'news'); + const hasReport = data?.events?.some(e => e.type === 'report'); + const change = data?.price?.avg_change_pct; + + return ( + onPress?.(day, data)} disabled={!isCurrentMonth}> + {({ pressed }) => ( + + {/* 日期数字 */} + + {day} + + + {/* 涨跌幅指示 */} + {change !== undefined && isCurrentMonth && ( + + {Math.abs(change) >= 3 && ( + 0 ? 'caret-up' : 'caret-down'} + size="2xs" + color="white" + /> + )} + + )} + + {/* 事件指示点 */} + {hasEvents && isCurrentMonth && ( + + {hasNews && ( + + )} + {hasReport && ( + + )} + + )} + + )} + + ); +}); + +CalendarCell.displayName = 'CalendarCell'; + +// 重要性颜色 +const getImportanceColor = (importance) => { + switch (importance) { + case 'S': return '#EF4444'; // 特级 - 红色 + case 'A': return '#F97316'; // 重要 - 橙色 + case 'B': return '#FBBF24'; // 一般 - 黄色 + default: return '#94A3B8'; // 普通 - 灰色 + } +}; + +const getImportanceLabel = (importance) => { + switch (importance) { + case 'S': return '特级'; + case 'A': return '重要'; + case 'B': return '一般'; + default: return ''; + } +}; + +// 事件类型图标和颜色 +const getEventTypeInfo = (eventType) => { + switch (eventType) { + case '政策': return { icon: 'document-text', color: '#F97316' }; + case '公司': return { icon: 'business', color: '#06B6D4' }; + case '行业': return { icon: 'trending-up', color: '#8B5CF6' }; + case '市场': return { icon: 'stats-chart', color: '#10B981' }; + default: return { icon: 'newspaper', color: '#60A5FA' }; + } +}; + +// ============ 日期详情卡片 ============ +const DayDetail = memo(({ date, data, onClose, onEventPress }) => { + if (!data) return null; + + const change = data.price?.avg_change_pct; + const stockCount = data.price?.stock_count; + const events = data.events || []; + const newsEvents = events.filter(e => e.type === 'news'); + const reportEvents = events.filter(e => e.type === 'report'); + + return ( + + {/* 头部渐变 */} + + + + + + + + {date} + + {newsEvents.length} 条事件 · {reportEvents.length} 篇研报 + + + + + + + + + + + {/* 涨跌幅卡片 */} + {change !== undefined && ( + + + + 概念平均涨跌幅 + {stockCount && ( + 基于 {stockCount} 只成分股 + )} + + + + {change > 0 ? '+' : ''}{change?.toFixed(2)} + + % + + + + )} + + {/* 事件列表 */} + {newsEvents.length > 0 && ( + 0 ? 3 : 0}> + + + 相关事件 + + {newsEvents.map((event, index) => { + const typeInfo = getEventTypeInfo(event.eventType); + const hasId = !!event.id; + + return ( + hasId && onEventPress?.(event)} + disabled={!hasId} + > + {({ pressed }) => ( + + {/* 事件头部 */} + + + + {event.eventType && ( + + + {event.eventType} + + + )} + {event.importance && ( + + + {getImportanceLabel(event.importance)} + + + )} + + {hasId && ( + + )} + + + {/* 事件标题 */} + + {event.title} + + + {/* 事件底部信息 */} + + + {event.hotScore && ( + + + {event.hotScore.toFixed(1)} + + )} + {event.viewCount > 0 && ( + + + {event.viewCount} + + )} + + {hasId && ( + 查看详情 + )} + + + )} + + ); + })} + + )} + + {/* 研报列表 */} + {reportEvents.length > 0 && ( + + + + 相关研报 + + {reportEvents.map((report, index) => ( + + + {report.title} + + + {report.source && ( + + + {report.source} + + )} + {report.rating && ( + + {report.rating} + + )} + {report.security_name && ( + · {report.security_name} + )} + + + ))} + + )} + + {/* 无事件提示 */} + {events.length === 0 && ( +
+ + 当日无相关事件记录 +
+ )} +
+
+ ); +}); + +DayDetail.displayName = 'DayDetail'; + +// ============ 主组件 ============ +const ConceptTimeline = ({ visible, onClose, concept }) => { + const insets = useSafeAreaInsets(); + const navigation = useNavigation(); + const [currentDate, setCurrentDate] = useState(new Date()); + const [loading, setLoading] = useState(false); + const [priceData, setPriceData] = useState({}); // 日期 -> 价格数据 + const [eventsData, setEventsData] = useState({}); // 日期 -> 事件数据 + const [selectedDay, setSelectedDay] = useState(null); + const [selectedData, setSelectedData] = useState(null); + + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + + // 获取数据 + const fetchData = useCallback(async () => { + if (!concept?.name && !concept?.concept_id) return; + + setLoading(true); + + // 计算日期范围(最近100天) + const endDate = new Date(); + const startDate = new Date(); + startDate.setDate(startDate.getDate() - 100); + const startStr = formatDate(startDate); + const endStr = formatDate(endDate); + + const conceptId = concept.concept_id; + const conceptName = concept.name; + + console.log('[ConceptTimeline] Fetching data for:', { conceptId, conceptName, startStr, endStr }); + + try { + // 并行获取价格、事件和研报数据 + const [priceRes, eventsRes, reportsRes] = await Promise.all([ + // 1. 价格时序数据 + fetch(`${CONCEPT_API_BASE}/concept/${conceptId}/price-timeseries?start_date=${startStr}&end_date=${endStr}`) + .then(r => { + console.log('[ConceptTimeline] Price API response status:', r.status); + return r.ok ? r.json() : { timeseries: [] }; + }) + .catch(err => { + console.error('[ConceptTimeline] Price API error:', err); + return { timeseries: [] }; + }), + + // 2. 事件数据 + fetch(`${MAIN_API_URL}/api/events/by-concept`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + concept_name: conceptName, + start_date: startStr, + end_date: endStr, + limit: 200, + }), + }) + .then(r => { + console.log('[ConceptTimeline] Events API response status:', r.status); + return r.ok ? r.json() : { success: false, data: [] }; + }) + .catch(err => { + console.error('[ConceptTimeline] Events API error:', err); + return { success: false, data: [] }; + }), + + // 3. 研报数据 + fetch(`${REPORT_API_BASE}/search?query=${encodeURIComponent(conceptName)}&mode=text&exact_match=1&size=30&start_date=${startStr}`) + .then(r => { + console.log('[ConceptTimeline] Report API response status:', r.status); + return r.ok ? r.json() : { data: { results: [] } }; + }) + .catch(err => { + console.error('[ConceptTimeline] Report API error:', err); + return { data: { results: [] } }; + }), + ]); + + console.log('[ConceptTimeline] Price data count:', priceRes.timeseries?.length || 0); + console.log('[ConceptTimeline] Events data count:', eventsRes.data?.length || 0); + console.log('[ConceptTimeline] Reports data count:', reportsRes.data?.results?.length || 0); + + // 辅助函数:从各种日期格式中提取 YYYY-MM-DD + const extractDateStr = (dateValue) => { + if (!dateValue) return null; + // 处理 ISO 格式 (2026-01-14T14:45:36) 或空格格式 (2026-01-14 14:45:36) + const str = String(dateValue); + const match = str.match(/^(\d{4}-\d{2}-\d{2})/); + return match ? match[1] : null; + }; + + // 处理价格数据 + const priceMap = {}; + (priceRes.timeseries || []).forEach(item => { + const dateStr = extractDateStr(item.trade_date); + if (dateStr) { + priceMap[dateStr] = { + avg_change_pct: item.avg_change_pct, + stock_count: item.stock_count, + }; + } + }); + setPriceData(priceMap); + + // 处理事件和研报数据,合并到同一个 map + const eventsMap = {}; + + // 处理事件 + (eventsRes.data || []).forEach(event => { + const dateStr = extractDateStr(event.event_date) || extractDateStr(event.created_at); + if (!dateStr) return; + + if (!eventsMap[dateStr]) { + eventsMap[dateStr] = []; + } + eventsMap[dateStr].push({ + type: 'news', + id: event.id, // 事件ID,用于跳转详情 + title: event.title || event.summary, + source: event.source || 'event', + content: event.description || event.summary, + eventType: event.event_type, // 事件类型:政策、公司、行业、市场 + importance: event.importance, // 重要性:S、A、B + hotScore: event.hot_score, // 热度分数 + viewCount: event.view_count, // 浏览次数 + }); + }); + + // 处理研报 + const reports = reportsRes.data?.results || reportsRes.results || []; + reports.forEach(report => { + const dateStr = extractDateStr(report.declare_date) || extractDateStr(report.publish_time); + if (!dateStr) return; + + if (!eventsMap[dateStr]) { + eventsMap[dateStr] = []; + } + eventsMap[dateStr].push({ + type: 'report', + title: report.report_title || report.title, + source: report.publisher || report.author, + content: report.content, + rating: report.rating, + security_name: report.security_name, + }); + }); + + setEventsData(eventsMap); + + console.log('[ConceptTimeline] Processed price dates:', Object.keys(priceMap).length); + console.log('[ConceptTimeline] Processed event dates:', Object.keys(eventsMap).length); + + } catch (error) { + console.error('[ConceptTimeline] fetchData error:', error); + } finally { + setLoading(false); + } + }, [concept]); + + useEffect(() => { + if (visible) { + fetchData(); + setSelectedDay(null); + setSelectedData(null); + } + }, [visible, fetchData]); + + // 切换月份 + const goToPrevMonth = useCallback(() => { + setCurrentDate(new Date(year, month - 1, 1)); + setSelectedDay(null); + setSelectedData(null); + }, [year, month]); + + const goToNextMonth = useCallback(() => { + setCurrentDate(new Date(year, month + 1, 1)); + setSelectedDay(null); + setSelectedData(null); + }, [year, month]); + + const goToToday = useCallback(() => { + setCurrentDate(new Date()); + setSelectedDay(null); + setSelectedData(null); + }, []); + + // 点击事件跳转到事件详情 + const handleEventPress = useCallback((event) => { + if (!event.id) return; + + // 先关闭时间轴模态框 + onClose?.(); + + // 延迟一下再跳转,确保模态框已关闭 + setTimeout(() => { + navigation.navigate('EventsDrawer', { + screen: 'EventDetail', + params: { + eventId: event.id, + }, + }); + }, 300); + }, [navigation, onClose]); + + // 生成日历数据 + const calendarData = useMemo(() => { + const daysInMonth = getMonthDays(year, month); + const firstDay = getFirstDayOfMonth(year, month); + const daysInPrevMonth = getMonthDays(year, month - 1); + + const days = []; + + // 上个月的天数 + for (let i = firstDay - 1; i >= 0; i--) { + days.push({ + day: daysInPrevMonth - i, + isCurrentMonth: false, + }); + } + + // 当月天数 + const today = new Date(); + const todayStr = formatDate(today); + + for (let i = 1; i <= daysInMonth; i++) { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`; + const isToday = dateStr === todayStr; + + days.push({ + day: i, + isCurrentMonth: true, + isToday, + dateStr, + data: { + price: priceData[dateStr], + events: eventsData[dateStr] || [], + }, + }); + } + + // 下个月的天数(补齐6行) + const remainingDays = 42 - days.length; + for (let i = 1; i <= remainingDays; i++) { + days.push({ + day: i, + isCurrentMonth: false, + }); + } + + return days; + }, [year, month, priceData, eventsData]); + + // 点击日期 + const handleDayPress = useCallback((day, data) => { + const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + setSelectedDay(dateStr); + setSelectedData(data); + }, [year, month]); + + const weekDays = ['日', '一', '二', '三', '四', '五', '六']; + const monthNames = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']; + + return ( + + + + {/* 头部 */} + + + + + + + + + + 历史时间轴 + + + {concept?.name} + + + + + 今天 + + + + + {/* 月份切换 */} + + + + + + {year}年 {monthNames[month]} + + + + + + + {/* 图例 */} + + + + + + + + + + + + 新闻 + + + + 研报 + + + + {/* 星期标题 */} + + {weekDays.map((day, index) => ( + + + {day} + + + ))} + + + {/* 日历主体 */} + + {loading ? ( +
+ + 加载历史数据... +
+ ) : ( + + {/* 日历网格 */} + + {calendarData.map((item, index) => ( + + ))} + + + {/* 选中日期详情 */} + {selectedDay && ( + { + setSelectedDay(null); + setSelectedData(null); + }} + onEventPress={handleEventPress} + /> + )} + + + + )} +
+
+
+
+ ); +}; + +const styles = StyleSheet.create({ + cell: { + width: CELL_SIZE, + height: CELL_SIZE * 1.1, + alignItems: 'center', + justifyContent: 'center', + padding: 2, + }, + cellDisabled: { + opacity: 0.3, + }, + cellToday: { + backgroundColor: 'rgba(139, 92, 246, 0.2)', + borderRadius: 8, + borderWidth: 1, + borderColor: 'rgba(139, 92, 246, 0.5)', + }, + cellPressed: { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + borderRadius: 8, + }, +}); + +export default memo(ConceptTimeline); diff --git a/MeAgent/src/screens/Concepts/index.js b/MeAgent/src/screens/Concepts/index.js new file mode 100644 index 00000000..b1beac99 --- /dev/null +++ b/MeAgent/src/screens/Concepts/index.js @@ -0,0 +1 @@ +export { default as ConceptList } from './ConceptList'; diff --git a/argon-pro-react-native/src/screens/Events/EventCard.js b/MeAgent/src/screens/Events/EventCard.js similarity index 97% rename from argon-pro-react-native/src/screens/Events/EventCard.js rename to MeAgent/src/screens/Events/EventCard.js index a47996de..3dbc0eaa 100644 --- a/argon-pro-react-native/src/screens/Events/EventCard.js +++ b/MeAgent/src/screens/Events/EventCard.js @@ -16,6 +16,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import { Ionicons } from '@expo/vector-icons'; import { StyleSheet, Dimensions } from 'react-native'; import { gradients, importanceGradients } from '../../theme'; +import { EventFollowButton } from '../../components/AddWatchlistButton'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); @@ -182,6 +183,12 @@ const EventCard = memo(({ event, onPress }) => { {event.view_count || 0} + {/* 关注按钮 */} + diff --git a/argon-pro-react-native/src/screens/Events/EventComments.js b/MeAgent/src/screens/Events/EventComments.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/EventComments.js rename to MeAgent/src/screens/Events/EventComments.js diff --git a/argon-pro-react-native/src/screens/Events/EventDetail.js b/MeAgent/src/screens/Events/EventDetail.js similarity index 97% rename from argon-pro-react-native/src/screens/Events/EventDetail.js rename to MeAgent/src/screens/Events/EventDetail.js index 3db55759..979c272f 100644 --- a/argon-pro-react-native/src/screens/Events/EventDetail.js +++ b/MeAgent/src/screens/Events/EventDetail.js @@ -160,16 +160,23 @@ const EventDetail = ({ route, navigation }) => { setSelectedStock(null); }, []); - // 查看更多股票信息 + // 查看更多股票信息 - 导航到股票详情页 const handleViewMoreStock = useCallback((stock) => { setSelectedStock(null); - // TODO: 跳转到股票详情页面 - toast.show({ - description: `即将查看 ${stock.stock_name || stock.name} 详情`, - placement: 'top', - bg: 'primary.500', + + // 获取股票代码和名称 + const stockCode = stock.stock_code || stock.code || ''; + const stockName = stock.stock_name || stock.name || ''; + + console.log('[EventDetail] 导航到股票详情:', { stockCode, stockName }); + + // 导航到股票详情页 + navigation.navigate('StockDetail', { + stockCode, + stockName, + eventTime: currentEvent?.event_time, // 传递事件时间用于历史K线定位 }); - }, [toast]); + }, [navigation, currentEvent]); // 点击概念 const handleConceptPress = useCallback((concept) => { diff --git a/argon-pro-react-native/src/screens/Events/EventList.js b/MeAgent/src/screens/Events/EventList.js similarity index 93% rename from argon-pro-react-native/src/screens/Events/EventList.js rename to MeAgent/src/screens/Events/EventList.js index a0dc4ded..57cf9a3c 100644 --- a/argon-pro-react-native/src/screens/Events/EventList.js +++ b/MeAgent/src/screens/Events/EventList.js @@ -225,45 +225,34 @@ const EventList = ({ navigation }) => { {/* 标题区域 */} - - - {/* 标题图标 */} - - - - - 事件中心 + + {/* 菜单按钮 - 移到左边 */} + navigation.openDrawer?.()} hitSlop={10}> + + + + + {/* 标题图标 */} + + + + + 事件中心 + + + + 发现市场热点,把握投资机会 - - - 发现市场热点,把握投资机会 - - + + {/* 模式切换 */} {renderModeToggle()} - {/* 菜单按钮 */} - navigation.openDrawer?.()} - > - {({ isPressed }) => ( - - - - )} - diff --git a/argon-pro-react-native/src/screens/Events/FilterModal.js b/MeAgent/src/screens/Events/FilterModal.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/FilterModal.js rename to MeAgent/src/screens/Events/FilterModal.js diff --git a/argon-pro-react-native/src/screens/Events/HistoricalEvents.js b/MeAgent/src/screens/Events/HistoricalEvents.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/HistoricalEvents.js rename to MeAgent/src/screens/Events/HistoricalEvents.js diff --git a/argon-pro-react-native/src/screens/Events/MainlineView.js b/MeAgent/src/screens/Events/MainlineView.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/MainlineView.js rename to MeAgent/src/screens/Events/MainlineView.js diff --git a/argon-pro-react-native/src/screens/Events/RelatedConcepts.js b/MeAgent/src/screens/Events/RelatedConcepts.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/RelatedConcepts.js rename to MeAgent/src/screens/Events/RelatedConcepts.js diff --git a/argon-pro-react-native/src/screens/Events/RelatedStocks.js b/MeAgent/src/screens/Events/RelatedStocks.js similarity index 85% rename from argon-pro-react-native/src/screens/Events/RelatedStocks.js rename to MeAgent/src/screens/Events/RelatedStocks.js index 49fab0f7..0b4120b7 100644 --- a/argon-pro-react-native/src/screens/Events/RelatedStocks.js +++ b/MeAgent/src/screens/Events/RelatedStocks.js @@ -4,7 +4,8 @@ */ import React, { memo, useCallback } from 'react'; -import { StyleSheet, Clipboard } from 'react-native'; +import { StyleSheet } from 'react-native'; +import * as ExpoClipboard from 'expo-clipboard'; import { Box, VStack, @@ -19,6 +20,7 @@ import { import { LinearGradient } from 'expo-linear-gradient'; import { Ionicons } from '@expo/vector-icons'; import { gradients } from '../../theme'; +import { StockWatchlistButton } from '../../components/AddWatchlistButton'; // 格式化涨跌幅 const formatChange = (value) => { @@ -114,28 +116,37 @@ const StockItem = memo(({ stock, quote, index, total, onPress, onCopyCode }) => - {/* 右侧:涨跌幅和价格 */} - - - + + - {formatChange(change)} - - - {price != null && ( - - ¥{price.toFixed(2)} - - )} - + + {formatChange(change)} + +
+ {price != null && ( + + ¥{price.toFixed(2)} + + )} + + {/* 加自选按钮 */} + + ); @@ -157,10 +168,10 @@ const RelatedStocks = ({ const toast = useToast(); // 复制股票代码 - const handleCopyCode = useCallback((code) => { + const handleCopyCode = useCallback(async (code) => { if (!code) return; try { - Clipboard.setString(code); + await ExpoClipboard.setStringAsync(code); toast.show({ description: `已复制 ${code}`, placement: 'top', @@ -169,6 +180,12 @@ const RelatedStocks = ({ }); } catch (error) { console.error('复制失败:', error); + toast.show({ + description: `复制失败`, + placement: 'top', + bg: 'danger.500', + duration: 1500, + }); } }, [toast]); diff --git a/argon-pro-react-native/src/screens/Events/SankeyFlow.js b/MeAgent/src/screens/Events/SankeyFlow.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/SankeyFlow.js rename to MeAgent/src/screens/Events/SankeyFlow.js diff --git a/argon-pro-react-native/src/screens/Events/StockDetailModal.js b/MeAgent/src/screens/Events/StockDetailModal.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/StockDetailModal.js rename to MeAgent/src/screens/Events/StockDetailModal.js diff --git a/argon-pro-react-native/src/screens/Events/TransmissionChain.js b/MeAgent/src/screens/Events/TransmissionChain.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/TransmissionChain.js rename to MeAgent/src/screens/Events/TransmissionChain.js diff --git a/argon-pro-react-native/src/screens/Events/index.js b/MeAgent/src/screens/Events/index.js similarity index 100% rename from argon-pro-react-native/src/screens/Events/index.js rename to MeAgent/src/screens/Events/index.js diff --git a/argon-pro-react-native/src/screens/Market/EventCalendar.js b/MeAgent/src/screens/Market/EventCalendar.js similarity index 100% rename from argon-pro-react-native/src/screens/Market/EventCalendar.js rename to MeAgent/src/screens/Market/EventCalendar.js diff --git a/argon-pro-react-native/src/screens/Market/MarketHot.js b/MeAgent/src/screens/Market/MarketHot.js similarity index 96% rename from argon-pro-react-native/src/screens/Market/MarketHot.js rename to MeAgent/src/screens/Market/MarketHot.js index c6fe2f30..ec925046 100644 --- a/argon-pro-react-native/src/screens/Market/MarketHot.js +++ b/MeAgent/src/screens/Market/MarketHot.js @@ -162,14 +162,23 @@ const MarketHot = ({ navigation }) => { {/* 标题栏 */} - - - 市场热点 - - - 涨停板块与个股分析 - - + + {/* 菜单按钮 - 移到左边 */} + navigation.openDrawer?.()} hitSlop={10}> + + + + + + + 市场热点 + + + + 涨停板块与个股分析 + + + navigation.navigate('TodayStats')} @@ -187,14 +196,6 @@ const MarketHot = ({ navigation }) => { > - navigation.openDrawer?.()} - p={2} - rounded="full" - bg="rgba(255,255,255,0.1)" - > - - diff --git a/argon-pro-react-native/src/screens/Market/SectorDetail.js b/MeAgent/src/screens/Market/SectorDetail.js similarity index 100% rename from argon-pro-react-native/src/screens/Market/SectorDetail.js rename to MeAgent/src/screens/Market/SectorDetail.js diff --git a/argon-pro-react-native/src/screens/Market/StockDetail.js b/MeAgent/src/screens/Market/StockDetail.js similarity index 100% rename from argon-pro-react-native/src/screens/Market/StockDetail.js rename to MeAgent/src/screens/Market/StockDetail.js diff --git a/argon-pro-react-native/src/screens/Market/TodayStats.js b/MeAgent/src/screens/Market/TodayStats.js similarity index 100% rename from argon-pro-react-native/src/screens/Market/TodayStats.js rename to MeAgent/src/screens/Market/TodayStats.js diff --git a/argon-pro-react-native/src/screens/Market/index.js b/MeAgent/src/screens/Market/index.js similarity index 100% rename from argon-pro-react-native/src/screens/Market/index.js rename to MeAgent/src/screens/Market/index.js diff --git a/MeAgent/src/screens/Profile/ProfileScreen.js b/MeAgent/src/screens/Profile/ProfileScreen.js new file mode 100644 index 00000000..b89e6f83 --- /dev/null +++ b/MeAgent/src/screens/Profile/ProfileScreen.js @@ -0,0 +1,457 @@ +/** + * 个人中心页面 + * 显示用户信息、自选概览、功能菜单 + */ + +import React, { useCallback } from 'react'; +import { StyleSheet, ScrollView, RefreshControl } from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Icon, + Pressable, + Spinner, +} from 'native-base'; +import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useNavigation } from '@react-navigation/native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { useAuth } from '../../contexts/AuthContext'; +import { useWatchlist } from '../../hooks/useWatchlist'; + +// 菜单配置 +const MENU_SECTIONS = [ + { + title: '我的服务', + items: [ + { + key: 'watchlist', + icon: 'star', + label: '我的自选', + desc: '股票和事件', + color: '#EC4899', + route: 'WatchlistDrawer', + }, + { + key: 'subscription', + icon: 'card', + label: '订阅管理', + desc: '会员服务', + color: '#F59E0B', + route: 'Subscription', + }, + { + key: 'messages', + icon: 'notifications', + label: '消息中心', + desc: '通知提醒', + color: '#3B82F6', + route: 'Messages', + }, + ], + }, + { + title: '更多功能', + items: [ + { + key: 'feedback', + icon: 'chatbubble-ellipses', + label: '意见反馈', + desc: '帮助改进', + color: '#8B5CF6', + route: 'Feedback', + }, + { + key: 'settings', + icon: 'settings', + label: '设置', + desc: '偏好设置', + color: '#6B7280', + route: 'Settings', + }, + { + key: 'about', + icon: 'information-circle', + label: '关于我们', + desc: '版本信息', + color: '#06B6D4', + route: 'About', + }, + ], + }, +]; + +/** + * 用户信息卡片 + */ +const UserCard = ({ user, subscription, isLoggedIn, onLogin, onLogout }) => { + if (!isLoggedIn) { + return ( + + + + + + + + + 登录/注册 + + + 登录后解锁更多功能 + + + + + + + ); + } + + const getSubscriptionInfo = () => { + if (!subscription || !subscription.is_active) { + return { label: '免费用户', color: '#6B7280', bgColor: 'rgba(107, 114, 128, 0.2)' }; + } + if (subscription.type === 'max') { + return { label: 'Max 会员', color: '#D4AF37', bgColor: 'rgba(212, 175, 55, 0.2)' }; + } + if (subscription.type === 'pro') { + return { label: 'Pro 会员', color: '#7C3AED', bgColor: 'rgba(124, 58, 237, 0.2)' }; + } + return { label: '会员', color: '#3B82F6', bgColor: 'rgba(59, 130, 246, 0.2)' }; + }; + + const subInfo = getSubscriptionInfo(); + + return ( + + + {/* 头像 */} + + + {(user?.nickname || user?.username || 'U').charAt(0).toUpperCase()} + + + + {/* 用户信息 */} + + + + {user?.nickname || user?.username || '用户'} + + + + {subInfo.label} + + + + {user?.email && ( + + {user.email} + + )} + + + {/* 退出按钮 */} + + + + + + + + ); +}; + +/** + * 自选概览卡片 + */ +const WatchlistOverview = ({ stocks = [], events = [], onPress }) => { + const stockCount = Array.isArray(stocks) ? stocks.length : 0; + const eventCount = Array.isArray(events) ? events.length : 0; + + return ( + + + + + + + + + + 我的自选 + + + + + + {stockCount} 只股票 + + + + + + {eventCount} 个事件 + + + + + + + + + + ); +}; + +/** + * 菜单项 + */ +const MenuItem = ({ item, onPress }) => { + return ( + onPress(item)}> + {({ pressed }) => ( + + + + + + + {item.label} + + + {item.desc} + + + + + )} + + ); +}; + +/** + * 菜单分组 + */ +const MenuSection = ({ section, onItemPress }) => { + return ( + + + {section.title} + + }> + {section.items.map((item) => ( + + ))} + + + ); +}; + +/** + * 个人中心主页面 + */ +const ProfileScreen = () => { + const navigation = useNavigation(); + const { user, isLoggedIn, isLoading, subscription, logout } = useAuth(); + const watchlistData = useWatchlist({ autoLoad: true }); + + // 确保 stocks 和 events 是数组,防止 undefined 导致渲染错误 + const stocks = watchlistData?.stocks || []; + const events = watchlistData?.events || []; + const refreshAll = watchlistData?.refreshAll; + + const [refreshing, setRefreshing] = React.useState(false); + + // 刷新数据 + const handleRefresh = useCallback(async () => { + setRefreshing(true); + if (refreshAll) { + await refreshAll(); + } + setRefreshing(false); + }, [refreshAll]); + + // 登录 + const handleLogin = useCallback(() => { + navigation.getParent()?.navigate('Login'); + }, [navigation]); + + // 退出登录 + const handleLogout = useCallback(async () => { + await logout(); + }, [logout]); + + // 跳转自选 + const handleWatchlistPress = useCallback(() => { + navigation.navigate('WatchlistDrawer'); + }, [navigation]); + + // 菜单项点击 + const handleMenuItemPress = useCallback((item) => { + if (item.route === 'WatchlistDrawer') { + navigation.navigate('WatchlistDrawer'); + } else if (item.route === 'Settings') { + navigation.navigate('SettingsDrawer'); + } else if (item.route === 'About') { + navigation.navigate('SettingsDrawer', { screen: 'About' }); + } else { + // 其他页面暂时显示提示 + console.log('Navigate to:', item.route); + } + }, [navigation]); + + if (isLoading) { + return ( + + + + ); + } + + return ( + + + {/* 背景渐变 */} + + + + } + > + {/* 标题 */} + + + 个人中心 + + + + {/* 用户卡片 */} + + + {/* 自选概览 */} + {isLoggedIn && ( + + )} + + {/* 菜单 */} + {MENU_SECTIONS.map((section) => ( + + ))} + + {/* 底部间距 */} + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + headerGradient: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: 200, + }, + avatar: { + width: 64, + height: 64, + borderRadius: 32, + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export default ProfileScreen; diff --git a/MeAgent/src/screens/Profile/index.js b/MeAgent/src/screens/Profile/index.js new file mode 100644 index 00000000..cb916e9e --- /dev/null +++ b/MeAgent/src/screens/Profile/index.js @@ -0,0 +1,5 @@ +/** + * 个人中心模块导出 + */ + +export { default as ProfileScreen } from './ProfileScreen'; diff --git a/MeAgent/src/screens/StockDetail/StockDetailScreen.js b/MeAgent/src/screens/StockDetail/StockDetailScreen.js new file mode 100644 index 00000000..877ea748 --- /dev/null +++ b/MeAgent/src/screens/StockDetail/StockDetailScreen.js @@ -0,0 +1,271 @@ +/** + * 股票详情页面 - Wind 风格 + * 显示股票实时行情、分时/K线图、相关事件/概念/公告 + */ + +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import { StyleSheet, Dimensions } from 'react-native'; +import { Box, useToast, ScrollView } from 'native-base'; +import { useNavigation, useRoute } from '@react-navigation/native'; +import { useDispatch, useSelector } from 'react-redux'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { + PriceHeader, + ChartTypeTabs, + MinuteChart, + KlineChart, + OrderBook, + RelatedInfoTabs, + EventsPanel, + ConceptsPanel, + AnnouncementsPanel, + RiseAnalysisModal, +} from './components'; + +import { stockDetailService } from '../../services/stockService'; + +import { useWatchlist } from '../../hooks/useWatchlist'; +import { useSingleQuote } from '../../hooks/useRealtimeQuote'; +import { + fetchStockDetail, + fetchMinuteData, + fetchKlineData, + setChartType, + selectCurrentStock, + selectMinuteData, + selectMinutePrevClose, + selectKlineData, + selectChartType, + selectOrderBook, + selectStockLoading, +} from '../../store/slices/stockSlice'; + +const { height: SCREEN_HEIGHT } = Dimensions.get('window'); + +/** + * 股票详情页面 + */ +const StockDetailScreen = () => { + const navigation = useNavigation(); + const route = useRoute(); + const dispatch = useDispatch(); + const toast = useToast(); + + // 从路由获取股票信息 + const { stockCode, stockName, eventTime } = route.params || {}; + + // 本地状态 + const [infoTab, setInfoTab] = useState('events'); // events | concepts | announcements + const [riseAnalysisData, setRiseAnalysisData] = useState([]); + const [selectedAnalysis, setSelectedAnalysis] = useState(null); + const [analysisModalOpen, setAnalysisModalOpen] = useState(false); + + // Redux 状态 + const currentStock = useSelector(selectCurrentStock); + const minuteData = useSelector(selectMinuteData); + const minutePrevClose = useSelector(selectMinutePrevClose); + const klineData = useSelector(selectKlineData); + const chartType = useSelector(selectChartType); + const orderBook = useSelector(selectOrderBook); + const loading = useSelector(selectStockLoading); + + // WebSocket 实时行情 + const { quote: realtimeQuote } = useSingleQuote(stockCode); + + // 自选股操作 + const { isInWatchlist, toggleStock } = useWatchlist({ autoLoad: false }); + const inWatchlist = isInWatchlist(stockCode); + + // 合并行情数据 + const quote = useMemo(() => ({ + ...currentStock, + ...realtimeQuote, + }), [currentStock, realtimeQuote]); + + // 加载涨幅分析数据 + const loadRiseAnalysis = useCallback(async () => { + if (!stockCode) return; + + try { + const result = await stockDetailService.getRiseAnalysis(stockCode); + if (result.success && result.data) { + setRiseAnalysisData(result.data); + console.log('[StockDetailScreen] 涨幅分析数据:', result.data.length, '条'); + } + } catch (error) { + console.error('[StockDetailScreen] 加载涨幅分析失败:', error); + } + }, [stockCode]); + + // 加载股票数据 + const loadStockData = useCallback(async () => { + if (!stockCode) { + console.log('[StockDetailScreen] 无股票代码'); + return; + } + + console.log('[StockDetailScreen] 开始加载数据:', { stockCode, stockName, chartType }); + + // 加载股票详情 + dispatch(fetchStockDetail(stockCode)); + + // 根据当前图表类型加载数据 + if (chartType === 'minute') { + dispatch(fetchMinuteData(stockCode)); + } else { + dispatch(fetchKlineData({ stockCode, type: chartType, eventTime })); + } + + // 异步加载涨幅分析数据 + loadRiseAnalysis(); + }, [dispatch, stockCode, stockName, chartType, eventTime, loadRiseAnalysis]); + + // 初始加载 + useEffect(() => { + loadStockData(); + }, [loadStockData]); + + // 切换图表类型 + const handleChartTypeChange = useCallback((type) => { + dispatch(setChartType(type)); + + if (type === 'minute') { + dispatch(fetchMinuteData(stockCode)); + } else { + if (!klineData[type] || klineData[type].length === 0) { + dispatch(fetchKlineData({ stockCode, type, eventTime })); + } + } + }, [dispatch, stockCode, klineData, eventTime]); + + // 返回 + const handleBack = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + // 切换自选 + const handleToggleWatchlist = useCallback(async () => { + const result = await toggleStock(stockCode, stockName); + if (result.success) { + toast.show({ + description: inWatchlist ? '已从自选中移除' : '已添加到自选', + placement: 'top', + duration: 2000, + }); + } + }, [toggleStock, stockCode, stockName, inWatchlist, toast]); + + // 打开涨幅分析弹窗 + const handleAnalysisPress = useCallback((analysis) => { + console.log('[StockDetailScreen] 点击涨幅分析:', analysis); + if (analysis) { + setSelectedAnalysis(analysis); + setAnalysisModalOpen(true); + } + }, []); + + // 关闭涨幅分析弹窗 + const handleCloseAnalysisModal = useCallback(() => { + setAnalysisModalOpen(false); + setSelectedAnalysis(null); + }, []); + + // 获取当前图表数据 + const currentChartData = useMemo(() => { + if (chartType === 'minute') { + return minuteData; + } + return klineData[chartType] || []; + }, [chartType, minuteData, klineData]); + + const isChartLoading = chartType === 'minute' ? loading.minute : loading.kline; + + // 渲染相关信息内容 + const renderInfoContent = () => { + switch (infoTab) { + case 'events': + return ; + case 'concepts': + return ; + case 'announcements': + return ; + default: + return null; + } + }; + + return ( + + + {/* 价格头部 - Wind 风格 */} + + + {/* 图表类型切换 */} + + + {/* 图表区域 */} + + {chartType === 'minute' ? ( + + ) : ( + + )} + + + {/* 5档盘口(可选,放在分时图右侧效果更好,这里简化) */} + {chartType === 'minute' && orderBook.bidPrices?.length > 0 && ( + + )} + + {/* 相关信息 Tab */} + + + {/* 相关信息内容 */} + + {renderInfoContent()} + + + {/* 涨幅分析弹窗 */} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); + +export default StockDetailScreen; diff --git a/MeAgent/src/screens/StockDetail/components/AnnouncementsPanel.js b/MeAgent/src/screens/StockDetail/components/AnnouncementsPanel.js new file mode 100644 index 00000000..f4e952d9 --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/AnnouncementsPanel.js @@ -0,0 +1,186 @@ +/** + * 公司公告面板 + * 显示股票的公司公告列表 + */ + +import React, { memo, useEffect, useState, useCallback } from 'react'; +import { FlatList, Linking } from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Pressable, + Spinner, + Icon, +} from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; +import { companyService } from '../../../services/companyService'; + +// 格式化日期 +const formatDate = (dateStr) => { + if (!dateStr) return ''; + const date = new Date(dateStr); + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; +}; + +// 公告类型颜色 +const getAnnouncementTypeColor = (type) => { + const typeColors = { + '定期报告': '#3B82F6', + '业绩预告': '#F59E0B', + '重大事项': '#EF4444', + '股权激励': '#8B5CF6', + '增减持': '#EC4899', + }; + return typeColors[type] || '#6B7280'; +}; + +/** + * 公告卡片 + */ +const AnnouncementCard = memo(({ announcement, onPress }) => { + return ( + onPress?.(announcement)}> + {({ pressed }) => ( + + + {/* 公告类型标签 */} + {announcement.type && ( + + + + {announcement.type} + + + + )} + + {/* 标题 */} + + {announcement.title} + + + {/* 底部信息 */} + + + + + {announcement.source || '公司公告'} + + + + + {formatDate(announcement.publish_date || announcement.date)} + + + + + )} + + ); +}); + +/** + * 公司公告面板 + */ +const AnnouncementsPanel = memo(({ stockCode }) => { + const [announcements, setAnnouncements] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // 加载公告数据 + const loadAnnouncements = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + setError(null); + + const result = await companyService.getAnnouncements(stockCode, 30); + + if (result.success) { + setAnnouncements(result.data); + } else { + setError(result.error); + } + + setLoading(false); + }, [stockCode]); + + useEffect(() => { + loadAnnouncements(); + }, [loadAnnouncements]); + + // 点击公告 + const handleAnnouncementPress = useCallback((announcement) => { + if (announcement.url) { + Linking.openURL(announcement.url); + } + }, []); + + // 加载中 + if (loading) { + return ( + + + 加载公告... + + ); + } + + // 错误 + if (error) { + return ( + + + {error} + + 重试 + + + ); + } + + // 无数据 + if (announcements.length === 0) { + return ( + + + 暂无公司公告 + + ); + } + + return ( + String(item.id || index)} + renderItem={({ item }) => ( + + )} + contentContainerStyle={{ paddingVertical: 8 }} + showsVerticalScrollIndicator={false} + /> + ); +}); + +AnnouncementsPanel.displayName = 'AnnouncementsPanel'; + +export default AnnouncementsPanel; diff --git a/MeAgent/src/screens/StockDetail/components/ChartTypeTabs.js b/MeAgent/src/screens/StockDetail/components/ChartTypeTabs.js new file mode 100644 index 00000000..1d7c4ca4 --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/ChartTypeTabs.js @@ -0,0 +1,67 @@ +/** + * 图表类型切换组件 + * 支持分时、日K、周K、月K切换 + */ + +import React, { memo } from 'react'; +import { Box, HStack, VStack, Text, Pressable } from 'native-base'; + +// 图表类型配置 +const CHART_TYPES = [ + { key: 'minute', label: '分时' }, + { key: 'daily', label: '日K' }, + { key: 'weekly', label: '周K' }, + { key: 'monthly', label: '月K' }, +]; + +/** + * 图表类型切换 + * @param {object} props + * @param {string} props.activeType - 当前选中的类型 + * @param {function} props.onChange - 切换回调 + */ +const ChartTypeTabs = memo(({ activeType, onChange }) => { + return ( + + + {CHART_TYPES.map((type) => { + const isActive = activeType === type.key; + return ( + onChange?.(type.key)} + flex={1} + > + + + {type.label} + + + + ); + })} + + + ); +}); + +ChartTypeTabs.displayName = 'ChartTypeTabs'; + +export default ChartTypeTabs; diff --git a/MeAgent/src/screens/StockDetail/components/ConceptsPanel.js b/MeAgent/src/screens/StockDetail/components/ConceptsPanel.js new file mode 100644 index 00000000..0ae224ac --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/ConceptsPanel.js @@ -0,0 +1,296 @@ +/** + * 相关概念面板 + * 显示股票相关的概念板块 + */ + +import React, { memo, useEffect, useState, useCallback } from 'react'; +import { FlatList } from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Pressable, + Spinner, + Icon, +} from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; +import * as WebBrowser from 'expo-web-browser'; +import { companyService } from '../../../services/companyService'; + +// MD5 实现(与概念中心保持一致) +const md5 = (string) => { + function md5cycle(x, k) { + var a = x[0], b = x[1], c = x[2], d = x[3]; + a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); + a = gg(a, b, c, d, k[1], 5, -165796510); d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); + a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); + a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); + x[0] = add32(a, x[0]); x[1] = add32(b, x[1]); x[2] = add32(c, x[2]); x[3] = add32(d, x[3]); + } + function cmn(q, a, b, x, s, t) { a = add32(add32(a, q), add32(x, t)); return add32((a << s) | (a >>> (32 - s)), b); } + function ff(a, b, c, d, x, s, t) { return cmn((b & c) | ((~b) & d), a, b, x, s, t); } + function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & (~d)), a, b, x, s, t); } + function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); } + function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | (~d)), a, b, x, s, t); } + function md51(s) { + var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; + for (i = 64; i <= s.length; i += 64) { md5cycle(state, md5blk(s.substring(i - 64, i))); } + s = s.substring(i - 64); + var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); + tail[i >> 2] |= 0x80 << ((i % 4) << 3); + if (i > 55) { md5cycle(state, tail); for (i = 0; i < 16; i++) tail[i] = 0; } + tail[14] = n * 8; md5cycle(state, tail); return state; + } + function md5blk(s) { + var md5blks = [], i; + for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); } + return md5blks; + } + var hex_chr = '0123456789abcdef'.split(''); + function rhex(n) { var s = '', j = 0; for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f]; return s; } + function hex(x) { for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); return x.join(''); } + function add32(a, b) { return (a + b) & 0xffffffff; } + function utf8Encode(str) { + let utf8 = ''; + for (let i = 0; i < str.length; i++) { + let charCode = str.charCodeAt(i); + if (charCode < 128) { utf8 += String.fromCharCode(charCode); } + else if (charCode < 2048) { utf8 += String.fromCharCode((charCode >> 6) | 192); utf8 += String.fromCharCode((charCode & 63) | 128); } + else { utf8 += String.fromCharCode((charCode >> 12) | 224); utf8 += String.fromCharCode(((charCode >> 6) & 63) | 128); utf8 += String.fromCharCode((charCode & 63) | 128); } + } + return utf8; + } + return hex(md51(utf8Encode(string))); +}; + +// 概念详情页 URL +const getConceptUrl = (conceptName) => `https://valuefrontier.cn/htmls/concept/${md5(conceptName)}/`; + +// 涨跌颜色 +const getChangeColor = (change) => { + if (change > 0) return '#EF4444'; + if (change < 0) return '#22C55E'; + return '#94A3B8'; +}; + +// 格式化涨跌幅 +const formatChange = (change) => { + if (change === undefined || change === null) return '--'; + const sign = change > 0 ? '+' : ''; + return `${sign}${Number(change).toFixed(2)}%`; +}; + +/** + * 概念卡片 + */ +const ConceptCard = memo(({ concept, onPress }) => { + const changePercent = concept.price_info?.avg_change_pct || 0; + const changeColor = getChangeColor(changePercent); + + return ( + onPress?.(concept)}> + {({ pressed }) => ( + + + {/* 左侧: 概念信息 */} + + + {concept.concept} + + + {/* 层级信息 */} + {concept.hierarchy && ( + + {concept.hierarchy.lv1 && ( + + {concept.hierarchy.lv1} + + )} + {concept.hierarchy.lv2 && ( + <> + / + + {concept.hierarchy.lv2} + + + )} + + )} + + {/* 标签 */} + {concept.tags && concept.tags.length > 0 && ( + + {concept.tags.slice(0, 3).map((tag, i) => ( + + + {tag} + + + ))} + + )} + + {/* 成分股数量 */} + + + + {concept.stock_count || 0}只成分股 + + + + + {/* 右侧: 涨跌幅 */} + + + {formatChange(changePercent)} + + + {concept.price_info?.trade_date || ''} + + + + + )} + + ); +}); + +/** + * 相关概念面板 + */ +const ConceptsPanel = memo(({ stockCode }) => { + const [concepts, setConcepts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // 加载概念数据 + const loadConcepts = useCallback(async () => { + if (!stockCode) return; + + setLoading(true); + setError(null); + + const result = await companyService.getRelatedConcepts(stockCode); + + if (result.success) { + setConcepts(result.data); + } else { + setError(result.error); + } + + setLoading(false); + }, [stockCode]); + + useEffect(() => { + loadConcepts(); + }, [loadConcepts]); + + // 点击概念 - 打开概念详情页 + const handleConceptPress = useCallback(async (concept) => { + const conceptName = concept.concept; + if (!conceptName) return; + + const url = getConceptUrl(conceptName); + try { + await WebBrowser.openBrowserAsync(url, { + toolbarColor: '#0A0A0F', + controlsColor: '#3B82F6', + }); + } catch (error) { + console.error('打开概念详情失败:', error); + } + }, []); + + // 加载中 + if (loading) { + return ( + + + 加载概念... + + ); + } + + // 错误 + if (error) { + return ( + + + {error} + + 重试 + + + ); + } + + // 无数据 + if (concepts.length === 0) { + return ( + + + 暂无相关概念 + + ); + } + + return ( + String(item.concept_id || item.concept)} + renderItem={({ item }) => ( + + )} + contentContainerStyle={{ paddingVertical: 8 }} + showsVerticalScrollIndicator={false} + /> + ); +}); + +ConceptsPanel.displayName = 'ConceptsPanel'; + +export default ConceptsPanel; diff --git a/MeAgent/src/screens/StockDetail/components/EventsPanel.js b/MeAgent/src/screens/StockDetail/components/EventsPanel.js new file mode 100644 index 00000000..73c0e5d5 --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/EventsPanel.js @@ -0,0 +1,204 @@ +/** + * 相关事件面板 + * 显示股票相关的事件列表 + */ + +import React, { memo, useEffect, useState, useCallback } from 'react'; +import { FlatList } from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Pressable, + Spinner, + Icon, +} from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; +import { useNavigation } from '@react-navigation/native'; +import { companyService } from '../../../services/companyService'; + +// 格式化日期 +const formatDate = (dateStr) => { + if (!dateStr) return ''; + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now - date; + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffHours < 1) return '刚刚'; + if (diffHours < 24) return `${diffHours}小时前`; + if (diffDays < 7) return `${diffDays}天前`; + + return `${date.getMonth() + 1}/${date.getDate()}`; +}; + +// 重要性颜色 +const getImportanceColor = (importance) => { + if (importance >= 4) return '#EF4444'; + if (importance >= 3) return '#F59E0B'; + return '#6B7280'; +}; + +/** + * 事件卡片 + */ +const EventCard = memo(({ event, onPress }) => { + return ( + onPress?.(event)}> + {({ pressed }) => ( + + + {/* 标题 */} + + {event.title || event.event_title} + + + {/* 摘要 */} + {event.summary && ( + + {event.summary} + + )} + + {/* 底部信息 */} + + + {/* 重要性标签 */} + {event.importance && ( + + + {event.importance >= 4 ? '重要' : event.importance >= 3 ? '关注' : '一般'} + + + )} + + {/* 相关股票数 */} + {event.related_stocks_count > 0 && ( + + + + {event.related_stocks_count}只股票 + + + )} + + + {/* 时间 */} + + {formatDate(event.event_time || event.created_at)} + + + + + )} + + ); +}); + +/** + * 相关事件面板 + */ +const EventsPanel = memo(({ stockName, stockCode }) => { + const navigation = useNavigation(); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // 加载事件数据 + const loadEvents = useCallback(async () => { + if (!stockName) return; + + setLoading(true); + setError(null); + + const result = await companyService.getRelatedEvents(stockName, 1, 20); + + if (result.success) { + setEvents(result.data); + } else { + setError(result.error); + } + + setLoading(false); + }, [stockName]); + + useEffect(() => { + loadEvents(); + }, [loadEvents]); + + // 点击事件 + const handleEventPress = useCallback((event) => { + navigation.navigate('EventDetail', { + eventId: event.id || event.event_id, + eventTitle: event.title || event.event_title, + }); + }, [navigation]); + + // 加载中 + if (loading) { + return ( + + + 加载事件... + + ); + } + + // 错误 + if (error) { + return ( + + + {error} + + 重试 + + + ); + } + + // 无数据 + if (events.length === 0) { + return ( + + + 暂无相关事件 + + ); + } + + return ( + String(item.id || item.event_id)} + renderItem={({ item }) => ( + + )} + contentContainerStyle={{ paddingVertical: 8 }} + showsVerticalScrollIndicator={false} + /> + ); +}); + +EventsPanel.displayName = 'EventsPanel'; + +export default EventsPanel; diff --git a/MeAgent/src/screens/StockDetail/components/KlineChart.js b/MeAgent/src/screens/StockDetail/components/KlineChart.js new file mode 100644 index 00000000..cab3899c --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/KlineChart.js @@ -0,0 +1,791 @@ +/** + * K线图组件 - Wind 风格 + * 显示日K/周K/月K蜡烛图,带成交量柱和触摸交互 + * 支持涨幅分析标记显示 + */ + +import React, { memo, useMemo, useState, useCallback } from 'react'; +import { Dimensions, PanResponder } from 'react-native'; +import { Box, Text, HStack, VStack, Spinner, Pressable, Icon } from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; +import Svg, { + Rect, + Line, + Text as SvgText, + G, + Path, + Defs, + LinearGradient as SvgLinearGradient, + Stop, + Circle, +} from 'react-native-svg'; + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const CHART_WIDTH = SCREEN_WIDTH - 32; +const PRICE_CHART_HEIGHT = 180; +const VOLUME_CHART_HEIGHT = 50; +const CHART_HEIGHT = PRICE_CHART_HEIGHT + VOLUME_CHART_HEIGHT + 20; +const PADDING = { top: 25, right: 55, bottom: 25, left: 10 }; +const CANDLE_GAP = 1.5; +const MAX_CANDLES = 60; + +// K线类型标签 +const TYPE_LABELS = { + daily: '日K', + weekly: '周K', + monthly: '月K', +}; + +// 格式化价格 +const formatPrice = (price) => { + if (price === undefined || price === null) return '--'; + return Number(price).toFixed(2); +}; + +// 格式化成交量 +const formatVolume = (volume) => { + if (!volume) return '--'; + if (volume >= 100000000) { + return `${(volume / 100000000).toFixed(2)}亿`; + } + if (volume >= 10000) { + return `${(volume / 10000).toFixed(0)}万`; + } + return String(volume); +}; + +// 格式化成交额 +const formatAmount = (amount) => { + if (!amount) return '--'; + if (amount >= 100000000) { + return `${(amount / 100000000).toFixed(2)}亿`; + } + if (amount >= 10000) { + return `${(amount / 10000).toFixed(0)}万`; + } + return String(Math.round(amount)); +}; + +// 格式化日期 +const formatDate = (dateStr) => { + if (!dateStr) return ''; + const cleaned = dateStr.replace(/-/g, ''); + if (cleaned.length === 8) { + return `${cleaned.substring(4, 6)}/${cleaned.substring(6, 8)}`; + } + return dateStr.substring(5, 10).replace('-', '/'); +}; + +// 格式化完整日期 +const formatFullDate = (dateStr) => { + if (!dateStr) return ''; + const cleaned = dateStr.replace(/-/g, ''); + if (cleaned.length === 8) { + return `${cleaned.substring(0, 4)}-${cleaned.substring(4, 6)}-${cleaned.substring(6, 8)}`; + } + return dateStr; +}; + +// 计算涨跌幅 +const calcChangePercent = (close, preClose) => { + if (!preClose || preClose === 0) return 0; + return ((close - preClose) / preClose) * 100; +}; + +// 标准化日期格式用于比较 +const normalizeDate = (dateStr) => { + if (!dateStr) return ''; + return dateStr.replace(/-/g, '').substring(0, 8); +}; + +/** + * K线图 + * @param {Array} data - K线数据 + * @param {string} type - K线类型: daily | weekly | monthly + * @param {boolean} loading - 加载状态 + * @param {Array} riseAnalysisData - 涨幅分析数据 + * @param {function} onAnalysisPress - 点击分析标记回调 + */ +const KlineChart = memo(({ data = [], type = 'daily', loading, riseAnalysisData = [], onAnalysisPress }) => { + const [selectedIndex, setSelectedIndex] = useState(null); + + // 构建日期到分析数据的映射 + const analysisMap = useMemo(() => { + const map = {}; + if (riseAnalysisData && riseAnalysisData.length > 0) { + riseAnalysisData.forEach(item => { + const dateKey = normalizeDate(item.trade_date); + if (dateKey) { + map[dateKey] = item; + } + }); + } + return map; + }, [riseAnalysisData]); + + // 计算图表数据 + const chartData = useMemo(() => { + if (!data || data.length === 0) { + return null; + } + + // 只取最后 MAX_CANDLES 根K线 + const displayData = data.slice(-MAX_CANDLES); + + // 计算价格范围 + let minPrice = Infinity; + let maxPrice = -Infinity; + let maxVolume = 0; + + displayData.forEach(d => { + const high = d.high || d.high_price || 0; + const low = d.low || d.low_price || 0; + const vol = d.volume || d.vol || 0; + + if (high > maxPrice) maxPrice = high; + if (low < minPrice) minPrice = low; + if (vol > maxVolume) maxVolume = vol; + }); + + // 增加上下边距 + const pricePadding = (maxPrice - minPrice) * 0.08; + minPrice = minPrice - pricePadding; + maxPrice = maxPrice + pricePadding; + + const priceRange = maxPrice - minPrice || 1; + const volumeRange = maxVolume || 1; + + // 图表绘制区域 + const drawWidth = CHART_WIDTH - PADDING.left - PADDING.right; + const candleWidth = Math.max(3, (drawWidth - (displayData.length - 1) * CANDLE_GAP) / displayData.length); + const priceDrawHeight = PRICE_CHART_HEIGHT - PADDING.top; + const volumeDrawHeight = VOLUME_CHART_HEIGHT - 5; + + // 计算每根K线的位置和尺寸 + const candles = displayData.map((d, i) => { + const open = d.open || d.open_price || 0; + const close = d.close || d.close_price || 0; + const high = d.high || d.high_price || 0; + const low = d.low || d.low_price || 0; + const volume = d.volume || d.vol || 0; + const amount = d.amount || d.turnover || 0; + const preClose = d.pre_close || (i > 0 ? displayData[i - 1].close || displayData[i - 1].close_price : open); + + const isUp = close >= open; + const x = PADDING.left + i * (candleWidth + CANDLE_GAP); + + // 价格图 + const bodyTop = PADDING.top + (1 - (Math.max(open, close) - minPrice) / priceRange) * priceDrawHeight; + const bodyBottom = PADDING.top + (1 - (Math.min(open, close) - minPrice) / priceRange) * priceDrawHeight; + const bodyHeight = Math.max(bodyBottom - bodyTop, 1); + + const highY = PADDING.top + (1 - (high - minPrice) / priceRange) * priceDrawHeight; + const lowY = PADDING.top + (1 - (low - minPrice) / priceRange) * priceDrawHeight; + + // 成交量图 + const volumeTop = PRICE_CHART_HEIGHT + 10; + const volumeHeight = Math.max((volume / volumeRange) * volumeDrawHeight, 1); + const volumeY = volumeTop + volumeDrawHeight - volumeHeight; + + // 检查是否有涨幅分析数据 + const dateKey = normalizeDate(d.date || d.trade_date); + const hasAnalysis = !!analysisMap[dateKey]; + const analysisData = analysisMap[dateKey] || null; + + return { + index: i, + x, + isUp, + bodyX: x, + bodyY: bodyTop, + bodyWidth: candleWidth, + bodyHeight, + wickX: x + candleWidth / 2, + highY, + lowY, + volumeX: x, + volumeY, + volumeWidth: candleWidth, + volumeHeight, + // 原始数据 + date: d.date || d.trade_date, + open, + close, + high, + low, + volume, + amount, + preClose, + changePercent: calcChangePercent(close, preClose), + // 涨幅分析 + hasAnalysis, + analysisData, + }; + }); + + // 计算5日/10日/20日均线 + const ma5 = [], ma10 = [], ma20 = []; + for (let i = 0; i < candles.length; i++) { + // MA5 + if (i >= 4) { + const sum5 = candles.slice(i - 4, i + 1).reduce((s, c) => s + c.close, 0); + const ma5Value = sum5 / 5; + const ma5Y = PADDING.top + (1 - (ma5Value - minPrice) / priceRange) * priceDrawHeight; + ma5.push({ x: candles[i].wickX, y: ma5Y, value: ma5Value }); + } + // MA10 + if (i >= 9) { + const sum10 = candles.slice(i - 9, i + 1).reduce((s, c) => s + c.close, 0); + const ma10Value = sum10 / 10; + const ma10Y = PADDING.top + (1 - (ma10Value - minPrice) / priceRange) * priceDrawHeight; + ma10.push({ x: candles[i].wickX, y: ma10Y, value: ma10Value }); + } + // MA20 + if (i >= 19) { + const sum20 = candles.slice(i - 19, i + 1).reduce((s, c) => s + c.close, 0); + const ma20Value = sum20 / 20; + const ma20Y = PADDING.top + (1 - (ma20Value - minPrice) / priceRange) * priceDrawHeight; + ma20.push({ x: candles[i].wickX, y: ma20Y, value: ma20Value }); + } + } + + // 生成均线路径 + const generateLinePath = (points) => { + if (points.length === 0) return ''; + return points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' '); + }; + + // 日期轴标签(显示5个) + const dateInterval = Math.floor(displayData.length / 4); + const dateLabels = [0, dateInterval, dateInterval * 2, dateInterval * 3, displayData.length - 1] + .filter(i => i < displayData.length) + .map(i => ({ + x: PADDING.left + i * (candleWidth + CANDLE_GAP) + candleWidth / 2, + label: formatDate(displayData[i]?.date || displayData[i]?.trade_date), + })); + + // 价格网格线 + const priceStep = priceRange / 4; + const priceGrids = [ + { price: maxPrice, y: PADDING.top }, + { price: minPrice + priceStep * 3, y: PADDING.top + priceDrawHeight * 0.25 }, + { price: minPrice + priceStep * 2, y: PADDING.top + priceDrawHeight * 0.5 }, + { price: minPrice + priceStep, y: PADDING.top + priceDrawHeight * 0.75 }, + { price: minPrice, y: PADDING.top + priceDrawHeight }, + ]; + + return { + candles, + dateLabels, + priceGrids, + maxVolume, + priceDrawHeight, + candleWidth, + ma5Path: generateLinePath(ma5), + ma10Path: generateLinePath(ma10), + ma20Path: generateLinePath(ma20), + ma5, + ma10, + ma20, + }; + }, [data, analysisMap]); + + // 创建触摸响应器 + const panResponder = useMemo(() => { + if (!chartData) return null; + + let startX = 0; + let startY = 0; + let startTime = 0; + + return PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderGrant: (evt) => { + const { locationX, locationY } = evt.nativeEvent; + startX = locationX; + startY = locationY; + startTime = Date.now(); + updateSelectedCandle(locationX, locationY, false); + }, + onPanResponderMove: (evt) => { + const { locationX, locationY } = evt.nativeEvent; + updateSelectedCandle(locationX, locationY, false); + }, + onPanResponderRelease: (evt) => { + const { locationX, locationY } = evt.nativeEvent; + const endTime = Date.now(); + const duration = endTime - startTime; + const distance = Math.sqrt(Math.pow(locationX - startX, 2) + Math.pow(locationY - startY, 2)); + + // 判断是否为点击(短时间、小移动距离) + const isTap = duration < 300 && distance < 15; + + if (isTap) { + // 检查是否点击了分析标记 + updateSelectedCandle(locationX, locationY, true); + } + + // 保持选中状态一段时间后清除 + setTimeout(() => setSelectedIndex(null), 2000); + }, + }); + }, [chartData, updateSelectedCandle]); + + // 更新选中的K线 + const updateSelectedCandle = useCallback((locationX, locationY, isTap = false) => { + if (!chartData) return; + + const { candles, candleWidth } = chartData; + const adjustedX = locationX; + + // 找到最近的K线 + let nearestIndex = -1; + let minDistance = Infinity; + + candles.forEach((candle, i) => { + const candleCenter = candle.x + candleWidth / 2; + const distance = Math.abs(adjustedX - candleCenter); + if (distance < minDistance) { + minDistance = distance; + nearestIndex = i; + } + }); + + if (nearestIndex >= 0 && minDistance < (candleWidth + CANDLE_GAP) * 2) { + const candle = candles[nearestIndex]; + + // 检查是否点击了涨幅分析标记(在顶部区域) + if (isTap && candle.hasAnalysis && locationY < PADDING.top + 20) { + // 触发分析弹窗 + onAnalysisPress?.(candle.analysisData); + return; + } + + setSelectedIndex(nearestIndex); + } + }, [chartData, onAnalysisPress]); + + // 获取选中的K线数据 + const selectedCandle = useMemo(() => { + if (selectedIndex === null || !chartData) return null; + return chartData.candles[selectedIndex]; + }, [selectedIndex, chartData]); + + // 加载状态 + if (loading) { + return ( + + + 加载K线数据... + + ); + } + + // 无数据状态 + if (!chartData) { + return ( + + 暂无K线数据 + + ); + } + + const { candles, dateLabels, priceGrids, maxVolume, ma5Path, ma10Path, ma20Path } = chartData; + + // 获取显示的均线值(选中的K线或最后一根) + const displayIndex = selectedIndex !== null ? selectedIndex : candles.length - 1; + const displayCandle = candles[displayIndex]; + const ma5Value = chartData.ma5[Math.max(0, displayIndex - 4)]?.value; + const ma10Value = chartData.ma10[Math.max(0, displayIndex - 9)]?.value; + const ma20Value = chartData.ma20[Math.max(0, displayIndex - 19)]?.value; + + return ( + + {/* 顶部信息栏 - Wind 风格 */} + + {/* 第一行:选中日期和OHLC */} + {selectedCandle ? ( + + + + {formatFullDate(selectedCandle.date)} + + + + + {formatPrice(selectedCandle.open)} + + + + {formatPrice(selectedCandle.high)} + + + + {formatPrice(selectedCandle.low)} + + + + + {formatPrice(selectedCandle.close)} + + + + + + + = 0 ? '#EF4444' : '#22C55E'} fontSize={11} fontWeight="bold"> + {selectedCandle.changePercent >= 0 ? '+' : ''}{selectedCandle.changePercent.toFixed(2)}% + + {/* 涨幅分析按钮 */} + {selectedCandle.hasAnalysis && ( + onAnalysisPress?.(selectedCandle.analysisData)} + hitSlop={5} + > + + + + 涨幅分析 + + + + )} + + + + + {formatVolume(selectedCandle.volume)} + + + + {formatAmount(selectedCandle.amount)} + + + + + ) : ( + + + {TYPE_LABELS[type] || 'K线'}走势 + + 触摸查看详情 + + )} + + + {/* 均线标签栏 */} + + + + MA5: + + {ma5Value ? formatPrice(ma5Value) : '--'} + + + + + MA10: + + {ma10Value ? formatPrice(ma10Value) : '--'} + + + + + MA20: + + {ma20Value ? formatPrice(ma20Value) : '--'} + + + + + {/* SVG 图表区域 */} + + + + + + + + + + + + + + {/* 价格区域网格线 */} + {priceGrids.map((item, i) => ( + + + + {formatPrice(item.price)} + + + ))} + + {/* 成交量分隔线 */} + + + {/* 均线 */} + {ma20Path && ( + + )} + {ma10Path && ( + + )} + {ma5Path && ( + + )} + + {/* K线蜡烛 */} + {candles.map((candle, i) => ( + + {/* 上下影线 */} + + {/* 蜡烛体 */} + + {/* 成交量柱 */} + + + ))} + + {/* 涨幅分析标记 - 金色五角星 */} + {candles.filter(c => c.hasAnalysis).map((candle, i) => { + const starCenterX = candle.wickX; + const starCenterY = Math.max(PADDING.top - 2, candle.highY - 14); + const starSize = 7; + // 五角星路径(相对于中心点) + const starPath = ` + M ${starCenterX} ${starCenterY - starSize} + L ${starCenterX + starSize * 0.22} ${starCenterY - starSize * 0.31} + L ${starCenterX + starSize * 0.95} ${starCenterY - starSize * 0.31} + L ${starCenterX + starSize * 0.36} ${starCenterY + starSize * 0.12} + L ${starCenterX + starSize * 0.59} ${starCenterY + starSize * 0.81} + L ${starCenterX} ${starCenterY + starSize * 0.38} + L ${starCenterX - starSize * 0.59} ${starCenterY + starSize * 0.81} + L ${starCenterX - starSize * 0.36} ${starCenterY + starSize * 0.12} + L ${starCenterX - starSize * 0.95} ${starCenterY - starSize * 0.31} + L ${starCenterX - starSize * 0.22} ${starCenterY - starSize * 0.31} + Z + `; + return ( + + {/* 连接线 */} + + {/* 星星光晕 */} + + {/* 金色五角星 */} + + + ); + })} + + {/* 选中的十字线 */} + {selectedCandle && ( + + {/* 垂直线 */} + + {/* 水平线 */} + + {/* 价格标签 */} + + + {formatPrice(selectedCandle.close)} + + {/* 成交量区域垂直线 */} + + + )} + + {/* 成交量标签 */} + + {formatVolume(maxVolume)} + + + {/* 日期轴标签 */} + {dateLabels.map((item, i) => ( + + {item.label} + + ))} + + + + {/* 底部图例 */} + + + + 阳线(涨) + + + + 阴线(跌) + + + + ); +}); + +KlineChart.displayName = 'KlineChart'; + +export default KlineChart; diff --git a/MeAgent/src/screens/StockDetail/components/MinuteChart.js b/MeAgent/src/screens/StockDetail/components/MinuteChart.js new file mode 100644 index 00000000..11e6adea --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/MinuteChart.js @@ -0,0 +1,545 @@ +/** + * 分时图组件 - Wind 风格 (使用 react-native-svg) + * 功能: + * - 分时线 + 均价线 + * - 触控十字线交互,显示选中点的详细数据 + * - 成交量柱状图 + * - 昨收参考线 + */ + +import React, { memo, useMemo, useState, useCallback, useRef } from 'react'; +import { Dimensions, StyleSheet } from 'react-native'; +import { Box, Text, HStack, VStack, Spinner, Pressable } from 'native-base'; +import Svg, { + Path, + Line, + Rect, + Circle, + Text as SvgText, + G, + Defs, + LinearGradient, + Stop, +} from 'react-native-svg'; +import { + GestureHandlerRootView, + PanGestureHandler, +} from 'react-native-gesture-handler'; + +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const CHART_WIDTH = SCREEN_WIDTH - 32; +const CHART_HEIGHT = 180; +const VOLUME_HEIGHT = 50; +const PADDING = { top: 15, right: 55, bottom: 5, left: 10 }; + +// 格式化时间 +const formatTime = (time) => { + if (!time) return ''; + if (typeof time === 'string') { + return time.substring(0, 5); + } + return ''; +}; + +// 格式化价格 +const formatPrice = (price) => { + if (price === undefined || price === null || isNaN(price)) return '--'; + return Number(price).toFixed(2); +}; + +// 格式化涨跌幅 +const formatPercent = (current, preClose) => { + if (!current || !preClose) return '0.00%'; + const change = ((current - preClose) / preClose) * 100; + const sign = change >= 0 ? '+' : ''; + return `${sign}${change.toFixed(2)}%`; +}; + +// 格式化成交量 +const formatVolume = (vol) => { + if (!vol) return '0'; + if (vol >= 10000) { + return `${(vol / 10000).toFixed(0)}万`; + } + return String(Math.round(vol)); +}; + +/** + * 生成折线路径 + */ +const generateLinePath = (points) => { + if (!points || points.length === 0) return ''; + + let path = `M ${points[0].x} ${points[0].y}`; + for (let i = 1; i < points.length; i++) { + path += ` L ${points[i].x} ${points[i].y}`; + } + return path; +}; + +/** + * 生成填充区域路径(用于渐变填充) + */ +const generateAreaPath = (points, bottomY) => { + if (!points || points.length === 0) return ''; + + let path = `M ${points[0].x} ${bottomY}`; + path += ` L ${points[0].x} ${points[0].y}`; + + for (let i = 1; i < points.length; i++) { + path += ` L ${points[i].x} ${points[i].y}`; + } + + path += ` L ${points[points.length - 1].x} ${bottomY}`; + path += ' Z'; + return path; +}; + +/** + * 分时图主组件 - Wind 风格 + */ +const MinuteChart = memo(({ data = [], preClose, loading }) => { + const [activeIndex, setActiveIndex] = useState(null); + const svgRef = useRef(null); + + // 处理图表数据 + const chartData = useMemo(() => { + if (!data || data.length === 0) { + return null; + } + + const prices = data.map(d => d.price || d.current_price || d.close || 0).filter(p => p > 0); + const avgPrices = data.map(d => d.avg_price || d.average_price || 0); + + if (prices.length === 0) { + return null; + } + + // 使用传入的昨收价,或从数据中获取,或使用第一个价格作为基准 + const effectivePreClose = preClose || + data[0]?.prev_close || + data[0]?.pre_close || + prices[0]; + + // 计算价格范围(以昨收为中心对称) + const validPrices = prices.filter(p => p > 0); + const validAvgPrices = avgPrices.filter(p => p > 0); + const allPrices = [...validPrices, ...validAvgPrices, effectivePreClose].filter(p => p > 0); + + const maxDiff = Math.max( + Math.abs(Math.max(...allPrices) - effectivePreClose), + Math.abs(effectivePreClose - Math.min(...allPrices)), + effectivePreClose * 0.02 // 至少 2% 的波动范围 + ) * 1.1; + + const minPrice = effectivePreClose - maxDiff; + const maxPrice = effectivePreClose + maxDiff; + const priceRange = maxPrice - minPrice; + const lastPrice = prices[prices.length - 1]; + const isUp = lastPrice >= effectivePreClose; + + // 图表绘制区域 + const drawWidth = CHART_WIDTH - PADDING.left - PADDING.right; + const drawHeight = CHART_HEIGHT - PADDING.top - PADDING.bottom; + + // 坐标转换函数 + const xScale = (index) => PADDING.left + (index / (data.length - 1 || 1)) * drawWidth; + const yScale = (price) => PADDING.top + ((maxPrice - price) / priceRange) * drawHeight; + + // 分时线点位 + const pricePoints = data.map((d, i) => { + const price = d.price || d.current_price || d.close || effectivePreClose; + return { + x: xScale(i), + y: yScale(price), + price, + time: d.time, + volume: d.volume, + avgPrice: d.avg_price || d.average_price, + }; + }); + + // 均价线点位 + const avgPoints = data + .map((d, i) => { + const avgPrice = d.avg_price || d.average_price; + if (!avgPrice || avgPrice <= 0) return null; + return { + x: xScale(i), + y: yScale(avgPrice), + price: avgPrice, + }; + }) + .filter(p => p !== null); + + // 成交量数据 + const volumes = data.map(d => d.volume || 0); + const maxVolume = Math.max(...volumes, 1); + + return { + pricePoints, + avgPoints, + volumes, + maxVolume, + minPrice, + maxPrice, + preClose: effectivePreClose, + lastPrice, + isUp, + drawWidth, + drawHeight, + yScale, + xScale, + priceRange, + }; + }, [data, preClose]); + + // 处理触控 + const handleTouch = useCallback((event) => { + if (!chartData || !data || data.length === 0) return; + + const { locationX } = event.nativeEvent; + const drawWidth = CHART_WIDTH - PADDING.left - PADDING.right; + + // 计算最近的数据点索引 + const relativeX = locationX - PADDING.left; + const index = Math.round((relativeX / drawWidth) * (data.length - 1)); + const clampedIndex = Math.max(0, Math.min(data.length - 1, index)); + + setActiveIndex(clampedIndex); + }, [chartData, data]); + + // 处理触控结束 + const handleTouchEnd = useCallback(() => { + setActiveIndex(null); + }, []); + + // 加载状态 + if (loading) { + return ( + + + 加载分时数据... + + ); + } + + // 无数据状态 + if (!chartData || chartData.pricePoints.length === 0) { + return ( + + 暂无分时数据 + + ); + } + + const lineColor = chartData.isUp ? '#EF4444' : '#22C55E'; + const areaGradientId = chartData.isUp ? 'areaGradientUp' : 'areaGradientDown'; + const activePoint = activeIndex !== null ? chartData.pricePoints[activeIndex] : null; + + // 计算昨收参考线 Y 坐标 + const preCloseY = chartData.yScale(chartData.preClose); + + return ( + + {/* 图表标题 */} + + 分时走势 + + + + 分时 + + + + 均价 + + + + + {/* 触控时显示的数据 */} + {activePoint && ( + + + + 时间 + {formatTime(activePoint.time) || '--'} + + + 价格 + {formatPrice(activePoint.price)} + + + 涨跌 + + {formatPercent(activePoint.price, chartData.preClose)} + + + {activePoint.avgPrice && ( + + 均价 + {formatPrice(activePoint.avgPrice)} + + )} + + 成交量 + {formatVolume(activePoint.volume)} + + + + )} + + {/* 主图表区域 */} + + + {/* 渐变定义 */} + + + + + + + + + + + + {/* 背景网格线 */} + + {/* 横向网格线(价格)- 5条 */} + {[0, 0.25, 0.5, 0.75, 1].map((ratio, i) => { + const y = PADDING.top + ratio * chartData.drawHeight; + return ( + + ); + })} + + + {/* 昨收参考虚线 */} + + + {/* 分时线填充区域 */} + + + {/* 分时线 */} + + + {/* 均价线 */} + {chartData.avgPoints.length > 1 && ( + + )} + + {/* 右侧价格和涨跌幅标签 - 分两行显示 */} + + {/* 最高价区域 */} + + {formatPrice(chartData.maxPrice)} + + + {formatPercent(chartData.maxPrice, chartData.preClose)} + + + {/* 昨收价区域 */} + + {formatPrice(chartData.preClose)} + + + 0.00% + + + {/* 最低价区域 */} + + {formatPrice(chartData.minPrice)} + + + {formatPercent(chartData.minPrice, chartData.preClose)} + + + + {/* 十字线和选中点 */} + {activeIndex !== null && activePoint && ( + + {/* 垂直线 */} + + {/* 水平线 */} + + {/* 选中点圆圈 */} + + + )} + + + + {/* 时间轴 */} + + {['09:30', '10:30', '11:30/13:00', '14:00', '15:00'].map((t, i) => ( + {t} + ))} + + + {/* 成交量图 */} + + + {data.map((item, i) => { + const volume = item.volume || 0; + const barHeight = (volume / chartData.maxVolume) * (VOLUME_HEIGHT - 10); + const price = item.price || item.close || 0; + const prevPrice = i > 0 ? (data[i - 1].price || data[i - 1].close || 0) : chartData.preClose; + const isUp = price >= prevPrice; + const isActive = activeIndex === i; + + const x = PADDING.left + (i / (data.length - 1 || 1)) * chartData.drawWidth; + const barWidth = Math.max(1, chartData.drawWidth / data.length - 1); + + return ( + + ); + })} + {/* 最大成交量标注 */} + + {formatVolume(chartData.maxVolume)} + + + + + ); +}); + +MinuteChart.displayName = 'MinuteChart'; + +export default MinuteChart; diff --git a/MeAgent/src/screens/StockDetail/components/OrderBook.js b/MeAgent/src/screens/StockDetail/components/OrderBook.js new file mode 100644 index 00000000..d7ac3eb1 --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/OrderBook.js @@ -0,0 +1,238 @@ +/** + * 5档盘口组件 - 优化版 + * 显示买卖5档挂单信息 + */ + +import React, { memo, useMemo } from 'react'; +import { Box, HStack, VStack, Text, Icon } from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; + +// 格式化价格 +const formatPrice = (price) => { + if (price === undefined || price === null || price === 0) return '--'; + return Number(price).toFixed(2); +}; + +// 格式化成交量(手) +const formatVolume = (volume) => { + if (!volume) return '--'; + // 假设volume单位是股,转换为手(100股=1手) + const lots = Math.round(volume / 100); + if (lots >= 10000) { + return `${(lots / 10000).toFixed(1)}万`; + } + return String(lots); +}; + +/** + * 单行盘口 + */ +const OrderRow = memo(({ label, price, volume, maxVolume, type, preClose }) => { + // 计算涨跌颜色 + const getPriceColor = () => { + if (!price || !preClose) return 'white'; + if (price > preClose) return '#EF4444'; + if (price < preClose) return '#22C55E'; + return 'white'; + }; + + // 计算成交量条宽度 + const barWidth = maxVolume > 0 ? (volume / maxVolume) * 100 : 0; + const barColor = type === 'ask' ? 'rgba(34, 197, 94, 0.2)' : 'rgba(239, 68, 68, 0.2)'; + + return ( + + {/* 成交量背景条 */} + + + {/* 档位标签 */} + + {label} + + + {/* 价格 */} + + {formatPrice(price)} + + + {/* 成交量 */} + + {formatVolume(volume)} + + + ); +}); + +/** + * 5档盘口 + * @param {object} props + * @param {array} props.askPrices - 卖盘价格 [卖1, 卖2, 卖3, 卖4, 卖5] + * @param {array} props.askVolumes - 卖盘数量 + * @param {array} props.bidPrices - 买盘价格 [买1, 买2, 买3, 买4, 买5] + * @param {array} props.bidVolumes - 买盘数量 + * @param {number} props.preClose - 昨收价 + */ +const OrderBook = memo(({ + askPrices = [], + askVolumes = [], + bidPrices = [], + bidVolumes = [], + preClose, +}) => { + // 计算最大成交量(用于背景条宽度) + const maxVolume = useMemo(() => { + const allVolumes = [...askVolumes, ...bidVolumes].filter(v => v > 0); + return allVolumes.length > 0 ? Math.max(...allVolumes) : 0; + }, [askVolumes, bidVolumes]); + + // 准备卖盘数据(卖5到卖1,从上到下) + const askRows = useMemo(() => { + const rows = []; + for (let i = 4; i >= 0; i--) { + rows.push({ + label: `卖${i + 1}`, + price: askPrices[i], + volume: askVolumes[i], + }); + } + return rows; + }, [askPrices, askVolumes]); + + // 准备买盘数据(买1到买5,从上到下) + const bidRows = useMemo(() => { + const rows = []; + for (let i = 0; i < 5; i++) { + rows.push({ + label: `买${i + 1}`, + price: bidPrices[i], + volume: bidVolumes[i], + }); + } + return rows; + }, [bidPrices, bidVolumes]); + + // 检查是否有数据 + const hasData = askPrices.some(p => p > 0) || bidPrices.some(p => p > 0); + + if (!hasData) { + return ( + + + 暂无盘口数据 + + + ); + } + + return ( + + {/* 标题栏 */} + + + + + 五档盘口 + + + + 单位: 手 + + + + + + {/* 表头 */} + + 档位 + 价格 + 数量 + + + {/* 卖盘 */} + + {askRows.map((row, i) => ( + + ))} + + + {/* 分隔线 */} + + + {/* 买盘 */} + + {bidRows.map((row, i) => ( + + ))} + + + + ); +}); + +OrderBook.displayName = 'OrderBook'; + +export default OrderBook; diff --git a/MeAgent/src/screens/StockDetail/components/PriceHeader.js b/MeAgent/src/screens/StockDetail/components/PriceHeader.js new file mode 100644 index 00000000..4567b5ba --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/PriceHeader.js @@ -0,0 +1,214 @@ +/** + * 股票价格头部组件 - Wind 风格 + * 显示实时价格、涨跌幅、成交量等关键信息 + */ + +import React, { memo } from 'react'; +import { + Box, + HStack, + VStack, + Text, + Icon, + Pressable, +} from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; + +// 涨跌颜色 +const getChangeColor = (change) => { + if (change > 0) return '#EF4444'; // 红色 + if (change < 0) return '#22C55E'; // 绿色 + return '#94A3B8'; // 灰色 +}; + +// 格式化价格 +const formatPrice = (price) => { + if (price === undefined || price === null) return '--'; + return Number(price).toFixed(2); +}; + +// 格式化涨跌幅 +const formatChange = (change) => { + if (change === undefined || change === null) return '--'; + const sign = change > 0 ? '+' : ''; + return `${sign}${Number(change).toFixed(2)}%`; +}; + +// 格式化涨跌额 +const formatChangeAmount = (amount) => { + if (amount === undefined || amount === null) return '--'; + const sign = amount > 0 ? '+' : ''; + return `${sign}${Number(amount).toFixed(2)}`; +}; + +// 格式化成交量 +const formatVolume = (volume) => { + if (!volume) return '--'; + if (volume >= 100000000) { + return `${(volume / 100000000).toFixed(2)}亿`; + } + if (volume >= 10000) { + return `${(volume / 10000).toFixed(0)}万`; + } + return String(volume); +}; + +// 格式化成交额 +const formatAmount = (amount) => { + if (!amount) return '--'; + if (amount >= 100000000) { + return `${(amount / 100000000).toFixed(2)}亿`; + } + if (amount >= 10000) { + return `${(amount / 10000).toFixed(0)}万`; + } + return String(amount); +}; + +// 格式化市值 +const formatMarketCap = (value) => { + if (!value) return '--'; + if (value >= 100000000) { + return `${(value / 100000000).toFixed(0)}亿`; + } + return formatAmount(value); +}; + +/** + * 指标项组件 + */ +const MetricItem = memo(({ label, value, color = 'white' }) => ( + + + {value} + + + {label} + + +)); + +/** + * 价格头部组件 - Wind 风格 + */ +const PriceHeader = memo(({ + stock, + quote, + isInWatchlist, + onToggleWatchlist, + onBack, +}) => { + const { + stock_code, + stock_name, + } = stock || {}; + + const { + current_price, + price, + change_percent, + change_amount, + volume, + amount, + open, + high, + low, + pre_close, + market_cap, + pe_ratio, + turnover_rate, + } = quote || {}; + + // 使用 current_price 或 price + const displayPrice = current_price || price; + const changeColor = getChangeColor(change_percent); + + return ( + + {/* 顶部导航栏 */} + + + + + + + + {stock_name || '--'} + + + {stock_code || '--'} + + + + + + + + + + + + + + {/* 价格信息区域 - Wind 风格 */} + + {/* 左侧: 主价格 */} + + + {formatPrice(displayPrice)} + + + + {formatChangeAmount(change_amount)} + + + {formatChange(change_percent)} + + + + + {/* 右侧: 指标网格 */} + + + + + + + + + + + pre_close ? '#EF4444' : high < pre_close ? '#22C55E' : 'white'} + /> + pre_close ? '#EF4444' : low < pre_close ? '#22C55E' : 'white'} + /> + + + + + ); +}); + +PriceHeader.displayName = 'PriceHeader'; + +export default PriceHeader; diff --git a/MeAgent/src/screens/StockDetail/components/RelatedInfoTabs.js b/MeAgent/src/screens/StockDetail/components/RelatedInfoTabs.js new file mode 100644 index 00000000..d662d14e --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/RelatedInfoTabs.js @@ -0,0 +1,59 @@ +/** + * 相关信息 Tab 组件 + * 显示相关事件、概念、公告的切换 Tab + */ + +import React, { memo } from 'react'; +import { Box, HStack, Text, Pressable } from 'native-base'; + +// Tab 配置 +const INFO_TABS = [ + { key: 'events', label: '相关事件' }, + { key: 'concepts', label: '相关概念' }, + { key: 'announcements', label: '公司公告' }, +]; + +/** + * 相关信息 Tab 切换 + */ +const RelatedInfoTabs = memo(({ activeTab, onChange }) => { + return ( + + {INFO_TABS.map((tab) => { + const isActive = activeTab === tab.key; + return ( + onChange?.(tab.key)} + flex={1} + > + + + {tab.label} + + + + ); + })} + + ); +}); + +RelatedInfoTabs.displayName = 'RelatedInfoTabs'; + +export default RelatedInfoTabs; diff --git a/MeAgent/src/screens/StockDetail/components/RiseAnalysisModal.js b/MeAgent/src/screens/StockDetail/components/RiseAnalysisModal.js new file mode 100644 index 00000000..9ada505a --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/RiseAnalysisModal.js @@ -0,0 +1,462 @@ +/** + * 涨幅分析弹窗组件 + * 显示股票涨幅超过5%时的异动原因分析 + */ + +import React, { memo, useCallback } from 'react'; +import { + StyleSheet, + ScrollView, + Modal, + TouchableOpacity, + View, + Dimensions, +} from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Icon, +} from 'native-base'; +import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; +import { BlurView } from 'expo-blur'; +import { LinearGradient } from 'expo-linear-gradient'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +const { height: SCREEN_HEIGHT } = Dimensions.get('window'); + +// 格式化日期 +const formatDate = (dateStr) => { + if (!dateStr) return ''; + return dateStr.replace(/-/g, '/'); +}; + +// 格式化涨幅 +const formatRiseRate = (rate) => { + if (rate === undefined || rate === null) return '--'; + const num = Number(rate); + const sign = num > 0 ? '+' : ''; + return `${sign}${num.toFixed(2)}%`; +}; + +// 格式化价格 +const formatPrice = (price) => { + if (price === undefined || price === null) return '--'; + return Number(price).toFixed(2); +}; + +// 格式化成交额 +const formatAmount = (amount) => { + if (!amount) return '--'; + if (amount >= 100000000) { + return `${(amount / 100000000).toFixed(2)}亿`; + } + if (amount >= 10000) { + return `${(amount / 10000).toFixed(0)}万`; + } + return String(Math.round(amount)); +}; + +/** + * 简单的Markdown渲染器 + * 支持标题、加粗、列表等基本格式 + */ +const SimpleMarkdown = memo(({ content }) => { + if (!content) return null; + + // 按行分割并处理 + const lines = content.split('\n'); + const elements = []; + + lines.forEach((line, index) => { + const trimmed = line.trim(); + if (!trimmed) { + elements.push(); + return; + } + + // 标题处理 + if (trimmed.startsWith('### ')) { + elements.push( + + {trimmed.substring(4)} + + ); + } else if (trimmed.startsWith('## ')) { + elements.push( + + {trimmed.substring(3)} + + ); + } else if (trimmed.startsWith('# ')) { + elements.push( + + {trimmed.substring(2)} + + ); + } + // 列表项处理 + else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) { + elements.push( + + + + {renderInlineFormatting(trimmed.substring(2))} + + + ); + } + // 数字列表处理 + else if (/^\d+\.\s/.test(trimmed)) { + const match = trimmed.match(/^(\d+)\.\s(.*)$/); + if (match) { + elements.push( + + {match[1]}. + + {renderInlineFormatting(match[2])} + + + ); + } + } + // 普通段落 + else { + elements.push( + + {renderInlineFormatting(trimmed)} + + ); + } + }); + + return {elements}; +}); + +/** + * 渲染行内格式(加粗等) + */ +const renderInlineFormatting = (text) => { + if (!text) return text; + + // 处理加粗 **text** 或 __text__ + const parts = text.split(/(\*\*[^*]+\*\*|__[^_]+__)/g); + + return parts.map((part, i) => { + if (part.startsWith('**') && part.endsWith('**')) { + return ( + + {part.slice(2, -2)} + + ); + } + if (part.startsWith('__') && part.endsWith('__')) { + return ( + + {part.slice(2, -2)} + + ); + } + return part; + }); +}; + +/** + * 内容区块组件 + */ +const Section = memo(({ title, icon, children, color = '#D4AF37' }) => ( + + + + + + + {title} + + + + {children} + + +)); + +/** + * 涨幅分析弹窗 + */ +const RiseAnalysisModal = memo(({ isOpen, onClose, analysis }) => { + const insets = useSafeAreaInsets(); + + const handleClose = useCallback(() => { + onClose?.(); + }, [onClose]); + + // 解构分析数据(即使 analysis 为 null 也安全) + const { + stock_code = '', + stock_name = '', + trade_date = '', + rise_rate = 0, + close_price = 0, + volume = 0, + amount = 0, + main_business = '', + rise_reason_brief = '', + rise_reason_detail = '', + announcements = '', + update_time = '', + } = analysis || {}; + + const riseRateNum = Number(rise_rate) || 0; + + return ( + + + + + + + + {/* 头部 */} + + + + + + + + 涨幅分析详情 + + + 异动原因解读 + + + + + + + + + {/* 股票信息头部 */} + + + + + + + {stock_name || '--'} + + + + {stock_code || '--'} + + + + + + + 日期: {formatDate(trade_date)} + + + = 0 ? 'rgba(239, 68, 68, 0.2)' : 'rgba(34, 197, 94, 0.2)'} + px={2} + py={1} + borderRadius={6} + > + + = 0 ? 'trending-up' : 'trending-down'} + size="xs" + color={riseRateNum >= 0 ? '#EF4444' : '#22C55E'} + /> + = 0 ? '#EF4444' : '#22C55E'} fontSize={11}> + 涨幅: {formatRiseRate(rise_rate)} + + + + + + 收盘价: ¥{formatPrice(close_price)} + + + + + + + + {/* 内容区域 */} + + {/* 涨幅原因简述 */} + {!!rise_reason_brief && ( +
+ + {rise_reason_brief} + +
+ )} + + {/* 详细分析 */} + {!!rise_reason_detail ? ( +
+ +
+ ) : !rise_reason_brief && ( +
+ + 暂无详细分析数据 + +
+ )} + + {/* 主营业务 */} + {!!main_business && ( +
+ + {main_business} + +
+ )} + + {/* 相关公告 */} + {!!announcements && announcements !== '[]' && ( +
+ +
+ )} + + {/* 交易数据 */} +
+ + + + 成交额 + + + {formatAmount(amount)} + + + + + + 收盘价 + + + ¥{formatPrice(close_price)} + + + + + + 涨幅 + + = 0 ? '#EF4444' : '#22C55E'} + fontSize={15} + fontWeight="medium" + > + {formatRiseRate(rise_rate)} + + + +
+ + {/* 更新时间 */} + {!!update_time && ( + + + 数据更新时间: {update_time} + + + )} + + {/* 底部安全区域 */} + +
+
+
+
+ ); +}); + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + }, + backdrop: { + flex: 1, + }, + modalContainer: { + minHeight: SCREEN_HEIGHT * 0.5, + maxHeight: SCREEN_HEIGHT * 0.9, + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + overflow: 'hidden', + backgroundColor: '#0F0F1A', + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 12, + borderBottomWidth: 1, + borderBottomColor: 'rgba(212, 175, 55, 0.2)', + }, + closeButton: { + backgroundColor: 'rgba(255,255,255,0.1)', + padding: 8, + borderRadius: 20, + }, + stockHeader: { + overflow: 'hidden', + }, + content: { + flex: 1, + }, + contentContainer: { + padding: 16, + }, +}); + +RiseAnalysisModal.displayName = 'RiseAnalysisModal'; + +export default RiseAnalysisModal; diff --git a/MeAgent/src/screens/StockDetail/components/index.js b/MeAgent/src/screens/StockDetail/components/index.js new file mode 100644 index 00000000..cd8f3cd2 --- /dev/null +++ b/MeAgent/src/screens/StockDetail/components/index.js @@ -0,0 +1,14 @@ +/** + * 股票详情组件导出 + */ + +export { default as PriceHeader } from './PriceHeader'; +export { default as ChartTypeTabs } from './ChartTypeTabs'; +export { default as MinuteChart } from './MinuteChart'; +export { default as KlineChart } from './KlineChart'; +export { default as OrderBook } from './OrderBook'; +export { default as RelatedInfoTabs } from './RelatedInfoTabs'; +export { default as EventsPanel } from './EventsPanel'; +export { default as ConceptsPanel } from './ConceptsPanel'; +export { default as AnnouncementsPanel } from './AnnouncementsPanel'; +export { default as RiseAnalysisModal } from './RiseAnalysisModal'; diff --git a/MeAgent/src/screens/StockDetail/index.js b/MeAgent/src/screens/StockDetail/index.js new file mode 100644 index 00000000..164c5cae --- /dev/null +++ b/MeAgent/src/screens/StockDetail/index.js @@ -0,0 +1,6 @@ +/** + * 股票详情模块导出 + */ + +export { default as StockDetailScreen } from './StockDetailScreen'; +export * from './components'; diff --git a/MeAgent/src/screens/Watchlist/AddWatchlistModal.js b/MeAgent/src/screens/Watchlist/AddWatchlistModal.js new file mode 100644 index 00000000..df20d483 --- /dev/null +++ b/MeAgent/src/screens/Watchlist/AddWatchlistModal.js @@ -0,0 +1,334 @@ +/** + * 添加自选股弹窗组件 + * 支持搜索股票并添加到自选 + */ + +import React, { useState, useCallback, useRef } from 'react'; +import { StyleSheet, Keyboard, ActivityIndicator } from 'react-native'; +import { + Modal, + Box, + VStack, + HStack, + Text, + Input, + Icon, + Pressable, + FlatList, + useToast, +} from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; +import { BlurView } from 'expo-blur'; +import { stockDetailService } from '../../services/stockService'; +import { useWatchlist } from '../../hooks/useWatchlist'; + +// 搜索结果项组件 +const SearchResultItem = ({ item, onAdd, isAdding, isInWatchlist }) => { + const { stock_code, stock_name, industry } = item; + const alreadyAdded = isInWatchlist(stock_code); + + return ( + !alreadyAdded && !isAdding && onAdd(item)} + disabled={alreadyAdded || isAdding} + > + {({ pressed }) => ( + + + + {stock_name} + + + + {stock_code} + + {!!industry && ( + + {industry} + + )} + + + + {alreadyAdded ? ( + + + + 已添加 + + + ) : isAdding ? ( + + ) : ( + + + + + 加自选 + + + + )} + + )} + + ); +}; + +// 热门股票配置 +const HOT_STOCKS = [ + { stock_code: '600519', stock_name: '贵州茅台' }, + { stock_code: '000858', stock_name: '五粮液' }, + { stock_code: '300750', stock_name: '宁德时代' }, + { stock_code: '601318', stock_name: '中国平安' }, + { stock_code: '600036', stock_name: '招商银行' }, + { stock_code: '000333', stock_name: '美的集团' }, + { stock_code: '002594', stock_name: '比亚迪' }, + { stock_code: '601012', stock_name: '隆基绿能' }, +]; + +/** + * 添加自选股弹窗 + * @param {object} props + * @param {boolean} props.isOpen - 是否打开 + * @param {function} props.onClose - 关闭回调 + */ +const AddWatchlistModal = ({ isOpen, onClose }) => { + const [searchText, setSearchText] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [addingCode, setAddingCode] = useState(null); + const searchTimeoutRef = useRef(null); + const toast = useToast(); + + const { addStock, isInWatchlist } = useWatchlist({ autoLoad: false }); + + // 搜索股票 + const handleSearch = useCallback(async (text) => { + setSearchText(text); + + // 清除之前的定时器 + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + + if (!text.trim()) { + setSearchResults([]); + return; + } + + // 防抖搜索 + searchTimeoutRef.current = setTimeout(async () => { + setIsSearching(true); + try { + const response = await stockDetailService.searchStocks(text, 20); + if (response.success && Array.isArray(response.data)) { + setSearchResults(response.data); + } else { + setSearchResults([]); + } + } catch (error) { + console.error('搜索股票失败:', error); + setSearchResults([]); + } finally { + setIsSearching(false); + } + }, 300); + }, []); + + // 添加到自选 + const handleAdd = useCallback(async (stock) => { + const { stock_code, stock_name } = stock; + setAddingCode(stock_code); + + try { + const result = await addStock(stock_code, stock_name); + if (result.success) { + toast.show({ + description: `已添加 ${stock_name} 到自选`, + placement: 'top', + duration: 2000, + }); + } else { + toast.show({ + description: result.error || '添加失败', + placement: 'top', + duration: 2000, + }); + } + } catch (error) { + toast.show({ + description: '添加失败,请重试', + placement: 'top', + duration: 2000, + }); + } finally { + setAddingCode(null); + } + }, [addStock, toast]); + + // 关闭弹窗 + const handleClose = useCallback(() => { + Keyboard.dismiss(); + setSearchText(''); + setSearchResults([]); + onClose?.(); + }, [onClose]); + + // 渲染列表数据 + const displayData = searchText.trim() ? searchResults : HOT_STOCKS; + const listTitle = searchText.trim() ? '搜索结果' : '热门股票'; + + return ( + + + + + {/* 头部 */} + + + 添加自选股 + + + + + + + + + {/* 搜索框 */} + + + } + InputRightElement={ + searchText ? ( + handleSearch('')} mr={3}> + + + ) : null + } + /> + + + {/* 列表标题 */} + + + {listTitle} + + {isSearching && ( + + )} + + + {/* 股票列表 */} + item.stock_code} + renderItem={({ item }) => ( + + )} + ListEmptyComponent={ + + + + {searchText ? '未找到相关股票' : '输入关键词搜索'} + + + } + keyboardShouldPersistTaps="handled" + showsVerticalScrollIndicator={false} + /> + + + + ); +}; + +const styles = StyleSheet.create({}); + +export default AddWatchlistModal; diff --git a/MeAgent/src/screens/Watchlist/WatchlistEventItem.js b/MeAgent/src/screens/Watchlist/WatchlistEventItem.js new file mode 100644 index 00000000..8a71e1bf --- /dev/null +++ b/MeAgent/src/screens/Watchlist/WatchlistEventItem.js @@ -0,0 +1,286 @@ +/** + * 自选事件列表项组件 + * 显示单个关注事件的信息 + */ + +import React, { memo } from 'react'; +import { StyleSheet } from 'react-native'; +import { + Box, + HStack, + VStack, + Text, + Icon, + Pressable, + Badge, +} from 'native-base'; +import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; + +// 事件类型配置 +const EVENT_TYPE_CONFIG = { + policy: { icon: 'bank', color: '#3B82F6', label: '政策' }, + industry: { icon: 'factory', color: '#8B5CF6', label: '产业' }, + company: { icon: 'domain', color: '#F59E0B', label: '公司' }, + market: { icon: 'chart-line', color: '#10B981', label: '市场' }, + tech: { icon: 'rocket-launch', color: '#EC4899', label: '科技' }, + default: { icon: 'newspaper', color: '#6B7280', label: '资讯' }, +}; + +// 重要性等级配置 +const IMPORTANCE_CONFIG = { + S: { color: '#EF4444', bgColor: 'rgba(239, 68, 68, 0.2)' }, + A: { color: '#F59E0B', bgColor: 'rgba(245, 158, 11, 0.2)' }, + B: { color: '#3B82F6', bgColor: 'rgba(59, 130, 246, 0.2)' }, + C: { color: '#6B7280', bgColor: 'rgba(107, 114, 128, 0.2)' }, +}; + +// 格式化日期 +const formatDate = (dateStr) => { + if (!dateStr) return ''; + const date = new Date(dateStr); + const now = new Date(); + const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return '今天'; + } else if (diffDays === 1) { + return '昨天'; + } else if (diffDays < 7) { + return `${diffDays}天前`; + } else { + const month = date.getMonth() + 1; + const day = date.getDate(); + return `${month}月${day}日`; + } +}; + +// 格式化热度 +const formatHotScore = (score) => { + if (!score) return ''; + if (score >= 10000) { + return `${(score / 10000).toFixed(1)}万`; + } + return String(score); +}; + +/** + * 自选事件列表项 + * @param {object} props + * @param {object} props.event - 事件信息 + * @param {function} props.onPress - 点击回调 + * @param {function} props.onUnfollow - 取消关注回调 + * @param {boolean} props.isEditing - 是否处于编辑模式 + */ +const WatchlistEventItem = memo(({ + event, + onPress, + onUnfollow, + isEditing = false, +}) => { + const { + id, + title, + event_type, + importance, + hot_score, + publish_time, + related_stocks = [], + follower_count, + } = event; + + const typeConfig = EVENT_TYPE_CONFIG[event_type] || EVENT_TYPE_CONFIG.default; + const importanceConfig = IMPORTANCE_CONFIG[importance] || IMPORTANCE_CONFIG.C; + + return ( + onPress?.(event)} disabled={isEditing}> + {({ pressed }) => ( + + + + + {/* 左侧:删除按钮(编辑模式)或事件类型图标 */} + {isEditing ? ( + onUnfollow?.(id)} + hitSlop={10} + > + + + + + ) : ( + + + + )} + + {/* 中间:事件信息 */} + + {/* 标题行 */} + + {!!importance && ( + + {importance} + + )} + + {title} + + + + {/* 相关股票 */} + {related_stocks.length > 0 && ( + + {related_stocks.slice(0, 3).map((stock, index) => ( + + + {stock.stock_name || stock.name || stock} + + + ))} + {related_stocks.length > 3 && ( + + + +{related_stocks.length - 3} + + + )} + + )} + + {/* 底部信息行 */} + + {/* 事件类型标签 */} + + + + {typeConfig.label} + + + + {/* 热度 */} + {hot_score > 0 && ( + + + + {formatHotScore(hot_score)} + + + )} + + {/* 关注人数 */} + {follower_count > 0 && ( + + + + {follower_count}人关注 + + + )} + + {/* 时间 */} + + {formatDate(publish_time)} + + + + + {/* 最右侧:箭头(非编辑模式) */} + {!isEditing && ( + + )} + + + + )} + + ); +}); + +WatchlistEventItem.displayName = 'WatchlistEventItem'; + +export default WatchlistEventItem; diff --git a/MeAgent/src/screens/Watchlist/WatchlistScreen.js b/MeAgent/src/screens/Watchlist/WatchlistScreen.js new file mode 100644 index 00000000..c8491b87 --- /dev/null +++ b/MeAgent/src/screens/Watchlist/WatchlistScreen.js @@ -0,0 +1,432 @@ +/** + * 自选股/自选事件主页面 + * 包含股票和事件两个 Tab + */ + +import React, { useState, useCallback, useEffect } from 'react'; +import { StyleSheet, RefreshControl } from 'react-native'; +import { + Box, + VStack, + HStack, + Text, + Icon, + Pressable, + FlatList, + Spinner, + useToast, +} from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; +import { BlurView } from 'expo-blur'; +import { useNavigation, useFocusEffect } from '@react-navigation/native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import WatchlistStockItem from './WatchlistStockItem'; +import WatchlistEventItem from './WatchlistEventItem'; +import AddWatchlistModal from './AddWatchlistModal'; +import { useWatchlist } from '../../hooks/useWatchlist'; +import { useWatchlistRealtimeQuotes } from '../../hooks/useRealtimeQuote'; + +// Tab 配置 +const TABS = [ + { key: 'stocks', label: '自选股', icon: 'trending-up' }, + { key: 'events', label: '自选事件', icon: 'newspaper-outline' }, +]; + +/** + * 自选股主页面 + */ +const WatchlistScreen = () => { + const navigation = useNavigation(); + const toast = useToast(); + + const [activeTab, setActiveTab] = useState('stocks'); + const [isEditing, setIsEditing] = useState(false); + const [isAddModalOpen, setIsAddModalOpen] = useState(false); + const [refreshing, setRefreshing] = useState(false); + + const { + stocks, + stocksWithQuotes, + events, + isLoadingStocks, + isLoadingEvents, + removeStock, + toggleEventFollow, + refreshAll, + loadWatchlist, + loadRealtimeQuotes, + loadFollowingEvents, + } = useWatchlist(); + + // WebSocket 实时行情 + const { + isConnected: wsConnected, + connectionState, + updateSubscription, + } = useWatchlistRealtimeQuotes(); + + // 页面获取焦点时刷新数据 + useFocusEffect( + useCallback(() => { + loadWatchlist(); + loadRealtimeQuotes(); + loadFollowingEvents(); + }, [loadWatchlist, loadRealtimeQuotes, loadFollowingEvents]) + ); + + // 当股票列表变化时更新 WebSocket 订阅 + useEffect(() => { + if (stocks.length > 0) { + updateSubscription(stocks); + } + }, [stocks, updateSubscription]); + + // 下拉刷新 + const handleRefresh = useCallback(async () => { + setRefreshing(true); + await refreshAll(); + setRefreshing(false); + }, [refreshAll]); + + // 点击股票 + const handleStockPress = useCallback((stock) => { + navigation.navigate('StockDetail', { + stockCode: stock.stock_code, + stockName: stock.stock_name, + }); + }, [navigation]); + + // 点击事件 + const handleEventPress = useCallback((event) => { + navigation.navigate('EventDetail', { + eventId: event.id, + }); + }, [navigation]); + + // 删除自选股 + const handleRemoveStock = useCallback(async (stockCode) => { + const result = await removeStock(stockCode); + if (result.success) { + toast.show({ + description: '已从自选中移除', + placement: 'top', + duration: 2000, + }); + } else { + toast.show({ + description: '移除失败,请重试', + placement: 'top', + duration: 2000, + }); + } + }, [removeStock, toast]); + + // 取消关注事件 + const handleUnfollowEvent = useCallback(async (eventId) => { + const result = await toggleEventFollow(eventId); + if (result.success) { + toast.show({ + description: '已取消关注', + placement: 'top', + duration: 2000, + }); + } else { + toast.show({ + description: '操作失败,请重试', + placement: 'top', + duration: 2000, + }); + } + }, [toggleEventFollow, toast]); + + // 获取连接状态显示 + const getConnectionStatus = () => { + if (wsConnected) { + return { color: '#22C55E', text: '实时' }; + } + if (connectionState === 'connecting' || connectionState === 'reconnecting') { + return { color: '#F59E0B', text: '连接中' }; + } + return { color: '#6B7280', text: '离线' }; + }; + + // 渲染头部 + const renderHeader = () => { + const connStatus = getConnectionStatus(); + return ( + + {/* 顶部栏 */} + + + + 我的自选 + + {/* WebSocket 连接状态指示器 */} + {activeTab === 'stocks' && stocks.length > 0 && ( + + + + {connStatus.text} + + + )} + + + {/* 编辑按钮 */} + setIsEditing(!isEditing)}> + + + + + {/* 添加按钮 */} + {activeTab === 'stocks' && ( + setIsAddModalOpen(true)}> + + + + + )} + + + + {/* Tab 切换 */} + + {TABS.map((tab) => { + const isActive = activeTab === tab.key; + return ( + { + setActiveTab(tab.key); + setIsEditing(false); + }} + flex={1} + > + + + + + {tab.label} + + + + {tab.key === 'stocks' ? stocksWithQuotes.length : events.length} + + + + + + ); + })} + + + ); + }; + + // 渲染空状态 + const renderEmptyState = () => { + const isStocks = activeTab === 'stocks'; + return ( + + + + + + {isStocks ? '还没有自选股' : '还没有关注事件'} + + + {isStocks + ? '点击右上角添加按钮,搜索并添加你感兴趣的股票' + : '在事件中心浏览事件,点击关注按钮添加到这里'} + + {isStocks && ( + setIsAddModalOpen(true)} mt={6}> + + + + + 添加自选股 + + + + + )} + + ); + }; + + // 渲染股票列表项 + const renderStockItem = ({ item }) => ( + + ); + + // 渲染事件列表项 + const renderEventItem = ({ item }) => ( + + ); + + // 当前显示的数据 + const currentData = activeTab === 'stocks' ? stocksWithQuotes : events; + const isLoading = activeTab === 'stocks' ? isLoadingStocks : isLoadingEvents; + + return ( + + + {/* 背景渐变 */} + + + {renderHeader()} + + {/* 加载状态 */} + {isLoading && currentData.length === 0 ? ( + + + + 加载中... + + + ) : ( + + activeTab === 'stocks' ? item.stock_code : String(item.id) + } + renderItem={activeTab === 'stocks' ? renderStockItem : renderEventItem} + ListEmptyComponent={renderEmptyState} + refreshControl={ + + } + contentContainerStyle={ + currentData.length === 0 ? styles.emptyContainer : styles.listContainer + } + showsVerticalScrollIndicator={false} + /> + )} + + {/* 添加自选股弹窗 */} + setIsAddModalOpen(false)} + /> + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + headerGradient: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: 200, + }, + listContainer: { + paddingBottom: 100, + }, + emptyContainer: { + flex: 1, + }, +}); + +export default WatchlistScreen; diff --git a/MeAgent/src/screens/Watchlist/WatchlistStockItem.js b/MeAgent/src/screens/Watchlist/WatchlistStockItem.js new file mode 100644 index 00000000..5101f53d --- /dev/null +++ b/MeAgent/src/screens/Watchlist/WatchlistStockItem.js @@ -0,0 +1,225 @@ +/** + * 自选股列表项组件 + * 显示单只股票的行情信息 + */ + +import React, { memo } from 'react'; +import { StyleSheet } from 'react-native'; +import { + Box, + HStack, + VStack, + Text, + Icon, + Pressable, +} from 'native-base'; +import { Ionicons } from '@expo/vector-icons'; +import { LinearGradient } from 'expo-linear-gradient'; + +// 涨跌颜色 +const getChangeColor = (change) => { + if (change > 0) return '#EF4444'; // 红色 + if (change < 0) return '#22C55E'; // 绿色 + return '#94A3B8'; // 灰色 +}; + +// 背景渐变色 +const getGradientColors = (change) => { + if (change > 0) return ['rgba(239, 68, 68, 0.08)', 'rgba(239, 68, 68, 0.02)']; + if (change < 0) return ['rgba(34, 197, 94, 0.08)', 'rgba(34, 197, 94, 0.02)']; + return ['rgba(148, 163, 184, 0.08)', 'rgba(148, 163, 184, 0.02)']; +}; + +// 格式化价格 +const formatPrice = (price) => { + if (price === undefined || price === null) return '--'; + return Number(price).toFixed(2); +}; + +// 格式化涨跌幅 +const formatChange = (change) => { + if (change === undefined || change === null) return '--'; + const sign = change > 0 ? '+' : ''; + return `${sign}${Number(change).toFixed(2)}%`; +}; + +// 格式化成交量 +const formatVolume = (volume) => { + if (!volume) return '--'; + if (volume >= 100000000) { + return `${(volume / 100000000).toFixed(2)}亿`; + } + if (volume >= 10000) { + return `${(volume / 10000).toFixed(0)}万`; + } + return String(volume); +}; + +/** + * 自选股列表项 + * @param {object} props + * @param {object} props.stock - 股票信息 { stock_code, stock_name } + * @param {object} props.quote - 实时行情 { current_price, change_percent, volume, ... } + * @param {function} props.onPress - 点击回调 + * @param {function} props.onRemove - 删除回调 + * @param {boolean} props.isEditing - 是否处于编辑模式 + */ +const WatchlistStockItem = memo(({ + stock, + quote, + onPress, + onRemove, + isEditing = false, +}) => { + const { stock_code, stock_name } = stock; + const { + current_price, + change_percent, + volume, + high, + low, + } = quote || {}; + + const changeColor = getChangeColor(change_percent); + const gradientColors = getGradientColors(change_percent); + + return ( + onPress?.(stock)} disabled={isEditing}> + {({ pressed }) => ( + + + + + {/* 左侧:删除按钮(编辑模式)或涨跌指示条 */} + {isEditing ? ( + onRemove?.(stock_code)} + hitSlop={10} + mr={3} + > + + + + + ) : ( + + )} + + {/* 中间:股票信息 */} + + + {stock_name || stock_code} + + + + {stock_code} + + {volume > 0 && ( + + 成交 {formatVolume(volume)} + + )} + + + + {/* 右侧:价格和涨跌幅 */} + + + {formatPrice(current_price)} + + + {change_percent !== undefined && change_percent !== 0 && ( + 0 ? 'caret-up' : 'caret-down'} + size="xs" + color={changeColor} + /> + )} + + {formatChange(change_percent)} + + + + + {/* 最右侧:箭头(非编辑模式) */} + {!isEditing && ( + + )} + + + {/* 额外信息行:最高/最低价 */} + {(high > 0 || low > 0) && !isEditing && ( + + + 最高 + {formatPrice(high)} + + + 最低 + {formatPrice(low)} + + + )} + + + )} + + ); +}); + +WatchlistStockItem.displayName = 'WatchlistStockItem'; + +export default WatchlistStockItem; diff --git a/MeAgent/src/screens/Watchlist/index.js b/MeAgent/src/screens/Watchlist/index.js new file mode 100644 index 00000000..dd8e361a --- /dev/null +++ b/MeAgent/src/screens/Watchlist/index.js @@ -0,0 +1,8 @@ +/** + * 自选股模块导出 + */ + +export { default as WatchlistScreen } from './WatchlistScreen'; +export { default as WatchlistStockItem } from './WatchlistStockItem'; +export { default as WatchlistEventItem } from './WatchlistEventItem'; +export { default as AddWatchlistModal } from './AddWatchlistModal'; diff --git a/argon-pro-react-native/src/services/api.js b/MeAgent/src/services/api.js similarity index 96% rename from argon-pro-react-native/src/services/api.js rename to MeAgent/src/services/api.js index 49fc9ab9..aa1ffc31 100644 --- a/argon-pro-react-native/src/services/api.js +++ b/MeAgent/src/services/api.js @@ -3,7 +3,7 @@ * 复用 Web 端的 API 逻辑,适配 React Native */ -const API_BASE_URL = 'https://api.valuefrontier.cn'; +export const API_BASE_URL = 'https://api.valuefrontier.cn'; // 静态数据存储地址(腾讯 COS) export const API_BASE = 'https://valuefrontier-1308417363.cos-website.ap-shanghai.myqcloud.com'; diff --git a/argon-pro-react-native/src/services/authService.js b/MeAgent/src/services/authService.js similarity index 100% rename from argon-pro-react-native/src/services/authService.js rename to MeAgent/src/services/authService.js diff --git a/MeAgent/src/services/companyService.js b/MeAgent/src/services/companyService.js new file mode 100644 index 00000000..3766b31b --- /dev/null +++ b/MeAgent/src/services/companyService.js @@ -0,0 +1,158 @@ +/** + * 公司/股票相关信息服务 + * 获取股票相关事件、概念、公告等 + */ + +import { apiRequest, API_BASE_URL } from './api'; + +// Concept API 基础地址 +const CONCEPT_API_BASE = `${API_BASE_URL}/concept-api`; + +/** + * 公司信息服务 + */ +export const companyService = { + /** + * 获取股票相关事件 + * @param {string} stockName - 股票名称 + * @param {number} page - 页码 + * @param {number} perPage - 每页条数 + * @returns {Promise} + */ + getRelatedEvents: async (stockName, page = 1, perPage = 10) => { + try { + if (!stockName) { + return { success: false, data: [], error: '缺少股票名称' }; + } + + console.log('[CompanyService] 获取相关事件:', { stockName, page }); + + const url = `/api/events?sort=new&importance=all&q=${encodeURIComponent(stockName)}&page=${page}&mode=vertical&per_page=${perPage}`; + const response = await apiRequest(url); + + if (response.success && response.data) { + return { + success: true, + data: response.data.events || [], + pagination: response.pagination, + }; + } + + return { success: false, data: [], error: '获取事件失败' }; + } catch (error) { + console.error('[CompanyService] getRelatedEvents 错误:', error); + return { success: false, data: [], error: error.message }; + } + }, + + /** + * 获取股票相关概念 + * @param {string} stockCode - 股票代码(6位数字) + * @returns {Promise} + */ + getRelatedConcepts: async (stockCode) => { + try { + if (!stockCode) { + return { success: false, data: [], error: '缺少股票代码' }; + } + + // 提取6位数字代码 + const pureCode = String(stockCode).match(/\d{6}/)?.[0]; + if (!pureCode) { + return { success: false, data: [], error: '无效的股票代码' }; + } + + console.log('[CompanyService] 获取相关概念:', pureCode); + + const url = `${CONCEPT_API_BASE}/stock/${pureCode}/concepts?size=50`; + const response = await fetch(url); + const data = await response.json(); + + if (data && data.concepts) { + return { + success: true, + data: data.concepts, + }; + } + + return { success: false, data: [], error: '获取概念失败' }; + } catch (error) { + console.error('[CompanyService] getRelatedConcepts 错误:', error); + return { success: false, data: [], error: error.message }; + } + }, + + /** + * 获取股票公告 + * @param {string} stockCode - 股票代码 + * @param {number} limit - 条数限制 + * @returns {Promise} + */ + getAnnouncements: async (stockCode, limit = 20) => { + try { + if (!stockCode) { + return { success: false, data: [], error: '缺少股票代码' }; + } + + // 提取6位数字代码 + const pureCode = String(stockCode).match(/\d{6}/)?.[0]; + if (!pureCode) { + return { success: false, data: [], error: '无效的股票代码' }; + } + + console.log('[CompanyService] 获取公告:', pureCode); + + const url = `/api/stock/${pureCode}/announcements?limit=${limit}`; + const response = await apiRequest(url); + + if (response.success && response.data) { + return { + success: true, + data: response.data, + }; + } + + return { success: false, data: [], error: '获取公告失败' }; + } catch (error) { + console.error('[CompanyService] getAnnouncements 错误:', error); + return { success: false, data: [], error: error.message }; + } + }, + + /** + * 获取股票基本信息 + * @param {string} stockCode - 股票代码 + * @returns {Promise} + */ + getStockInfo: async (stockCode) => { + try { + if (!stockCode) { + return { success: false, data: null, error: '缺少股票代码' }; + } + + const pureCode = String(stockCode).match(/\d{6}/)?.[0]; + if (!pureCode) { + return { success: false, data: null, error: '无效的股票代码' }; + } + + console.log('[CompanyService] 获取股票信息:', pureCode); + + const url = `/api/financial/stock-info/${pureCode}`; + const response = await apiRequest(url); + + if (response.success && response.data) { + return { + success: true, + data: response.data, + }; + } + + return { success: false, data: null, error: '获取股票信息失败' }; + } catch (error) { + console.error('[CompanyService] getStockInfo 错误:', error); + return { success: false, data: null, error: error.message }; + } + }, +}; + +export default companyService; diff --git a/argon-pro-react-native/src/services/eventService.js b/MeAgent/src/services/eventService.js similarity index 100% rename from argon-pro-react-native/src/services/eventService.js rename to MeAgent/src/services/eventService.js diff --git a/argon-pro-react-native/src/services/pushService.js b/MeAgent/src/services/pushService.js similarity index 100% rename from argon-pro-react-native/src/services/pushService.js rename to MeAgent/src/services/pushService.js diff --git a/MeAgent/src/services/stockService.js b/MeAgent/src/services/stockService.js new file mode 100644 index 00000000..22527824 --- /dev/null +++ b/MeAgent/src/services/stockService.js @@ -0,0 +1,537 @@ +/** + * 股票详情服务层 + * 获取股票详情、K线数据、分时数据等 + */ + +import { apiRequest, API_BASE_URL } from './api'; + +/** + * 股票详情服务 + */ +export const stockDetailService = { + /** + * 批量获取股票报价(含5档盘口)- 使用 flex-screen API 获取完整数据 + * @param {string[]} codes - 股票代码数组 + * @param {string} eventTime - 可选的事件时间 + * @returns {Promise} { [code]: { name, price, change, bidPrices, askPrices, ... } } + */ + getQuotes: async (codes, eventTime = null) => { + try { + // 确保所有股票代码都带有交易所后缀 + const formattedCodes = codes.map(code => stockDetailService.formatStockCode(code)); + + const requestBody = { + codes: formattedCodes, + include_order_book: true, // 请求5档盘口数据 + }; + if (eventTime) { + requestBody.event_time = eventTime; + } + + console.log('[StockDetailService] 获取股票报价:', formattedCodes.slice(0, 5)); + + // 使用 flex-screen API 获取更完整的行情数据(与Web端一致) + const response = await apiRequest('/api/flex-screen/quotes', { + method: 'POST', + body: JSON.stringify(requestBody), + }); + + if (response.success && response.data) { + console.log('[StockDetailService] 报价响应成功:', Object.keys(response.data).length, '只股票'); + // 转换字段名以兼容现有组件 + const normalizedData = {}; + Object.entries(response.data).forEach(([code, quote]) => { + // 调试日志:打印原始 API 响应 + console.log('[StockDetailService] 原始报价数据:', code, { + last_px: quote.last_px, + prev_close_px: quote.prev_close_px, + change: quote.change, + change_pct: quote.change_pct, + }); + + normalizedData[code] = { + name: quote.name, + price: quote.last_px, + current_price: quote.last_px, + pre_close: quote.prev_close_px, + open: quote.open_px, + high: quote.high_px, + low: quote.low_px, + volume: quote.total_volume_trade, + amount: quote.total_value_trade, + change: quote.change, + change_percent: quote.change_pct, + change_amount: quote.change, + bid_prices: quote.bid_prices || [], + bid_volumes: quote.bid_volumes || [], + ask_prices: quote.ask_prices || [], + ask_volumes: quote.ask_volumes || [], + update_time: quote.update_time, + }; + }); + return { success: true, data: normalizedData }; + } else { + console.warn('[StockDetailService] 报价响应格式异常:', response); + return { success: false, data: {} }; + } + } catch (error) { + console.error('[StockDetailService] getQuotes 错误:', error); + return { success: false, error: error.message }; + } + }, + + /** + * 获取单只股票详情 + * 使用 /api/stock/{code}/quote-detail API 获取完整行情数据 + * @param {string} code - 股票代码 + * @returns {Promise} 股票详情 + */ + getStockDetail: async (code) => { + try { + const pureCode = stockDetailService.getPureCode(code); + const formattedCode = stockDetailService.formatStockCode(code); + console.log('[StockDetailService] 获取股票详情:', { code, pureCode, formattedCode }); + + // 并行请求 quote-detail 和 market/trade(用于获取成交量成交额) + const [quoteResponse, tradeResponse] = await Promise.all([ + apiRequest(`/api/stock/${pureCode}/quote-detail`), + apiRequest(`/api/market/trade/${pureCode}?limit=60`), + ]); + + // 从 market/trade 获取最新一天的成交量成交额 + let latestTrade = null; + if (tradeResponse.success && tradeResponse.data && tradeResponse.data.length > 0) { + latestTrade = tradeResponse.data[tradeResponse.data.length - 1]; + } + + if (quoteResponse.success && quoteResponse.data) { + const quote = quoteResponse.data; + + console.log('[StockDetailService] quote-detail 响应:', { + current_price: quote.current_price, + yesterday_close: quote.yesterday_close, + change_percent: quote.change_percent, + turnover_rate: quote.turnover_rate, + }); + + const normalizedData = { + stock_code: formattedCode, + stock_name: quote.name || '', + // 价格字段 + current_price: quote.current_price || 0, + price: quote.current_price || 0, + pre_close: quote.yesterday_close || 0, + open: quote.today_open || 0, + high: quote.today_high || 0, + low: quote.today_low || 0, + // 涨跌 + change_amount: (quote.current_price - quote.yesterday_close) || 0, + change_percent: quote.change_percent || 0, + // 成交量/成交额(从 market/trade 获取) + volume: latestTrade?.volume || 0, + amount: latestTrade?.amount || 0, + // 换手率/市盈率/市值 + turnover_rate: quote.turnover_rate || latestTrade?.turnover_rate || 0, + pe_ratio: quote.pe || 0, + market_cap: quote.market_cap || quote.circ_mv || 0, + // 52周高低 + week52_high: quote.week52_high || 0, + week52_low: quote.week52_low || 0, + // 资金流向 + net_inflow: quote.net_inflow || 0, + main_inflow_ratio: quote.main_inflow_ratio || 0, + net_active_buy_ratio: quote.net_active_buy_ratio || 0, + // 行业信息 + industry: quote.industry || quote.sw_industry_l2 || '', + sw_industry_l1: quote.sw_industry_l1 || '', + // 5档盘口(quote-detail 不提供,置空) + bidPrices: [], + bidVolumes: [], + askPrices: [], + askVolumes: [], + // 更新时间 + update_time: quote.update_time, + }; + + return { success: true, data: normalizedData }; + } + + // 如果 quote-detail 失败,只使用 market/trade 数据 + if (latestTrade) { + console.log('[StockDetailService] quote-detail 无数据,使用 market/trade'); + const normalizedData = { + stock_code: formattedCode, + stock_name: latestTrade.stock_name || '', + current_price: latestTrade.close || 0, + price: latestTrade.close || 0, + pre_close: latestTrade.pre_close || 0, + open: latestTrade.open || 0, + high: latestTrade.high || 0, + low: latestTrade.low || 0, + change_amount: (latestTrade.close - latestTrade.pre_close) || 0, + change_percent: latestTrade.change_percent || 0, + volume: latestTrade.volume || 0, + amount: latestTrade.amount || 0, + turnover_rate: latestTrade.turnover_rate || 0, + pe_ratio: latestTrade.pe_ratio || 0, + bidPrices: [], + bidVolumes: [], + askPrices: [], + askVolumes: [], + trade_date: latestTrade.date, + }; + return { success: true, data: normalizedData }; + } + + return { success: false, error: '获取股票信息失败' }; + } catch (error) { + console.error('[StockDetailService] getStockDetail 错误:', error); + return { success: false, error: error.message }; + } + }, + + /** + * 获取K线数据 + * @param {string} code - 股票代码 + * @param {string} type - K线类型: minute | daily | weekly | monthly + * @param {string} eventTime - 可选的事件时间 + * @returns {Promise} { success: true, data: [...klineData] } + */ + getKlineData: async (code, type = 'daily', eventTime = null) => { + try { + // 分时数据需要带后缀(ClickHouse 存储格式),日/周/月K线用纯数字 + const isMinuteType = type === 'minute' || type === 'timeline'; + const apiCode = isMinuteType + ? stockDetailService.formatStockCode(code) // 带后缀:000001.SZ + : stockDetailService.getPureCode(code); // 纯数字:000001 + + let url = `/api/stock/${apiCode}/kline?type=${type}`; + if (eventTime) { + url += `&event_time=${eventTime}`; + } + + console.log('[StockDetailService] 获取K线数据:', { code, apiCode, type, url }); + + const response = await apiRequest(url); + + // API 返回格式: { code, name, data, trade_date, type, prev_close }(不是 {success, data} 格式) + if (response && response.data) { + console.log('[StockDetailService] K线数据点数:', response.data?.length || 0); + // 标准化数据字段名称 + const normalizedData = stockDetailService.normalizeKlineData(response.data, type); + return { + success: true, + data: normalizedData, + stockName: response.name, + tradeDate: response.trade_date, + prevClose: response.prev_close, // timeline 类型返回昨收价 + }; + } + + // 处理错误响应 + if (response && response.error) { + return { success: false, data: [], error: response.error }; + } + + return { success: false, data: [], error: '获取K线数据失败' }; + } catch (error) { + console.error('[StockDetailService] getKlineData 错误:', error); + return { success: false, data: [], error: error.message }; + } + }, + + /** + * 获取分时数据(使用 latest-minute API 获取最新交易日数据) + * 同时获取昨收价用于涨跌幅计算 + * @param {string} code - 股票代码 + * @returns {Promise} { success: true, data: [...minuteData], prevClose: number } + */ + getMinuteData: async (code) => { + try { + const pureCode = stockDetailService.getPureCode(code); + + console.log('[StockDetailService] 获取分时数据:', { code, pureCode }); + + // 并行获取分时数据和昨收价 + // 注意:market/trade API 返回数据按日期升序排列,最新数据在数组末尾 + const [minuteResponse, tradeResponse] = await Promise.all([ + apiRequest(`/api/stock/${pureCode}/latest-minute`), + apiRequest(`/api/market/trade/${pureCode}?limit=60`), // 获取最近60天交易数据 + ]); + + // 从 market/trade 获取昨收价(取最新一天的 pre_close) + let prevClose = 0; + if (tradeResponse.success && tradeResponse.data && tradeResponse.data.length > 0) { + // 取数组最后一条(最新的交易数据),其 pre_close 就是昨收价 + const latestTrade = tradeResponse.data[tradeResponse.data.length - 1]; + prevClose = latestTrade.pre_close || 0; + console.log('[StockDetailService] 昨收价:', prevClose, '日期:', latestTrade.date); + } + + if (minuteResponse && minuteResponse.data) { + console.log('[StockDetailService] 分时数据点数:', minuteResponse.data?.length || 0); + + // 转换分钟K线数据为分时格式,并计算累计均价 + let totalAmount = 0; + let totalVolume = 0; + + const minuteData = minuteResponse.data.map(item => { + const price = item.close || 0; + const volume = item.volume || 0; + const amount = item.amount || (price * volume); + + totalAmount += amount; + totalVolume += volume; + const avgPrice = totalVolume > 0 ? totalAmount / totalVolume : price; + + return { + time: item.time || '', + price: price, + avg_price: avgPrice, + volume: volume, + open: item.open || 0, + high: item.high || 0, + low: item.low || 0, + close: item.close || 0, + prev_close: prevClose, // 每条数据都带上昨收价 + }; + }); + + return { + success: true, + data: minuteData, + stockName: minuteResponse.name, + tradeDate: minuteResponse.trade_date, + prevClose: prevClose, // 返回昨收价 + }; + } + + if (minuteResponse && minuteResponse.error) { + return { success: false, data: [], error: minuteResponse.error }; + } + + return { success: false, data: [], error: '获取分时数据失败' }; + } catch (error) { + console.error('[StockDetailService] getMinuteData 错误:', error); + return { success: false, data: [], error: error.message }; + } + }, + + /** + * 获取日K线数据 + * @param {string} code - 股票代码 + * @param {string} eventTime - 可选的事件时间 + * @returns {Promise} { success: true, data: [...klineData] } + */ + getDailyKline: async (code, eventTime = null) => { + return stockDetailService.getKlineData(code, 'daily', eventTime); + }, + + /** + * 获取周K线数据 + * @param {string} code - 股票代码 + * @returns {Promise} { success: true, data: [...klineData] } + */ + getWeeklyKline: async (code) => { + return stockDetailService.getKlineData(code, 'weekly'); + }, + + /** + * 获取月K线数据 + * @param {string} code - 股票代码 + * @returns {Promise} { success: true, data: [...klineData] } + */ + getMonthlyKline: async (code) => { + return stockDetailService.getKlineData(code, 'monthly'); + }, + + /** + * 搜索股票 + * @param {string} keyword - 搜索关键词(代码或名称) + * @returns {Promise} { success: true, data: [...stocks] } + */ + searchStocks: async (keyword) => { + try { + if (!keyword || keyword.trim().length < 1) { + return { success: true, data: [] }; + } + + console.log('[StockDetailService] 搜索股票:', keyword); + + const response = await apiRequest(`/api/stock/search?q=${encodeURIComponent(keyword)}`); + + if (response.success) { + console.log('[StockDetailService] 搜索结果:', response.data?.length || 0); + } + + return response; + } catch (error) { + console.error('[StockDetailService] searchStocks 错误:', error); + return { success: false, data: [], error: error.message }; + } + }, + + /** + * 格式化股票代码 + * @param {string} code - 原始股票代码 + * @returns {string} 格式化后的代码(如 000001.SZ) + */ + formatStockCode: (code) => { + const pureCode = String(code).match(/\d{6}/)?.[0]; + if (!pureCode) return code; + + // 根据首位判断交易所 + const firstDigit = pureCode[0]; + if (['6', '5'].includes(firstDigit)) { + return `${pureCode}.SH`; // 上交所 + } else if (['0', '3', '1', '2'].includes(firstDigit)) { + return `${pureCode}.SZ`; // 深交所 + } else if (['8', '4'].includes(firstDigit)) { + return `${pureCode}.BJ`; // 北交所 + } + + return pureCode; + }, + + /** + * 获取纯股票代码(不含交易所后缀) + * @param {string} code - 股票代码 + * @returns {string} 6位数字代码 + */ + getPureCode: (code) => { + const match = String(code).match(/\d{6}/); + return match ? match[0] : code; + }, + + /** + * 获取交易所标识 + * @param {string} code - 股票代码 + * @returns {string} SH | SZ | BJ + */ + getExchange: (code) => { + const pureCode = stockDetailService.getPureCode(code); + const firstDigit = pureCode[0]; + + if (['6', '5'].includes(firstDigit)) return 'SH'; + if (['0', '3', '1', '2'].includes(firstDigit)) return 'SZ'; + if (['8', '4'].includes(firstDigit)) return 'BJ'; + + return 'SH'; + }, + + /** + * 判断是否为指数 + * @param {string} code - 代码 + * @returns {boolean} + */ + isIndex: (code) => { + const pureCode = stockDetailService.getPureCode(code); + // 常见指数代码 + const indexCodes = ['000001', '399001', '399006', '000300', '000016', '000905']; + return indexCodes.includes(pureCode) || pureCode.startsWith('88'); + }, + + /** + * 获取涨幅分析数据 + * 返回涨幅超过5%的日期的异动分析 + * @param {string} code - 股票代码 + * @param {string} startDate - 开始日期(可选) + * @param {string} endDate - 结束日期(可选) + * @returns {Promise} { success: true, data: [...analysisData] } + */ + getRiseAnalysis: async (code, startDate = null, endDate = null) => { + try { + const pureCode = stockDetailService.getPureCode(code); + console.log('[StockDetailService] 获取涨幅分析:', { code, pureCode }); + + let url = `/api/market/rise-analysis/${pureCode}`; + const params = []; + if (startDate) params.push(`start_date=${startDate}`); + if (endDate) params.push(`end_date=${endDate}`); + if (params.length > 0) url += `?${params.join('&')}`; + + const response = await apiRequest(url); + + if (response.success && response.data) { + console.log('[StockDetailService] 涨幅分析数据:', response.data.length, '条'); + return { + success: true, + data: response.data.map(item => ({ + stock_code: item.stock_code, + stock_name: item.stock_name, + trade_date: item.trade_date, + rise_rate: item.rise_rate || item.change_pct, + close_price: item.close_price || item.close, + volume: item.volume, + amount: item.amount, + main_business: item.main_business, + rise_reason_brief: item.rise_reason_brief, + rise_reason_detail: item.rise_reason_detail, + announcements: item.announcements, + update_time: item.update_time, + })), + }; + } + + return { success: false, data: [], error: response.error || '获取涨幅分析失败' }; + } catch (error) { + console.error('[StockDetailService] getRiseAnalysis 错误:', error); + return { success: false, data: [], error: error.message }; + } + }, + + /** + * 标准化K线数据字段名称 + * 将API返回的不同字段名统一为组件期望的格式 + * @param {Array} data - 原始数据 + * @param {string} type - 数据类型 minute | timeline | daily | weekly | monthly + * @returns {Array} 标准化后的数据 + */ + normalizeKlineData: (data, type) => { + if (!data || !Array.isArray(data)) { + return []; + } + + // timeline 类型:分时走势数据(包含均价线) + if (type === 'timeline') { + return data.map(item => ({ + time: item.time || '', + price: item.price ?? 0, + avg_price: item.avg_price ?? 0, + volume: item.volume ?? 0, + change_percent: item.change_percent ?? 0, + })); + } + + // minute 类型:分钟K线数据 + if (type === 'minute') { + // 分时数据标准化 + return data.map(item => ({ + time: item.time || '', + price: item.price ?? item.close ?? item.current_price ?? 0, + avg_price: item.avg_price ?? item.average_price ?? 0, + volume: item.volume ?? item.vol ?? 0, + high: item.high ?? item.high_price ?? 0, + low: item.low ?? item.low_price ?? 0, + open: item.open ?? item.open_price ?? 0, + close: item.close ?? item.close_price ?? 0, + prev_close: item.prev_close ?? item.pre_close ?? 0, + change_percent: item.change_percent ?? item.changePct ?? 0, + })); + } + + // K线数据标准化(日/周/月) + return data.map(item => ({ + date: item.date || item.trade_date || item.time || '', + open: item.open ?? item.open_price ?? 0, + close: item.close ?? item.close_price ?? 0, + high: item.high ?? item.high_price ?? 0, + low: item.low ?? item.low_price ?? 0, + volume: item.volume ?? item.vol ?? 0, + prev_close: item.prev_close ?? item.pre_close ?? 0, + change_percent: item.change_percent ?? item.changePct ?? 0, + })); + }, +}; + +export default stockDetailService; diff --git a/MeAgent/src/services/watchlistService.js b/MeAgent/src/services/watchlistService.js new file mode 100644 index 00000000..28fcfe03 --- /dev/null +++ b/MeAgent/src/services/watchlistService.js @@ -0,0 +1,184 @@ +/** + * 自选股服务层 + * 管理用户的自选股和自选事件 + */ + +import { apiRequest } from './api'; + +/** + * 自选股服务 + */ +export const watchlistService = { + // ============ 自选股相关 ============ + + /** + * 获取用户自选股列表 + * @returns {Promise} { success: true, data: [{ id, stock_code, stock_name, created_at }] } + */ + getWatchlist: async () => { + try { + console.log('[WatchlistService] 获取自选股列表'); + const response = await apiRequest('/api/account/watchlist'); + + if (response.success) { + console.log('[WatchlistService] 自选股数量:', response.data?.length || 0); + } + + return response; + } catch (error) { + console.error('[WatchlistService] getWatchlist 错误:', error); + return { success: false, error: error.message }; + } + }, + + /** + * 添加股票到自选股 + * @param {string} stockCode - 股票代码 + * @param {string} stockName - 股票名称(可选) + * @returns {Promise} { success: true, data: { id } } + */ + addToWatchlist: async (stockCode, stockName = '') => { + try { + console.log('[WatchlistService] 添加自选股:', stockCode, stockName); + + const response = await apiRequest('/api/account/watchlist', { + method: 'POST', + body: JSON.stringify({ + stock_code: stockCode, + stock_name: stockName, + }), + }); + + if (response.success) { + console.log('[WatchlistService] 添加成功:', response.data); + } + + return response; + } catch (error) { + console.error('[WatchlistService] addToWatchlist 错误:', error); + return { success: false, error: error.message }; + } + }, + + /** + * 从自选股移除股票 + * @param {string} stockCode - 股票代码 + * @returns {Promise} { success: true } + */ + removeFromWatchlist: async (stockCode) => { + try { + console.log('[WatchlistService] 移除自选股:', stockCode); + + const response = await apiRequest(`/api/account/watchlist/${stockCode}`, { + method: 'DELETE', + }); + + if (response.success) { + console.log('[WatchlistService] 移除成功'); + } + + return response; + } catch (error) { + console.error('[WatchlistService] removeFromWatchlist 错误:', error); + return { success: false, error: error.message }; + } + }, + + /** + * 获取自选股实时行情 + * @returns {Promise} + * { success: true, data: [{ stock_code, stock_name, current_price, change_percent, ... }] } + */ + getWatchlistRealtime: async () => { + try { + console.log('[WatchlistService] 获取自选股实时行情'); + + const response = await apiRequest('/api/account/watchlist/realtime'); + + if (response.success) { + console.log('[WatchlistService] 实时行情数量:', response.data?.length || 0); + } + + return response; + } catch (error) { + console.error('[WatchlistService] getWatchlistRealtime 错误:', error); + return { success: false, error: error.message }; + } + }, + + /** + * 检查股票是否在自选中 + * @param {string} stockCode - 股票代码 + * @param {Array} watchlist - 自选股列表 + * @returns {boolean} + */ + isInWatchlist: (stockCode, watchlist = []) => { + // 标准化股票代码(提取6位数字) + const normalizeCode = (code) => { + const match = String(code).match(/\d{6}/); + return match ? match[0] : code; + }; + + const normalizedCode = normalizeCode(stockCode); + return watchlist.some(item => normalizeCode(item.stock_code) === normalizedCode); + }, + + // ============ 自选事件相关 ============ + + /** + * 获取用户关注的事件列表 + * @returns {Promise} { success: true, data: [...events] } + */ + getFollowingEvents: async () => { + try { + console.log('[WatchlistService] 获取关注事件列表'); + + const response = await apiRequest('/api/account/events/following'); + + if (response.success) { + console.log('[WatchlistService] 关注事件数量:', response.data?.length || 0); + } + + return response; + } catch (error) { + console.error('[WatchlistService] getFollowingEvents 错误:', error); + return { success: false, error: error.message }; + } + }, + + /** + * 切换事件关注状态 + * @param {number} eventId - 事件ID + * @returns {Promise} { success: true, data: { is_following, follower_count } } + */ + toggleEventFollow: async (eventId) => { + try { + console.log('[WatchlistService] 切换事件关注:', eventId); + + const response = await apiRequest(`/api/events/${eventId}/follow`, { + method: 'POST', + }); + + if (response.success) { + console.log('[WatchlistService] 关注状态:', response.data); + } + + return response; + } catch (error) { + console.error('[WatchlistService] toggleEventFollow 错误:', error); + return { success: false, error: error.message }; + } + }, + + /** + * 检查事件是否被关注 + * @param {number} eventId - 事件ID + * @param {Array} followingEvents - 关注的事件列表 + * @returns {boolean} + */ + isEventFollowed: (eventId, followingEvents = []) => { + return followingEvents.some(event => event.id === eventId); + }, +}; + +export default watchlistService; diff --git a/MeAgent/src/services/websocketService.js b/MeAgent/src/services/websocketService.js new file mode 100644 index 00000000..0c8e8cf7 --- /dev/null +++ b/MeAgent/src/services/websocketService.js @@ -0,0 +1,556 @@ +/** + * WebSocket 实时行情服务 + * 支持上交所(SSE)和深交所(SZSE)双通道 + */ + +import { AppState } from 'react-native'; + +// WebSocket 服务器地址 +const WS_ENDPOINTS = { + sse: 'wss://api.valuefrontier.cn/ws/sse', // 上交所 + szse: 'wss://api.valuefrontier.cn/ws/szse', // 深交所 +}; + +// 心跳间隔 (毫秒) +const HEARTBEAT_INTERVAL = 30000; + +// 重连延迟 (毫秒) +const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000]; + +// 连接状态 +const ConnectionState = { + DISCONNECTED: 'disconnected', + CONNECTING: 'connecting', + CONNECTED: 'connected', + RECONNECTING: 'reconnecting', +}; + +/** + * WebSocket 连接管理器 + */ +class WebSocketManager { + constructor(exchange) { + this.exchange = exchange; + this.url = WS_ENDPOINTS[exchange]; + this.ws = null; + this.state = ConnectionState.DISCONNECTED; + this.reconnectAttempts = 0; + this.heartbeatTimer = null; + this.reconnectTimer = null; + this.subscriptions = new Set(); + this.messageHandlers = new Set(); + this.stateHandlers = new Set(); + this.lastPong = null; + } + + /** + * 连接 WebSocket + */ + connect() { + if (this.state === ConnectionState.CONNECTING || this.state === ConnectionState.CONNECTED) { + return; + } + + this.state = ConnectionState.CONNECTING; + this._notifyStateChange(); + + try { + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + console.log(`[WS-${this.exchange}] 连接成功`); + this.state = ConnectionState.CONNECTED; + this.reconnectAttempts = 0; + this._notifyStateChange(); + this._startHeartbeat(); + this._resubscribe(); + }; + + this.ws.onmessage = (event) => { + this._handleMessage(event.data); + }; + + this.ws.onerror = (error) => { + console.error(`[WS-${this.exchange}] 错误:`, error.message); + }; + + this.ws.onclose = (event) => { + console.log(`[WS-${this.exchange}] 连接关闭:`, event.code, event.reason); + this._stopHeartbeat(); + this.ws = null; + + if (this.state !== ConnectionState.DISCONNECTED) { + this.state = ConnectionState.RECONNECTING; + this._notifyStateChange(); + this._scheduleReconnect(); + } + }; + } catch (error) { + console.error(`[WS-${this.exchange}] 创建连接失败:`, error); + this._scheduleReconnect(); + } + } + + /** + * 断开连接 + */ + disconnect() { + this.state = ConnectionState.DISCONNECTED; + this._stopHeartbeat(); + this._cancelReconnect(); + + if (this.ws) { + this.ws.close(1000, 'Client disconnect'); + this.ws = null; + } + + this._notifyStateChange(); + } + + /** + * 订阅股票行情 + * @param {string[]} codes - 股票代码列表 + */ + subscribe(codes) { + if (!Array.isArray(codes)) { + codes = [codes]; + } + + codes.forEach(code => this.subscriptions.add(code)); + + if (this.state === ConnectionState.CONNECTED && this.ws) { + this._sendSubscribe(codes); + } + } + + /** + * 取消订阅 + * @param {string[]} codes - 股票代码列表 + */ + unsubscribe(codes) { + if (!Array.isArray(codes)) { + codes = [codes]; + } + + codes.forEach(code => this.subscriptions.delete(code)); + + if (this.state === ConnectionState.CONNECTED && this.ws) { + this._sendUnsubscribe(codes); + } + } + + /** + * 添加消息处理器 + * @param {function} handler - 消息处理函数 + */ + addMessageHandler(handler) { + this.messageHandlers.add(handler); + return () => this.messageHandlers.delete(handler); + } + + /** + * 添加状态变化处理器 + * @param {function} handler - 状态处理函数 + */ + addStateHandler(handler) { + this.stateHandlers.add(handler); + return () => this.stateHandlers.delete(handler); + } + + /** + * 获取当前连接状态 + */ + getState() { + return this.state; + } + + /** + * 是否已连接 + */ + isConnected() { + return this.state === ConnectionState.CONNECTED; + } + + // ============ 私有方法 ============ + + _sendSubscribe(codes) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; + + const message = JSON.stringify({ + action: 'subscribe', + codes: codes, + }); + this.ws.send(message); + console.log(`[WS-${this.exchange}] 订阅:`, codes); + } + + _sendUnsubscribe(codes) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return; + + const message = JSON.stringify({ + action: 'unsubscribe', + codes: codes, + }); + this.ws.send(message); + console.log(`[WS-${this.exchange}] 取消订阅:`, codes); + } + + _resubscribe() { + if (this.subscriptions.size > 0) { + const codes = Array.from(this.subscriptions); + this._sendSubscribe(codes); + } + } + + _handleMessage(data) { + try { + // 处理 pong 响应 + if (data === 'pong') { + this.lastPong = Date.now(); + return; + } + + const message = JSON.parse(data); + + // 通知所有处理器 + this.messageHandlers.forEach(handler => { + try { + handler(message, this.exchange); + } catch (error) { + console.error(`[WS-${this.exchange}] 消息处理器错误:`, error); + } + }); + } catch (error) { + console.error(`[WS-${this.exchange}] 解析消息失败:`, error); + } + } + + _startHeartbeat() { + this._stopHeartbeat(); + this.heartbeatTimer = setInterval(() => { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send('ping'); + } + }, HEARTBEAT_INTERVAL); + } + + _stopHeartbeat() { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + } + + _scheduleReconnect() { + this._cancelReconnect(); + + const delay = RECONNECT_DELAYS[ + Math.min(this.reconnectAttempts, RECONNECT_DELAYS.length - 1) + ]; + + console.log(`[WS-${this.exchange}] ${delay}ms 后重连 (第${this.reconnectAttempts + 1}次)`); + + this.reconnectTimer = setTimeout(() => { + this.reconnectAttempts++; + this.connect(); + }, delay); + } + + _cancelReconnect() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + } + + _notifyStateChange() { + this.stateHandlers.forEach(handler => { + try { + handler(this.state, this.exchange); + } catch (error) { + console.error(`[WS-${this.exchange}] 状态处理器错误:`, error); + } + }); + } +} + +/** + * 统一的 WebSocket 行情服务 + * 自动管理上交所和深交所双通道 + */ +class RealtimeQuoteService { + constructor() { + this.managers = { + sse: new WebSocketManager('sse'), + szse: new WebSocketManager('szse'), + }; + this.quoteHandlers = new Set(); + this.stateHandlers = new Set(); + this.appStateSubscription = null; + this._initialized = false; + } + + /** + * 初始化服务 + */ + initialize() { + if (this._initialized) return; + this._initialized = true; + + // 监听 App 前后台切换 + this.appStateSubscription = AppState.addEventListener('change', (nextAppState) => { + if (nextAppState === 'active') { + console.log('[RealtimeQuote] App 进入前台,重连 WebSocket'); + this.connect(); + } else if (nextAppState === 'background') { + console.log('[RealtimeQuote] App 进入后台,断开 WebSocket'); + this.disconnect(); + } + }); + + // 设置消息处理器 + Object.values(this.managers).forEach(manager => { + manager.addMessageHandler((message, exchange) => { + this._handleQuoteMessage(message, exchange); + }); + + manager.addStateHandler((state, exchange) => { + this._notifyStateChange(); + }); + }); + } + + /** + * 连接所有通道 + */ + connect() { + this.initialize(); + Object.values(this.managers).forEach(manager => manager.connect()); + } + + /** + * 断开所有通道 + */ + disconnect() { + Object.values(this.managers).forEach(manager => manager.disconnect()); + } + + /** + * 订阅股票行情 + * @param {string[]} codes - 股票代码列表 + */ + subscribe(codes) { + if (!Array.isArray(codes)) { + codes = [codes]; + } + + // 按交易所分类 + const sseCodes = []; + const szseCodes = []; + + codes.forEach(code => { + const exchange = this._getExchange(code); + if (exchange === 'sse') { + sseCodes.push(code); + } else { + szseCodes.push(code); + } + }); + + if (sseCodes.length > 0) { + this.managers.sse.subscribe(sseCodes); + } + if (szseCodes.length > 0) { + this.managers.szse.subscribe(szseCodes); + } + } + + /** + * 取消订阅 + * @param {string[]} codes - 股票代码列表 + */ + unsubscribe(codes) { + if (!Array.isArray(codes)) { + codes = [codes]; + } + + const sseCodes = []; + const szseCodes = []; + + codes.forEach(code => { + const exchange = this._getExchange(code); + if (exchange === 'sse') { + sseCodes.push(code); + } else { + szseCodes.push(code); + } + }); + + if (sseCodes.length > 0) { + this.managers.sse.unsubscribe(sseCodes); + } + if (szseCodes.length > 0) { + this.managers.szse.unsubscribe(szseCodes); + } + } + + /** + * 添加行情数据处理器 + * @param {function} handler - 处理函数 (quotes) => void + * @returns {function} 取消订阅函数 + */ + addQuoteHandler(handler) { + this.quoteHandlers.add(handler); + return () => this.quoteHandlers.delete(handler); + } + + /** + * 添加连接状态处理器 + * @param {function} handler - 处理函数 (state) => void + * @returns {function} 取消订阅函数 + */ + addStateHandler(handler) { + this.stateHandlers.add(handler); + return () => this.stateHandlers.delete(handler); + } + + /** + * 获取连接状态 + */ + getConnectionState() { + const sseState = this.managers.sse.getState(); + const szseState = this.managers.szse.getState(); + + if (sseState === ConnectionState.CONNECTED && szseState === ConnectionState.CONNECTED) { + return 'connected'; + } + if (sseState === ConnectionState.CONNECTING || szseState === ConnectionState.CONNECTING) { + return 'connecting'; + } + if (sseState === ConnectionState.RECONNECTING || szseState === ConnectionState.RECONNECTING) { + return 'reconnecting'; + } + return 'disconnected'; + } + + /** + * 是否已连接 + */ + isConnected() { + return this.managers.sse.isConnected() || this.managers.szse.isConnected(); + } + + /** + * 销毁服务 + */ + destroy() { + this.disconnect(); + if (this.appStateSubscription) { + this.appStateSubscription.remove(); + this.appStateSubscription = null; + } + this.quoteHandlers.clear(); + this.stateHandlers.clear(); + this._initialized = false; + } + + // ============ 私有方法 ============ + + /** + * 根据股票代码判断交易所 + */ + _getExchange(code) { + // 提取纯数字代码 + const numericCode = String(code).replace(/\D/g, ''); + + // 上交所: 6开头 + // 深交所: 0、3开头 + if (numericCode.startsWith('6')) { + return 'sse'; + } + return 'szse'; + } + + /** + * 处理行情消息 + */ + _handleQuoteMessage(message, exchange) { + // 消息格式转换 + let quotes = {}; + + if (message.type === 'quote' && message.data) { + // 单条行情 + const data = message.data; + const code = data.stock_code || data.code; + if (code) { + quotes[code] = this._normalizeQuote(data); + } + } else if (message.type === 'quotes' && Array.isArray(message.data)) { + // 批量行情 + message.data.forEach(item => { + const code = item.stock_code || item.code; + if (code) { + quotes[code] = this._normalizeQuote(item); + } + }); + } else if (message.stock_code || message.code) { + // 直接是行情数据 + const code = message.stock_code || message.code; + quotes[code] = this._normalizeQuote(message); + } + + // 通知处理器 + if (Object.keys(quotes).length > 0) { + this.quoteHandlers.forEach(handler => { + try { + handler(quotes); + } catch (error) { + console.error('[RealtimeQuote] 处理器错误:', error); + } + }); + } + } + + /** + * 标准化行情数据 + */ + _normalizeQuote(data) { + return { + stock_code: data.stock_code || data.code, + stock_name: data.stock_name || data.name, + current_price: parseFloat(data.current_price || data.price || data.current || 0), + change_percent: parseFloat(data.change_percent || data.pct_chg || data.change_pct || 0), + change_amount: parseFloat(data.change_amount || data.change || 0), + volume: parseInt(data.volume || data.vol || 0, 10), + amount: parseFloat(data.amount || data.turnover || 0), + open: parseFloat(data.open || data.open_price || 0), + high: parseFloat(data.high || data.high_price || 0), + low: parseFloat(data.low || data.low_price || 0), + pre_close: parseFloat(data.pre_close || data.prev_close || 0), + bid_prices: data.bid_prices || data.bidPrices || [], + bid_volumes: data.bid_volumes || data.bidVolumes || [], + ask_prices: data.ask_prices || data.askPrices || [], + ask_volumes: data.ask_volumes || data.askVolumes || [], + update_time: data.update_time || data.time || new Date().toISOString(), + }; + } + + _notifyStateChange() { + const state = this.getConnectionState(); + this.stateHandlers.forEach(handler => { + try { + handler(state); + } catch (error) { + console.error('[RealtimeQuote] 状态处理器错误:', error); + } + }); + } +} + +// 导出单例 +export const realtimeQuoteService = new RealtimeQuoteService(); + +// 导出连接状态常量 +export { ConnectionState }; + +export default realtimeQuoteService; diff --git a/argon-pro-react-native/src/services/ztService.js b/MeAgent/src/services/ztService.js similarity index 100% rename from argon-pro-react-native/src/services/ztService.js rename to MeAgent/src/services/ztService.js diff --git a/argon-pro-react-native/src/store/index.js b/MeAgent/src/store/index.js similarity index 67% rename from argon-pro-react-native/src/store/index.js rename to MeAgent/src/store/index.js index 6516e53a..2554458b 100644 --- a/argon-pro-react-native/src/store/index.js +++ b/MeAgent/src/store/index.js @@ -4,10 +4,14 @@ import { configureStore } from '@reduxjs/toolkit'; import eventsReducer from './slices/eventsSlice'; +import watchlistReducer from './slices/watchlistSlice'; +import stockReducer from './slices/stockSlice'; const store = configureStore({ reducer: { events: eventsReducer, + watchlist: watchlistReducer, + stock: stockReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ diff --git a/argon-pro-react-native/src/store/slices/eventsSlice.js b/MeAgent/src/store/slices/eventsSlice.js similarity index 100% rename from argon-pro-react-native/src/store/slices/eventsSlice.js rename to MeAgent/src/store/slices/eventsSlice.js diff --git a/MeAgent/src/store/slices/stockSlice.js b/MeAgent/src/store/slices/stockSlice.js new file mode 100644 index 00000000..33f72ad4 --- /dev/null +++ b/MeAgent/src/store/slices/stockSlice.js @@ -0,0 +1,280 @@ +/** + * 股票详情状态管理 Slice + * 管理当前查看的股票详情、K线数据、盘口数据 + */ + +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { stockDetailService } from '../../services/stockService'; + +// 初始状态 +const initialState = { + // 当前查看的股票 + currentStock: null, + // 分时数据 + minuteData: [], + // 分时数据对应的昨收价 + minutePrevClose: 0, + // K线数据 + klineData: { + daily: [], + weekly: [], + monthly: [], + }, + // 当前图表类型 + chartType: 'minute', // minute | daily | weekly | monthly + // 5档盘口 + orderBook: { + bidPrices: [], + bidVolumes: [], + askPrices: [], + askVolumes: [], + }, + // 加载状态 + loading: { + stock: false, + minute: false, + kline: false, + }, + // 错误信息 + error: null, + // 最后更新时间 + lastUpdate: null, +}; + +/** + * 获取股票详情 + */ +export const fetchStockDetail = createAsyncThunk( + 'stock/fetchStockDetail', + async (stockCode, { rejectWithValue }) => { + try { + const response = await stockDetailService.getStockDetail(stockCode); + + if (response.success) { + return response.data; + } else { + return rejectWithValue(response.error || '获取股票详情失败'); + } + } catch (error) { + return rejectWithValue(error.message); + } + } +); + +/** + * 获取分时数据 + */ +export const fetchMinuteData = createAsyncThunk( + 'stock/fetchMinuteData', + async (stockCode, { rejectWithValue }) => { + try { + console.log('[stockSlice] fetchMinuteData 开始:', stockCode); + const response = await stockDetailService.getMinuteData(stockCode); + + console.log('[stockSlice] fetchMinuteData 响应:', { + success: response.success, + dataLength: response.data?.length || 0, + prevClose: response.prevClose, + firstItem: response.data?.[0], + }); + + if (response.success) { + return { + data: response.data || [], + prevClose: response.prevClose || 0, + }; + } else { + return rejectWithValue(response.error || '获取分时数据失败'); + } + } catch (error) { + console.error('[stockSlice] fetchMinuteData 错误:', error); + return rejectWithValue(error.message); + } + } +); + +/** + * 获取K线数据 + */ +export const fetchKlineData = createAsyncThunk( + 'stock/fetchKlineData', + async ({ stockCode, type = 'daily', eventTime = null }, { rejectWithValue }) => { + try { + console.log('[stockSlice] fetchKlineData 开始:', { stockCode, type, eventTime }); + const response = await stockDetailService.getKlineData(stockCode, type, eventTime); + + console.log('[stockSlice] fetchKlineData 响应:', { + success: response.success, + dataLength: response.data?.length || 0, + firstItem: response.data?.[0], + }); + + if (response.success) { + return { type, data: response.data || [] }; + } else { + return rejectWithValue(response.error || '获取K线数据失败'); + } + } catch (error) { + console.error('[stockSlice] fetchKlineData 错误:', error); + return rejectWithValue(error.message); + } + } +); + +/** + * 加载股票详情页全部数据 + */ +export const loadStockPage = createAsyncThunk( + 'stock/loadStockPage', + async (stockCode, { dispatch }) => { + // 并行加载股票详情和分时数据 + await Promise.all([ + dispatch(fetchStockDetail(stockCode)), + dispatch(fetchMinuteData(stockCode)), + ]); + return stockCode; + } +); + +// Slice 定义 +const stockSlice = createSlice({ + name: 'stock', + initialState, + reducers: { + // 清除当前股票数据 + clearCurrentStock: (state) => { + state.currentStock = null; + state.minuteData = []; + state.minutePrevClose = 0; + state.klineData = { daily: [], weekly: [], monthly: [] }; + state.orderBook = { bidPrices: [], bidVolumes: [], askPrices: [], askVolumes: [] }; + state.error = null; + }, + // 设置图表类型 + setChartType: (state, action) => { + state.chartType = action.payload; + }, + // 更新实时报价(用于 WebSocket) + updateStockQuote: (state, action) => { + const quote = action.payload; + if (state.currentStock) { + state.currentStock = { + ...state.currentStock, + ...quote, + }; + // 更新盘口数据 + if (quote.bidPrices) { + state.orderBook.bidPrices = quote.bidPrices; + state.orderBook.bidVolumes = quote.bidVolumes || []; + } + if (quote.askPrices) { + state.orderBook.askPrices = quote.askPrices; + state.orderBook.askVolumes = quote.askVolumes || []; + } + state.lastUpdate = new Date().toISOString(); + } + }, + // 追加分时数据点 + appendMinuteData: (state, action) => { + const newPoint = action.payload; + if (state.minuteData.length > 0) { + // 检查是否是同一分钟的数据(更新)还是新分钟(追加) + const lastPoint = state.minuteData[state.minuteData.length - 1]; + if (lastPoint.time === newPoint.time) { + state.minuteData[state.minuteData.length - 1] = newPoint; + } else { + state.minuteData.push(newPoint); + } + } else { + state.minuteData.push(newPoint); + } + }, + // 清除错误 + clearError: (state) => { + state.error = null; + }, + }, + extraReducers: (builder) => { + builder + // 获取股票详情 + .addCase(fetchStockDetail.pending, (state) => { + state.loading.stock = true; + state.error = null; + }) + .addCase(fetchStockDetail.fulfilled, (state, action) => { + state.loading.stock = false; + state.currentStock = action.payload; + // 更新盘口数据 + if (action.payload) { + const { bidPrices, bidVolumes, askPrices, askVolumes } = action.payload; + state.orderBook = { + bidPrices: bidPrices || [], + bidVolumes: bidVolumes || [], + askPrices: askPrices || [], + askVolumes: askVolumes || [], + }; + } + state.lastUpdate = new Date().toISOString(); + }) + .addCase(fetchStockDetail.rejected, (state, action) => { + state.loading.stock = false; + state.error = action.payload; + }) + // 获取分时数据 + .addCase(fetchMinuteData.pending, (state) => { + state.loading.minute = true; + }) + .addCase(fetchMinuteData.fulfilled, (state, action) => { + state.loading.minute = false; + state.minuteData = action.payload.data; + state.minutePrevClose = action.payload.prevClose; + }) + .addCase(fetchMinuteData.rejected, (state, action) => { + state.loading.minute = false; + state.error = action.payload; + }) + // 获取K线数据 + .addCase(fetchKlineData.pending, (state) => { + state.loading.kline = true; + }) + .addCase(fetchKlineData.fulfilled, (state, action) => { + state.loading.kline = false; + const { type, data } = action.payload; + state.klineData[type] = data; + }) + .addCase(fetchKlineData.rejected, (state, action) => { + state.loading.kline = false; + state.error = action.payload; + }); + }, +}); + +// 导出 actions +export const { + clearCurrentStock, + setChartType, + updateStockQuote, + appendMinuteData, + clearError, +} = stockSlice.actions; + +// Selectors +export const selectCurrentStock = (state) => state.stock.currentStock; +export const selectMinuteData = (state) => state.stock.minuteData; +export const selectMinutePrevClose = (state) => state.stock.minutePrevClose; +export const selectKlineData = (state) => state.stock.klineData; +export const selectChartType = (state) => state.stock.chartType; +export const selectOrderBook = (state) => state.stock.orderBook; +export const selectStockLoading = (state) => state.stock.loading; +export const selectStockError = (state) => state.stock.error; + +// 获取当前图表类型的K线数据 +export const selectCurrentKlineData = (state) => { + const { chartType, minuteData, klineData } = state.stock; + if (chartType === 'minute') { + return minuteData; + } + return klineData[chartType] || []; +}; + +export default stockSlice.reducer; diff --git a/MeAgent/src/store/slices/watchlistSlice.js b/MeAgent/src/store/slices/watchlistSlice.js new file mode 100644 index 00000000..ab8d53f2 --- /dev/null +++ b/MeAgent/src/store/slices/watchlistSlice.js @@ -0,0 +1,325 @@ +/** + * 自选股状态管理 Slice + * 管理用户的自选股和自选事件 + */ + +import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { watchlistService } from '../../services/watchlistService'; + +// 初始状态 +const initialState = { + // 自选股列表 + stocks: [], + // 自选事件列表 + events: [], + // 实时行情 { code: QuoteData } + realtimeQuotes: {}, + // 加载状态 + loading: { + stocks: false, + events: false, + realtime: false, + adding: false, + removing: false, + }, + // 错误信息 + error: null, + // 最后更新时间 + lastUpdate: null, +}; + +/** + * 获取自选股列表 + */ +export const fetchWatchlist = createAsyncThunk( + 'watchlist/fetchWatchlist', + async (_, { rejectWithValue }) => { + try { + const response = await watchlistService.getWatchlist(); + + if (response.success) { + return response.data || []; + } else { + return rejectWithValue(response.error || '获取自选股列表失败'); + } + } catch (error) { + return rejectWithValue(error.message); + } + } +); + +/** + * 获取自选股实时行情 + */ +export const fetchWatchlistRealtime = createAsyncThunk( + 'watchlist/fetchWatchlistRealtime', + async (_, { rejectWithValue }) => { + try { + const response = await watchlistService.getWatchlistRealtime(); + + if (response.success) { + return response.data || []; + } else { + return rejectWithValue(response.error || '获取实时行情失败'); + } + } catch (error) { + return rejectWithValue(error.message); + } + } +); + +/** + * 添加股票到自选股 + */ +export const addToWatchlist = createAsyncThunk( + 'watchlist/addToWatchlist', + async ({ stockCode, stockName }, { rejectWithValue, dispatch }) => { + try { + const response = await watchlistService.addToWatchlist(stockCode, stockName); + + if (response.success) { + // 添加成功后刷新列表 + dispatch(fetchWatchlist()); + return { stockCode, stockName, id: response.data?.id }; + } else { + return rejectWithValue(response.error || '添加自选股失败'); + } + } catch (error) { + return rejectWithValue(error.message); + } + } +); + +/** + * 从自选股移除股票 + */ +export const removeFromWatchlist = createAsyncThunk( + 'watchlist/removeFromWatchlist', + async (stockCode, { rejectWithValue }) => { + try { + const response = await watchlistService.removeFromWatchlist(stockCode); + + if (response.success) { + return stockCode; + } else { + return rejectWithValue(response.error || '移除自选股失败'); + } + } catch (error) { + return rejectWithValue(error.message); + } + } +); + +/** + * 获取关注的事件列表 + */ +export const fetchFollowingEvents = createAsyncThunk( + 'watchlist/fetchFollowingEvents', + async (_, { rejectWithValue }) => { + try { + const response = await watchlistService.getFollowingEvents(); + + if (response.success) { + return response.data || []; + } else { + return rejectWithValue(response.error || '获取关注事件列表失败'); + } + } catch (error) { + return rejectWithValue(error.message); + } + } +); + +/** + * 切换事件关注状态 + */ +export const toggleEventFollow = createAsyncThunk( + 'watchlist/toggleEventFollow', + async (eventId, { rejectWithValue, dispatch }) => { + try { + const response = await watchlistService.toggleEventFollow(eventId); + + if (response.success) { + // 刷新关注事件列表 + dispatch(fetchFollowingEvents()); + return { + eventId, + isFollowing: response.data?.is_following, + followerCount: response.data?.follower_count, + }; + } else { + return rejectWithValue(response.error || '操作失败'); + } + } catch (error) { + return rejectWithValue(error.message); + } + } +); + +// Slice 定义 +const watchlistSlice = createSlice({ + name: 'watchlist', + initialState, + reducers: { + // 清除错误 + clearError: (state) => { + state.error = null; + }, + // 更新实时行情(用于 WebSocket 推送) + updateRealtimeQuote: (state, action) => { + const { code, quote } = action.payload; + state.realtimeQuotes[code] = { + ...state.realtimeQuotes[code], + ...quote, + updateTime: new Date().toISOString(), + }; + }, + // 批量更新实时行情 + updateRealtimeQuotes: (state, action) => { + const quotes = action.payload; + Object.entries(quotes).forEach(([code, quote]) => { + state.realtimeQuotes[code] = { + ...state.realtimeQuotes[code], + ...quote, + updateTime: new Date().toISOString(), + }; + }); + state.lastUpdate = new Date().toISOString(); + }, + // 乐观更新 - 添加自选股 + optimisticAddStock: (state, action) => { + const { stockCode, stockName } = action.payload; + if (!state.stocks.find(s => s.stock_code === stockCode)) { + state.stocks.unshift({ + stock_code: stockCode, + stock_name: stockName, + created_at: new Date().toISOString(), + }); + } + }, + // 乐观更新 - 移除自选股 + optimisticRemoveStock: (state, action) => { + const stockCode = action.payload; + state.stocks = state.stocks.filter(s => { + const code1 = String(s.stock_code).match(/\d{6}/)?.[0]; + const code2 = String(stockCode).match(/\d{6}/)?.[0]; + return code1 !== code2; + }); + }, + }, + extraReducers: (builder) => { + builder + // 获取自选股列表 + .addCase(fetchWatchlist.pending, (state) => { + state.loading.stocks = true; + state.error = null; + }) + .addCase(fetchWatchlist.fulfilled, (state, action) => { + state.loading.stocks = false; + state.stocks = action.payload; + }) + .addCase(fetchWatchlist.rejected, (state, action) => { + state.loading.stocks = false; + state.error = action.payload; + }) + // 获取自选股实时行情 + .addCase(fetchWatchlistRealtime.pending, (state) => { + state.loading.realtime = true; + }) + .addCase(fetchWatchlistRealtime.fulfilled, (state, action) => { + state.loading.realtime = false; + // 将数组转换为对象 + const quotes = {}; + action.payload.forEach(item => { + quotes[item.stock_code] = item; + }); + state.realtimeQuotes = quotes; + state.lastUpdate = new Date().toISOString(); + }) + .addCase(fetchWatchlistRealtime.rejected, (state, action) => { + state.loading.realtime = false; + state.error = action.payload; + }) + // 添加自选股 + .addCase(addToWatchlist.pending, (state) => { + state.loading.adding = true; + }) + .addCase(addToWatchlist.fulfilled, (state) => { + state.loading.adding = false; + }) + .addCase(addToWatchlist.rejected, (state, action) => { + state.loading.adding = false; + state.error = action.payload; + }) + // 移除自选股 + .addCase(removeFromWatchlist.pending, (state) => { + state.loading.removing = true; + }) + .addCase(removeFromWatchlist.fulfilled, (state, action) => { + state.loading.removing = false; + // 从列表中移除 + const stockCode = action.payload; + state.stocks = state.stocks.filter(s => { + const code1 = String(s.stock_code).match(/\d{6}/)?.[0]; + const code2 = String(stockCode).match(/\d{6}/)?.[0]; + return code1 !== code2; + }); + }) + .addCase(removeFromWatchlist.rejected, (state, action) => { + state.loading.removing = false; + state.error = action.payload; + }) + // 获取关注事件列表 + .addCase(fetchFollowingEvents.pending, (state) => { + state.loading.events = true; + }) + .addCase(fetchFollowingEvents.fulfilled, (state, action) => { + state.loading.events = false; + state.events = action.payload; + }) + .addCase(fetchFollowingEvents.rejected, (state, action) => { + state.loading.events = false; + state.error = action.payload; + }) + // 切换事件关注 + .addCase(toggleEventFollow.fulfilled, (state, action) => { + const { eventId, isFollowing } = action.payload; + if (!isFollowing) { + // 取消关注时从列表移除 + state.events = state.events.filter(e => e.id !== eventId); + } + }); + }, +}); + +// 导出 actions +export const { + clearError, + updateRealtimeQuote, + updateRealtimeQuotes, + optimisticAddStock, + optimisticRemoveStock, +} = watchlistSlice.actions; + +// Selectors +export const selectWatchlistStocks = (state) => state.watchlist.stocks; +export const selectWatchlistEvents = (state) => state.watchlist.events; +export const selectRealtimeQuotes = (state) => state.watchlist.realtimeQuotes; +export const selectWatchlistLoading = (state) => state.watchlist.loading; +export const selectWatchlistError = (state) => state.watchlist.error; + +// 检查股票是否在自选中 +export const selectIsInWatchlist = (stockCode) => (state) => { + const normalizeCode = (code) => String(code).match(/\d{6}/)?.[0] || code; + const normalizedCode = normalizeCode(stockCode); + return state.watchlist.stocks.some( + item => normalizeCode(item.stock_code) === normalizedCode + ); +}; + +// 检查事件是否被关注 +export const selectIsEventFollowed = (eventId) => (state) => { + return state.watchlist.events.some(event => event.id === eventId); +}; + +export default watchlistSlice.reducer; diff --git a/argon-pro-react-native/src/theme/index.js b/MeAgent/src/theme/index.js similarity index 100% rename from argon-pro-react-native/src/theme/index.js rename to MeAgent/src/theme/index.js diff --git a/argon-pro-react-native/src/types/event.js b/MeAgent/src/types/event.js similarity index 100% rename from argon-pro-react-native/src/types/event.js rename to MeAgent/src/types/event.js diff --git a/argon-pro-react-native/src/utils/tradingDayUtils.js b/MeAgent/src/utils/tradingDayUtils.js similarity index 100% rename from argon-pro-react-native/src/utils/tradingDayUtils.js rename to MeAgent/src/utils/tradingDayUtils.js diff --git a/argon-pro-react-native/navigation/Menu.js b/argon-pro-react-native/navigation/Menu.js deleted file mode 100644 index 264ea381..00000000 --- a/argon-pro-react-native/navigation/Menu.js +++ /dev/null @@ -1,255 +0,0 @@ -import React from "react"; -import { - ScrollView, - StyleSheet, - Dimensions, - Image, - TouchableOpacity, - Linking, -} from "react-native"; -import { Block, Text, theme } from "galio-framework"; -import { useSafeArea } from "react-native-safe-area-context"; -import { Box, HStack, VStack, Icon, Pressable, Spinner } from "native-base"; -import { Ionicons } from "@expo/vector-icons"; -import { LinearGradient } from "expo-linear-gradient"; -import Images from "../constants/Images"; -import { DrawerItem as DrawerCustomItem } from "../components/index"; -import { useAuth } from "../src/contexts/AuthContext"; - -const { width } = Dimensions.get("screen"); - -// 金色主题色 -const GOLD_PRIMARY = '#D4AF37'; - -// 用户卡片组件 -const UserCard = ({ navigation }) => { - const { user, isLoggedIn, isLoading, subscription, logout } = useAuth(); - - const handleLoginPress = () => { - navigation.closeDrawer(); - // 使用 getParent 获取根导航器来导航到 Login - navigation.getParent()?.navigate("Login"); - }; - - const handleLogoutPress = async () => { - await logout(); - }; - - // 获取订阅显示文本 - const getSubscriptionText = () => { - if (!subscription || !subscription.is_active) { - return "免费用户"; - } - const typeMap = { pro: "Pro 会员", max: "Max 会员" }; - return typeMap[subscription.type] || "免费用户"; - }; - - if (isLoading) { - return ( - - - - - 加载中... - - - - ); - } - - if (!isLoggedIn) { - return ( - - - - - - - - - 登录/注册 - - - 登录解锁更多功能 - - - - - - - ); - } - - // 已登录状态 - return ( - - - - - {(user?.username || user?.nickname || "U").charAt(0).toUpperCase()} - - - - - {user?.nickname || user?.username || "用户"} - - - - - {getSubscriptionText()} - - - - - - - - - - ); -}; - -function CustomDrawerContent({ - drawerPosition, - navigation, - profile, - focused, - state, - ...rest -}) { - const insets = useSafeArea(); - const screens = [ - { title: "事件中心", navigateTo: "EventsDrawer" }, - { title: "市场热点", navigateTo: "MarketDrawer" }, - { title: "Home", navigateTo: "HomeDrawer" }, - { title: "Profile", navigateTo: "ProfileDrawer" }, - { title: "Account", navigateTo: "AccountDrawer" }, - { title: "Elements", navigateTo: "ElementsDrawer" }, - { title: "Articles", navigateTo: "ArticlesDrawer" }, - { title: "Settings", navigateTo: "SettingsDrawer" }, - ]; - return ( - - {/* 品牌头部 - 黑金主题 */} - - - - - - 价值前沿 - - - VALUE FRONTIER - - - - - - - {/* 用户卡片 */} - - - - - {screens.map((item, index) => { - return ( - - ); - })} - - - - DOCUMENTATION - - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - header: { - paddingHorizontal: 28, - paddingBottom: theme.SIZES.BASE, - paddingTop: theme.SIZES.BASE * 3, - justifyContent: "center", - }, -}); - -export default CustomDrawerContent;